來設定 Next 專案的 i18n 功能。
Next App Router 架構納入了 React Server component ,所以要想辦法設定能夠在 Client 跟 Server 兩邊通用的 i18n 架構。
主要參考這篇: i18n with Next.js 13 and app directory / App Router
首先建立動態語系的頁面。
pnpm exec nx generate @nx/next:page --name="[lang]"
安裝套件。
pnpm add -D react-i18next i18next-resources-to-backend
首先來設定 Server Component 的部分,在 src 目錄下新增以下結構。
src
└── i18n
├── locales
│ ├── en
│ │ └── common.json
│ └── zh-TW
│ └── common.json
├── index.ts
└── settings.ts
locales 目錄下根據語系開立目錄,並新增不同 namespace 的語系檔案。
settings 做整體的設定,包含使用語系,預設 namespace 等。
export const fallbackLng = 'en';
export const languages = [fallbackLng, 'zh-TW'];
export const defaultNS = 'common';
export function getOptions(
lng = fallbackLng,
ns = defaultNS as string | string[],
) {
return {
// debug: true,
supportedLngs: languages,
fallbackLng,
lng,
fallbackNS: defaultNS,
defaultNS,
ns,
};
}
index 中載入翻譯檔,設定並生成 i18n 實體,然後製作翻譯函式 useTranslation,這邊雖然用 hook 的命名方式但並不是 hook。
// index.ts
import { createInstance } from 'i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { initReactI18next } from 'react-i18next/initReactI18next';
import { getOptions } from './settings';
const initI18next = async (lng: string, ns?: string | string[]) => {
const i18nInstance = createInstance();
await i18nInstance
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`),
),
)
.init(getOptions(lng, ns));
return i18nInstance;
};
export async function useTranslation(
lng: string,
ns?: string | string[],
options = {} as { keyPrefix?: string },
) {
const i18nextInstance = await initI18next(lng, ns);
return {
t: i18nextInstance.getFixedT(
lng,
Array.isArray(ns) ? ns[0] : ns,
options.keyPrefix,
),
i18n: i18nextInstance,
};
}
然後就能用在 page 上了。
import { useTranslation } from '../../src/i18n';
export interface PageProps {
params: {
lang: string;
};
}
export default async function Page({ params: { lang } }: PageProps) {
const { t } = await useTranslation(lang);
return <div>{t('select')}</div>;
}
Client Component 的部分就用原本 hook 的方式設定。
追加一個 client.ts。
src
└── i18n
├── locales
│ ├── en
│ │ └── common.json
│ └── zh-TW
│ └── common.json
├── index.ts
├── client.ts
└── settings.ts
hook 設定要限定 client side。
'use client';
import { useEffect } from 'react';
import i18next from 'i18next';
import {
initReactI18next,
useTranslation as useTranslationOrg,
} from 'react-i18next';
import resourcesToBackend from 'i18next-resources-to-backend';
import { getOptions, languages } from './settings';
const runsOnServerSide = typeof window === 'undefined';
i18next
.use(initReactI18next)
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`./locales/${language}/${namespace}.json`),
),
)
.init({
...getOptions(),
lng: undefined,
detection: {
order: ['path', 'htmlTag', 'cookie', 'navigator'],
},
preload: runsOnServerSide ? languages : [],
});
export function useTranslation(
lng: string,
ns?: string | string[],
options = {} as { keyPrefix?: string },
) {
const ret = useTranslationOrg(ns, options);
useEffect(() => {
ret.i18n.changeLanguage(lng);
}, [lng]);
return ret;
}
這樣如果 page 設定為 Client Component,就用 hook 的方式。
'use client';
import { useTranslation } from '../../src/i18n/client';
export interface PageProps {
params: {
lang: string;
};
}
export default function Page({ params: { lang } }: PageProps) {
const { t } = useTranslation(lang);
return <div>{t('select')}</div>;
}