콘텐츠로 바로 가기
Next.js i18n

App Router를 지원하는 Next.js i18n

Next.js 애플리케이션을 위한 Server Components, ISR, 엣지 최적화 번역.

1middleware
middleware.ts
2getRequestConfig
i18n/request.ts
3getMessages()
app/[locale]/page.tsx
4useTranslations()
components/Hero.tsx
cdn.better-i18n.com/your-org/your-project/{locale}/translations.jsonmax-age=60

Setup

Set up in 4 steps

설치

프로젝트에 @better-i18n/next와 next-intl을 추가하세요.

terminal
npm install @better-i18n/next next-intl

로케일 감지를 위한 미들웨어 추가

미들웨어는 Accept-Language 헤더와 URL 접두사를 읽어 사용자의 로케일을 감지하고 그에 따라 리디렉션합니다.

middleware.ts
import { createBetterI18nMiddleware } from '@better-i18n/next';

export default createBetterI18nMiddleware({
  project: 'your-org/your-project',
  defaultLocale: 'en',
  localePrefix: 'always',
});

export const config = { matcher: ['/((?!api|_next).*)'] };

Server Component에서 메시지 로드하기

루트 레이아웃에서 getMessages()를 사용해 서버 측에서 번역을 가져와 BetterI18nProvider에 전달하세요.

app/[locale]/layout.tsx
// app/[locale]/layout.tsx
import { BetterI18nProvider } from '@better-i18n/next/client';
import { getMessages } from '@better-i18n/next/server';

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export default async function RootLayout({ children, params }) {
  const { locale } = await params;
  const messages = await getMessages(config, locale);

  return (
    <html lang={locale}>
      <body>
        <BetterI18nProvider locale={locale} messages={messages} config={config}>
          {children}
        </BetterI18nProvider>
      </body>
    </html>
  );
}

Client Component에서 번역 사용하기

어떤 Client Component에서든 useTranslations()를 호출하세요. 메시지는 이미 서버에서 하이드레이션되어 있어 추가 요청이 필요 없습니다.

components/HeroSection.tsx
'use client';
import { useTranslations } from 'next-intl';

export function HeroSection() {
  const t = useTranslations('home');
  return <h1>{t('title')}</h1>;
}

Routing

Edge 런타임 및 로케일 감지

전 세계적으로 50밀리초 미만의 응답 시간을 위해 에지에서 로케일 감지 및 메시지 로딩을 실행합니다.

미들웨어 설정

미들웨어 파일 하나로 Next.js 앱에 로케일 감지와 라우팅을 추가하세요.

middleware.ts
// middleware.ts — locale detection
import { createBetterI18nMiddleware } from '@better-i18n/next'

export default createBetterI18nMiddleware({
  project: 'your-org/your-project',
  defaultLocale: 'en',
  localePrefix: 'always',
})

export const config = { matcher: ['/((?!api|_next).*)'] }
middleware.ts
// middleware.ts — Edge-based locale detection
import { NextRequest, NextResponse } from 'next/server';

const SUPPORTED_LOCALES = ['en', 'de', 'fr', 'ja', 'es'] as const;
const DEFAULT_LOCALE = 'en';

function getPreferredLocale(request: NextRequest): string {
  // 1. Check URL prefix
  const pathname = request.nextUrl.pathname;
  const urlLocale = SUPPORTED_LOCALES.find(
    (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
  );
  if (urlLocale) return urlLocale;

  // 2. Check cookie
  const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value;
  if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as any)) {
    return cookieLocale;
  }

  // 3. Parse Accept-Language header
  const acceptLang = request.headers.get('accept-language') ?? '';
  const preferred = acceptLang
    .split(',')
    .map((part) => part.split(';')[0].trim().substring(0, 2))
    .find((code) => SUPPORTED_LOCALES.includes(code as any));

  return preferred ?? DEFAULT_LOCALE;
}

