diff --git a/html/src/localization/localizationHelperCLI.js b/html/src/localization/localizationHelperCLI.js
index cd6a22f6..f2708d54 100644
--- a/html/src/localization/localizationHelperCLI.js
+++ b/html/src/localization/localizationHelperCLI.js
@@ -7,7 +7,7 @@ const path = require('node:path');
const yargs = require("yargs/yargs");
const { hideBin } = require("yargs/helpers")
-function* GetLocalizationObjects() {
+const getLocalizationObjects = function* () {
const localeFolder = './src/localization';
const folders = fs.readdirSync(localeFolder, { withFileTypes: true }).filter(file => file.isDirectory());
for (const folder of folders) {
@@ -17,6 +17,23 @@ function* GetLocalizationObjects() {
}
}
+const addKey = function (obj, objects, value, above_key) {
+ console.log(`Adding key to ${obj.language} at path '${objects.join('.')}' with value '${value}' above key '${above_key}'`);
+
+ let currentObj = obj;
+ let i = 0;
+
+ // Last element is final key not object so loop n - 1 times
+ for (; i < objects.length - 1; i++) {
+ if (!Object.hasOwn(currentObj, objects[i])) {
+ currentObj[objects[i]] = {};
+ }
+
+ currentObj = currentObj[objects[i]];
+ }
+ InsertKeyInObj(currentObj, objects[i], value, above_key);
+}
+
// Shamelessly stolen from https://stackoverflow.com/a/55017155/11030436
const InsertKeyInObj = (obj, key, value, above_key) => {
const keys = Object.keys(obj);
@@ -43,55 +60,46 @@ const InsertKeyInObj = (obj, key, value, above_key) => {
return newObj;
}, {})
delete ret.dummy;
- return ret;
+
+ // Clear keys on old object
+ for (const key of keys) {
+ delete obj[key];
+ }
+
+ // Assign new properties to old object
+ Object.assign(obj, ret);
}
-const AddLocalizationKey = (key, value, above_key) => {
- // Use dummy key in case the user wants to add a key to the root json object
- // unlikely, but still a valid use case
- const objects = ['dummy', ...key.split('.')];
+const addLocalizationKey = (key, value, above_key) => {
+ const objects = key.split('.');
- for (const [localePath, localeObj] of GetLocalizationObjects()) {
- let dummy = { 'dummy': localeObj }
- let lastObj = dummy;
- let currentObj = dummy.dummy;
- // Have index start at one to skip the dummy key
- let i = 1;
-
- // Last element is final key not object so loop n - 1 times
- for (; i < objects.length - 1; i++) {
- if (!Object.hasOwn(currentObj, objects[i])) {
- currentObj[objects[i]] = {};
- }
-
- lastObj = currentObj;
- currentObj = currentObj[objects[i]];
- }
- lastObj[objects[i - 1]] = InsertKeyInObj(currentObj, objects[i], value, above_key);
- fs.writeFileSync(localePath, `${JSON.stringify(dummy.dummy, null, 4)}\n`);
+ for (const [localePath, localeObj] of getLocalizationObjects()) {
+ addKey(localeObj, objects, value, above_key);
+ fs.writeFileSync(localePath, `${JSON.stringify(localeObj, null, 4)}\n`);
}
console.log(`\`${key}:${value}\` added to every localization file!`);
}
-const RemoveLocalizationKey = (key) => {
- const removeKey = (obj, objects, i) => {
- if (!(Object.hasOwn(obj, objects[i]))) {
- return;
- }
-
- if (objects.length - 1 === i) {
- delete obj[objects[i]];
- } else {
- removeKey(obj[objects[i]], objects, i + 1);
- if (Object.keys(obj[objects[i]]).length === 0) {
- delete obj[objects[i]];
- }
- }
+const removeKey = (obj, objects, i = 0) => {
+ console.log(`Removing key from ${obj.language} at path '${objects.join('.')}'`);
+ if (!(Object.hasOwn(obj, objects[i]))) {
+ return;
}
+ if (objects.length - 1 === i) {
+ delete obj[objects[i]];
+ } else {
+ removeKey(obj[objects[i]], objects, i + 1);
+ if (Object.keys(obj[objects[i]]).length === 0) {
+ delete obj[objects[i]];
+ }
+ }
+}
+
+const removeLocalizationKey = (key) => {
const objects = key.split('.');
- for (const [localePath, localeObj] of GetLocalizationObjects()) {
+ for (const [localePath, localeObj] of getLocalizationObjects()) {
removeKey(localeObj, objects, 0);
// All the localization files seem to have a trailing new line, so add
@@ -102,18 +110,103 @@ const RemoveLocalizationKey = (key) => {
console.log(`\`${key}\` removed from every localization file!`);
}
+// Yes this code is extremely slow, but it doesn't run very often so.
+const Validate = function () {
+ const files = [...getLocalizationObjects()];
+ const enIndex = files.findIndex(file => path.dirname(file[0]).endsWith("en"));
+ const [_, enObj] = files.splice(enIndex, 1)[0];
+
+ const traverse = function (obj, predicate, pathes = []) {
+ for (const key in obj) {
+ if (typeof obj[key] === 'string' || obj[key] instanceof String) {
+ predicate(obj, key, pathes);
+ } else {
+ traverse(obj[key], predicate, [...pathes, key]);
+ }
+ }
+ }
+
+ let hasRemoved = false;
+ for (const [_, localeObj] of files) {
+ toRemove = []
+ traverse(localeObj, (_, key, pathes) => {
+ let currObj = enObj;
+ for (const pathSegment of pathes) {
+ if (Object.hasOwn(currObj, pathSegment)) {
+ currObj = currObj[pathSegment]
+ } else {
+ toRemove.push([...pathes, key]);
+ return;
+ }
+ }
+ if (!Object.hasOwn(currObj, key)) {
+ toRemove.push([...pathes, key]);
+ }
+ });
+
+ // Remove after traversal finishes to not modify while iterating
+ for (const toRemovePath of toRemove) {
+ removeKey(localeObj, toRemovePath);
+ hasRemoved = true;
+ }
+ }
+
+ toAdd = []
+ traverse(enObj, (obj, key, pathes) => {
+ // Add above_key to the toAdd entry
+ if (toAdd.length > 0 && typeof toAdd.at(-1)[3] === 'undefined' && toAdd.at(-1)[1].at(-2) === pathes.at(-1)) {
+ toAdd.at(-1)[3] = key;
+ }
+
+ for (const [_, localeObj] of files) {
+ let currObj = localeObj;
+ for (const pathSegment of pathes) {
+ if (Object.hasOwn(currObj, pathSegment)) {
+ currObj = currObj[pathSegment];
+ } else {
+ toAdd.push([localeObj, [...pathes, key], obj[key], undefined]);
+ return;
+ }
+ }
+
+ if (!Object.hasOwn(currObj, key)) {
+ toAdd.push([localeObj, [...pathes, key], obj[key], undefined]);
+ }
+ }
+ });
+
+ for (const addObj of toAdd) {
+ addKey(...addObj);
+ }
+
+ if (toAdd.length > 0 || hasRemoved) {
+ for (const [localePath, localeObj] of files) {
+ fs.writeFileSync(localePath, `${JSON.stringify(localeObj, null, 4)}\n`);
+ }
+ } else {
+ console.log("validation passed!");
+ }
+
+}
+
const cliParser = yargs(hideBin(process.argv))
.command({
command: 'add [above_key]',
aliases: ['a', 'replace', 'r'],
desc: 'adds or replaces a key and value to all localization files above `above_key`',
- handler: (argv) => AddLocalizationKey(argv.key, argv.value, argv.above_key)
+ handler: (argv) => addLocalizationKey(argv.key, argv.value, argv.above_key)
})
.command({
command: 'remove ',
aliases: ['rm', 'r'],
desc: 'removes key from all localization files',
- handler: (argv) => RemoveLocalizationKey(argv.key)
+ handler: (argv) => removeLocalizationKey(argv.key)
+ })
+ .command({
+ command: 'validate',
+ aliases: [],
+ desc: 'removes keys from other languages that don\'t exist in the en translation and adds keys that don\'t exist in other languages',
+ handler: Validate
})
.demandCommand(1)
.example([
diff --git a/html/src/localization/zh-CN/en.json b/html/src/localization/zh-CN/en.json
index e4651d8a..6b06e74e 100644
--- a/html/src/localization/zh-CN/en.json
+++ b/html/src/localization/zh-CN/en.json
@@ -574,6 +574,7 @@
"request_invite": "申请加入",
"request_invite_with_message": "发送带消息的加入申请",
"invite_to_group": "邀请加入群组",
+ "send_boop": "Send Boop",
"manage_gallery_icon": "管理相册 / 图标",
"accept_friend_request": "接受好友申请",
"decline_friend_request": "拒绝好友申请",
@@ -630,6 +631,7 @@
"friended": "添加为好友的时间",
"unfriended": "解除好友的时间",
"avatar_cloning": "是否允许克隆模型",
+ "booping": "Booping",
"avatar_cloning_allow": "允许",
"avatar_cloning_deny": "不允许",
"home_location": "出生点",
@@ -1384,6 +1386,15 @@
"cancel": "取消",
"create_post": "创建帖子",
"edit_post": "编辑帖子"
+ },
+ "boop_dialog": {
+ "header": "Boop",
+ "emoji_manager": "Emoji Manager",
+ "select_emoji": "Select Emoji",
+ "my_emojis": "My Emojis",
+ "default_emojis": "Default Emojis",
+ "cancel": "Cancel",
+ "send": "Send"
}
},
"prompt": {