mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
The bottom tabs showed their route names ("Mail", "Calendar", …) in
every language; they now use the webmail's navigation names, and an
unavailable tab says so with one translated message instead of an
English "(unavailable)" suffix. The first loading screen is
translated as well.
The Unified Inbox selection toolbar, its back and clear-search buttons,
and the drawer's close, expand/collapse and remove-account buttons get
screen reader labels. Its unknown-sender fallback, selection count and
"account(s) could not be loaded" banner are proper translations and
plurals now.
The key harvest and the translation coverage test did not look at
App.tsx, which sits next to src/; they do now.
102 lines
3.8 KiB
JavaScript
102 lines
3.8 KiB
JavaScript
// Collect t('key', 'English fallback') calls whose key exists in neither the
|
|
// vendored webmail catalog nor the RN overlay, and add them to
|
|
// locales/rn/en.json using the inline fallback as the English text.
|
|
// Run from the RN repo root: `node scripts/harvest-rn-keys.mjs [--check]`.
|
|
// `--check` only reports (exit 1 when something is missing) - used by the
|
|
// translation coverage test message.
|
|
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
import { join, dirname, relative } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, '..');
|
|
const SRC = join(ROOT, 'src');
|
|
const EN = join(ROOT, 'locales', 'en', 'common.json');
|
|
const OVERLAY = join(ROOT, 'locales', 'rn', 'en.json');
|
|
const CHECK_ONLY = process.argv.includes('--check');
|
|
|
|
function flatten(obj, prefix = '', out = new Map()) {
|
|
for (const [k, v] of Object.entries(obj)) {
|
|
const key = prefix ? `${prefix}.${k}` : k;
|
|
if (v && typeof v === 'object' && !Array.isArray(v)) flatten(v, key, out);
|
|
else out.set(key, v);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function walk(dir, out = []) {
|
|
for (const entry of readdirSync(dir)) {
|
|
const p = join(dir, entry);
|
|
if (statSync(p).isDirectory()) {
|
|
if (entry === '__tests__' || entry === 'node_modules') continue;
|
|
walk(p, out);
|
|
} else if (/\.tsx?$/.test(entry)) {
|
|
out.push(p);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function setDeep(obj, key, value) {
|
|
const parts = key.split('.');
|
|
let cur = obj;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
if (!cur[parts[i]] || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
|
|
cur = cur[parts[i]];
|
|
}
|
|
if (cur[parts[parts.length - 1]] === undefined) cur[parts[parts.length - 1]] = value;
|
|
}
|
|
|
|
function sortDeep(obj) {
|
|
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
|
|
return Object.keys(obj).sort().reduce((acc, k) => {
|
|
acc[k] = sortDeep(obj[k]);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
const en = flatten(JSON.parse(readFileSync(EN, 'utf8')));
|
|
const overlay = JSON.parse(readFileSync(OVERLAY, 'utf8'));
|
|
const overlayFlat = flatten(overlay);
|
|
|
|
// t('key', 'fallback') / t('key', "fallback") / t('key', `fallback`) - the
|
|
// fallback may span lines; keep it simple: single-quoted or double-quoted,
|
|
// non-nested strings. `tr` is the same function where `t` is taken (a
|
|
// template or timer variable).
|
|
const CALL_RE = /\btr?\(\s*'([^']+)'\s*,\s*(?:'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)")/g;
|
|
const KEY_ONLY_RE = /\btr?\(\s*'([^']+)'/g;
|
|
|
|
const added = [];
|
|
const missingNoFallback = [];
|
|
// App.tsx (the tab bar and loading screen) sits next to src/.
|
|
for (const file of [...walk(SRC), join(ROOT, 'App.tsx')]) {
|
|
const text = readFileSync(file, 'utf8');
|
|
const withFallback = new Map();
|
|
for (const m of text.matchAll(CALL_RE)) {
|
|
const value = (m[2] ?? m[3] ?? '').replace(/\\(['"])/g, '$1');
|
|
if (value) withFallback.set(m[1], value);
|
|
}
|
|
for (const m of text.matchAll(KEY_ONLY_RE)) {
|
|
const key = m[1];
|
|
if (en.has(key) || overlayFlat.has(key)) continue;
|
|
const fallback = withFallback.get(key);
|
|
if (!fallback) {
|
|
missingNoFallback.push(`${key} (${relative(ROOT, file)})`);
|
|
continue;
|
|
}
|
|
if (!added.some((a) => a.key === key)) added.push({ key, fallback, file: relative(ROOT, file) });
|
|
}
|
|
}
|
|
|
|
if (added.length === 0 && missingNoFallback.length === 0) {
|
|
console.log('All t() keys are covered.');
|
|
process.exit(0);
|
|
}
|
|
for (const a of added) console.log(`${CHECK_ONLY ? 'missing' : 'added'}: ${a.key} = ${JSON.stringify(a.fallback)} (${a.file})`);
|
|
for (const m of missingNoFallback) console.log(`missing (no inline fallback, add by hand): ${m}`);
|
|
|
|
if (CHECK_ONLY) process.exit(1);
|
|
for (const a of added) setDeep(overlay, a.key, a.fallback);
|
|
writeFileSync(OVERLAY, JSON.stringify(sortDeep(overlay), null, 2) + '\n');
|
|
console.log(`Wrote ${added.length} key(s) to locales/rn/en.json`);
|