// The Studio's words in every locale, without a browser: // // node test/locales.test.mjs // // - locales// 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);