@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.
@@ -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
+ ```