- 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
142 lines
6.0 KiB
Markdown
142 lines
6.0 KiB
Markdown
# 설계서: i18n 뼈대 — ko/en/ja/es (#352)
|
||
|
||
> **상태**: Approved
|
||
> **작성**: [AI] Architect · **최종수정**: 2026-06-15
|
||
> **추적성** — Redmine: #352
|
||
> · 구현 파일: `frontend/package.json`, `frontend/next.config.ts`, `frontend/src/i18n/*` (신규), `frontend/src/messages/{ko,en,ja,es}.json` (신규), `frontend/src/components/LanguageSwitcher.tsx` (신규)
|
||
> · 테스트: 수동 (언어 전환 + ko fallback)
|
||
|
||
## 1. 목적
|
||
|
||
장기적으로 영어/일본어/스페인어 시장으로 확장 가능하도록 i18n 뼈대 구축. 본 이슈는 *뼈대*만 — 약 30개 키 초기 번역.
|
||
|
||
## 2. 범위
|
||
|
||
- **포함**
|
||
- `next-intl` 라이브러리 도입 (Next.js 16 App Router 권장).
|
||
- 4개 로케일: `ko, en, ja, es`. 기본 `ko`, fallback `ko`.
|
||
- 메시지 디렉토리 `src/messages/{ko,en,ja,es}.json`.
|
||
- Provider (`<NextIntlClientProvider>`) 루트 layout 적용.
|
||
- `LanguageSwitcher` 컴포넌트 (헤더 우측).
|
||
- 로케일 저장: localStorage `tasteby_locale`.
|
||
- 초기 번역 키 ~30개: 헤더(로그인/검색/메뉴) + 주요 액션(저장/취소/삭제/확인) + 페이지 제목.
|
||
- **제외**
|
||
- URL 라우팅 i18n (`/ko/...`).
|
||
- SEO meta tags i18n.
|
||
- 식당명/리뷰 등 사용자 콘텐츠 번역.
|
||
- 어드민 페이지(운영자만 사용, 한국어 유지).
|
||
- 식당 카드/상세 시트 전체 키 추출 (점진).
|
||
|
||
## 3. 인수조건
|
||
|
||
- [ ] `npm i next-intl` + 4개 메시지 파일 생성.
|
||
- [ ] `tasteby_locale`이 localStorage에 있으면 사용, 없으면 브라우저 언어 감지(`navigator.language`) → 매칭 안되면 `ko`.
|
||
- [ ] 헤더 우측 LanguageSwitcher 드롭다운(국기 + 코드).
|
||
- [ ] 초기 번역 키 약 30개 — 4개 언어 모두 채움.
|
||
- [ ] 미번역 키는 `ko` fallback (에러 없이).
|
||
- [ ] 빌드/배포 회귀 없음.
|
||
|
||
## 4. 컨텍스트 & 제약
|
||
|
||
- Next.js 16 + App Router + `"use client"` 컴포넌트 다수.
|
||
- 기존 `auth-context`처럼 i18n도 React Context 패턴.
|
||
- `next-intl`은 server/client 모두 지원, 본 프로젝트는 client-side switching이라 `NextIntlClientProvider` 중심.
|
||
- URL 라우팅 변경 없이 단순 메시지만 교체(낮은 비용).
|
||
- 폰트: Pretendard Variable이 한국어/영어 잘 표시, 일본어는 시스템 폰트 fallback OK, 스페인어는 라틴 문자라 OK.
|
||
|
||
## 5. 아키텍처 개요
|
||
|
||
```
|
||
frontend/
|
||
├── src/
|
||
│ ├── i18n/
|
||
│ │ ├── config.ts ← 로케일 목록/기본값 상수
|
||
│ │ ├── LocaleProvider.tsx ← NextIntlClientProvider wrap + localStorage 저장
|
||
│ │ └── useTranslations.ts ← (next-intl 재export)
|
||
│ ├── messages/
|
||
│ │ ├── ko.json ← 기본
|
||
│ │ ├── en.json
|
||
│ │ ├── ja.json
|
||
│ │ └── es.json
|
||
│ ├── components/
|
||
│ │ └── LanguageSwitcher.tsx ← 헤더용
|
||
│ └── app/
|
||
│ └── layout.tsx ← LocaleProvider로 감싸기
|
||
```
|
||
|
||
## 6. 데이터 모델
|
||
|
||
### 메시지 키 (초기 ~30)
|
||
```json
|
||
{
|
||
"header": {
|
||
"search": "검색", "login": "로그인", "logout": "로그아웃",
|
||
"menu": "메뉴", "myReviews": "내 리뷰", "favorites": "즐겨찾기"
|
||
},
|
||
"actions": {
|
||
"save": "저장", "cancel": "취소", "delete": "삭제",
|
||
"edit": "수정", "confirm": "확인", "close": "닫기",
|
||
"loading": "로딩 중...", "submit": "제출"
|
||
},
|
||
"filter": {
|
||
"title": "필터", "cuisine": "음식 종류", "price": "가격대",
|
||
"region": "지역", "channel": "채널", "reset": "초기화"
|
||
},
|
||
"restaurant": {
|
||
"rating": "평점", "address": "주소", "phone": "전화",
|
||
"website": "웹사이트", "closed": "폐업", "tempClosed": "임시휴업"
|
||
},
|
||
"review": {
|
||
"title": "리뷰", "write": "리뷰 작성", "noReviews": "아직 리뷰가 없습니다"
|
||
}
|
||
}
|
||
```
|
||
|
||
총 5개 카테고리 × 평균 6개 = 30개 키.
|
||
|
||
## 7. 함수 명세
|
||
|
||
| 함수/컴포넌트 | 책임 | 비고 |
|
||
|---|---|---|
|
||
| `i18n/config.ts` | LOCALES, DEFAULT_LOCALE 상수 | 단순 |
|
||
| `LocaleProvider.tsx` | NextIntlClientProvider wrap + 메시지 동적 로딩 + localStorage 동기화 | client |
|
||
| `LanguageSwitcher.tsx` | 헤더 드롭다운 (국기 + 코드) | client, 44px 터치 |
|
||
| `messages/<lang>.json` | 키 → 텍스트 | flat or nested |
|
||
|
||
## 8. 흐름
|
||
|
||
1. 사용자 첫 방문 → `tasteby_locale` 없음 → `navigator.language.split('-')[0]`이 LOCALES에 있으면 사용, 아니면 `ko`.
|
||
2. LocaleProvider가 해당 로케일 메시지 파일 import → NextIntlClientProvider에 전달.
|
||
3. 컴포넌트는 `useTranslations('header')` 등으로 호출.
|
||
4. LanguageSwitcher에서 변경 → localStorage 저장 → 페이지 새로고침 또는 state 업데이트.
|
||
|
||
## 9. 엣지케이스
|
||
|
||
- **메시지 파일 누락 키**: next-intl 기본 동작은 키 자체 표시 + 콘솔 경고. fallback 처리는 messages 명시.
|
||
- **localStorage 비활성/SSR**: typeof window 체크.
|
||
- **로케일 코드 대소문자**: 항상 소문자 정규화.
|
||
- **placeholder 변수**: next-intl ICU 메시지 형식 지원 (`{name}` 등). 초기 키에는 미적용.
|
||
|
||
## 10. 테스트
|
||
|
||
- 수동:
|
||
- 한국어 첫 방문 → "검색" 표시.
|
||
- LanguageSwitcher에서 English → "Search" 표시.
|
||
- 새로고침 후 영어 유지.
|
||
- 메시지 누락 키 → 콘솔 경고 + 키 표시.
|
||
|
||
## 11. 리스크 & 대안
|
||
|
||
- **선택**: `next-intl` (Next.js 16 App Router 권장, ICU 메시지, 활발 유지보수).
|
||
- **대안 A**: `react-i18next` — 더 일반적이지만 App Router 통합 next-intl이 더 매끄러움.
|
||
- **대안 B**: 자체 구현 + Context — 의존성 ↓ but 기능/표준화 ↓.
|
||
- **트레이드오프**: 30개 키는 단순하지만 ICU 메시지(복수형, 성별 등) 필요 시 next-intl 가치 큼.
|
||
|
||
## 12. 미해결 질문
|
||
|
||
- 식당명/리뷰 콘텐츠 번역 — 사용자 작성이라 자동 번역(LLM)? 별도 정책 결정.
|
||
- URL 라우팅 i18n (`/en/`) — 후속.
|
||
- SEO meta tags i18n — 후속.
|
||
- 어드민 페이지는 운영자 한국어 유지 — 확정.
|
||
- 통화/날짜 포맷(Intl API) — 후속.
|