export function middleware(request: NextRequest) {
  const locale = getPreferredLocale(request);
  const { pathname } = request.nextUrl;

  const hasLocale = SUPPORTED_LOCALES.some(
    (l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`
  );

  if (!hasLocale) {
    return NextResponse.redirect(
      new URL(`/${locale}${pathname}`, request.url)
    );
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Edge 호환 메시지 로딩

즉각적인 응답을 위해 가변운 인메모리 TTL 캐시로 엓지에서 번역을 캐싱하세요.

lib/edge-messages.ts
// lib/edge-messages.ts — Edge-compatible message loading
const messageCache = new Map<string, { data: Record<string, string>; ts: number }>();
const TTL = 60_000; // 1 minute cache at edge

export async function getEdgeMessages(
  locale: string,
  namespace: string
): Promise<Record<string, string>> {
  const cacheKey = `${locale}:${namespace}`;
  const cached = messageCache.get(cacheKey);

  if (cached && Date.now() - cached.ts < TTL) {
    return cached.data;
  }

  const response = await fetch(
    `https://cdn.better-i18n.com/your-org/your-project/${locale}/${namespace}.json`,
    { next: { revalidate: 60 } }
  );

  const data = await response.json();
  messageCache.set(cacheKey, { data, ts: Date.now() });
  return data;
}

i18n을 사용하는 Edge API 경로

최소한의 콜드 스타트로 엔지 함수에서 번역된 API 응답을 반환하세요.

app/api/translate/route.ts
// app/api/translate/route.ts — Edge API route with i18n
import { getEdgeMessages } from '@/lib/edge-messages';

export const runtime = 'edge';

export async function GET(request: Request) {
  const url = new URL(request.url);
  const locale = url.searchParams.get('locale') ?? 'en';
  const key = url.searchParams.get('key') ?? '';

  const messages = await getEdgeMessages(locale, 'api-responses');
  const translated = messages[key] ?? key;

  return Response.json({ text: translated, locale });
}

Rendering

정보·감시·정찰(ISR) 및 국제화

증분 정적 재생성과 국제화를 결합하여 빠르고 항상 최신 상태를 유지하는 다국어 페이지를 구현하세요.

PublishR2 writeCDN purgeRevalidatePage served
revalidate = 3600app/[locale]/layout.tsx~60 min
revalidate = 1800app/[locale]/[slug]/page.tsx~30 min
revalidatePath()app/api/revalidate/route.ts< 1 min

빠른 시작

몇 줄의 코드만으로 Next.js 앱에 i18n을 추가하세요.

app/[locale]/page.tsx
// app/[locale]/page.tsx
import { getTranslations } from 'next-intl/server';

export default async function Page({ params }: { params: Promise<{ locale: string }> }) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'home' });

  return (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>
    </main>
  );
}
app/[locale]/layout.tsx
// app/[locale]/layout.tsx — ISR with i18n
import { getMessages } from '@better-i18n/next/server';
import { BetterI18nProvider } from '@better-i18n/next/client';

export const revalidate = 3600; // Revalidate every hour

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export default async function LocaleLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const messages = await getMessages(config, locale);

  return (
    <BetterI18nProvider locale={locale} messages={messages} config={config}>
      {children}
    </BetterI18nProvider>
  );
}

generateStaticParams를 사용한 ISR

빌드 시점에 모든 로케일에 대한 페이지를 사전 렌더링한 다음, ISR로 일정에 따라 갱신하세요.

app/[locale]/[slug]/page.tsx
// app/[locale]/[slug]/page.tsx — Generate static pages per locale
import { getMessages } from '@better-i18n/next/server';

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export async function generateStaticParams() {
  const locales = ['en', 'de', 'fr', 'ja'];
  const slugs = await fetchAllSlugs();
  return locales.flatMap((locale) =>
    slugs.map((slug) => ({ locale, slug }))
  );
}

export const revalidate = 1800; // ISR: refresh every 30 min

export default async function Page({
  params,
}: {
  params: Promise<{ locale: string; slug: string }>;
}) {
  const { locale, slug } = await params;
  const messages = await getMessages(config, locale, { namespaces: ['blog'] });
  return <article><h1>{messages.blog[slug + '.title']}</h1></article>;
}

온디맨드 재검증

