From af5fbb52822e73b2d163909483f1d2bfc33e52d4 Mon Sep 17 00:00:00 2001 From: Tyke Chen <190473011+chentyke@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:21:31 +0800 Subject: [PATCH] test: add i18n duplicate key check --- main/manager-web/package.json | 2 + main/manager-web/scripts/check-i18n.js | 96 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 main/manager-web/scripts/check-i18n.js diff --git a/main/manager-web/package.json b/main/manager-web/package.json index e9f0d65c..a059bdd9 100644 --- a/main/manager-web/package.json +++ b/main/manager-web/package.json @@ -4,6 +4,8 @@ "private": true, "scripts": { "serve": "vue-cli-service serve", + "test": "npm run test:i18n", + "test:i18n": "node scripts/check-i18n.js", "build": "vue-cli-service build", "analyze": "cross-env ANALYZE=true vue-cli-service build" }, diff --git a/main/manager-web/scripts/check-i18n.js b/main/manager-web/scripts/check-i18n.js new file mode 100644 index 00000000..14c82809 --- /dev/null +++ b/main/manager-web/scripts/check-i18n.js @@ -0,0 +1,96 @@ +const fs = require('fs'); +const path = require('path'); + +const rootDir = path.resolve(__dirname, '..'); +const locales = [ + { name: 'zh_CN', file: 'src/i18n/zh_CN.js' }, + { name: 'zh_TW', file: 'src/i18n/zh_TW.js' }, + { name: 'en', file: 'src/i18n/en.js' }, + { name: 'de', file: 'src/i18n/de.js' }, + { name: 'vi', file: 'src/i18n/vi.js' }, + { name: 'pt_BR', file: 'src/i18n/pt_BR.js' }, +]; + +function readLocale(locale) { + const filePath = path.join(rootDir, locale.file); + const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/); + const keys = []; + const seen = new Map(); + const duplicates = []; + + lines.forEach((line, index) => { + const match = line.match(/^\s*['"]([^'"]+)['"]\s*:/); + if (!match) { + return; + } + + const key = match[1]; + keys.push(key); + if (!seen.has(key)) { + seen.set(key, []); + } + seen.get(key).push(index + 1); + }); + + seen.forEach((lineNumbers, key) => { + if (lineNumbers.length > 1) { + duplicates.push({ key, lineNumbers }); + } + }); + + return { + ...locale, + filePath, + keys, + keySet: new Set(keys), + duplicates, + }; +} + +function diffKeys(left, right) { + return [...left].filter((key) => !right.has(key)).sort(); +} + +function formatKeyList(keys) { + return keys.length ? keys.map((key) => ` - ${key}`).join('\n') : ''; +} + +function main() { + const results = locales.map(readLocale); + const base = results.find((locale) => locale.name === 'zh_CN'); + const errors = []; + + results.forEach((locale) => { + locale.duplicates.forEach(({ key, lineNumbers }) => { + errors.push( + `${locale.file} has duplicate key "${key}" on lines ${lineNumbers.join(', ')}` + ); + }); + }); + + results + .filter((locale) => locale !== base) + .forEach((locale) => { + const missing = diffKeys(base.keySet, locale.keySet); + const extra = diffKeys(locale.keySet, base.keySet); + + if (missing.length) { + errors.push(`${locale.file} is missing keys from ${base.file}:\n${formatKeyList(missing)}`); + } + + if (extra.length) { + errors.push(`${locale.file} has keys that do not exist in ${base.file}:\n${formatKeyList(extra)}`); + } + }); + + if (errors.length) { + console.error(`i18n check failed with ${errors.length} issue(s):`); + console.error(errors.join('\n\n')); + process.exit(1); + } + + const keyCount = base.keySet.size; + console.log(`i18n check passed: ${results.length} locale files, ${keyCount} keys each.`); +} + +main();