- 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
58 lines
2.0 KiB
TypeScript
58 lines
2.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useLocale } from "@/i18n/LocaleProvider";
|
|
import { LOCALES, LOCALE_LABELS } from "@/i18n/config";
|
|
|
|
// #352 — 헤더용 언어 전환 드롭다운. 44px 터치 영역 + ARIA listbox 패턴.
|
|
export default function LanguageSwitcher() {
|
|
const { locale, setLocale } = useLocale();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setOpen((v) => !v)}
|
|
aria-haspopup="listbox"
|
|
aria-expanded={open}
|
|
aria-label="언어 선택 / Select language"
|
|
className="min-h-[44px] min-w-[44px] px-2 py-1.5 flex items-center gap-1 text-sm rounded-lg hover:bg-brand-50 touch-manipulation"
|
|
>
|
|
<span aria-hidden="true">{LOCALE_LABELS[locale].flag}</span>
|
|
<span className="text-xs text-gray-500 uppercase">{locale}</span>
|
|
</button>
|
|
{open && (
|
|
<>
|
|
<button
|
|
aria-hidden="true"
|
|
tabIndex={-1}
|
|
className="fixed inset-0 z-40 cursor-default"
|
|
onClick={() => setOpen(false)}
|
|
/>
|
|
<ul
|
|
role="listbox"
|
|
aria-label="언어 목록"
|
|
className="absolute right-0 mt-1 bg-surface rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50 min-w-[160px]"
|
|
>
|
|
{LOCALES.map((l) => (
|
|
<li key={l}>
|
|
<button
|
|
role="option"
|
|
aria-selected={l === locale}
|
|
onClick={() => { setLocale(l); setOpen(false); }}
|
|
className={`w-full text-left px-3 py-2 text-sm flex items-center gap-2 hover:bg-brand-50 ${
|
|
l === locale ? "font-semibold text-brand-700" : "text-gray-700"
|
|
}`}
|
|
>
|
|
<span aria-hidden="true">{LOCALE_LABELS[l].flag}</span>
|
|
<span>{LOCALE_LABELS[l].native}</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|