docs: add public and developer docs (#844)

Drew Smirnoff created

Change summary

docs/docs/index.md        |   1 
docs/docs/localization.md | 388 ++++++++++++++++++++++++++
i18n/README.md            | 609 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 998 insertions(+)

Detailed changes

docs/docs/index.md πŸ”—

@@ -19,6 +19,7 @@ Ready to dive in? Here are a few places to start:
 - πŸš€ [Installation Guide](.//installation.md) - Get Matcha running on your machine
 - πŸ“– [Usage & Shortcuts](./usage.md) - Learn how to navigate the interface
 - βš™οΈ [Configuration](./Configuration.md) - Set up your accounts and preferences
+- 🌍 [Localization](./localization.md) - Add or edit translations for your language
 
 ## Core Features
 

docs/docs/localization.md πŸ”—

@@ -0,0 +1,388 @@
+# Localization Guide
+
+Matcha uses a custom i18n (internationalization) system to support multiple languages. This guide explains how to add new translations or edit existing ones.
+
+## Changing Language
+
+Set your preferred language in the config file (`~/.config/matcha/config.json`):
+
+```toml
+language = "uk"  # or "es", "de", "fr", etc.
+```
+
+Or in Matcha Settings menu β†’ General β†’ Language.
+
+## File Structure
+
+```
+i18n/
+β”œβ”€β”€ locales/
+β”‚   β”œβ”€β”€ en.json       # English (base)
+β”‚   β”œβ”€β”€ uk.json       # Ukrainian
+β”‚   β”œβ”€β”€ es.json       # Spanish
+β”‚   └── ...
+└── languages/
+    β”œβ”€β”€ en.go         # English plural rules
+    β”œβ”€β”€ uk.go         # Ukrainian plural rules
+    └── ...
+```
+
+## Adding a New Translation
+
+### 1. Create Translation File
+
+Copy `i18n/locales/en.json` to `i18n/locales/[lang].json`:
+
+```bash
+cp i18n/locales/en.json i18n/locales/es.json
+```
+
+### 2. Update Language Code
+
+Change the `language` field:
+
+```json
+{
+  "language": "es",
+  "messages": {
+    ...
+  }
+}
+```
+
+### 3. Translate All Strings
+
+Translate all message values while preserving:
+- JSON structure
+- Placeholder variables: `{count}`, `{latest}`, `{current}`, etc.
+- Technical terms: S/MIME, PGP, IMAP, SMTP, etc.
+- Commands and file paths
+
+**Example:**
+
+```json
+"composer": {
+  "title": "Redactar nuevo correo",
+  "from": "De",
+  "to_placeholder": "Ingrese direcciones de correo de destinatarios.",
+  "send": "Enviar"
+}
+```
+
+### 4. Handle Plural Forms
+
+Different languages have different plural rules. Matcha supports:
+
+- `one` - Singular (1)
+- `few` - Few items (2-4 in some languages)
+- `many` - Many items (5+ in some languages)
+- `other` - Default/all other counts
+
+**English (simple):**
+```json
+"address_count": {
+  "one": "{count} address",
+  "other": "{count} addresses"
+}
+```
+
+**Ukrainian (complex):**
+```json
+"address_count": {
+  "one": "{count} адрСса",
+  "few": "{count} адрСси",
+  "other": "{count} адрСс"
+}
+```
+
+**Arabic (very complex):**
+```json
+"hours_ago": {
+  "zero": "Ω…Ω†Ψ° {count} Ψ³Ψ§ΨΉΨ©",
+  "one": "Ω…Ω†Ψ° Ψ³Ψ§ΨΉΨ© واحدة",
+  "two": "Ω…Ω†Ψ° Ψ³Ψ§ΨΉΨͺΩŠΩ†",
+  "few": "Ω…Ω†Ψ° {count} Ψ³Ψ§ΨΉΨ§Ψͺ",
+  "many": "Ω…Ω†Ψ° {count} Ψ³Ψ§ΨΉΨ©",
+  "other": "Ω…Ω†Ψ° {count} Ψ³Ψ§ΨΉΨ©"
+}
+```
+
+### 5. Register Language (Optional)
+
+If adding a completely new language not in `i18n/languages/`, create the plural rules file:
+
+**i18n/languages/es.go:**
+```go
+package languages
+
+import "github.com/floatpane/matcha/i18n"
+
+func init() {
+	i18n.RegisterLanguage(&i18n.Locale{
+		Code:       "es",
+		Name:       "Spanish",
+		NativeName: "EspaΓ±ol",
+		Direction:  "ltr",
+		PluralFunc: i18n.SpanishPlural,
+	})
+}
+```
+
+Plural function already exists in `i18n/plural_rules.go` for common languages.
+
+### 6. Test Translation
+
+1. Build matcha: `go build`
+2. Set language in config: `language = "es"`
+3. Restart matcha
+4. Verify all UI elements display translated text
+
+## Editing Existing Translations
+
+### 1. Find Translation File
+
+Open `i18n/locales/[lang].json` for your language.
+
+### 2. Locate Translation Key
+
+Translation keys follow dot notation matching UI structure:
+
+- `composer.*` - Email composer screen
+- `inbox.*` - Inbox view
+- `settings.*` - Settings menu
+- `settings_general.*` - General settings
+- `settings_accounts.*` - Account settings
+- `choice.*` - Main menu
+- `common.*` - Shared UI elements
+
+**Example key paths:**
+```
+composer.title
+inbox.all_accounts
+settings_general.language
+settings_encryption.password_label
+```
+
+### 3. Update Translation
+
+Edit the string value:
+
+```json
+"composer": {
+  "title": "Redactar correo nuevo"  // Old
+  "title": "Escribir nuevo correo"  // New
+}
+```
+
+### 4. Rebuild and Test
+
+```bash
+go build
+./matcha
+```
+
+## Translation Guidelines
+
+### Do Translate:
+βœ… All UI text visible to users  
+βœ… Help text and tips  
+βœ… Button labels  
+βœ… Menu items  
+βœ… Error messages shown in UI  
+βœ… Status messages  
+
+### Don't Translate:
+❌ Error logs (backend)  
+❌ Debug messages  
+❌ Protocol names (IMAP, SMTP, PGP, S/MIME)  
+❌ File paths  
+❌ Environment variables  
+❌ Command names (`matcha update`)  
+❌ Code/technical identifiers  
+
+### Placeholder Variables
+
+Keep variables intact:
+
+```json
+// βœ… Correct
+"update_available": "Mise Γ  jour disponible: {latest} (installΓ©: {current})"
+
+// ❌ Wrong - renamed variable
+"update_available": "Mise Γ  jour disponible: {derniere} (installΓ©: {actuel})"
+
+// ❌ Wrong - removed variable
+"update_available": "Mise Γ  jour disponible (installΓ©)"
+```
+
+### Context-Aware Translation
+
+Some keys need context:
+
+```json
+// Button in composer
+"send": "Enviar"
+
+// Status message
+"sent": "Enviado correctamente"
+
+// Different contexts, different translations
+```
+
+## Common Translation Keys
+
+### Navigation
+```json
+"common": {
+  "yes": "SΓ­",
+  "no": "No",
+  "cancel": "Cancelar",
+  "save": "Guardar",
+  "delete": "Eliminar",
+  "back": "Volver"
+}
+```
+
+### Relative Time
+```json
+"inbox": {
+  "just_now": "Ahora mismo",
+  "minute_ago": {
+    "one": "Hace {count} minuto",
+    "other": "Hace {count} minutos"
+  },
+  "hour_ago": {
+    "one": "Hace {count} hora",
+    "other": "Hace {count} horas"
+  }
+}
+```
+
+## Plural Rules Reference
+
+### English, Spanish, Portuguese
+```
+one:   1
+other: 0, 2-∞
+```
+
+### French
+```
+one:   0, 1
+other: 2-∞
+```
+
+### German
+```
+one:   1
+other: 0, 2-∞
+```
+
+### Russian, Ukrainian
+```
+one:   1, 21, 31, 41...
+few:   2-4, 22-24, 32-34...
+other: 0, 5-20, 25-30...
+```
+
+### Polish
+```
+one:   1
+few:   2-4, 22-24, 32-34... (not 12-14)
+many:  0, 5-21, 25-31...
+other: fractions
+```
+
+### Arabic
+```
+zero:  0
+one:   1
+two:   2
+few:   3-10
+many:  11-99
+other: 100+, fractions
+```
+
+### Japanese, Chinese
+```
+other: all numbers (no plural distinction)
+```
+
+## Testing Checklist
+
+When adding/editing translations:
+
+- [ ] All UI screens display in target language
+- [ ] Plural forms work correctly (test with 0, 1, 2, 5, 21 items)
+- [ ] Variable interpolation works (`{count}`, `{latest}`, etc.)
+- [ ] No English text visible (except technical terms)
+- [ ] Help text fits in UI (not truncated)
+- [ ] Special characters display correctly
+- [ ] RTL languages render properly (Arabic)
+
+## Contributing Translations
+
+1. Fork the repository
+2. Add/edit translation file in `i18n/locales/`
+3. Test thoroughly
+4. Submit pull request with:
+   - Translation file changes
+   - Screenshots showing translated UI
+   - Note about plural form testing
+
+## Dynamic Language Switching
+
+Language changes currently require restart. To make dynamic:
+
+1. Save language to config
+2. Call `i18n.GetManager().SetLanguage(lang)`
+3. Trigger full UI re-render
+
+**Implementation:**
+
+```go
+// In settings handler
+func (m *Settings) changeLanguage(newLang string) tea.Cmd {
+    m.cfg.Language = newLang
+    config.SaveConfig(m.cfg)
+    i18n.GetManager().SetLanguage(newLang)
+    
+    // Force complete UI rebuild
+    return func() tea.Msg {
+        return LanguageChangedMsg{Language: newLang}
+    }
+}
+```
+
+Full dynamic switching requires rebuilding all TUI models with new translations.
+
+## Troubleshooting
+
+### Translation Not Showing
+
+1. Check language code matches file name (`uk.json` β†’ `language = "uk"`)
+2. Verify JSON syntax is valid
+3. Rebuild: `go build`
+4. Clear cache: `rm -rf ~/.cache/matcha`
+5. Restart matcha
+
+### Missing Translations
+
+If key missing, falls back to:
+1. Base language (English)
+2. Translation key itself (e.g., `composer.title`)
+
+Check logs for fallback warnings.
+
+### Plural Forms Not Working
+
+1. Verify plural rules defined for language in `i18n/plural_rules.go`
+2. Check JSON structure matches expected forms (`one`, `few`, `many`, `other`)
+3. Use `tn()` function in code, not `t()`
+
+## Reference
+
+- Translation files: `i18n/locales/*.json`
+- Plural rules: `i18n/plural_rules.go`
+- Language registry: `i18n/languages/*.go`
+- Unicode CLDR: https://cldr.unicode.org/index/cldr-spec/plural-rules

i18n/README.md πŸ”—

@@ -0,0 +1,609 @@
+# i18n - Custom Internationalization Library
+
+Custom-built internationalization system for matcha email client. Zero external i18n dependencies - everything implemented from scratch.
+
+## Table of Contents
+
+- [Architecture Overview](#architecture-overview)
+- [Directory Structure](#directory-structure)
+- [Core Components](#core-components)
+  - [Translation Manager](#translation-manager)
+  - [Bundle System](#bundle-system)
+  - [Localizer](#localizer)
+  - [Plural Rules Engine](#plural-rules-engine)
+  - [Template System](#template-system)
+  - [Language Registry](#language-registry)
+- [Data Flow](#data-flow)
+- [Translation File Format](#translation-file-format)
+- [Plural Forms](#plural-forms)
+- [Caching Strategy](#caching-strategy)
+- [Language Detection](#language-detection)
+- [Adding/Editing Languages](#addingediting-languages)
+
+## Architecture Overview
+
+```
+β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+β”‚                      Application Layer                      β”‚
+β”‚              (TUI components call t(), tn(), tpl())         β”‚
+β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
+                           β”‚
+β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+β”‚                    Global Manager                           β”‚
+β”‚  β€’ Singleton instance                                       β”‚
+β”‚  β€’ Current language state                                   β”‚
+β”‚  β€’ Localizer instances                                      β”‚
+β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
+                           β”‚
+         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+         β”‚                 β”‚                 β”‚
+β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
+β”‚     Bundle      β”‚ β”‚  Localizer β”‚ β”‚     Cache      β”‚
+β”‚  All messages   β”‚ β”‚  Per-lang  β”‚ β”‚  Translations  β”‚
+β”‚  All locales    β”‚ β”‚  instance  β”‚ β”‚  Rendered text β”‚
+β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
+         β”‚                β”‚
+         β”‚         β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+         β”‚         β”‚   Pluralizer    β”‚
+         β”‚         β”‚  Plural rules   β”‚
+         β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
+         β”‚
+β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
+β”‚                   Translation Files                         β”‚
+β”‚            locales/en.json, locales/uk.json, ...            β”‚
+β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
+```
+
+## Directory Structure
+
+```
+i18n/
+β”œβ”€β”€ manager.go           # Global singleton manager
+β”œβ”€β”€ bundle.go            # Translation bundle storage
+β”œβ”€β”€ locale.go            # Locale representation
+β”œβ”€β”€ message.go           # Message structure (with plural forms)
+β”œβ”€β”€ localizer.go         # Per-language translation engine
+β”œβ”€β”€ loader.go            # Load & embed translation files
+β”œβ”€β”€ parser.go            # JSON parser for translation files
+β”œβ”€β”€ plural_rules.go      # Plural rule functions for all languages
+β”œβ”€β”€ pluralizer.go        # Plural form selection logic
+β”œβ”€β”€ template.go          # Simple {var} template engine
+β”œβ”€β”€ interpolator.go      # Variable interpolation in strings
+β”œβ”€β”€ cache.go             # In-memory translation cache
+β”œβ”€β”€ fallback.go          # Fallback chain (uk β†’ en)
+β”œβ”€β”€ detector.go          # Auto-detect user language
+β”œβ”€β”€ registry.go          # Available languages registry
+β”œβ”€β”€ formatter.go         # Number formatting per locale
+β”œβ”€β”€ date_formatter.go    # Date/time formatting per locale
+β”œβ”€β”€ context.go           # Context keys for locale passing
+β”œβ”€β”€ errors.go            # Error types
+β”œβ”€β”€ init.go              # Package initialization
+β”œβ”€β”€ embed.go             # Embed translation files in binary
+β”œβ”€β”€ languages/           # Language-specific implementations
+β”‚   β”œβ”€β”€ base.go          # Base language interface
+β”‚   β”œβ”€β”€ en.go            # English locale registration
+β”‚   β”œβ”€β”€ uk.go            # Ukrainian locale registration
+β”‚   β”œβ”€β”€ es.go            # Spanish locale registration
+β”‚   └── ...              # Other languages
+└── locales/             # Translation JSON files
+    β”œβ”€β”€ en.json          # English translations (base)
+    β”œβ”€β”€ uk.json          # Ukrainian translations
+    └── ...              # Other language files
+```
+
+## Core Components
+
+### Translation Manager
+
+**File:** `manager.go`
+
+Global singleton that coordinates all i18n operations. Provides three main translation functions:
+
+- `T(key)` - Simple translation lookup
+- `Tn(key, count, data)` - Translation with pluralization
+- `Tpl(key, data)` - Translation with template variables
+
+```go
+// Usage in TUI components
+import "github.com/floatpane/matcha/i18n"
+
+func render() string {
+    title := i18n.GetManager().T("composer.title")
+    count := i18n.GetManager().Tn("inbox.unread", 5, map[string]interface{}{
+        "count": 5,
+    })
+    return title + " - " + count
+}
+```
+
+Maintains:
+- Current active language
+- Bundle with all loaded translations
+- Localizer instances per language
+- Translation cache
+
+### Bundle System
+
+**File:** `bundle.go`
+
+Central storage for all translation messages across all languages. Thread-safe with RWMutex.
+
+```go
+type Bundle struct {
+    defaultLang string
+    messages    map[string]MessageMap  // lang β†’ message ID β†’ Message
+    locales     map[string]*Locale     // lang β†’ Locale info
+    mu          sync.RWMutex
+}
+```
+
+Responsibilities:
+- Store translations for all languages
+- Retrieve messages by language + key
+- Register locale definitions
+- List available languages
+
+### Localizer
+
+**File:** `localizer.go`
+
+Per-language translation engine. Each active language gets one localizer instance.
+
+```go
+type Localizer struct {
+    lang    string
+    bundle  *Bundle
+    locale  *Locale
+    cache   *Cache
+}
+```
+
+Core methods:
+- `Localize(messageID)` - Basic lookup
+- `LocalizePlural(messageID, count, data)` - With plural rules
+- `LocalizeTemplate(messageID, data)` - With variable substitution
+
+Handles:
+- Message lookup with fallback
+- Plural form selection
+- Template interpolation
+- Result caching
+
+### Plural Rules Engine
+
+**Files:** `plural_rules.go`, `pluralizer.go`
+
+Implements CLDR plural rules for all supported languages.
+
+```go
+type PluralForm int
+
+const (
+    Zero PluralForm = iota  // 0 items
+    One                     // 1 item
+    Two                     // 2 items (Arabic, Welsh)
+    Few                     // 2-4 items (Slavic languages)
+    Many                    // 5+ items (Slavic languages)
+    Other                   // Default/fallback
+)
+```
+
+Each language has specific rules:
+
+**English/Spanish:** Simple (one/other)
+```go
+func EnglishPlural(n int) PluralForm {
+    if n == 1 {
+        return One
+    }
+    return Other
+}
+```
+
+**Ukrainian/Russian:** Complex (one/few/many)
+```go
+func UkrainianPlural(n int) PluralForm {
+    mod10 := n % 10
+    mod100 := n % 100
+    
+    if mod10 == 1 && mod100 != 11 {
+        return One     // 1, 21, 31, 41...
+    }
+    if mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14) {
+        return Few     // 2-4, 22-24, 32-34...
+    }
+    return Many        // 0, 5-20, 25-30...
+}
+```
+
+**Arabic:** Very complex (zero/one/two/few/many/other)
+
+### Template System
+
+**Files:** `template.go`, `interpolator.go`
+
+Simple variable substitution using `{variable}` syntax.
+
+```go
+template := "Update available: {latest} (current: {current})"
+data := map[string]interface{}{
+    "latest": "1.5.0",
+    "current": "1.4.0",
+}
+result := Interpolate(template, data)
+// β†’ "Update available: 1.5.0 (current: 1.4.0)"
+```
+
+**Design philosophy:** Keep simple - no complex logic, just variable replacement. Complex formatting done in Go code before passing to templates.
+
+### Language Registry
+
+**File:** `registry.go`
+
+Global registry of all available languages. Languages self-register on init.
+
+```go
+// In languages/uk.go
+func init() {
+    i18n.RegisterLanguage(&i18n.Locale{
+        Code:       "uk",
+        Name:       "Ukrainian",
+        NativeName: "Π£ΠΊΡ€Π°Ρ—Π½ΡΡŒΠΊΠ°",
+        Direction:  "ltr",
+        PluralFunc: i18n.UkrainianPlural,
+    })
+}
+```
+
+Registry provides:
+- `GetLanguage(code)` - Lookup locale by code
+- `AvailableLanguages()` - List all registered locales
+- `LanguageCodes()` - List all language codes
+
+## Data Flow
+
+### Translation Lookup Flow
+
+```
+1. TUI calls t("composer.title")
+              ↓
+2. Manager.T() β†’ GetLocalizer(currentLang)
+              ↓
+3. Localizer checks cache
+   β€’ Cache hit? β†’ return cached value
+   β€’ Cache miss? β†’ continue
+              ↓
+4. Bundle.GetMessage(lang, "composer.title")
+   β€’ Found? β†’ return Message
+   β€’ Not found? β†’ try fallback chain (uk β†’ en)
+              ↓
+5. Return message.One (singular form)
+              ↓
+6. Cache result for future lookups
+              ↓
+7. Return translated string to TUI
+```
+
+### Pluralization Flow
+
+```
+1. TUI calls tn("inbox.unread", 5, {"count": 5})
+              ↓
+2. Manager.Tn() β†’ GetLocalizer(currentLang)
+              ↓
+3. Get Message from bundle
+              ↓
+4. Call locale.PluralFunc(5)
+   β€’ English: 5 β†’ Other
+   β€’ Ukrainian: 5 β†’ Many
+   β€’ Arabic: 5 β†’ Few
+              ↓
+5. Select appropriate form from Message:
+   β€’ message.One (n=1)
+   β€’ message.Few (n=2-4 in Ukrainian)
+   β€’ message.Many (n=5+ in Ukrainian)
+   β€’ message.Other (fallback)
+              ↓
+6. Interpolate {count} β†’ "5"
+              ↓
+7. Return "5 листів" (Ukrainian) or "5 emails" (English)
+```
+
+## Translation File Format
+
+**Location:** `locales/*.json`
+
+**Structure:**
+
+```json
+{
+  "language": "uk",
+  "messages": {
+    "common": {
+      "yes": "Π’Π°ΠΊ",
+      "no": "Ні",
+      "save": "Π—Π±Π΅Ρ€Π΅Π³Ρ‚ΠΈ"
+    },
+    "composer": {
+      "title": "Написати Π½ΠΎΠ²ΠΈΠΉ лист",
+      "send": "Надіслати",
+      "from": "Π’Ρ–Π΄"
+    },
+    "inbox": {
+      "unread": {
+        "one": "{count} Π½Π΅ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½ΠΈΠΉ лист",
+        "few": "{count} Π½Π΅ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½Ρ– листи",
+        "other": "{count} Π½Π΅ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Π½ΠΈΡ… листів"
+      }
+    }
+  }
+}
+```
+
+**Nested structure:** Dot notation for keys (`composer.title`, `inbox.unread`)
+
+**Plural objects:** When value is object with `one`/`few`/`many`/`other`, it's treated as plural form.
+
+## Plural Forms
+
+Languages use different plural categories:
+
+| Language | Forms Used | Example (emails) |
+|----------|-----------|------------------|
+| English | one, other | 1 email, 2 emails |
+| Spanish | one, other | 1 correo, 2 correos |
+| German | one, other | 1 E-Mail, 2 E-Mails |
+| French | one, other | 0 courriel, 1 courriel, 2 courriels |
+| Ukrainian | one, few, other | 1 лист, 2 листи, 5 листів |
+| Russian | one, few, other | 1 письмо, 2 письма, 5 писСм |
+| Polish | one, few, many, other | 1 e-mail, 2 e-maile, 5 e-maili |
+| Arabic | zero, one, two, few, many, other | Complex rules |
+| Japanese | other | All numbers use same form |
+| Chinese | other | All numbers use same form |
+
+## Caching Strategy
+
+**File:** `cache.go`
+
+Simple in-memory string cache with RWMutex for thread safety.
+
+**Cache key format:** `{lang}:{messageID}:{count}:{hash(data)}`
+
+**Why cache?**
+- Translation lookup involves: map lookups, plural calculation, interpolation
+- Typical UI has ~100 translated strings
+- Redrawing UI on every keypress = wasted CPU
+- Cache hit = instant string return
+
+**Cache invalidation:** Cache cleared when language changes via `SetLanguage()`.
+
+**No size limit:** Translation cache is small (~KB for entire UI) and only grows to unique message combinations actually used.
+
+## Language Detection
+
+**File:** `detector.go`
+
+Detection priority:
+
+1. **Config file:** `language = "uk"` in `~/.config/matcha/config.toml`
+2. **Environment:** `LANG`, `LC_ALL`, `LC_MESSAGES`
+3. **System locale:** OS-specific detection
+4. **Default:** Falls back to `"en"`
+
+```go
+func DetectLanguage(cfg *config.Config) string {
+    // 1. Check config
+    if cfg.Language != "" && isValidLanguage(cfg.Language) {
+        return cfg.Language
+    }
+    
+    // 2. Check environment
+    if lang := detectFromEnv(); lang != "" {
+        return lang
+    }
+    
+    // 3. Check system
+    if lang := detectFromSystem(); lang != "" {
+        return lang
+    }
+    
+    // 4. Default
+    return "en"
+}
+```
+
+Normalizes language codes: `en_US.UTF-8` β†’ `en`, `uk_UA` β†’ `uk`
+
+## Adding/Editing Languages
+
+### Adding a New Language
+
+**1. Create translation file:**
+
+```bash
+cd i18n/locales
+cp en.json xx.json  # Replace 'xx' with language code (ISO 639-1)
+```
+
+**2. Update language code:**
+
+```json
+{
+  "language": "xx",
+  "messages": { ... }
+}
+```
+
+**3. Translate all strings:**
+
+- Keep JSON structure identical to `en.json`
+- Translate all message values
+- Preserve placeholders: `{count}`, `{latest}`, `{current}`
+- Don't translate technical terms: S/MIME, PGP, IMAP, SMTP
+- Don't translate commands: `matcha update`
+
+**4. Handle plural forms:**
+
+Check plural rules for your language at [CLDR Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules).
+
+Add plural forms as needed:
+
+```json
+"hours_ago": {
+  "one": "{count} hour ago",
+  "other": "{count} hours ago"
+}
+```
+
+For complex plurals (Ukrainian, Arabic, Polish):
+
+```json
+"address_count": {
+  "one": "{count} адрСса",
+  "few": "{count} адрСси",
+  "other": "{count} адрСс"
+}
+```
+
+**5. Register language (if not already in `languages/`):**
+
+Create `languages/xx.go`:
+
+```go
+package languages
+
+import "github.com/floatpane/matcha/i18n"
+
+func init() {
+    i18n.RegisterLanguage(&i18n.Locale{
+        Code:       "xx",
+        Name:       "YourLanguage",
+        NativeName: "YourLanguageInNativeScript",
+        Direction:  "ltr",  // or "rtl" for Arabic, Hebrew, etc.
+        PluralFunc: i18n.YourLanguagePlural,
+    })
+}
+```
+
+If plural function doesn't exist in `plural_rules.go`, add it:
+
+```go
+// In plural_rules.go
+func YourLanguagePlural(n int) PluralForm {
+    // Implement CLDR rules for your language
+    if n == 1 {
+        return One
+    }
+    return Other
+}
+```
+
+**6. Test:**
+
+```bash
+go build
+# Edit config: language = "xx"
+./matcha
+```
+
+Verify:
+- All UI elements translated
+- Plural forms work (test with 0, 1, 2, 5, 21 items)
+- Variables interpolate correctly
+- No English leaks through
+
+### Editing Existing Translations
+
+**1. Open translation file:**
+
+```bash
+vi i18n/locales/uk.json
+```
+
+**2. Find key using dot notation:**
+
+Translation keys follow UI structure:
+- `composer.*` - Email composer
+- `inbox.*` - Inbox view  
+- `settings_general.*` - General settings
+- `common.*` - Shared elements
+
+**3. Update translation:**
+
+```json
+{
+  "composer": {
+    "title": "Old translation"  // Change this
+  }
+}
+```
+
+**4. Rebuild and test:**
+
+```bash
+go build
+./matcha
+```
+
+Language changes apply instantly (no restart needed).
+
+### Translation Guidelines
+
+**Do translate:**
+- All visible UI text
+- Button labels
+- Menu items
+- Help text
+- Tips
+- Error messages shown to user
+- Status messages
+
+**Don't translate:**
+- Backend error logs
+- Debug messages
+- Protocol names (IMAP, SMTP, S/MIME, PGP)
+- File paths (`~/.config/matcha/`)
+- Environment variables (`$EDITOR`)
+- Commands (`matcha update`)
+- Technical identifiers
+
+**Variable placeholders:**
+
+Always keep variables unchanged:
+
+```json
+// βœ… Correct
+"update_available": "ДоступнС оновлСння: {latest} (встановлСно: {current})"
+
+// ❌ Wrong - renamed variable
+"update_available": "ДоступнС оновлСння: {останній} (встановлСно: {ΠΏΠΎΡ‚ΠΎΡ‡Π½ΠΈΠΉ})"
+```
+
+### Testing Checklist
+
+- [ ] All screens display in target language
+- [ ] Test plural forms with different counts (0, 1, 2, 5, 21)
+- [ ] Variables interpolate correctly
+- [ ] No untranslated English text (except technical terms)
+- [ ] Text fits in UI (not truncated)
+- [ ] Special characters render properly
+- [ ] RTL languages display correctly (Arabic, Hebrew)
+- [ ] Date/time formats work
+- [ ] Help text makes sense in context
+
+### Contributing Translations
+
+1. Fork repository
+2. Add/edit translation file in `i18n/locales/`
+3. Test thoroughly with checklist above
+4. Submit pull request with:
+   - Translation file
+   - Screenshots of translated UI
+   - Note about plural form testing
+   - List any technical challenges
+
+**Translation quality > completeness.** Better to have 80% high-quality translation than 100% machine-translated text.
+
+---
+
+**For more details on using translations in code, see:** [Documentation](https://docs.matcha.floatpane.com/localization).