介绍
i18next
基本用法
初始化
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)
| 选项 | 类型 | 描述 |
|---|---|---|
| defaultValue | string | 如果未找到翻译,则返回 defaultValue |
| lng | string | 覆盖要使用的语言 |
| lngs | string[] | 覆盖要使用的语言,使用优先匹配到翻译的第一个语言 |
| fallbackLng | string | 如果未找到,则覆盖语言以查找关键字 |
自动渲染
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 的数量,来决定显示单数还是复数,即显示 key 或 key_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 <img />"
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; } |
| escapeValue | true | 转义传入的值以避免 XSS 注入 |
| useRawValueToEscape | false | 如果为真,则传递到转义函数的值不会转换为字符串,而是与执行其自身类型检查的自定义转义函数一起使用 |
| 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的内置格式化函数。
这个需要icu(International 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目前加载翻译的优先级为:
I18n.translations数组- 本地缓存
- 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 命名空间中的键
源码分析
目的:
- 分析 i18next 如何加载翻译文件?
- 在
init方法中传resources和使用backend有何区别? - 命名空间的延迟加载流程,何时加载?
- 语言变了,
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);
}
}
- 调用
i18next的t,会走到Translator实例中的translate方法。在这个方法中会调用实例中的resolve方法,在里面遍历defaultNS和key中解析到的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方法,是用来获取默认选项的。其中loadPath和addPath分别对应默认加载路径和上传路径。 - 此外默认选项中还有一个
request方法,是用来调用http请求的。 read、readMulti方法内部调用了_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方法,里面会根据环境中支持fetch或XMLHttpRequest,调用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);
}
}
- 接下来看哪里会调用
BackendConnector的load方法,全局搜索发现在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 插件,直接上图:

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