Add many new language packs with RTL support, refresh brand assets, and harden review rendering with tests. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
4.4 KiB
TypeScript
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;
|