localization.md

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):

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:

cp i18n/locales/en.json i18n/locales/es.json

2. Update Language Code

Change the language field:

{
  "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:

"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):

"address_count": {
  "one": "{count} address",
  "other": "{count} addresses"
}

Ukrainian (complex):

"address_count": {
  "one": "{count} адреса",
  "few": "{count} адреси",
  "other": "{count} адрес"
}

Arabic (very complex):

"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:

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:

"composer": {
  "title": "Redactar correo nuevo"  // Old
  "title": "Escribir nuevo correo"  // New
}

4. Rebuild and Test

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:

// ✅ 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:

// Button in composer
"send": "Enviar"

// Status message
"sent": "Enviado correctamente"

// Different contexts, different translations

Common Translation Keys

"common": {
  "yes": "Sí",
  "no": "No",
  "cancel": "Cancelar",
  "save": "Guardar",
  "delete": "Eliminar",
  "back": "Volver"
}

Relative Time

"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:

// 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.jsonlanguage = "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