likes
comments
collection

前端国际化解决方案(i18next)

作者站长头像
站长
· 阅读数 12

i18next 是通用的国际化解决方案,对三大前端框架都有良好的支持:

以 React 为例,讲解其在项目中的使用方法。

基础使用(for 小白)

首先安装所需依赖:

$ yarn add i18next react-i18next i18next-browser-languagedetector

假设我们的项目暂时只支持中英文两种语言,那么就在项目中创建 i18n 文件夹,里面放入 index.tszh/translation.jsonen/translation.json 三个文件:

i18n
├── index.ts
├── zh
│   └── translation.json
└── en
    └── translation.json

其中两个 JSON 文件是国际化数据,例如 zh/translation.json文件内容为:

{
  "header": {
    "title": "主标题",
    "subtitle": "副标题"
  },
  "footer": {
    "about": "关于",
    "feedback": "反馈"
  }
}

en/translation.json 文件内容为:

{
  "header": {
    "title": "Title",
    "subtitle": "Subtitle"
  },
  "footer": {
    "about": "About",
    "feedback": "Feedback"
  }
}

然后在 index.ts中编写代码如下:

import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import en from './en/translation.json'
import zh from './zh/translation.json'

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: { translation: en },
      zh: { translation: zh },
    },
    fallbackLng: 'en',
    debug: true,
    react: {
      useSuspense: false,
    },
    detection: {
      order: ['querystring', 'navigator', 'localStorage'],
      lookupQuerystring: 'lang',
    },
  })

export default i18n

这里解释几个参数:

  • LanguageDetector 是一个插件,帮助用户探测当前的语言环境,提供了很多种探测方式,包括从 URL 的查询参数中取值、使用浏览器语言或者本地存储等,它的参数被放在了下面 init 参数的 detection 字段里面
  • resources 对象指定了当前应用的国际化语言配置,该对象的 key 就是设置的语言字段,value 也是一个对象,key 表示命名空间,value 是该空间下的文本翻译键值对。
  • fallbackLng 指定了未匹配任何语言时的默认语言

更多的参数可以参考官方文档。接下来就可以在组件中使用了:

import './App.css'
import { useTranslation } from 'react-i18next'

function App() {
  const { t } = useTranslation()
  return (
    <div className="App">
      <h2>{t('header.title')}</h2>
      <footer>{t('footer.about')}</footer>
    </div>
  )
}

export default App

会自动根据语言环境匹配中英文,并输出对应的文案。

命名空间(for 高级读者)

对于小型项目来说,上面的配置已经够用了,但是对于多人协作的大型项目,所有人都改一个多语言配置文件的话,很容易发生冲突,这个时候命名空间的重要性就体现出来了,我们把目录结构改造一下:

i18n
├── index.ts
├── zh
│   ├── home.json
│   └── product.json
└── en
    ├── home.json
    └── product.json

其中 homeproduct就是我们定义的命名空间了,对应 index.ts稍微改造一下:

import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import enHome from './en/home.json'
import enProduct from './en/product.json'
import zhHome from './zh/home.json'
import zhProduct from './en/product.json'

i18n
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources: {
      en: { home: enHome, product: enProduct },
      zh: { home: zhHome, product: zhProduct },
    },
    ...
  })

export default i18n

然后在使用的时候,可以指定命名空间:

import './App.css'
import { useTranslation } from 'react-i18next'

function App() {
  const { t } = useTranslation(['home', 'product'])
  return (
    <div className="App">
      <h2>{t('product:header.title')}</h2>
      <footer>{t('footer.about')}</footer>
    </div>
  )
}

export default App

在初始化的时候,可以设置命名空间列表(ns)并指定默认的命名空间(defaultNS):

