sync-settings-keys.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. /*
  2. * Synchronize keys of settings.json across locales using zh-CN as canonical.
  3. * - Ensures every locale has exactly the same set of nested keys
  4. * - Keeps existing translations where keys exist
  5. * - Fills missing keys with zh-CN text as placeholder
  6. * - Drops extra keys not present in zh-CN (notably for en, but applies consistently)
  7. */
  8. const fs = require("fs");
  9. const path = require("path");
  10. const ROOT = process.cwd();
  11. const MESSAGES_DIR = path.join(ROOT, "messages");
  12. const LOCALES = ["en", "ja", "ru", "zh-TW"];
  13. const CANONICAL = "zh-CN";
  14. function isObject(v) {
  15. return v && typeof v === "object" && !Array.isArray(v);
  16. }
  17. function flatten(obj, prefix = "") {
  18. const out = {};
  19. for (const [k, v] of Object.entries(obj || {})) {
  20. const key = prefix ? `${prefix}.${k}` : k;
  21. if (isObject(v)) Object.assign(out, flatten(v, key));
  22. else out[key] = v;
  23. }
  24. return out;
  25. }
  26. function mergeWithCanonical(cn, target) {
  27. const result = Array.isArray(cn) ? [] : {};
  28. for (const [k, v] of Object.entries(cn)) {
  29. const tVal = target?.[k];
  30. if (isObject(v)) {
  31. // Canonical expects an object; only descend if target also has an object, else ignore target
  32. const tchild = isObject(tVal) ? tVal : {};
  33. result[k] = mergeWithCanonical(v, tchild);
  34. } else {
  35. // Canonical expects a leaf (string/number/bool/array/null). If target is an object, ignore it.
  36. if (Object.prototype.hasOwnProperty.call(target || {}, k) && !isObject(tVal)) {
  37. result[k] = tVal;
  38. } else {
  39. result[k] = v;
  40. }
  41. }
  42. }
  43. return result;
  44. }
  45. function sortKeysDeep(obj) {
  46. if (!isObject(obj)) return obj;
  47. const out = {};
  48. for (const key of Object.keys(obj).sort()) {
  49. out[key] = sortKeysDeep(obj[key]);
  50. }
  51. return out;
  52. }
  53. function loadJSON(p) {
  54. return JSON.parse(fs.readFileSync(p, "utf8"));
  55. }
  56. function saveJSON(p, data) {
  57. fs.writeFileSync(p, JSON.stringify(data, null, 2) + "\n", "utf8");
  58. }
  59. function ensureSettings(locale) {
  60. const cnPath = path.join(MESSAGES_DIR, CANONICAL, "settings.json");
  61. const targetPath = path.join(MESSAGES_DIR, locale, "settings.json");
  62. const cn = loadJSON(cnPath);
  63. const t = loadJSON(targetPath);
  64. const merged = mergeWithCanonical(cn, t);
  65. // Drop extras implicitly by not copying unknown keys; merged contains only canonical keys
  66. const sorted = sortKeysDeep(merged);
  67. // Stats
  68. const cnKeys = Object.keys(flatten(cn));
  69. const tKeys = Object.keys(flatten(t));
  70. const mergedKeys = Object.keys(flatten(sorted));
  71. const missingBefore = cnKeys.filter((k) => !tKeys.includes(k));
  72. const extraBefore = tKeys.filter((k) => !cnKeys.includes(k));
  73. const missingAfter = cnKeys.filter((k) => !mergedKeys.includes(k));
  74. const extraAfter = mergedKeys.filter((k) => !cnKeys.includes(k));
  75. saveJSON(targetPath, sorted);
  76. return {
  77. locale,
  78. targetPath,
  79. cnCount: cnKeys.length,
  80. before: { count: tKeys.length, missing: missingBefore.length, extra: extraBefore.length },
  81. after: { count: mergedKeys.length, missing: missingAfter.length, extra: extraAfter.length },
  82. };
  83. }
  84. function main() {
  85. const reports = [];
  86. for (const loc of LOCALES) {
  87. const p = path.join(MESSAGES_DIR, loc, "settings.json");
  88. if (!fs.existsSync(p)) {
  89. console.error(`[skip] ${loc} has no settings.json`);
  90. continue;
  91. }
  92. reports.push(ensureSettings(loc));
  93. }
  94. // Print summary
  95. for (const r of reports) {
  96. console.log(
  97. `${r.locale}: cn=${r.cnCount}, before=${r.before.count} (-${r.before.missing} missing, +${r.before.extra} extra), after=${r.after.count} (-${r.after.missing} missing, +${r.after.extra} extra)`
  98. );
  99. }
  100. }
  101. if (require.main === module) {
  102. main();
  103. }