gsmol/scripts/build-locale.mjs
2026-10-09 13:47:50 +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}`);