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": {