1. 项目概述:为什么我们需要一个强大的国际化方案?
如果你正在开发一个面向全球用户的Next.js应用,那么“国际化”这个词对你来说绝对不陌生。它不仅仅是把页面上的文字从中文翻译成英文那么简单。真正的国际化,或者说i18n,是一个系统工程,涉及到语言切换、日期/货币/数字格式化、复数规则处理、甚至是从右到左的布局适配。在Next.js这个全栈框架里,实现一个优雅、高效且易于维护的i18n方案,曾经是让很多开发者头疼的问题。直到你遇到了i18next和next-i18next这对黄金组合。
简单来说,i18next是一个功能极其强大的国际化框架,它几乎成了JavaScript生态里i18n的事实标准。而next-i18next则是专门为Next.js应用量身定做的胶水层,它把i18next的强大能力无缝地集成到Next.js的服务端渲染、静态生成和客户端渲染的各个生命周期中。这意味着,你可以在getStaticProps里预加载翻译文件,在服务端就准备好正确的语言内容,然后在客户端实现丝滑的语言切换,而无需刷新整个页面。
我接手过好几个从零开始或者中途引入国际化的Next.js项目,踩过的坑不计其数。比如,如何管理成百上千个翻译键值对?如何让非技术同事(比如产品经理、运营)也能方便地参与翻译工作?如何在保持开发体验的同时,优化生产环境的加载性能?i18next/next-i18next这套方案,正是在反复实践中被验证为最成熟、最灵活的选择。它不是一个简单的翻译库,而是一个完整的国际化工作流解决方案。
2. 核心架构与设计哲学拆解
2.1 i18next:不仅仅是翻译字典
很多人第一次接触i18next,会以为它就是一个键值对的存储和查询工具。这大大低估了它的能力。i18next的核心设计哲学是“上下文感知的国际化”。它理解翻译不是一对一的映射,而是根据语言、复数、上下文、插值变量甚至格式进行动态转换的过程。
命名空间:这是i18next管理大量翻译文本的第一道防线。你可以把翻译文件按功能模块划分,比如common.json放通用按钮文字,homepage.json放首页内容,userProfile.json放用户资料相关文本。这样不仅结构清晰,更重要的是可以实现按需加载。当用户访问首页时,只加载common和homepage的翻译,而不是一次性加载所有语言的所有文本,这对性能至关重要。
插值与格式化:这是体现其强大之处的地方。假设有一条欢迎语:“Hello {{name}}, you have {{count}} new messages.”。i18next不仅能替换{{name}}和{{count}},还能根据count的值和当前语言的复数规则,自动选择“message”或“messages”。它还能深度集成像date-fns这样的库,让你轻松实现“Last updated on {{date, LL}}”这样的格式化输出,日期会根据用户的语言环境自动呈现为“2023年10月27日”或“October 27, 2023”。
后端与插件生态:i18next本身不关心你的翻译文件存在哪里。它通过“后端”概念来解耦。你可以使用文件系统后端从本地public/locales文件夹读取JSON,也可以使用i18next-http-backend从CDN或API动态加载。更强大的是其插件生态,比如i18next-browser-languagedetector可以自动检测用户浏览器语言,i18next-localstorage-cache可以将翻译缓存到本地存储,避免重复请求。
2.2 next-i18next:Next.js场景下的深度集成
next-i18next的任务,就是把上述i18next的所有能力,以最符合Next.js范式的方式引入。它的设计紧密围绕Next.js的渲染模式。
服务端数据注入:这是next-i18next最核心的价值。在传统的React SPA中,国际化通常在客户端完成,这会导致初始HTML内容为空或为默认语言,然后JavaScript加载后再替换,不利于SEO和首屏体验。next-i18next通过一个自定义的serverSideTranslations函数,让你在getStaticProps或getServerSideProps中,就能预先加载好当前语言和命名空间的翻译内容,并将其作为props注入到页面组件。这样,服务端渲染出的HTML直接就是目标语言的内容,对搜索引擎和用户都极其友好。
路由与语言检测集成:它支持两种主流的URL国际化策略:子路径(如/en/about)和域名(如en.example.com)。通过简单的配置,它可以自动从URL中解析出语言代码,并传递给i18next实例。同时,它也尊重Next.js的locale和locales配置,使得预渲染多语言静态页面变得非常简单。
客户端平滑过渡:服务端注入初始化翻译后,在客户端,next-i18next会复用同一个i18next实例。当你调用i18n.changeLanguage(‘zh’)切换语言时,它会智能地加载新语言的翻译文件(如果尚未加载),更新所有使用翻译的组件,并且通常可以配合Next.js的router.push实现URL的更新,而页面状态(如表单输入、组件状态)得以保留,体验非常流畅。
注意:
next-i18next从v13版本开始,为了拥抱Next.js 13+的App Router和React Server Components的新范式,其架构发生了重大变化。如果你使用的是pages/目录的老项目,应继续使用next-i18next@v13之前的版本。如果是全新的基于App Router的项目,则需要使用最新版本,其配置和使用方式有所不同。本文后续的实操部分将主要聚焦于更成熟稳定的、基于pages/目录的版本,因为这是目前大多数生产项目采用的架构。
3. 从零到一的完整配置与实操
3.1 项目初始化与环境准备
假设我们有一个全新的Next.js项目(使用pages路由器)。首先,通过npm或yarn安装核心依赖:
npm install next-i18next i18next # 或 yarn add next-i18next i18next这里只需要安装这两个包。next-i18next会自动安装它所依赖的i18next相关插件,如检测器、后端等,我们无需手动处理。
3.2 核心配置文件详解
接下来,在项目根目录创建next-i18next.config.js文件。这个文件是next-i18next的神经中枢,所有行为都由它控制。
// next-i18next.config.js /** * @type {import('next-i18next').UserConfig} */ module.exports = { i18n: { // 应用支持的语言列表 locales: ['en', 'zh-CN', 'de'], // 默认语言,当检测不到用户语言时使用 defaultLocale: 'en', // 自定义域名映射(可选) // domains: [ // { // domain: 'example.com', // defaultLocale: 'en', // }, // { // domain: 'example.cn', // defaultLocale: 'zh-CN', // }, // ] }, // 翻译文件存放的目录,相对于项目根目录 localePath: './public/locales', // 是否在控制台输出重载翻译等调试信息 debug: process.env.NODE_ENV === 'development', // 是否使用浅层合并来更新翻译,通常设为true以获得更好性能 shallowRender: true, // 插值配置,这里我们使用React作为默认的插值转义方式,更安全 interpolation: { escapeValue: false, // React已经处理了XSS转义 }, // 自定义语言检测器的顺序(可选) // detection: { // order: ['path', 'cookie', 'htmlTag', 'subdomain'], // caches: ['cookie'], // }, };关键配置解析:
locales: 这里我使用了‘zh-CN’而不是简单的‘zh’。这是一个最佳实践,因为中文有简体(CN)和繁体(TW、HK)之分,语义和用词差异很大。明确区分有助于后续的本地化细节处理。localePath: 约定优于配置。将翻译文件放在public/locales下,每个语言一个文件夹,是社区通用做法,也方便与一些翻译管理平台集成。shallowRender: 设置为true可以避免在语言切换时不必要的组件深度重渲染,提升性能。
3.3 翻译文件的结构与管理
按照配置,我们在public下创建locales目录,结构如下:
public/ locales/ en/ common.json homepage.json zh-CN/ common.json homepage.json de/ common.json homepage.json每个JSON文件的内容就是键值对。但键的命名有讲究,我推荐使用点分隔的命名空间路径,这能让键名自解释,也方便在代码中查找。
// public/locales/en/common.json { "header": { "title": "My International App", "description": "A demo application showcasing i18n" }, "button": { "submit": "Submit", "cancel": "Cancel", "save": "Save Changes" }, "message": { "welcome": "Welcome, {{name}}!", "unreadMessages": "You have {{count}} unread message", "unreadMessages_plural": "You have {{count}} unread messages" } }// public/locales/zh-CN/common.json { "header": { "title": "我的国际化应用", "description": "一个展示国际化功能的演示应用" }, "button": { "submit": "提交", "cancel": "取消", "save": "保存更改" }, "message": { "welcome": "欢迎,{{name}}!", "unreadMessages": "您有 {{count}} 条未读消息", "unreadMessages_plural": "您有 {{count}} 条未读消息" } }注意中文的复数处理:在中文里,名词通常没有单复数变化,所以“unreadMessages”和“unreadMessages_plural”的翻译内容可以是一样的。但键名仍需遵循i18next的复数规则(通过_plural后缀),框架内部会根据count的值来选择合适的键。对于中文,你甚至可以配置i18next使用特殊的复数规则,简化翻译文件。
3.4 在Next.js中初始化和使用
首先,我们需要创建一个自定义的_app.js文件(如果还没有),并用appWithTranslation高阶组件包裹我们的应用组件。这个HOC会初始化i18next实例,并提供必要的上下文。
// pages/_app.js import { appWithTranslation } from 'next-i18next'; const MyApp = ({ Component, pageProps }) => { return <Component {...pageProps} />; }; export default appWithTranslation(MyApp);然后,修改next.config.js,将next-i18next的配置导入并合并到Next.js的配置中。
// next.config.js const { i18n } = require('./next-i18next.config'); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, i18n, // 关键:将i18n配置合并进来 }; module.exports = nextConfig;现在,在任何一个页面组件中,我们就可以使用serverSideTranslations来获取翻译,并使用useTranslation这个Hook来进行渲染了。
// pages/index.js import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; import { useTranslation } from 'next-i18next'; export default function HomePage() { const { t, i18n } = useTranslation('common'); // 指定命名空间‘common’ const changeLanguage = (lng) => { i18n.changeLanguage(lng); }; return ( <div> <h1>{t('header.title')}</h1> <p>{t('header.description')}</p> <p>{t('message.welcome', { name: '访客' })}</p> <p>{t('message.unreadMessages', { count: 5 })}</p> <button onClick={() => changeLanguage('en')}>English</button> <button onClick={() => changeLanguage('zh-CN')}>中文</button> <button onClick={() => changeLanguage('de')}>Deutsch</button> </div> ); } // 关键:在服务端获取翻译 export async function getStaticProps({ locale }) { return { props: { ...(await serverSideTranslations(locale, ['common'])), // 加载‘common’命名空间 }, }; }代码解读:
getStaticProps中的serverSideTranslations(locale, [‘common’]):这是魔法发生的地方。它根据当前请求的locale(由Next.js路由中间件自动解析),去public/locales/{locale}/目录下找到common.json文件,将其内容加载并序列化,作为props传给页面组件。useTranslation(‘common’):Hook接收一个命名空间参数,返回t函数和i18n实例。t函数用于根据键名获取翻译文本。t(‘message.welcome’, { name: ‘访客’ }):展示了插值功能,{{name}}会被替换为‘访客’。t(‘message.unreadMessages’, { count: 5 }):展示了复数功能。当count为5时,i18next会自动查找并使用unreadMessages_plural这个键对应的翻译。
运行项目,访问/、/zh-CN、/de,你应该能看到对应语言的内容。点击按钮切换语言,内容会无刷新更新。
4. 高级特性与性能优化实战
4.1 按需加载命名空间与代码分割
在大型应用中,common命名空间可能被所有页面使用,但userDashboard的翻译可能只在用户仪表盘页面需要。一次性加载所有翻译会增大初始页面负载。next-i18next与i18next完美支持动态导入和代码分割。
首先,你不需要在serverSideTranslations里预加载所有命名空间。只需要加载当前页面必须的。
// pages/dashboard.js export async function getStaticProps({ locale }) { return { props: { ...(await serverSideTranslations(locale, ['common', 'dashboard'])), // 只加载common和dashboard }, }; }然后,在组件内部,如果某个子组件或某个用户操作需要另一个命名空间(比如settings),你可以动态加载它:
// components/UserSettings.js import { useTranslation } from 'next-i18next'; export default function UserSettings() { // 首先,确保基础命名空间已加载 const { t, i18n } = useTranslation('common'); const handleOpenSettings = async () => { // 动态加载‘settings’命名空间 if (!i18n.hasResourceBundle(i18n.language, 'settings')) { await i18n.loadNamespaces('settings'); } // 现在可以使用settings命名空间了 const settingsT = i18n.getFixedT(i18n.language, 'settings'); console.log(settingsT('notifications.title')); }; return <button onClick={handleOpenSettings}>{t('button.settings')}</button>; }更优雅的方式是使用next/dynamic配合一个包装组件:
// components/SettingsModal.js import { useTranslation } from 'next-i18next'; const SettingsModal = () => { const { t } = useTranslation('settings'); // 直接使用settings命名空间 return <div>{t('title')}</div>; }; // 在父组件中动态导入,并指定需要预加载的命名空间 const DynamicSettingsModal = dynamic( () => import('./SettingsModal').then((mod) => { // 这里可以触发命名空间加载,但更好的方式是在Modal打开时由内部组件处理 return mod; }), { ssr: false } // 通常设置类组件不需要SSR );性能心得:对于首屏关键内容涉及的翻译,务必在getStaticProps中预加载。对于弹窗、标签页等交互后才出现的内容,使用动态加载。可以利用i18n.isInitialized和i18n.hasResourceBundle方法来避免重复加载。
4.2 服务端渲染与静态生成的深度优化
对于使用getStaticProps的静态页面,next-i18next会为配置中的每一种语言(locales数组中的每一项)都生成一个独立的静态页面。例如,你有[‘en’, ‘zh-CN’]两种语言,pages/about.js就会生成/about(默认英文)和/zh-CN/about两个静态HTML文件。这非常有利于SEO。
但这里有一个常见的陷阱:共享组件中的翻译。假设你有一个<Footer />组件,它在多个页面中使用,并且有自己的翻译键(比如在common.json的footer对象下)。如果你只在某个页面(如首页)的getStaticProps中加载了common命名空间,那么当直接访问/zh-CN/about时,<Footer />组件能正确显示中文吗?
答案是:可以,但有条件。因为serverSideTranslations加载的翻译资源,会被注入到页面的初始HTML中的一个全局变量里。next-i18next初始化的i18next实例是一个单例,它在客户端会复用这些预加载的资源。只要Footer组件使用的命名空间(这里是common)在访问该页面时被加载过(无论是在当前页面还是之前访问过的其他页面),并且资源包还在内存中,它就能工作。
为了万无一失,尤其是对于重要的全局组件,我推荐两种做法:
- 在布局组件中加载:如果你使用
pages/_app.js或一个全局布局组件,可以考虑在这里加载全局必需的命名空间(如common)。但注意,_app.js中的getInitialProps或getServerSideProps会影响所有页面的性能,需谨慎。 - 在每个页面显式加载:更保守和清晰的做法是,在每个使用到该共享组件的页面的
getStaticProps中,都加载其所需的命名空间。虽然可能略有重复,但保证了独立性,也便于静态分析。
4.3 自定义语言检测与持久化策略
默认情况下,next-i18next会按照next.config.js中i18n配置的顺序(通常是[‘path’, ‘cookie’, ‘htmlTag’, …])来检测语言。但你可能需要更复杂的行为,比如:
- 根据用户账户设置优先于URL路径。
- 将语言选择持久化到数据库或更长期的存储中。
这可以通过自定义detection配置和操作i18n实例来实现。
// next-i18next.config.js module.exports = { i18n: { ... }, // 自定义检测 detection: { order: ['cookie', 'path', 'htmlTag', 'navigator'], // 优先读cookie,再读路径 caches: ['cookie'], // 检测到的语言会存入名为‘next-i18next’的cookie lookupCookie: 'preferred_language', // 自定义cookie名 }, };在客户端,你可以结合自己的用户系统来覆盖这个逻辑:
// 在用户登录后或设置页面 import i18n from 'next-i18next'; // 注意:需要从你初始化的地方导入实例,或使用useTranslation钩子获取的i18n function setUserLanguage(lng) { // 1. 更改i18next实例的语言 i18n.changeLanguage(lng); // 2. 可选:更新Next.js路由,保持URL与语言一致 const { pathname, asPath, query } = router; router.push({ pathname, query }, asPath, { locale: lng }); // 3. 可选:将偏好发送到后端,保存到用户配置 api.saveUserPreference({ language: lng }); // 4. 可选:设置自定义cookie Cookies.set('preferred_language', lng, { expires: 365 }); }实操心得:语言持久化是个需要权衡的问题。只存在Cookie里,用户换设备就失效。存到后端用户配置里,又需要用户已登录。一个常见的混合策略是:未登录用户使用Cookie+路径检测;已登录用户优先使用后端配置,并在登录时同步到前端Cookie和i18n实例。
5. 工程化与协作流程
5.1 翻译文件的管理与自动化
当项目有几十个页面、上百个组件时,翻译键值对可能达到数千条。手动维护JSON文件很快就会变成噩梦。你需要引入工程化管理。
提取翻译键:使用i18next-scanner或i18next-parser这样的工具,它们可以扫描你的源代码(.js,.jsx,.ts,.tsx)中的t(‘…’)函数调用,自动提取出所有键,并合并或更新到对应的JSON翻译文件中。这可以集成到你的构建流程或Git钩子中。
# 示例 package.json 脚本 "scripts": { "i18n:extract": "i18next-parser --config i18next-parser.config.js" }翻译协作平台:对于有专业翻译团队或需要产品经理参与的项目,可以考虑使用像locize,phrase,crowdin这样的商业化平台,或者开源方案如pontoon。它们通常提供Web界面、翻译记忆、机器翻译集成和与代码仓库的同步功能。i18next有与这些平台对接的后端插件(如i18next-locize-backend),可以实现开发环境实时更新翻译,而无需重新构建。
5.2 类型安全与键名验证
在大型项目中,最大的痛点之一是:敲错了翻译键名,直到运行时才发现显示的是键名本身。为了解决这个问题,强烈推荐使用TypeScript和i18next的类型定义来获得类型安全。
首先,为你的翻译资源定义类型。你可以手动创建,但更高效的是使用工具自动生成。
// i18next.d.ts 或类似文件 import ‘i18next’; import common from ‘./public/locales/en/common.json’; import homepage from ‘./public/locales/en/homepage.json’; declare module ‘i18next’ { interface CustomTypeOptions { defaultNS: ‘common’; resources: { common: typeof common; homepage: typeof homepage; }; } }然后,在你的组件中使用时,t函数就会对你的键名进行智能提示和类型检查:
const { t } = useTranslation(‘common’); t(‘header.title’); // ✅ 正确,有提示 t(‘header.titlle’); // ❌ TypeScript报错:属性‘titlle’不存在如果你不想用TypeScript,也可以使用ESLint插件eslint-plugin-i18next,它可以在代码检查阶段检测未定义的翻译键。
5.3 测试策略
国际化应用的测试需要额外关注:
- 单元测试:测试组件时,需要为
i18next和useTranslationHook提供模拟。react-i18next(next-i18next基于它)提供了测试工具。 - 快照测试:确保UI在不同语言下的渲染结果符合预期。注意快照中可能包含动态数据(如日期),需要固定。
- 端到端测试:使用Cypress或Playwright等工具,模拟用户切换语言,并断言页面内容是否正确变化。需要特别注意等待翻译加载完成。
- 翻译覆盖率:确保每种语言的所有键都有对应的翻译值,没有缺失。可以写一个简单的Node.js脚本,比较不同语言JSON文件的键是否一致。
6. 常见陷阱、问题排查与调试技巧
6.1 翻译未显示,显示键名(如“common:header.title”)
这是最常见的问题,根本原因是i18next没有找到对应键的翻译。
排查步骤:
- 检查命名空间:确认
useTranslation(‘common’)传入的命名空间,是否与键所在的文件(common.json)匹配。 - 检查键路径:确认
t(‘header.title’)的路径,在JSON中是否确实是{ “header”: { “title”: “…” } }。注意大小写和标点。 - 检查语言是否加载:在开发工具控制台查看网络请求,确认当前语言的翻译文件(如
/locales/zh-CN/common.json)是否成功加载。如果没有,检查next-i18next.config.js中的localePath配置,以及服务器文件服务是否正确。 - 检查初始状态:在组件挂载后,打印
i18n.language和i18n.hasResourceBundle(i18n.language, ‘common’),确认实例的语言和资源包状态。 - 使用
debug模式:在配置中设置debug: true,i18next会在控制台输出详细的加载和查找信息,非常有用。
6.2 语言切换后,部分组件没有更新
这通常是因为该组件没有订阅i18next的语言变化事件。
解决方案:
- 确保所有需要国际化的组件都使用了
useTranslationHook。这个Hook内部会订阅语言变化,并触发组件重新渲染。 - 如果是在类组件中,可以使用
withTranslation高阶组件,或者使用I18nextProvider上下文。 - 检查是否有组件被
React.memo包裹且依赖项列表中没有包含i18n相关的值。如果组件的渲染只依赖于不变的props,语言变化时它不会更新。你需要将t函数返回的结果或i18n.language加入到依赖项中。
6.3 静态导出(next export)与多语言
next export用于生成纯静态HTML文件。next-i18next完全支持静态导出,但需要理解其行为。
当你运行next build && next export时,Next.js会为每个页面和每种语言的组合生成一个独立的HTML文件。例如,pages/about.js会生成:
out/about.html(默认语言,如en)out/zh-CN/about.htmlout/de/about.html
关键点:
- 你需要一个静态文件服务器(如Nginx)来根据访问路径提供正确的HTML文件。通常需要配置重写规则,将根路径
/重写到默认语言页面(如/en/index.html)。 - 客户端路由(
next/link)在静态导出后仍然工作,但语言切换(i18n.changeLanguage)可能会触发对新的语言HTML文件的完整页面请求,而不是单页应用内的无刷新切换,这取决于你的服务器配置和next-i18next的版本。对于纯静态站点,更常见的做法是使用简单的<a>标签链接到不同语言的页面,或者使用支持SPA路由的静态托管服务(如Vercel、Netlify)。
6.4 性能问题:翻译文件过大
如果你的应用支持10种语言,每种语言有1MB的翻译文本,那么初始加载所有语言显然是不可接受的。
优化策略:
- 按命名空间分割:如前所述,这是最基本且有效的策略。
- 按路由代码分割:结合Next.js的动态导入和
i18next的loadNamespaces,实现翻译文件的懒加载。 - 压缩JSON:在构建时,可以考虑对翻译JSON文件进行压缩(移除空格、缩短键名),但这会降低可读性,通常与翻译管理平台不兼容。
- 使用CDN:将翻译文件托管到CDN,利用HTTP/2多路复用和缓存。
- 评估必要性:是否真的需要支持这么多语言?是否所有文本都需要翻译?对于用户生成内容(UGC),或许可以保留原语言。
6.5 与App Router的兼容性问题
如前所述,Next.js 13+的App Router引入了全新的数据获取和组件模型(Server Components)。next-i18next的最新版本(v13+)已经重构以支持App Router,但API和配置方式与pages/路由器有较大差异。
主要变化:
- 不再需要
_app.js中的appWithTranslation。 - 配置从
next-i18next.config.js移到了next.config.js中,并且格式略有不同。 - 在Server Component中,你需要使用异步的
getTranslation函数来获取t函数。 - 在Client Component中,你仍然使用
useTranslation,但需要确保该组件文件开头有‘use client’指令。
迁移建议:如果你的项目是基于pages/的且稳定运行,除非有强烈理由,否则不必急于迁移到App Router。如果你是新项目,并且决定使用App Router,请务必仔细阅读next-i18next官方文档中关于App Router的章节,因为其模式仍在不断演进中。
7. 总结与个人实践心得
折腾过好几套国际化方案,从最初手写一个简单的<IntlProvider>上下文,到使用react-intl,最终锚定在i18next/next-i18next上,核心原因就一个:它考虑到了生产级应用的所有细枝末节。它不仅仅是一个库,更是一套经过大量项目验证的最佳实践集合。
我最欣赏它的一点是清晰的关注点分离。翻译文件是纯JSON,可以由非开发人员管理;检测、加载、插值、格式化这些脏活累活由框架完成;开发者只需要在正确的生命周期(getStaticProps)调用一个函数,在组件里用一个Hook,就能获得完整的国际化能力。这种设计让代码易于维护和测试。
在实际项目中,我强烈建议在项目初期就引入next-i18next,并建立好翻译文件的提取和同步流程。如果等到项目后期,UI文本散落在几百个组件里时再来做国际化,那将是一场重构噩梦。另外,一定要用上TypeScript的类型定义,它能帮你避免大量低级错误,提升开发效率。
关于性能,我的经验是:默认按命名空间分割,对首屏关键路径的命名空间进行服务端预加载,对非关键路径或交互后内容使用动态加载。这个策略在绝大多数场景下都能取得很好的平衡。
最后,国际化不仅仅是技术实现,更是产品和设计思维。不同的语言文本长度差异巨大(德语通常比英语长,中文通常较短),这会影响UI布局。日期、数字、货币的格式千差万别。甚至颜色、图标在某些文化中可能有不同含义。作为开发者,我们需要和产品、设计同学紧密协作,构建一个真正具有“全球感”的应用,而i18next/next-i18next为我们提供了实现这一目标的坚实技术基础。