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.
This commit is contained in:
parent
80aca8a29f
commit
70087fa470
7 changed files with 1080 additions and 258 deletions
46
scripts/build-locale.mjs
Normal file
46
scripts/build-locale.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#!/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}`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue