Add comprehensive i18n localization support
- Add go-i18n package for internationalization - Create i18n middleware to extract Accept-Language header - Add translation files for en, es, fr, de, pt languages - Localize all handler error messages and responses - Add language context to all API handlers Supported languages: English, Spanish, French, German, Portuguese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
412
docs/# Full Localization Plan for Casera.md
Normal file
412
docs/# Full Localization Plan for Casera.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# Full Localization Plan for Casera
|
||||
|
||||
## Overview
|
||||
|
||||
Complete localization of the Casera property management app across three codebases:
|
||||
- **Go API** - Server-side localization of errors, emails, push notifications, lookup data
|
||||
- **KMM/Android** - Compose Multiplatform string resources
|
||||
- **iOS** - Apple String Catalogs (.xcstrings)
|
||||
|
||||
**Target Languages**: English (base), Spanish, French, German, Portuguese (extensible)
|
||||
|
||||
**Key Decisions**:
|
||||
- API returns localized strings (server-side translation)
|
||||
- Accept-Language header determines locale
|
||||
- Clients display what API returns for API content
|
||||
- Clients localize their own UI strings
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Go API Localization
|
||||
|
||||
### 1.1 Add Dependency
|
||||
|
||||
```bash
|
||||
go get github.com/nicksnyder/go-i18n/v2
|
||||
```
|
||||
|
||||
### 1.2 Directory Structure
|
||||
|
||||
```
|
||||
myCribAPI-go/
|
||||
├── internal/
|
||||
│ └── i18n/
|
||||
│ ├── i18n.go # Core setup, bundle, T() helper
|
||||
│ ├── middleware.go # Gin middleware for Accept-Language
|
||||
│ └── translations/
|
||||
│ ├── en.json # English (base)
|
||||
│ ├── es.json # Spanish
|
||||
│ ├── fr.json # French
|
||||
│ ├── de.json # German
|
||||
│ └── pt.json # Portuguese
|
||||
├── templates/
|
||||
│ └── emails/
|
||||
│ ├── en/
|
||||
│ │ ├── welcome.html
|
||||
│ │ ├── verification.html
|
||||
│ │ └── password_reset.html
|
||||
│ ├── es/
|
||||
│ ├── fr/
|
||||
│ ├── de/
|
||||
│ └── pt/
|
||||
```
|
||||
|
||||
### 1.3 Core Files to Create
|
||||
|
||||
**internal/i18n/i18n.go**:
|
||||
- Initialize i18n bundle with embedded translation files
|
||||
- Provide `T(localizer, messageID, data)` helper function
|
||||
- Load all JSON translation files at startup
|
||||
|
||||
**internal/i18n/middleware.go**:
|
||||
- Parse Accept-Language header
|
||||
- Match against supported languages (en, es, fr, de, pt)
|
||||
- Store localizer in Gin context
|
||||
- Provide `GetLocalizer(c)` helper
|
||||
|
||||
### 1.4 Translation File Format
|
||||
|
||||
```json
|
||||
{
|
||||
"error.invalid_credentials": "Invalid credentials",
|
||||
"error.username_taken": "Username already taken",
|
||||
"error.email_taken": "Email already registered",
|
||||
"error.not_authenticated": "Not authenticated",
|
||||
"error.task_not_found": "Task not found",
|
||||
"error.residence_access_denied": "You don't have access to this property",
|
||||
"message.logged_out": "Logged out successfully",
|
||||
"message.task_deleted": "Task deleted successfully",
|
||||
"push.task_due_soon.title": "Task Due Soon",
|
||||
"push.task_due_soon.body": "{{.TaskTitle}} is due {{.DueDate}}"
|
||||
}
|
||||
```
|
||||
|
||||
### 1.5 Handler Update Pattern
|
||||
|
||||
```go
|
||||
// Before
|
||||
c.JSON(400, gin.H{"error": "Invalid credentials"})
|
||||
|
||||
// After
|
||||
localizer := i18n.GetLocalizer(c)
|
||||
c.JSON(400, gin.H{"error": i18n.T(localizer, "error.invalid_credentials", nil)})
|
||||
```
|
||||
|
||||
### 1.6 Database for Lookup Translations
|
||||
|
||||
Add translation table for task categories, priorities, statuses, frequencies:
|
||||
|
||||
```sql
|
||||
CREATE TABLE lookup_translations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
table_name VARCHAR(50) NOT NULL,
|
||||
record_id INT NOT NULL,
|
||||
locale VARCHAR(10) NOT NULL,
|
||||
field_name VARCHAR(50) NOT NULL,
|
||||
translated_value TEXT NOT NULL,
|
||||
UNIQUE(table_name, record_id, locale, field_name)
|
||||
);
|
||||
```
|
||||
|
||||
Update lookup endpoints to return translated names based on locale.
|
||||
|
||||
### 1.7 Critical Go Files to Modify
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `internal/i18n/i18n.go` | CREATE | Core i18n bundle and helpers |
|
||||
| `internal/i18n/middleware.go` | CREATE | Locale detection middleware |
|
||||
| `internal/i18n/translations/*.json` | CREATE | Translation files (5 languages) |
|
||||
| `internal/router/router.go` | MODIFY | Add i18n middleware |
|
||||
| `internal/handlers/auth_handler.go` | MODIFY | Localize ~22 error strings |
|
||||
| `internal/handlers/task_handler.go` | MODIFY | Localize ~30 error strings |
|
||||
| `internal/handlers/residence_handler.go` | MODIFY | Localize ~20 error strings |
|
||||
| `internal/handlers/contractor_handler.go` | MODIFY | Localize ~15 error strings |
|
||||
| `internal/handlers/document_handler.go` | MODIFY | Localize ~15 error strings |
|
||||
| `internal/services/email_service.go` | MODIFY | Template-based emails |
|
||||
| `internal/services/notification_service.go` | MODIFY | Localized push content |
|
||||
| `internal/services/lookup_service.go` | MODIFY | Return translated lookups |
|
||||
| `templates/emails/**` | CREATE | Email templates per language |
|
||||
|
||||
---
|
||||
|
||||
## Part 2: KMM/Android Localization
|
||||
|
||||
### 2.1 Strategy
|
||||
|
||||
Use Compose Multiplatform Resources (already in build.gradle.kts via `compose.components.resources`).
|
||||
|
||||
### 2.2 Directory Structure
|
||||
|
||||
```
|
||||
MyCribKMM/composeApp/src/commonMain/
|
||||
└── composeResources/
|
||||
├── values/
|
||||
│ └── strings.xml # English (base)
|
||||
├── values-es/
|
||||
│ └── strings.xml # Spanish
|
||||
├── values-fr/
|
||||
│ └── strings.xml # French
|
||||
├── values-de/
|
||||
│ └── strings.xml # German
|
||||
└── values-pt/
|
||||
└── strings.xml # Portuguese
|
||||
```
|
||||
|
||||
### 2.3 String Resource Format
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Auth -->
|
||||
<string name="auth_login_title">Sign In</string>
|
||||
<string name="auth_login_subtitle">Manage your properties with ease</string>
|
||||
<string name="auth_login_username_label">Username or Email</string>
|
||||
<string name="auth_login_password_label">Password</string>
|
||||
<string name="auth_login_button">Sign In</string>
|
||||
<string name="auth_forgot_password">Forgot Password?</string>
|
||||
|
||||
<!-- Properties -->
|
||||
<string name="properties_title">My Properties</string>
|
||||
<string name="properties_empty_title">No properties yet</string>
|
||||
<string name="properties_empty_subtitle">Add your first property to get started!</string>
|
||||
<string name="properties_add_button">Add Property</string>
|
||||
|
||||
<!-- Tasks -->
|
||||
<string name="tasks_title">Tasks</string>
|
||||
<string name="tasks_add_title">Add New Task</string>
|
||||
<string name="tasks_column_overdue">Overdue</string>
|
||||
<string name="tasks_column_due_soon">Due Soon</string>
|
||||
|
||||
<!-- Common -->
|
||||
<string name="common_save">Save</string>
|
||||
<string name="common_cancel">Cancel</string>
|
||||
<string name="common_delete">Delete</string>
|
||||
<string name="common_loading">Loading…</string>
|
||||
<string name="common_error_generic">Something went wrong. Please try again.</string>
|
||||
|
||||
<!-- Accessibility -->
|
||||
<string name="a11y_back">Back</string>
|
||||
<string name="a11y_close">Close</string>
|
||||
<string name="a11y_add_property">Add Property</string>
|
||||
</resources>
|
||||
```
|
||||
|
||||
### 2.4 Usage in Compose
|
||||
|
||||
```kotlin
|
||||
import casera.composeapp.generated.resources.Res
|
||||
import casera.composeapp.generated.resources.*
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
|
||||
@Composable
|
||||
fun LoginScreen() {
|
||||
Text(stringResource(Res.string.auth_login_title))
|
||||
Button(onClick = { /* ... */ }) {
|
||||
Text(stringResource(Res.string.auth_login_button))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 Critical KMM Files to Modify
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `composeResources/values/strings.xml` | CREATE | Base English strings (~500) |
|
||||
| `composeResources/values-es/strings.xml` | CREATE | Spanish translations |
|
||||
| `composeResources/values-fr/strings.xml` | CREATE | French translations |
|
||||
| `composeResources/values-de/strings.xml` | CREATE | German translations |
|
||||
| `composeResources/values-pt/strings.xml` | CREATE | Portuguese translations |
|
||||
| `ui/screens/LoginScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||
| `ui/screens/ResidencesScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||
| `ui/screens/TasksScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||
| `ui/screens/ContractorsScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||
| `ui/screens/DocumentsScreen.kt` | MODIFY | Replace hardcoded strings |
|
||||
| `ui/components/*.kt` | MODIFY | Replace hardcoded strings (33 files) |
|
||||
| All other screen files | MODIFY | Replace hardcoded strings |
|
||||
|
||||
---
|
||||
|
||||
## Part 3: iOS Localization
|
||||
|
||||
### 3.1 Strategy
|
||||
|
||||
Use Apple String Catalogs (`.xcstrings`) - modern approach with Xcode visual editor.
|
||||
|
||||
### 3.2 Create String Catalog
|
||||
|
||||
1. Xcode: File > New > File > String Catalog
|
||||
2. Name: `Localizable.xcstrings`
|
||||
3. Add languages: English, Spanish, French, German, Portuguese
|
||||
|
||||
### 3.3 Type-Safe String Access
|
||||
|
||||
Create `iosApp/iosApp/Helpers/L10n.swift`:
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
|
||||
enum L10n {
|
||||
enum Auth {
|
||||
static let loginTitle = String(localized: "auth_login_title")
|
||||
static let loginSubtitle = String(localized: "auth_login_subtitle")
|
||||
static let loginUsernameLabel = String(localized: "auth_login_username_label")
|
||||
static let loginPasswordLabel = String(localized: "auth_login_password_label")
|
||||
static let loginButton = String(localized: "auth_login_button")
|
||||
static let forgotPassword = String(localized: "auth_forgot_password")
|
||||
}
|
||||
|
||||
enum Properties {
|
||||
static let title = String(localized: "properties_title")
|
||||
static let emptyTitle = String(localized: "properties_empty_title")
|
||||
static let emptySubtitle = String(localized: "properties_empty_subtitle")
|
||||
static let addButton = String(localized: "properties_add_button")
|
||||
}
|
||||
|
||||
enum Tasks {
|
||||
static let title = String(localized: "tasks_title")
|
||||
static let addTitle = String(localized: "tasks_add_title")
|
||||
static let columnOverdue = String(localized: "tasks_column_overdue")
|
||||
static let columnDueSoon = String(localized: "tasks_column_due_soon")
|
||||
}
|
||||
|
||||
enum Common {
|
||||
static let save = String(localized: "common_save")
|
||||
static let cancel = String(localized: "common_cancel")
|
||||
static let delete = String(localized: "common_delete")
|
||||
static let loading = String(localized: "common_loading")
|
||||
static let errorGeneric = String(localized: "common_error_generic")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Usage in SwiftUI
|
||||
|
||||
```swift
|
||||
// Before
|
||||
Text("My Properties")
|
||||
.navigationTitle("My Properties")
|
||||
|
||||
// After
|
||||
Text(L10n.Properties.title)
|
||||
.navigationTitle(L10n.Properties.title)
|
||||
```
|
||||
|
||||
### 3.5 Critical iOS Files to Modify
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `iosApp/Localizable.xcstrings` | CREATE | String catalog with all translations |
|
||||
| `iosApp/Helpers/L10n.swift` | CREATE | Type-safe string access |
|
||||
| `Login/LoginView.swift` | MODIFY | Replace ~7 hardcoded strings |
|
||||
| `Login/LoginViewModel.swift` | MODIFY | Replace ~12 error messages |
|
||||
| `Register/RegisterView.swift` | MODIFY | Replace ~10 hardcoded strings |
|
||||
| `Residence/*.swift` | MODIFY | Replace ~30 hardcoded strings |
|
||||
| `Task/*.swift` | MODIFY | Replace ~50 hardcoded strings |
|
||||
| `Contractor/*.swift` | MODIFY | Replace ~20 hardcoded strings |
|
||||
| `Document/*.swift` | MODIFY | Replace ~15 hardcoded strings |
|
||||
| `Profile/*.swift` | MODIFY | Replace ~20 hardcoded strings |
|
||||
| All other view files | MODIFY | Replace hardcoded strings |
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Implementation Order
|
||||
|
||||
### Phase 1: Infrastructure (Do First)
|
||||
|
||||
1. **Go API**:
|
||||
- Add go-i18n dependency
|
||||
- Create `internal/i18n/` package with i18n.go and middleware.go
|
||||
- Create base `en.json` with all extractable strings
|
||||
- Add middleware to router
|
||||
- Test with curl using Accept-Language header
|
||||
|
||||
2. **KMM**:
|
||||
- Create `composeResources/values/strings.xml` with ~50 core strings
|
||||
- Verify Compose resources compile correctly
|
||||
- Update one screen (LoginScreen) as proof of concept
|
||||
|
||||
3. **iOS**:
|
||||
- Create `Localizable.xcstrings` in Xcode
|
||||
- Create `L10n.swift` helper
|
||||
- Add ~50 core strings
|
||||
- Update LoginView as proof of concept
|
||||
|
||||
### Phase 2: API Full Localization
|
||||
|
||||
1. Update all handlers to use localized errors
|
||||
2. Add lookup_translations table and seed data
|
||||
3. Update lookup service to return translated names
|
||||
4. Move email templates to files, create Spanish versions
|
||||
5. Update push notification service for localized content
|
||||
|
||||
### Phase 3: Mobile String Extraction
|
||||
|
||||
**Order by feature (same for KMM and iOS)**:
|
||||
1. Auth screens (Login, Register, Verify, Password Reset, Apple Sign In)
|
||||
2. Property screens (List, Detail, Form, Join, Share, Manage Users)
|
||||
3. Task screens (List, Detail, Form, Complete, Actions, Kanban)
|
||||
4. Contractor screens (List, Detail, Form)
|
||||
5. Document screens (List, Detail, Form, Warranties)
|
||||
6. Profile screens (Profile, Settings, Notifications)
|
||||
7. Common components (Dialogs, Cards, Empty States, Loading)
|
||||
|
||||
### Phase 4: Create Translation Files
|
||||
|
||||
- Create es.json, fr.json, de.json, pt.json for Go API
|
||||
- Create values-es/, values-fr/, values-de/, values-pt/ for KMM
|
||||
- Add all language translations to iOS String Catalog
|
||||
|
||||
### Phase 5: Testing & Polish
|
||||
|
||||
- Test all screens in each language
|
||||
- Verify email rendering in each language
|
||||
- Test push notifications
|
||||
- Verify lookup data translations
|
||||
- Handle edge cases (long strings, RTL future-proofing)
|
||||
|
||||
---
|
||||
|
||||
## String Naming Convention
|
||||
|
||||
Use consistent keys across all platforms:
|
||||
|
||||
```
|
||||
<category>_<screen/feature>_<element>
|
||||
|
||||
Examples:
|
||||
auth_login_title
|
||||
auth_login_button
|
||||
properties_list_empty_title
|
||||
properties_form_field_name
|
||||
tasks_detail_button_complete
|
||||
common_button_save
|
||||
common_button_cancel
|
||||
error_network_timeout
|
||||
a11y_button_back
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Estimated String Counts
|
||||
|
||||
| Platform | Approximate Strings |
|
||||
|----------|---------------------|
|
||||
| Go API - Errors | ~100 |
|
||||
| Go API - Email templates | ~50 per language |
|
||||
| Go API - Push notifications | ~20 |
|
||||
| Go API - Lookup data | ~50 |
|
||||
| KMM/Android | ~500 |
|
||||
| iOS | ~500 |
|
||||
| **Total unique strings** | ~700-800 |
|
||||
|
||||
---
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
1. **Extract**: All English strings defined first
|
||||
2. **Export**: JSON (Go), XML (Android), .xcstrings (iOS)
|
||||
3. **Translate**: Use Lokalise, Crowdin, or manual translation
|
||||
4. **Import**: Place translated files in correct locations
|
||||
5. **Test**: Verify in each language
|
||||
Reference in New Issue
Block a user