如何在 2026 年打造 AI 驅動的 i18n 現代化 Web App
結合 Lingui 與 AI 翻譯打造多語系 Web App 的完整指南。利用 Next.js、Claude 和 T3 Turbo 技術堆疊,實現自動化支援 17 種語言。

標題:聽我說,我們真的得聊聊 2026 年的 i18n
內容:
聽著,我們得談談 2026 年的 i18n(國際化)。
大多數的教學還是會告訴你要手動翻譯字串、聘請翻譯人員,或是使用一些蹩腳的 Google Translate API。但問題是:你現在可是生活在 Claude Sonnet 4.5 的時代。為什麼你還在用 2019 年的方式做翻譯?
我要展示給你看,我們如何構建一個能流利說 17 種語言的正式環境 Web App,而且使用的是一個真正合理的兩段式 i18n 架構:
- Lingui:負責提取(extraction)、編譯(compilation)和執行階段(runtime)的魔法。
- 自定義 i18n 套件:由 LLM 驅動,負責自動化、具備語境意識(context-aware)的翻譯。
我們的技術堆疊(Stack)?Create T3 Turbo 搭配 Next.js、tRPC、Drizzle、Postgres、Tailwind 以及 AI SDK。如果你在 2026 年還沒用這套,那我們得好好聊聊了。
開始動手吧。
傳統 i18n 的問題
傳統的 i18n 工作流程通常長這樣:
# 提取字串
$ lingui extract
# ??? 想辦法弄到翻譯 ???
# (聘請翻譯、使用可疑的服務、崩潰大哭)
# 編譯
$ lingui compile
中間那個步驟?簡直是場惡夢。你通常面臨三種選擇:
- 花大錢請人工翻譯(又慢又貴)
- 使用基本的翻譯 API(缺乏語境,讀起來像機器人)
- 手動翻譯(無法規模化)
我們有更好的做法。
兩段式架構
這是我們的設置:
┌─────────────────────────────────────────────┐
│ Next.js App (Lingui Integration) │
│ ├─ 使用 macros 提取字串 │
│ ├─ 程式碼中的 Trans/t 元件 │
│ └─ 使用編譯後的 catalogs 進行執行階段 i18n │
└─────────────────────────────────────────────┘
↓ 生成 .po 檔案
┌─────────────────────────────────────────────┐
│ @acme/i18n Package (LLM Translation) │
│ ├─ 讀取 .po 檔案 │
│ ├─ 使用 Claude/GPT-5 進行批次翻譯 │
│ ├─ 具備語境意識、針對產品優化 │
│ └─ 寫入翻譯後的 .po 檔案 │
└─────────────────────────────────────────────┘
↓ 編譯為 TypeScript
┌─────────────────────────────────────────────┐
│ Compiled Message Catalogs │
│ └─ 快速、型別安全的執行階段翻譯 │
└─────────────────────────────────────────────┘
第一部分 (Lingui) 處理開發者體驗 (DX)。 第二部分 (自定義 i18n 套件) 處理翻譯的魔法。
讓我們深入探討每一個部分。

