feat(i18n): nine languages, each at its own address; release v6.3.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

The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese,
Korean and Hindi alongside English — 623 strings per language, 5,607
translations, covering the landing page, the key exchange, the chat, group
calls and every error along the way.

Each locale is a real page at a real URL (/de/, /fr/, …), generated at build
time from locales/*.json. That is the whole point: the app is client-rendered,
so a language that only swaps strings at runtime has no address for a crawler
to index and no link anyone can share. Each page carries its own canonical, a
reciprocal hreflang cluster and translated schema.org.

The URL decides the language, always. A stored preference only applies where
the address does not say, and nothing ever redirects on Accept-Language —
that is how sites become invisible to search engines outside one country.

robots.txt and sitemap.xml did not exist before; both are generated now.

Bugs found on the way, each with a test that would have caught it:
  - partner logos used page-relative paths and 404'd from any /xx/ page
  - post-build stamped ?v= only into the root shell, leaving locales behind
  - "Back online" appeared on every page load, not just after being offline
  - update timestamps were hard-coded to US format for every reader
  - t() threw on a partial window, taking whole components down with it
This commit is contained in:
lockbitchat
2026-08-29 12:46:15 -04:00
parent 943e04e7ff
commit 98d42ac2fb
73 changed files with 33861 additions and 1299 deletions
+11 -1
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
import { t } from '../src/i18n/index.js';
const effects = [];
const setterCalls = [];
@@ -9,6 +10,10 @@ const callbackCalls = [];
const context = {
window: {},
// The component reaches its copy through t(); in the browser that arrives as an
// ES import, which a bare VM script cannot evaluate. Supply the real function so
// the component sees exactly what it sees at runtime.
t,
React: {
useState(initialValue) {
const index = stateIndex++;
@@ -26,7 +31,12 @@ const context = {
}
};
const source = fs.readFileSync(new URL('../src/components/ui/FileTransfer.jsx', import.meta.url), 'utf8');
// vm.runInNewContext evaluates a script, not a module, so the import statements are
// stripped and their bindings handed in through the context above — the same
// substitution the bundler performs, done by hand.
const source = fs
.readFileSync(new URL('../src/components/ui/FileTransfer.jsx', import.meta.url), 'utf8')
.replace(/^import\s[^;]*;\s*$/gm, '');
vm.runInNewContext(source, context);
const manager = {