dump-untranslated.mjs 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. // Dump every (locale, dottedKey, enValue) tuple that the parity check would
  2. // flag as "identical to en, not in allow-list, not auto-allowed". Output is
  3. // JSON, structured so apply-translations.mjs can consume the same shape after
  4. // translations are filled in.
  5. //
  6. // Usage: node scripts/dump-untranslated.mjs > /tmp/untranslated.json
  7. //
  8. // Logic is imported from check-i18n-parity.mjs (compareLocales) so this stays
  9. // in lockstep with the gate.
  10. import fs from 'node:fs';
  11. import path from 'node:path';
  12. import process from 'node:process';
  13. import url from 'node:url';
  14. const scriptDir = path.dirname(url.fileURLToPath(import.meta.url));
  15. const localesDir = path.resolve(scriptDir, '../src/i18n/locales');
  16. const tsPath = path.resolve(scriptDir, '../node_modules/typescript/lib/typescript.js');
  17. const tsModule = await import(url.pathToFileURL(tsPath).href);
  18. const ts = tsModule.default ?? tsModule;
  19. const { compareLocales } = await import(url.pathToFileURL(path.join(scriptDir, 'check-i18n-parity.mjs')).href);
  20. function collectLeaves(node, prefix, leaves) {
  21. if (!ts.isObjectLiteralExpression(node)) return;
  22. for (const prop of node.properties) {
  23. if (!ts.isPropertyAssignment(prop)) continue;
  24. let name;
  25. if (ts.isIdentifier(prop.name)) name = prop.name.text;
  26. else if (ts.isStringLiteral(prop.name)) name = prop.name.text;
  27. else continue;
  28. const p = prefix ? `${prefix}.${name}` : name;
  29. if (ts.isObjectLiteralExpression(prop.initializer)) {
  30. collectLeaves(prop.initializer, p, leaves);
  31. } else if (ts.isStringLiteral(prop.initializer) || ts.isNoSubstitutionTemplateLiteral(prop.initializer)) {
  32. leaves.set(p, prop.initializer.text);
  33. }
  34. }
  35. }
  36. function loadLocale(filePath) {
  37. const sf = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest, true);
  38. const leaves = new Map();
  39. ts.forEachChild(sf, (n) => { if (ts.isExportAssignment(n)) collectLeaves(n.expression, '', leaves); });
  40. return leaves;
  41. }
  42. const codes = ['en', 'de', 'fr', 'it', 'ja', 'pt-BR', 'zh-CN', 'zh-TW'];
  43. const locales = Object.fromEntries(codes.map((c) => [c, loadLocale(path.join(localesDir, `${c}.ts`))]));
  44. const { reports } = compareLocales(locales);
  45. // Reports look like: { label: 'de: leaves identical to en (untranslated?)', items: ['<key>: "<value>"', ...] }
  46. // We need to reverse-engineer the (locale, key, enValue) tuples — easier to
  47. // just walk en and check each locale ourselves with the live ALLOWED, which
  48. // is what compareLocales does anyway. So mirror that here.
  49. const en = locales.en;
  50. const out = {};
  51. for (const code of codes) {
  52. if (code === 'en') continue;
  53. const map = locales[code];
  54. const flagged = {};
  55. for (const r of reports) {
  56. if (r.label !== `${code}: leaves identical to en (untranslated?)`) continue;
  57. for (const item of r.items) {
  58. // Item format: "<dottedKey>: "<value>""
  59. const m = item.match(/^(\S+):\s+"(.*)"$/);
  60. if (!m) continue;
  61. const key = m[1];
  62. const enValue = en.get(key);
  63. if (enValue !== undefined && map.get(key) === enValue) {
  64. flagged[key] = enValue;
  65. }
  66. }
  67. }
  68. out[code] = flagged;
  69. }
  70. process.stdout.write(JSON.stringify(out, null, 2));