第一部分:在 Next.js 中設置 Lingui
安裝
在你的 T3 Turbo monorepo 中:
# 在 apps/nextjs 目錄下
pnpm add @lingui/core @lingui/react @lingui/macro
pnpm add -D @lingui/cli @lingui/swc-plugin
Lingui 設定
建立 apps/nextjs/lingui.config.ts:
import type { LinguiConfig } from "@lingui/conf";
const config: LinguiConfig = {
locales: [
"en", "zh_CN", "zh_TW", "ja", "ko",
"de", "fr", "es", "pt", "ar", "it",
"ru", "tr", "th", "id", "vi", "hi"
],
sourceLocale: "en",
fallbackLocales: {
default: "en"
},
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/messages",
include: ["src"],
},
],
};
export default config;
開箱即用支援 17 種語言。 為什麼不呢?
Next.js 整合
更新 next.config.js 以使用 Lingui 的 SWC plugin:
const linguiConfig = require("./lingui.config");
module.exports = {
experimental: {
swcPlugins: [
[
"@lingui/swc-plugin",
{
// 這能讓你的構建速度更快
},
],
],
},
// ... 其他設定
};
伺服器端設置 (Server-Side Setup)
建立 src/utils/i18n/appRouterI18n.ts:
import { setupI18n } from "@lingui/core";
import { allMessages } from "./initLingui";
const locales = ["en", "zh_CN", "zh_TW", /* ... */] as const;
const instances = new Map<string, ReturnType<typeof setupI18n>>();
// 為所有語系預先建立 i18n 實例
locales.forEach((locale) => {
const i18n = setupI18n({
locale,
messages: { [locale]: allMessages[locale] },
});
instances.set(locale, i18n);
});
export function getI18nInstance(locale: string) {
return instances.get(locale) ?? instances.get("en")!;
}
為什麼這樣做? Server Components 沒有 React Context。這能讓你在伺服器端進行翻譯。
客戶端 Provider (Client-Side Provider)
建立 src/providers/LinguiClientProvider.tsx:
"use client";
import { I18nProvider } from "@lingui/react";
import { setupI18n } from "@lingui/core";
import { useEffect, useState } from "react";
export function LinguiClientProvider({
children,
locale,
messages
}: {
children: React.ReactNode;
locale: string;
messages: any;
}) {
const [i18n] = useState(() =>
setupI18n({
locale,
messages: { [locale]: messages },
})
);
useEffect(() => {
i18n.load(locale, messages);
i18n.activate(locale);
}, [locale, messages, i18n]);
return <I18nProvider i18n={i18n}>{children}</I18nProvider>;
}
在 layout.tsx 中包裹你的 App:
import { LinguiClientProvider } from "@/providers/LinguiClientProvider";
import { getLocale } from "@/utils/i18n/localeDetection";
import { allMessages } from "@/utils/i18n/initLingui";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const locale = getLocale();
return (
<html lang={locale}>
<body>
<LinguiClientProvider locale={locale} messages={allMessages[locale]}>
{children}
</LinguiClientProvider>
</body>
</html>
);
}
在程式碼中使用翻譯
在 Server Components 中:
import { msg } from "@lingui/core/macro";
import { getI18nInstance } from "@/utils/i18n/appRouterI18n";
export async function generateMetadata({ params }) {
const locale = getLocale();
const i18n = getI18nInstance(locale);
return {
title: i18n._(msg`Pricing Plans | acme`),
description: i18n._(msg`Choose the perfect plan for you`),
};
}
在 Client Components 中:
"use client";
import { Trans, useLingui } from "@lingui/react/macro";
export function PricingCard() {
const { t } = useLingui();
return (
<div>
<h1><Trans>Pricing Plans</Trans></h1>
<p>{t`Ultimate entertainment experience`}</p>
{/* 帶有變數的情況 */}
<p>{t`${credits} credits remaining`}</p>
</div>
);
}
Macro 語法是關鍵。 Lingui 會在構建時(build time)提取這些字串。
第二部分:AI 驅動的翻譯套件
這裡開始變得有趣了。
套件結構
建立 packages/i18n/:
packages/i18n/
├── package.json
├── src/
│ ├── translateWithLLM.ts # 核心 LLM 翻譯邏輯
│ ├── enhanceTranslations.ts # 批次處理器
│ └── utils.ts # 輔助工具
package.json
{
"name": "@acme/i18n",
"version": "0.1.0",
"dependencies": {
"@acme/ai": "workspace:*",
"openai": "^4.77.3",
"pofile": "^1.1.4",
"zod": "^3.23.8"
}
}
LLM 翻譯引擎
這是我們的獨門秘方 —— translateWithLLM.ts:
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { z } from "zod";
const translationSchema = z.object({
translations: z.array(
z.object({
msgid: z.string(),
msgstr: z.string(),
})
),
});
export async function translateWithLLM(
messages: Array<{ msgid: string; msgstr: string }>,
targetLocale: string,
options?: { model?: string }
) {
const prompt = `You are a professional translator for acme, an AI-powered creative platform.
Translate the following strings from English to ${getLanguageName(targetLocale)}.
CONTEXT:
- acme is a platform for AI chat, image generation, and creative content
- Keep brand names unchanged (acme, Claude, etc.)
- Preserve HTML tags, variables like {count}, and placeholders
- Adapt culturally where appropriate
- Maintain tone: friendly, creative, engaging
STRINGS TO TRANSLATE:
${JSON.stringify(messages, null, 2)}
Return a JSON object with this structure:
{
"translations": [
{ "msgid": "original", "msgstr": "translation" },
...
]
}`;
const result = await generateText({
model: openai(options?.model ?? "gpt-4o"),
prompt,
temperature: 0.3, // 數值越低 = 越穩定
});
const parsed = translationSchema.parse(JSON.parse(result.text));
return parsed.translations;
}
function getLanguageName(locale: string): string {
const names: Record<string, string> = {
zh_CN: "Simplified Chinese",
zh_TW: "Traditional Chinese",
ja: "Japanese",
ko: "Korean",
de: "German",
fr: "French",
es: "Spanish",
pt: "Portuguese",
ar: "Arabic",
// ... 等等
};
return names[locale] ?? locale;
}
為什麼這行得通:
- 語境意識 (Context-aware):LLM 知道 acme 是什麼。
- 結構化輸出:Zod schema 確保 JSON 格式有效。
- 低溫度 (Low temperature):翻譯結果一致且穩定。
- 保留格式:HTML 和變數都能保持原樣。
批次翻譯處理器
建立 enhanceTranslations.ts:
import fs from "fs";
import path from "path";
import pofile from "pofile";
import { translateWithLLM } from "./translateWithLLM";
const BATCH_SIZE = 30; // 一次翻譯 30 個字串
const DELAY_MS = 1000; // 速率限制
export async function enhanceTranslations(
locale: string,
catalogPath: string
) {
const poPath = path.join(catalogPath, locale, "messages.po");
const po = pofile.parse(fs.readFileSync(poPath, "utf-8"));
// 找出未翻譯的項目
const untranslated = po.items.filter(
(item) => item.msgid && (!item.msgstr || item.msgstr[0] === "")
);
if (untranslated.length === 0) {
console.log(`✓ ${locale}: 所有字串已翻譯`);
return;
}
console.log(`正在為 ${locale} 翻譯 ${untranslated.length} 個字串...`);
// 批次處理
for (let i = 0; i < untranslated.length; i += BATCH_SIZE) {
const batch = untranslated.slice(i, i + BATCH_SIZE);
const messages = batch.map((item) => ({
msgid: item.msgid,
msgstr: item.msgstr?.[0] ?? "",
}));
try {
const translations = await translateWithLLM(messages, locale);
// 更新 PO 檔案
translations.forEach((translation, index) => {
const item = batch[index];
if (item) {
item.msgstr = [translation.msgstr];
}
});
console.log(` ${i + batch.length}/${untranslated.length} 已翻譯`);
// 儲存進度
fs.writeFileSync(poPath, po.toString());
// 速率限制
if (i + BATCH_SIZE < untranslated.length) {
await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
}
} catch (error) {
console.error(` 翻譯批次時發生錯誤: ${error}`);
// 繼續下一個批次
}
}
console.log(`✓ ${locale}: 翻譯完成!`);
}
批次處理可以避免觸發 token 限制並節省成本。
翻譯腳本
建立 apps/nextjs/script/i18n.ts:
import { enhanceTranslations } from "@acme/i18n";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
const LOCALES = [
"zh_CN", "zh_TW", "ja", "ko", "de",
"fr", "es", "pt", "ar", "it", "ru"
];
async function main() {
// 步驟 1: 從程式碼中提取字串
console.log("📝 正在提取字串...");
await execAsync("pnpm run lingui:extract --clean");
// 步驟 2: 自動翻譯缺失的字串
console.log("\n🤖 正在使用 AI 進行翻譯...");
const catalogPath = "./src/locales";
for (const locale of LOCALES) {
await enhanceTranslations(locale, catalogPath);
}
// 步驟 3: 編譯為 TypeScript
console.log("\n⚡ 正在編譯 catalogs...");
await execAsync("npx lingui compile --typescript");
console.log("\n✅ 完成!所有翻譯已更新。");
}
main().catch(console.error);
加入到 package.json:
{
"scripts": {
"i18n": "tsx script/i18n.ts",
"lingui:extract": "lingui extract",
"lingui:compile": "lingui compile --typescript"
}
}
執行你的 i18n 流程
# 一個指令搞定所有事
$ pnpm run i18n
📝 正在提取字串...
Catalog statistics for src/locales/{locale}/messages:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ en │ 847 │ 0 │
│ zh_CN │ 847 │ 123 │
│ ja │ 847 │ 89 │
└──────────┴─────────────┴─────────┘
🤖 正在使用 AI 進行翻譯...
正在為 zh_CN 翻譯 123 個字串...
30/123 已翻譯
60/123 已翻譯
90/123 已翻譯
123/123 已翻譯
✓ zh_CN: 翻譯完成!
⚡ 正在編譯 catalogs...
✅ 完成!所有翻譯已更新。
就這樣。 在程式碼中加入新字串,執行 pnpm i18n,砰 —— 自動翻譯成 17 種語言。

