iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0

要在 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 頁面新增點什麼。

https://res.cloudinary.com/dhcsjvhjg/image/upload/v1728052284/Screenshot_2024-10-04_at_10.31.11_PM_dt7kdb.png

有資料後就能到 Exports 頁面輸出得到 json 檔案。

https://res.cloudinary.com/dhcsjvhjg/image/upload/v1728052345/Screenshot_2024-10-04_at_10.32.16_PM_gtnmhc.png

再來先設定 Nextjs 的環境變數,需要 Tolgee 的 API key。

NEXT_PUBLIC_TOLGEE_API_KEY=<your api key>
NEXT_PUBLIC_TOLGEE_API_URL="http://localhost:8085"

新增 API key 的頁面在右上角使用者選單中。

https://res.cloudinary.com/dhcsjvhjg/image/upload/v1728052543/Screenshot_2024-10-04_at_10.35.33_PM_yechc7.png

如果懶得一直更新 API key 的話記得設定為不會過期。

Tolgee 設定

// 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>
  );
}

next-intl 設定

// 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>
  );
};


上一篇
翻譯管理工具 Tolgee
下一篇
Tolgee Chrome 插件工具
系列文
Awesome self hosted 30天30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言