feat(i18n): #352 다국어 뼈대 ko/en/ja/es

- next-intl 5.x 도입
- src/i18n/config.ts: LOCALES 상수, detectBrowserLocale, LOCALE_LABELS(국기/네이티브명)
- src/i18n/LocaleProvider.tsx: NextIntlClientProvider wrap + localStorage tasteby_locale 저장
- src/messages/{ko,en,ja,es}.json: 초기 30개 키 (header/actions/filter/restaurant/review 5 카테고리)
- src/components/LanguageSwitcher.tsx: 헤더용 드롭다운 (국기 + native, ARIA listbox, 44px 터치)
- providers.tsx: LocaleProvider로 AuthProvider 감싸기
- page.tsx 헤더에 LanguageSwitcher 배치

설계서: docs/design/352-i18n-skeleton/README.md (Approved)

언어 선택 근거:
- ko: 기본
- en: 글로벌 1순위
- ja: 일본 사용자 + 한국 음식 관광
- es: 5억 화자, 라틴아메리카 + 스페인 확장

미번역 키는 ko fallback. URL 라우팅(/en/)/SEO meta/사용자 콘텐츠 번역은 후속.

Refs: #352
This commit is contained in:
joungmin
2026-06-15 15:58:21 +09:00
parent fda2d76514
commit 6cbf7feaf5
12 changed files with 1164 additions and 4 deletions

View File

@@ -0,0 +1,56 @@
"use client";
import { NextIntlClientProvider } from "next-intl";
import { useEffect, useState } from "react";
import { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, Locale, detectBrowserLocale, isLocale } from "./config";
// #352 — 메시지 파일을 정적 import (Tree-shaking 가능, 클라이언트 번들에 4언어 모두 포함되지만
// 30개 키 수준이라 부담 미미. 키가 늘어나면 동적 import로 분할 검토)
import ko from "@/messages/ko.json";
import en from "@/messages/en.json";
import ja from "@/messages/ja.json";
import es from "@/messages/es.json";
const MESSAGES: Record<Locale, typeof ko> = { ko, en, ja, es };
interface LocaleContextValue {
locale: Locale;
setLocale: (l: Locale) => void;
}
import { createContext, useContext } from "react";
const LocaleContext = createContext<LocaleContextValue | null>(null);
export function useLocale() {
const ctx = useContext(LocaleContext);
if (!ctx) throw new Error("useLocale must be used within LocaleProvider");
return ctx;
}
export function LocaleProvider({ children }: { children: React.ReactNode }) {
// SSR 단계는 기본 로케일로 시작 (hydration mismatch 방지)
const [locale, setLocaleState] = useState<Locale>(DEFAULT_LOCALE);
useEffect(() => {
if (typeof window === "undefined") return;
const saved = localStorage.getItem(LOCALE_STORAGE_KEY);
const initial: Locale = isLocale(saved) ? saved : detectBrowserLocale();
if (initial !== locale) setLocaleState(initial);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const setLocale = (l: Locale) => {
setLocaleState(l);
if (typeof window !== "undefined") {
localStorage.setItem(LOCALE_STORAGE_KEY, l);
}
};
return (
<LocaleContext.Provider value={{ locale, setLocale }}>
<NextIntlClientProvider locale={locale} messages={MESSAGES[locale]}>
{children}
</NextIntlClientProvider>
</LocaleContext.Provider>
);
}