Så bygger du en modern AI-driven webbapp med i18n 2026
Komplett guide till att bygga flerspråkiga webbappar med Lingui + AI-översättning. Stöd 17 språk automatiskt med Next.js, Claude och T3 Turbo.

Hörni, vi måste snacka om i18n år 2026.
De flesta tutorials kommer säga åt dig att översätta strängar manuellt, anlita översättare eller använda något svajigt Google Translate-API. Men grejen är den: du lever i Claude Sonnet 4.5-eran. Varför översätter du som om det vore 2019?
Jag tänker visa hur vi byggde en produktions-webapp som talar 17 språk flytande, med hjälp av en tvådelad i18n-arkitektur som faktiskt är logisk:
- Lingui för extrahering, kompilering och runtime-magi
- Ett skräddarsytt i18n-paket drivet av LLM:er för automatiserade, kontextmedvetna översättningar
Vår stack? Create T3 Turbo med Next.js, tRPC, Drizzle, Postgres, Tailwind och AI SDK. Om du inte använder detta 2026 behöver vi ta ett helt annat snack.
Då kör vi.
Problemet med traditionell i18n
Traditionella arbetsflöden för i18n ser ut så här:
# Extrahera strängar
$ lingui extract
# ??? Få tag på översättningar på något sätt ???
# (anlita översättare, använd skumma tjänster, gråt en skvätt)
# Kompilera
$ lingui compile
Det där mellansteg? Det är en mardröm. Antingen:
- Betalar du $$$ för mänskliga översättare (långsamt, dyrt)
- Använder grundläggande översättnings-API:er (blinda för kontext, låter robotaktigt)
- Översätter manuellt (skalar inte)
Vi gör det bättre.
Den tvådelade arkitekturen
Här är vår setup:
┌─────────────────────────────────────────────┐
│ Next.js App (Lingui Integration) │
│ ├─ Extrahera strängar med makron │
│ ├─ Trans/t-komponenter i din kod │
│ └─ Runtime i18n med kompilerade kataloger │
└─────────────────────────────────────────────┘
↓ genererar .po-filer
┌─────────────────────────────────────────────┐
│ @acme/i18n Package (LLM-översättning) │
│ ├─ Läser .po-filer │
│ ├─ Batch-översätter med Claude/GPT-5 │
│ ├─ Kontextmedveten, produktspecifik │
│ └─ Skriver översatta .po-filer │
└─────────────────────────────────────────────┘
↓ kompilerar till TypeScript
┌─────────────────────────────────────────────┐
│ Compiled Message Catalogs │
│ └─ Snabb, typsäker runtime-översättning │
└─────────────────────────────────────────────┘
Del 1 (Lingui) hanterar utvecklarupplevelsen (DX). Del 2 (Skräddarsytt i18n-paket) hanterar översättningsmagin.
Låt oss dyka ner i detaljerna.

