docs: document i18n implementation with external JSON resource files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -63,6 +63,15 @@ assets/
|
||||
|
||||
API base URL: `http://localhost:8090/api/v1/`
|
||||
|
||||
## Internationalization (i18n)
|
||||
- **Translation files**: `assets/i18n/en.json` (default/fallback) and `assets/i18n/cs.json` — flat key-value JSON with `_meta` block
|
||||
- **Service**: `lib/core/i18n/translation_service.dart` — singleton `TranslationService`, loads EN first as fallback then overlays current locale
|
||||
- **Providers**: `lib/core/i18n/i18n_provider.dart` — `localeProvider` (persisted via SharedPreferences), `tProvider` (shortcut to `t()`)
|
||||
- **Usage**: `final t = ref.watch(tProvider); t('key', params: {'name': 'value'})` — named params support different word order per language
|
||||
- **Language picker**: `lib/shared/widgets/language_picker.dart` — flag buttons (🇬🇧/🇨🇿), available in onboarding and profile screen
|
||||
- **Validator**: `lib/core/i18n/i18n_validator.dart` — used in tests; checks key completeness, param parity, malformed placeholders
|
||||
- **Default locale**: `'cs'` — loaded from SharedPreferences on startup
|
||||
|
||||
## Widget API Patterns
|
||||
- **Dual-param widgets**: `shared/widgets/` accept both entity param (e.g. `task: TaskEntity?`) AND individual raw params — private getters resolve which to use. Keep backward compat when extending.
|
||||
- **`PetType` canonical location**: `domain/entities/pet_type.dart` — never redeclare locally.
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# Internationalization (i18n) — External JSON Resource Files
|
||||
|
||||
## Overview
|
||||
|
||||
Added full i18n support to KittieMobile using external JSON translation files stored in `assets/i18n/`. All user-facing strings are now decoupled from Dart code. Supported locales: **English (en)** and **Czech (cs)**. Default locale is `cs`.
|
||||
|
||||
## Files Added / Modified
|
||||
|
||||
### New files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `assets/i18n/en.json` | English translations (111 keys, default/fallback) |
|
||||
| `assets/i18n/cs.json` | Czech translations (111 keys) |
|
||||
| `assets/i18n/README.md` | Translator guide (format, rules, how to add new language) |
|
||||
| `lib/core/i18n/translation_service.dart` | Singleton service: load, fallback, param substitution |
|
||||
| `lib/core/i18n/i18n_provider.dart` | Riverpod providers: `localeProvider`, `tProvider`, `translationServiceProvider` |
|
||||
| `lib/core/i18n/i18n_validator.dart` | Validation utility for tests and CI |
|
||||
| `lib/shared/widgets/language_picker.dart` | Flag-based EN/CS switcher widget |
|
||||
| `lib/shared/utils/czech_date.dart` | `localizedDateHeader()` and `localizedGreeting()` helpers |
|
||||
| `test/i18n_test.dart` | 18 tests covering JSON validity, key completeness, param substitution |
|
||||
|
||||
### Modified files
|
||||
- `pubspec.yaml` — added `assets/i18n/` asset bundle entry
|
||||
- `lib/main.dart` — async `TranslationService.init()` before `runApp`, reads saved locale from SharedPreferences
|
||||
- `lib/domain/entities/pet_type.dart` — `localizedName()` method uses `t()`
|
||||
- All UI screens and shared widgets — hardcoded strings replaced with `t()` calls
|
||||
|
||||
## Architecture
|
||||
|
||||
### JSON format
|
||||
Flat key-value structure with dot-notation grouping. The `_meta` block is required but skipped during lookup.
|
||||
|
||||
```json
|
||||
{
|
||||
"_meta": { "language": "English", "locale": "en", "version": "1.0.0", "author": "KittieMobile Team" },
|
||||
"onboarding.welcome_title": "Welcome to KittieMobile!",
|
||||
"tasks.error": "Error: {error}",
|
||||
"tasks.count": "{count} tasks completed"
|
||||
}
|
||||
```
|
||||
|
||||
Czech can reorder named params:
|
||||
```json
|
||||
{
|
||||
"tasks.count": "Dokončeno {count} úkolů"
|
||||
}
|
||||
```
|
||||
|
||||
### TranslationService (`lib/core/i18n/translation_service.dart`)
|
||||
- Singleton: `TranslationService.instance`
|
||||
- `init({String locale})` — loads `en.json` as fallback, then overlays requested locale
|
||||
- `setLocale(String locale)` — hot-switch locale, notifies listeners via `ChangeNotifier`
|
||||
- `t(String key, {Map<String, String>? params})` — lookup chain: current locale → EN fallback → key itself (never returns null, never crashes)
|
||||
- Sanitizes loaded values: strips control chars (U+0000–U+001F except `\n`) and zero-width chars (U+200B/C/D, U+FEFF)
|
||||
|
||||
### Parameter substitution rules
|
||||
| Situation | Behavior |
|
||||
|-----------|----------|
|
||||
| `{paramName}` with matching key in params | Replaced with value |
|
||||
| `{paramName}` with no matching key | Left as-is in output |
|
||||
| `{param` (unclosed `}`) | Written as-is, then ` value` appended at end |
|
||||
| `}` without opening `{` | Treated as literal text |
|
||||
|
||||
### Riverpod providers (`lib/core/i18n/i18n_provider.dart`)
|
||||
- `translationServiceProvider` — `Provider<TranslationService>` exposing singleton
|
||||
- `localeProvider` — `NotifierProvider<LocaleNotifier, String>` — holds current locale string, persists to SharedPreferences on change
|
||||
- `tProvider` — `Provider<String Function(String, {Map<String, String>? params})>` — watches `localeProvider` so widgets rebuild on locale change
|
||||
|
||||
### Language picker (`lib/shared/widgets/language_picker.dart`)
|
||||
`LanguagePicker` is a `ConsumerWidget` rendering two flag buttons (🇬🇧 / 🇨🇿). Active locale gets a highlighted border. Calling `ref.read(localeProvider.notifier).setLocale(locale)` persists and propagates the change.
|
||||
|
||||
Placed in:
|
||||
- Onboarding screen (first step)
|
||||
- Profile screen (settings area)
|
||||
|
||||
### I18nValidator (`lib/core/i18n/i18n_validator.dart`)
|
||||
Used in `test/i18n_test.dart` for CI validation:
|
||||
- `validateTranslationFile(String json)` — checks valid JSON, string-only values, no bad characters, `_meta` presence, well-formed `{param}` placeholders, valid param names `[a-zA-Z0-9_]`
|
||||
- `compareTranslations(Map en, Map other)` — returns `missingKeys`, `extraKeys`, `emptyValues`, `mismatchedParams`
|
||||
|
||||
## Usage in widgets
|
||||
|
||||
```dart
|
||||
// In ConsumerWidget / ConsumerStatefulWidget
|
||||
final t = ref.watch(tProvider);
|
||||
|
||||
Text(t('onboarding.welcome_title'))
|
||||
Text(t('tasks.count', params: {'count': '5'}))
|
||||
Text(t('tasks.error', params: {'error': e.toString()}))
|
||||
```
|
||||
|
||||
For widgets without `ref` access (e.g. `initState`):
|
||||
```dart
|
||||
TranslationService.instance.t('key')
|
||||
```
|
||||
|
||||
## Tests (`test/i18n_test.dart`)
|
||||
18 tests — all passing:
|
||||
- `en.json` and `cs.json` are valid JSON
|
||||
- `cs.json` has all keys from `en.json`, no extras, no empty values
|
||||
- No control/zero-width chars in values
|
||||
- Param placeholders in `cs.json` match those in `en.json`
|
||||
- Fallback: missing key → key string, missing locale → English
|
||||
- Param substitution: correct, missing closing `}`, missing param, different word order
|
||||
- `I18nValidator`: invalid JSON, missing `_meta`, non-string values, unclosed placeholder, invalid param name, `extractParams`
|
||||
|
||||
Run: `flutter test test/i18n_test.dart`
|
||||
|
||||
## Adding a new language
|
||||
|
||||
1. Copy `assets/i18n/en.json` → `assets/i18n/<locale>.json`
|
||||
2. Translate all values (keep `{param}` placeholders unchanged)
|
||||
3. Add locale code to `TranslationService._availableLocales` in `translation_service.dart`
|
||||
4. Add a flag button in `language_picker.dart`
|
||||
5. Run `flutter test test/i18n_test.dart` to validate
|
||||
Reference in New Issue
Block a user