import { DEFAULT_LOCALE, getLocale, type Locale } from './locale' import { en } from './messages/en' import { type MessageKey, zhCN } from './messages/zh-CN' /** * Translation core. The zh-CN dictionary is the reference: it defines the key * set and every caller is type-checked against it, while the English dictionary * is declared as a total `Record` so a missing or renamed * key fails `tsc` instead of silently falling back at runtime. */ export type { MessageKey } /** Membership test against the reference (zh-CN) dictionary. */ const MESSAGE_KEYS: ReadonlySet = new Set(Object.keys(zhCN)) export function isMessageKey(key: string): key is MessageKey { return MESSAGE_KEYS.has(key) } /** Values accepted by `{placeholder}` interpolation. */ export type MessageParams = Record const DICTIONARIES: Record> = { 'zh-CN': zhCN, en, } function interpolate(template: string, params?: MessageParams): string { if (!params) return template return template.replace(/\{(\w+)\}/g, (match, name: string) => (name in params ? String(params[name]) : match)) } /** * Look up `key` in the active locale, then in the reference dictionary, then * give up and return the key itself. The last resort is deliberate: a raw * `repo.tabs.files` on screen is an obvious bug report, whereas an empty string * is invisible. */ export function translate(locale: Locale, key: string, params?: MessageParams): string { const hit = DICTIONARIES[locale][key] ?? DICTIONARIES[DEFAULT_LOCALE][key] if (hit === undefined) { if (import.meta.env.DEV) console.warn(`[i18n] missing message: ${key}`) return key } return interpolate(hit, params) } /** Imperative lookup for non-component code (formatters, stores, `meta.ts`). */ export function t(key: MessageKey, params?: MessageParams): string { return translate(getLocale(), key, params) } const PLURAL_RULES: Partial> = {} function pluralRules(locale: Locale): Intl.PluralRules { PLURAL_RULES[locale] ??= new Intl.PluralRules(locale) return PLURAL_RULES[locale] } /** * Plural-aware lookup. Dictionaries carry `_one` / `_other` suffixed pairs and * the category is chosen by `Intl.PluralRules`, so languages with richer plural * sets can add `_few` / `_many` entries later without changing call sites. * A missing suffixed entry degrades to `_other`, then to the bare key. */ export function translateCount(locale: Locale, key: string, count: number, params?: MessageParams): string { const category = pluralRules(locale).select(count) const withCategory = `${key}_${category}` const suffix = withCategory in DICTIONARIES[locale] || withCategory in DICTIONARIES[DEFAULT_LOCALE] return translate(locale, suffix ? withCategory : `${key}_other`, { count, ...params }) } export function tCount(key: MessageKey, count: number, params?: MessageParams): string { return translateCount(getLocale(), key, count, params) } /** Same lookup as `t`, bound to an explicit locale (SSR/tests/tooling). */ export function tIn(locale: Locale, key: MessageKey, params?: MessageParams): string { return translate(locale, key, params) }