i18n
国际化,Internationalization,简写为i18n,
需要注意的点
i18n需要注意很多,不仅仅是语言翻译,还需要从书写习惯、文化、各种不同数据表式、甚至宗教文化等。
比如:
- 货币单位: $、¥、€
- 日期格式:中国2024/5/30、英国05/30/24
- 数字:中10000、英美10,000
- 书写顺序:中国从左到右、阿拉伯国家从右到左
还有其他很多
前后端语言偏好对齐
前端可以将user agent
的语言传给后端:
- 通过请求的header的方式传给后端(
accept-language
或者自定义):Accept-Language: "zh-CN"
- 通过cookie的方式
各个语言偏好的值,建议进行标准化 - 参考《全球各国语言代码缩写与国家对照表》(自己搜一下)
Accept-Language头
Accept-Language
请求 HTTP 标头表示客户端所偏好的自然语言和区域设置。服务器利用内容协商机制从这些提议中选出一项,并通过 Content-Language
响应标头将这一选择告知客户端。浏览器会根据其当前活跃的用户界面语言为该标头设定所需的值。
当服务器无法通过其他方式(比如使用依赖于用户明确决定的特定 URL)确定目标内容语言时,这个标头可作为提示使用。服务器绝不应覆盖用户的明确语言选择。Accept-Language
的内容常常超出用户的控制范围(例如在旅行时)。用户也可能希望访问使用的语言与用户界面并不相同的页面。
但是目前一般不用Accept-Language
来做前端的国际化处理,前端一般会更具域名或者子路径的方式来做国际化处理
// 使用质量价值语法对多个类型进行加权:权重->也就是优先级
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5
前端处理国际化
前端处理国际化需要注意两个东西:
- 需要进行国际化的资源(代码、图片、数据等)
- 前端路由
现代网站基本都把网站的国际化进行分开了,所谓分开,指的是在地址上做标识,而不是通过什么storage、cookie去做标识,比如:
- 力扣:美国(总站)leetcode.com,中国leetcode.cn
- React文档:总站react.dev,中文zh-hans.react.dev
举力扣的例子其实并不太好,因为这是相当于一个公司(业务)针对不同环境做出的不同平台,希望懂得鉴别。
而通过在URL
上做国际化标识,正是前端路由需要注意的地方,两种模式:
- 一种是不同语言不同的域名,比如
react
- 还有一种是放在url的path里,比如
xxx.com/zh/...
Nextjs整合locale
Next.js has built-in support for internationalized (i18n) routing since v10.0.0
. You can provide a list of locales, the default locale, and domain-specific locales and Next.js will automatically handle the routing.
官网摘录,nextjs已经支持i18n
,直接在next.config.js
里面配置就好了,同时支持domain
和sub-path
两种模式:
// url path模式
module.exports = {
i18n: { '
locales: ['en-US', 'fr', 'nl-NL'],
defaultLocale: 'en-US'
}
}
// `/blog`
// `/fr/blog`
// `/nl-nl/blog`
nextjs的就不写了,看文档即可nextjs-i18n
关于nextj如何整合常见的国际化方案比如react-i18next
,其实和其他架子搭起来的项目一样,后面章节会用Vite
起的项目做一个方案。
普通项目整合i18n
目前有两种方案:react-i18next
和react-intl
。
react-i18next
是i18n
的react
实现封装,在下载量和用户量以及资历来说,i18n
都属于老牌。
结合项目fe-demo做国际化
安装
本体:i18next react-i18next
三个插件:
i18next-http-backend
:从服务端拉取翻译配置文件i18next-browser-languagedetector
:浏览器的语言偏好加载相应的国际化资源
pnpm install react-i18next i18next --save
# if you'd like to detect user language and load translation
pnpm install i18next-http-backend i18next-browser-languagedetector --save
还有个插件可以用一下,直接工程本地文件进行打包加载:
pnpm install i18next-resources-to-backend --save
用法:
import i18next from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
i18next
.use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
.init({ /* other options */ })
和http-backend
的区别就是,http-backend
是作为请求,作为静态资源访问,你可以用http-backend
配置到cdn
。而resourcesToBackend
是从打包文件里面读出来的,"当做内存的内容读出来的"。
基础配置写好
// i18n.ts
import i18n from 'i18next'
import languageDetector from 'i18next-browser-languagedetector'
import i18nextHttpBackend from 'i18next-http-backend'
import { initReactI18next } from 'react-i18next'
import { I18N_LANGUAGES } from '@/constants/i18n'
i18n
.use(i18nextHttpBackend)
.use(languageDetector)
.use(initReactI18next) // passes i18n down to react-i18next
.init({
debug: true,
supportedLngs: Object.values(I18N_LANGUAGES),
fallbackLng: I18N_LANGUAGES.ZH_CN,
interpolation: { escapeValue: false },
// i18next-http-backend translation file path
// https://github.com/i18next/i18next-http-backend
backend: {
path: '/public/locales/{{lng}}/{{ns}}.json',
},
})
export default i18n
注意这里官方文档写漏了,需要用I18nextProvider
去包裹提供上下文:
// main.tsx
import App from './App'
import i18n from './i18n'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<I18nextProvider i18n={i18n}>
<BrowserRouter basename={import.meta.env.VITE_PUBLIC_PATH as string}>
<App />
</BrowserRouter>
</I18nextProvider>
</React.StrictMode>,
)
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { I18N_LANGUAGES } from '@/constants/i18n'
const Welcome: FC = () => {
const { t, i18n } = useTranslation('welcome')
return (
<div>
国际化的你好 : {t('nihao')}
<br />
当前语言:{i18n.language}, 切换语言:{' '}
<select value={i18n.language} onChange={(e) => i18n.changeLanguage(e.target.value)}>
<option value={I18N_LANGUAGES.ZH_CN}>中文</option>
<option value={I18N_LANGUAGES.ZH_TW}>繁体</option>
<option value={I18N_LANGUAGES.EN_US}>英文</option>
</select>
</div>
)
}
export default Welcome
路由整合i18n
语言符号的优先级:路由带的locale > i18n存在localestorage的locale > 浏览器语言检测
目前i18n自动完成了后两部分,路由的locale还需要我们自己去写。
统一规定:locale
标识path统一放在路由最前面,即/:locale?/xxxxx
方案:会做一个全局处理,当进入页面的时候执行这个全局处理,这个全局处理需要从路由里面检测locale
,再进行切换语言的操作;注意一个点react-router
的path params
只有在这个path
的children
下才可以请求到,所以我们得在每一个页面的element
下去检测,为了方便我们直接写一个wrapper
:
// i18NRouterWrapper.tsx
import { FC, PropsWithChildren, useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { I18N_DEFAULT_LOCALE, I18N_LANGUAGES } from '@/constants/i18n'
import i18n from '.'
const I18NRouterWrapper: FC<PropsWithChildren> = ({ children }) => {
const { locale = I18N_DEFAULT_LOCALE } = useParams()
useEffect(() => {
const languageTags = Object.values(I18N_LANGUAGES)
if (languageTags.includes(locale as I18N_LANGUAGES)) i18n.changeLanguage(locale)
}, [locale])
return <>{children}</>
}
export default I18NRouterWrapper
在useRoutes
传入路由配置的时候,处理下:
const addLocaleConfig = config.map((item) => {
item.path = item.path?.startsWith('/:locale?') ? item.path : `/:locale?${item.path}`
item.element = <I18NRouterWrapper>{item.element}</I18NRouterWrapper>
return item
})
export default addLocaleConfig
这样路由的整合就可以了
手动切换
结合路由的整合做一个手动切换,考虑路由为最大优先级
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { I18N_DEFAULT_LOCALE, I18N_LANGUAGES } from '@/constants/i18n'
const Welcome: FC = () => {
const { t, i18n } = useTranslation('welcome')
const { locale: routeLocale } = useParams()
const { pathname } = useLocation()
const navigate = useNavigate()
// 手动change
const changeLanguage = (changeLocale: I18N_LANGUAGES) => {
if (changeLocale === i18n.language) return
i18n.changeLanguage(changeLocale)
const pathExcludeLocale = routeLocale ? pathname.replace(`/${routeLocale}`, '') : pathname
if (changeLocale !== I18N_DEFAULT_LOCALE) {
navigate(`/${changeLocale}${pathExcludeLocale}`, { replace: true })
} else {
navigate(pathExcludeLocale, { replace: true })
}
}
return (
<div>
国际化的你好 : {t('nihao')}
<br />
当前语言:{i18n.language}, 切换语言:{' '}
<select value={i18n.language} onChange={(e) => changeLanguage(e.target.value as I18N_LANGUAGES)}>
<option value={I18N_LANGUAGES.ZH_CN}>中文</option>
<option value={I18N_LANGUAGES.ZH_TW}>繁体</option>
<option value={I18N_LANGUAGES.EN_US}>英文</option>
</select>
</div>
)
}
export default Welcome
现有问题:进阶AST
在代码内容中的国际化配置做完,剩下的就是写,有很多种场景:
- 纯文本字段,带默认值
- 含有一定的变量需要替换
- 货币单位
- 文字方向
- 日期格式等
这些场景在i18n-next
官网或者查一下就知道了。
现在的问题是,可能在某一次开发忘了某些地方需要国际化,或者每次开发的时候翻译文件还没准备好,需要怎么处理?
忘了某些地方需要国际化?
思路:结合AST
,开发打包工具插件,在构建打包的时候,对代码做TextNode
和文本检测
,在重新注入i18n国际化的格式。
难点:检测TextNode
和代码中需要进行格式化的地方,场景比较多,比如图片alt
、元素title
等。
翻译文件还没准备好
思路:同样的,开发打包构建工具的插件,在构建打包的时候,在第一个问题能解决的前提下,肯定能知道需要翻译的地方,对上述列举的场景结合大模型AI
,自动翻译和处理好翻译文件,传到远端覆盖翻译文件配置。
难点:自动对齐翻译文件