feat(i18n): Enhanced internationalization support with variable interpolation

- Refactored the `t` function in LanguageContext to support multiple parameter combinations
- Added variable interpolation feature, allowing variables in translated texts
- Optimized translation logic to improve accuracy and performance
- Enhanced type safety by using TypeScript generics to ensure correct typing
This commit is contained in:
YiZixuan
2025-09-09 22:38:43 +08:00
parent b6d4ca5269
commit 1296cf6a32
+72 -23
View File
@@ -1,16 +1,22 @@
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react"; import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
import { translations, Language, TranslationModule, TranslationKey } from "@/translations"; import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
type TranslationVars = Record<string, string | number>;
type LanguageContextType = { type LanguageContextType = {
language: Language; language: Language;
setLanguage: (language: Language) => void; setLanguage: (language: Language) => void;
t: <M extends TranslationModule>(key: string, module?: M) => string; t: {
(key: string): string;
(key: string, vars: TranslationVars): string;
<M extends TranslationModule>(key: string, module: M): string;
<M extends TranslationModule>(key: string, module: M, vars: TranslationVars): string;
};
}; };
// ❗ Create the context with `undefined` to enforce provider usage
const LanguageContext = createContext<LanguageContextType | undefined>(undefined); const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
// ✅ Stable custom hook // eslint-disable-next-line react-refresh/only-export-components
export const useLanguage = () => { export const useLanguage = () => {
const context = useContext(LanguageContext); const context = useContext(LanguageContext);
if (!context) { if (!context) {
@@ -23,33 +29,76 @@ export const LanguageProvider = ({ children }: { children: ReactNode }) => {
const [language, setLanguage] = useState<Language>("en"); const [language, setLanguage] = useState<Language>("en");
const fallbackLanguage: Language = "en"; const fallbackLanguage: Language = "en";
const t = useCallback(<M extends TranslationModule>(key: string, module?: M): string => { const interpolate = (template: string, vars: TranslationVars): string => {
return template.replace(/{(\w+)}/g, (match, key) => {
return vars[key] !== undefined ? String(vars[key]) : match;
});
};
const t = useCallback(((
key: string,
moduleOrVars?: TranslationModule | TranslationVars,
vars?: TranslationVars
): string => {
let module: TranslationModule | undefined;
let variables: TranslationVars | undefined;
if (typeof moduleOrVars === "string") {
module = moduleOrVars;
variables = vars;
} else if (typeof moduleOrVars === "object") {
variables = moduleOrVars;
}
const langPack = translations[language]; const langPack = translations[language];
const fallbackPack = translations[fallbackLanguage]; const fallbackPack = translations[fallbackLanguage];
let translatedText = "";
if (module) { if (module) {
const valCur = langPack?.[module]?.[key as TranslationKey<M>]; const moduleKey = key as TranslationKey<typeof module>;
if (typeof valCur === "string") return valCur; const valCur = langPack?.[module]?.[moduleKey];
if (typeof valCur === "string") {
translatedText = valCur;
} else {
const valEn = fallbackPack?.[module]?.[moduleKey];
translatedText = typeof valEn === "string" ? valEn : key;
}
} else {
let found = false;
const valEn = fallbackPack?.[module]?.[key as TranslationKey<M>]; for (const mod of Object.keys(langPack) as TranslationModule[]) {
if (typeof valEn === "string") return valEn; const moduleTranslations = langPack[mod];
if (moduleTranslations && key in moduleTranslations) {
const val = moduleTranslations[key as keyof typeof moduleTranslations];
if (typeof val === "string") {
translatedText = val;
found = true;
break;
}
}
}
return key; if (!found) {
for (const mod of Object.keys(fallbackPack) as TranslationModule[]) {
const moduleTranslations = fallbackPack[mod];
if (moduleTranslations && key in moduleTranslations) {
const val = moduleTranslations[key as keyof typeof moduleTranslations];
if (typeof val === "string") {
translatedText = val;
found = true;
break;
}
}
}
}
if (!found) {
translatedText = key;
}
} }
for (const mod in langPack) { return variables ? interpolate(translatedText, variables) : translatedText;
const m = mod as TranslationModule; }) as LanguageContextType['t'], [language]);
const val = langPack[m]?.[key as any];
if (typeof val === "string") return val;
}
for (const mod in fallbackPack) {
const m = mod as TranslationModule;
const val = fallbackPack[m]?.[key as any];
if (typeof val === "string") return val;
}
return key;
}, [language]);
return ( return (
<LanguageContext.Provider value={{ language, setLanguage, t }}> <LanguageContext.Provider value={{ language, setLanguage, t }}>