클라이언트 앱 다국어(i18n) 가이드
작성일: 2026-04-06 Last Updated: 2026-04-06 대상: Browser Extension, Tauri Desktop App, Flutter Mobile App
1. 다국어 아키텍처 원칙
핵심 원칙
| 원칙 | 설명 |
|---|---|
| 키 기반 번역 | UI에 직접 문자열 삽입 금지, 반드시 t('key') 형태 사용 |
| ICU MessageFormat | 복수형, 성별, 선택 등 복잡한 패턴 처리의 표준 |
| 기본 언어 fallback | 번역 누락 시 기본 언어(en) 표시, 빈 문자열 금지 |
| 컴파일 타임 검증 | 타입 안전한 번역 키 사용 (자동 완성 지원) |
| 지연 로딩 | 사용 중인 언어만 로드, 전체 언어 번들 금지 |
ICU MessageFormat 예시
# 복수형
items_count = "{count, plural, =0 {항목 없음} one {# 항목} other {# 항목}}"
# 성별
greeting = "{gender, select, male {그가} female {그녀가} other {그 사람이}} 로그인했습니다"
# 중첩 (복수형 + 성별)
notification = "{gender, select,
male {{count, plural, one {그가 # 개를 추가했습니다} other {그가 # 개를 추가했습니다}}}
female {{count, plural, one {그녀가 # 개를 추가했습니다} other {그녀가 # 개를 추가했습니다}}}
other {{count, plural, one {# 개가 추가되었습니다} other {# 개가 추가되었습니다}}}
}"
2. 번역 파일 관리
파일 형식
| 플랫폼 | 파일 형식 | 위치 |
|---|---|---|
| Browser Extension | JSON (messages.json) | public/_locales/{locale}/messages.json |
| Tauri (프론트엔드) | JSON (네임스페이스별) | src/shared/i18n/locales/{locale}/ |
| Flutter | ARB (Application Resource Bundle) | lib/l10n/app_{locale}.arb |
폴더 구조
# Browser Extension (chrome.i18n 표준)
public/_locales/
+-- en/messages.json
+-- ko/messages.json
+-- ja/messages.json
# 프론트엔드 공통 (네임스페이스 분리)
src/shared/i18n/
+-- locales/
| +-- en/
| | +-- common.json # 공통 (버튼, 라벨)
| | +-- auth.json # 인증 관련
| | +-- dashboard.json # 대시보드
| | +-- errors.json # 에러 메시지
| +-- ko/
| | +-- common.json
| | +-- auth.json
| | +-- dashboard.json
| | +-- errors.json
+-- index.ts # i18n 초기화
+-- types.ts # 타입 정의
# Flutter (ARB)
lib/l10n/
+-- app_en.arb # 기본 언어 (모든 키 포함)
+-- app_ko.arb
+-- app_ja.arb
번역 키 네이밍 컨벤션
| 패턴 | 예시 | 설명 |
|---|---|---|
{기능}.{컴포넌트}.{동작} | auth.login.submit | 기능별 분류 |
{기능}.{상태}.{설명} | auth.error.invalidEmail | 상태별 분류 |
common.{카테고리}.{이 름} | common.button.save | 공통 요소 |
error.{코드} | error.networkTimeout | 에러 메시지 |
번역 파일 JSON 예시
{
"auth": {
"login": {
"title": "로그인",
"email": "이메일",
"password": "비밀번호",
"submit": "로그인",
"forgotPassword": "비밀번호 찾기",
"noAccount": "계정이 없으신가요? {signUpLink}에서 가입하세요"
},
"error": {
"invalidEmail": "올바른 이메일 형식이 아닙니다",
"wrongPassword": "비밀번호가 일치하지 않습니다",
"tooManyAttempts": "{minutes}분 후 다시 시도하세요"
}
}
}
Flutter ARB 예시
{
"@@locale": "ko",
"loginTitle": "로그인",
"@loginTitle": {
"description": "로그인 페이지 제목"
},
"itemCount": "{count, plural, =0{항목 없음} =1{1개 항목} other{{count}개 항목}}",
"@itemCount": {
"description": "아이템 개수 표시",
"placeholders": {
"count": {
"type": "int"
}
}
}
}
3. RTL(Right-to-Left) 지원
RTL 언어 목록
| 언어 | 코드 | 사용 인구 |
|---|---|---|
| 아랍어 | ar | ~4억 |
| 히브리어 | he | ~900만 |
| 페르시아어 | fa | ~1.1억 |
| 우르두어 | ur | ~2.3억 |
RTL 구현 체크리스트
| 항목 | 구현 방법 | 적용 대상 |
|---|---|---|
| HTML dir 속성 | <html dir="rtl"> 또는 dir="auto" | Browser Ext, Tauri |
| CSS 논리적 속성 | margin-inline-start 대신 margin-left 사용 금지 | Browser Ext, Tauri |
| Flexbox 방향 | direction: rtl 시 자동 반전 | Browser Ext, Tauri |
| 아이콘 미러링 | 방향성 있는 아이콘(화살표 등) 좌우 반전 | 전체 |
| 텍스트 정렬 | text-align: start 사용 (left 금지) | Browser Ext, Tauri |
| Flutter Directionality | Directionality 위젯 또는 TextDirection | Flutter |
| Flutter EdgeInsetsDirectional | EdgeInsets 대신 EdgeInsetsDirectional | Flutter |
CSS 논리적 속성 매핑
/* 금지: 물리적 속성 */
margin-left: 16px;
padding-right: 8px;
border-left: 1px solid;
text-align: left;
/* 권장: 논리적 속성 */
margin-inline-start: 16px;
padding-inline-end: 8px;
border-inline-start: 1px solid;
text-align: start;
4. 복수형/성별 처리
CLDR 복수형 규칙
| 카테고리 | 영어 | 한국어 | 아랍어 |
|---|---|---|---|
| zero | - | - | 0 |
| one | 1 | - | 1 |
| two | - | - | 2 |
| few | - | - | 3-10 |
| many | - | - | 11-99 |
| other | 나머지 | 전부 | 100+ |
한국어는 복수형 구분이 없어
other만 사용. 하지만 다국어 지원 시 모든 CLDR 카테고리를 기본 언어(en)에서 정의해야 한다.
구현 예시
// i18next (Browser Ext, Tauri)
// en/common.json
{
"item_count_zero": "No items",
"item_count_one": "{{count}} item",
"item_count_other": "{{count}} items"
}
// 사용
t('item_count', { count: 5 }) // "5 items"
// Flutter (intl 패키지)
// app_en.arb
{
"itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}"
}
// 사용
AppLocalizations.of(context)!.itemCount(5) // "5 items"
5. 날짜/숫자/통화 포맷
Intl API 활용 (Browser Ext, Tauri)
// 날짜 포맷
new Intl.DateTimeFormat('ko-KR', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(date); // "2026년 4월 6일"
// 숫자 포맷
new Intl.NumberFormat('ko-KR').format(1234567.89); // "1,234,567.89"
// 통화 포맷
new Intl.NumberFormat('ko-KR', {
style: 'currency',
currency: 'KRW'
}).format(50000); // "₩50,000"
// 상대 시간
new Intl.RelativeTimeFormat('ko', { numeric: 'auto' })
.format(-3, 'hour'); // "3시간 전"
Flutter (intl 패키지)
import 'package:intl/intl.dart';
// 날짜 포맷
DateFormat.yMMMMd('ko').format(DateTime.now()); // "2026년 4월 6일"
// 숫자 포맷
NumberFormat('#,###', 'ko').format(1234567); // "1,234,567"
// 통화 포맷
NumberFormat.currency(locale: 'ko_KR', symbol: '₩', decimalDigits: 0)
.format(50000); // "₩50,000"
포맷 원칙
| 원칙 | 설명 |
|---|---|
| 서버에서 ISO 8601 수신 | 2026-04-06T12:00:00Z |
| 클라이언트에서 로컬 변환 | Intl API 또는 intl 패키지 사용 |
| 통화 코드 서버 전달 | 금액과 함께 ISO 4217 통화 코드 전달 |
| 하드코딩 포 맷 금지 | YYYY-MM-DD 같은 고정 포맷 사용 금지 |
| 로케일 연동 | 앱 언어 설정에 따라 포맷 자동 변경 |
6. 언어 감지 전략
우선순위 (높은 순)
| 순위 | 소스 | 설명 |
|---|---|---|
| 1 | 사용자 명시 설정 | 앱 설정에서 직접 선택한 언어 |
| 2 | 서버 사용자 프로필 | 백엔드에 저장된 언어 설정 |
| 3 | 브라우저/OS 설정 | 시스템 언어 |
| 4 | 기본 언어 | en (fallback) |
플랫폼별 감지 방법
| 플랫폼 | 감지 API | 예시 |
|---|---|---|
| Browser Extension | chrome.i18n.getUILanguage() | "ko" |
| Browser (일반) | navigator.languages[0] | "ko-KR" |
| Tauri (Rust) | sys_locale::get_locale() | "ko-KR" |
| Tauri (프론트엔드) | navigator.languages[0] | "ko-KR" |
| Flutter (Android) | Platform.localeName | "ko_KR" |
| Flutter (iOS) | Platform.localeName | "ko_KR" |
| Flutter (권장) | PlatformDispatcher.instance.locale | Locale('ko', 'KR') |
구현 패턴
// 언어 감지 순서 (TypeScript)
function detectLanguage(): string {
// 1. 사용자 명시 설정
const saved = localStorage.getItem('user_language');
if (saved) return saved;
// 2. 서버 프로필 (캐시된)
const profile = getCachedUserProfile();
if (profile?.language) return profile.language;
// 3. 브라우저/OS
const browserLang = navigator.languages?.[0] || navigator.language;
const supported = ['en', 'ko', 'ja', 'zh'];
const detected = browserLang.split('-')[0];
if (supported.includes(detected)) return detected;
// 4. 기본값
return 'en';
}
7. 플랫폼별 i18n 라이브러리 권장
Browser Extension
| 라이브러리 | 용도 | 비고 |
|---|---|---|
chrome.i18n (내장) | Manifest 기반 번역 | Chrome 표준, 제한적 기능 |
i18next + react-i18next | 런타임 번역 | 가장 범용적, ICU 지원 |
@formatjs/intl | ICU MessageFormat | FormatJS 생태계 |
typesafe-i18n | 타입 안전 번역 | 자동 타입 생성 |
권장 조합: i18next + react-i18next + i18next-icu (ICU 플러그인)
Tauri Desktop App
| 라이브러리 | 용도 | 비고 |
|---|---|---|
i18next + 프레임워크 바인딩 | 프론트엔드 번역 | React/Vue/Svelte 지원 |
rust-i18n (Rust 측) | Rust 백엔드 메시지 | 에러 메시지, 트레이 메뉴 |
@formatjs/intl | ICU MessageFormat | FormatJS 생태계 |
lingui | 매크로 기반 | 번역 키 자동 추출 |
권장 조합: i18next (프론트엔드) + rust-i18n (Rust)
Flutter
| 라이브러리 | 용도 | 비고 |
|---|---|---|
flutter_localizations (내장) | 위젯 로컬라이제이션 | Material/Cupertino |
intl + flutter gen-l10n | ARB 기반 번역 | 공식 권장 |
easy_localization | 간편한 i18n | JSON/YAML 지원 |
slang | 타입 안전 | 코드 생성 기반 |
권장 조합: flutter_localizations + intl + flutter gen-l10n (공식 표준)
8. 번역 워크플로우
자동 키 추출
# i18next-parser (TS/JS 프로젝트)
npx i18next-parser 'src/**/*.{ts,tsx}' --output 'src/shared/i18n/locales/$LOCALE/$NAMESPACE.json'
# Flutter gen-l10n
flutter gen-l10n
번역 서비스 연동
| 서비스 | 특징 | API 지원 |
|---|---|---|
| Crowdin | Git 연동, OTA 업데이트 | O |
| Lokalise | 실시간 협업, 스크린샷 연동 | O |
| Phrase | 번역 메모리, 용어집 | O |
| Transifex | 오픈소스 지원 | O |
| Weblate | 셀프 호스팅 가능 | O |
워크플로우 자동화 파이프라인
1. 개발자가 코드에 t('new.key') 추가
2. CI에서 키 추출 (i18next-parser / gen-l10n)
3. 누락 키 감지 -> 번역 서비스로 자동 Push
4. 번역가가 번역 완료
5. CI에서 번역 파일 Pull -> PR 생성
6. 머지 후 앱에 반영 (또는 OTA 업데이트)
CI 검증 체크리스트
- 모든 키가 기본 언어(en)에 존재하는가
- 번역 파일이 유효한 JSON/ARB 형식인가
- ICU 메시지 구문이 올바른가
- 플레이스홀더가 모든 언어에 동일하게 존재하는가
- 미번역 키 목록 리포트 생성
- 사용되지 않는 키(orphan) 감지
9. 스토어 심사 관련 i18n 요구사항
Chrome Web Store
| 요구사항 | 구현 방법 |
|---|---|
| 스토어 등록 정보 번역 | Chrome Developer Dashboard에서 각 언어별 입력 |
_locales/ 필수 | default_locale 지정 시 _locales/ 존재 필수 |
name, description 번역 | __MSG_appName__ 형태로 manifest.json에 참조 |
Apple App Store
| 요구사항 | 구현 방법 |
|---|---|
| 앱 이름 로컬라이제이션 | InfoPlist.strings 파일로 각 언어별 앱 이름 |
| 스토어 메타데이터 | App Store Connect에서 각 언어별 설명/스크린샷 |
| 앱 내 언어와 스토어 언어 일치 | 지원 언어 목록 동기화 |
Google Play Store
| 요구사항 | 구현 방법 |
|---|---|
| 스토어 등록 정보 번역 | Play Console에서 각 언어별 입력 |
resConfigs 설정 | 사용하는 언어만 APK에 포함 (빌드 최적화) |
| 기본 언어 설정 | defaultConfig { resConfigs "en", "ko" } |
10. 구현 체크리스트
초기 설정
- i18n 라이브러리 설치 및 초기화
- 번역 파일 폴더 구조 생성
- 기본 언어(en) + 한국어(ko) 최소 설정
- 타입 안전 번역 키 설정 (자동 완성)
- 언어 감지 로직 구현 (4단계 우선순위)
- 언어 전환 UI 구현
품질 보장
- ICU MessageFormat 복수형 처리
- RTL 레이아웃 테스트 (아랍어 등)
- 긴 문자열 UI 깨짐 테스트 (독일어 등 ~30% 길어짐)
- 날짜/숫자/통화 로컬 포맷 확인
- 번역 누락 시 fallback 동작 확인
- CI에서 번역 파일 검증 자동화
배포
- 스토어 메타데이터 번역 준비
- 스크린샷 각 언어별 준비
- OTA 번역 업데이트 설정 (선택)
- 번역 서비스 CI 연동 설정