要在 Nextjs 設定翻譯要分別處理 client component 跟 server component。
先安裝用到的套件。
pnpm add next-intl @tolgee/react
幾個關鍵檔案的分配位置大概像這樣。
├── next.config.js
├── messages
│ ├── en.json
│ └── cs.json
└── src
├── middleware.ts
├── navigation.ts
├── i18n
│ └── request.ts
├── tolgee
│ ├── shared.ts
│ ├── client.tsx
│ └── server.tsx
└── app
└── [locale]
├── layout.tsx
└── page.tsx
首先是將先前 app
底下的頁面中間再多加一層 [locale]
作為語言分別用。
然後 messages
底下的 json 檔案是從 Tolgee 輸出的靜態資料,還沒有東西可以輸出的話先到 Tolgee 的 Translations 頁面新增點什麼。
有資料後就能到 Exports 頁面輸出得到 json 檔案。
再來先設定 Nextjs 的環境變數,需要 Tolgee 的 API key。
NEXT_PUBLIC_TOLGEE_API_KEY=<your api key>
NEXT_PUBLIC_TOLGEE_API_URL="http://localhost:8085"
新增 API key 的頁面在右上角使用者選單中。
如果懶得一直更新 API key 的話記得設定為不會過期。
// src/tolgee/shared.ts
import { DevTools, Tolgee } from "@tolgee/web";
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY;
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL;
export const ALL_LOCALES = ["en", "zh-Hant"];
export const DEFAULT_LOCALE = "en";
export async function getStaticData(
languages: string[],
namespaces: string[] = [""],
) {
const result: Record<string, any> = {};
for (const lang of languages) {
for (const namespace of namespaces) {
if (namespace) {
result[`${lang}:${namespace}`] = (
await import(`../../messages/${namespace}/${lang}.json`)
).default;
} else {
result[lang] = (await import(`../../messages/${lang}.json`)).default;
}
}
}
return result;
}
export function TolgeeBase() {
return Tolgee().use(DevTools()).updateDefaults({
apiKey,
apiUrl,
fallbackLanguage: "en",
});
}
shared
是 client 跟 server 都共用的部分,主要設定有哪些語言,從哪裡取得靜態翻譯資料。
// src/tolgee/client.ts
import { TolgeeBase } from "./shared";
import { TolgeeProvider, useTolgeeSSR } from "@tolgee/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
type Props = {
locales: any;
locale: string;
children: React.ReactNode;
};
const tolgee = TolgeeBase().init();
export const TolgeeNextProvider = ({ locale, locales, children }: Props) => {
// synchronize SSR and client first render
const tolgeeSSR = useTolgeeSSR(tolgee, locale, locales);
const router = useRouter();
useEffect(() => {
const { unsubscribe } = tolgeeSSR.on("permanentChange", () => {
// refresh page when there is a translation update
router.refresh();
});
return () => unsubscribe();
}, [tolgeeSSR, router]);
return (
<TolgeeProvider tolgee={tolgeeSSR} options={{ useSuspense: false }}>
{children}
</TolgeeProvider>
);
};
// src/tolgee.server.ts
import { getLocale } from "next-intl/server";
import { TolgeeBase, ALL_LOCALES, getStaticData } from "./shared";
import { createServerInstance } from "@tolgee/react/server";
export const { getTolgee, getTranslate, T } = createServerInstance({
getLocale,
createTolgee: async (locale) =>
TolgeeBase().init({
// load all languages on the server
staticData: await getStaticData(ALL_LOCALES),
observerOptions: {
fullKeyEncode: true,
},
language: locale,
// using custom fetch to avoid aggressive caching
fetch: async (input, init) => {
const data = await fetch(input, { ...init, next: { revalidate: 0 } });
return data;
},
}),
});
要在 root layout 套用 TolgeeProvider
才能讓 client 端翻譯起作用。
import "~/styles/globals.css";
import { GeistSans } from "geist/font/sans";
import { type Metadata } from "next";
import { TRPCReactProvider } from "~/trpc/react";
import SessionProvider from "./_components/SessionProvider";
import { TolgeeNextProvider } from "~/tolgee/client";
import { ALL_LOCALES, getStaticData } from "~/tolgee/shared";
import { notFound } from "next/navigation";
export const metadata: Metadata = {
title: "Create T3 App",
description: "Generated by create-t3-app",
icons: [{ rel: "icon", url: "/favicon.ico" }],
};
type Props = {
children: React.ReactNode;
params: { locale: string };
};
export default async function RootLayout({
children,
params: { locale },
}: Readonly<Props>) {
if (!ALL_LOCALES.includes(locale)) {
notFound();
}
// make sure you provide all the necessary locales
// for the inital SSR render (e.g. fallback languages)
const locales = await getStaticData([locale]);
return (
<html lang={locale} className={`${GeistSans.variable}`}>
<body>
<TolgeeNextProvider locale={locale} locales={locales}>
<SessionProvider>
<TRPCReactProvider>{children}</TRPCReactProvider>
</SessionProvider>
</TolgeeNextProvider>
</body>
</html>
);
}
// src/middleware.ts
import createMiddleware from "next-intl/middleware";
import { ALL_LOCALES, DEFAULT_LOCALE } from "./tolgee/shared";
export default createMiddleware({
locales: ALL_LOCALES,
defaultLocale: DEFAULT_LOCALE,
localePrefix: "as-needed",
});
export const config = {
// Skip the paths that should not be internationalized
matcher: ["/((?!api|_next|.*\\..*).*)"],
};
// src/navigation.ts
import { createSharedPathnamesNavigation } from "next-intl/navigation";
import { ALL_LOCALES } from "./tolgee/shared";
// read more about next-intl library
// https://next-intl-docs.vercel.app
export const { Link, redirect, usePathname, useRouter } =
createSharedPathnamesNavigation({ locales: ALL_LOCALES });
// next.config.js
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
* for Docker builds.
*/
await import("./src/env.js");
import createNextIntlPlugin from "next-intl/plugin";
const withNextIntl = createNextIntlPlugin();
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
};
export default withNextIntl(config);
// i18n/request.ts
import { getRequestConfig } from "next-intl/server";
export default getRequestConfig(async ({ locale }) => {
return {
// do this to make next-intl not emit any warnings
messages: { locale },
};
});
因為不在經由 next-intl 執行翻譯,為了防止 next-intl 報錯在這裡擋掉。
兩邊都設定好後就能在頁面上試用翻譯功能。
Server component
import { getTranslate } from "~/tolgee/server";
export default async function IndexPage() {
// because this is server component, use `getTranslate`
// not useTranslate from '@tolgee/react'
const t = await getTranslate();
return (
<main>
<h1>{t("message.helloWorld")}</h1>
</main>
);
}
Client component
"use client";
import { useTranslate } from "@tolgee/react";
export const Page = () => {
const { t } = useTranslate();
return (
<main>
<h1>{t("message.helloWorld")}</h1>
</main>
);
};
export default Page;
另外切換語言會以直接切換路由的方式進行
// src/app/[locale]/_components/LangSelector.tsx
"use client";
import React, { type ChangeEvent, useTransition } from "react";
import { usePathname, useRouter } from "~/navigation";
import { useTolgee } from "@tolgee/react";
export const LangSelector: React.FC = () => {
const tolgee = useTolgee(["language"]);
const locale = tolgee.getLanguage();
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
function onSelectChange(event: ChangeEvent<HTMLSelectElement>) {
const newLocale = event.target.value;
startTransition(() => {
router.replace(pathname, { locale: newLocale });
});
}
return (
<select onChange={onSelectChange} value={locale}>
<option value="en">English</option>
<option value="zh-Hant">繁體中文</option>
</select>
);
};