@buivietphi/skill-mobile-mt 2.0.1 → 2.2.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/AGENTS.md +96 -40
- package/README.md +77 -40
- package/SKILL.md +762 -54
- package/package.json +1 -1
- package/shared/bug-detection.md +411 -27
- package/shared/code-generation-templates.md +656 -0
- package/shared/code-review.md +899 -37
- package/shared/complex-ui-patterns.md +526 -0
- package/shared/data-flow-patterns.md +422 -0
- package/shared/debugging-intelligence.md +787 -0
- package/shared/error-handling.md +394 -0
- package/shared/i18n-localization.md +426 -0
- package/shared/intent-analysis.md +473 -0
- package/shared/navigation-patterns.md +375 -0
- package/shared/prompt-engineering.md +176 -20
- package/shared/spec-to-code.md +293 -0
- package/shared/storage-patterns.md +312 -0
- package/shared/testing-patterns.md +428 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# Mobile i18n & Localization
|
|
2
|
+
|
|
3
|
+
> Multi-language support for React Native, Flutter, iOS, Android.
|
|
4
|
+
> Covers: setup, translation management, RTL, date/number formatting, dynamic locale switching.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Decision Matrix — Pick Your Stack
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
PLATFORM → RECOMMENDED LIBRARY
|
|
12
|
+
──────────────────────────────────────────────────────────────────
|
|
13
|
+
React Native → i18next + react-i18next (most popular, flexible)
|
|
14
|
+
OR expo-localization + custom (Expo projects)
|
|
15
|
+
|
|
16
|
+
Flutter → slang (type-safe, code generation) ← recommended
|
|
17
|
+
OR flutter_localizations + .arb files (official)
|
|
18
|
+
OR easy_localization (simple)
|
|
19
|
+
|
|
20
|
+
iOS Native → .xcstrings / Localizable.strings (built-in Xcode)
|
|
21
|
+
Android Native → strings.xml per locale (built-in Android Studio)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## React Native — i18next
|
|
27
|
+
|
|
28
|
+
### Setup
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install i18next react-i18next
|
|
32
|
+
npm install @react-native-async-storage/async-storage # for locale persistence
|
|
33
|
+
# or with expo:
|
|
34
|
+
npx expo install expo-localization
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### File Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
src/
|
|
41
|
+
├── i18n/
|
|
42
|
+
│ ├── index.ts ← i18next init
|
|
43
|
+
│ ├── en.json ← English (base language)
|
|
44
|
+
│ ├── vi.json ← Vietnamese
|
|
45
|
+
│ ├── ja.json ← Japanese
|
|
46
|
+
│ ├── ar.json ← Arabic (RTL)
|
|
47
|
+
│ └── types.ts ← TypeScript type for translation keys
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Translation Files
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
// i18n/en.json
|
|
54
|
+
{
|
|
55
|
+
"common": {
|
|
56
|
+
"ok": "OK",
|
|
57
|
+
"cancel": "Cancel",
|
|
58
|
+
"loading": "Loading...",
|
|
59
|
+
"error": "Something went wrong",
|
|
60
|
+
"retry": "Try again",
|
|
61
|
+
"empty": "No results found"
|
|
62
|
+
},
|
|
63
|
+
"auth": {
|
|
64
|
+
"login": "Login",
|
|
65
|
+
"logout": "Logout",
|
|
66
|
+
"email": "Email address",
|
|
67
|
+
"password": "Password",
|
|
68
|
+
"forgotPassword": "Forgot password?",
|
|
69
|
+
"errors": {
|
|
70
|
+
"invalidEmail": "Please enter a valid email",
|
|
71
|
+
"wrongPassword": "Incorrect password"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"profile": {
|
|
75
|
+
"title": "My Profile",
|
|
76
|
+
"greeting": "Hello, {{name}}!",
|
|
77
|
+
"itemCount": "{{count}} item",
|
|
78
|
+
"itemCount_other": "{{count}} items"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
// i18n/vi.json
|
|
85
|
+
{
|
|
86
|
+
"common": {
|
|
87
|
+
"ok": "Đồng ý",
|
|
88
|
+
"cancel": "Hủy",
|
|
89
|
+
"loading": "Đang tải...",
|
|
90
|
+
"error": "Có lỗi xảy ra",
|
|
91
|
+
"retry": "Thử lại",
|
|
92
|
+
"empty": "Không có kết quả"
|
|
93
|
+
},
|
|
94
|
+
"auth": {
|
|
95
|
+
"login": "Đăng nhập",
|
|
96
|
+
"logout": "Đăng xuất",
|
|
97
|
+
"email": "Địa chỉ email",
|
|
98
|
+
"password": "Mật khẩu",
|
|
99
|
+
"forgotPassword": "Quên mật khẩu?",
|
|
100
|
+
"errors": {
|
|
101
|
+
"invalidEmail": "Vui lòng nhập email hợp lệ",
|
|
102
|
+
"wrongPassword": "Mật khẩu không đúng"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"profile": {
|
|
106
|
+
"title": "Hồ sơ của tôi",
|
|
107
|
+
"greeting": "Xin chào, {{name}}!",
|
|
108
|
+
"itemCount": "{{count}} mục"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### i18next Init
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// i18n/index.ts
|
|
117
|
+
import i18n from 'i18next';
|
|
118
|
+
import { initReactI18next } from 'react-i18next';
|
|
119
|
+
import * as Localization from 'expo-localization';
|
|
120
|
+
import MMKV from '../storage'; // your MMKV wrapper
|
|
121
|
+
|
|
122
|
+
import en from './en.json';
|
|
123
|
+
import vi from './vi.json';
|
|
124
|
+
import ar from './ar.json';
|
|
125
|
+
|
|
126
|
+
const LANGUAGE_KEY = 'user_language';
|
|
127
|
+
|
|
128
|
+
export const SUPPORTED_LANGUAGES = ['en', 'vi', 'ja', 'ar'] as const;
|
|
129
|
+
export type Language = typeof SUPPORTED_LANGUAGES[number];
|
|
130
|
+
|
|
131
|
+
// Get saved or device language
|
|
132
|
+
const savedLang = MMKV.getString(LANGUAGE_KEY);
|
|
133
|
+
const deviceLang = Localization.getLocales()[0]?.languageCode ?? 'en';
|
|
134
|
+
const initialLang = savedLang ?? (SUPPORTED_LANGUAGES.includes(deviceLang as Language) ? deviceLang : 'en');
|
|
135
|
+
|
|
136
|
+
i18n.use(initReactI18next).init({
|
|
137
|
+
resources: { en: { translation: en }, vi: { translation: vi }, ar: { translation: ar } },
|
|
138
|
+
lng: initialLang,
|
|
139
|
+
fallbackLng: 'en',
|
|
140
|
+
interpolation: { escapeValue: false },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export function changeLanguage(lang: Language) {
|
|
144
|
+
i18n.changeLanguage(lang);
|
|
145
|
+
MMKV.setString(LANGUAGE_KEY, lang); // persist choice
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export default i18n;
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Usage in Components
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { useTranslation } from 'react-i18next';
|
|
155
|
+
import { I18nManager } from 'react-native';
|
|
156
|
+
|
|
157
|
+
export function ProfileScreen() {
|
|
158
|
+
const { t, i18n } = useTranslation();
|
|
159
|
+
const isRTL = I18nManager.isRTL;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<View style={[styles.container, isRTL && styles.rtl]}>
|
|
163
|
+
<Text>{t('profile.title')}</Text>
|
|
164
|
+
<Text>{t('profile.greeting', { name: 'Phi' })}</Text>
|
|
165
|
+
<Text>{t('profile.itemCount', { count: 5 })}</Text>
|
|
166
|
+
|
|
167
|
+
{/* Language switcher */}
|
|
168
|
+
<Button title="Tiếng Việt" onPress={() => changeLanguage('vi')} />
|
|
169
|
+
<Button title="English" onPress={() => changeLanguage('en')} />
|
|
170
|
+
</View>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// TypeScript type-safe keys (optional but recommended)
|
|
175
|
+
// Generate from en.json with i18next-resources-for-ts
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### RTL Support (Arabic, Hebrew)
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// App.tsx — apply RTL on init
|
|
182
|
+
import { I18nManager } from 'react-native';
|
|
183
|
+
import * as Updates from 'expo-updates';
|
|
184
|
+
|
|
185
|
+
function applyRTL(language: string) {
|
|
186
|
+
const isRTL = language === 'ar' || language === 'he';
|
|
187
|
+
|
|
188
|
+
if (I18nManager.isRTL !== isRTL) {
|
|
189
|
+
I18nManager.allowRTL(isRTL);
|
|
190
|
+
I18nManager.forceRTL(isRTL);
|
|
191
|
+
// Requires reload to take effect
|
|
192
|
+
Updates.reloadAsync(); // Expo
|
|
193
|
+
// RNRestart.Restart(); // react-native-restart (bare RN)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// RTL-aware styles
|
|
198
|
+
const styles = StyleSheet.create({
|
|
199
|
+
row: {
|
|
200
|
+
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
|
|
201
|
+
},
|
|
202
|
+
text: {
|
|
203
|
+
textAlign: I18nManager.isRTL ? 'right' : 'left',
|
|
204
|
+
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Date / Number Formatting
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Use Intl API (built into Hermes/V8)
|
|
213
|
+
const locale = i18n.language;
|
|
214
|
+
|
|
215
|
+
// Date
|
|
216
|
+
const formatDate = (date: Date) =>
|
|
217
|
+
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeStyle: 'short' }).format(date);
|
|
218
|
+
|
|
219
|
+
// Number / Currency
|
|
220
|
+
const formatCurrency = (amount: number, currency = 'USD') =>
|
|
221
|
+
new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
|
|
222
|
+
|
|
223
|
+
// Relative time (2 days ago, in 3 hours)
|
|
224
|
+
const formatRelative = (date: Date) =>
|
|
225
|
+
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(
|
|
226
|
+
Math.round((date.getTime() - Date.now()) / 86400000), 'day'
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Usage
|
|
230
|
+
formatDate(new Date()); // "Jan 15, 2025, 2:30 PM"
|
|
231
|
+
formatCurrency(99.99); // "$99.99" / "99,99 €"
|
|
232
|
+
formatRelative(yesterday); // "yesterday" / "hôm qua"
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Flutter — slang (Recommended)
|
|
238
|
+
|
|
239
|
+
### Setup
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
# pubspec.yaml
|
|
243
|
+
dependencies:
|
|
244
|
+
flutter_localizations:
|
|
245
|
+
sdk: flutter
|
|
246
|
+
slang: ^4.0.0
|
|
247
|
+
slang_flutter: ^4.0.0
|
|
248
|
+
|
|
249
|
+
dev_dependencies:
|
|
250
|
+
slang_build_runner: ^4.0.0
|
|
251
|
+
build_runner: ^2.0.0
|
|
252
|
+
|
|
253
|
+
flutter:
|
|
254
|
+
generate: true
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Translation Files
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
// assets/i18n/en.i18n.json
|
|
261
|
+
{
|
|
262
|
+
"common": {
|
|
263
|
+
"ok": "OK",
|
|
264
|
+
"cancel": "Cancel",
|
|
265
|
+
"loading": "Loading..."
|
|
266
|
+
},
|
|
267
|
+
"auth": {
|
|
268
|
+
"login": "Login",
|
|
269
|
+
"greeting": "Hello, $name!",
|
|
270
|
+
"itemCount(n)": {
|
|
271
|
+
"one": "$n item",
|
|
272
|
+
"other": "$n items"
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
// assets/i18n/vi.i18n.json
|
|
280
|
+
{
|
|
281
|
+
"common": {
|
|
282
|
+
"ok": "Đồng ý",
|
|
283
|
+
"cancel": "Hủy",
|
|
284
|
+
"loading": "Đang tải..."
|
|
285
|
+
},
|
|
286
|
+
"auth": {
|
|
287
|
+
"login": "Đăng nhập",
|
|
288
|
+
"greeting": "Xin chào, $name!",
|
|
289
|
+
"itemCount(n)": "$n mục"
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Generate + Use
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
dart run build_runner build
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```dart
|
|
301
|
+
// main.dart
|
|
302
|
+
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
303
|
+
import 'i18n/strings.g.dart'; // generated
|
|
304
|
+
|
|
305
|
+
void main() {
|
|
306
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
307
|
+
LocaleSettings.useDeviceLocale(); // or .setLocale(AppLocale.vi)
|
|
308
|
+
runApp(TranslationProvider(child: MyApp()));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class MyApp extends StatelessWidget {
|
|
312
|
+
Widget build(BuildContext context) => MaterialApp(
|
|
313
|
+
locale: TranslationProvider.of(context).flutterLocale,
|
|
314
|
+
supportedLocales: AppLocaleUtils.supportedLocales,
|
|
315
|
+
localizationsDelegates: GlobalMaterialLocalizations.delegates,
|
|
316
|
+
home: HomeScreen(),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Usage — fully type-safe, autocomplete
|
|
321
|
+
final t = Translations.of(context);
|
|
322
|
+
|
|
323
|
+
Text(t.common.ok)
|
|
324
|
+
Text(t.auth.greeting(name: 'Phi'))
|
|
325
|
+
Text(t.auth.itemCount(n: 5))
|
|
326
|
+
|
|
327
|
+
// Change language
|
|
328
|
+
LocaleSettings.setLocale(AppLocale.vi);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## iOS Native (Xcode .xcstrings)
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
Localizable.xcstrings (modern, Xcode 15+):
|
|
337
|
+
- Edit in Xcode string catalog UI
|
|
338
|
+
- Automatic plural rules per language
|
|
339
|
+
- Supports all Apple platforms
|
|
340
|
+
|
|
341
|
+
File structure:
|
|
342
|
+
App/
|
|
343
|
+
├── en.lproj/Localizable.xcstrings ← base language
|
|
344
|
+
├── vi.lproj/Localizable.xcstrings
|
|
345
|
+
└── ar.lproj/Localizable.xcstrings
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
```swift
|
|
349
|
+
// Usage
|
|
350
|
+
Text(String(localized: "auth.login")) // SwiftUI (auto-localizes)
|
|
351
|
+
label.text = NSLocalizedString("auth.login", comment: "") // UIKit
|
|
352
|
+
|
|
353
|
+
// With interpolation
|
|
354
|
+
Text("profile.greeting \(userName)")
|
|
355
|
+
// Localizable.xcstrings: "profile.greeting %@" → "Xin chào, %@!"
|
|
356
|
+
|
|
357
|
+
// Date/Number formatting
|
|
358
|
+
let formatted = Date().formatted(.dateTime.month(.wide).day().year())
|
|
359
|
+
let price = amount.formatted(.currency(code: "VND").locale(Locale(identifier: "vi_VN")))
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Android Native (strings.xml)
|
|
365
|
+
|
|
366
|
+
```xml
|
|
367
|
+
<!-- res/values/strings.xml (English, default) -->
|
|
368
|
+
<resources>
|
|
369
|
+
<string name="common_ok">OK</string>
|
|
370
|
+
<string name="auth_login">Login</string>
|
|
371
|
+
<string name="profile_greeting">Hello, %s!</string>
|
|
372
|
+
<plurals name="profile_item_count">
|
|
373
|
+
<item quantity="one">%d item</item>
|
|
374
|
+
<item quantity="other">%d items</item>
|
|
375
|
+
</plurals>
|
|
376
|
+
</resources>
|
|
377
|
+
|
|
378
|
+
<!-- res/values-vi/strings.xml (Vietnamese) -->
|
|
379
|
+
<resources>
|
|
380
|
+
<string name="common_ok">Đồng ý</string>
|
|
381
|
+
<string name="auth_login">Đăng nhập</string>
|
|
382
|
+
<string name="profile_greeting">Xin chào, %s!</string>
|
|
383
|
+
<plurals name="profile_item_count">
|
|
384
|
+
<item quantity="other">%d mục</item>
|
|
385
|
+
</plurals>
|
|
386
|
+
</resources>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```kotlin
|
|
390
|
+
// Usage
|
|
391
|
+
getString(R.string.auth_login)
|
|
392
|
+
getString(R.string.profile_greeting, "Phi")
|
|
393
|
+
resources.getQuantityString(R.plurals.profile_item_count, 5, 5)
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## i18n Checklist
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
SETUP:
|
|
402
|
+
□ Translation keys are namespaced (auth.login, not just "login")
|
|
403
|
+
□ Base language file has ALL keys
|
|
404
|
+
□ Missing key fallback configured (falls back to English)
|
|
405
|
+
□ Language preference persisted on device (MMKV/SharedPreferences)
|
|
406
|
+
□ Language detected from device locale on first launch
|
|
407
|
+
|
|
408
|
+
CONTENT:
|
|
409
|
+
□ No hardcoded strings in components (all through t() / getString)
|
|
410
|
+
□ Plurals handled correctly (1 item vs 2 items)
|
|
411
|
+
□ Interpolation used for dynamic values (name, count)
|
|
412
|
+
□ Date/number/currency formatted per locale (Intl API)
|
|
413
|
+
|
|
414
|
+
RTL (if supporting Arabic/Hebrew):
|
|
415
|
+
□ I18nManager.forceRTL() applied and app reloaded
|
|
416
|
+
□ flexDirection mirrors on RTL
|
|
417
|
+
□ textAlign mirrors on RTL
|
|
418
|
+
□ Icons and chevrons mirror on RTL
|
|
419
|
+
□ Back navigation arrow mirrors on RTL
|
|
420
|
+
|
|
421
|
+
TESTING:
|
|
422
|
+
□ Test with locale set to each supported language
|
|
423
|
+
□ Test with system language set to unsupported → falls back to English
|
|
424
|
+
□ Test RTL layout on Arabic device or simulator
|
|
425
|
+
□ Test long strings (German/Finnish) don't break UI layout
|
|
426
|
+
```
|