#!/usr/bin/env node // The site's catalogs (apps/web/locales, docs/TRANSLATING.md → "The website"). English is the source: // a page's own text is in its HTML, marked with data-i18n (apps/web/i18n-html.ts); what the build and // the scripts say is written in en.json by hand (site.meta.*, site.gen.*, site.js.*, // site.lang.*). // // node scripts/i18n-site.mjs check: en.json matches the pages, every language has every // key, then the framework's `waybetter i18n check` on the // site's catalogs (exit 1 on errors) // node scripts/i18n-site.mjs --write write the pages' text and notes into en.json / en.meta.json // (hand-written keys and notes are kept) and languages.json // node scripts/i18n-site.mjs --verbose list every warning // node scripts/i18n-site.mjs tr only these languages (plus English) import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { i18nCommand } from '../waybetter/packages/waybetter/cli/i18n.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const web = join(root, 'apps/web'); const locales = join(web, 'locales'); const args = process.argv.slice(2); const write = args.includes('--write'); const { CODE_PREFIXES, extractPages } = await import('../apps/web/i18n.ts'); const { LANGUAGES } = await import('../apps/web/site.config.ts'); const readJson = (file, fallback) => (existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : fallback); const byHand = key => CODE_PREFIXES.some(prefix => key.startsWith(prefix)); let errors = 0; const fail = message => { errors += 1; console.error(`ERROR ${message}`); }; try { const pages = extractPages(web); const enFile = join(locales, 'en.json'); const metaFile = join(locales, 'en.meta.json'); const en = readJson(enFile, { $schema: '../../../waybetter/packages/waybetter/i18n.schema.json', locale: 'en', name: 'English', englishName: 'English', status: 'source', messages: {} }); const meta = readJson(metaFile, { $schema: '../../../waybetter/packages/waybetter/i18n-meta.schema.json' }); const hand = Object.fromEntries(Object.entries(en.messages).filter(([key]) => byHand(key))); const handNotes = Object.fromEntries(Object.entries(meta).filter(([key]) => byHand(key))); const wanted = { ...en, messages: { ...pages.messages, ...hand } }; const wantedMeta = { $schema: meta.$schema, ...pages.notes, ...handNotes }; const text = value => `${JSON.stringify(value, null, 2)}\n`; if (write) { writeFileSync(enFile, text(wanted)); writeFileSync(metaFile, text(wantedMeta)); console.log(`wrote apps/web/locales/en.json (${Object.keys(pages.messages).length} page + ${Object.keys(hand).length} hand-written messages) and en.meta.json`); } else { if (text(wanted) !== text(en)) { const stale = [...new Set([...Object.keys(wanted.messages), ...Object.keys(en.messages)])].filter(key => wanted.messages[key] !== en.messages[key]); fail(`apps/web/locales/en.json is out of date with the pages (${stale.slice(0, 5).join(', ')}${stale.length > 5 ? ', …' : stale.length ? '' : 'key order'}): run \`node scripts/i18n-site.mjs --write\``); } if (text(wantedMeta) !== text(meta)) fail('apps/web/locales/en.meta.json is out of date with the pages: run `node scripts/i18n-site.mjs --write`'); // A site page is never half translated: every published language has every key. for (const code of LANGUAGES) { if (code === 'en' || !existsSync(join(locales, `${code}.json`))) continue; const catalog = readJson(join(locales, `${code}.json`)); const missing = Object.keys(wanted.messages).filter(key => !(key in catalog.messages)); if (missing.length) fail(`apps/web/locales/${code}.json lacks ${missing.length} message(s): ${missing.slice(0, 5).join(', ')}${missing.length > 5 ? ', …' : ''}`); } } const code = await i18nCommand(['check', ...args, '--config', join(web, 'waybetter.i18n.json')], { cwd: root }); if (errors) console.error(`i18n-site: ${errors} error(s)`); process.exit(code || (errors ? 1 : 0)); } catch (error) { console.error(`i18n-site: ${error instanceof Error ? error.message : error}`); process.exit(1); }