Files
CashuMints.space/web/src/i18n/config.ts
T
michilisandCursor 47e6537dde Expand i18n locales and improve reviews UI
Add many new language packs with RTL support, refresh brand assets, and harden review rendering with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 05:06:58 +02:00

112 lines
4.4 KiB
TypeScript

/**
* The locale table. Adding a language is this file, `xx.json`, and nothing else.
*
* Everything downstream reads from `LOCALES`: the Astro i18n config, the language
* switcher, the hreflang set, the sitemap, the suggestion bar and the catalog loader.
* There is deliberately no second list to keep in step.
*/
export const DEFAULT_LOCALE = 'en' as const;
export interface LocaleInfo {
/** URL prefix and catalog filename. Also the value of `<html lang>`. */
code: string;
/**
* The language's own name for itself, for the switcher. Never a flag: a flag is a
* country, and Spanish is not Spain.
*/
label: string;
/**
* BCP 47 tag handed to Intl. Region included because number and date formatting
* differ by region even when the language does not.
*/
intl: string;
/** `og:locale`. Underscored, region required by the OpenGraph spec. */
og: string;
/** Writing direction for the document. */
dir?: 'rtl';
}
/**
* Order matters: this is the order the switcher renders in, and the default locale
* comes first.
*/
export const LOCALES: LocaleInfo[] = [
{ code: 'en', label: 'English', intl: 'en-GB', og: 'en_US' },
{ code: 'es', label: 'Español', intl: 'es-ES', og: 'es_ES' },
{ code: 'nl', label: 'Nederlands', intl: 'nl-NL', og: 'nl_NL' },
{ code: 'zh', label: '简体中文', intl: 'zh-CN', og: 'zh_CN' },
{ code: 'hi', label: 'हिन्दी', intl: 'hi-IN', og: 'hi_IN' },
{ code: 'ar', label: 'العربية الفصحى', intl: 'ar-SA', og: 'ar_SA', dir: 'rtl' },
{ code: 'fr', label: 'Français', intl: 'fr-FR', og: 'fr_FR' },
{ code: 'pt', label: 'Português', intl: 'pt-BR', og: 'pt_BR' },
{ code: 'it', label: 'Italiano', intl: 'it-IT', og: 'it_IT' },
{ code: 'tr', label: 'Türkçe', intl: 'tr-TR', og: 'tr_TR' },
{ code: 'pl', label: 'Polski', intl: 'pl-PL', og: 'pl_PL' },
{ code: 'uk', label: 'Українська', intl: 'uk-UA', og: 'uk_UA' },
{ code: 'ro', label: 'Română', intl: 'ro-RO', og: 'ro_RO' },
{ code: 'el', label: 'Ελληνικά', intl: 'el-GR', og: 'el_GR' },
{ code: 'cs', label: 'Čeština', intl: 'cs-CZ', og: 'cs_CZ' },
{ code: 'sv', label: 'Svenska', intl: 'sv-SE', og: 'sv_SE' },
{ code: 'da', label: 'Dansk', intl: 'da-DK', og: 'da_DK' },
{ code: 'id', label: 'Bahasa Indonesia', intl: 'id-ID', og: 'id_ID' },
{ code: 'ur', label: 'اردو', intl: 'ur-PK', og: 'ur_PK', dir: 'rtl' },
{ code: 'ru', label: 'Русский', intl: 'ru-RU', og: 'ru_RU' },
{ code: 'de', label: 'Deutsch', intl: 'de-DE', og: 'de_DE' },
{ code: 'ja', label: '日本語', intl: 'ja-JP', og: 'ja_JP' },
{ code: 'ar-eg', label: 'العربية المصرية', intl: 'ar-EG', og: 'ar_EG', dir: 'rtl' },
{ code: 'vi', label: 'Tiếng Việt', intl: 'vi-VN', og: 'vi_VN' },
];
export type Locale = string;
/** Just the codes, for the Astro config and for validating a URL segment. */
export const LOCALE_CODES: string[] = LOCALES.map((l) => l.code);
/** Every locale except the default, which is the set that carries a URL prefix. */
export const PREFIXED_LOCALES: string[] = LOCALE_CODES.filter((code) => code !== DEFAULT_LOCALE);
export function isLocale(value: unknown): value is Locale {
return typeof value === 'string' && LOCALE_CODES.includes(value);
}
export function localeInfo(locale: Locale): LocaleInfo {
return LOCALES.find((l) => l.code === locale) ?? LOCALES[0]!;
}
/** The Intl tag for a locale, for `Intl.NumberFormat` and friends. */
export function intlTag(locale: Locale): string {
return localeInfo(locale).intl;
}
/**
* Namespaces whose keys are sent to the browser.
*
* Islands render strings at runtime, so their copy has to reach the client. The static
* half of the site (home page sections, about, the legal pages) is rendered at build
* time and its copy never needs to ship. Splitting on the namespace keeps the inline
* payload to roughly a third of the catalog.
*
* `scripts/check-i18n.mjs` fails the build if an island reaches for a key outside this
* list, so the split cannot silently rot into a missing string on a real page.
*/
export const CLIENT_NAMESPACES = [
'common',
'time',
'status',
'pulse',
'mints',
'mint',
'nut',
// The federation index's own count line, and the module names the live island
// redraws when a fresh announcement disagrees with the build.
'fedimints',
'fedimint',
'card',
'reviews',
'feed',
'login',
'signer',
'notfound',
] as const;