2026년형 AI 기반 i18n 모던 웹앱 구축하기

Lingui와 AI 번역을 활용한 다국어 웹앱 구축 완벽 가이드. Next.js, Claude, T3 Turbo를 사용해 17개 언어를 자동으로 지원하는 방법을 소개합니다.

2026년형 AI 기반 i18n 모던 웹앱 구축하기
Feng LiuFeng Liu
2026년 1월 24일

Title: 2026년, AI로 17개 언어를 지원하는 웹앱 만들기 (i18n 가이드) Excerpt: 대부분의 튜토리얼은 여전히 구식 번역 방식을 가르칩니다. 하지만 우리는 Claude Sonnet 4.5 시대에 살고 있습니다. Lingui와 LLM을 활용해 단 2~3달러로 17개 언어를 완벽하게 지원하는 프로덕션 웹앱 구축 방법을 소개합니다.


자, 2026년의 i18n(국제화)에 대해 진지하게 이야기해 봅시다.

대부분의 튜토리얼은 문자열을 수동으로 번역하거나, 번역가를 고용하거나, 엉성한 Google 번역 API를 쓰라고 할 겁니다. 하지만 중요한 건 이겁니다: 여러분은 지금 Claude Sonnet 4.5 시대에 살고 있습니다. 왜 아직도 2019년 방식처럼 번역하고 계신가요?

우리가 어떻게 17개 언어를 유창하게 구사하는 프로덕션 웹앱을 만들었는지 보여드리겠습니다. 말이 되는 2단계 i18n 아키텍처를 사용했습니다:

  1. Lingui: 추출(extraction), 컴파일, 런타임 처리를 담당
  2. 커스텀 i18n 패키지: LLM을 활용해 문맥을 파악하는 자동화된 번역 담당

우리의 기술 스택은요? Next.js, tRPC, Drizzle, Postgres, Tailwind, 그리고 AI SDK가 포함된 Create T3 Turbo입니다. 2026년에도 이 스택을 안 쓰고 있다면, 우린 좀 다른 대화를 나눠야 할 것 같네요.

자, 만들어 봅시다.


기존 i18n의 문제점

전통적인 i18n 워크플로우는 보통 이렇습니다:

# 문자열 추출
$ lingui extract

# ??? 어떻게든 번역을 가져옴 ???
# (번역가 고용, 미심쩍은 서비스 이용, 눈물 흘리기)

# 컴파일
$ lingui compile

저 중간 단계요? 악몽입니다. 보통 다음 중 하나를 겪게 되죠:

  • 사람 번역가에게 큰돈을 쓴다 (느리고 비쌈)
  • 기본적인 번역 API를 쓴다 (문맥을 못 읽고 로봇 같은 말투)
  • 직접 수동으로 번역한다 (확장성 없음)

우리는 더 나은 방법으로 합니다.


2단계 아키텍처 (The Two-Piece Architecture)

우리의 설정은 다음과 같습니다:

┌─────────────────────────────────────────────┐
│  Next.js App (Lingui 통합)                 │
│  ├─ 매크로를 사용해 문자열 추출              │
│  ├─ 코드 내 Trans/t 컴포넌트 사용            │
│  └─ 컴파일된 카탈로그로 런타임 i18n 처리      │
└─────────────────────────────────────────────┘
              ↓ .po 파일 생성
┌─────────────────────────────────────────────┐
│  @acme/i18n 패키지 (LLM 번역)               │
│  ├─ .po 파일 읽기                          │
│  ├─ Claude/GPT-5로 일괄 번역                │
│  ├─ 문맥 인식, 제품 특화 번역                │
│  └─ 번역된 .po 파일 쓰기                    │
└─────────────────────────────────────────────┘
              ↓ TypeScript로 컴파일
┌─────────────────────────────────────────────┐
│  컴파일된 메시지 카탈로그 (Message Catalogs)  │
│  └─ 빠르고 타입 안전한(Type-safe) 런타임 번역 │
└─────────────────────────────────────────────┘

**1단계 (Lingui)**는 개발자 경험(DX)을 담당합니다. **2단계 (커스텀 i18n 패키지)**는 번역의 마법을 담당합니다.

