Files
honeyDueAPI/docs/LOCALIZATION_PLAN.md
Trey t 7438dfd9b1 Fix timeout middleware panic on proxy/WebSocket routes and worker healthcheck
The TimeoutMiddleware wraps the response writer in *http.timeoutWriter which
doesn't implement http.Flusher. When the admin reverse proxy or WebSocket
upgrader tries to flush, it panics and crashes the container (502 Bad Gateway).
Skip timeout for /admin, /_next, and /ws routes.

Also fix the Dockerfile HEALTHCHECK to detect the worker process — the worker
has no HTTP server so the curl-based check always failed, marking it unhealthy.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 19:56:12 -06:00

413 lines
13 KiB
Markdown

# 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