--- id: frontend-i18n-patterns title: i18n — 번역 / 복수형 / RTL / ICU category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [frontend, i18n, l10n, react, vibe-coding] tech_stack: { language: "TS / React / i18next / FormatJS", applicable_to: ["Web", "Mobile"] } applied_in: [] aliases: [internationalization, localization, ICU MessageFormat, RTL, plural rules] --- # i18n > "1 item / 5 items" 가 모든 언어에 통하지 않음 (러시아어 1/2-4/5+, 아랍어 0/1/2/few/many/other). **ICU MessageFormat** 표준. `react-i18next` / `react-intl` (FormatJS) / `lingui`. ## 📖 핵심 개념 - ICU MessageFormat: `{count, plural, one {# item} other {# items}}`. - Locale = 언어+지역 (`en-US`, `pt-BR`). - RTL: 아랍어/히브리어 — 화면 좌우 반전. - Lazy loading: 큰 번역 파일은 lang 별로 로딩. ## 💻 코드 패턴 ### react-i18next ```ts // i18n.ts import i18n from 'i18next'; import { initReactI18next } from 'react-i18next'; import HttpBackend from 'i18next-http-backend'; i18n.use(HttpBackend).use(initReactI18next).init({ fallbackLng: 'en', supportedLngs: ['en', 'ko', 'ja', 'ar'], interpolation: { escapeValue: false }, // React 자체 escape backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' }, }); ``` ```ts // public/locales/en/common.json { "greeting": "Hello, {{name}}", "items": "{count, plural, one {# item} other {# items}}", "lastSeen": "Last seen {date, date, medium}" } ``` ```tsx import { useTranslation } from 'react-i18next'; function Hello({ name, count }: { name: string; count: number }) { const { t } = useTranslation(); return

{t('greeting', { name })}: {t('items', { count })}

; } ``` ### FormatJS / react-intl ```tsx import { FormattedMessage, useIntl } from 'react-intl'; ``` ### Lingui (compile-time, 작은 bundle) ```tsx import { Trans, t } from '@lingui/macro'; Hello {name}; const msg = t`You have ${n} items`; // macro 가 build 시 catalog 추출 ``` ### 날짜 / 숫자 ```ts new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(1234.5); // $1,234.50 new Intl.DateTimeFormat('ko-KR', { dateStyle: 'medium' }).format(new Date()); // 2026. 5. 9. new Intl.RelativeTimeFormat('en').format(-3, 'day'); // 3 days ago new Intl.ListFormat('en').format(['A', 'B', 'C']); // A, B, and C ``` ### RTL ```tsx import { useTranslation } from 'react-i18next'; const { i18n } = useTranslation(); const dir = i18n.dir(); // 'rtl' | 'ltr' return ...; ``` ```css /* logical properties (RTL 자동) */ .box { padding-inline-start: 1rem; } /* LTR=left, RTL=right */ .icon { margin-inline-end: 0.5rem; } ``` ### 복수형 cases ``` {count, plural, =0 {No items} one {One item} few {# items} // 슬라브 언어 등 many {# items} // 러시아어 등 other {# items}} ``` ### Type-safe key ```ts import type { TFunction } from 'i18next'; type Keys = 'greeting' | 'items' | 'lastSeen'; type SafeT = (k: Keys, opts?: Record) => string; ``` 또는 i18next-typescript / typesafe-i18n 사용. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | Next.js 프로젝트 | next-intl 또는 next-i18next | | 가벼운 / 작은 앱 | i18next | | 강력 type 안전 + 작은 bundle | Lingui | | Apple 표준 | Apple FormatJS / NSLocalizedString | | Plural / gender / select 복잡 | ICU MessageFormat 필수 | | Translation memory | Crowdin / Lokalise / Phrase | ## ❌ 안티패턴 - **문장 concat**: "Hello " + name + "!" — 문장 순서가 언어마다 다름. - **숫자 + 단위 직접 결합**: ICU plural. - **Hard-coded date format**: Intl.DateTimeFormat. - **`px` margin 으로 RTL 깨짐**: logical properties. - **번역 미완료 = `{key}` 노출**: fallback + 자동화 (Lokalise PR). - **런타임 detection 만**: SSR 시 UA / Accept-Language. - **모든 언어 한 번에 로드**: lazy load lang별. ## 🤖 LLM 활용 힌트 - ICU MessageFormat 강력 권장. - Intl API 표준 (NumberFormat, DateTimeFormat, RelativeTimeFormat). - RTL = `dir` + logical properties. ## 🔗 관련 문서 - [[Frontend_A11y_Testing]]