第2章:技术选型——前端框架的i18n库对比
做国际化,第一步就是选工具。前端这块,React、Vue、Angular三足鼎立,各自的i18n方案也完全不同。我这些年三个框架都折腾过,踩过不少坑,今天把心得掰开了讲。
2.1 React生态:react-i18next
React的国际化方案,我个人最推荐react-i18next。它基于i18next核心库,生态非常成熟。
核心优势:
- 支持命名空间,大型项目按模块拆分翻译文件
- 支持延迟加载,翻译文件可以按需加载
- 支持插值、复数、日期格式化
- TypeScript支持完善
来个最简单的例子:
// 安装
npm install react-i18next i18next
// 配置
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
i18n.use(initReactI18next).init({
resources: {
en: { translation: { "hello": "Hello" } },
zh: { translation: { "hello": "你好" } }
},
lng: 'zh',
fallbackLng: 'en'
});
// 组件中使用
function App() {
const { t } = useTranslation();
return <h1>{t('hello')}</h1>;
}
我的经验:react-i18next的命名空间功能特别好用。我在一个电商项目里,把翻译拆成了common、order、payment三个命名空间,维护起来清爽多了。
2.2 Vue生态:vue-i18n
Vue的官方方案是vue-i18n,目前已经到v9版本,支持Vue 3的Composition API。
说实话,Vue的国际化比React更「开箱即用」。你想想看,Vue本身就有模板语法,vue-i18n直接提供了自定义指令,用起来很顺手。
// 安装
npm install vue-i18n@9
// 配置
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'zh',
fallbackLocale: 'en',
messages: {
en: { hello: 'Hello' },
zh: { hello: '你好' }
}
});
app.use(i18n);
// 模板中使用
<template>
<p>{{ $t('hello') }}</p>
</template>
注意:vue-i18n v8和v9的API变化很大。我曾经在一个老项目上升级,结果一堆$t调用报错。如果你在用Vue 2,老老实实用v8版本。
2.3 Angular生态:@angular/localize
Angular的国际化方案比较特殊。它用的是@angular/localize,走的是编译期处理路线。
什么意思呢?就是你在模板里写i18n标记,Angular编译时会自动提取翻译,生成多语言版本。这种方式性能好,但灵活性差一些。
// 模板中标记
<h1 i18n="@@welcomeMessage">Welcome</h1>
// 提取翻译文件
ng extract-i18n --output-path src/locale
// 生成的messages.xlf文件
<trans-unit id="welcomeMessage">
<source>Welcome</source>
<target>欢迎</target>
</trans-unit>
三框架对比总结:
| 特性 | React (react-i18next) | Vue (vue-i18n) | Angular (@angular/localize) |
|---|---|---|---|
| 学习成本 | 中等 | 低 | 高 |
| 运行时切换语言 | 支持 | 支持 | 不支持(需重新编译) |
| 按需加载 | 支持 | 支持 | 不支持 |
| TypeScript支持 | 优秀 | 良好 | 优秀 |
| 大型项目适用性 | 优秀 | 良好 | 良好 |
2.4 后端语言国际化支持
前端搞定了,后端也不能落下。后端国际化主要解决两个问题:错误信息多语言、邮件模板多语言。
Java:ResourceBundle + Spring国际化
Java的国际化方案很成熟。JDK自带的ResourceBundle,配合Spring的MessageSource,用起来很稳。
// messages_zh_CN.properties
user.notfound=用户不存在
// messages_en_US.properties
user.notfound=User not found
// Spring配置
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("messages");
source.setDefaultEncoding("UTF-8");
return source;
}
// 使用
@Autowired
private MessageSource messageSource;
public String getMessage(String code, Locale locale) {
return messageSource.getMessage(code, null, locale);
}
我的建议:Java项目里,把错误码和国际化消息分开管理。错误码用枚举,消息用properties文件。这样前端可以根据错误码显示对应的文案,后端只管返回错误码就行。
Go:golang.org/x/text
Go的国际化库相对轻量。官方提供了golang.org/x/text,支持消息格式化、复数规则等。
import (
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func main() {
p := message.NewPrinter(language.Chinese)
p.Printf("你好,%s!", "世界")
// 复数处理
p.Printf("你有 %d 条消息", 5)
}
说实话,Go的国际化生态不如Java丰富。我在一个Go微服务项目里,最后自己封装了一层,用JSON文件管理翻译,配合中间件根据请求头切换语言。
Node.js:i18next
Node.js这边,前端用的i18next在后端也能用。前后端统一库,维护起来省心不少。
const i18next = require('i18next');
const Backend = require('i18next-fs-backend');
i18next.use(Backend).init({
lng: 'zh',
fallbackLng: 'en',
backend: {
loadPath: './locales/{{lng}}/{{ns}}.json'
}
});
// Express中间件
app.use((req, res, next) => {
req.t = i18next.getFixedT(req.headers['accept-language']);
next();
});
避坑指南:我曾经在Node后端用i18next时,忘了处理并发问题。多个请求同时修改i18next的locale配置,导致用户A看到的是用户B的语言。记住,每个请求都要创建独立的t函数实例,别共用全局配置。
2.5 我的选型建议
说了这么多,到底怎么选?我个人的经验是:
- 前端:React项目无脑react-i18next,Vue项目用vue-i18n v9,Angular项目如果不需要运行时切换语言,用@angular/localize没问题
- 后端:Java项目用Spring MessageSource,Go项目自己封装一层,Node项目用i18next保持前后端统一
- 关键考量:运行时切换语言的需求、翻译文件的组织方式、团队的技术栈熟悉度
嗯,技术选型这块就聊到这儿。下一章我们聊聊翻译文件的组织策略,那可是个大坑,我在一个项目里因为翻译文件乱放,差点被运维同事追着打。