i18next.init({
  ns: ['common', 'moduleA', 'moduleB'],
  defaultNS: 'moduleA'
}

i18next.t('myKey') // 使用默认命名空间
i18next.t('common:myKey') // 指定 common 命名空间
i18next.t('myKey', { ns: 'common' }) // 指定 common 命名空间(推荐写法)

自定义语言探测逻辑(for 资深专家)

其实国际化最关键的一环是语言环境的探测, i18next-browser-languageDetector 这个库提供了非常全面的探测逻辑:

前端国际化解决方案(i18next)

从代码中可以看到,内置了 8 种语言探测器,根据用户设置的 order 顺序依次调用探测器函数获取探测结果,并选择第一个匹配到的结果,例如:

detection: {
  order: ['querystring', 'navigator'],
  lookupQuerystring: 'lang',
}

如果没有在 url 中手动设置 lang=xxx语言参数,则会取 navigator.language的值,如果我们设置了则会拿到 xxx这个值:

前端国际化解决方案(i18next)

注意看,它的 detected 列表是 ['xxx', 'zh-CN', 'zh', 'en', 'ja', 'zh-CN'],你可能会疑问后面的值是哪里来的,实际上是 navigator 这个探测器加进去的,对照源码就知道了:

前端国际化解决方案(i18next)

先把 navigator.languages数组加进去,然后再把 navigator.languages加进去,所以会有重复的 zh-CN,但是需要注意,虽然 xxx不是合法的语言,但是并不会丢掉它选择 zh-CN,而是会走到退守策略,有三种设置方式:

// 直接设置退守语言为中文,只要没匹配上都是中文
fallbackLng: 'zh',
// 只设置 xxx 的退守语言是中文,其他不设置
fallbackLng: {
  xxx: ['zh'],
},
// 自己处理字符串,返回指定退守语言
fallbackLng: (code) => {
  if (code === 'xxxx') return 'zh'
  return 'en'
},

可以看到是非常灵活方便的,这里有一点需要强调,除了退守策略之外还有最优匹配策略,例如我们设置 lang=zh-xxx,匹配到 zh-xxx非合法的语言,但是这个时候会自动选择中文,因为内部还有一个逻辑就是对 -进行分割,取前面的值进行匹配。

虽然 i18n 库可以覆盖大部分场景,也有不能满足业务需要的时候,举个例子:在 Electron 开发的桌面应用里面,我们希望加载语言和设置语言的逻辑是从本地文件中读取用户上次设置的值,这个时候就需要自定义语言探测逻辑了。

写一个自定义的探测器也比较简单,上面的 navigator 探测器的代码很简单,就是按照下面的模板来的:

import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'

const myDetector = {
  name: 'myDetectorName',
  // 探测逻辑
  lookup(options) {
    return 'en';
  },
  // 保存逻辑
  cacheUserLanguage(lng, options) {
    
  },
}
const languageDetector = new LanguageDetector()
languageDetector.addDetector(myDetector)
i18next.use(languageDetector).init({
  detection: options,
})

参照 i18next-browser-languagedetector 的源码分分钟可以写一个,但是这个库有个很大的缺陷:不支持异步探测,我们可以使用更高级的方式来写探测插件,格式如下:

{
  type: 'languageDetector',
  async: true, // 是否异步
  init: function(services, detectorOptions, i18nextOptions) {
    /* use services and options */
  },
  detect: function(callback) {
    /* return detected language */
    // callback('de'); if you used the async flag
    return 'de';
  },
  cacheUserLanguage: function(lng) {
    /* cache language */
  }
}

这种方式既可以支持同步探测,也可以支持异步探测,只需要定义 detectcacheUserLanguage两个方法即可,示例代码如下:

import i18n, { ModuleType } from 'i18next'

// 异步探测
const detect = async (callback) => {
  // 这里可以做读本地文件等异步操作
  setTimeout(() => callback('zh'), 5000)
}

// 保存用户设置
const cacheUserLanguage = (lng) => {
  setting.set('locale', lng)
}

export const AsyncDetector = {
  type: 'languageDetector' as ModuleType,
  async: true,
  init: Function.prototype,
  detect,
  cacheUserLanguage,
}

i18n
  .use(AsyncDetector)
  .init({ /*...*/ })

最终的效果是拿到异步探测结果之后立即更新语言。

写在最后(个人建议)

整个用下来的感觉就是 i18next 配合 react-i18next 来做国际化非常赞,完美支持类组件和函数组件。配置国际化语言的时候有 flat 和 nested 两种方式:

前端国际化解决方案(i18next)

虽然用 i18n.t('header.title')都能拿到结果,且 nested 方式更精简一些,但我个人更推荐 flat 的方式,因为维护起来更方便,设想当你的同事改文案的时候,只要全局搜索一下 header.title 字符串就可以定位,如果写成 nested 方式就比较吃力了。