🗺️ 一篇文章了解如何在 Next.js 中集成 i18n 国际化(含踩坑及开发配置)
国际化框架对比
在 Next.js 的官方示例仓库中展示了三种国际化的库 next-i18next,next-translate,next-intl ,以下是三个库官方介绍的特点:
- next-i18next 使用起来比较灵活,它提供了多种功能,包括 SSR、多语言支持、多个命名空间和多种翻译格式,基于文件的静态国际化、基于 API 的动态国际化等,它的 API 是基于 i18next 库的,适用于大型项目。
- next-intl 是 next.js 内置的国际化方案,使用起来比较简单,但功能较为有限,它的 API 是基于 React Intl 的,主要用于小型项目或简单的国际化需求。
- next-translate 与 next-i18next 基本一致。两者的具体区别如下:
next-i18next:
- 使用 i18next 和 react-i18next 库,配置简单。
- 提供了一些附加功能,如与 SimpleLocalize 进行易于集成的翻译内容管理,SSG/SSR 支持等。
- 在默认配置下,可以从本地目录结构中加载翻译。可以使用 getServerSideProps 或 getStaticProps 从 API 或其他数据源中加载翻译。
next-translate:
- 配置较为简单,支持使用 JSON 或 YML 格式的翻译文件。
- 可以使用 useTranslation hook 在组件中使用翻译。
如果需要使用翻译内容管理,可以选择 next-i18next。如果需要简单的翻译功能,可以选择 next-translate。
以下是三个库的 npm 下载量对比图:
根据下载量来说,目前 next-i18next 是 Next.js 中实现 i18n 最流行的方案。
❗️ 如果你使用的是 Next.js 13 以上版本且使用了
app router
的路由方式,那么你 不需要使用 next-i18next ,而是直接使用 i18next 和 react-i18next。
next-i18next 框架引入及配置
Next.js 项目可以通过 next-i18next 包来快速实现国际化支持,首先安装依赖:
yarn add next-i18next react-i18next i18next
添加配置文件 next-i18next.config.js
:
//next-i18next.config.js
/**
* @type {import('next-i18next').UserConfig}
*/
const path = require('path');
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'zh'],
localePath: path.resolve('./public/locales'),
},
};
defaultLocale
:默认的语言,在访问该语言的国际化路由时无需增加前缀。
locales
:配置 Next.js 的国际化路由,例如访问 /blog
时页面为英文,访问 /zh/blog
时页面为中文。
localePath: path.resolve('./public/locales'),
:将翻译相关的内容统一存放在 /public/locales
目录中:
在 next.config.ts
中引入国际化配置:
// next.config.ts
/** @type {import('next').NextConfig} */
const { i18n } = require('./next-i18next.config');
const nextConfig = {
i18n
// ...more config
};
module.exports = nextConfig;
翻译文件编写
我们可以将翻译相关的文件统一配置在/public/locales
目录中。项目中的每一个大模块需要单独维护一个翻译的 json 文件。
例如我们需要新增 Order 订单相关翻译内容,我们就新建 order.json
,在 json
文件中以嵌套 json 的形式配置翻译,例如下面这样:
{
components:{
SubscribeFormModal:{
onOK:{
xxx:xxx
}
}
}
}
关于 key 值的定义及如何嵌套我们需要根据团队的具体业务以及使用习惯进行决定,并没有一个通用最佳实践。
实现语言切换器组件
语言切换器组件的实现参考了 umi 中内置的语言切换器样式,使用 Chakra UI
进行改造,在切换语言时以跳转路由的形式切换语言,并设置相关的 cookie。
import {
Text,
Menu,
MenuButton,
MenuItem,
MenuList,
MenuButtonProps,
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
const langIcon = (
<svg
viewBox="0 0 24 24"
focusable="false"
width="1em"
height="1em"
fill="currentColor"
aria-hidden="true"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z "
className="css-c4d79v"
/>
</svg>
);
const LANG_MAP = {
en: {
label: 'English',
icon: '🇺🇸',
},
zh: {
label: '中文',
icon: '🇨🇳',
},
} as const;
const LangSelect: React.FC<MenuButtonProps> = (props) => {
const router = useRouter();
const { pathname, asPath, query, locale } = router;
return (
<Menu autoSelect={false}>
<MenuButton p="12px" {...props}>
{langIcon}
</MenuButton>
<MenuList w="max-content" minW="120px">
{Object.entries(LANG_MAP).map(([key, lang]) => (
<MenuItem
key={key}
display="flex"
alignItems="center"
fontSize="sm"
{...(key === locale ? { bg: 'A7Gray.200' } : {})}
onClick={() => {
document.cookie = `NEXT_LOCALE=${key}; max-age=31536000; path=/`;
router.push({ pathname, query }, asPath, { locale: key });
}}
>
<Text mr="8px">{lang.icon}</Text>
<Text>{lang.label}</Text>
</MenuItem>
))}
</MenuList>
</Menu>
);
};
export default LangSelect;
实现效果如下:
国际化路由持久化
Next.js 支持使用 NEXT_LOCALE cookie
覆盖 accept-language header
,这个 cookie 可以通过语言切换器设置。当用户再次访问网站时,它将使用这个 cookie 中指定的区域设置来重定向到正确的本地化位置。例如,用户设置了 accept-language header 为 fr
,但是 NEXT_LOCALE cookie
设置为 en
,在访问 / 时用户将重定向到 en 本地化位置,直到 cookie 被删除或过期为止。
开发环境配置
Vscode i18n ally 插件
为了便于翻译内容的添加及维护,我们需要安装 i18n-ally
插件:
在 .vscode/setting.json
文件中添加插件配置:
{
"i18n-ally.localesPaths": "public/locales",
"i18n-ally.keystyle": "nested"
}
在配置成功后,在组件中使用翻译内容 key
时,我们可以直接预览到默认语言的内容:
在单击翻译内容 key
时,会弹出所有语言的翻译内容,如果需要修改或添加还可以直接点击图标进入编辑页面:
使用插件后可以有效避免开发完成后修改翻译时需要频繁在组件和翻译内容中切换,而且当 key 值不存在时也会进行提示,大大降低开发心智成本。
配置翻译文件类型约束
在开发时由于 key 值往往比较长,在使用时仍然需要在组件和翻译内容中切换,因此我们可以通过配置翻译文件的类型约束以实现代码提示。新增 types/i18next.d.ts
文件,写入以下内容:
import 'i18next';
import api from '../../public/locales/en/order.json';
import common from '../../public/locales/en/common.json';
interface I18nNamespaces {
api: typeof order;
common: typeof common;
}
declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'common';
resources: I18nNamespaces;
}
}
这个全局类型声明文件中,我们将 api
及 common
模块的翻译文件引入,并通过 typeof
的方式将 json
文件转换为 ts 类型。
在配置声明文件后,使用翻译内容的 key 时就能获取到完整的代码提示,并且如果使用错误的 key 值时会直接报错,能够有效防止在页面中渲染错误的翻译内容。
如何在页面中使用翻译
引入语言文件
引入语言文件操作过程如下:
首先使用 appWithTranslation
HOC 包裹 App 组件:
import { appWithTranslation } from 'next-i18next'
const MyApp = ({ Component, pageProps }) => (
<Component {...pageProps} />
)
export default appWithTranslation(MyApp)
appWithTranslation
是 next-i18next 库中的一个高阶组件,它的作用是将应用包装在一个 I18nextProvider
组件中,该组件提供了翻译所需的 i18next 实例和翻译相关的配置。
然后实现一个 getLocaleProps
函数,这个函数是一个异步函数,它的返回值将被传递给一个页面组件,这个函数可以与 getStaticProps
或 getServerSideProps
一起使用,,因为它需要等待获取翻译文件后才能返回数据。获取到的翻译文件将被传递给页面组件,以便在客户端上使用。
import { GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { I18nNamespaces } from '@/types/i18next';
const getLocaleProps =
(namespaces: (keyof I18nNamespaces)[]): GetStaticProps =>
async ({ locale }) => ({
props: {
...(await serverSideTranslations(locale!, namespaces)),
},
});
export default getLocaleProps;
在页面组件中的使用方式,将需要使用的翻译文件命名空间传入即可:
export const getStaticProps = getLocaleProps(['common', 'order']);
要注意 getStaticProps 方法只能在 页面组件 中使用,而不能在子组件中使用。
如果当前页面是一个动态路由页面,还需要搭配 getStaticPaths
使用:
export const getStaticPaths: GetStaticPaths = async () => ({
paths: [],
fallback: 'blocking',
});
具体参数含义可以参考我的这篇文章:🏞快速理解 Next.js 中数据获取方式的作用及区别(getInitialProps、getServerSideProps、getStaticProps)
如果未来需要将站点进行静态编译,那么 next-i18next 也可以进行改造支持,具体可以参考这篇文章 locize.com/blog/next-i…
通过 hook 使用翻译
在引入语言文件后,就可以在所有渲染于该页面的组件中使用了:
import { GetStaticPaths } from 'next';
import { useTranslation } from 'next-i18next';
import { getLocaleProps } from '@/helper/utils';
const Page = () => {
const { t } = useTranslation('developer');
return (
<>
<div>{t(翻译内容的 key 值)}</div>
<div>t(带有命名空间的 key 值, {
ns: 命名空间,
})
</div>
</>
);
};
export const getStaticPaths: GetStaticPaths = async () => ({
paths: [],
fallback: 'blocking',
});
export const getStaticProps = getLocaleProps([
'common',
'order',
]);
export default Page;
这里要注意
useTranslation
只能使用next-i18next
包中引入的,不能使用react-i18next
包中引入的。
国际化测试
在 E2E 测试中使用国际化文案
由于在集成国际化功能的过程中已经维护了一份翻译文件,我们可以以这一份翻译文件作为文案索引,在测试中引入翻译文件,在需要使用文案获取元素的位置中直接使用翻译的 key 拿到文案:
import orderLocale from '../../public/locales/en/order.json';
import commonLocale from '../../public/locales/en/common.json';
describe('Test My Order', () => {
it('Should visit my order', () => {
cy.visit('/order');
cy.contains(orderLocale.Tab.order.title).should(
'be.visible'
);
});
}
这样在未来修改翻译文件中的文案时无需再次修改测试,降低维护成本。
踩坑
Can't resolve 'fs' 问题
如果你配置完成后出现了下图中的问题:
可以在 next.config.ts 中新增一个自定义的 webpack 的配置:
/** @type {import('next').NextConfig} */
const { i18n } = require('./next-i18next.config');
const nextConfig = {
i18n,
+ webpack: (config,{isServer}) => {
+ if (!isServer) {
+ config.resolve = {
+ ...config.resolve,
+ fallback: {
+ ...config.resolve.fallback,
+ fs: false,
+ },
+ };
+ }
+ config.module = {
+ ...config.module,
+ exprContextCritical: false,
+ };
+ return config;
+ },
// ...more
}
module.exports = nextConfig
报错的原因如下:
- 在客户端中使用
fs
模块会导致构建失败,因为浏览器不支持fs
模块。因此需要在非服务端的情况下在fallback
中将fs
替换为false
。 exprContextCritical
是 webpack 中的一个配置选项,它用于控制是否在编译过程中(即在模块转换时)对应用程序代码执行的上下文表达式抛出警告或者抛出错误信息。将exprContextCritical
设置为false
可以避免在控制台频繁打印错误。
具体的上下文大家也可以参考这个 github issue: github.com/i18next/nex…
如何在 jest 中测试带有 i18n 的组件
如果你需要在 jest 中测试带有 next-i18next 的组件,那么你需要 mock 一下相关的 hook,并在测试时手动传入上下文,首先实现一个 createI18nMock
方法:
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import common from '../../public/locales/en/common.json';
i18next.use(initReactI18next).init({
lng: 'en',
fallbackLng: 'en',
ns: ['common'],
defaultNS: 'common',
resources: {
en: {
common,
},
},
});
export default i18next;
这个方法的作用是创建一个 i18n 实例,初始化它的配置,然后将它导出。在 jest.setup.ts
中加入以下配置:
import i18next from './src/test-utils/createI18nMock';
// Mock next-i18next
jest.mock('next-i18next', () => {
return {
useTranslation: () => {
return {
t: (key, option) =>
i18next.getResource('en', option?.ns || 'common', key),
i18n: {
changeLanguage: () => new Promise(() => {}),
},
};
},
};
});
jest.mock
用于模拟 next-i18next
模块的 useTranslation
函数。在这个mock函数中,t
函数被定义为返回 i18next.getResource
的结果。测试中的使用方式如下:
import { I18nextProvider } from 'react-i18next';
import i18next from '@/test-utils/createI18nMock';
<I18nextProvider i18n={i18next}>
// 你的组件
</I18nextProvider>
总结
根据这篇文章进行配置,相信你可以获得一个相对舒服的国际化开发体验。如果文章对你有帮助除了收藏外,不妨为作者点个赞支持下,respect!
转载自:https://juejin.cn/post/7238619890993758263