Files
CashuMints.space/web/src/i18n/index.ts
T

135 lines
4.4 KiB
TypeScript

/**
* Build-time i18n entry point. Imported from .astro frontmatter, never from an island.
*
* Every catalog is imported statically here, which is exactly what we want at build
* time (one process renders every locale) and exactly what we do not want in the
* browser. Islands import `./client` instead, which reads the one locale the page
* already carries. Nothing in this file reaches a bundle: Astro frontmatter runs on the
* build machine only.
*
* Every locale in `LOCALES` needs a line in both places below. A locale listed in the
* table with no catalog here does not fail: it silently renders English under its own
* `lang` and URL prefix, which is worse than failing. `test/i18n-wiring.test.mjs`
* fails instead.
*/
import en from './en.json';
import es from './es.json';
import nl from './nl.json';
import zh from './zh.json';
import hi from './hi.json';
import ar from './ar.json';
import fr from './fr.json';
import pt from './pt.json';
import it from './it.json';
import tr from './tr.json';
import pl from './pl.json';
import uk from './uk.json';
import ro from './ro.json';
import el from './el.json';
import cs from './cs.json';
import sv from './sv.json';
import da from './da.json';
import id from './id.json';
import ur from './ur.json';
import ru from './ru.json';
import de from './de.json';
import ja from './ja.json';
import areg from './ar-eg.json';
import vi from './vi.json';
import { CLIENT_NAMESPACES, DEFAULT_LOCALE, LOCALE_CODES, isLocale, type Locale } from './config.js';
import { createTranslator, type Catalog, type Translator } from './translate.js';
export * from './config.js';
export * from './routing.js';
export type { Translator, Vars } from './translate.js';
const CATALOGS: Record<string, Catalog> = {
en: en as Catalog,
es: es as Catalog,
nl: nl as Catalog,
zh: zh as Catalog,
hi: hi as Catalog,
ar: ar as Catalog,
fr: fr as Catalog,
pt: pt as Catalog,
it: it as Catalog,
tr: tr as Catalog,
pl: pl as Catalog,
uk: uk as Catalog,
ro: ro as Catalog,
el: el as Catalog,
cs: cs as Catalog,
sv: sv as Catalog,
da: da as Catalog,
id: id as Catalog,
ur: ur as Catalog,
ru: ru as Catalog,
de: de as Catalog,
ja: ja as Catalog,
'ar-eg': areg as Catalog,
vi: vi as Catalog,
};
/** The English catalog, which is what every other locale falls back to key by key. */
export const BASE_CATALOG: Catalog = CATALOGS[DEFAULT_LOCALE]!;
export function catalogFor(locale: Locale): Catalog {
return CATALOGS[locale] ?? BASE_CATALOG;
}
const translators = new Map<string, Translator>();
/**
* The translator for one locale. Cached, because every component on every page asks
* for it and there are ~180 pages.
*/
export function useI18n(locale: Locale): Translator {
const code = isLocale(locale) ? locale : DEFAULT_LOCALE;
let t = translators.get(code);
if (!t) {
t = createTranslator(code, catalogFor(code), BASE_CATALOG);
translators.set(code, t);
}
return t;
}
/**
* Which keys are missing from which locale, as the build reports them.
*
* English is the source of truth: a key it does not have is not a key. Exported so
* both the build warning in `astro.config.mjs` and `scripts/check-i18n.mjs` answer the
* question the same way.
*/
export function missingKeys(): Record<string, string[]> {
const expected = Object.keys(BASE_CATALOG);
const out: Record<string, string[]> = {};
for (const code of LOCALE_CODES) {
if (code === DEFAULT_LOCALE) continue;
const catalog = catalogFor(code);
const missing = expected.filter((key) => catalog[key] === undefined);
if (missing.length > 0) out[code] = missing;
}
return out;
}
/**
* The subset of a catalog an island can reach, resolved against English first.
*
* Two things happen here that matter. Keys outside `CLIENT_NAMESPACES` are dropped, so
* the Spanish home page does not inline the Dutch legal pages or its own. And every
* key that survives is resolved: a key this locale is missing arrives already filled
* with the English string, so the browser needs no fallback catalog and ships exactly
* one language.
*/
export function clientCatalog(locale: Locale): Catalog {
const catalog = catalogFor(locale);
const out: Catalog = {};
for (const key of Object.keys(BASE_CATALOG)) {
const namespace = key.split('.')[0] ?? '';
if (!(CLIENT_NAMESPACES as readonly string[]).includes(namespace)) continue;
out[key] = catalog[key] ?? BASE_CATALOG[key]!;
}
return out;
}