前言
背景:在工作中需要搭建一个企业官网,实现国际化功能
项目场景:企业官网,需要多语言
技术选型:Next.js App Router + next-intl + ts
一、国际化整体思路
路由级别:所有页面路径都带语言前缀(如 /en/products)
文案管理:本地 JSON 词典
组件支持:语言切换器
二、核心代码
1. 本地 json 字典创建
创建如下目录,以完成本地国际化字典的创建
tree
locales
├── index.ts
├── zh.json
├── en.json
├── es.json
├── fr.json
└── ru.json
2. index.ts
2.1 导出语言类型
2.2 设置默认语言
2.3 通过locale拿到对应的字典
2.4 返回Message类型
ts
export const locales = ["zh", "en", "fr", "es", "ru"] as const;
export type Locale = typeof locales[number];
export const defaultLocale: Locale = "zh";
export async function getDictionary(locale: Locale) {
let cur: any;
switch (locale) {
case 'en':
cur = (await import('./en.json')).default; break;
case 'fr':
cur = (await import('./fr.json')).default; break;
case 'es':
cur = (await import('./es.json')).default; break;
case 'ru':
cur = (await import('./ru.json')).default; break;
case 'zh':
cur = (await import('./zh.json')).default; break;
default:
cur = (await import('./zh.json')).default; break;
}
return cur;
}
export type Messages = Awaited<ReturnType<typeof getDictionary>>;
as const
将元素定义为字面类型,此处的意思为,"zh"
不再是string
类型,而是"zh"
类型typeof locales[number]
此为索引访问,将typeof locales
存在数组中的类型转为联合类型cur = (await import('./zh.json')).default;
动态导入 json 文件,json 文件通常为默认导出,所以取default
typeof getDictionary
用来获取getDictionary
的数据类型,此处结果为(locale: Locale) => Promise<any>
ReturnType<typeof getDictionary>
提取函数的返回类型,此处结果为Promise<any>
Awaited<ReturnType<typeof getDictionary>>
解开Promise
类型,获取其解析的类型,此处结果为Messages
3. 本地国际化的路由配置 next-intl/routing
3.1 导入 defineRouting
3.2 设置支持的语言,默认语言,语言前缀
ts
import { defineRouting } from 'next-intl/routing';
import { locales as supportedLocales, defaultLocale } from '@/locales';
export const routing = defineRouting({
locales: [...supportedLocales],
defaultLocale: defaultLocale,
localePrefix: 'always'
});
locales as supportedLocales
将导出的locales
赋予别名supportedLocales
defineRouting
为next-intl
的方法,用到的配置参数有:
2.1locales
:国际化支持的全部语言
2.2defaultLocale
:当没有语言时的默认语言
2.3localePrefix
:默认情况下,应用的路径名将在与目录结构匹配的前缀下提供(例如/en/about
→app/[locale]/about/page.tsx)
默认值为always
了解更多
4. 国际化中间件配置 next-intl/middleware
4.1 createMiddleware
为 next-intl
提供的方法,可以通过此方法创建一个中间件
4.2 可以在 config
中去配置匹配器 matcher
来表明需要应用此中间件的路由
ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: [
'/((?!_next|.*\\..*|api).*)'
]
};
此文件应处于项目根目录或者src目录下
了解更多
5. 服务器和客户端组件 next-intl/server
5.1 获取语言并判断合法性 5.2 获取国际化字典 5.3 返回语言和对应的字典
ts
import { getRequestConfig } from 'next-intl/server';
import { routing } from './routing';
import type { Locale } from '@/locales';
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
if (!locale || !routing.locales.includes(locale as Locale)) {
locale = routing.defaultLocale;
}
const messages = (await import(`../locales/${locale}.json`)).default;
return {
locale,
messages
};
});
i18n/request.ts
可用于为仅服务器代码提供配置,配置通过getRequestConfig
函数提供NextIntlClientProvider
可用于为客户端组件提供配置
了解更多
6. 引入国际化navigation next-intl/navigation
ts
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
export const {Link, redirect, usePathname, useRouter, getPathname} = createNavigation(routing);
7. 多语言切换组件
7.1 定义选项数据
7.2 实现路由跳转
7.3 定义item和点击事件
7.4 预览语言的lable
ts
export const LanguageSwitcher: FC<{ lang: Locale; messages: Messages }> = ({
lang,
messages,
}) => {
const router = useRouter();
const pathname = usePathname();
const options = useMemo(() => {
return (locales as readonly string[]).map((code) => ({
code,
label:
messages.language?.[code as keyof typeof messages.language] ??
code,
}));
}, [messages.language]);
const navigateTo = (next: Locale) => {
router.replace({ pathname: pathname || '/'}, { locale: next });
};
const items: MenuProps["items"] = options.map((opt) => ({
key: opt.code,
label: (
<div className="flex items-center gap-2">
<span>{opt.label}</span>
{opt.code === lang && <Check size={16} />}
</div>
),
}));
const onClick: MenuProps["onClick"] = ({ key }) => {
navigateTo(key as Locale);
};
const currentLabel = useMemo(() => {
const map = messages.language as undefined | Record<string, string>;
const native = map?.[lang] ?? lang.toUpperCase();
const prefixMap: Record<Locale, string> = {
zh: 'China',
en: 'English',
fr: 'France',
es: 'Spain',
ru: 'Russia',
};
return `${prefixMap[lang]}-${native}`;
}, [messages.language, lang]);
return (
<Dropdown
menu={{ items, onClick }}
trigger={["hover"]}
placement="bottomRight"
className="-mr-4"
>
<AntButton
type="text"
size="middle"
aria-label={messages.ui?.selectLanguage ?? "Select Language"}
icon={<LanguagesIcon size={18} />
}
>
<span className="ml-1 text-sm text-[#555]">{currentLabel}</span>
</AntButton>
</Dropdown>
);
};
8. 国际化使用
8.1 在服务端组件中-同步组件使用 next-intl
提供的同步方法: learn more
ts
import {useTranslations} from 'next-intl';
export default function HomePage() {
const t = useTranslations('xxx');
return <h1>{t('xxx')}</h1>;
}
8.2 在服务端组件中-异步组件使用 next-intl
提供的异步方法: learn more
ts
import {getTranslations} from 'next-intl/server';
export default async function ProfilePage() {
const t = await getTranslations('xxx');
return <h1>{t('xxx')}</h1>;
}
8.3 在客户端组件中,使 NextIntlClientProvider
处于客户端组件的上游:learn more
ts
import {NextIntlClientProvider} from 'next-intl';
export default async function RootLayout(/* ... */) {
return (
<html lang={locale}>
<body>
<NextIntlClientProvider>...</NextIntlClientProvider>
</body>
</html>
);
}
三、常见坑与解决方案
To be continue
四、效果展示
To be continue
五、总结
To be continue