feat(i18n): Arabic, Hebrew, Persian and Urdu, and a layout that mirrors; release v6.4.0
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

Four right-to-left languages at /ar/, /he/, /fa/ and /ur/ — thirteen in all.

The layout no longer has a left and a right, it has a start and an end: the
margins, insets and corners are CSS logical properties now, so they follow
dir on <html>. Directional glyphs flip; keys, safety codes and session
descriptors are pinned left-to-right so bidi cannot reorder what two people
compare against each other's screens.

Also fixed: the Service Worker was registered as './sw.js', which 404s from
every locale subdirectory, so twelve of the thirteen pages had no worker at
all. And the bundler ran before the dictionaries were generated, so a newly
added language could reach the page ahead of the app that renders it.
This commit is contained in:
lockbitchat
2026-08-29 18:05:20 -04:00
parent 98d42ac2fb
commit 5e32f547b9
68 changed files with 15147 additions and 500 deletions
+37
View File
@@ -23,6 +23,43 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
);
}
// The dictionaries have to reach the bundle the browser actually downloads, not just
// the module on disk. `build:js` bundles whatever src/i18n/generated.js says at the
// moment it runs, so if generation happened after bundling, a newly added language
// would ship a correct page — right <html lang>, right <html dir> — wrapped around an
// app that has never heard of it and quietly falls back to English. That failure looks
// like a translation bug and is really a build-order bug, so it is pinned here.
{
const bundle = readFileSync(path.join(ROOT, 'dist/app.js'), 'utf8');
const { SUPPORTED_LOCALES, DICTIONARIES } = await import(
pathToFileURL(path.join(ROOT, 'src/i18n/generated.js'))
);
// esbuild escapes anything outside ASCII, so nothing but pure Latin is findable as
// literal text: Latin-1 becomes \xNN, everything above it \uXXXX, both uppercase.
// Escape the needle the same way before looking for it.
const asEmitted = (text) =>
[...text]
.map((ch) => {
const code = ch.codePointAt(0);
if (code < 0x80) return ch;
const hex = (n, width) => n.toString(16).toUpperCase().padStart(width, '0');
if (code <= 0xff) return `\\x${hex(code, 2)}`;
return [...ch].map((unit) => `\\u${hex(unit.charCodeAt(0), 4)}`).join('');
})
.join('');
const absent = SUPPORTED_LOCALES.filter(
(code) => !bundle.includes(asEmitted(DICTIONARIES[code]['hero.headlineTop']))
);
assert.deepEqual(absent, [],
`dist/app.js carries no strings for: ${absent.join(', ')} — run \`npm run build\`, ` +
'and check that build:i18n still runs before build:js');
assert.ok(bundle.includes(`SUPPORTED_LOCALES = ${JSON.stringify(SUPPORTED_LOCALES).replace(/,/g, ', ')}`),
'the bundled locale registry disagrees with src/i18n/generated.js');
}
// The real module, as shipped.
{
const live = await import(pathToFileURL(path.join(ROOT, 'src/i18n/index.js')));