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]>
79 lines
5 KiB
JavaScript
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);
|