gsmol/scripts/build-locale.mjs
randogoth 70087fa470 feat: base UI strings on carrier's shared Fluent source
Vendors carrier's locales/en-US/main.ftl (extended to cover gsmol's
remaining strings) as the single source both clients draw wording
from, adds a dependency-free .ftl-to-JSON build step (devbox run
locale) since the file only ever uses Fluent's flat subset, and
replaces every hardcoded string in index.html/app.js with a lookup
into the generated dictionary.

Along the way: removes connect()'s requirePin check, which turned out
to be unreachable under the app's own invariants (every call site
already guarantees its target host is pinned before calling it), and
fixes a leftover call to store.setMasterSaved — removed in the earlier
settings redesign — that was silently breaking onboarding's "I saved
it" button.
2026-10-08 23:43:59 +03:00

46 lines
2 KiB
JavaScript

#!/usr/bin/env node
// Converts locales/en-US/main.ftl into web/locales/en-US.json: a flat
// id -> string dictionary the browser can fetch with no parser of its own.
//
// Deliberately hand-rolled rather than a dependency on @fluent/syntax: the
// source file only ever uses Fluent's simplest shape (comments, flat
// `id = value` messages, `{ $var }` references, and `{"literal"}` string
// inserts for embedded newlines) — confirmed by grep before writing this,
// not assumed. If the file ever gains a select expression, a term, or a
// multiline value, this script will need to grow with it; until then a
// real parser is more dependency than the input justifies.
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const srcPath = join(root, "locales/en-US/main.ftl");
const outPath = join(root, "web/locales/en-US.json");
const MESSAGE = /^([a-zA-Z][a-zA-Z0-9_-]*)\s*=\s*(.*)$/;
// A Fluent string literal, e.g. {"\u000A"} — only used in this file to
// embed a literal newline inside an otherwise single-line value.
const LITERAL = /\{"((?:[^"\\]|\\.)*)"\}/g;
function unescapeLiteral(text) {
return text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/\\(.)/g, "$1");
}
function parseFtl(text) {
const messages = {};
for (const line of text.split("\n")) {
if (!line || line.startsWith("#")) continue; // blank line or comment
const match = MESSAGE.exec(line);
if (!match) continue;
const [, id, rawValue] = match;
messages[id] = rawValue.replace(LITERAL, (_, inner) => unescapeLiteral(inner));
}
return messages;
}
const messages = parseFtl(readFileSync(srcPath, "utf8"));
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, JSON.stringify(messages, null, 2) + "\n");
console.log(`wrote ${Object.keys(messages).length} messages to ${outPath}`);