하나씩 자세히 살펴보죠.


기술 흐름도: 웹 UI → AI 번역 클라우드 → 데이터베이스, 화살표로 연결된 3계층 구조

파트 1: Next.js에 Lingui 설정하기

설치 (Installation)

T3 Turbo 모노레포에서 다음을 실행합니다:

# apps/nextjs 폴더에서
pnpm add @lingui/core @lingui/react @lingui/macro
pnpm add -D @lingui/cli @lingui/swc-plugin

Lingui 설정 (Config)

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 통합

Lingui의 SWC 플러그인을 사용하도록 next.config.js를 업데이트합니다:

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가 없기 때문입니다. 이 방식은 서버 사이드 번역을 가능하게 해줍니다.

클라이언트 사이드 프로바이더 (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에서 앱을 감싸줍니다:

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

코드에서 번역 사용하기

서버 컴포넌트에서:

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`),
  };
}

클라이언트 컴포넌트에서:

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

매크로 문법이 핵심입니다. Lingui는 빌드 타임에 이것들을 추출합니다.


파트 2: 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 스키마가 유효한 JSON을 보장합니다.
  • 낮은 Temperature: 일관된 번역 결과를 얻습니다.
  • 포맷 보존: HTML 태그와 변수들이 그대로 유지됩니다.

일괄 번역 프로세서 (Batch Processor)

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; // 속도 제한 (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"));

  // 번역되지 않은 항목 찾기
  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}...`);

  // 배치 단위로 처리
  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} translated`);

      // 진행 상황 저장
      fs.writeFileSync(poPath, po.toString());

      // 속도 제한
      if (i + BATCH_SIZE < untranslated.length) {
        await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
      }
    } catch (error) {
      console.error(`  Error translating batch: ${error}`);
      // 다음 배치로 계속 진행
    }
  }

  console.log(`✓ ${locale}: Translation complete!`);
}

**일괄 처리(Batch processing)**는 토큰 제한을 방지하고 비용을 절약해 줍니다.

번역 스크립트

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("📝 Extracting strings...");
  await execAsync("pnpm run lingui:extract --clean");

  // 2단계: 누락된 문자열 자동 번역
  console.log("\n🤖 Translating with AI...");
  const catalogPath = "./src/locales";

  for (const locale of LOCALES) {
    await enhanceTranslations(locale, catalogPath);
  }

  // 3단계: TypeScript로 컴파일
  console.log("\n⚡ Compiling catalogs...");
  await execAsync("npx lingui compile --typescript");

  console.log("\n✅ Done! All translations updated.");
}

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

📝 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.

이게 끝입니다. 코드에 새로운 문자열을 추가하고, pnpm i18n을 실행하면, 짜잔 - 17개 언어로 번역됩니다.


전/후 비교 화면: 왼쪽은 번역 서류와 1000달러 청구서를 들고 스트레스 받는 개발자

로케일 전환 (Locale Switching)

UX 부분도 잊지 마세요. 여기 로케일 스위처가 있습니다:

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

훅 구현:

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

쿠키에 선호도 저장:

// 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-Safe Translations)

타입 안전성을 원하시나요? Lingui가 해결해 줍니다:

// 이렇게 하는 대신:
t`Hello ${name}`

// msg descriptor를 사용하세요:
import { msg } from "@lingui/core/macro";

const greeting = msg`Hello ${name}`;
const translated = i18n._(greeting);

IDE가 번역 키를 자동 완성해 줄 겁니다. 아름답죠.


성능 고려사항

1. 빌드 타임 컴파일

Lingui는 번역을 최소화된 JSON으로 컴파일합니다. 런타임 파싱 오버헤드가 없습니다.

// 컴파일된 결과물 (minified):
export const messages = JSON.parse('{"ICt8/V":["영상"],"..."}');

2. 서버 카탈로그 미리 로드

시작 시 모든 카탈로그를 한 번 로드합니다 (위의 appRouterI18n.ts 참조). 요청마다 파일 I/O가 발생하지 않습니다.

3. 클라이언트 번들 크기

활성 로케일만 클라이언트로 전송합니다:

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

우리가 배운 모범 사례 (Best Practices)

1. 항상 매크로를 사용하세요

// ❌ 나쁨: 런타임 번역 (추출되지 않음)
const text = t("Hello world");

// ✅ 좋음: 매크로 (빌드 타임에 추출됨)
const text = t`Hello world`;

2. 문맥(Context)이 전부입니다

번역가를 위한 주석을 추가하세요:

// i18n: 가격표 헤더에 표시됨
<Trans>Monthly</Trans>

// i18n: 결제 폼 제출 버튼
<button>{t`Subscribe Now`}</button>

Lingui는 이것들을 번역가 노트로 추출합니다.

3. 복수형(Plurals)을 제대로 처리하세요

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 설정에 추가:

module.exports = {
  plugins: [
    require('tailwindcss-rtl'),
  ],
};

방향성 클래스 사용:

<div className="ms-4"> {/* margin-start, LTR/RTL 모두 작동 */}

배포 체크리스트

배포하기 전에 확인하세요:

  • pnpm i18n을 실행하여 모든 번역이 최신인지 확인
  • 프로덕션 모드에서 각 로케일 테스트
  • 로케일 쿠키 지속성 확인
  • 아랍어 RTL 레이아웃 확인
  • 로케일 스위처 UX 테스트
  • SEO를 위한 hreflang 태그 추가
  • 필요시 로케일 기반 라우팅 설정
  • LLM 번역 비용 모니터링

결과

이 시스템을 도입한 후:

  • 17개 언어 지원 (기본으로)
  • 약 850개 문자열 자동 번역
  • 전체 번역에 총 2~3달러 비용
  • 새 문자열 추가 시 2분 업데이트 주기
  • 수동 번역 작업 제로
  • 문맥을 파악한 고품질 번역

비교해 볼까요:

  • 사람 번역가: 단어당 $0.10-0.30 = $1,000+
  • 전통적인 서비스: 여전히 비싸고, 여전히 느림
  • 수동 작업: 확장 불가능

2026년에 이것이 중요한 이유

보세요, 웹은 글로벌입니다. 2026년에 영어로만 서비스를 내놓는다면, 전 세계의 90%를 버리고 가는 셈입니다.

하지만 전통적인 i18n은 고통스럽습니다. 이 접근 방식은 그걸 사소한 일로 만들어줍니다:

  1. Trans/t 매크로로 코드 작성 (2초 소요)
  2. pnpm i18n 실행 (자동화)
  3. 전 세계에 배포 (수익 창출)

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`;