Del 1: Sätta upp Lingui i Next.js
Installation
I ditt T3 Turbo-monorepo:
# I apps/nextjs
pnpm add @lingui/core @lingui/react @lingui/macro
pnpm add -D @lingui/cli @lingui/swc-plugin
Lingui Config
Skapa 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 språk direkt ur lådan. För varför inte?
Next.js Integration
Uppdatera next.config.js för att använda Linguis SWC-plugin:
const linguiConfig = require("./lingui.config");
module.exports = {
experimental: {
swcPlugins: [
[
"@lingui/swc-plugin",
{
// Detta gör dina byggen snabbare
},
],
],
},
// ... resten av din config
};
Server-Side Setup
Skapa 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>>();
// För-skapa i18n-instanser för alla språk
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")!;
}
Varför? Server Components har inte React Context. Detta ger dig översättningar på serversidan.
Client-Side Provider
Skapa 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>;
}
Wrappa din app i layout.tsx:
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>
);
}
Använda översättningar i din kod
I 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`),
};
}
I 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>
{/* Med variabler */}
<p>{t`${credits} credits remaining`}</p>
</div>
);
}
Makro-syntaxen är NYCKELN. Lingui extraherar dessa vid byggtid (build time).
Del 2: Det AI-drivna översättningspaketet
Här börjar det bli riktigt intressant.
Paketstruktur
Skapa packages/i18n/:
packages/i18n/
├── package.json
├── src/
│ ├── translateWithLLM.ts # Kärnan för LLM-översättning
│ ├── enhanceTranslations.ts # Batch-processor
│ └── utils.ts # Hjälpfunktioner
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-översättningsmotorn
Här är den hemliga såsen – 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, // Lägre = mer konsekvent
});
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",
// ... osv
};
return names[locale] ?? locale;
}
Varför detta fungerar:
- Kontextmedveten: LLM:en vet vad "acme" är.
- Strukturerad output: Zod-schemat garanterar giltig JSON.
- Låg temperatur: Konsekventa översättningar.
- Bevarar formatering: HTML och variabler förblir intakta.
Batch-översättningsprocessor
Skapa enhanceTranslations.ts:
import fs from "fs";
import path from "path";
import pofile from "pofile";
import { translateWithLLM } from "./translateWithLLM";
const BATCH_SIZE = 30; // Översätt 30 strängar åt gången
const DELAY_MS = 1000; // Rate limiting
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"));
// Hitta oöversatta objekt
const untranslated = po.items.filter(
(item) => item.msgid && (!item.msgstr || item.msgstr[0] === "")
);
if (untranslated.length === 0) {
console.log(`✓ ${locale}: All strings translated`);
return;
}
console.log(`Translating ${untranslated.length} strings for ${locale}...`);
// Processa i batcher
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);
// Uppdatera PO-filen
translations.forEach((translation, index) => {
const item = batch[index];
if (item) {
item.msgstr = [translation.msgstr];
}
});
console.log(` ${i + batch.length}/${untranslated.length} translated`);
// Spara framsteg
fs.writeFileSync(poPath, po.toString());
// Rate limiting
if (i + BATCH_SIZE < untranslated.length) {
await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
}
} catch (error) {
console.error(` Error translating batch: ${error}`);
// Fortsätt med nästa batch
}
}
console.log(`✓ ${locale}: Translation complete!`);
}
Batch-processering förhindrar att vi slår i taket för tokens och sparar kostnader.
Översättningsskriptet
Skapa 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() {
// Steg 1: Extrahera strängar från kod
console.log("📝 Extracting strings...");
await execAsync("pnpm run lingui:extract --clean");
// Steg 2: Auto-översätt saknade strängar
console.log("\n🤖 Translating with AI...");
const catalogPath = "./src/locales";
for (const locale of LOCALES) {
await enhanceTranslations(locale, catalogPath);
}
// Steg 3: Kompilera till TypeScript
console.log("\n⚡ Compiling catalogs...");
await execAsync("npx lingui compile --typescript");
console.log("\n✅ Done! All translations updated.");
}
main().catch(console.error);
Lägg till i package.json:
{
"scripts": {
"i18n": "tsx script/i18n.ts",
"lingui:extract": "lingui extract",
"lingui:compile": "lingui compile --typescript"
}
}
Köra din i18n-pipeline
# Ett kommando för att styra dem alla
$ pnpm run i18n
📝 Extracting strings...
Catalog statistics for src/locales/{locale}/messages:
┌──────────┬─────────────┬─────────┐
│ Language │ Total count │ Missing │
├──────────┼─────────────┼─────────┤
│ en │ 847 │ 0 │
│ zh_CN │ 847 │ 123 │
│ ja │ 847 │ 89 │
└──────────┴─────────────┴─────────┘
🤖 Translating with AI...
Translating 123 strings for zh_CN...
30/123 translated
60/123 translated
90/123 translated
123/123 translated
✓ zh_CN: Translation complete!
⚡ Compiling catalogs...
✅ Done! All translations updated.
Så enkelt är det. Lägg till en ny sträng i din kod, kör pnpm i18n, boom – översatt till 17 språk.

Byta språk (Locale Switching)
Glöm inte UX-biten. Här är en språkväljare:
"use client";
import { useLocaleSwitcher } from "@/hooks/useLocaleSwitcher";
import { useLocale } from "@/hooks/useLocale";
const LOCALES = {
en: "English",
zh_CN: "简体中文",
zh_TW: "繁體中文",
ja: "日本語",
ko: "한국어",
// ... osv
};
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-implementationen:
// hooks/useLocaleSwitcher.tsx
"use client";
import { setUserLocale } from "@/utils/i18n/localeDetection";
export function useLocaleSwitcher() {
const switchLocale = (locale: string) => {
setUserLocale(locale);
window.location.reload(); // Tvinga omladdning för att applicera språk
};
return { switchLocale };
}
Spara preferensen i en 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 år
});
}
export function getLocale(): string {
const cookieStore = cookies();
return cookieStore.get("NEXT_LOCALE")?.value ?? "en";
}
Avancerat: Typsäkra översättningar
Vill du ha typsäkerhet? Lingui löser det:
// Istället för detta:
t`Hello ${name}`
// Använd msg descriptor:
import { msg } from "@lingui/core/macro";
const greeting = msg`Hello ${name}`;
const translated = i18n._(greeting);
Din IDE kommer att autokomplettera översättningsnycklar. Vackert.
Prestandaöverväganden
1. Kompilera vid byggtid (Build Time)
Lingui kompilerar översättningar till minifierad JSON. Ingen overhead för parsing vid runtime.
// Kompilerad output (minifierad):
export const messages = JSON.parse('{"ICt8/V":["视频"],"..."}');
2. Förladda server-kataloger
Ladda alla kataloger en gång vid uppstart (se appRouterI18n.ts ovan). Ingen fil-I/O vid varje request.
3. Klientens bundle-storlek
Skeppa bara det aktiva språket till klienten:
<LinguiClientProvider
locale={locale}
messages={allMessages[locale]} // Endast ett språk
>
4. Kostnadsoptimering för LLM
- Batch-översättningar: 30 strängar per API-anrop
- Cacha översättningar: Översätt inte om oförändrade strängar
- Använd billigare modeller: GPT-4o-mini för icke-kritiska språk
Vår kostnad? ~$2-3 för 800+ strängar × 16 språk. Småpengar jämfört med mänskliga översättare.
Integration med hela tech-stacken
Låt oss se hur detta spelar ihop med resten av T3 Turbo:
tRPC med 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 }) => {
// Felmeddelanden kan också översättas!
if (!ctx.session?.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: ctx.i18n._(msg`You must be logged in`),
});
}
// ... prenumerationslogik
}),
});
Skicka med i18n-instansen via context:
// 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,
};
};
Databas med Drizzle
Spara användarens språkpreferens:
// 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"),
// ... andra fält
});
AI SDK Integration
Översätt AI-svar "on the fly":
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 };
}
Best Practices vi lärt oss
1. Använd alltid makron
// ❌ Dåligt: Runtime-översättning (extraheras ej)
const text = t("Hello world");
// ✅ Bra: Makro (extraheras vid byggtid)
const text = t`Hello world`;
2. Kontext är allt
Lägg till kommentarer för översättare (eller AI:n):
// i18n: This appears in the pricing table header
<Trans>Monthly</Trans>
// i18n: Button to submit payment form
<button>{t`Subscribe Now`}</button>
Lingui extraherar dessa som anteckningar till översättaren.
3. Hantera plural korrekt
import { Plural } from "@lingui/react/macro";
<Plural
value={count}
one="# credit remaining"
other="# credits remaining"
/>
Olika språk har olika regler för plural. Lingui hanterar det.
4. Datum/Nummer-formatering
Använd Intl API:er:
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-stöd
För arabiska, hantera textriktning:
export default function RootLayout({ children }) {
const locale = getLocale();
const direction = locale === "ar" ? "rtl" : "ltr";
return (
<html lang={locale} dir={direction}>
<body>{children}</body>
</html>
);
}
Lägg till i Tailwind config:
module.exports = {
plugins: [
require('tailwindcss-rtl'),
],
};
Använd riktningsklasser:
<div className="ms-4"> {/* margin-start, fungerar för både LTR/RTL */}
Checklista för deployment
Innan du skeppar:
- Kör
pnpm i18nför att säkerställa att alla översättningar är uppdaterade - Testa varje språk i produktionsläge
- Verifiera att språk-cookien sparas korrekt
- Kolla RTL-layout för arabiska
- Testa UX för språkväljaren
- Lägg till hreflang-taggar för SEO
- Sätt upp språkbaserad routing om det behövs
- Övervaka kostnader för LLM-översättning
Resultatet
Efter att ha implementerat detta system:
- 17 språk stöds direkt ur lådan
- ~850 strängar översatta automatiskt
- $2-3 total kostnad för fullständig översättning
- 2 minuters uppdateringscykel när nya strängar läggs till
- Noll manuellt översättningsarbete
- Kontextmedvetna översättningar av hög kvalitet
Jämför det med:
- Mänskliga översättare: $0.10-0.30 per ord = $1,000+
- Traditionella tjänster: Fortfarande dyrt, fortfarande långsamt
- Manuellt arbete: Skalar inte
Varför detta spelar roll 2026
Hörni, webben är global. Om du bara skeppar på engelska år 2026 lämnar du 90% av världen utanför.
Men traditionell i18n är smärtsamt. Det här tillvägagångssättet gör det busenkelt:
- Skriv kod med Trans/t-makron (tar 2 sekunder)
- Kör
pnpm i18n(automatiserat) - Skeppa till världen (profit)
Kombinationen av Linguis utvecklarupplevelse + LLM-drivna översättningar är en game-changer. Du får:
- Typsäkra översättningar
- Noll overhead vid runtime
- Automatisk extrahering
- Kontextmedvetna AI-översättningar
- Småpengar per språk
- Skalar oändligt
Gå steget längre
Vill du levla upp? Prova:
Dynamisk innehållsöversättning
Spara översättningar i din databas:
// 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"),
// ... osv
});
Auto-översätt när du sparar:
import { translateWithLLM } from "@acme/i18n";
export const blogRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ input }) => {
// Översätt till alla språk
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),
});
}),
});
Användarbidragna översättningar
Låt användare skicka in bättre översättningar:
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);
// Meddela maintainers
await sendEmail({
to: "i18n@acme.com",
subject: `New translation suggestion for ${input.locale}`,
body: `"${input.msgid}" → "${input.suggestion}"`,
});
}),
});
A/B-testning av översättningar
Testa vilka översättningar som konverterar bäst:
const variant = await abTest.getVariant("pricing-cta", locale);
const ctaText = variant === "A"
? t`Start Your Free Trial`
: t`Try acme Free`;
Koden
Allt detta är produktionskod från en riktig app. Hela implementationen finns i vårt monorepo:
t3-acme-app/
├── apps/nextjs/
│ ├── lingui.config.ts
│ ├── src/
│ │ ├── locales/ # Kompilerade kataloger
│ │ ├── utils/i18n/ # i18n-verktyg
│ │ └── providers/ # LinguiClientProvider
│ └── script/i18n.ts # Översättningsskript
└── packages/i18n/
└── src/
├── translateWithLLM.ts
├── enhanceTranslations.ts
└── utils.ts
Slutord
Att bygga en flerspråkig AI-app år 2026 är inte svårt längre. Verktygen finns här:
- Lingui för extrahering och runtime
- Claude/GPT för kontextmedveten översättning
- T3 Turbo för bästa DX i gamet
Sluta betala tusentals dollar för översättningar. Sluta begränsa din app till engelska.
Bygg globalt. Skeppa snabbt. Använd AI.
Så gör vi år 2026.
Frågor? Problem? Hitta mig på Twitter eller kolla in Lingui-dokumentationen och AI SDK-dokumentationen.
Gå nu och skeppa den där flerspråkiga appen. Världen väntar.
Dela detta

Feng Liu
shenjian8628@gmail.com