插件
i18n Ally 插件id: lokalise.i18n-ally
支持react-i18next和next-intl 会自动扫描语言包 给出代码提示
对语言包的格式有要求 必须把同一个语言的翻译写在同一个json里 而不能分成不同的json
并且翻译函数必须是t
react项目
使用react-i18next进行国际化
依赖
react-i18next i18next i18next-browser-languagedetector
配置i18next
resource/
语言包
i18n-types
import locales from './resource/zh.json'
declare module 'i18next' {
interface CustomTypeOptions {
resources: typeof locales
}
}
拓展模块类型 在useTranslation的时候有类型提示
index
import { initReactI18next } from 'react-i18next'
import i18n, { ReadCallback } from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import { pick } from 'lodash-es'
/** 支持的语言 */
export const supportedLngs = ['zh', 'en'] as const
/** 支持的语言 */
export type SupportedLng = (typeof supportedLngs)[number]
i18n
.use(LanguageDetector)
.use(initReactI18next)
// 自定义backend插件 不需要手动将语言包插入
.use({
type: 'backend',
// lng已经被i18n转化过
// ns==='translation'表示全部命名空间
read: async (lng: SupportedLng, ns: string, cb: ReadCallback) => {
import(`./resource/${lng}.json`)
.then((res) => res.default)
.then((data) => (ns === 'translation' ? data : pick(data, ns)))
.then((resource) => cb(null, resource))
.catch((e) => cb(e, null))
},
})
.init({
// 会将字符串自动转化到supportedLngs
supportedLngs,
nonExplicitSupportedLngs: false,
fallbackLng: 'en',
interpolation: {
escapeValue: false,
skipOnVariables: false,
},
detection: {
// 缓存相关 从navigator获取语言 然后存在lcoalStorage里
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
lookupLocalStorage: 'i18n_lang',
},
})
antd和dayjs的locale
import { FC, PropsWithChildren, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { App, ConfigProvider } from 'antd'
import type { Locale } from 'antd/es/locale'
import antd_en_US from 'antd/es/locale/en_US'
import antd_zh_CN from 'antd/es/locale/zh_CN'
import { StyleProvider } from '@ant-design/cssinjs'
import dayjs from 'dayjs'
import 'dayjs/locale/zh-cn'
import { Subject } from 'rxjs'
import { match } from 'ts-pattern'
import { SupportedLng } from '@/locales'
/** antd 首屏样式 样式兼容 本地化 主题等 */
export const AntdProvider: FC<PropsWithChildren> = (props) => {
const { i18n } = useTranslation()
const [local, setLocale] = useState<Locale>()
const lng$ = useMemo(() => {
const _lng$ = new Subject<SupportedLng>()
_lng$.subscribe((lng) => {
setLocale(
match(lng)
.with('zh', () => antd_zh_CN)
.with('en', () => antd_en_US)
.exhaustive(),
)
dayjs.locale(
match(lng)
.with('zh', () => 'zh-cn')
.with('en', () => 'en')
.exhaustive(),
)
})
return _lng$
}, [])
useEffect(() => {
lng$.next(i18n.language as SupportedLng)
const onLanguageChanged = (lng: string) => {
lng$.next(lng as SupportedLng)
}
i18n.on('languageChanged', onLanguageChanged)
return () => {
i18n.off('languageChanged', onLanguageChanged)
}
}, [i18n, lng$])
return (
<StyleProvider layer>
<ConfigProvider locale={local}>
<App className='app'> {props.children}</App>
</ConfigProvider>
</StyleProvider>
)
}
export default AntdProvider
使用
const t = useTranslation()
注意事项
在语言包未加载好时 react-i18next默认进入suspense
在根路由最好加入一个Suspense
next项目
比较通用的国际化策略是为每个语言提供不同的url前缀 例如/zh/xxx /en/xxx 参考mdn
这也是next-intl默认的方式
配置
基础配置
next.config.ts
import {NextConfig} from 'next';
import createNextIntlPlugin from 'next-intl/plugin';
const nextConfig: NextConfig = {};
const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);
@/i18n/
按照文档 进行配置
注意 只执行Initial setup这一节的内容
修改@/i18n/request.ts
import { Formats, hasLocale } from 'next-intl'
import { getRequestConfig } from 'next-intl/server'
import { routing } from './routing'
// 日期时间货币等
export const formats: Formats = {}
export default getRequestConfig(async ({ requestLocale }) => {
const requested = await requestLocale
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
// 获取语言包
const { default: messages } = await import(`@/../messages/${locale}.json`)
return {
locale,
messages,
formats,
}
})
类型配置
创建@/i18n/type.ts
import locales from '@/../messages/zh.json'
import { formats } from './request'
import { routing } from './routing'
/** 具有locale属性的路径参数 */
export type LocaleParams = {
params: Promise<{ locale: Locale }>
}
declare module 'next-intl' {
interface AppConfig {
Locale: (typeof routing.locales)[number]
Messages: typeof locales
Formats: typeof formats
}
}
客户端组件的上下文
在app/[loacle]/layout里添加 NextIntlClientProvider 为客户端提供国际化信息
参考文档
国际化的404页面
app/[loacle]/[...rest]/page这是实际上的404页面 这个页面可以使用国际化相关内容
无法国际化的情形
proxy.ts会把页面重定向到/[locale] 但这是有条件的. 无法重定向的情况需要app/下的文件进行处理
not-found
如果url像/xxx.txt 则不会被重定向 因此需要全局的not-found页面 即app/not-found.这个页面无法国际化
page
如果打包为静态html(即,next配置中设置output为export) proxy.ts就不会生效 此时/对应的是 /page 这个组件需要对页面重定向
'use client'
import { redirect } from 'next/navigation'
import { FC, useEffect } from 'react'
import { Skeleton } from 'antd'
import { routing } from '@/i18n/routing'
const Page: FC = () => {
useEffect(() => {
const lng =
navigator.languages.find((l) => routing.locales.includes(l as any)) ?? routing.defaultLocale
redirect(`/${lng}`)
}, [])
return (
<html lang='en'>
<body>
<Skeleton className='p-4' active />
</body>
</html>
)
}
export default Page
layout
这个页面无法国际化 所以不能设置html的lang属性 因此直接导出React.Fragment即可
使用next-intl
通用
以下客户端和服务端都可以用
@/i18n/routing可以获取国际化配置 例如所有支持语言等@/i18n/navigation具有国际化能力的路由组件和函数next-intl
import { type Locale, hasLocale } from 'next-intl'
支持语言的类型 判断语言是否合法
服务端
包括但不限于服务端组件、metadata的生成函数、server action 都可以使用同一套api
// 列举常见函数
import { getFormatter, getMessages, getLocale, getTranslations } from 'next-intl/server'
这些函数全都是异步函数
const locale = await getLocale() // 获取当前的语言
const t = await getTranslations() // 获取翻译函数
t('common.test')
可以生成国际化的元数据(例如标签页title) 也可以用于服务端组件的翻译
客户端
客户端是与服务端很相似的一系列hook
import { useLocale, useMessages, useFormatter } from 'next-intl'
dayjs的国际化
dayjs.lcoale与渲染完全无关 可以在组件内部直接调用 不需要在useEffect里面调用
打包为静态html
- next.config.js 将output设置为export
- 确保
app/page具有重定向能力 - 删除
/[locale]/[...rest]/page. 这种收集全部的动态路由无法被打包为静态html - 在
/[locale]/layout添加以下导出
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }))
}
这个函数用于配置SSG 打包为静态html需要指明动态路由的所有可能情况
- 改造
/[locale]/layout假设有一个环境变量EXPORT控制是否静态导出
import { setRequestLocale } from 'next-intl/server'
const RootLayout: FC<PropsWithChildren> = async (props) => {
// 不能放在FC的类型里面 过不了类型检查
const { children, params } = props as PropsWithChildren & {
params: Promise<{ locale: Locale }>
}
const locale = await match(process.env.EXPORT === 'true')
.with(true, async () => {
// 静态html
const { locale } = await params
return locale
})
.with(false, getLocale)
.exhaustive()
if (!hasLocale(routing.locales, locale)) {
notFound()
}
if (process.env.EXPORT === 'true') {
setRequestLocale(locale)
}
return (
<html lang={locale}>xxx</html>
)
}
原理:
服务端获取国际化信息时 都是默认从headers里获取的 但是静态导出是不能调用header的 因此要从路径参数locale里获取
而setRequestLocale可以强制设置locale
只有在执行setRequestLocale后 服务端调用getXXX才不会出错
- 元数据
生产环境 元数据可能和页面不是同一个上下文 需要从路径中获取国际化信息
import { type LocaleParams } from '@/i18n/type'
export async function generateMetadata(props: LocaleParams): Promise<Metadata> {
const { params } = props
const { locale } = await params
const t = await getTranslations({ locale })
return {
title: t('metadata.title'),
}
}