코드

이 모든 것은 실제 앱에서 사용 중인 프로덕션 코드입니다. 전체 구현은 우리의 모노레포에 있습니다:

t3-acme-app/
├── apps/nextjs/
│   ├── lingui.config.ts
│   ├── src/
│   │   ├── locales/           # 컴파일된 카탈로그
│   │   ├── utils/i18n/        # i18n 유틸리티
│   │   └── providers/         # LinguiClientProvider
│   └── script/i18n.ts         # 번역 스크립트
└── packages/i18n/
    └── src/
        ├── translateWithLLM.ts
        ├── enhanceTranslations.ts
        └── utils.ts

마치며

2026년에 다국어 AI 앱을 만드는 건 더 이상 어렵지 않습니다. 도구들은 이미 여기 있습니다:

  • Lingui: 추출 및 런타임
  • Claude/GPT: 문맥 인식 번역
  • T3 Turbo: 최고의 DX(개발자 경험)

번역에 수천 달러를 쓰지 마세요. 앱을 영어에만 가두지 마세요.

글로벌하게 만드세요. 빠르게 배포하세요. AI를 활용하세요.

이게 바로 우리가 2026년에 일하는 방식입니다.


질문이나 문제가 있나요? Twitter에서 저를 찾거나 Lingui 문서AI SDK 문서를 확인해 보세요.

자, 이제 그 다국어 앱을 배포하러 가세요. 세상이 기다리고 있습니다.

공유하기

Feng Liu

Feng Liu

shenjian8628@gmail.com

2026년형 AI 기반 i18n 모던 웹앱 구축하기 | Feng Liu