i18next 用法及源码学习

298 阅读8分钟

介绍

i18next

image-20250109100139149

image-20250109100232866

基本用法

初始化
init

i18next.init(options, callback) // -> returns a Promise

所有翻译加载完毕后或失败时(使用后端的情况下)出现错误时,将调用回调。

import i18next from 'i18next';
​
i18next.init({
  lng: 'en', // 如果你使用 language detector 插件, 不需要定义 lng 选项
  resources: {
    en: {
      translation: {
        "key": "hello world"
      }
    }
  }
}, (err, t) => {
  if (err) return console.log('something went wrong loading', err);
  t('key'); // -> 与 i18next.t 相同
});
​
​
// 返回期约
i18next
  .init({ /* options */ })
  .then(function(t) { t('key'); });
use

i18next.use(module)

使用函数可以将附加插件加载到 i18next,use 方法要在init以前调用。

import i18next from 'i18next';
​
// 用于从后端请求翻译文件
import Backend from 'i18next-http-backend'; 
​
// 用于在浏览器环境中将语言文件缓存到浏览器的 localStorage,避免每次用户访问页面时都需要重新加载语言文件,并且可以缓存当前语言设置。
import Cache from 'i18next-localstorage-cache'; 
​
// 语言探测器,用于探测浏览器环境当前语言
import LanguageDetector from 'i18next-browser-languagedetector';    
​
i18next
  .use(Backend)
  .use(Cache)
  .use(LanguageDetector)
  .init(options, callback);
使用翻译
// en
{
    "translation": {
        "Hello": "hello world"  // 表示默认命名空间下,有一条 key 为 "Hello" 的翻译
    },
    "namespace1": {
        "Hi": "hi"
    }
}
i18next.t('Hello');     // hello world
i18next.t('Hi', { ns: 'namespace1' });      // hi
深层嵌套
{
    "look": {
        "deep": "value of look deep"
    }
}
i18next.t('look.deep');     // value of look deep
传递默认值

如果找不到这条翻译,就会使用默认值显示,如果没有传递默认值,并且找不到翻译,就会把 key 直接显示

i18next.t('key', 'default value to show');  //  default value to show
i18next.t('key', { defaultValue: 'default value to show' });    // default value to show

如果您使用saveMissing 功能,它还会将 defaultValue 传递给您选择的后端

多个后备键
{
    "error": {
      "unspecific": "Something went wrong.",
      "404": "The page was not found."
    }
}
t(['error.504', 'error.unspecific']);   // Something went wrong.
概述选项

i18next.t(key, options)

选项类型描述
defaultValuestring如果未找到翻译,则返回 defaultValue
lngstring覆盖要使用的语言
lngsstring[]覆盖要使用的语言,使用优先匹配到翻译的第一个语言
fallbackLngstring如果未找到,则覆盖语言以查找关键字

自动渲染

useTranslation

useTranslation hook,使用其返回的t方法显示翻译,可以在切换语言时自动渲染,更新翻译。

以前的 react-native-i18n 是通过 mobx 状态更新引起页面翻译渲染的。

const MyScreen = () => {
  const {t} = useTranslation();
    
  return (
      <Text style={styles.text}>{t('Welcome to i18next')}</Text>    // 切换语言后会自动替换为新语言的翻译
  );
};
​
export default withTranslation()(MyScreen);
withTranslation

如果是类组件,或是不想用 useTranslation hook,想直接用i18next.t(...),可以使用高阶组件withTranslation,也可以在切换语言时自动渲染。

class MyScreen extends Component {
  render() {
    return (
        <Text style={styles.text}>{i18next.t('Welcome to i18next')}</Text>  // 切换语言后会自动替换为新语言的翻译
    );
  }
};
​
export default withTranslation()(MyScreen); // 高阶组件包裹

复数

单数/复数

i18next 会根据 count 的数量,来决定显示单数还是复数,即显示 keykey_other

需要 intl 的支持,由于 React Native 环境没有 intl,所以需要安装 intl 依赖

