main
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<MessageKey, string>` 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<string> = 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<string, string | number>
const DICTIONARIES: Record<Locale, Record<string, string>> = {
'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<Record<Locale, Intl.PluralRules>> = {}
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)
}