From 1296cf6a32c58000e65eb9bc409e02e96030faf4 Mon Sep 17 00:00:00 2001 From: YiZixuan Date: Tue, 9 Sep 2025 22:38:43 +0800 Subject: [PATCH] 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 --- application/src/contexts/LanguageContext.tsx | 95 +++++++++++++++----- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/application/src/contexts/LanguageContext.tsx b/application/src/contexts/LanguageContext.tsx index 4419d86..31e7662 100644 --- a/application/src/contexts/LanguageContext.tsx +++ b/application/src/contexts/LanguageContext.tsx @@ -1,16 +1,22 @@ import React, { createContext, useContext, useState, ReactNode, useCallback } from "react"; import { translations, Language, TranslationModule, TranslationKey } from "@/translations"; +type TranslationVars = Record; + type LanguageContextType = { language: Language; setLanguage: (language: Language) => void; - t: (key: string, module?: M) => string; + t: { + (key: string): string; + (key: string, vars: TranslationVars): string; + (key: string, module: M): string; + (key: string, module: M, vars: TranslationVars): string; + }; }; -// ❗ Create the context with `undefined` to enforce provider usage const LanguageContext = createContext(undefined); -// ✅ Stable custom hook +// eslint-disable-next-line react-refresh/only-export-components export const useLanguage = () => { const context = useContext(LanguageContext); if (!context) { @@ -23,33 +29,76 @@ export const LanguageProvider = ({ children }: { children: ReactNode }) => { const [language, setLanguage] = useState("en"); const fallbackLanguage: Language = "en"; - const t = useCallback((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 fallbackPack = translations[fallbackLanguage]; + let translatedText = ""; + if (module) { - const valCur = langPack?.[module]?.[key as TranslationKey]; - if (typeof valCur === "string") return valCur; + const moduleKey = key as TranslationKey; + 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]; - if (typeof valEn === "string") return valEn; + for (const mod of Object.keys(langPack) as TranslationModule[]) { + 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) { - const m = mod as TranslationModule; - 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 variables ? interpolate(translatedText, variables) : translatedText; + }) as LanguageContextType['t'], [language]); return (