다크 모드
다국어 처리
Invite 서비스는 next-intl v4를 사용하여 3개 언어를 지원합니다. URL 경로 기반이 아닌, 쿠키 + Accept-Language 헤더 기반으로 로케일을 감지합니다.
지원 로케일
| 코드 | 언어 | 비고 |
|---|---|---|
en | English | 기본 로케일 |
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 (탭, 공지, 멤버, 시간, 에러, 갤러리, 댓글, 리캡, 시간 표시) |
loop | Loop 정보 (호스트, 참여자 수) |
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.tsx의 generateMetadata에서 초대장의 OG 태그도 로케일에 맞게 생성합니다.
제목: "생일 파티에 초대받았어요 | truloop"
설명: "언제, 어디서, 누구랑 만나는지 확인해보세요."규칙
주의
- 모든 사용자 표시 텍스트는 반드시 next-intl을 사용합니다. 하드코딩된 문자열은 허용하지 않습니다.
- 새 텍스트 추가 시
en.json,ko.json,ja.json세 파일 모두에 번역을 추가해야 합니다. - 컴포넌트에서는
useTranslations('namespace')훅으로 접근합니다.
변경 이력
| 날짜 | 내용 |
|---|---|
| 2026-03-11 | Accept-Language 파싱을 RFC 7231 기반 negotiator + @formatjs/intl-localematcher로 변경. 쿠키 갱신 로직 추가 (TLP-250) |
| 2026-03-10 | 소스 코드 검증: 네임스페이스 설명 상세화 (invite 하위 구조, comments/gallery 용도 보강) |