mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(i18n): 21-language pipeline, landing/docs/API wiring, landing+API translations
Shared Claude Code translation pipeline (scripts/i18n, no API key) plus Astro/VitePress/Scalar i18n wiring. Landing and API reference translated into all 20 languages; docs i18n wiring + English source anchors. The translated docs markdown (apps/docs/<locale>/**, 3,620 files) follows in a companion PR because it exceeds GitHub's per-PR CI file limit.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// scripts/i18n/check-parity.mjs
|
||||
// Parity/staleness gate for the web-surfaces translation pipeline. For every
|
||||
// registered adapter and every non-en locale, it replays extract() + load() and
|
||||
// fails if any unit is missing a translation or its stored sourceHash no longer
|
||||
// matches the current English source (stale). This is the build-time mirror of
|
||||
// the app's TypeScript key-parity guarantee.
|
||||
//
|
||||
// It imports the SAME adapter registry the CLI uses (adapters/registry.mjs), so
|
||||
// the surface list is defined once and never drifts from translate.mjs.
|
||||
import { ADAPTERS } from "./adapters/registry.mjs";
|
||||
import { hash } from "./lib/hash.mjs";
|
||||
import { localeCodes } from "./lib/shared-i18n.mjs";
|
||||
|
||||
/**
|
||||
* Validate one adapter's translations against the current English source.
|
||||
* A problem is: a locale missing a unit, or a stored sourceHash that no longer
|
||||
* matches the current English source hash (stale). Human-refined units still
|
||||
* count as stale here: staleness means "needs review", which is exactly what a
|
||||
* parity check should flag. `en` is always the source and never a target.
|
||||
*
|
||||
* @param {{ name: string, extract: () => Promise<any[]>, load: (l: string) => Promise<Map<string, any>> }} adapter
|
||||
* @param {string[]} locales
|
||||
* @returns {Promise<{ ok: boolean, problems: string[] }>}
|
||||
*/
|
||||
export async function checkAdapter(adapter, locales) {
|
||||
const units = await adapter.extract();
|
||||
const sourceHashes = new Map(units.map((u) => [u.id, hash(u.sourceText)]));
|
||||
const problems = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
if (locale === "en") continue;
|
||||
const stored = await adapter.load(locale);
|
||||
for (const [id, srcHash] of sourceHashes) {
|
||||
const entry = stored.get(id);
|
||||
if (!entry) {
|
||||
problems.push(`[${adapter.name}] ${locale}: missing translation for unit "${id}"`);
|
||||
continue;
|
||||
}
|
||||
if (entry.sourceHash !== srcHash) {
|
||||
problems.push(
|
||||
`[${adapter.name}] ${locale}: stale translation for unit "${id}" ` +
|
||||
`(stored ${entry.sourceHash || "<none>"}, source ${srcHash})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: problems.length === 0, problems };
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the parity check across every registered adapter for every non-en locale.
|
||||
* @param {{ log?: (msg: string) => void }} [opts]
|
||||
* @returns {Promise<{ ok: boolean, problems: string[] }>}
|
||||
*/
|
||||
export async function runParityCheck({ log = () => {} } = {}) {
|
||||
const surfaces = Object.keys(ADAPTERS);
|
||||
if (surfaces.length === 0) {
|
||||
log("No adapters registered yet. Parity check is a no-op until a surface adapter lands.");
|
||||
return { ok: true, problems: [] };
|
||||
}
|
||||
|
||||
const locales = localeCodes().filter((c) => c !== "en");
|
||||
const problems = [];
|
||||
for (const surface of surfaces) {
|
||||
const mod = await ADAPTERS[surface]();
|
||||
const report = await checkAdapter(mod.adapter, locales);
|
||||
problems.push(...report.problems);
|
||||
log(`[${surface}] ${report.ok ? "OK" : `${report.problems.length} problem(s)`}`);
|
||||
}
|
||||
return { ok: problems.length === 0, problems };
|
||||
}
|
||||
|
||||
// Only run the CLI body when invoked directly, not when imported by tests.
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runParityCheck({ log: (m) => process.stdout.write(`${m}\n`) })
|
||||
.then((report) => {
|
||||
if (!report.ok) {
|
||||
process.stderr.write("\nTranslation parity check FAILED:\n");
|
||||
for (const p of report.problems) process.stderr.write(` - ${p}\n`);
|
||||
process.stderr.write(
|
||||
"\nExport the affected surface/locale (pnpm i18n:translate --export), translate the\n" +
|
||||
"pending batch, then import it (--import) and commit the regenerated files.\n",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stdout.write("Translation parity check passed.\n");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user