为20多种语言构建React Native应用:i18n经验
Source: Dev.to
在移动应用中支持二十多种语言并不是一个检查清单项目——它是一项持续的工程承诺,涉及技术栈的每一层:UI 布局、排版、数据存储、API 设计以及发布工作流。
以下是我在构建一款具备广泛多语言支持的语言学习应用时总结的关键经验教训。
i18n 库的选择
对于 React Native,主要选项如下:
| 库 | 亮点 | 大小(gzipped) |
|---|---|---|
| i18next + react‑i18next | 功能完整(命名空间、复数、插值、语言检测) | ~20 KB |
| react‑native‑localize + 自定义方案 | 更底层、可控;适合简单需求 | – |
| expo‑localization | 适用于 Expo‑托管工作流;在裸 RN 中功能受限 | – |
我选择了 i18next 与 react‑i18next。当翻译文件超过约 200 条键时,命名空间支持非常关键——按功能(onboarding、settings、lesson、error)拆分可以保持文件易于管理,并支持懒加载。
// i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { getLocales } from 'expo-localization';
i18n
.use(initReactI18next)
.init({
resources: {
en: { translation: require('./locales/en.json') },
es: { translation: require('./locales/es.json') },
// …
},
lng: getLocales()[0].languageCode ?? 'en',
fallbackLng: 'en',
interpolation: { escapeValue: false },
});
文本扩展:布局杀手
英语是最紧凑的书写语言之一。当你将 UI 文本翻译成德语、芬兰语或葡萄牙语时,按钮和标签可能会溢出。
扩展系数(相对于英语)
| 语言 | 平均文本扩展 |
|---|---|
| German | +25 % – 35 % |
| Finnish | +25 % – 30 % |
| Portuguese | +20 % – 30 % |
| Spanish | +15 % – 25 % |
| French | +15 % – 20 % |
| Japanese | –15 % – 25 %(通常更短) |
| Chinese | –30 % – 40 % |
典型的失效模式
- 按钮换行成两行
- 导航标签被截断
- 表格单元格溢出
- 输入占位符文本被裁剪
我如何降低风险
- 从一开始就使用最坏情况(德语)文本进行设计。
- 强制执行规则:每个 UI 文本在组件视为完成之前必须使用德语翻译进行测试——德语在拉丁字母语言中通常产生最长的字符串。
实际代码示例
// ❌ Bad: fixed‑width button
<Button style={{ width: 120 }}>
{t('save_button')}
</Button>
// ✅ Good: minimum width with flexible growth
<Button style={{ minWidth: 120, paddingHorizontal: 16 }}>
{t('save_button')}
</Button>
adjustsFontSizeToFit 配合合理的 minimumFontScale 可以处理大多数溢出情况而不破坏布局。对于按钮,优先使用 paddingHorizontal 而不是硬编码宽度。
从右到左布局:阿拉伯语和希伯来语不可或缺
阿拉伯语(在北非和中东地区使用)和希伯来语都是从右到左(RTL)书写的脚本。支持它们需要全局的 RTL 标志,而不是逐组件的微调。
import { I18nManager } from 'react-native';
import * as Updates from 'expo-updates';
async function activateRTL() {
if (!I18nManager.isRTL) {
I18nManager.forceRTL(true);
await Updates.reloadAsync(); // app restart required
}
}
注意事项
- RTL 开关会强制重新加载应用——无法即时切换。
- 并非所有第三方组件都遵循 RTL 标志;自定义图标(如尖角、返回箭头)需要手动镜像。
- 阿拉伯文中的数字仍保持从左到右;混合方向的文本可能需要显式的双向控制字符。
- 始终在真实设备上进行测试——模拟器中的 RTL 渲染过去常出现边缘情况。
字体支持:CJK 问题
React Native 的默认字体栈对拉丁文、 Cyrillic(西里尔文)和希腊文的支持相当完善。对于 CJK(中文、日文、韩文),则使用系统字体:
- iOS – 稳定(PingFang SC/TC、Hiragino Sans)
- Android – 不一致;取决于设备厂商和操作系统版本
在语言学习应用中,渲染质量直接影响可读性,因此我们需要一个有保障的字体。
解决方案
- 打包 CJK 字体(例如 Noto Sans CJK、Source Han Sans)。预计会导致 APK 增大 2–5 MB。
- 对于使用 Expo 管理的应用,可使用
@expo-google-fonts/noto-sans-sc(以及相应的其他字体)。
创建一个感知语言的 Text 包装组件,自动选择合适的字体族:
// components/LText.tsx
import { Text, TextProps } from 'react-native';
import { useLanguage } from '../hooks/useLanguage';
const FONT_MAP: Record<string, string> = {
zh: 'NotoSansSC',
ja: 'NotoSansJP',
ko: 'NotoSansKR',
ar: 'NotoSansArabic',
default: 'System',
};
export function LText({ style, ...props }: TextProps) {
const { language } = useLanguage();
const fontFamily = FONT_MAP[language] ?? FONT_MAP.default;
return <Text style={[{ fontFamily }, style]} {...props} />;
}
现在,所有 UI 文本都会自动使用当前语言对应的正确字体。
Source: …
复数形式:不仅仅是 “0、1、多个”
英语只有两种复数形式(单数和复数)。许多语言有更多:
| 语言 | 复数形式数量 |
|---|---|
| 俄语 | 4(one、few、many、other) |
| 阿拉伯语 | 6 |
| 波兰语 | 4(规则与俄语不同) |
i18next 遵循 CLDR(通用地区数据仓库)规则,并使用后缀如 _zero、_one、_two、_few、_many、_other。
示例 – 俄语复数
// locales/ru.json
{
"items_count_one": "{{count}} элемент",
"items_count_few": "{{count}} элемента",
"items_count_many": "{{count}} элементов",
"items_count_other": "{{count}} элемента"
}
代码中的使用方式:
import { useTranslation } from 'react-i18next';
function ItemCounter({ count }: { count: number }) {
const { t } = useTranslation();
return <Text>{t('items_count', { count })}</Text>;
}
i18next 会根据 count 的值以及当前语言的复数规则自动选择正确的键。
TL;DR 检查清单
| ✅ | 项目 |
|---|---|
| i18n 库 | 使用 i18next + react-i18next;按命名空间组织翻译文件。 |
| 布局 | 为德语长度的字符串设计;避免固定宽度;使用 adjustsFontSizeToFit。 |
| RTL | 调用 I18nManager.forceRTL(true) 并重新加载应用;在真实设备上进行测试。 |
| 字体 | 捆绑可靠的 CJK 字体(或使用 Expo Google Fonts);包装 Text 以切换字体族。 |
| 复数形式 | 利用 i18next 基于 CLDR 的复数规则;为每种语言提供所有必需的键。 |
把国际化视为从第一天起的首要任务,就能避免昂贵的后期改造,为全球用户提供精致的使用体验。
i18next 中的复数形式
{
"items_count_zero": "{{count}} предметов",
"items_count_one": "{{count}} предмет",
"items_count_few": "{{count}} предмета",
"items_count_many": "{{count}} предметов",
"items_count_other": "{{count}} предмета"
}
陷阱
- JavaScript 的
Intl.PluralRules是在 i18next 之外进行运行时复数处理的好帮手。- 如果可能,避免在翻译字符串中嵌入数字。让 UI 分别组合数字和复数名词。
- 日期和数字格式是特定于语言环境的。使用
Intl.DateTimeFormat和Intl.NumberFormat—— 切勿硬编码分隔符。
Source: …
翻译工作流:人工问题
技术本地化(i18n)是容易的部分。管理 20 多种语言的翻译是一项运营挑战。
小规模可行的工作流
- 仅使用英文作为源字符串 – 切勿从已有翻译再翻译。类似“传话游戏”的错误会累计。
- 自动化键提取 –
i18next-parser扫描代码库并为译者生成仅包含键的 JSON。 - 翻译记忆库 – 使用 Weblate、Crowdin,或甚至带有翻译记忆脚本的共享 Google 表格,可显著降低成本并提升一致性。
- 机器翻译首轮 + 人工审校 – 对欧洲语言使用 DeepL,对亚洲语言使用 Google Cloud Translation。人工审校可以捕捉机器翻译遗漏的习语错误和上下文不匹配。
- 为译者提供截图上下文 – “back” 这类字符串在没有 UI 截图的情况下容易产生歧义。Crowdin 的嵌入式编辑器或自动截图生成工具可以消除这种歧义。
最糟糕的结果是术语不统一——在不同界面上使用了三个不同的词来表达同一概念,因为三位译者分别在三个不同的界面上工作且没有统一的词汇表。请尽早建立词汇表并强制执行。
性能:懒加载语言环境
打包 20 多个语言文件会累积占用。即使每种语言只有 50 KB,20 种语言也大约是 1 MB 的翻译 JSON,在启动时加载——其中大多数用户根本用不到。
懒加载方案
i18n.use(initReactI18next).init({
partialBundledLanguages: true,
resources: {
en: { translation: require('./locales/en.json') }, // bundle default
},
backend: {
loadPath: `${FileSystem.documentDirectory}locales/{{lng}}/{{ns}}.json`,
},
});
// On language change:
async function switchLanguage(lng: string) {
await downloadLocaleIfNeeded(lng); // fetch from CDN, write to FileSystem
await i18n.changeLanguage(lng);
}
权衡 – 语言切换时的首次加载延迟。接受在首次选择未打包的语言时出现加载状态;后续加载会从文件系统缓存中瞬间完成。
测试问题
大多数项目对 i18n 的自动化测试投入不足。一个最小可行的做法:
- 快照测试:针对每种语言进行快照,以捕获布局回归。
- 字符串长度测试:断言 UI 关键字符串的翻译文本不超过最大长度。
- RTL 冒烟测试:编写一个 E2E 测试,切换到阿拉伯语并验证主要导航流程不出错。
缺失翻译 lint——在 CI 中加入一步,如果任何键在英文 locale 中出现而在其他 locale 中缺失则构建失败。
# CI step using i18next-parser output
for lang in es fr de ja zh ar ko; do
node scripts/check-missing-keys.js --base en --target $lang
done
i18n 债务的增长速度快于大多数技术债务。在 CI 中捕获缺失翻译而不是等到生产环境再发现,值得投入设置成本。
我正在构建 Pocket Linguist,这是一款面向 iOS 的 AI 驱动语言导师。它使用间隔重复、摄像头翻译和对话式 AI,帮助你更快达到日常会话流利度。免费试用。