135 lines
4.4 KiB
TypeScript
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;
|
|
}
|