Next.js 中使用 Cookie 进行 i18n 国际化

上次博客记录了如何在 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>; }