mirror of
https://github.com/veeso/termscp.git
synced 2026-07-09 09:55:10 +02:00
refactor(site): type-safe i18n keys derived from en
This commit is contained in:
@@ -26,4 +26,11 @@ describe("i18n", () => {
|
||||
const t = useTranslations("en");
|
||||
expect(t("hero.tabs", { host: "1.2.3.4" })).toContain("1.2.3.4");
|
||||
});
|
||||
|
||||
it("interpolates the full string exactly", () => {
|
||||
const t = useTranslations("en");
|
||||
expect(t("hero.tabs", { host: "10.0.0.1" })).toBe(
|
||||
"termscp — 10.0.0.1 (sftp) connected",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,18 @@ export const locales = ["en", "zh-CN", "it", "fr", "es"] as const;
|
||||
export type Locale = (typeof locales)[number];
|
||||
export const defaultLocale: Locale = "en";
|
||||
|
||||
const dictionaries: Record<Locale, Record<string, string>> = {
|
||||
en: en as Record<string, string>,
|
||||
"zh-CN": zhCN as Record<string, string>,
|
||||
it: it as Record<string, string>,
|
||||
fr: fr as Record<string, string>,
|
||||
es: es as Record<string, string>,
|
||||
};
|
||||
/** Authoritative set of translation keys, derived from the en dictionary. */
|
||||
export type TranslationKey = keyof typeof en;
|
||||
|
||||
// `en` is left uncast so it remains the source of truth for `TranslationKey`.
|
||||
// Other locales may omit keys, so they are typed as partial dictionaries.
|
||||
const dictionaries = {
|
||||
en,
|
||||
"zh-CN": zhCN as Partial<Record<TranslationKey, string>>,
|
||||
it: it as Partial<Record<TranslationKey, string>>,
|
||||
fr: fr as Partial<Record<TranslationKey, string>>,
|
||||
es: es as Partial<Record<TranslationKey, string>>,
|
||||
} satisfies Record<Locale, Partial<Record<TranslationKey, string>>>;
|
||||
|
||||
export function isLocale(value: string): value is Locale {
|
||||
return (locales as readonly string[]).includes(value);
|
||||
@@ -23,8 +28,17 @@ export function isLocale(value: string): value is Locale {
|
||||
/** Resolve a translator for `locale`, falling back to en, then to the key. */
|
||||
export function useTranslations(locale: Locale) {
|
||||
const dict = dictionaries[locale] ?? dictionaries[defaultLocale];
|
||||
return (key: string, vars?: Record<string, string>): string => {
|
||||
let value = dict[key] ?? dictionaries[defaultLocale][key] ?? key;
|
||||
// `TranslationKey | (string & {})` keeps autocomplete for known keys while
|
||||
// still accepting arbitrary strings, preserving the "return key if missing"
|
||||
// contract at runtime.
|
||||
return (
|
||||
key: TranslationKey | (string & {}),
|
||||
vars?: Record<string, string>,
|
||||
): string => {
|
||||
let value =
|
||||
dict[key as TranslationKey] ??
|
||||
dictionaries[defaultLocale][key as TranslationKey] ??
|
||||
key;
|
||||
if (vars) {
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
value = value.replaceAll(`{${k}}`, v);
|
||||
|
||||
Reference in New Issue
Block a user