번역이 업데이트되면 ISR 재검증을 트리거하세요 — Better I18N 게시 웹훅에 연결하면 됩니다.

app/api/revalidate/route.ts
// app/api/i18n/revalidate/route.ts — On-demand ISR for translation updates
import { createRevalidateHandler } from '@better-i18n/next/revalidate';

// Called by the Better i18n publish webhook — verifies the HMAC signature,
// then revalidates the paths/tags below.
export const POST = createRevalidateHandler({
  secret: process.env.BETTER_I18N_WEBHOOK_SECRET!,
  revalidatePaths: ['/'],
  revalidateTags: ['i18n-messages'],
});

Advanced

고급 패턴

중첩된 레이아웃, 병렬 경로, 타입 안전 번역을 지원하는 서버 액션.

app/[locale]/dashboard/layout.tsx
// app/[locale]/dashboard/layout.tsx — Nested layout with namespace
import { getMessages } from '@better-i18n/next/server';
import { BetterI18nProvider } from '@better-i18n/next/client';
import { DashboardNav } from '@/components/DashboardNav';

const config = { project: 'your-org/your-project', defaultLocale: 'en' };

export default async function DashboardLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  // Load the dashboard-specific namespace alongside common messages
  const messages = await getMessages(config, locale, {
    namespaces: ['common', 'dashboard'],
  });

  return (
    <BetterI18nProvider locale={locale} messages={messages} config={config}>
      <DashboardNav />
      <main>{children}</main>
    </BetterI18nProvider>
  );
}

i18n을 사용한 병렬 라우트

모듈식이고 로케일을 인식하는 레이아웃을 위해 병렬 라우트 슬롯마다 독립적으로 번역을 로드하세요.

app/[locale]/@analytics/page.tsx
// app/[locale]/@analytics/page.tsx — Parallel route with i18n
import { getTranslations } from 'next-intl/server';

export default async function AnalyticsSlot({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: 'analytics' });

  return (
    <section aria-label={t('title')}>
      <h2>{t('title')}</h2>
      <p>{t('description')}</p>
    </section>
  );
}

// app/[locale]/layout.tsx — Consuming parallel routes
export default function Layout({
  children,
  analytics,
  notifications,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  notifications: React.ReactNode;
}) {
  return (
    <div>
      <main>{children}</main>
      <aside>{analytics}</aside>
      <aside>{notifications}</aside>
    </div>
  );
}

번역이 포함된 Server Action

server action에서 번역된 유효성 검사 오류와 성공 메시지를 반환하세요.

app/[locale]/contact/actions.ts
// app/[locale]/contact/actions.ts — Server action with i18n
'use server';
import { getTranslations } from 'next-intl/server';
import { headers } from 'next/headers';

export async function submitContactForm(formData: FormData) {
  const headersList = await headers();
  // Set by createBetterI18nMiddleware — see the routing section above
  const locale = headersList.get('x-locale') ?? 'en';
  const t = await getTranslations({ locale, namespace: 'contact' });

  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  if (!email || !message) {
    return { error: t('validation.required') };
  }

  try {
    await sendEmail({ email, message, locale });
    return { success: t('form.success') };
  } catch {
    return { error: t('form.error') };
  }
}

// app/[locale]/contact/page.tsx — Using the server action
'use client';
import { useTranslations } from 'next-intl';
import { submitContactForm } from './actions';

export default function ContactPage() {
  const t = useTranslations('contact');

  return (
    <form action={submitContactForm}>
      <label>{t('form.email')}</label>
      <input name="email" type="email" required />
      <label>{t('form.message')}</label>
      <textarea name="message" required />
      <button type="submit">{t('form.submit')}</button>
    </form>
  );
}

일반적인 국제화 문제 해결

수분 불일치, 누락된 로케일 대체 처리, 날짜/숫자 서식 차이를 수정하십시오.

// Fix: Hydration mismatch with date/number formatting
// Problem: Server renders "1,000" but client renders "1.000"
// Solution: BetterI18nProvider already passes an explicit timeZone down to
// NextIntlClientProvider, so server and client share the same formatting locale.

