timsquad 3.3.0 → 3.5.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 +95 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/full.js +1 -0
- package/dist/commands/full.js.map +1 -1
- package/dist/commands/git/pr.js +6 -5
- package/dist/commands/git/pr.js.map +1 -1
- package/dist/commands/git/release.js +2 -7
- package/dist/commands/git/release.js.map +1 -1
- package/dist/commands/improve.js +2 -2
- package/dist/commands/improve.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/log.d.ts.map +1 -1
- package/dist/commands/log.js +2 -2
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +6 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/retro.js +8 -8
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/session.js +3 -3
- package/dist/commands/session.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 +228 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +23 -1
- 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/event-queue.d.ts.map +1 -1
- package/dist/daemon/event-queue.js +2 -2
- package/dist/daemon/event-queue.js.map +1 -1
- package/dist/daemon/index.d.ts +4 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +214 -52
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jsonl-watcher.d.ts +1 -0
- package/dist/daemon/jsonl-watcher.d.ts.map +1 -1
- package/dist/daemon/jsonl-watcher.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-notes.d.ts +33 -0
- package/dist/daemon/session-notes.d.ts.map +1 -0
- package/dist/daemon/session-notes.js +74 -0
- package/dist/daemon/session-notes.js.map +1 -0
- package/dist/daemon/session-state.d.ts +27 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +165 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +9 -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/agent-generator.d.ts +4 -0
- package/dist/lib/agent-generator.d.ts.map +1 -1
- package/dist/lib/agent-generator.js +52 -3
- package/dist/lib/agent-generator.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/project.d.ts.map +1 -1
- package/dist/lib/project.js +8 -3
- package/dist/lib/project.js.map +1 -1
- package/dist/lib/skill-generator.d.ts.map +1 -1
- package/dist/lib/skill-generator.js +22 -1
- package/dist/lib/skill-generator.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 +4 -4
- package/templates/base/agents/base/tsq-architect.md +2 -2
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/knowledge/checklists/plan-quality.md +31 -0
- package/templates/base/knowledge/checklists/stability-verification.md +14 -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/base/skills/stability-verification/SKILL.md +64 -0
- package/templates/base/skills/stability-verification/references/release-checklist.md +34 -0
- package/templates/base/skills/stability-verification/references/security-fix-patterns.md +112 -0
- package/templates/base/skills/stability-verification/rules/verification-layers.md +67 -0
- package/templates/base/skills/stability-verification/rules/verification-workflow.md +69 -0
- package/templates/base/skills/stability-verification/scripts/verify.sh +294 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/rules/build-gate.md +28 -0
- package/templates/platforms/claude-code/rules/completion-verification.md +30 -0
- package/templates/platforms/claude-code/rules/context-monitor.md +23 -0
- package/templates/platforms/claude-code/rules/plan-review.md +45 -0
- package/templates/platforms/claude-code/rules/quality-guards.md +43 -0
- package/templates/platforms/claude-code/rules/session-notes.md +18 -0
- package/templates/platforms/claude-code/rules/skill-suggest.md +27 -0
- package/templates/platforms/claude-code/scripts/build-gate.sh +73 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +93 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/scripts/safe-guard.sh +83 -0
- package/templates/platforms/claude-code/scripts/skill-rules.json +85 -0
- package/templates/platforms/claude-code/scripts/skill-suggest.sh +105 -0
- package/templates/platforms/claude-code/settings.json +111 -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,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` → 시스템 언어 변경 시 실시간 반영 (시스템 모드일 때만)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Localization Setup (flutter_localizations + intl)
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: "설정 누락 → 번역 미적용, 런타임 에러, 날짜/숫자 포맷 불일치"
|
|
5
|
+
tags: flutter-localizations, intl, l10n-yaml, pubspec, setup
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Localization Setup (flutter_localizations + intl)
|
|
9
|
+
|
|
10
|
+
**Impact: CRITICAL (설정 누락 → 번역 미적용, 런타임 에러, 날짜/숫자 포맷 불일치)**
|
|
11
|
+
|
|
12
|
+
flutter_localizations + intl 패키지 설정, pubspec.yaml generate 플래그, l10n.yaml 구성,
|
|
13
|
+
MaterialApp delegate 등록. 프로젝트 초기에 반드시 완료해야 하는 i18n 인프라.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
flutter:
|
|
21
|
+
sdk: flutter
|
|
22
|
+
flutter_localizations:
|
|
23
|
+
sdk: flutter
|
|
24
|
+
intl: any # flutter_localizations가 버전 관리
|
|
25
|
+
|
|
26
|
+
# 코드 생성 활성화 (필수!)
|
|
27
|
+
flutter:
|
|
28
|
+
generate: true
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### l10n.yaml 설정
|
|
32
|
+
|
|
33
|
+
**Incorrect (l10n.yaml 없이 수동 관리):**
|
|
34
|
+
```dart
|
|
35
|
+
// ARB 파일을 직접 파싱하거나 하드코딩된 Map 사용
|
|
36
|
+
// → 타입 안전성 없음, 키 오타 런타임까지 발견 불가
|
|
37
|
+
final Map<String, String> translations = {
|
|
38
|
+
'hello': 'Hello',
|
|
39
|
+
'welcome': 'Welcome, $name',
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Correct (l10n.yaml로 자동 코드 생성):**
|
|
44
|
+
```yaml
|
|
45
|
+
# l10n.yaml (프로젝트 루트)
|
|
46
|
+
arb-dir: lib/l10n
|
|
47
|
+
template-arb-file: app_en.arb
|
|
48
|
+
output-localization-file: app_localizations.dart
|
|
49
|
+
output-class: AppLocalizations
|
|
50
|
+
# nullable-getter: false # null 안전 접근 (선택, 기본 true)
|
|
51
|
+
# synthetic-package: true # .dart_tool/에 생성 (기본 true)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### MaterialApp 설정
|
|
55
|
+
|
|
56
|
+
**Incorrect (delegate 누락):**
|
|
57
|
+
```dart
|
|
58
|
+
MaterialApp(
|
|
59
|
+
// localizationsDelegates 없음 → AppLocalizations.of(context) 실패
|
|
60
|
+
// supportedLocales 없음 → 시스템 로캘 무시
|
|
61
|
+
home: const HomeScreen(),
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Correct (완전한 delegate 등록):**
|
|
66
|
+
```dart
|
|
67
|
+
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
68
|
+
|
|
69
|
+
class MyApp extends ConsumerWidget {
|
|
70
|
+
const MyApp({super.key});
|
|
71
|
+
|
|
72
|
+
@override
|
|
73
|
+
Widget build(BuildContext context, WidgetRef ref) {
|
|
74
|
+
final locale = ref.watch(localeProvider);
|
|
75
|
+
|
|
76
|
+
return MaterialApp.router(
|
|
77
|
+
// 1. Localization Delegates (3종 필수)
|
|
78
|
+
localizationsDelegates: const [
|
|
79
|
+
AppLocalizations.delegate, // 앱 번역
|
|
80
|
+
GlobalMaterialLocalizations.delegate, // Material 위젯 번역
|
|
81
|
+
GlobalWidgetsLocalizations.delegate, // 기본 위젯 방향성
|
|
82
|
+
GlobalCupertinoLocalizations.delegate, // Cupertino 위젯 번역
|
|
83
|
+
],
|
|
84
|
+
// 2. 지원 로캘 목록
|
|
85
|
+
supportedLocales: AppLocalizations.supportedLocales,
|
|
86
|
+
// 3. 현재 로캘 (null → 시스템 로캘 사용)
|
|
87
|
+
locale: locale,
|
|
88
|
+
routerConfig: ref.watch(routerProvider),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 번역 사용
|
|
95
|
+
|
|
96
|
+
```dart
|
|
97
|
+
// 위젯에서 번역 접근
|
|
98
|
+
class WelcomeScreen extends StatelessWidget {
|
|
99
|
+
const WelcomeScreen({super.key});
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
Widget build(BuildContext context) {
|
|
103
|
+
// AppLocalizations.of(context) → nullable (로캘 미지원 시 null)
|
|
104
|
+
final l10n = AppLocalizations.of(context)!;
|
|
105
|
+
// 또는 extension 사용:
|
|
106
|
+
// final l10n = context.l10n;
|
|
107
|
+
|
|
108
|
+
return Text(l10n.welcomeMessage('Tim'));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 편의 Extension (선택)
|
|
113
|
+
extension BuildContextL10n on BuildContext {
|
|
114
|
+
AppLocalizations get l10n => AppLocalizations.of(this)!;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 코드 생성 실행
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# ARB 파일 추가/수정 후 코드 생성
|
|
122
|
+
flutter gen-l10n
|
|
123
|
+
|
|
124
|
+
# 또는 빌드 시 자동 생성 (generate: true 설정 시)
|
|
125
|
+
flutter run # 자동으로 gen-l10n 실행
|
|
126
|
+
flutter build # 마찬가지
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 규칙
|
|
130
|
+
|
|
131
|
+
- `pubspec.yaml` → `flutter: generate: true` 필수 (자동 코드 생성 활성화)
|
|
132
|
+
- `l10n.yaml` → 프로젝트 루트에 반드시 생성, `arb-dir` + `template-arb-file` 지정
|
|
133
|
+
- `localizationsDelegates` → 4개 등록 (App + Material + Widgets + Cupertino)
|
|
134
|
+
- `supportedLocales` → `AppLocalizations.supportedLocales` 사용 (ARB 파일에서 자동 유도)
|
|
135
|
+
- 하드코딩된 문자열 Map 사용 금지 → ARB + 코드 생성으로 타입 안전 접근
|
|
136
|
+
- `context.l10n` Extension → 반복적인 `AppLocalizations.of(context)!` 줄이기
|
|
137
|
+
- `flutter gen-l10n` → ARB 파일 변경 후 반드시 실행 (빌드 시 자동이지만 IDE 지원용)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Plural & Gender (ICU MessageFormat)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: "복수형 미처리 → '1 items' 문법 오류, 성별 미처리 → 포용성 저하"
|
|
5
|
+
tags: icu, plural, gender, select, ordinal, message-format
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Plural & Gender (ICU MessageFormat)
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (복수형 미처리 → '1 items' 문법 오류, 성별 미처리 → 포용성 저하)**
|
|
11
|
+
|
|
12
|
+
ICU MessageFormat 기반 복수형, 성별, 서수 처리. ARB 파일에 직접 ICU 구문 작성,
|
|
13
|
+
intl 패키지가 자동 파싱. 언어별 복수형 규칙 차이를 올바르게 처리.
|
|
14
|
+
|
|
15
|
+
### 복수형 (Plural)
|
|
16
|
+
|
|
17
|
+
**Incorrect (조건문으로 복수형 처리):**
|
|
18
|
+
```dart
|
|
19
|
+
// 코드에서 직접 분기 → 번역 불가, 언어별 규칙 대응 불가
|
|
20
|
+
String getItemText(int count) {
|
|
21
|
+
if (count == 0) return 'No items';
|
|
22
|
+
if (count == 1) return '1 item';
|
|
23
|
+
return '$count items';
|
|
24
|
+
}
|
|
25
|
+
// → 러시아어는 1, 2-4, 5-20, 21 등 복수형 규칙이 다름
|
|
26
|
+
// → 아랍어는 6가지 복수형 (zero, one, two, few, many, other)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Correct (ARB 파일에 ICU plural 구문):**
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"matchCount": "{count, plural, =0{No matches} =1{1 match} other{{count} matches}}",
|
|
33
|
+
"@matchCount": {
|
|
34
|
+
"description": "Number of available matches on the board",
|
|
35
|
+
"placeholders": {
|
|
36
|
+
"count": {
|
|
37
|
+
"type": "int",
|
|
38
|
+
"example": "5"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 복수형 카테고리
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
ICU 복수형 카테고리 (언어별로 사용하는 카테고리가 다름):
|
|
49
|
+
|
|
50
|
+
=0 → 정확히 0 (선택, 없으면 other 사용)
|
|
51
|
+
=1 → 정확히 1 (선택, 없으면 one 사용)
|
|
52
|
+
=2 → 정확히 2 (선택)
|
|
53
|
+
zero → 0 범주 (아랍어 등)
|
|
54
|
+
one → 1 범주 (대부분 언어의 단수)
|
|
55
|
+
two → 2 범주 (아랍어, 웨일스어)
|
|
56
|
+
few → 소수 범주 (슬라브어: 2-4, 아랍어: 3-10)
|
|
57
|
+
many → 다수 범주 (슬라브어: 5+, 아랍어: 11-99)
|
|
58
|
+
other → 기본 (필수! 모든 언어에서 폴백)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 언어별 복수형 예시
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
// English (en) — one, other
|
|
65
|
+
{
|
|
66
|
+
"messageCount": "{count, plural, =0{No messages} one{1 message} other{{count} messages}}"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Korean (ko) — other만 사용 (복수형 구분 없음)
|
|
70
|
+
{
|
|
71
|
+
"messageCount": "{count, plural, =0{메시지 없음} other{메시지 {count}개}}"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Russian (ru) — one, few, many, other
|
|
75
|
+
{
|
|
76
|
+
"messageCount": "{count, plural, =0{Нет сообщений} one{{count} сообщение} few{{count} сообщения} many{{count} сообщений} other{{count} сообщений}}"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Arabic (ar) — zero, one, two, few, many, other
|
|
80
|
+
{
|
|
81
|
+
"messageCount": "{count, plural, zero{لا رسائل} one{رسالة واحدة} two{رسالتان} few{{count} رسائل} many{{count} رسالة} other{{count} رسالة}}"
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 성별 (Select)
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"profileGreeting": "{gender, select, male{He joined} female{She joined} other{They joined}} the match",
|
|
90
|
+
"@profileGreeting": {
|
|
91
|
+
"description": "Greeting text when a player joins a match",
|
|
92
|
+
"placeholders": {
|
|
93
|
+
"gender": {
|
|
94
|
+
"type": "String",
|
|
95
|
+
"example": "male"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Korean (성별 구분 불필요한 언어)
|
|
102
|
+
{
|
|
103
|
+
"profileGreeting": "{gender, select, male{매치에 참가했습니다} female{매치에 참가했습니다} other{매치에 참가했습니다}}"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 복합 (Plural + 다른 변수)
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"matchInvite": "{userName} invited you to {count, plural, =1{a match} other{{count} matches}}",
|
|
112
|
+
"@matchInvite": {
|
|
113
|
+
"description": "Match invitation notification text",
|
|
114
|
+
"placeholders": {
|
|
115
|
+
"userName": { "type": "String", "example": "Tim" },
|
|
116
|
+
"count": { "type": "int", "example": "2" }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 서수 (Ordinal — 선택)
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"rankPosition": "You are {position, select, 1{1st} 2{2nd} 3{3rd} other{{position}th}} place",
|
|
127
|
+
"@rankPosition": {
|
|
128
|
+
"description": "User ranking position in leaderboard",
|
|
129
|
+
"placeholders": {
|
|
130
|
+
"position": { "type": "int", "example": "3" }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Dart 코드에서 사용
|
|
137
|
+
|
|
138
|
+
```dart
|
|
139
|
+
// 자동 생성된 AppLocalizations 메서드 사용
|
|
140
|
+
Text(context.l10n.matchCount(0)); // "No matches"
|
|
141
|
+
Text(context.l10n.matchCount(1)); // "1 match"
|
|
142
|
+
Text(context.l10n.matchCount(42)); // "42 matches"
|
|
143
|
+
|
|
144
|
+
Text(context.l10n.profileGreeting('female')); // "She joined the match"
|
|
145
|
+
Text(context.l10n.profileGreeting('other')); // "They joined the match"
|
|
146
|
+
|
|
147
|
+
Text(context.l10n.matchInvite('Tim', 3)); // "Tim invited you to 3 matches"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 규칙
|
|
151
|
+
|
|
152
|
+
- 복수형 → ARB 파일에 ICU `{count, plural, ...}` 구문 사용, 코드 분기 금지
|
|
153
|
+
- `other` 카테고리 → 필수 (모든 복수형/select에서 폴백)
|
|
154
|
+
- 언어별 복수형 규칙 → 번역가에게 언어 규칙 안내 (CLDR 참조)
|
|
155
|
+
- 성별 → `{gender, select, male{...} female{...} other{...}}` 사용
|
|
156
|
+
- `other` in select → 성 중립 표현 (포용성)
|
|
157
|
+
- 복합 구문 → plural + 변수 결합 가능, 중첩은 가독성 위해 최소화
|
|
158
|
+
- placeholder type → `int` (plural), `String` (select) 정확히 지정
|
|
159
|
+
- 한국어/일본어 등 → 복수형 구분 불필요해도 `other` 필수 (ICU 규격)
|