Come costruire una Web App moderna AI-Powered e i18n nel 2026
Guida completa per costruire webapp multilingue con Lingui + traduzioni AI. Supporta automaticamente 17 lingue usando Next.js, Claude e T3 Turbo.

Senti, dobbiamo parlare di i18n nel 2026.
La maggior parte dei tutorial ti dirร di tradurre manualmente le stringhe, assumere traduttori o usare qualche API di Google Translate messa su alla buona. Ma ecco il punto: vivi nell'era di Claude Sonnet 4.5. Perchรฉ traduci come se fossimo nel 2019?
Sto per mostrarti come abbiamo costruito una webapp in produzione che parla fluentemente 17 lingue, utilizzando un'architettura i18n a due parti che ha davvero senso:
- Lingui per l'estrazione, la compilazione e la magia a runtime
- Un pacchetto i18n personalizzato alimentato da LLM per traduzioni automatizzate e consapevoli del contesto
Il nostro stack? Create T3 Turbo con Next.js, tRPC, Drizzle, Postgres, Tailwind e l'AI SDK. Se non stai usando questo nel 2026, dobbiamo fare un altro tipo di conversazione.
Costruiamo.
Il Problema con l'i18n Tradizionale
I workflow i18n tradizionali assomigliano a questo:
# Extract strings
$ lingui extract
# ??? Somehow get translations ???
# (hire translators, use sketchy services, cry)
# Compile
$ lingui compile
Quel passaggio intermedio? ร un incubo. Ti ritrovi a:
- Pagare $$$ per traduttori umani (lento, costoso)
- Usare API di traduzione basilari (cieche al contesto, suonano robotiche)
- Tradurre manualmente (non scala)
Noi facciamo di meglio.
L'Architettura a Due Parti
Ecco il nostro setup:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Next.js App (Lingui Integration) โ
โ โโ Extract strings with macros โ
โ โโ Trans/t components in your code โ
โ โโ Runtime i18n with compiled catalogs โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ generates .po files
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ @acme/i18n Package (LLM Translation) โ
โ โโ Reads .po files โ
โ โโ Batch translates with Claude/GPT-5 โ
โ โโ Context-aware, product-specific โ
โ โโ Writes translated .po files โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ compiles to TypeScript
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Compiled Message Catalogs โ
โ โโ Fast, type-safe runtime translations โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Il Pezzo 1 (Lingui) gestisce la developer experience. Il Pezzo 2 (Pacchetto i18n Personalizzato) gestisce la magia della traduzione.
Approfondiamo entrambi.

