@@ -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
@@ -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).