ACQUIRING IMAGE…

LOG ENTRYCAT / 技术

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

介绍在 Next.js 中使用 Cookie 替代 URL 路由参数实现 i18n 国际化的方案,提供更简洁的链接和更好的用户体验。

前端开发

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