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