ref: https://www.i18next.com/
i18next是一个用JavaScript编写的国际化框架。
i18next为您提供了一个完整的解决方案,本地化您的产品从web端到移动端和桌面端。
在react项目中安i18next
依赖:
- i18next
- react-i18next
- i18next-browser-languagedetector,用于检测用户语言
npm install i18next react-i18next i18next-browser-languagedetector
创建i18n.js
文件:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(LanguageDetector) // 检测用户语言
.use(initReactI18next) // 将i18n实例传递给react-i18next
.init({ // 初始化 i18next
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React已经转义了
},
resources: {
en: {
translation: {
// 此处放置翻译内容
description: {
part1: 'Edit <1>src/App.js</1> and save to reload.',
part2: 'Learn React'
}
}
},
de: {
translation: {
description: {
part1: 'Ändere <1>src/App.js</1> und speichere um neu zu laden.',
part2: 'Lerne React'
}
}
}
}
});
export default i18n;
在index.js
中导入i18n.js
:
React>=18.0.0
的版本:
import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
// 导入 i18n (需要绑定)
import './i18n';
const root = createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
更早的版本:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
// 导入 i18n (需要绑定)
import './i18n';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
使用:在第一个文本中使用了Trans组件,在第二个文本中使用了useTranslation (hook) :
// App.js
import logo from './logo.svg';
import './App.css';
import { useTranslation, Trans } from 'react-i18next';
function App() {
const { t } = useTranslation();
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
<Trans i18nKey="description.part1">
Edit <code>src/App.js</code> and save to reload.
</Trans>
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
{t('description.part2')}
</a>
</header>
</div>
);
}
export default App;
语言切换器
// App.jsx
...
const lngs = {
en: { nativeName: 'English' },
de: { nativeName: 'Deutsch' }
};
function App() {
...
<img src={logo} className="App-logo" alt="logo" />
<div>
{Object.keys(lngs).map((lng) => (
<button key={lng} style={{ fontWeight: i18n.resolvedLanguage === lng ? 'bold' : 'normal' }} type="submit" onClick={() => i18n.changeLanguage(lng)}>
{lngs[lng].nativeName}
</button>
))}
</div>
<p>
<Trans i18nKey="description.part1">
Edit <code>src/App.js</code> and save to reload.
</Trans>
</p>
...
}
自i18next v21以来,i18next. resolvedlanguage
被设置为当前解析的语言,并且可以用作主要使用的语言,上例中有使用。
i18next.language vs. i18next.languages
i18next.language;
// 设置为当前检测或设置的语言
i18next.languages;
// 设置为将用于查找翻译值的语言代码数组
// 设置语言后,将使用新的语言代码填充此数组
// 除非被覆盖,否则将使用该代码的不太特定的版本填充此数组,以用于回退目的,然后是回退语言列表
// 初始化回退语言
i18next.init({
fallbackLng: ["es", "fr", "en-US", "dev"]
});
// 改变语言
i18next.changeLanguage("en-US-xx");
// 新语言和它的更一般的形式,然后是回退
i18next.languages; // ["en-US-xx", "en-US", "en", "es", "fr", "dev"]
// 再次改变语言
i18next.changeLanguage("de-DE");
// 不保留上一次设置的语言
i18next.languages; // ["de-DE", "de", "es", "fr", "en-US", "dev"]
处理复数和插值
示例:计算语言变化的次数
// App.js
...
import { useState } from 'react';
...
function App() {
...
const [count, setCounter] = useState(0);
return (
...
<div>
{Object.keys(lngs).map((lng) => (
<button key={lng} style={{ fontWeight: i18n.resolvedLanguage === lng ? 'bold' : 'normal' }} type="submit" onClick={() => {
i18n.changeLanguage(lng);
setCounter(count + 1);
}}>
{lngs[lng].nativeName}
</button>
))}
</div>
<p>
<i>{t('counter', { count })}</i>
</p>
...
);
}
export default App;
拓展翻译源:
// i18n.js
...
...
resources: {
en: {
translation: {
description: {
part1: 'Edit <1>src/App.js</1> and save to reload.',
part2: 'Learn React'
},
counter_one: 'Changed language just once',
counter_other: 'Changed language already {{count}} times'
}
},
de: {
translation: {
description: {
part1: 'Ändere <1>src/App.js</1> und speichere um neu zu laden.',
part2: 'Lerne React'
},
counter_one: 'Die Sprache wurde erst ein mal gewechselt',
counter_other: 'Die Sprache wurde {{count}} mal gewechselt'
}
}
}
...
i18next会根据count数值选择正确的复数格式,了解更多 - Plurals
处理多种复数:
// 翻译源:
{
"key_zero": "zero",
"key_one": "singular",
"key_two": "two",
"key_few": "few",
"key_many": "many",
"key_other": "other"
}
// 使用:
t('key', {count: 0}); // -> "zero"
t('key', {count: 1}); // -> "singular"
t('key', {count: 2}); // -> "two"
t('key', {count: 3}); // -> "few"
t('key', {count: 4}); // -> "few"
t('key', {count: 5}); // -> "few"
t('key', {count: 11}); // -> "many"
t('key', {count: 99}); // -> "many"
t('key', {count: 100}); // -> "other"
这些复数是用Intl API简化的。你可能需要Polyfill Intl.PluralRules的API,如果它不可用,它将退回到i18next JSON格式v3复数处理。
注:变量名必须是count
并且必须存在:i18next.t('key', {count: 1});
polyfill: 只需导入它以确保Intl.PluralRules
在您的环境中可用:
npm install intl-pluralrules
import 'intl-pluralrules'
如果Intl.PluralRules
已经存在,包含一个selectRange()
方法,并且支持多种语言环境,那么将不会加载polyfill。
格式化
在i18next和Luxon的帮助下使用不同的日期格式来处理日期和时间。
npm install luxon
创建一个Footer
组件并在App.js
中引用:
import './Footer.css';
const Footer = ({ t }) => (
<div className="Footer">
<div>{t('footer.date', { date: new Date() })}</div>
</div>
);
export default Footer;
// 在App.js中引入并这样使用
// <Footer t={t} />
导入luxon
并定义一个格式函数,如文档中所述,并添加新的翻译key:
...
import { DateTime } from 'luxon';
...
.init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false,
// format: (value, format, lng) => { // 遗留使用方法
// if (value instanceof Date) {
// return DateTime.fromJSDate(value).setLocale(lng).toLocaleString(DateTime[format])
// }
// return value;
// }
},
resources: {
en: {
translation: {
...
footer: {
date: 'Today is {{date, DATE_HUGE}}'
}
}
},
de: {
translation: {
...
footer: {
date: 'Heute ist {{date, DATE_HUGE}}'
}
}
}
}
});
// 新的使用方法
i18n.services.formatter.add('DATE_HUGE', (value, lng, options) => {
return DateTime.fromJSDate(value).setLocale(lng).toLocaleString(DateTime.DATE_HUGE)
});
export default i18n;
English:
German:
Context
基于当前时间的特定问候信息,即早上、晚上等。
创建一个getGreetingTime
函数,并使用结果作为页脚翻译的内容:
// Footer.jsx
import { DateTime } from 'luxon';
import './Footer.css';
const getGreetingTime = (d = DateTime.now()) => {
const split_afternoon = 12; // 24小时制分配下午
const split_evening = 17; // 24小时制分配晚上
const currentHour = parseFloat(d.toFormat('hh'));
if (currentHour >= split_afternoon && currentHour <= split_evening) {
return 'afternoon';
} else if (currentHour >= split_evening) {
return 'evening';
}
return 'morning';
}
const Footer = ({ t }) => (
<div className="Footer">
<div>{t('footer.date', { date: new Date(), context: getGreetingTime() })}</div>
</div>
);
export default Footer;
添加i18next-http-backend插件(从服务器加载翻译)和翻译key:
npm install i18next-http-backend
...
import Backend from 'i18next-http-backend';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
...
resources: {
en: {
translation: {
...
footer: {
date: 'Today is {{date, DATE_HUGE}}',
date_morning: 'Good morning! Today is {{date, DATE_HUGE}} | Have a nice day!',
date_afternoon: 'Good afternoon! It\'s {{date, DATE_HUGE}}',
date_evening: 'Good evening! Today was the {{date, DATE_HUGE}}'
}
}
},
de: {
translation: {
...
footer: {
date: 'Heute ist {{date, DATE_HUGE}}',
date_morning: 'Guten Morgen! Heute ist {{date, DATE_HUGE}} | Wünsche einen schönen Tag!',
date_afternoon: 'Guten Tag! Es ist {{date, DATE_HUGE}}',
date_evening: 'Guten Abend! Heute war {{date, DATE_HUGE}}'
}
}
}
}
});
...
效果:
分离翻译和代码
在i18next-http-backend的帮助下将翻译从代码中分离出来,并将它们放在专用的json文件中。
将翻译文件移动到public
文件夹:
移除翻译代码:
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { DateTime } from 'luxon';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false,
// format: (value, format, lng) => {
// if (value instanceof Date) {
// return DateTime.fromJSDate(value).setLocale(lng).toLocaleString(DateTime[format])
// }
// return value;
// }
}
});
i18n.services.formatter.add('DATE_HUGE', (value, lng, options) => {
return DateTime.fromJSDate(value).setLocale(lng).toLocaleString(DateTime.DATE_HUGE)
});
export default i18n;
现在翻译是异步加载的啦,所以用一个Subspense组件包装你的应用来确保你防止以下错误:
Uncaught Error: App suspended while rendering, but no fallback UI was specified.
// App.jsx
import { Suspense } from 'react';
function App() {
...
}
// 在这里,app从页面捕获suspense,以防翻译未加载完成
export default function WrappedApp() {
return (
<Suspense fallback="...is loading">
<App />
</Suspense>
);
}
如果你想支持一种新的语言,你只需要创建一个新的文件夹和一个新的翻译json文件。这使您可以直接将翻译json发送给一些专业的翻译人员。或者,如果您正在使用翻译管理系统,您可以使用cli同步文件。
具体代码可在这里查看。
多个namespaces
将翻译拆分为多个文件,使用:在调用useTranslation
时指定它
const { t } = useTranslation(['translation', 'common']);
// ...
// t('look.deep', { ns: 'common' })
[withTranslation](withTranslation (HOC) - react-i18next documentation):
withTranslation(['translation', 'common'])(MyComponent);
// ...
// t('look.deep', { ns: 'common' })
[Translation](Translation (render prop) - react-i18next documentation):
<Translation ns={['translation', 'common']}>
{
(t) => <p>{t('look.deep', { ns: 'common' })}</p>
}
</Translation>