上次博客记录了如何在 Next.js 中使用 i18n 来为网站添加国际化。但是上次的方案存在一个问题:「利用了 Url 中的路由作为语言参数传入」。这个情况好的一点是在分享链接的时候可以附带语言;坏的一点是观感不好,而且链接复杂。
为此,这篇文章寻找到了使用 Cookie 来作为语言参数的方案。
安装依赖
pnpm install i18next react-i18next i18next-resources-to-backend目录及相关源码
- i18n
- locales
- en
- zh-CN
- ja-JP
...ts files该目录具体可以随意定义,下面是相关说明和源码:
首先是各种语言的翻译文件,使用 json 格式,示例参考如下,你可以根据需要添加更多的语言文件:
// i18n/locales/en/common.json // zh-CN or ja-JP are also available
{
"hello": "Hello",
"welcome": "Good Morning! {{name}}"
}然后是定义一个公用的配置文件,首先是
settings.ts,定义了语言的默认值、支持的语言、语言的 Cookie 名称等;如果你需要获得更多信息可以参考 https://www.i18next.com/overview/configuration-options// i18n/settings.ts
import type { InitOptions } from 'i18next';
export const FALLBACK_LOCALE = 'en';
export const supportedLocales = ['en', 'zh-CN', 'ja-JP'] as const;
export type Locales = (typeof supportedLocales)[number];
export const LANGUAGE_COOKIE = 'chosen_language';
export function getOptions(lang = FALLBACK_LOCALE, ns = 'common'): InitOptions {
return {
// debug: true,
supportedLngs: supportedLocales,
fallbackLng: FALLBACK_LOCALE,
lng: lang,
ns,
};
}
export const languages = [
{ value: 'en', label: 'English' },
{ value: 'zh-CN', label: '简体中文' },
{ value: 'ja-JP', label: '日本語' },
] as const;接着是服务端组件使用的初始化文件,
server.ts:// i18n/server.ts
import {createInstance} from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import {initReactI18next} from 'react-i18next/initReactI18next';
import {FALLBACK_LOCALE,getOptions,Locales,LANGUAGE_COOKIE} from './settings';
import {cookies} from 'next/headers';
async function initI18next(lang: Locales, namespace: string) {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(lang: string, ns: string) => import(`./locales/${lang}/${ns}.json`),
),
)
.init(getOptions(lang, namespace));
return i18nInstance;
}
export async function createTranslation(ns: string) {
const lang = getLocale();
const i18nextInstance = await initI18next(lang, ns);
return {
t: i18nextInstance.getFixedT(lang, Array.isArray(ns) ? ns[0] : ns),
};
}
export function getLocale() {
return (cookies().get(LANGUAGE_COOKIE)?.value ?? FALLBACK_LOCALE) as Locales;
}
接着是客户端组件使用的初始化文件,
client.ts:// i18n/client.ts
'use client';
import {useEffect} from 'react';
import i18next, {i18n} from 'i18next';
import {initReactI18next, useTranslation as useTransAlias} from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import {
Locales,
LANGUAGE_COOKIE,
getOptions,
supportedLocales,
} from './settings';
import {useLocale} from './locale-provider';
const runsOnServerSide = typeof window === 'undefined';
i18next
.use(initReactI18next)
.use(
resourcesToBackend(
(lang: string, ns: string) => import(`./locales/${lang}/${ns}.json`),
),
)
.init({
...getOptions(),
lng: undefined,
detection: {
order: ['cookie'],
lookupCookie: LANGUAGE_COOKIE,
caches: ['cookie'],
},
preload: runsOnServerSide ? supportedLocales : [],
});
export function useTranslation(ns: string) {
const lng = useLocale();
const translator = useTransAlias(ns);
const {i18n} = translator;
if (runsOnServerSide && lng && i18n.resolvedLanguage !== lng) {
i18n.changeLanguage(lng);
} else {
useCustomTranslationImplem(i18n, lng);
}
return translator;
}
function useCustomTranslationImplem(i18n: i18n, lng: Locales) {
useEffect(() => {
if (!lng || i18n.resolvedLanguage === lng) return;
i18n.changeLanguage(lng);
}, [lng, i18n]);
}为了使得客户端组件可以获得语言的上下文,需要定义一个
LocaleProvider 组件;同时该组件需要在顶级的布局文件里调用。// i18n/locale-provider.tsx
'use client';
import {createContext, useContext} from 'react';
import {FALLBACK_LOCALE, Locales} from '../i18n/settings';
const Context = createContext<Locales>(FALLBACK_LOCALE);
export function LocaleProvider({
children,
value,
}: {
children: React.ReactNode;
value: Locales;
}) {
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export function useLocale() {
return useContext(Context);
}// app/layout.tsx
import { Inter } from 'next/font/google';
import './globals.css';
import { Providers } from './providers';
import { getLocale } from '@/i18n/server';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const locale = getLocale();
return (
<html lang={locale}>
<body className={inter.className}>
<LocaleProvider value={locale}>
{children}
</LocaleProvider>
</body>
</html>
);
}基本的配置已经完成,接下来我们需要一个
server action 来设置 Cookie 以帮助用户切换语言。// i18n/switch-locale.ts
'use server';
import {cookies} from 'next/headers';
import {LANGUAGE_COOKIE} from './settings';
export async function switchLocaleAction(value: string) {
cookies().set(LANGUAGE_COOKIE, value);
return {success: true};
}最后,我们编写一个客户端组件来调用这个 action 以帮助用户切换语言。
// LanguageSwitcher.tsx
'use client';
import React from 'react';
import { Select, SelectItem } from '@nextui-org/react';
import { switchLocaleAction } from '@/i18n/switch-locale';
import { useTranslation } from '@/i18n/client';
import { languages } from '@/i18n/settings';
export default function LanguageSwitcher() {
const { i18n } = useTranslation('home');
const handleLocaleChange = (value: string) => {
switchLocaleAction(value);
};
return (
<>
<Select
onChange={(e) => handleLocaleChange(e.target.value)}
defaultSelectedKeys={i18n.resolvedLanguage ? [i18n.resolvedLanguage] : []}
placeholder="Select language"
>
{languages.map((language) => (
<SelectItem
key={language.value}>
{language.label}
</SelectItem>
))}
</Select>
</>
);
}大功告成!接下来你可以在你任何需要的地方使用 i18n 了。
假如你拥有一个
test 页面,那么你可以这样使用:// app/test/page.tsx
import { createTranslation } from '@/i18n/server';
export default async function Test() {
const { t } = await createTranslation('common');
return <div>{t('hello')}</div>;
}如果是客户端组件,你可以这样使用:
// app/test/page.tsx
'use client';
import { useTranslation } from '@/i18n/client';
export default function Test() {
const { t } = useTranslation('common');
return <div>{t('welcome', { name: 'Cunoe' })}</div>;
}