本文主要介绍基于Next.js的国际化解决方案,会根据 2 种打包模式分别介绍不同的集成方式
项目地址:zhaoth/React-Next-Admin (github.com)
以 Nodejs 或者 docker 的部署方式国际化集成,分支参考feature/i18n,静态导出模式基于 master
线上地址:react-next-admin.pages.dev
Static Exports部署方式国际化集成
next.config.js中去掉 output:export 也可以直接服务部署
安装next-intl
npm install next-intl
next-intl 国内目前文档无法正常访问
设置 next.config.js
添加 output: 'export'配置,由于此配置,当运行 npm run build 时,Next.js 将在 out 文件夹中生成静态 HTML/CSS/JS 文件。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
设置国际化文件结构
创建翻译文件
export default {
welcome: 'Welcome',
dashboard: 'Dashboard',
list: 'List',
ahookList: 'ahookList',
proList: 'proList',
};
引入新添加的翻译文件
import login from './locales/en/login';
import menu from '@/i18n/locales/en/menu';
export default {
login,
menu,
};
更新文件结构
首先,创建 [locale] 文件夹并移动其中的现有 page.tsx 文件、layout.tsx 文件和 about 文件夹。不要忘记更新导入。
然后在跟目录的 page.tsx中将用户重定向到默认语言,如下:
'use client';
import { redirect } from 'next/navigation';
import useSettingStore from '@/store/useSettingStore';
import { staticRouter } from '@/static/staticRouter';
export default function Home() {
const defaultLocale = useSettingStore((state) => state.defaultLocale);
// 静态 build 模式下 不能用 next/router 需要用next/navigation
redirect(`/${defaultLocale}/${staticRouter.login}`);
}
注意:使用静态导出时,不能在没有前缀的情况下使用默认区域设置。我们必须将传入的请求重定向到默认语言。如文档中所述。
更新 app/[locale]/layout.tsx
import { EmptyLayout } from '@/components';
import React from 'react';
import { Props } from '@/typing/Layout';
import { locales } from '@/static/locales';
//function to generate the routes for all the locales
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default function Layout({ children, params: { locale } }: Props) {
return (
<>
<EmptyLayout params={{
locale: locale,
}}>{children}</EmptyLayout>
</>
);
}
EmptyLayout.tsx
import '@/app/globals.css';
import AntdStyledComponentsRegistry from '@/lib/antd-registry';
import React from 'react';
import { NextIntlClientProvider } from 'next-intl';
import { Props } from '@/typing/Layout';
import en from '@/i18n/en';
import zh from '@/i18n/zh';
import { timeZone } from '@/static/locales';
export const metadata: { title: string, description: string } = {
title: 'React Next Admin',
description: '',
};
export default function EmptyLayout({ children, params: { locale } }: Props) {
const messages = locale === 'en' ? en : zh;
return (
<NextIntlClientProvider locale={locale} messages={messages} timeZone={timeZone}>
<AntdStyledComponentsRegistry>
{children}
</AntdStyledComponentsRegistry>
</NextIntlClientProvider>
);
}
在这里我们除了往布局里面传递了 children 之后还传递了一个params参数,并添加 generateStaticParams 函数以生成所有区域设置的静态路由,同时我们在 emptylayout添加上下文提供程序 NextIntlClientProvider
更新页面和组件以使用翻译
'use client'
import { useTranslations } from 'next-intl'
export default function HomePage() {
const t = useTranslations('HomePage')
return (
<div>
{t('helloWorld')}
</div>
)
}
添加“use client”(截至目前,仅在客户端组件中支持使用 next-intl 的翻译)导入 useTranslations 钩子并在我们的 jsx 中使用它
创建语言切换组件
import React, { useState } from 'react';
import { Group } from 'antd/es/radio';
import { usePathname as useIntlPathname, useRouter as useIntlRouter } from '@/lib/language';
import useSettingStore from '@/store/useSettingStore';
import { RadioChangeEvent } from 'antd';
export default function ChangeLanguage() {
const options = [
{ label: 'EN', value: 'en' },
{ label: '中', value: 'zh' },
];
const intlPathname = useIntlPathname();
const intlRouter = useIntlRouter();
const setDefaultLocale = useSettingStore((state) => state.setDefaultLocale);
const defaultLocale = useSettingStore((state) => state.defaultLocale);
const [value, setValue] = useState(defaultLocale);
const onLanguageChange = ({ target: { value } }: RadioChangeEvent) => {
setValue(value);
setDefaultLocale(value);
intlRouter.replace(intlPathname, { locale: value });
};
return (
<>
<Group options={options} onChange={onLanguageChange} value={value} key={value}>
</Group>
</>
);
}
封装基于国际化的Link, redirect, usePathname, useRouter
由于静态导出模式,用 nextjs 自带的路由跳转的时候都必须添加 locale 较为麻烦下面是基于next-intl的createLocalizedPathnamesNavigation封装的路由,用法和 nextjs 路由一致
export const { Link, redirect, usePathname, useRouter } =
createLocalizedPathnamesNavigation({
locales,
pathnames,
localePrefix,
});
以 Nodejs 或者 docker 的部署方式国际化集成
安装依赖
npm install react-i18next i18next i18next-browser-languagedetector i18next-resources-to-backend
accept-language
目录结构
添加 i18n配置文件夹及相关配置
其中包括语言文件 json 和 i18n 国际化设置工具类
语言json
{
"title": "Hi there!",
"to-second-page": "To second page"
}
setting.ts
国际化初始设置文件
export const fallbackLng = 'en'
export const languages = [fallbackLng, 'zh']
export const defaultNS = 'translation'
export function getOptions (lng = fallbackLng, ns = defaultNS) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns
}
}
client.ts
封装组件中调用的useTranslation方法
'use client'
import { useEffect, useState } from 'react'
import i18next from 'i18next'
import { initReactI18next, useTranslation as useTranslationOrg } from 'react-i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import LanguageDetector from 'i18next-browser-languagedetector'
import { getOptions,languages } from '@/i18n/settings'
const runsOnServerSide = typeof window === 'undefined'
// 在客户端,正常的单例模式是可以的
i18next
.use(initReactI18next)
.use(LanguageDetector)
// @ts-ignores
.use(resourcesToBackend((language, namespace) => import(`./locales/${language}/${namespace}.json`)))
.init({
...getOptions(),
lng: undefined, // 在客户端检测语言
detection: {
order: ['path', 'htmlTag', 'cookie', 'navigator'],
},
preload: runsOnServerSide ? languages : []
})
export function useTranslation(lng:any, ns:any, options?:any) {
const ret = useTranslationOrg(ns, options)
const { i18n } = ret
if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
i18n.changeLanguage(lng)
} else {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [activeLng, setActiveLng] = useState(i18n.resolvedLanguage)
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (activeLng === i18n.resolvedLanguage) return
setActiveLng(i18n.resolvedLanguage)
}, [activeLng, i18n.resolvedLanguage])
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (!lng || i18n.resolvedLanguage === lng) return
i18n.changeLanguage(lng)
}, [lng, i18n])
}
return ret
}
添加middleware
nextjs 的特性之一,基于中间件拦截i18n 并进行转发
'use client'
import { NextResponse } from 'next/server'
import acceptLanguage from 'accept-language'
import { fallbackLng, languages } from '@/i18n/settings'
acceptLanguage.languages(languages)
export const config = {
// matcher: '/:lng*'
matcher: ['/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)']
}
const cookieName = 'i18next'
export function middleware(req:any) {
let lng
if (req.cookies.has(cookieName)) lng = acceptLanguage.get(req.cookies.get(cookieName).value)
if (!lng) lng = acceptLanguage.get(req.headers.get('Accept-Language'))
if (!lng) lng = fallbackLng
// Redirect if lng in path is not supported
if (
!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith('/_next')
) {
return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
}
if (req.headers.has('referer')) {
const refererUrl = new URL(req.headers.get('referer'))
const lngInReferer = languages.find((l) => refererUrl.pathname.startsWith(`/${l}`))
const response = NextResponse.next()
if (lngInReferer) response.cookies.set(cookieName, lngInReferer)
return response
}
return NextResponse.next()
}
页面中使用国际化
初始化组件时添加语言参数
export default function Login({ params: { lng } }
引用封装好的useTranslation,使用时添加对应语言文件包
const { t } = useTranslation(lng, 'login')
页面中使用
<ProFormCheckbox noStyle name="autoLogin">
{t('title')}自动登录
</ProFormCheck