88 lines
3.1 KiB
TypeScript
88 lines
3.1 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 all three locales) 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.
|
|
*/
|
|
import en from './en.json';
|
|
import es from './es.json';
|
|
import nl from './nl.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,
|
|
};
|
|
|
|
/** 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;
|
|
}
|