timsquad 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +288 -0
- package/README.md +158 -151
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +170 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +94 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +231 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/upgrade.js +5 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/daemon/entry.js +3 -3
- package/dist/daemon/entry.js.map +1 -1
- package/dist/daemon/index.d.ts +3 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +137 -45
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/meta-cache.d.ts +1 -0
- package/dist/daemon/meta-cache.d.ts.map +1 -1
- package/dist/daemon/meta-cache.js +9 -0
- package/dist/daemon/meta-cache.js.map +1 -1
- package/dist/daemon/session-state.d.ts +19 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +132 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +7 -1
- package/dist/daemon/shutdown.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compile-rules.d.ts +66 -0
- package/dist/lib/compile-rules.d.ts.map +1 -0
- package/dist/lib/compile-rules.js +114 -0
- package/dist/lib/compile-rules.js.map +1 -0
- package/dist/lib/compiler.d.ts +105 -0
- package/dist/lib/compiler.d.ts.map +1 -0
- package/dist/lib/compiler.js +368 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +6 -0
- package/dist/lib/template.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +2 -0
- package/dist/types/project.js.map +1 -1
- package/package.json +1 -1
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/skills/controller/SKILL.md +111 -0
- package/templates/base/skills/controller/references/README.md +35 -0
- package/templates/base/skills/controller/rules/README.md +18 -0
- package/templates/base/skills/mobile/dart/SKILL.md +69 -0
- package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
- package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
- package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
- package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
- package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
- package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
- package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
- package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
- package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
- package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
- package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
- package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
- package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
- package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
- package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
- package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
- package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
- package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
- package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
- package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
- package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
- package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
- package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
- package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
- package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
- package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
- package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
- package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
- package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
- package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
- package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
- package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
- package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
- package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
- package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
- package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +57 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/settings.json +75 -3
- package/templates/project-types/mobile-app/config.yaml +123 -0
- package/templates/project-types/mobile-app/process/workflow.xml +191 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: i18n Architecture & Translation Workflow
|
|
3
|
+
category: reference
|
|
4
|
+
source: internal
|
|
5
|
+
tags: architecture, directory, workflow, ci, missing-keys, translation
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# i18n Architecture & Translation Workflow
|
|
9
|
+
|
|
10
|
+
i18n 서비스 아키텍처. 디렉토리 구조, 번역 워크플로우, CI 자동 검증, 누락 키 감지.
|
|
11
|
+
|
|
12
|
+
## Key Concepts
|
|
13
|
+
|
|
14
|
+
- **ARB 중심**: 모든 번역의 원본은 ARB 파일 (코드에 문자열 하드코딩 금지)
|
|
15
|
+
- **코드 생성**: `flutter gen-l10n` → 타입 안전한 `AppLocalizations` 클래스 자동 생성
|
|
16
|
+
- **CI 검증**: 빌드 파이프라인에서 누락 키, 미사용 키, 포맷 오류 자동 감지
|
|
17
|
+
- **번역 프로세스**: 개발자 (키 추가) → 번역가 (번역) → 검수 (QA) → 머지
|
|
18
|
+
|
|
19
|
+
## Directory Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
lib/
|
|
23
|
+
├── l10n/
|
|
24
|
+
│ ├── app_en.arb # 기본 (template) — 모든 키 + 메타데이터
|
|
25
|
+
│ ├── app_ko.arb # 한국어
|
|
26
|
+
│ ├── app_ms.arb # 말레이어
|
|
27
|
+
│ ├── app_id.arb # 인도네시아어
|
|
28
|
+
│ └── app_zh.arb # 중국어 (간체)
|
|
29
|
+
│
|
|
30
|
+
├── core/
|
|
31
|
+
│ └── l10n/
|
|
32
|
+
│ ├── locale_notifier.dart # LocaleNotifier (Riverpod)
|
|
33
|
+
│ ├── app_locale.dart # AppLocale enum (지원 로캘 목록)
|
|
34
|
+
│ ├── l10n_extension.dart # BuildContext.l10n extension
|
|
35
|
+
│ └── locale_observer.dart # 시스템 로캘 변경 감지
|
|
36
|
+
│
|
|
37
|
+
├── features/
|
|
38
|
+
│ └── settings/
|
|
39
|
+
│ └── presentation/
|
|
40
|
+
│ └── screens/
|
|
41
|
+
│ └── language_settings_screen.dart # 언어 설정 UI
|
|
42
|
+
│
|
|
43
|
+
└── .dart_tool/
|
|
44
|
+
└── flutter_gen/
|
|
45
|
+
└── gen_l10n/ # 자동 생성 (커밋 X)
|
|
46
|
+
├── app_localizations.dart
|
|
47
|
+
├── app_localizations_en.dart
|
|
48
|
+
├── app_localizations_ko.dart
|
|
49
|
+
└── ...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Translation Workflow
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌──────────────────────────────────────────────────────────┐
|
|
56
|
+
│ 번역 워크플로우 │
|
|
57
|
+
├──────────┬────────────────┬──────────────┬───────────────┤
|
|
58
|
+
│ 1. 개발자 │ 2. 번역가 │ 3. 검수 │ 4. 머지 │
|
|
59
|
+
├──────────┼────────────────┼──────────────┼───────────────┤
|
|
60
|
+
│ app_en.arb│ app_ko.arb │ 화면 확인 │ PR 승인 │
|
|
61
|
+
│ 키 추가 │ app_ms.arb │ 문맥 확인 │ CI 통과 │
|
|
62
|
+
│ @메타데이터│ app_id.arb │ 길이 확인 │ 머지 │
|
|
63
|
+
│ 설명 작성 │ 번역 작성 │ RTL 확인 │ 배포 │
|
|
64
|
+
├──────────┼────────────────┼──────────────┼───────────────┤
|
|
65
|
+
│ PR 생성 │ 번역 PR 생성 │ 리뷰 코멘트 │ gen-l10n 실행 │
|
|
66
|
+
└──────────┴────────────────┴──────────────┴───────────────┘
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 단계별 상세
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
1. 개발자 (키 추가)
|
|
73
|
+
- app_en.arb에 새 키 + @메타데이터 추가
|
|
74
|
+
- description에 화면 위치, 용도, 맥락 설명
|
|
75
|
+
- placeholder에 type, example 명시
|
|
76
|
+
- 스크린샷/화면 위치 정보 첨부 (번역가 맥락 전달)
|
|
77
|
+
|
|
78
|
+
2. 번역가 (번역)
|
|
79
|
+
- 각 로캘 ARB 파일에 번역 추가
|
|
80
|
+
- ICU 복수형/성별 규칙 적용 (언어별)
|
|
81
|
+
- placeholder 위치 조정 (언어 어순에 맞게)
|
|
82
|
+
|
|
83
|
+
3. 검수 (QA)
|
|
84
|
+
- 실제 화면에서 번역 확인 (텍스트 잘림, 줄바꿈)
|
|
85
|
+
- RTL 레이아웃 확인 (해당 시)
|
|
86
|
+
- 복수형/성별 변형 모두 확인
|
|
87
|
+
- 문화적 적절성 확인
|
|
88
|
+
|
|
89
|
+
4. 머지
|
|
90
|
+
- CI 자동 검증 통과 확인
|
|
91
|
+
- flutter gen-l10n 빌드 성공 확인
|
|
92
|
+
- 머지 후 릴리스 포함
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## CI Automated Verification
|
|
96
|
+
|
|
97
|
+
### 누락 키 감지 스크립트
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
#!/bin/bash
|
|
101
|
+
# scripts/check_l10n.sh — CI에서 실행
|
|
102
|
+
|
|
103
|
+
set -e
|
|
104
|
+
|
|
105
|
+
echo "=== i18n Key Verification ==="
|
|
106
|
+
|
|
107
|
+
# 1. gen-l10n 빌드 테스트
|
|
108
|
+
flutter gen-l10n
|
|
109
|
+
echo "✓ gen-l10n succeeded"
|
|
110
|
+
|
|
111
|
+
# 2. 누락 키 체크 (template ARB 기준)
|
|
112
|
+
TEMPLATE="lib/l10n/app_en.arb"
|
|
113
|
+
ERRORS=0
|
|
114
|
+
|
|
115
|
+
for ARB in lib/l10n/app_*.arb; do
|
|
116
|
+
if [ "$ARB" = "$TEMPLATE" ]; then continue; fi
|
|
117
|
+
|
|
118
|
+
LOCALE=$(basename "$ARB" .arb | sed 's/app_//')
|
|
119
|
+
|
|
120
|
+
# template의 비-메타 키 추출
|
|
121
|
+
TEMPLATE_KEYS=$(grep -oP '^\s*"(?!@|@@)\K[^"]+' "$TEMPLATE" | sort)
|
|
122
|
+
LOCALE_KEYS=$(grep -oP '^\s*"(?!@|@@)\K[^"]+' "$ARB" | sort)
|
|
123
|
+
|
|
124
|
+
# 누락 키 찾기
|
|
125
|
+
MISSING=$(comm -23 <(echo "$TEMPLATE_KEYS") <(echo "$LOCALE_KEYS"))
|
|
126
|
+
if [ -n "$MISSING" ]; then
|
|
127
|
+
echo "✗ $LOCALE: Missing keys:"
|
|
128
|
+
echo "$MISSING" | sed 's/^/ /'
|
|
129
|
+
ERRORS=$((ERRORS + 1))
|
|
130
|
+
else
|
|
131
|
+
echo "✓ $LOCALE: All keys present"
|
|
132
|
+
fi
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
if [ $ERRORS -gt 0 ]; then
|
|
136
|
+
echo "FAIL: $ERRORS locale(s) have missing keys"
|
|
137
|
+
exit 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo "=== All i18n checks passed ==="
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 미사용 키 감지
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
#!/bin/bash
|
|
147
|
+
# scripts/check_unused_l10n.sh
|
|
148
|
+
|
|
149
|
+
TEMPLATE="lib/l10n/app_en.arb"
|
|
150
|
+
UNUSED=0
|
|
151
|
+
|
|
152
|
+
# template의 키 목록 추출
|
|
153
|
+
KEYS=$(grep -oP '^\s*"(?!@|@@)\K[^"]+' "$TEMPLATE")
|
|
154
|
+
|
|
155
|
+
for KEY in $KEYS; do
|
|
156
|
+
# Dart 코드에서 사용 여부 확인 (.l10n.$KEY 또는 l10n.$KEY)
|
|
157
|
+
if ! grep -rq "\.$KEY" lib/ --include="*.dart" 2>/dev/null; then
|
|
158
|
+
echo "⚠ Potentially unused: $KEY"
|
|
159
|
+
UNUSED=$((UNUSED + 1))
|
|
160
|
+
fi
|
|
161
|
+
done
|
|
162
|
+
|
|
163
|
+
if [ $UNUSED -gt 0 ]; then
|
|
164
|
+
echo "WARNING: $UNUSED potentially unused key(s)"
|
|
165
|
+
fi
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## ARB Key Organization
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
// 키 그룹화 전략 (featureName_ 접두사로 자연 정렬)
|
|
172
|
+
{
|
|
173
|
+
"@@locale": "en",
|
|
174
|
+
|
|
175
|
+
// === Common (공통) ===
|
|
176
|
+
"common_cancelButton": "Cancel",
|
|
177
|
+
"common_confirmButton": "Confirm",
|
|
178
|
+
"common_deleteButton": "Delete",
|
|
179
|
+
"common_loadingMessage": "Loading...",
|
|
180
|
+
"common_retryButton": "Retry",
|
|
181
|
+
|
|
182
|
+
// === Error (에러) ===
|
|
183
|
+
"error_networkTimeout": "Network timeout. Please try again.",
|
|
184
|
+
"error_serverError": "Server error. Please try later.",
|
|
185
|
+
"error_unauthorized": "Please log in again.",
|
|
186
|
+
|
|
187
|
+
// === Match (매치) ===
|
|
188
|
+
"matchDetail_inviteButton": "Invite to Match",
|
|
189
|
+
"matchDetail_playerCount": "{count, plural, =0{No players} =1{1 player} other{{count} players}}",
|
|
190
|
+
"matchList_emptyState": "No matches available",
|
|
191
|
+
"matchList_title": "Available Matches",
|
|
192
|
+
|
|
193
|
+
// === Settings (설정) ===
|
|
194
|
+
"settings_languageTitle": "Language",
|
|
195
|
+
"settings_systemLanguage": "System Language"
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Common Pitfalls
|
|
200
|
+
|
|
201
|
+
1. **gen-l10n 미실행**: ARB 수정 후 코드 생성 안 하면 IDE 자동완성 미반영
|
|
202
|
+
2. **@@locale 누락**: ARB 파일에 로캘 코드 없으면 파일명에서 유추 (명시 권장)
|
|
203
|
+
3. **ICU 구문 오류**: 중괄호 불일치, other 누락 → gen-l10n 빌드 실패
|
|
204
|
+
4. **텍스트 길이**: 독일어 등 긴 번역 → UI 레이아웃 깨짐 (Expanded/Flexible 사용)
|
|
205
|
+
5. **날짜/숫자 포맷**: ARB placeholder format 미지정 → 로캘별 포맷 미적용
|
|
206
|
+
6. **컨텍스트 없는 description**: 번역가가 맥락 모르면 오역 → 화면 위치/용도 필수
|
|
207
|
+
7. **하드코딩된 문자열**: 로그, 에러 메시지도 ARB로 (사용자 노출 가능성)
|
|
208
|
+
8. **synthetic-package 이해**: `.dart_tool/` 에 생성 → `.gitignore` 에 이미 포함 (커밋 X)
|
|
209
|
+
|
|
210
|
+
## Examples
|
|
211
|
+
|
|
212
|
+
### 최소 구현 체크리스트
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
[ ] pubspec.yaml: flutter_localizations + intl + generate: true
|
|
216
|
+
[ ] l10n.yaml: arb-dir, template-arb-file, output-localization-file
|
|
217
|
+
[ ] lib/l10n/app_en.arb: 기본 키 + @메타데이터
|
|
218
|
+
[ ] lib/l10n/app_ko.arb (등): 번역 ARB
|
|
219
|
+
[ ] MaterialApp: localizationsDelegates (4종) + supportedLocales
|
|
220
|
+
[ ] core/l10n/: LocaleNotifier + AppLocale enum
|
|
221
|
+
[ ] main.dart: SharedPreferences 초기화 + ProviderScope override
|
|
222
|
+
[ ] 설정 화면: 언어 선택 UI
|
|
223
|
+
[ ] CI: 누락 키 검증 스크립트
|
|
224
|
+
[ ] 위젯 테스트: RTL 레이아웃 확인
|
|
225
|
+
```
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ARB File Management
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: "ARB 구조 불일치 → 빌드 실패, 키 누락 → 런타임 null, 네이밍 비일관 → 유지보수 비용 증가"
|
|
5
|
+
tags: arb, app-resource-bundle, placeholder, gen-l10n, naming
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ARB File Management
|
|
9
|
+
|
|
10
|
+
**Impact: CRITICAL (ARB 구조 불일치 → 빌드 실패, 키 누락 → 런타임 null, 네이밍 비일관 → 유지보수 비용 증가)**
|
|
11
|
+
|
|
12
|
+
ARB (Application Resource Bundle) 파일 구조, @placeholder 메타데이터, 네이밍 컨벤션,
|
|
13
|
+
자동 생성 워크플로우. 모든 번역의 원본 소스.
|
|
14
|
+
|
|
15
|
+
### 기본 ARB 파일 구조
|
|
16
|
+
|
|
17
|
+
**Incorrect (메타데이터 없는 평면 구조):**
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"hello": "Hello",
|
|
21
|
+
"welcome": "Welcome, {name}",
|
|
22
|
+
"items": "You have {count} items"
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Correct (메타데이터 포함 구조화된 ARB):**
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"@@locale": "en",
|
|
30
|
+
"@@last_modified": "2026-02-18T10:00:00+09:00",
|
|
31
|
+
|
|
32
|
+
"appTitle": "MyApp",
|
|
33
|
+
"@appTitle": {
|
|
34
|
+
"description": "The application title shown in AppBar"
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
"welcomeMessage": "Welcome, {userName}!",
|
|
38
|
+
"@welcomeMessage": {
|
|
39
|
+
"description": "Greeting shown on home screen after login",
|
|
40
|
+
"placeholders": {
|
|
41
|
+
"userName": {
|
|
42
|
+
"type": "String",
|
|
43
|
+
"example": "Tim"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"matchCount": "{count, plural, =0{No matches} =1{1 match} other{{count} matches}}",
|
|
49
|
+
"@matchCount": {
|
|
50
|
+
"description": "Number of available matches",
|
|
51
|
+
"placeholders": {
|
|
52
|
+
"count": {
|
|
53
|
+
"type": "int",
|
|
54
|
+
"example": "5"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 키 네이밍 컨벤션
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
패턴: featureName_context_element
|
|
65
|
+
|
|
66
|
+
예시:
|
|
67
|
+
✅ matchDetail_inviteButton → 매치 상세 화면의 초대 버튼
|
|
68
|
+
✅ matchDetail_playerCount → 매치 상세의 참가자 수
|
|
69
|
+
✅ settings_languageLabel → 설정 화면의 언어 레이블
|
|
70
|
+
✅ common_cancelButton → 공통 취소 버튼
|
|
71
|
+
✅ error_networkTimeout → 에러 메시지: 네트워크 타임아웃
|
|
72
|
+
✅ validation_emailInvalid → 유효성 검사: 이메일 형식 오류
|
|
73
|
+
|
|
74
|
+
❌ invite → 맥락 없음 (어디서 사용?)
|
|
75
|
+
❌ button1 → 의미 없는 번호
|
|
76
|
+
❌ match_detail_invite_button → snake_case 혼용 (camelCase 사용)
|
|
77
|
+
❌ MATCH_INVITE → 대문자 (camelCase 사용)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 다국어 ARB 파일 관리
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
// lib/l10n/app_en.arb (기본 — template ARB)
|
|
84
|
+
{
|
|
85
|
+
"@@locale": "en",
|
|
86
|
+
"matchDetail_inviteButton": "Invite to Match",
|
|
87
|
+
"@matchDetail_inviteButton": {
|
|
88
|
+
"description": "Button to invite a player to the match"
|
|
89
|
+
},
|
|
90
|
+
"matchDetail_playerCount": "{count, plural, =0{No players} =1{1 player} other{{count} players}}",
|
|
91
|
+
"@matchDetail_playerCount": {
|
|
92
|
+
"description": "Number of players in the match",
|
|
93
|
+
"placeholders": {
|
|
94
|
+
"count": { "type": "int" }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// lib/l10n/app_ko.arb
|
|
100
|
+
{
|
|
101
|
+
"@@locale": "ko",
|
|
102
|
+
"matchDetail_inviteButton": "매치 초대",
|
|
103
|
+
"matchDetail_playerCount": "{count, plural, =0{참가자 없음} =1{1명} other{{count}명}}"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// lib/l10n/app_ms.arb
|
|
107
|
+
{
|
|
108
|
+
"@@locale": "ms",
|
|
109
|
+
"matchDetail_inviteButton": "Jemput ke Perlawanan",
|
|
110
|
+
"matchDetail_playerCount": "{count, plural, =0{Tiada pemain} =1{1 pemain} other{{count} pemain}}"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### @placeholder 메타데이터
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"orderSummary": "Order #{orderId} — {itemCount} items, {totalPrice}",
|
|
119
|
+
"@orderSummary": {
|
|
120
|
+
"description": "Order summary displayed in confirmation screen",
|
|
121
|
+
"placeholders": {
|
|
122
|
+
"orderId": {
|
|
123
|
+
"type": "String",
|
|
124
|
+
"example": "A1234"
|
|
125
|
+
},
|
|
126
|
+
"itemCount": {
|
|
127
|
+
"type": "int",
|
|
128
|
+
"format": "compact",
|
|
129
|
+
"example": "3"
|
|
130
|
+
},
|
|
131
|
+
"totalPrice": {
|
|
132
|
+
"type": "double",
|
|
133
|
+
"format": "currency",
|
|
134
|
+
"optionalParameters": {
|
|
135
|
+
"decimalDigits": 2,
|
|
136
|
+
"symbol": "$"
|
|
137
|
+
},
|
|
138
|
+
"example": "29.99"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
"lastUpdated": "Last updated: {date}",
|
|
144
|
+
"@lastUpdated": {
|
|
145
|
+
"description": "Timestamp for last data refresh",
|
|
146
|
+
"placeholders": {
|
|
147
|
+
"date": {
|
|
148
|
+
"type": "DateTime",
|
|
149
|
+
"format": "yMMMd",
|
|
150
|
+
"example": "Feb 18, 2026"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 자동 생성
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# ARB 파일에서 Dart 코드 생성
|
|
161
|
+
flutter gen-l10n
|
|
162
|
+
|
|
163
|
+
# 생성 결과 (synthetic-package: true 기본):
|
|
164
|
+
# .dart_tool/flutter_gen/gen_l10n/
|
|
165
|
+
# ├── app_localizations.dart # 추상 클래스
|
|
166
|
+
# ├── app_localizations_en.dart # English 구현
|
|
167
|
+
# └── app_localizations_ko.dart # Korean 구현
|
|
168
|
+
|
|
169
|
+
# import 경로:
|
|
170
|
+
# import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 규칙
|
|
174
|
+
|
|
175
|
+
- `app_en.arb` → template ARB (기본), 모든 키와 `@` 메타데이터 포함
|
|
176
|
+
- 번역 ARB (app_ko.arb 등) → 키와 값만 포함, `@` 메타데이터 생략 가능
|
|
177
|
+
- `@@locale` → 각 ARB 파일 최상단에 로캘 코드 명시
|
|
178
|
+
- 키 네이밍 → `featureName_context` camelCase, 공통 키는 `common_` 접두사
|
|
179
|
+
- `@placeholder` → 모든 동적 값에 `type`, `example` 필수, 숫자/날짜는 `format` 추가
|
|
180
|
+
- template ARB의 모든 키 → 번역 ARB에도 존재해야 함 (누락 시 gen-l10n 경고)
|
|
181
|
+
- ARB 파일 변경 후 `flutter gen-l10n` 실행 (빌드 시 자동이지만 IDE 자동완성용)
|
|
182
|
+
- description → 번역가를 위한 맥락 설명, 화면 위치/용도 명시
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Runtime Locale Switching
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: "전환 미지원 → 앱 재시작 필요, 영속화 누락 → 매 시작 시 시스템 로캘로 리셋"
|
|
5
|
+
tags: locale, riverpod, shared-preferences, runtime, system-locale
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Runtime Locale Switching
|
|
9
|
+
|
|
10
|
+
**Impact: MEDIUM (전환 미지원 → 앱 재시작 필요, 영속화 누락 → 매 시작 시 시스템 로캘로 리셋)**
|
|
11
|
+
|
|
12
|
+
런타임 로캘 전환. Riverpod 기반 LocaleNotifier, SharedPreferences 영속화,
|
|
13
|
+
시스템 로캘 감지, 앱 재시작 없이 즉시 반영.
|
|
14
|
+
|
|
15
|
+
### LocaleNotifier (Riverpod)
|
|
16
|
+
|
|
17
|
+
**Incorrect (전역 변수 + setState):**
|
|
18
|
+
```dart
|
|
19
|
+
// 전역 변수로 로캘 관리 → 위젯 트리 재빌드 불완전
|
|
20
|
+
Locale currentLocale = const Locale('en');
|
|
21
|
+
|
|
22
|
+
void changeLocale(Locale locale) {
|
|
23
|
+
currentLocale = locale;
|
|
24
|
+
// setState? → MaterialApp 레벨까지 전파 불가
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Correct (Riverpod StateNotifier + 영속화):**
|
|
29
|
+
```dart
|
|
30
|
+
/// 지원 로캘 목록
|
|
31
|
+
enum AppLocale {
|
|
32
|
+
en(Locale('en'), 'English'),
|
|
33
|
+
ko(Locale('ko'), '한국어'),
|
|
34
|
+
ms(Locale('ms'), 'Bahasa Melayu'),
|
|
35
|
+
id(Locale('id'), 'Bahasa Indonesia');
|
|
36
|
+
|
|
37
|
+
const AppLocale(this.locale, this.displayName);
|
|
38
|
+
final Locale locale;
|
|
39
|
+
final String displayName;
|
|
40
|
+
|
|
41
|
+
static AppLocale fromCode(String code) {
|
|
42
|
+
return AppLocale.values.firstWhere(
|
|
43
|
+
(l) => l.locale.languageCode == code,
|
|
44
|
+
orElse: () => AppLocale.en,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// 로캘 상태 관리 + 영속화
|
|
50
|
+
class LocaleNotifier extends StateNotifier<Locale?> {
|
|
51
|
+
final SharedPreferences _prefs;
|
|
52
|
+
static const _key = 'app_locale';
|
|
53
|
+
|
|
54
|
+
LocaleNotifier(this._prefs) : super(null) {
|
|
55
|
+
_loadSavedLocale();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void _loadSavedLocale() {
|
|
59
|
+
final saved = _prefs.getString(_key);
|
|
60
|
+
if (saved != null) {
|
|
61
|
+
state = AppLocale.fromCode(saved).locale;
|
|
62
|
+
}
|
|
63
|
+
// null → 시스템 로캘 사용 (MaterialApp.locale = null)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// 로캘 변경 (즉시 반영 + 영속화)
|
|
67
|
+
Future<void> setLocale(AppLocale appLocale) async {
|
|
68
|
+
state = appLocale.locale;
|
|
69
|
+
await _prefs.setString(_key, appLocale.locale.languageCode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// 시스템 로캘로 리셋
|
|
73
|
+
Future<void> resetToSystem() async {
|
|
74
|
+
state = null;
|
|
75
|
+
await _prefs.remove(_key);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// 현재 유효 로캘 (상태 또는 시스템)
|
|
79
|
+
Locale effectiveLocale(BuildContext context) {
|
|
80
|
+
return state ?? Localizations.localeOf(context);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Riverpod Providers
|
|
85
|
+
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
|
|
86
|
+
throw UnimplementedError('Override in ProviderScope');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
final localeNotifierProvider =
|
|
90
|
+
StateNotifierProvider<LocaleNotifier, Locale?>((ref) {
|
|
91
|
+
return LocaleNotifier(ref.watch(sharedPreferencesProvider));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
final localeProvider = Provider<Locale?>((ref) {
|
|
95
|
+
return ref.watch(localeNotifierProvider);
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### MaterialApp 연동
|
|
100
|
+
|
|
101
|
+
```dart
|
|
102
|
+
class MyApp extends ConsumerWidget {
|
|
103
|
+
const MyApp({super.key});
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
107
|
+
final locale = ref.watch(localeProvider);
|
|
108
|
+
|
|
109
|
+
return MaterialApp.router(
|
|
110
|
+
locale: locale, // null → 시스템 로캘 사용
|
|
111
|
+
localizationsDelegates: const [
|
|
112
|
+
AppLocalizations.delegate,
|
|
113
|
+
GlobalMaterialLocalizations.delegate,
|
|
114
|
+
GlobalWidgetsLocalizations.delegate,
|
|
115
|
+
GlobalCupertinoLocalizations.delegate,
|
|
116
|
+
],
|
|
117
|
+
supportedLocales: AppLocalizations.supportedLocales,
|
|
118
|
+
// 시스템 로캘과 지원 로캘 매칭 전략
|
|
119
|
+
localeResolutionCallback: (deviceLocale, supportedLocales) {
|
|
120
|
+
for (final supported in supportedLocales) {
|
|
121
|
+
if (supported.languageCode == deviceLocale?.languageCode) {
|
|
122
|
+
return supported;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return supportedLocales.first; // 폴백: 첫 번째 (en)
|
|
126
|
+
},
|
|
127
|
+
routerConfig: ref.watch(routerProvider),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### main.dart 초기화
|
|
134
|
+
|
|
135
|
+
```dart
|
|
136
|
+
Future<void> main() async {
|
|
137
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
138
|
+
|
|
139
|
+
// SharedPreferences 초기화 (앱 시작 시 1회)
|
|
140
|
+
final prefs = await SharedPreferences.getInstance();
|
|
141
|
+
|
|
142
|
+
runApp(
|
|
143
|
+
ProviderScope(
|
|
144
|
+
overrides: [
|
|
145
|
+
sharedPreferencesProvider.overrideWithValue(prefs),
|
|
146
|
+
],
|
|
147
|
+
child: const MyApp(),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 설정 화면 UI
|
|
154
|
+
|
|
155
|
+
```dart
|
|
156
|
+
class LanguageSettingsScreen extends ConsumerWidget {
|
|
157
|
+
const LanguageSettingsScreen({super.key});
|
|
158
|
+
|
|
159
|
+
@override
|
|
160
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
161
|
+
final currentLocale = ref.watch(localeProvider);
|
|
162
|
+
|
|
163
|
+
return Scaffold(
|
|
164
|
+
appBar: AppBar(title: Text(context.l10n.settings_languageTitle)),
|
|
165
|
+
body: ListView(
|
|
166
|
+
children: [
|
|
167
|
+
// 시스템 로캘 옵션
|
|
168
|
+
RadioListTile<Locale?>(
|
|
169
|
+
title: Text(context.l10n.settings_systemLanguage),
|
|
170
|
+
subtitle: Text(_getSystemLocaleName(context)),
|
|
171
|
+
value: null,
|
|
172
|
+
groupValue: currentLocale,
|
|
173
|
+
onChanged: (_) {
|
|
174
|
+
ref.read(localeNotifierProvider.notifier).resetToSystem();
|
|
175
|
+
},
|
|
176
|
+
),
|
|
177
|
+
const Divider(),
|
|
178
|
+
// 지원 로캘 목록
|
|
179
|
+
...AppLocale.values.map((appLocale) {
|
|
180
|
+
return RadioListTile<Locale?>(
|
|
181
|
+
title: Text(appLocale.displayName),
|
|
182
|
+
value: appLocale.locale,
|
|
183
|
+
groupValue: currentLocale,
|
|
184
|
+
onChanged: (_) {
|
|
185
|
+
ref.read(localeNotifierProvider.notifier).setLocale(appLocale);
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
}),
|
|
189
|
+
],
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
String _getSystemLocaleName(BuildContext context) {
|
|
195
|
+
final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
|
|
196
|
+
return systemLocale.languageCode.toUpperCase();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 시스템 로캘 변경 감지
|
|
202
|
+
|
|
203
|
+
```dart
|
|
204
|
+
// WidgetsBindingObserver로 시스템 로캘 변경 감지
|
|
205
|
+
class LocaleObserver extends WidgetsBindingObserver {
|
|
206
|
+
final VoidCallback onLocaleChanged;
|
|
207
|
+
|
|
208
|
+
LocaleObserver({required this.onLocaleChanged});
|
|
209
|
+
|
|
210
|
+
@override
|
|
211
|
+
void didChangeLocales(List<Locale>? locales) {
|
|
212
|
+
// 사용자가 "시스템 로캘 사용" 선택한 경우에만 반응
|
|
213
|
+
onLocaleChanged();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 규칙
|
|
219
|
+
|
|
220
|
+
- `LocaleNotifier` → Riverpod StateNotifier, `Locale?` 상태 (null = 시스템 로캘)
|
|
221
|
+
- `SharedPreferences` → 선택 로캘 영속화, 앱 재시작 시 복원
|
|
222
|
+
- `MaterialApp.locale` → `null` 전달 시 시스템 로캘 자동 사용
|
|
223
|
+
- `localeResolutionCallback` → 지원 로캘과 시스템 로캘 매칭, 미지원 시 폴백
|
|
224
|
+
- 앱 재시작 없이 전환 → `locale` 변경 시 MaterialApp 재빌드 → 즉시 반영
|
|
225
|
+
- "시스템 언어 사용" 옵션 → 사용자에게 항상 제공 (기본값)
|
|
226
|
+
- `didChangeLocales` → 시스템 언어 변경 시 실시간 반영 (시스템 모드일 때만)
|