第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保持前后端统一
  • 关键考量:运行时切换语言的需求、翻译文件的组织方式、团队的技术栈熟悉度

嗯,技术选型这块就聊到这儿。下一章我们聊聊翻译文件的组织策略,那可是个大坑,我在一个项目里因为翻译文件乱放,差点被运维同事追着打。