open-standards/wai/quantum-studio/test/locales.test.mjs
Transaction Science f090e8b6d1 quantum-studio: the course in the Studio and the desktop app, and the interface in Simplified Chinese
The Course pane lists the 21 lessons, shows a lesson's text and exercise,
loads its starter, and checks the program in the editor with the engine
(engine_course_check in the page, the same commands in the desktop app):
each case's verdict, each rule, What went wrong? from the diagnostics, the
verdict and the critique, the hints one at a time with the engine's pointer,
and a hint from the assistant when a model is connected, any program in its
answer held back. Progress is kept only in this browser and says so.

The interface's words are data, locales/<locale>/<area>.json for every part,
English the source and Simplified Chinese marked machine-assisted; the Studio
follows the browser's language until the reader picks one with the switch.
test/locales.test.mjs holds every locale to every key and placeholder. The
e2e checks the course in English and in Chinese. wai-web's engine tests pin
23 native and 16 wasm32, a course check the same bytes in both.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 08:43:06 -04:00

79 lines
5 KiB
JavaScript

// The Studio's words in every locale, without a browser:
//
// node test/locales.test.mjs
//
// - locales/<locale>/ holds the same parts of the interface for every locale,
// and locales/index.js gathers each of them;
// - every key of English is in every locale, and no other, with the same
// placeholders;
// - every locale but English is marked machine-assisted, to be reviewed;
// - every key the interface asks for (t('…') or tr('…') in src/) is in English;
// - the language is the reader's choice, else the browser's first language the
// Studio is written in, else English, as the engine picks a locale.
// Exit 0 iff every check passes.
import { readFileSync, readdirSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const dir = join(here, 'locales');
let fails = 0;
const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
const locales = readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
check(locales.includes('en') && locales.includes('zh-CN'), `the locales are ${locales.join(', ')}`);
const areas = (l) => readdirSync(join(dir, l)).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, '')).sort();
const index = readFileSync(join(dir, 'index.js'), 'utf8');
const { AREAS, default: gathered } = await import(join(dir, 'index.js'));
for (const l of locales) {
check(JSON.stringify(areas(l)) === JSON.stringify(areas('en')), `${l} has the parts English has (${areas(l).join(', ')})`);
for (const a of areas(l)) check(index.includes(`'./${l}/${a}.json'`), `locales/index.js gathers ${l}/${a}.json`);
}
check(JSON.stringify([...AREAS].sort()) === JSON.stringify(areas('en')), `AREAS names every part (${AREAS.join(', ')})`);
const holes = (s) => [...String(s).matchAll(/\{([a-z_]+)\}/g)].map((m) => m[1]).sort().join(',');
const read = (l, a) => JSON.parse(readFileSync(join(dir, l, `${a}.json`), 'utf8'));
const seen = new Map();
for (const a of areas('en')) {
const en = read('en', a);
for (const k of Object.keys(en)) {
if (k === '_meta') continue;
check(!seen.has(k), `${k} is in one part only (${a}${seen.has(k) ? ', ' + seen.get(k) : ''})`);
seen.set(k, a);
}
for (const l of locales.filter((x) => x !== 'en')) {
const other = read(l, a);
const keys = (o) => Object.keys(o).filter((k) => k !== '_meta').sort();
const missing = keys(en).filter((k) => !(k in other));
const extra = keys(other).filter((k) => !(k in en));
check(!missing.length && !extra.length, `${l}/${a}.json has every key of en/${a}.json and no other${missing.length ? '; missing ' + missing.join(', ') : ''}${extra.length ? '; extra ' + extra.join(', ') : ''}`);
const off = keys(en).filter((k) => k in other && holes(en[k]) !== holes(other[k]));
check(!off.length, `${l}/${a}.json keeps each key's placeholders${off.length ? ': ' + off.join(', ') : ''}`);
const empty = keys(other).filter((k) => typeof other[k] !== 'string' || !other[k].trim());
check(!empty.length, `${l}/${a}.json has words for every key${empty.length ? ': ' + empty.join(', ') : ''}`);
check(/^machine-assisted/.test(other._meta?.translation ?? ''), `${l}/${a}.json is marked machine-assisted, to be reviewed`);
}
}
// Every key the interface asks for by name is in English.
const src = join(here, 'src');
const asked = new Set();
for (const f of readdirSync(src).filter((f) => f.endsWith('.js'))) {
for (const m of readFileSync(join(src, f), 'utf8').matchAll(/\b(?:t|tr)\('([a-z0-9_.-]+)'/g)) asked.add(m[1]);
}
const absent = [...asked].filter((k) => !(k in gathered.en)).sort();
check(asked.size > 100 && !absent.length, `every key the interface asks for is in English (${asked.size} asked${absent.length ? '; absent: ' + absent.join(', ') : ''})`);
for (const l of locales) check(Object.keys(gathered[l]).length === Object.keys(gathered.en).length, `${l} gathers ${Object.keys(gathered[l]).length} keys, as English does`);
// The language.
const { chooseLocale, localeOf, t, locale } = await import(join(src, 'i18n.js'));
check(locale() === 'en' && t('frame.run') === '▶ Run', 'outside a page the Studio speaks English');
check(t('frame.lint', { qubits: 2, gates: 3 }) === '2 qubits · 3 gates', 'placeholders are filled');
for (const [stored, languages, want] of [
[null, ['zh-CN', 'en'], 'zh-CN'], [null, ['zh-Hans-CN'], 'zh-CN'], [null, ['fr-FR', 'zh-CN'], 'zh-CN'], [null, ['zh-TW', 'en-US'], 'en'],
[null, ['de-DE'], 'en'], ['en', ['zh-CN'], 'en'], ['zh-CN', ['en-GB'], 'zh-CN'], ['xx', ['zh'], 'zh-CN'], [null, [], 'en'],
]) check(chooseLocale({ stored, languages }) === want, `chosen ${stored ?? 'nothing'}, the browser ${JSON.stringify(languages)}: ${want}`);
for (const [tag, want] of [['zh_CN.UTF-8', 'zh-CN'], ['zh', 'zh-CN'], ['zh-SG', 'zh-CN'], ['zh-TW', 'en'], ['en-GB', 'en']]) check(localeOf(tag) === want, `${tag} reads as ${want}`);
console.log(fails ? `${fails} failed` : 'every check passed');
process.exit(fails ? 1 : 0);