Refactor: Move i18n documentation from separate files to DocC

- Created new Localization.md article in DocC catalog
- Integrated user guide and developer guide content into single article
- Deleted separate Documentation/i18n-guide.md and i18n-developer.md
- Updated README.md to link to DocC documentation
- Updated GettingStarted.md to reference Localization guide

Documentation now follows project-wide DocC standards instead of separate Markdown files.
This commit is contained in:
phranck
2026-02-14 19:56:38 +01:00
parent 1a4c1d8782
commit d711979118
5 changed files with 505 additions and 813 deletions
-377
View File
@@ -1,377 +0,0 @@
# TUIkit i18n Developer Guide
This guide is for developers working on TUIkit's localization infrastructure, not for app developers using TUIkit. See [i18n-guide.md](i18n-guide.md) for end-user documentation.
## Architecture Overview
The TUIkit localization system consists of:
### 1. **LocalizationService** (`LocalizationService.swift`)
Core service that manages:
- Language selection and persistence
- JSON translation file loading
- String resolution with fallback chains
- Thread-safe access via `NSLock`
### 2. **LocalizationKey** (`LocalizationKeys.swift`)
Type-safe enum hierarchy providing:
- Compile-time safe key references
- Organized by category (Button, Label, Error, etc.)
- Convenient extensions for LocalizedString and Text views
### 3. **LocalizedString** (`LocalizedString.swift`)
Simple View component that displays localized strings by key.
### 4. **Translation Files** (`Localization/translations/*.json`)
JSON files for each language:
- `en.json` - English (primary reference)
- `de.json` - German
- `fr.json` - French
- `it.json` - Italian
- `es.json` - Spanish
## Adding New Translation Categories
To add a new category of strings (e.g., for a new UI area):
### Step 1: Update LocalizationKey Enum
Edit `Sources/TUIkit/Localization/LocalizationKeys.swift`:
```swift
public enum LocalizationKey {
// ... existing categories
/// New category for notifications
public enum Notification: String {
case success = "notification.success"
case warning = "notification.warning"
case error = "notification.error"
}
}
// Add convenient extension
extension LocalizedString {
public init(_ key: LocalizationKey.Notification) {
self.init(key.rawValue)
}
}
extension Text {
public init(localized key: LocalizationKey.Notification) {
self.init(localized: key.rawValue)
}
}
extension LocalizationService {
public func string(for key: LocalizationKey.Notification) -> String {
string(for: key.rawValue)
}
}
```
### Step 2: Add Keys to All Translation Files
For each of the 5 language files:
**en.json**:
```json
{
"notification.success": "Success",
"notification.warning": "Warning",
"notification.error": "Error",
...
}
```
**de.json**:
```json
{
"notification.success": "Erfolg",
"notification.warning": "Warnung",
"notification.error": "Fehler",
...
}
```
**fr.json**, **it.json**, **es.json**: Similar translations
### Step 3: Add Tests
Edit `Tests/TUIkitTests/LocalizationKeyConsistencyTests.swift`:
```swift
// MARK: - Notification Key Tests
func testAllNotificationKeysExistInTranslations() {
let keys = [
LocalizationKey.Notification.success,
LocalizationKey.Notification.warning,
LocalizationKey.Notification.error,
]
for key in keys {
XCTAssertNotNil(
englishTranslations[key.rawValue],
"Notification key '\(key.rawValue)' not found in translations"
)
}
}
```
Also update `testNoExtraneousKeysInTranslations()` to include the new keys in the `enumKeys` set.
Update `testAllEnumKeysAreCovered()` to reflect the new key count.
### Step 4: Add to Service Extensions (optional)
If you want LocalizationService to support the new category:
```swift
// LocalizationService.swift (inside public extension)
public func string(for key: LocalizationKey.Notification) -> String {
string(for: key.rawValue)
}
```
### Step 5: Run Tests
```bash
swift test --filter LocalizationKeyConsistencyTests
```
All consistency tests should pass, verifying:
- Every enum key exists in translations
- No extraneous keys in translation files
- Correct total key count
## Adding a New Language
To add support for a completely new language:
### Step 1: Update Language Enum
Edit `Sources/TUIkit/Localization/LocalizationService.swift`:
```swift
public enum Language: String, Codable {
case english = "en"
case german = "de"
case french = "fr"
case italian = "it"
case spanish = "es"
case portuguese = "pt" // NEW
public var displayName: String {
switch self {
// ... existing cases
case .portuguese: "Português" // NEW
}
}
}
```
### Step 2: Create Translation File
Create `Sources/TUIkit/Localization/translations/pt.json` with ALL keys from the framework:
```json
{
"button.ok": "Tudo bem",
"button.cancel": "Cancelar",
...
// Include ALL keys - use en.json as reference
}
```
**Important**: Every key from en.json must be present in the new language file.
### Step 3: Run Consistency Tests
```bash
swift test --filter LocalizationKeyConsistencyTests
```
This will verify that all enum keys exist in the new language file.
### Step 4: Test the New Language
```swift
let service = LocalizationService()
service.setLanguage(.portuguese)
// Verify strings load correctly
let ok = service.string(for: .button(.ok))
XCTAssertNotEqual(ok, "button.ok") // Should be translated
```
## Key Design Principles
### 1. Dot-Notation Keys
Keys use `category.subcategory` format:
- `button.ok` - Button in Button category
- `error.not_found` - "not_found" error in Error category
- `validation.email_invalid` - "email_invalid" in Validation category
**Why**: Clearly organizes related strings, easy to grep, matches JSON structure.
### 2. Enum Categories
Each JSON prefix becomes an enum:
- All `button.*` keys → `LocalizationKey.Button` enum
- All `error.*` keys → `LocalizationKey.Error` enum
- All `validation.*` keys → `LocalizationKey.Validation` enum
**Why**: Type safety, IDE autocomplete, compile-time checking.
### 3. Fallback Chain
Resolution tries: Current Language → English → Key
```swift
service.setLanguage(.french)
let text = service.string(for: "button.ok")
// Tries: French English Returns "button.ok"
```
**Why**: Graceful degradation if a key is missing or language incomplete.
### 4. Persistence
Language choice is saved to disk and restored on app restart:
- **macOS**: `~/Library/Application Support/tuikit/language`
- **Linux**: `~/.config/tuikit/language`
**Why**: Users expect their language preference to persist.
### 5. Thread Safety
`LocalizationService` uses `NSLock()` for thread-safe access:
- Language switching is atomic
- Translation loading is atomic
- Multiple threads can safely access simultaneously
**Why**: Apps may need to change language from background threads.
## Testing Strategy
### LocalizationServiceTests.swift
Core functionality tests:
- Bundle loading for all languages
- String resolution and dot-notation
- Fallback behavior
- Language switching
- Persistence to disk
- Thread safety
### LocalizationKeyTests.swift
Type-safe key testing:
- Each key category resolves correctly
- Keys work across all languages
- Enum raw values match string keys
### LocalizationKeyConsistencyTests.swift
**Most important**: Validates sync between code and translations:
- Every enum key exists in en.json
- Every enum key exists in all other languages
- No extraneous keys in translation files
- Expected total key count
**Run after any localization changes**:
```bash
swift test --filter LocalizationKeyConsistencyTests
```
## JSON File Format
### Structure
```json
{
"category.key": "English text",
"category.another_key": "More text",
...
}
```
### Rules
1. **Flat structure**: No nested objects (all keys are top-level)
2. **Dot-separated keys**: `"button.ok"`, not `"button": { "ok": ... }`
3. **UTF-8 encoding**: Required for non-Latin characters
4. **Valid JSON**: No trailing commas, proper escaping
### Example
```json
{
"button.ok": "OK",
"button.cancel": "Cancel",
"error.not_found": "Not found",
"error.unicode_test": "Unicode: café, Ñoño, 日本語",
"placeholder.enter_name": "Enter name..."
}
```
## Performance Considerations
### Caching
Translations are cached per language after first load:
```swift
// First access: loads from Bundle, caches
let text1 = service.string(for: "button.ok")
// Subsequent accesses: use cache
let text2 = service.string(for: "button.ok") // Cache hit
```
### Bundle Resources
Translation files are bundled at compile time:
- Included in `Package.swift` with `.copy("Localization/translations")`
- Loaded via `Bundle.module.url(...)`
- Zero runtime overhead for bundling
### Lock Contention
While `NSLock` is used, contention is minimal:
- Lock is only held during cache operations
- String lookups are fast dictionary access
- Lock is released immediately after
## Common Pitfalls
### 1. Forgetting a Language
Adding a key but not translating to all 5 languages:
- **Caught by**: `testNoExtraneousKeysInTranslations()` in consistency tests
- **Fix**: Add key to all 5 JSON files
### 2. Typos in Enum
Using `button.ок` (Cyrillic 'k') instead of `button.ok` (Latin 'k'):
- **Caught by**: IDE if you copy from enum
- **Prevention**: Always use `LocalizationKey.Button.ok.rawValue`
### 3. Mismatched JSON Structure
Changing key format after release (e.g., `button_ok``button.ok`):
- **Issue**: Breaks existing preferences if keys changed
- **Prevention**: Keys are permanent API, don't rename
### 4. Incomplete Translations
Leaving English text in non-English translations:
- **Testing**: Manual review of all languages
- **Tools**: String comparison scripts (planned for future)
### 5. Character Encoding Issues
Special characters garbled in translation files:
- **Cause**: File saved in wrong encoding
- **Fix**: Ensure UTF-8 encoding, validate with `jsonlint`
## Future Enhancements
Planned improvements to the localization system:
1. **Pluralization**: Handle plural forms ("1 item", "2 items")
2. **Interpolation**: Support `"Hello {name}"` style formatting
3. **Context**: Multiple translations based on context (e.g., Cancel as button vs command)
4. **Import/Export Tool**: `tuikit i18n export --format=xliff` for easier translation management
5. **String Analysis Tool**: Detect unused keys and missing translations
6. **RTL Support**: Right-to-left language support for Arabic, Hebrew
## References
- **Main Documentation**: See [i18n-guide.md](i18n-guide.md)
- **LocalizationService API**: `Sources/TUIkit/Localization/LocalizationService.swift`
- **LocalizationKey Enums**: `Sources/TUIkit/Localization/LocalizationKeys.swift`
- **Translation Files**: `Sources/TUIkit/Localization/translations/`
-435
View File
@@ -1,435 +0,0 @@
# TUIkit Internationalization (i18n) Guide
TUIkit provides comprehensive internationalization support with 5 languages built-in: English, German, French, Italian, and Spanish. All framework strings use a type-safe, dot-notation based localization system.
## Table of Contents
1. [Quick Start](#quick-start)
2. [Using Localized Strings](#using-localized-strings)
3. [Type-Safe Keys](#type-safe-keys)
4. [Switching Languages](#switching-languages)
5. [Adding New Keys](#adding-new-keys)
6. [Adding New Languages](#adding-new-languages)
7. [Advanced Usage](#advanced-usage)
## Quick Start
### Basic Usage
Use `LocalizedString` to display localized text:
```swift
import TUIkit
VStack {
LocalizedString(.button(.ok))
LocalizedString(.label(.name))
LocalizedString(.error(.notFound))
}
```
Or use the `Text(localized:)` convenience initializer:
```swift
Text(localized: .dialog(.confirm))
```
### Switching Languages at Runtime
```swift
// In your app
AppState.shared.setLanguage(.german)
// The UI automatically re-renders with German strings
```
## Using Localized Strings
### With LocalizedString View
`LocalizedString` is a `View` that displays a localized string:
```swift
VStack {
LocalizedString(.button(.save))
LocalizedString(.error(.invalidInput))
LocalizedString(.validation(.emailInvalid))
}
```
### With Text View
Use the `Text(localized:)` initializer for cases where you need a `Text` view directly:
```swift
struct MyControl: View {
var body: some View {
VStack {
Text(localized: .menu(.file))
Text(localized: .placeholder(.search))
}
}
}
```
### With String Keys
For dynamic key resolution or advanced use cases:
```swift
let key = "button.ok"
let localizedText = LocalizationService.shared.string(for: key)
```
## Type-Safe Keys
All framework strings are available as type-safe `LocalizationKey` enums. This provides:
- **Compile-time safety**: Typos are caught by the compiler
- **IDE autocomplete**: Full support for all available keys
- **Refactoring safety**: Safe renaming across the codebase
### Key Categories
#### Button Keys
```swift
LocalizationKey.Button.ok
LocalizationKey.Button.cancel
LocalizationKey.Button.save
LocalizationKey.Button.delete
// ... and 17 more
```
#### Label Keys
```swift
LocalizationKey.Label.name
LocalizationKey.Label.description
LocalizationKey.Label.value
LocalizationKey.Label.status
// ... and 13 more
```
#### Error Keys
```swift
LocalizationKey.Error.invalidInput
LocalizationKey.Error.notFound
LocalizationKey.Error.accessDenied
LocalizationKey.Error.timeout
// ... and 7 more
```
#### Placeholder Keys
```swift
LocalizationKey.Placeholder.search
LocalizationKey.Placeholder.enterText
LocalizationKey.Placeholder.selectOption
// ... and 3 more
```
#### Menu Keys
```swift
LocalizationKey.Menu.file
LocalizationKey.Menu.edit
LocalizationKey.Menu.view
LocalizationKey.Menu.help
// ... and 4 more
```
#### Dialog Keys
```swift
LocalizationKey.Dialog.confirm
LocalizationKey.Dialog.deleteConfirmation
LocalizationKey.Dialog.unsavedChanges
// ... and 4 more
```
#### Validation Keys
```swift
LocalizationKey.Validation.emailInvalid
LocalizationKey.Validation.passwordTooShort
LocalizationKey.Validation.usernameTaken
LocalizationKey.Validation.fieldRequired
```
## Switching Languages
### Get Current Language
```swift
let current = AppState.shared.currentLanguage
print(current.displayName) // "Deutsch", "Français", etc.
```
### Supported Languages
- `.english` - English
- `.german` - Deutsch
- `.french` - Français
- `.italian` - Italiano
- `.spanish` - Español
### Change Language at Runtime
```swift
// Using AppState
AppState.shared.setLanguage(.german)
// Or directly via LocalizationService
LocalizationService.shared.setLanguage(.french)
```
### Language Persistence
Language preferences are automatically saved to disk:
- **macOS**: `~/Library/Application Support/tuikit/language`
- **Linux**: `~/.config/tuikit/language`
The saved preference is restored when the app restarts.
## Adding New Keys
When you need to add new localized strings to the framework:
### 1. Add to LocalizationKey Enum
Edit `Sources/TUIkit/Localization/LocalizationKeys.swift`:
```swift
public enum LocalizationKey {
public enum Button: String {
// ... existing cases
case myNewButton = "button.my_new_button"
}
}
```
### 2. Add to All Translation Files
Add the same key to all 5 translation JSON files:
- `Sources/TUIkit/Localization/translations/en.json`
- `Sources/TUIkit/Localization/translations/de.json`
- `Sources/TUIkit/Localization/translations/fr.json`
- `Sources/TUIkit/Localization/translations/it.json`
- `Sources/TUIkit/Localization/translations/es.json`
**en.json**:
```json
{
"button.my_new_button": "My New Button",
...
}
```
**de.json**:
```json
{
"button.my_new_button": "Mein neuer Button",
...
}
```
And similarly for French, Italian, and Spanish.
### 3. Update Tests (if needed)
If adding a new category, add corresponding test methods in:
- `Tests/TUIkitTests/LocalizationServiceTests.swift`
- `Tests/TUIkitTests/LocalizationKeyConsistencyTests.swift`
### 4. Run Consistency Tests
Verify that all keys in the enum exist in the translation files:
```bash
swift test --filter LocalizationKeyConsistencyTests
```
## Adding New Languages
To add support for a new language:
### 1. Add to Language Enum
Edit `Sources/TUIkit/Localization/LocalizationService.swift`:
```swift
public enum Language: String, Codable {
case english = "en"
case german = "de"
case french = "fr"
case italian = "it"
case spanish = "es"
case portuguese = "pt" // New language
public var displayName: String {
switch self {
case .english: "English"
case .german: "Deutsch"
case .french: "Français"
case .italian: "Italiano"
case .spanish: "Español"
case .portuguese: "Português" // Add display name
}
}
}
```
### 2. Create Translation File
Create a new JSON file with all keys translated:
`Sources/TUIkit/Localization/translations/pt.json`:
```json
{
"button.ok": "OK",
"button.cancel": "Cancelar",
...
}
```
Make sure all keys from the other language files are included.
### 3. Update Package.swift
The translations are automatically discovered from the directory, no changes needed.
### 4. Test the New Language
```swift
let service = LocalizationService()
service.setLanguage(.portuguese)
let ok = service.string(for: LocalizationKey.Button.ok)
print(ok) // "OK"
```
## Advanced Usage
### Direct Service Access
For advanced scenarios, access the localization service directly:
```swift
let service = LocalizationService.shared
// Get current language
let current = service.currentLanguage
// Get a string
let text = service.string(for: "button.ok")
// Change language
service.setLanguage(.german)
```
### Using Raw String Keys
While not recommended (loses type safety), you can use raw string keys:
```swift
let text = LocalizationService.shared.string(for: "button.ok")
```
### Fallback Behavior
String resolution uses a fallback chain:
1. **Current language**: Try to find the key in the active language
2. **English**: If not found, fall back to English
3. **Key itself**: If not found in any language, return the key as-is
This ensures the app always has something to display, even with incomplete translations.
### Environment Access
`LocalizationService` is available in the environment:
```swift
struct MyView: View {
@Environment(\.localizationService) var localization
var body: some View {
Text(localization.string(for: "button.ok"))
}
}
```
## Integration with Views
### In Custom Components
```swift
struct MyDialog: View {
var body: some View {
VStack {
LocalizedString(.dialog(.confirm))
HStack {
Button("OK") { /* ... */ }
.buttonLabel(LocalizedString(.button(.cancel)))
}
}
}
}
```
### In Form Validation
```swift
struct LoginForm: View {
@State var email = ""
@State var showError = false
var body: some View {
VStack {
TextField(LocalizationKey.Placeholder.enterName.rawValue, text: $email)
if showError {
LocalizedString(.validation(.emailInvalid))
.foregroundColor(.red)
}
}
}
}
```
## Best Practices
1. **Always use type-safe keys**: Use `LocalizationKey` enums instead of raw strings
2. **Group related strings**: Keep strings organized by category (Button, Label, Error, etc.)
3. **Keep keys concise**: Use short, descriptive key names
4. **Run consistency tests**: Always run `LocalizationKeyConsistencyTests` after adding keys
5. **Test all languages**: Verify strings look correct in all supported languages
6. **Document dynamic strings**: If using raw keys, document why they're necessary
## Troubleshooting
### String Not Appearing
- Check that the key exists in `LocalizationKey` enum
- Verify the key is in all translation JSON files
- Run consistency tests: `swift test --filter LocalizationKeyConsistencyTests`
### Wrong Language Showing
- Verify the language was set: `AppState.shared.currentLanguage`
- Check that the language is supported (en, de, fr, it, es)
- Ensure the translation file exists for that language
### Translation File Errors
If JSON syntax errors prevent loading:
- Validate JSON: Use `jsonlint` or an online JSON validator
- Check for missing commas or quotes
- Ensure no trailing commas in objects/arrays
## Related Documentation
- **Localization Service API**: See `LocalizationService.swift`
- **Localization Keys**: See `LocalizationKeys.swift`
- **Translation Files**: `Sources/TUIkit/Localization/translations/`
+1 -1
View File
@@ -202,7 +202,7 @@ struct MyView: View {
**Supported languages**: English, Deutsch, Français, Italiano, Español
For complete documentation, see [Documentation/i18n-guide.md](Documentation/i18n-guide.md) and [Documentation/i18n-developer.md](Documentation/i18n-developer.md).
For complete documentation, see [Localization Guide](https://github.com/phranck/TUIkit/blob/main/Sources/TUIkit/TUIkit.docc/Articles/Localization.md) in the DocC documentation.
## Architecture
@@ -105,3 +105,4 @@ renderOnce {
- Learn about the framework's <doc:Architecture>
- Explore <doc:StateManagement> for reactive UIs
- Customize your app's look with <doc:ThemingGuide>
- Build multilingual apps with <doc:Localization>
@@ -0,0 +1,503 @@
# Localization
Build multilingual terminal applications with TUIkit's comprehensive internationalization system.
## Overview
TUIkit provides built-in support for 5 languages: English, German, French, Italian, and Spanish. All framework strings use a type-safe, dot-notation based localization system with persistent language preferences and automatic fallback chains.
### Key Features
- **Type-safe keys**: Compile-time verified `LocalizationKey` enums with IDE autocomplete
- **5 languages built-in**: EN, DE, FR, IT, ES with complete translations
- **Persistent storage**: Language preference automatically saved and restored (XDG-compatible paths)
- **Fallback chain**: Current language → English → key itself for graceful degradation
- **Thread-safe**: Safe language switching from any thread at runtime
- **JSON-based**: Easy to extend with new strings and languages
## Quick Start
### Display Localized Strings
Use `LocalizedString` to show localized text:
```swift
import TUIkit
VStack {
LocalizedString(.button(.ok))
LocalizedString(.error(.notFound))
LocalizedString(.dialog(.confirm))
}
```
Or use the `Text(localized:)` convenience initializer:
```swift
Text(localized: .button(.save))
```
### Switch Language at Runtime
```swift
AppState.shared.setLanguage(.german)
// UI automatically re-renders with German strings
```
### Supported Languages
- `.english` - English
- `.german` - Deutsch
- `.french` - Français
- `.italian` - Italiano
- `.spanish` - Español
## Type-Safe Keys
All localized strings are available as `LocalizationKey` enums organized by category. This provides compile-time safety, IDE autocomplete, and refactoring support.
### Key Categories
#### Button Keys
21 button strings including: ok, cancel, yes, no, save, delete, close, apply, reset, submit, search, clear, add, remove, edit, done, next, previous, back, forward, refresh
```swift
LocalizationKey.Button.ok
LocalizationKey.Button.cancel
LocalizationKey.Button.save
```
#### Label Keys
17 label strings including: search, name, description, value, status, error, warning, info, loading, empty, none, page, item, items, total, from, to
```swift
LocalizationKey.Label.name
LocalizationKey.Label.description
LocalizationKey.Label.status
```
#### Error Keys
11 error strings including: invalidInput, requiredField, notFound, accessDenied, networkError, unknown, invalidFormat, operationFailed, timeout, fileNotFound, permissionDenied
```swift
LocalizationKey.Error.invalidInput
LocalizationKey.Error.notFound
LocalizationKey.Error.timeout
```
#### Placeholder Keys
6 placeholder strings: search, enterText, enterValue, selectOption, enterName, chooseFile
```swift
LocalizationKey.Placeholder.search
LocalizationKey.Placeholder.enterText
LocalizationKey.Placeholder.selectOption
```
#### Menu Keys
8 menu strings: file, edit, view, help, new, open, save, exit
```swift
LocalizationKey.Menu.file
LocalizationKey.Menu.edit
LocalizationKey.Menu.help
```
#### Dialog Keys
7 dialog strings: confirm, deleteConfirmation, unsavedChanges, overwriteConfirmation, exitConfirmation, success, error
```swift
LocalizationKey.Dialog.confirm
LocalizationKey.Dialog.deleteConfirmation
LocalizationKey.Dialog.unsavedChanges
```
#### Validation Keys
4 validation strings: emailInvalid, passwordTooShort, usernameTaken, fieldRequired
```swift
LocalizationKey.Validation.emailInvalid
LocalizationKey.Validation.passwordTooShort
LocalizationKey.Validation.usernameTaken
```
## Using Localized Strings
### LocalizedString View
Display a localized string as a View component:
```swift
struct MyView: View {
var body: some View {
VStack {
LocalizedString(.button(.save))
LocalizedString(.error(.invalidInput))
LocalizedString(.validation(.emailInvalid))
}
}
}
```
### Text View with Localization
Use the `Text(localized:)` initializer:
```swift
struct MyControl: View {
var body: some View {
VStack {
Text(localized: .menu(.file))
Text(localized: .placeholder(.search))
}
}
}
```
### Direct Service Access
For advanced use cases, access the service directly:
```swift
let service = LocalizationService.shared
let text = service.string(for: .button(.ok))
```
## Language Switching
### Get Current Language
```swift
let current = AppState.shared.currentLanguage
print(current.displayName) // "English", "Deutsch", etc.
```
### Change Language
```swift
// Via AppState
AppState.shared.setLanguage(.german)
// Or directly via service
LocalizationService.shared.setLanguage(.french)
```
### Language Persistence
Language preferences are automatically saved to:
- **macOS**: `~/Library/Application Support/tuikit/language`
- **Linux**: `~/.config/tuikit/language`
The saved preference is restored when the app restarts.
### Fallback Behavior
String resolution uses a fallback chain:
1. Try to find the key in the active language
2. If not found, fall back to English
3. If still not found, return the key itself as-is
This ensures the UI always has something to display, even with incomplete translations.
## Environment Access
`LocalizationService` is available in the view environment:
```swift
struct MyView: View {
@Environment(\.localizationService) var localization
var body: some View {
Text(localization.string(for: .button(.ok)))
}
}
```
## Adding New Keys
To add new localized strings to the framework:
### 1. Update LocalizationKey Enum
Edit `Sources/TUIkit/Localization/LocalizationKeys.swift`:
```swift
public enum LocalizationKey {
public enum Button: String {
// ... existing cases
case myNewButton = "button.my_new_button"
}
}
```
Then add convenient extensions:
```swift
extension LocalizedString {
public init(_ key: LocalizationKey.Button) {
self.init(key.rawValue)
}
}
extension Text {
public init(localized key: LocalizationKey.Button) {
self.init(localized: key.rawValue)
}
}
extension LocalizationService {
public func string(for key: LocalizationKey.Button) -> String {
string(for: key.rawValue)
}
}
```
### 2. Add to All Translation Files
Add the same key to all 5 translation JSON files:
**en.json**:
```json
{
"button.my_new_button": "My New Button",
...
}
```
**de.json**:
```json
{
"button.my_new_button": "Mein neuer Button",
...
}
```
Similar for `fr.json`, `it.json`, `es.json`.
### 3. Update Tests
Add tests in `Tests/TUIkitTests/LocalizationKeyConsistencyTests.swift`:
```swift
@Test("All button keys exist in translations")
func allButtonKeysExist() {
let keys = [
LocalizationKey.Button.ok,
LocalizationKey.Button.myNewButton,
// ... all other button keys
]
for key in keys {
#expect(englishTranslations[key.rawValue] != nil)
}
}
```
### 4. Run Consistency Tests
Verify all keys exist in translation files:
```bash
swift test --filter LocalizationKeyConsistencyTests
```
## Adding New Languages
To add support for a new language:
### 1. Update Language Enum
Edit `Sources/TUIkit/Localization/LocalizationService.swift`:
```swift
public enum Language: String, Codable {
case english = "en"
case german = "de"
case french = "fr"
case italian = "it"
case spanish = "es"
case portuguese = "pt" // NEW
public var displayName: String {
switch self {
case .english: "English"
case .german: "Deutsch"
case .french: "Français"
case .italian: "Italiano"
case .spanish: "Español"
case .portuguese: "Português" // NEW
}
}
}
```
### 2. Create Translation File
Create `Sources/TUIkit/Localization/translations/pt.json` with **all** keys from other language files:
```json
{
"button.ok": "OK",
"button.cancel": "Cancelar",
...
// Must include ALL keys (use en.json as reference)
}
```
### 3. Test the New Language
```swift
let service = LocalizationService()
service.setLanguage(.portuguese)
let ok = service.string(for: .button(.ok))
```
### 4. Run Consistency Tests
```bash
swift test --filter LocalizationKeyConsistencyTests
```
## Design Principles
### Dot-Notation Keys
Keys use `category.key_name` format:
```
button.ok
error.not_found
validation.email_invalid
```
This organizes related strings, makes them easy to find, and matches JSON structure.
### Enum Categories
Each JSON prefix becomes an enum category:
- All `button.*` keys → `LocalizationKey.Button`
- All `error.*` keys → `LocalizationKey.Error`
- All `validation.*` keys → `LocalizationKey.Validation`
This provides type safety, IDE autocomplete, and compile-time verification.
### Thread Safety
``LocalizationService`` uses `NSLock` for thread-safe access:
```swift
// Safe to call from any thread
DispatchQueue.global().async {
AppState.shared.setLanguage(.german)
}
```
### Performance Caching
Translations are cached per language after first load:
```swift
let text1 = service.string(for: .button(.ok)) // Loads and caches
let text2 = service.string(for: .button(.ok)) // Uses cache
```
## JSON File Format
### Structure
```json
{
"category.key": "English text",
"category.another_key": "More text",
...
}
```
### Rules
1. **Flat structure**: No nested objects (all keys are top-level)
2. **Dot-separated keys**: `"button.ok"`, not nested objects
3. **UTF-8 encoding**: Required for non-Latin characters
4. **Valid JSON**: No trailing commas, proper escaping
### Example
```json
{
"button.ok": "OK",
"button.cancel": "Cancel",
"error.not_found": "Not found",
"error.unicode_test": "Unicode: café, Ñoño, 日本語",
"placeholder.enter_name": "Enter name..."
}
```
## Best Practices
1. **Always use type-safe keys**: Use `LocalizationKey` enums instead of raw strings
2. **Keep keys organized**: Group related strings by category
3. **Use descriptive names**: Short, clear key names
4. **Run consistency tests**: Always verify after adding keys
5. **Test all languages**: Check translations look correct in each language
6. **Handle missing strings**: Rely on the fallback chain for incomplete translations
## Testing
TUIkit includes comprehensive localization tests:
### LocalizationServiceTests
Core functionality tests covering:
- Bundle loading for all languages
- String resolution with dot-notation
- Fallback behavior
- Language switching
- Persistence to disk
- Thread safety
### LocalizationKeyConsistencyTests
Validation tests ensuring:
- Every enum key exists in all translation files
- No extraneous keys in translation files
- Expected total key count
Run after any localization changes:
```bash
swift test --filter LocalizationKeyConsistencyTests
```
## Troubleshooting
### String Not Appearing
- Verify the key exists in `LocalizationKey` enum
- Check the key is in all translation JSON files
- Run consistency tests to verify
### Wrong Language Showing
- Verify language was set: `AppState.shared.currentLanguage`
- Confirm language is supported (en, de, fr, it, es)
- Check translation file exists for that language
### JSON Syntax Errors
If translation files won't load:
- Validate JSON syntax
- Check for missing commas or quotes
- Ensure UTF-8 encoding
- No trailing commas in objects
## See Also
- <doc:GettingStarted> — Getting started with TUIkit
- <doc:AppLifecycle> — Application lifecycle and setup
- <doc:Architecture> — Framework architecture