Skip to content

다국어 처리

Invite 서비스는 next-intl v4를 사용하여 3개 언어를 지원합니다. URL 경로 기반이 아닌, 쿠키 + Accept-Language 헤더 기반으로 로케일을 감지합니다.

지원 로케일

코드언어비고
enEnglish기본 로케일
ko한국어
ja日本語
typescript
// i18n/config.ts
import Negotiator from 'negotiator';
import { match } from '@formatjs/intl-localematcher';

export const locales = ['en', 'ko', 'ja'] as const;
export const defaultLocale = 'en' as const;
export type Locale = (typeof locales)[number];

export function resolveLocaleFromHeader(acceptLanguage: string): Locale {
  const negotiator = new Negotiator({
    headers: { 'accept-language': acceptLanguage },
  });
  const languages = negotiator.languages();
  try {
    return match(languages, [...locales], defaultLocale) as Locale;
  } catch {
    return defaultLocale;
  }
}

resolveLocaleFromHeader는 RFC 7231 기반으로 Accept-Language 헤더의 q값 우선순위를 존중하여 최적의 로케일을 매칭합니다. Middleware, Request Config, OG 메타데이터 생성 등 모든 로케일 감지 지점에서 이 함수를 공유합니다.

로케일 감지 흐름

로케일은 다음 우선순위로 결정됩니다.

쿠키 확인

locale 쿠키가 있으면 해당 값을 사용합니다.

Accept-Language 헤더 감지

쿠키가 없으면 resolveLocaleFromHeader로 브라우저의 Accept-Language 헤더를 파싱합니다.

  • RFC 7231 기반 q값 우선순위를 존중하여 최적 로케일 매칭
  • negotiator로 헤더 파싱 → @formatjs/intl-localematcher로 지원 로케일과 매칭
  • 매칭 실패 시 기본값(en) 반환

쿠키 설정/갱신

감지된 로케일을 locale 쿠키에 저장합니다 (유효기간 1년). 이미 쿠키가 있더라도 Accept-Language 변경이 감지되면 쿠키를 갱신합니다.

설정 구조

Middleware (middleware.ts)

첫 방문 시 Accept-Language 헤더를 파싱하여 locale 쿠키를 설정합니다.

typescript
export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  const acceptLanguage = request.headers.get('accept-language') || '';
  const detected = resolveLocaleFromHeader(acceptLanguage);
  const existing = request.cookies.get('locale')?.value;

  // Accept-Language 변경 시 쿠키 갱신
  if (existing !== detected) {
    response.cookies.set('locale', detected, {
      maxAge: 60 * 60 * 24 * 365, // 1년
    });
  }

  return response;
}

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

정보

Middleware는 API 라우트(/api), Next.js 내부 경로(/_next), 정적 파일을 제외한 모든 경로에 적용됩니다. 기존 쿠키가 있더라도 Accept-Language 변경이 감지되면 쿠키를 갱신하므로, 사용자가 브라우저 언어 설정을 변경하면 다음 방문 시 반영됩니다.

Request Config (i18n/request.ts)

next-intl의 getRequestConfig를 통해 요청별 로케일과 번역 메시지를 제공합니다.

typescript
export default getRequestConfig(async () => {
  const cookieStore = await cookies();
  const headerStore = await headers();

  // 1. 쿠키 → 2. Accept-Language → 3. 기본값
  let locale = cookieStore.get('locale')?.value as Locale | undefined;
  if (!locale) {
    const acceptLanguage = headerStore.get('accept-language') || '';
    locale = resolveLocaleFromHeader(acceptLanguage);
  }
  if (!locale || !locales.includes(locale)) locale = defaultLocale;

  return {
    locale,
    messages: (await import(`../messages/${locale}.json`)).default,
  };
});

Next.js Plugin (next.config.ts)

next-intl 플러그인을 Next.js 설정에 통합합니다.

typescript
import createNextIntlPlugin from 'next-intl/plugin';

const withNextIntl = createNextIntlPlugin('./i18n/request.ts');
export default withNextIntl(nextConfig);

Root Layout (app/layout.tsx)

서버에서 로케일과 메시지를 가져와 NextIntlClientProvider로 클라이언트에 전달합니다.

tsx
export default async function RootLayout({ children }) {
  const locale = await getLocale();
  const messages = await getMessages();

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

번역 파일 구조

번역 파일은 messages/ 디렉토리에 JSON 형식으로 저장됩니다.

messages/
├── en.json    # 영어
├── ko.json    # 한국어
└── ja.json    # 일본어

네임스페이스 구조

번역 키는 기능 단위로 네임스페이스를 구분합니다.

네임스페이스용도
guestRegistration비회원 등록 폼 전체 (제목, 라벨, 에러, 인증, 회원 안내)
invitation초대장 공통 (앱 열기, 웹 계속하기, 호스트 초대 메시지)
invite초대 페이지 UI (탭, 공지, 멤버, 시간, 에러, 갤러리, 댓글, 리캡, 시간 표시)
loopLoop 정보 (호스트, 참여자 수)
comments댓글 섹션 (개수, 시간, 답글, 앱에서 더보기)
gallery사진 갤러리 (촬영 수, 앱에서 더보기)
download앱 다운로드

사용 예시

tsx
'use client';
import { useTranslations } from 'next-intl';

export function GuestRegistrationForm() {
  const t = useTranslations('guestRegistration');

  return (
    <form>
      <label>{t('nameLabel')}</label>
      <input placeholder={t('namePlaceholder')} />
      <button>{t('submitButton')}</button>
    </form>
  );
}

ICU 복수형 지원

번역 파일에서 ICU MessageFormat의 복수형 문법을 지원합니다.

json
{
  "dateSelectionHint": "{count, plural, =0 {가능한 날짜를 선택해주세요} other {#개 날짜 선택됨}}"
}
tsx
t('dateSelectionHint', { count: selectedDates.length });

OG 메타데이터 다국어

app/[token]/page.tsxgenerateMetadata에서 초대장의 OG 태그도 로케일에 맞게 생성합니다.

제목: "생일 파티에 초대받았어요 | truloop"
설명: "언제, 어디서, 누구랑 만나는지 확인해보세요."

규칙

주의

  • 모든 사용자 표시 텍스트는 반드시 next-intl을 사용합니다. 하드코딩된 문자열은 허용하지 않습니다.
  • 새 텍스트 추가 시 en.json, ko.json, ja.json 세 파일 모두에 번역을 추가해야 합니다.
  • 컴포넌트에서는 useTranslations('namespace') 훅으로 접근합니다.

변경 이력

날짜내용
2026-03-11Accept-Language 파싱을 RFC 7231 기반 negotiator + @formatjs/intl-localematcher로 변경. 쿠키 갱신 로직 추가 (TLP-250)
2026-03-10소스 코드 검증: 네임스페이스 설명 상세화 (invite 하위 구조, comments/gallery 용도 보강)