語言切換 (Locale Switching)
別忘了使用者體驗的部分。這是一個語言切換器:
"use client";
import { useLocaleSwitcher } from "@/hooks/useLocaleSwitcher";
import { useLocale } from "@/hooks/useLocale";
const LOCALES = {
en: "English",
zh_CN: "简体中文",
zh_TW: "繁體中文",
ja: "日本語",
ko: "한국어",
// ... 等等
};
export function LocaleSelector() {
const currentLocale = useLocale();
const { switchLocale } = useLocaleSwitcher();
return (
<select
value={currentLocale}
onChange={(e) => switchLocale(e.target.value)}
>
{Object.entries(LOCALES).map(([code, name]) => (
<option key={code} value={code}>
{name}
</option>
))}
</select>
);
}
Hook 的實作:
// hooks/useLocaleSwitcher.tsx
"use client";
import { setUserLocale } from "@/utils/i18n/localeDetection";
export function useLocaleSwitcher() {
const switchLocale = (locale: string) => {
setUserLocale(locale);
window.location.reload(); // 強制重新整理以套用語言
};
return { switchLocale };
}
將偏好設定儲存在 cookie 中:
// utils/i18n/localeDetection.ts
import { cookies } from "next/headers";
export function setUserLocale(locale: string) {
cookies().set("NEXT_LOCALE", locale, {
maxAge: 365 * 24 * 60 * 60, // 1 年
});
}
export function getLocale(): string {
const cookieStore = cookies();
return cookieStore.get("NEXT_LOCALE")?.value ?? "en";
}
進階:型別安全的翻譯
想要型別安全 (Type Safety) 嗎?Lingui 幫你搞定:
// 不要這樣寫:
t`Hello ${name}`
// 使用 msg descriptor:
import { msg } from "@lingui/core/macro";
const greeting = msg`Hello ${name}`;
const translated = i18n._(greeting);
你的 IDE 會自動補全翻譯鍵值 (translation keys)。太完美了。
效能考量
1. 在構建時編譯 (Compile at Build Time)
Lingui 會將翻譯編譯成最小化的 JSON。沒有執行階段的解析開銷。
// 編譯後的輸出 (minified):
export const messages = JSON.parse('{"ICt8/V":["视频"],"..."}');
2. 預先載入 Server Catalogs
在啟動時載入所有 catalogs(見上方的 appRouterI18n.ts)。不需要在每次請求時進行檔案 I/O。
3. 客戶端 Bundle 大小
只傳送當前活躍的語系給客戶端:
<LinguiClientProvider
locale={locale}
messages={allMessages[locale]} // 只傳送一個語系
>
4. LLM 成本優化
- 批次翻譯:每次 API 呼叫處理 30 個字串
- 快取翻譯:不重複翻譯未變更的字串
- 使用較便宜的模型:對於非關鍵語言使用 GPT-4o-mini
我們的成本?800+ 個字串 × 16 種語言,大約 2-3 美元。跟人工翻譯比起來簡直是零頭。
完整技術堆疊整合
讓我們看看這如何與 T3 Turbo 的其他部分協作:
tRPC 與 i18n
// server/api/routers/user.ts
import { createTRPCRouter, publicProcedure } from "../trpc";
import { msg } from "@lingui/core/macro";
export const userRouter = createTRPCRouter({
subscribe: publicProcedure
.mutation(async ({ ctx }) => {
// 錯誤訊息也可以翻譯!
if (!ctx.session?.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: ctx.i18n._(msg`You must be logged in`),
});
}
// ... 訂閱邏輯
}),
});
透過 context 傳遞 i18n 實例:
// server/api/trpc.ts
import { getI18nInstance } from "@/utils/i18n/appRouterI18n";
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const locale = getLocale();
const i18n = getI18nInstance(locale);
return {
session: await getServerAuthSession(),
i18n,
locale,
};
};
資料庫與 Drizzle
儲存使用者的語言偏好:
// packages/db/schema/user.ts
import { pgTable, text, varchar } from "drizzle-orm/pg-core";
export const users = pgTable("user", {
id: varchar("id", { length: 255 }).primaryKey(),
locale: varchar("locale", { length: 10 }).default("en"),
// ... 其他欄位
});
AI SDK 整合
即時翻譯 AI 的回應:
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { useLingui } from "@lingui/react/macro";
export function useAIChat() {
const { i18n } = useLingui();
const chat = async (prompt: string) => {
const systemPrompt = i18n._(msg`You are a helpful AI assistant for acme.`);
return generateText({
model: openai("gpt-4"),
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt },
],
});
};
return { chat };
}
我們學到的最佳實踐
1. 永遠使用 Macros
// ❌ 壞習慣:執行階段翻譯 (不會被提取)
const text = t("Hello world");
// ✅ 好習慣:Macro (會在構建時被提取)
const text = t`Hello world`;
2. 語境就是一切
為翻譯人員(或 AI)添加註解:
// i18n: This appears in the pricing table header
<Trans>Monthly</Trans>
// i18n: Button to submit payment form
<button>{t`Subscribe Now`}</button>
Lingui 會將這些提取為翻譯註解。
3. 正確處理複數
import { Plural } from "@lingui/react/macro";
<Plural
value={count}
one="# credit remaining"
other="# credits remaining"
/>
不同語言有不同的複數規則。Lingui 會處理這些。
4. 日期/數字格式化
使用 Intl API:
const date = new Intl.DateTimeFormat(locale, {
dateStyle: "long",
}).format(new Date());
const price = new Intl.NumberFormat(locale, {
style: "currency",
currency: "USD",
}).format(29.99);
5. RTL 支援
對於阿拉伯語,處理文字方向:
export default function RootLayout({ children }) {
const locale = getLocale();
const direction = locale === "ar" ? "rtl" : "ltr";
return (
<html lang={locale} dir={direction}>
<body>{children}</body>
</html>
);
}
加入到 Tailwind config:
module.exports = {
plugins: [
require('tailwindcss-rtl'),
],
};
使用方向性 class:
<div className="ms-4"> {/* margin-start, 同時適用於 LTR/RTL */}
部署檢查清單
在你發布之前:
- 執行
pnpm i18n確保所有翻譯都是最新的 - 在正式環境模式下測試每個語系
- 驗證 locale cookie 的持久性
- 檢查阿拉伯語的 RTL 版面配置
- 測試語言切換器的 UX
- 為 SEO 加入 hreflang 標籤
- 如果需要,設定基於語系的路由 (locale-based routing)
- 監控 LLM 翻譯成本
結果
實施這套系統後:
- 開箱即用支援 17 種語言
- ~850 個字串 自動翻譯
- 總成本 2-3 美元 完成全站翻譯
- 2 分鐘更新週期 當加入新字串時
- 零手動翻譯工作
- 具備語境意識的高品質翻譯
對比一下:
- 人工翻譯:每個字 0.10-0.30 美元 = 1,000 美元以上
- 傳統服務:依然昂貴,依然緩慢
- 手動工作:無法規模化
為什麼這在 2026 年很重要
聽著,網路是全球性的。如果你在 2026 年只發布英文版,你就拋棄了世界上 90% 的用戶。
但傳統的 i18n 實在太痛苦了。這個方法讓它變得微不足道:
- 用 Trans/t macros 寫程式碼(花 2 秒鐘)
- 執行
pnpm i18n(自動化) - 發布給全世界(然後獲利)
Lingui 的開發者體驗 + LLM 驅動的翻譯 的結合徹底改變了遊戲規則。你將獲得:
- 型別安全的翻譯
- 零執行階段開銷
- 自動提取
- 具備語境意識的 AI 翻譯
- 每個語言只需幾分錢
- 無限擴展
更進一步
想更上一層樓嗎?試試這些:
動態內容翻譯
將翻譯儲存在資料庫中:
// packages/db/schema/content.ts
export const blogPosts = pgTable("blog_post", {
id: varchar("id", { length: 255 }).primaryKey(),
titleEn: text("title_en"),
titleZhCn: text("title_zh_cn"),
titleJa: text("title_ja"),
// ... 等等
});
儲存時自動翻譯:
import { translateWithLLM } from "@acme/i18n";
export const blogRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ input }) => {
// 翻譯成所有語言
const translations = await Promise.all(
LOCALES.map(async (locale) => {
const result = await translateWithLLM(
[{ msgid: input.title, msgstr: "" }],
locale
);
return [locale, result[0].msgstr];
})
);
await db.insert(blogPosts).values({
id: generateId(),
titleEn: input.title,
...Object.fromEntries(translations),
});
}),
});
使用者提供的翻譯
讓使用者提交更好的翻譯:
export const i18nRouter = createTRPCRouter({
suggestTranslation: publicProcedure
.input(z.object({
msgid: z.string(),
locale: z.string(),
suggestion: z.string(),
}))
.mutation(async ({ input }) => {
await db.insert(translationSuggestions).values(input);
// 通知維護者
await sendEmail({
to: "i18n@acme.com",
subject: `New translation suggestion for ${input.locale}`,
body: `"${input.msgid}" → "${input.suggestion}"`,
});
}),
});
A/B 測試翻譯
測試哪種翻譯的轉換率更高:
const variant = await abTest.getVariant("pricing-cta", locale);
const ctaText = variant === "A"
? t`Start Your Free Trial`
: t`Try acme Free`;
程式碼
所有這些都是來自真實 App 的正式環境程式碼。完整的實作在我們的 monorepo 中:
t3-acme-app/
├── apps/nextjs/
│ ├── lingui.config.ts
│ ├── src/
│ │ ├── locales/ # 編譯後的 catalogs
│ │ ├── utils/i18n/ # i18n 工具
│ │ └── providers/ # LinguiClientProvider
│ └── script/i18n.ts # 翻譯腳本
└── packages/i18n/
└── src/
├── translateWithLLM.ts
├── enhanceTranslations.ts
└── utils.ts
最後的想法
在 2026 年構建多語言 AI App 已經不再困難。工具都準備好了:
- Lingui 負責提取和執行階段
- Claude/GPT 負責具備語境意識的翻譯
- T3 Turbo 提供最佳的開發者體驗
別再花幾千美元做翻譯了。別再把你的 App 限制在只有英文了。
做全球化產品。快速發布。善用 AI。
這就是我們在 2026 年的做法。
有問題?發現錯誤?在 Twitter 上找我,或是查看 Lingui 文件 和 AI SDK 文件。
現在,去發布那個多語言 App 吧。世界正在等著你。
摘要: 本文介紹了如何在 2026 年利用 Claude/GPT 等 LLM 技術與 Lingui 框架,構建一套自動化、低成本且高品質的 Web App 多語言(i18n)系統。作者劉峰分享了基於 T3 Turbo Stack 的實戰架構,展示如何透過 AI 解決傳統翻譯流程昂貴且緩慢的痛點,實現 17 種語言的快速部署與自動更新。
分享

Feng Liu
shenjian8628@gmail.com