Parte 1: Configurare Lingui in Next.js
Installazione
Nel tuo monorepo T3 Turbo:
# In apps/nextjs
pnpm add @lingui/core @lingui/react @lingui/macro
pnpm add -D @lingui/cli @lingui/swc-plugin
Configurazione Lingui
Crea 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 lingue pronte all'uso. Perchรฉ no?
Integrazione Next.js
Aggiorna next.config.js per usare il plugin SWC di Lingui:
const linguiConfig = require("./lingui.config");
module.exports = {
experimental: {
swcPlugins: [
[
"@lingui/swc-plugin",
{
// This makes your builds faster
},
],
],
},
// ... rest of your config
};
Setup Server-Side
Crea 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>>();
// Pre-create i18n instances for all locales
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")!;
}
Perchรฉ? I Server Components non hanno il React Context. Questo ti offre traduzioni server-side.
Provider Client-Side
Crea 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>;
}
Avvolgi la tua app in 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>
);
}
Usare le Traduzioni nel Tuo Codice
Nei 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`),
};
}
Nei 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>
{/* With variables */}
<p>{t`${credits} credits remaining`}</p>
</div>
);
}
La sintassi macro รจ la CHIAVE. Lingui estrae queste stringhe durante la build.
Parte 2: Il Pacchetto di Traduzione Potenziato dall'AI
Qui รจ dove la cosa si fa interessante.
Struttura del Pacchetto
Crea packages/i18n/:
packages/i18n/
โโโ package.json
โโโ src/
โ โโโ translateWithLLM.ts # Core LLM translation
โ โโโ enhanceTranslations.ts # Batch processor
โ โโโ utils.ts # Helpers
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"
}
}
Il Motore di Traduzione LLM
Ecco l'ingrediente segreto - 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, // Lower = more consistent
});
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",
// ... etc
};
return names[locale] ?? locale;
}
Perchรฉ funziona:
- Consapevole del contesto: L'LLM sa cos'รจ "acme".
- Output strutturato: Lo schema Zod assicura un JSON valido.
- Bassa temperatura: Traduzioni coerenti.
- Preserva la formattazione: HTML e variabili rimangono intatti.
Processore di Traduzione in Batch
Crea enhanceTranslations.ts:
import fs from "fs";
import path from "path";
import pofile from "pofile";
import { translateWithLLM } from "./translateWithLLM";
const BATCH_SIZE = 30; // Translate 30 strings at a time
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"));
// Find untranslated items
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}...`);
// Process in batches
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);
// Update PO file
translations.forEach((translation, index) => {
const item = batch[index];
if (item) {
item.msgstr = [translation.msgstr];
}
});
console.log(` ${i + batch.length}/${untranslated.length} translated`);
// Save progress
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}`);
// Continue with next batch
}
}
console.log(`โ ${locale}: Translation complete!`);
}
L'elaborazione in batch previene i limiti dei token e risparmia sui costi.
Lo Script di Traduzione
Crea 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() {
// Step 1: Extract strings from code
console.log("๐ Extracting strings...");
await execAsync("pnpm run lingui:extract --clean");
// Step 2: Auto-translate missing strings
console.log("\n๐ค Translating with AI...");
const catalogPath = "./src/locales";
for (const locale of LOCALES) {
await enhanceTranslations(locale, catalogPath);
}
// Step 3: Compile to TypeScript
console.log("\nโก Compiling catalogs...");
await execAsync("npx lingui compile --typescript");
console.log("\nโ
Done! All translations updated.");
}
main().catch(console.error);
Aggiungi a package.json:
{
"scripts": {
"i18n": "tsx script/i18n.ts",
"lingui:extract": "lingui extract",
"lingui:compile": "lingui compile --typescript"
}
}
Eseguire la tua Pipeline i18n
# One command to rule them all
$ 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.
Tutto qui. Aggiungi una nuova stringa nel tuo codice, esegui pnpm i18n, boom - tradotto in 17 lingue.

Cambio Lingua (Locale Switching)
Non dimenticare la parte UX. Ecco un selettore di lingua:
"use client";
import { useLocaleSwitcher } from "@/hooks/useLocaleSwitcher";
import { useLocale } from "@/hooks/useLocale";
const LOCALES = {
en: "English",
zh_CN: "็ฎไฝไธญๆ",
zh_TW: "็น้ซไธญๆ",
ja: "ๆฅๆฌ่ช",
ko: "ํ๊ตญ์ด",
// ... etc
};
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>
);
}
L'implementazione dell'hook:
// hooks/useLocaleSwitcher.tsx
"use client";
import { setUserLocale } from "@/utils/i18n/localeDetection";
export function useLocaleSwitcher() {
const switchLocale = (locale: string) => {
setUserLocale(locale);
window.location.reload(); // Force reload to apply locale
};
return { switchLocale };
}
Salva la preferenza in un 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 year
});
}
export function getLocale(): string {
const cookieStore = cookies();
return cookieStore.get("NEXT_LOCALE")?.value ?? "en";
}
Avanzato: Traduzioni Type-Safe
Vuoi la type safety? Lingui ti copre le spalle:
// Instead of this:
t`Hello ${name}`
// Use msg descriptor:
import { msg } from "@lingui/core/macro";
const greeting = msg`Hello ${name}`;
const translated = i18n._(greeting);
Il tuo IDE autocompleterร le chiavi di traduzione. Bellissimo.
Considerazioni sulle Performance
1. Compilazione in fase di Build
Lingui compila le traduzioni in JSON minificato. Nessun overhead di parsing a runtime.
// Compiled output (minified):
export const messages = JSON.parse('{"ICt8/V":["่ง้ข"],"..."}');
2. Pre-caricamento dei Cataloghi Server
Carica tutti i cataloghi una volta all'avvio (vedi appRouterI18n.ts sopra). Nessun I/O su file ad ogni richiesta.
3. Dimensione del Bundle Client
Invia al client solo la lingua attiva:
<LinguiClientProvider
locale={locale}
messages={allMessages[locale]} // Only one locale
>
4. Ottimizzazione Costi LLM
- Traduzioni in batch: 30 stringhe per chiamata API
- Cache delle traduzioni: Non ritradurre stringhe invariate
- Usa modelli piรน economici: GPT-4o-mini per lingue non critiche
Il nostro costo? ~$2-3 per 800+ stringhe ร 16 lingue. Spiccioli rispetto ai traduttori umani.
L'Integrazione Full Stack
Vediamo come questo si integra con il resto di T3 Turbo:
tRPC con 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 }) => {
// Errors can be translated too!
if (!ctx.session?.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: ctx.i18n._(msg`You must be logged in`),
});
}
// ... subscription logic
}),
});
Passa l'istanza i18n tramite il contesto:
// 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,
};
};
Database con Drizzle
Salva la preferenza della lingua dell'utente:
// 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"),
// ... other fields
});
Integrazione AI SDK
Traduci le risposte dell'AI al volo:
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 che Abbiamo Imparato
1. Usa Sempre le Macro
// โ Bad: Runtime translation (not extracted)
const text = t("Hello world");
// โ
Good: Macro (extracted at build time)
const text = t`Hello world`;
2. Il Contesto รจ Tutto
Aggiungi commenti per i traduttori:
// i18n: This appears in the pricing table header
<Trans>Monthly</Trans>
// i18n: Button to submit payment form
<button>{t`Subscribe Now`}</button>
Lingui estrae questi come note per il traduttore.
3. Gestisci Correttamente i Plurali
import { Plural } from "@lingui/react/macro";
<Plural
value={count}
one="# credit remaining"
other="# credits remaining"
/>
Lingue diverse hanno regole diverse per i plurali. Lingui se ne occupa.
4. Formattazione Data/Numero
Usa le API Intl:
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. Supporto RTL
Per l'Arabo, gestisci la direzione:
export default function RootLayout({ children }) {
const locale = getLocale();
const direction = locale === "ar" ? "rtl" : "ltr";
return (
<html lang={locale} dir={direction}>
<body>{children}</body>
</html>
);
}
Aggiungi alla config di Tailwind:
module.exports = {
plugins: [
require('tailwindcss-rtl'),
],
};
Usa classi direzionali:
<div className="ms-4"> {/* margin-start, works for both LTR/RTL */}
Checklist di Deployment
Prima di shippare:
- Esegui
pnpm i18nper assicurarti che tutte le traduzioni siano aggiornate - Testa ogni lingua in modalitร produzione
- Verifica la persistenza del cookie della lingua
- Controlla il layout RTL per l'Arabo
- Testa la UX del selettore di lingua
- Aggiungi i tag hreflang per la SEO
- Imposta il routing basato sulla lingua se necessario
- Monitora i costi di traduzione LLM
I Risultati
Dopo aver implementato questo sistema:
- 17 lingue supportate pronte all'uso
- ~850 stringhe tradotte automaticamente
- $2-3 costo totale per la traduzione completa
- Ciclo di aggiornamento di 2 minuti quando si aggiungono nuove stringhe
- Zero lavoro di traduzione manuale
- Traduzioni di alta qualitร , consapevoli del contesto
Confrontalo con:
- Traduttori umani: $0.10-0.30 per parola = $1,000+
- Servizi tradizionali: Ancora costosi, ancora lenti
- Lavoro manuale: Non scala
Perchรฉ Questo รจ Importante nel 2026
Senti, il web รจ globale. Se nel 2026 shippi solo in inglese, stai lasciando indietro il 90% del mondo.
Ma l'i18n tradizionale รจ doloroso. Questo approccio lo rende banale:
- Scrivi codice con le macro Trans/t (ci vogliono 2 secondi)
- Esegui
pnpm i18n(automatizzato) - Shippa al mondo (profitto)
La combinazione della developer experience di Lingui + traduzioni potenziate da LLM cambia le regole del gioco. Ottieni:
- Traduzioni type-safe
- Overhead a runtime pari a zero
- Estrazione automatica
- Traduzioni AI consapevoli del contesto
- Spiccioli per lingua
- Scala all'infinito
Andare Oltre
Vuoi salire di livello? Prova:
Traduzione di Contenuti Dinamici
Salva le traduzioni nel tuo database:
// 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"),
// ... etc
});
Traduci automaticamente al salvataggio:
import { translateWithLLM } from "@acme/i18n";
export const blogRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ input }) => {
// Translate to all languages
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),
});
}),
});
Traduzioni Fornite dagli Utenti
Lascia che gli utenti inviino traduzioni migliori:
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);
// Notify maintainers
await sendEmail({
to: "i18n@acme.com",
subject: `New translation suggestion for ${input.locale}`,
body: `"${input.msgid}" โ "${input.suggestion}"`,
});
}),
});
A/B Testing delle Traduzioni
Testa quali traduzioni convertono meglio:
const variant = await abTest.getVariant("pricing-cta", locale);
const ctaText = variant === "A"
? t`Start Your Free Trial`
: t`Try acme Free`;
Il Codice
Tutto questo รจ codice di produzione di un'app reale. L'implementazione completa รจ nel nostro monorepo:
t3-acme-app/
โโโ apps/nextjs/
โ โโโ lingui.config.ts
โ โโโ src/
โ โ โโโ locales/ # Compiled catalogs
โ โ โโโ utils/i18n/ # i18n utilities
โ โ โโโ providers/ # LinguiClientProvider
โ โโโ script/i18n.ts # Translation script
โโโ packages/i18n/
โโโ src/
โโโ translateWithLLM.ts
โโโ enhanceTranslations.ts
โโโ utils.ts
Pensieri Finali
Costruire un'app AI multilingue nel 2026 non รจ piรน difficile. Gli strumenti sono qui:
- Lingui per estrazione e runtime
- Claude/GPT per traduzioni consapevoli del contesto
- T3 Turbo per la migliore DX in circolazione
Smetti di pagare migliaia di dollari per le traduzioni. Smetti di limitare la tua app all'inglese.
Costruisci globalmente. Shippa velocemente. Usa l'AI.
ร cosรฌ che lo facciamo nel 2026.
Domande? Problemi? Trovami su Twitter o controlla la documentazione di Lingui e la documentazione dell'AI SDK.
Ora vai e shippa quell'app multilingue. Il mondo sta aspettando.
Condividi questo

Feng Liu
shenjian8628@gmail.com