236 lines
5.3 KiB
Markdown
236 lines
5.3 KiB
Markdown
# 다국어 지원 (i18n) 가이드
|
|
|
|
라온누리 프로젝트의 다국어 지원 시스템 가이드입니다.
|
|
|
|
---
|
|
|
|
## 필수 적용 규칙
|
|
|
|
**모든 새로운 UI 및 페이지는 다국어 지원이 필수입니다.**
|
|
|
|
---
|
|
|
|
## 기본 규칙
|
|
|
|
1. **하드코딩 텍스트 금지**: 모든 사용자 대면 텍스트는 번역 파일(`messages/ko.json`, `messages/en.json`, `messages/ja.json`)에 저장
|
|
2. **번역 키 사용**: `useTranslations('namespace')` 훅으로 번역 텍스트 사용
|
|
3. **타입 안전 Link**: `import {Link} from '@/i18n/routing'` 사용 (next/link 대신)
|
|
4. **타입 안전 Router**: `import {useRouter} from '@/i18n/routing'` 사용 (next/navigation 대신)
|
|
|
|
---
|
|
|
|
## 페이지 생성 시
|
|
|
|
```typescript
|
|
// ❌ 잘못된 방식
|
|
"use client";
|
|
import {useRouter} from "next/navigation";
|
|
|
|
export default function NewPage() {
|
|
return <h1>새 페이지</h1>; // 하드코딩 금지!
|
|
}
|
|
|
|
// ✅ 올바른 방식
|
|
"use client";
|
|
import {useRouter} from "@/i18n/routing"; // next-intl router
|
|
import {useTranslations} from "next-intl";
|
|
|
|
export default function NewPage() {
|
|
const t = useTranslations('newPage');
|
|
|
|
return <h1>{t('title')}</h1>;
|
|
}
|
|
|
|
// messages/ko.json에 추가:
|
|
// "newPage": { "title": "새 페이지" }
|
|
|
|
// messages/en.json에 추가:
|
|
// "newPage": { "title": "New Page" }
|
|
|
|
// messages/ja.json에 추가:
|
|
// "newPage": { "title": "新しいページ" }
|
|
```
|
|
|
|
---
|
|
|
|
## 컴포넌트 생성 시
|
|
|
|
```typescript
|
|
// 공통 컴포넌트는 namespace를 props로 받거나 고정
|
|
"use client";
|
|
import {useTranslations} from "next-intl";
|
|
|
|
export function MyComponent() {
|
|
const t = useTranslations('components.myComponent');
|
|
|
|
return (
|
|
<Button>{t('submit')}</Button>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Server Component에서 번역 사용
|
|
|
|
Server Component에서는 `getTranslations()` 함수를 사용합니다:
|
|
|
|
```typescript
|
|
// ✅ Server Component
|
|
import {getTranslations} from "next-intl/server";
|
|
|
|
export default async function WritingDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{locale: string; writingId: string}>;
|
|
}) {
|
|
const {writingId} = await params;
|
|
const t = await getTranslations('interaction');
|
|
|
|
return (
|
|
<Container>
|
|
<BackButton label={t('back')} />
|
|
<Heading>{writing.title}</Heading>
|
|
</Container>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 서비스 레이어에서 번역 사용
|
|
|
|
React 훅을 사용할 수 없는 곳에서는 `src/utils/i18n.ts`의 `t()` 함수를 사용합니다:
|
|
|
|
```typescript
|
|
// src/services/firebaseAuth.ts
|
|
import { t } from "@/utils/i18n";
|
|
|
|
export function getErrorMessage(code: string): string {
|
|
switch (code) {
|
|
case "auth/invalid-email":
|
|
return t("errors.auth.invalidEmail");
|
|
case "auth/weak-password":
|
|
return t("errors.auth.weakPassword", { min: 8 });
|
|
default:
|
|
return t("errors.auth.unknown");
|
|
}
|
|
}
|
|
```
|
|
|
|
**파일 위치**: `src/utils/i18n.ts`
|
|
- `detectLocale()`: URL path 우선 → navigator.language fallback
|
|
- `t()`: nested key 지원, 파라미터 치환
|
|
|
|
---
|
|
|
|
## 체크리스트 (페이지/컴포넌트 생성 시)
|
|
|
|
- [ ] 모든 사용자 대면 텍스트를 번역 키로 추출
|
|
- [ ] `messages/ko.json`, `messages/en.json`, `messages/ja.json`에 번역 추가
|
|
- [ ] `useTranslations` 훅 사용 (Client Component)
|
|
- [ ] `getTranslations` 함수 사용 (Server Component)
|
|
- [ ] next-intl의 Link/Router 사용
|
|
- [ ] 파라미터가 있는 경우 `{name}` 플레이스홀더 사용
|
|
- [ ] 일본어는 어린이 친화적 표현 (한자 최소화, ひらがな 우선)
|
|
|
|
---
|
|
|
|
## 일본어 번역 주의사항
|
|
|
|
- **한자 최소화**: 초등학생 대상이므로 가능한 한 히라가나 사용
|
|
- **정중한 표현**: です/ます 체 사용
|
|
- **예시**:
|
|
```json
|
|
// ❌ Bad
|
|
"login": "ログイン"
|
|
|
|
// ✅ Good
|
|
"login": "ログインする"
|
|
```
|
|
|
|
---
|
|
|
|
## 언어별 파일
|
|
|
|
| 언어 | 파일 | 라인 수 | 키 개수 |
|
|
|------|------|--------|--------|
|
|
| 한국어 | `messages/ko.json` | 407줄 | 220+ 키 |
|
|
| 영어 | `messages/en.json` | 407줄 | 220+ 키 |
|
|
| 일본어 | `messages/ja.json` | 407줄 | 220+ 키 |
|
|
|
|
---
|
|
|
|
## 언어 전환 UI
|
|
|
|
LocaleSwitcher 컴포넌트 (`src/components/layout/LocaleSwitcher.tsx`):
|
|
- 국기 이모지 (🇰🇷 🇺🇸 🇯🇵)
|
|
- 현재 언어 체크 표시
|
|
- Portal 사용 (z-index 이슈 방지)
|
|
|
|
---
|
|
|
|
## 설정 파일
|
|
|
|
### middleware.ts
|
|
|
|
```typescript
|
|
import createMiddleware from "next-intl/middleware";
|
|
import { routing } from "./i18n/routing";
|
|
|
|
export default createMiddleware(routing);
|
|
|
|
export const config = {
|
|
matcher: ["/", "/(ko|en|ja)/:path*"],
|
|
};
|
|
```
|
|
|
|
### i18n/routing.ts
|
|
|
|
```typescript
|
|
import { defineRouting } from "next-intl/routing";
|
|
|
|
export const routing = defineRouting({
|
|
locales: ["ko", "en", "ja"],
|
|
defaultLocale: "ko",
|
|
localePrefix: "always",
|
|
});
|
|
```
|
|
|
|
### next.config.ts
|
|
|
|
```typescript
|
|
import createNextIntlPlugin from "next-intl/plugin";
|
|
|
|
const withNextIntl = createNextIntlPlugin();
|
|
|
|
export default withNextIntl({
|
|
// ... other config
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 라우팅 구조
|
|
|
|
```
|
|
/ko/* - 한국어 페이지
|
|
/en/* - 영어 페이지
|
|
/ja/* - 일본어 페이지
|
|
```
|
|
|
|
- 브라우저 언어 자동 감지 (Accept-Language 헤더)
|
|
- NEXT_LOCALE 쿠키 저장
|
|
- localeDetection: true 설정
|
|
|
|
---
|
|
|
|
## 관련 문서
|
|
|
|
- [next-intl Documentation](https://next-intl-docs.vercel.app/)
|
|
- [CLAUDE.md](./CLAUDE.md) - 개발 가이드
|
|
- [TECH_STACK.md](./TECH_STACK.md) - 기술 스택
|
|
|
|
---
|
|
|
|
© 2024 BlueNovaLab. All rights reserved. |