// en
{
    "person": "Person",
    "person_other": "People"    // 使用 key_other 的形式,表示复数
}// zh
{
    "person": "一个人",
    "person_other": "多个人"
}
t('person', {count: 1}; // 一个人 | Person
t('person', {count: 3}; // 多个人 | People
具有多个复数的语言
//en
{
    "apple_zero": "Zero apple",
    "apple_one": "An apple",
    "apple_other": "Many apple"
}// zh
{
    "apple_zero": "没有苹果",
    "apple_one": "一个苹果",
    "apple_other": "许多苹果"
}
t('apple', {count: 1});     // 一个苹果 | An apple
t('apple', {count: 0});     // 没有苹果 | Zero apple
t('apple', {count: 11});    // 许多苹果 | Many apple
区间复数
{
    "pen_interval": "(1)[一支笔];(2-7)[几支笔];(7-inf)[一堆笔];"     // 采用 (范围区间)[翻译]; 的格式,将会显示 count 匹配到范围的翻译
}
import i18next from 'i18next';
import intervalPlural from 'i18next-intervalplural-postprocessor';  // 区间复数插件
​
i18next
  .use(intervalPlural)
  .init(i18nextOptions);
t('pen_interval', {count: 1, postProcess: 'interval'});     // 一支笔
t('pen_interval', {count: 6, postProcess: 'interval'});     // 几支笔
t('pen_interval', {count: 10, postProcess: 'interval'});    // 一堆笔

插值

基本
{
    "key": "{{what}} is {{how}}"
}
i18next.t('key', { what: 'i18next', how: 'great' });    // i18next is great
传入对象
{
    "key": "I am {{author.name}}"
}
const author = { 
    name: 'Jan',
    github: 'jamuhl'
};
i18next.t('key', { author });   // -> I am Jan
转义
{
    "keyEscaped": "no danger {{myVar}}",
    "keyUnescaped": "dangerous {{- myVar}}"
}
i18next.t('keyEscaped', { myVar: '<img />' });      // -> "no danger &lt;img &#x2F;&gt;"
​
i18next.t('keyUnescaped', { myVar: '<img />' });    // -> "dangerous <img />"
​
i18next.t('keyEscaped', { myVar: '<img />', interpolation: { escapeValue: false } });   // -> "no danger <img />" (obviously could be dangerous)
附加选项

可以传入 interpolation 对象来附加选项:

选项默认值描述
escape/转义函数function escape(str) { return str; }
escapeValuetrue转义传入的值以避免 XSS 注入
useRawValueToEscapefalse如果为真,则传递到转义函数的值不会转换为字符串,而是与执行其自身类型检查的自定义转义函数一起使用
prefix"{{"插值前缀
suffix"}}"插值后缀

嵌套

可以使用$t(...)的表达式,在翻译中嵌套翻译。

{
    "nesting1": "1 $t(nesting2)",
    "nesting2": "2 $t(nesting3)",
    "nesting3": "3",
}
i18next.t('nesting1');      // 1 2 3
将选项传递给嵌套
{
    "girlsAndBoys": "They have $t(girls, {"count": {{girls}} }) and $t(boys, {"count": {{boys}} })",
    "boys": "{{count}} boy",
    "boys_other": "{{count}} boys",
    "girls": "{{count}} girl",
    "girls_other": "{{count}} girls",
}
i18next.t('girlsAndBoys', {girls: 3, boys: 2});     // They have 3 girls and 2 boys

Formatting

i18next>=21.3.0开始,您可以使用基于Intl API的内置格式化函数。

这个需要icuInternational Components for Unicode)的支持,但是 react-native 的 js 环境中没有full-icu(浏览器环境是有的),所以,需要手动引入Polyfill

ICU是一个跨平台的基于 Unicode 的全球化库

// 引入 en 的 icu
import 'intl/locale-data/jsonp/en';
示例

数字格式化

// en.json
{
    "intlNumber": "Some {{val, number}}",
}
t('intlNumber', {
    val: 1000.1,
    formatParams: {val: {minimumFractionDigits: 3}},
}); // Some 1,000.100

插件

插件的类型有以下:

backend、logger、languageDetector、i18nFormat、postProcessor、formatter、3rdParty

插件类型功能常见场景
Backend加载翻译资源从文件系统或 HTTP 服务中加载翻译文件
Logger提供调试和监控功能打印错误、警告或调试信息
LanguageDetector自动检测用户语言根据浏览器、URL、Cookie 自动选择语言
i18nFormat自定义翻译格式化逻辑支持 ICU 格式或其他复杂语法
PostProcessor翻译后进一步处理结果添加前缀、后缀或修改翻译结果
Formatter提供复杂的插值和格式化处理格式化日期、数字或其他动态内容
3rdParty与第三方库或框架集成React、Vue、Angular 等框架的集成插件
i18next-http-backend

通过这个插件可以实现HTTP异步加载翻译,也可以配合 saveMissing,自动上传缺失翻译。

如果不设置加载路径或是 request 方法则是异步拿/locales/{{lng}}/{{ns}}.json路径的翻译文件。

momoko目前加载翻译的优先级为:

  1. I18n.translations数组
  2. 本地缓存
  3. crowdin

由于 react-native 运行环境不支持通过 HTTP的方式获取文件,因此需要借助react-native-fs自己实现一个类似的库,配合momoko的翻译加载逻辑。

命名空间

命名空间是 i18next 国际化框架中的一项功能,可让您分离加载到多个文件中的翻译。

语义原因

通常,您希望将一些部分分开,因为它们属于同一类。我们在大多数项目中都这样做,例如:

  • common.json -> 随处可重复使用的内容,例如按钮标签“保存”、“取消”
  • validation.json -> 所有验证文本
  • glossary.json -> 我们希望在文本中重复使用的单词
技术/编辑原因

更常见的情况是,您不想预先加载所有翻译,或者至少减少加载量。

初始化
i18next.init({
  ns: ['common', 'moduleA', 'moduleB'],
  defaultNS: 'common'
});
​
// 初始化后加载其他命名空间
i18next.loadNamespaces('anotherNamespace', (err, t) => { /* ... */ });
翻译目录
translate
├── zh
│   ├── common.json
│   └── moduleA.json
│   └── moduleB.json
└── en
    ├── common.json
    └── moduleA.json
    └── moduleB.json
使用
i18next.t('myKey'); // common 命名空间内的键(默认命名空间)可以直接使用
i18next.t('moduleA:key');   // 使用 moduleA 命名空间中的键
i18next.t('key', { ns: 'moduleA' });    // 使用 moduleA 命名空间中的键

源码分析

目的:

  1. 分析 i18next 如何加载翻译文件?
  2. init方法中传resources 和使用backend有何区别?
  3. 命名空间的延迟加载流程,何时加载?
  4. 语言变了,react-i18next是如何触发页面渲染的呢?
如何加载翻译
  • init时传入了resources,会将resources作为参数,构造ResourceStore对象
class I18n {
    init(options = {}, callback) {
      // 用 resources 构建 ResourceStore 实例
      this.store = new ResourceStore(this.options.resources, this.options);
    }
}
  • ResourceStore中构建实例,会将resources作为成员变量data保存起来,在getResource方法中,会从data中查找翻译。
class ResourceStore {
  constructor(data, options = { ns: ['translation'], defaultNS: 'translation' }) {
    // ResourceStore 中把它作为成员变量 data 保存起来
    this.data = data || {};
  }
​
    // 查找翻译
  getResource(lng, ns, key, options = {}) {
    let path;
    if (lng.indexOf('.') > -1) {
      path = lng.split('.');
    } else {
      // path 最终是构造成 [lng, ns, key0, key1, ...] 的字符串数组
      path = [lng, ns]; 
      if (key) {
        if (Array.isArray(key)) {
          path.push(...key);
        } else if (isString(key) && keySeparator) {
          path.push(...key.split(keySeparator));
        } else {
          path.push(key);
        }
      }
    }
      
    // 从 data 里面根据 path 数组,拿到对应的翻译
    const result = getPath(this.data, path);
  }
}
  • 调用i18nextt,会走到Translator实例中的translate方法。在这个方法中会调用实例中的resolve方法,在里面遍历defaultNSkey中解析到的namespaces,从而去调用实例中的getResource方法,也这个方法内最终会调用resourceStore中的getResource
class Translator {
  // 其实就是 i18next.t
  translate(keys, options, lastKey) {
     const resolved = this.resolve(keys, options);
     let res = resolved?.res;
     return res;
  }
     
  resolve(keys, options = {}) {
    let found;
    const extracted = this.extractFromKey(k, options);
    let namespaces = extracted.namespaces;
    namespaces.forEach((ns) => {
      found = this.getResource(code, ns, possibleKey, options);
    });
    return { res: found, usedKey, exactUsedKey, usedLng, usedNS };
  }
  
  // 从 resourceStore 中获取翻译
  getResource(code, ns, key, options = {}) {
    if (this.i18nFormat?.getResource) 
        return this.i18nFormat.getResource(code, ns, key, options);
    return this.resourceStore.getResource(code, ns, key, options);
  }
}

总结:在init中传入resources,会被构建成resourceStore中的data,在调用t方法时,从这个data中获取对应的翻译。

使用了backend的翻译加载流程
  • 首先看i8next-http-backend的代码,里面有一个getDefaults方法,是用来获取默认选项的。其中loadPathaddPath分别对应默认加载路径和上传路径。
  • 此外默认选项中还有一个request方法,是用来调用http请求的。
  • readreadMulti方法内部调用了_readAny方法,最终走到loadUrl方法,在里面调用选项中的request方法,并且在响应完成后调用callback
// i18next-http-backend
import request from './request.js'class Backend {
    const getDefaults = () => {
      return {
        ...,
        loadPath: '/locales/{{lng}}/{{ns}}.json',
        addPath: '/locales/add/{{lng}}/{{ns}}',
        request,
      }
    }
    // 初始化插件,在 i18next 的 init 方法执行时执行
    init: (services, options = {}, allOptions = {}) => void ;
​
    // 读取多个翻译文件
    readMulti: (languages, namespaces, callback) => void;
​
    // 读取单个翻译文件
    read: (language, namespace, callback) => void;
    
    // 读取翻译
    _readAny: (languages, loadUrlLanguages, namespaces, loadUrlNamespaces, callback) => {
      let loadPath = this.options.loadPath;
      loadPath = makePromise(loadPath);
      loadPath.then(resolvedLoadPath => {
        this.loadUrl(url, callback, loadUrlLanguages, loadUrlNamespaces);
      });
    };
    
    // 调用 http 请求,获取翻译文件
    loadUrl: (url, callback, languages, namespaces) => {
      this.options.request(this.options, url, payload, (err, res) => {
        if (res && ((res.status >= 500 && res.status < 600) || !res.status)) 
            return callback('failed loading ' + url + '; status code: ' + res.status, true /* retry */)
        if (res && res.status >= 400 && res.status < 500) 
            return callback('failed loading ' + url + '; status code: ' + res.status, false /* no retry */)
          let ret;
          try {
            if (typeof res.data === 'string') {
            ret = this.options.parse(res.data, languages, namespaces)
          } else { // fallback, which omits calling the parse function
            ret = res.data
          }
          callback(null, ret);
        }
      });
    };
    
    // 调用 http 请求,上传翻译文件
    create: (languages, namespace, key, fallbackValue, callback) => void;
    
    reload: () => void;
}
  • 默认的request方法,里面会根据环境中支持fetchXMLHttpRequest,调用http请求。
// request.js // 判断环境中是否有 fetch 方法
let fetchApi
if (typeof fetch === 'function') {
  if (typeof global !== 'undefined' && global.fetch) {
    fetchApi = global.fetch
  } else if (typeof window !== 'undefined' && window.fetch) {
    fetchApi = window.fetch
  } else {
    fetchApi = fetch
  }
}
​
// 判断环境中是否支持 XMLHttpRequest
export function hasXMLHttpRequest () {
  return (typeof XMLHttpRequest === 'function' || typeof XMLHttpRequest === 'object')
}
​
const request = (options, url, payload, callback) => {
  if (typeof payload === 'function') {
    callback = payload
    payload = undefined
  }
  callback = callback || (() => {})
​
  if (fetchApi && url.indexOf('file:') !== 0) {
    // use fetch api
    return requestWithFetch(options, url, payload, callback)
  }
​
  if (hasXMLHttpRequest() || typeof ActiveXObject === 'function') {
    // use xml http request
    return requestWithXmlHttpRequest(options, url, payload, callback)
  }
​
  callback(new Error('No fetch and no xhr implementation found!'))
}
​
export default request;
  • i18next 在init方法会将所有Backend插件构建成一个BackendConnector实例。
class I18n {
    use(module) {
        if (!module) 
            throw new Error('You are passing an undefined module! Please check the object you are passing to i18next.use()')
        if (!module.type) 
            throw new Error('You are passing a wrong module! Please check the object you are passing to i18next.use()')
        // 将插件保存到 this.modules 中
        if (module.type === 'backend') {
          this.modules.backend = module;
        }
​
        if (module.type === 'logger' || (module.log && module.warn && module.error)) {
          this.modules.logger = module;
        }
​
        if (module.type === 'languageDetector') {
          this.modules.languageDetector = module;
        }
​
        if (module.type === 'i18nFormat') {
          this.modules.i18nFormat = module;
        }
​
        if (module.type === 'postProcessor') {
          postProcessor.addPostProcessor(module);
        }
​
        if (module.type === 'formatter') {
          this.modules.formatter = module;
        }
​
        if (module.type === '3rdParty') {
          this.modules.external.push(module);
        }
​
        return this;
      }
    
    init(options = {}, callback) {
      // init 时使用 BackendConnector 初始化了 init 前使用 use 加载的 backend 插件
      const s = this.services;
      s.backendConnector = new BackendConnector(
        createClassOnDemand(this.modules.backend),
        s.resourceStore,
        s,
        this.options,
      );
    }
}
  • 在这个BackendConnector中,会调用Backend中的read方法,通过http下载翻译。
class Connector {
​
    // 加载翻译
    load(languages, namespaces, callback) {
        this.prepareLoading(languages, namespaces, {}, callback);
    }
​
    // 重新加载翻译
    reload(languages, namespaces, callback) {
        this.prepareLoading(languages, namespaces, { reload: true }, callback);
    }
    
    // 准备加载
    prepareLoading(languages, namespaces, options = {}, callback) {
        // 获得需要加载的名称队列
        const toLoad = this.queueLoad(languages, namespaces, options, callback);
        // 遍历需要加载的名称()
        toLoad.toLoad.forEach((name) => {
          this.loadOne(name);
        });
    }
    
    // 构建加载队列
    queueLoad(languages, namespaces, options, callback) {
        const toLoad = {};
        languages.forEach((lng) => {
            namespaces.forEach((ns) => {
                
                // 名称是 “语言|命名空间” 的字符串格式
                const name = `${lng}|${ns}`;
                // 如果该<语言|命名空间>已经加载过了,不会再重新加载
                if (!options.reload && this.store.hasResourceBundle(lng, ns)) {
                  this.state[name] = 2; // loaded
                } else if (this.state[name] < 0) {
                  // nothing to do for err
                } else if (this.state[name] === 1) {
                  if (pending[name] === undefined) pending[name] = true;
                } else {
                  this.state[name] = 1; // pending
​
                  hasAllNamespaces = false;
​
                  if (pending[name] === undefined) 
                      pending[name] = true;
                  // 拼接需要加载的"语言|命名空间"
                  if (toLoad[name] === undefined) 
                      toLoad[name] = true;
                  if (toLoadNamespaces[ns] === undefined) 
                      toLoadNamespaces[ns] = true;
                }
            });
        });
        return {
          // ...
          toLoad: Object.keys(toLoad),
        };
    }
    
    // 调用 Backend 的 read 方法,期约解决后走到 loaded 回调
    loadOne(name, prefix = '') {
        const s = name.split('|');
        const lng = s[0];
        const ns = s[1];
        this.read(lng, ns, 'read', undefined, undefined, (err, data) => {
            this.loaded(name, err, data);
        });
    }
    
    // 调用 Backend 插件的 [fcName] 方法,并且由其参数数量决定走不走 callback
    read(lng, ns, fcName, tried = 0, wait = this.retryTimeout, callback) {
        const resolver = (err, data) => {
          callback(err, data);
        };
        
        const fc = this.backend[fcName].bind(this.backend);
        if (fc.length === 2) {
            // no callback
            try {
                const r = fc(lng, ns);
                if (r && typeof r.then === 'function') {
                    // promise
                    r.then((data) => resolver(null, data)).catch(resolver);
                } else {
                    // sync
                    resolver(null, r);
                }
            } catch (err) {
                resolver(err);
            }
            return;
        }
        // normal with callback
        return fc(lng, ns, resolver);
    }
    
    // 将请求到的翻译添加到 data 中
    loaded(name, err, data) {
        if (!err && data) {
            this.store.addResourceBundle(
                lng, 
                ns, 
                data, 
                undefined, 
                undefined, 
                { skipCopy: true }
            );
        }
        // 触发事件通知
        this.emit('loaded', loaded);
    }
}
  • 接下来看哪里会调用BackendConnectorload方法,全局搜索发现在I18next实例中的loadResources方法中,而在changeLanguage中会调用loadResources方法。
class I18n {
    // 加载资源
    loadResources(language, callback = noop) {
        const usedLng = isString(language) ? language : this.language;
        // 注意这里,只有选项中没有传入 resources 时,才会通过 Backend 加载翻译
        if (!this.options.resources || this.options.partialBundledLanguages) {
            const toLoad = [];
            // 往 toLoad 数组中加入需要加载的语言
            const append = lng => {
                if (!lng) return;
                const lngs = this.services.languageUtils.toResolveHierarchy(lng);
                lngs.forEach(l => {
                  if (toLoad.indexOf(l) < 0) 
                      toLoad.push(l);
                });
            };
            if (!usedLng) {
                // 如果没有传入的语言,使用 fallback 语言
                const fallbacks = this.services.languageUtils.getFallbackCodes(this.options.fallbackLng);
                fallbacks.forEach(l => append(l));
            } else {
                append(usedLng);
            }
            // 加载需要加载的语言
            this.services.backendConnector.load(
                toLoad, 
                // 加载 options.ns 中的所有命名空间
                this.options.ns,
                (e) => {
                    if (!e && !this.resolvedLanguage && this.language)  
                        this.setResolvedLanguage(this.language);
                }
            );
        }
    }
    
    // 切换语言
    changeLanguage(lng, callback) {
        const setLng = lngs => {
          if (!lng && !lngs && this.services.languageDetector) 
              lngs = [];
          const l = isString(lngs) ? 
                lngs : this.services.languageUtils.getBestMatchFromCodes(lngs);
​
          if (l) {
            if (!this.language) {
              setLngProps(l);
            }
            if (!this.translator.language) 
                this.translator.changeLanguage(l);
            // 通过 languageDetector 插件将当前语言储存在缓存中
            this.services.languageDetector?.cacheUserLanguage?.(l);
          }
          // 加载翻译
          this.loadResources(l, err => {
            done(err, l);
          });
        };
    }
}

总结:在init方法中,会调用changeLanguage方法,初始化当前语言。在changeLanguage方法中,会判断ResourceStore中当前翻译包是否存在,如果不存在,则会通过http的方法,加载翻译。

命名空间的延迟加载流程,何时加载

在上面的代码中,可以看出,每次切换语言都会加载初始化选项中ns数组的所有命名空间。那么怎么做到延迟加载指定命名空间的翻译呢?

  • I18next 实例下,有一个loadNamespaces方法,可以向this.options.ns数组中插入新的 namespace,然后再调用一次loadResources方法,从而实现命名空间翻译的延迟加载
  • 下次切换语言时,由于这个延迟加载的 namespace 已经在this.options.ns中了,不用再次调用`loadNamespaces,也会一起加载。
class I18n {
  // 加载指定命名空间,ns 参数可以传入数组或字符串
  loadNamespaces(ns, callback) {
    if (isString(ns)) ns = [ns];
    // 向 this.options.ns 中插入新命名空间
    ns.forEach(n => {
      if (this.options.ns.indexOf(n) < 0) this.options.ns.push(n);
    });
    // 加载翻译资源
    this.loadResources(err => {
      if (callback) callback(err);
    });
  }
}

总结:如果想要使用命名空间的延迟加载,在初始化的 ns 数组中,不要传入该命名空间的名称,而是在使用该命名空间的翻译前,调用i18next.loadNamespaces方法即可(或者使用 react-i18next 的 withTranslation,传入 namespace 数组参数)。命名空间的延时加载需要搭配Backend插件使用

切换语言,如何触发页面渲染

首先看 react-i18next 的代码

export const useTranslation = (ns, props = {}) => {
  const getT = () => memoGetT;
    
  const isMounted = useRef(true);
  
  // i18n 配置
  const i18nOptions = { ...getDefaults(), ...i18n.options.react, ...props };
  
  // 获取最新的 t 方法
  const getNewT = () =>
    alwaysNewT(
      i18n,
      props.lng || null,
      i18nOptions.nsMode === 'fallback' ? namespaces : namespaces[0],
      keyPrefix,
    );
  
  // 使用一个状态来保存 t 函数
  const [t, setT] = useState(getT);
​
  useEffect(() => {
    const { bindI18n, bindI18nStore } = i18nOptions;
​
    // 通过事件更新 t 状态,触发渲染
    const boundReset = () => {
      if (isMounted.current) 
          setT(getNewT);
    };
​
    // 绑定事件,监听 bindI18n 事件, 默认值是 languageChanged
    if (bindI18n) 
        i18n?.on(bindI18n, boundReset);
    if (bindI18nStore) 
        i18n?.store.on(bindI18nStore, boundReset);
​
    // 解绑事件
    return () => {
      isMounted.current = false;
      if (i18n) 
          bindI18n?.split(' ').forEach((e) => i18n.off(e, boundReset));
      if (bindI18nStore && i18n)
        bindI18nStore.split(' ').forEach((e) => i18n.store.off(e, boundReset));
    };
  }, [i18n, joinedNS]);
​
  const ret = [t, i18n, ready];
  ret.t = t;
  ret.i18n = i18n;
  ret.ready = ready;
​
  if (ready) return ret;
​
  if (!ready && !useSuspense) return ret;
};
// 默认配置
let defaultOptions = {
  // ...
  bindI18n: 'languageChanged',
  bindI18nStore: '',
};

总结:从代码中,不难看出,在切换语言时,会触发languageChanged事件,从而触发useEffect中的事件监听,更新t函数的引用(t函数是一个useState状态)从而引起页面渲染。

i18n Ally

再介绍一个比较好用的 vscode 插件,直接上图: img

img

  • 代码内预览翻译
  • 翻译代码补全
  • 翻译缺失提醒
{
  "i18n-ally.localesPaths": [
    "src/translates"        // 定位翻译文件地址
  ],
  "i18n-ally.defaultNamespace": "translation",      // 命名空间
  "i18n-ally.displayLanguage": "zh",        // 代码中预览的语言
}