// app/[locale]/layout.tsx
import { getFormatter } from 'next-intl/server';

export default async function Layout({ children, params }: {
  children: React.ReactNode;
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  // Pre-format on server with the explicit locale
  const format = await getFormatter({ locale });

  return (
    <html lang={locale} suppressHydrationWarning>
      <body>{children}</body>
    </html>
  );
}

// components/Price.tsx — Client component
'use client';
import { useFormatter } from 'next-intl';

export function Price({ amount }: { amount: number }) {
  const format = useFormatter();
  // useFormatter automatically uses the locale/timeZone from BetterI18nProvider
  // ensuring server and client render the same output
  return <span>{format.number(amount, { style: 'currency', currency: 'USD' })}</span>;
}

로케일 폴백 체인

pt-BR와 같은 지역 변형이 pt로, 그다음 en으로 대체되도록 폴백 체인을 정의하세요.

lib/i18n-config.ts
// lib/i18n-config.ts — Locale fallback chain
const FALLBACK_CHAIN: Record<string, string[]> = {
  'pt-BR': ['pt', 'en'],
  'zh-TW': ['zh-CN', 'en'],
  'en-GB': ['en'],
  'de-AT': ['de', 'en'],
};

export function resolveMessages(
  locale: string,
  allMessages: Record<string, Record<string, string>>
): Record<string, string> {
  const chain = FALLBACK_CHAIN[locale] ?? ['en'];
  const primary = allMessages[locale] ?? {};

  // Merge fallback messages (primary overrides fallbacks)
  return chain.reduceRight(
    (merged, fallbackLocale) => ({
      ...merged,
      ...(allMessages[fallbackLocale] ?? {}),
    }),
    primary
  );
}

일관된 날짜 형식

timeZone를 명시적으로 UTC로 설정해 서버/클라이언트 간 날짜 불일치를 방지하세요.

components/LocalizedDate.tsx
// components/LocalizedDate.tsx — Consistent date formatting
'use client';
import { useFormatter, useLocale } from 'next-intl';

export function LocalizedDate({ date }: { date: Date | string }) {
  const format = useFormatter();
  const locale = useLocale();
  const dateObj = typeof date === 'string' ? new Date(date) : date;

  return (
    <time dateTime={dateObj.toISOString()}>
      {format.dateTime(dateObj, {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
        // Explicitly set timeZone to avoid server/client mismatch
        timeZone: 'UTC',
      })}
    </time>
  );
}

Capabilities

기능

App Router 및 Pages Router 지원
자동 로케일 감지를 위한 미들웨어
React Server Components 지원
generateStaticParams로 정적 생성
점진적 정적 재생성(ISR)
타입 안전 번역
엣지 CDN 제공
hreflang로 SEO 최적화
로케일 기반 라우팅
Integrations

인기 있는 Next.js i18n 라이브러리와 호환

Better I18N은 좋아하는 i18n 라이브러리를 대체하는 것이 아닙니다 — 더 강력하게 만들어주는 번역 관리 레이어입니다.

Better I18N + next-intl

App Router 지원, 타입 안전 메시지, ICU 구문을 갖춘 가장 인기 있는 Next.js i18n 라이브러리.Better I18N은 next-intl JSON 형식으로 번역을 직접 동기화합니다. 대시보드에서 관리하고 CDN을 통해 즉시 배포하세요.

Better I18N + next-i18next

i18next 기반의 검증된 Next.js i18n 라이브러리. Pages Router와 App Router 마이그레이션에 적합합니다.i18next 호환 네임스페이스 JSON으로 내보내기. Better I18N이 번역 워크플로를, next-i18next가 런타임을 담당합니다.

Better I18N + Lingui

뛰어난 DX와 자동 메시지 추출을 갖춘 가볍고 매크로 기반의 i18n 라이브러리.Lingui CLI로 메시지를 추출하고, Better I18N에서 번역을 관리하고, GitHub 연동으로 자동 동기화합니다.

다른 프레임워크 가이드 살펴보기

Next.js i18n으로 개발을 시작하세요

무료 플랜을 사용할 수 있습니다. 신용카드가 필요 없습니다.