Files
kvitt/web/scripts/gen-i18n.mjs
T
jacobh2 2ae65a5a9f
Build Kvitt images / build (push) Failing after 1h14m5s
W0 web foundation: Vite + React SPA under /app/, sync-pull ledger, auth, tokens, shared components
web/ is the browser client's scaffold: Vite 8, React 19, TypeScript 5.9 strict, React Router 7 (basename /app, every
fixed route with a lazy placeholder page), TanStack Query 5 (refetch on focus and every 60 s), openapi-typescript
types generated from contracts/openapi.yaml with openapi-fetch. Reads go through GET /sync (no REST reads exist for
the ledger, SPEC §7): useLedger() pulls from cursor 0 and then incrementally, restarting on a new epoch or
cursor_ahead. Login pairs the browser as "Web · <browser>" with an invite code (E-3); every call sends the SPEC
§14.3.1 headers, a 401 signs out. kvitt-domain's split, balances, allocation and settlement rules are ported to
web/src/lib and pass every case of contracts/test-vectors. Design tokens from design-spec §2 in tokens.css
(light-dark()), Material Symbols Rounded (Apache-2.0) as generated inline SVG, sv/en catalogs generated from
docs/ux/strings.csv plus web-strings.csv. Gate: cd web && npm run check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-21 21:03:25 +02:00

61 lines
2.4 KiB
JavaScript

// Builds src/i18n/{sv,en}.json from docs/ux/strings.csv (the app's strings, SPEC §13.4) and src/i18n/web-strings.csv
// (web-only keys, prefix web_, same columns). Android placeholders become positional braces: %1$s and %1$d -> {1},
// a bare %d or %s -> {1}, {2}, … in order of appearance, %% -> %. Strings marked formatted=false are copied as is.
// Plurals keep their key:one / key:other keys. `--check` compares with the files on disk instead of writing.
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { parseCsvRecords } from '../src/lib/csv.ts';
const root = new URL('../', import.meta.url);
const sources = [new URL('../docs/ux/strings.csv', root), new URL('src/i18n/web-strings.csv', root)];
export function convertPlaceholders(text, formatted) {
if (!formatted) return text;
let next = 0;
return text.replace(/%(?:(\d+)\$)?([sd%])/g, (_match, index, type) => {
if (type === '%') return '%';
if (index) return `{${index}}`;
next += 1;
return `{${next}}`;
});
}
export function buildCatalogs() {
const catalogs = { en: {}, sv: {} };
for (const source of sources) {
for (const record of parseCsvRecords(readFileSync(source, 'utf-8'))) {
const key = record.key;
if (!key) continue;
if (key in catalogs.en) throw new Error(`duplicate string key ${key} in ${source.pathname}`);
const formatted = !(record.notes ?? '').includes('formatted=false');
catalogs.en[key] = convertPlaceholders(record.en, formatted);
catalogs.sv[key] = convertPlaceholders(record.sv, formatted);
}
}
const sorted = (obj) =>
Object.fromEntries(
Object.keys(obj)
.sort()
.map((k) => [k, obj[k]]),
);
return { en: sorted(catalogs.en), sv: sorted(catalogs.sv) };
}
const check = process.argv.includes('--check');
const catalogs = buildCatalogs();
let stale = false;
for (const lang of ['sv', 'en']) {
const target = new URL(`src/i18n/${lang}.json`, root);
const text = JSON.stringify(catalogs[lang], null, 2) + '\n';
if (check) {
const current = existsSync(target) ? readFileSync(target, 'utf-8') : '';
if (current !== text) {
console.error(`src/i18n/${lang}.json is stale: run npm run i18n:import`);
stale = true;
}
} else {
writeFileSync(target, text);
console.log(`src/i18n/${lang}.json: ${Object.keys(catalogs[lang]).length} keys`);
}
}
if (stale) process.exit(1);