我如何将缺失的翻译变成编译时的 TypeScript 错误
Source: Dev.to
运行时缺失翻译的问题
大多数 React i18n 库只在运行时捕获缺失的翻译,导致用户在开发者注意之前就看到破碎的 UI。
使用基于键的库(如 react‑i18next)的示例:
const { t } = useTranslation();
return {t("welcome.message")};
// en.json
{
"welcome": {
"message": "Welcome back!"
}
}
// es.json
{
"welcome": {}
// oops – forgot this one
}
TypeScript 无法知道 es.json 中缺少 "welcome.message"。西班牙语用户会看到原始键字符串,问题只有在切换语言时才会显现。有些库提供键映射的代码生成,但这会增加构建步骤,并且只能保证键的存在,无法保证实际内容。
编译时解决方案:react-scoped-i18n
react-scoped-i18n 不使用引用外部文件的字符串键,而是直接将翻译作为普通对象字面量传递给 t() 函数:
const { t } = useI18n();
return (
{t({
en: `Welcome back, ${name}!`,
es: `¡Bienvenido de nuevo, ${name}!`,
})}
);
参数是普通对象,TypeScript 可以对其进行完整的类型检查。
设置 Provider
一次性创建 i18n 配置:
// i18n/index.ts
import { createI18n } from "react-scoped-i18n";
export const { useI18n, I18nProvider } = createI18n({
languages: ["en", "es", "sl"],
defaultLanguage: "en",
});
createI18n 会根据 languages 数组生成类型:
type Language = "en" | "es" | "sl";
type Translations = Record;
t() 函数期望一个 Translations 对象,这意味着 每个语言键都必须存在。遗漏任意语言会触发 TypeScript 错误:
return (
{t({
en: `Welcome back, ${name}!`,
es: `¡Bienvenido de nuevo, ${name}!`,
// TypeScript Error: Property 'sl' is missing in type
// '{ en: string; es: string; }' but required in type 'Translations'
})}
);
无需代码生成、构建步骤或插件——类型约束直接来源于你的配置。
处理复数形式
复数形式使用 tPlural() 管理,同样要求每种语言的完整性:
const { tPlural } = useI18n();
return (
{tPlural(count, {
en: {
one: `You have one apple.`,
many: `You have ${count} apples.`,
},
es: {
one: `Tienes una manzana.`,
many: `Tienes ${count} manzanas.`,
},
sl: {
one: `Imaš eno jabolko.`,
two: `Imaš dve jabolki.`, // Slovenian dual form
many: `Imaš ${count} jabolk.`,
},
})}
);
在类型层面,所有可能的类别(negative、zero、one、two、many 等)对每种语言都是可用的;你只需定义实际需要的形式。这使得支持双数、六元复数或其他复杂复数规则的语言时,无需硬编码。
权衡与适用场景
-
优点
- 编译时保证每个受支持语言都有对应的翻译。
- 无需外部代码生成或构建时工具。
- 编写组件时即可获得即时反馈。
-
缺点
- 内联翻译不适合依赖外部翻译平台(Crowdin、Lokalise 等)的工作流。
- 随着支持语言数量的增加,组件文件会变得更大、更嘈杂。三到五种语言还算舒适,十种就会显得沉重。
对于自行维护翻译的小团队来说,这种权衡通常是值得的。
试一试
react-scoped-i18n 是开源项目,代码托管在 GitHub: