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

分类
技术
标签
前端开发

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