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:
56
frontend/src/i18n/LocaleProvider.tsx
Normal file
56
frontend/src/i18n/LocaleProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user