# Lazygit Internationalization (i18n) Architecture: TranslationSet and Runtime Merging

> Explore lazygit's i18n architecture. Learn how TranslationSet and runtime merging with JSON files enable type-safe translations and automatic fallback.

- Repository: [Jesse Duffield/lazygit](https://github.com/jesseduffield/lazygit)
- Tags: architecture
- Published: 2026-03-02

---

**lazygit's i18n system uses a type-safe `TranslationSet` struct that merges English defaults with language-specific JSON files at runtime, ensuring automatic fallback for untranslated strings.**

lazygit's internationalization architecture is built around a centralized translation model that prioritizes compile-time type safety while supporting dynamic language configuration. Implemented primarily within the `pkg/i18n` directory, the system relies on a Go struct containing approximately 800 string fields—one for every user-facing message—which is populated with English defaults and optionally overlaid with embedded JSON translation files. This approach guarantees that UI components always have valid text to display, even when specific translations are incomplete or missing.

## Core Data Type – The TranslationSet Struct

At the heart of lazygit's localization system lies the **`TranslationSet`** struct defined in **[pkg/i18n/english.go]**. This struct acts as a single source of truth for all user-visible strings, containing a typed field for every message used throughout the application.

```go
type TranslationSet struct {
    NotEnoughSpace string
    DiffTitle      string
    // … ~800 more fields, one per UI string
}

```

The **English defaults** are returned by `EnglishTranslationSet()`, which builds a `TranslationSet` literal populated with English text. This function serves as the foundation for all localization operations, ensuring that every string has at least an English value available.

## Loading and Merging Translations

The entry point for translation initialization is **`NewTranslationSetFromConfig`** in **[pkg/i18n/i18n.go]**. This function orchestrates language detection, file loading, and the merging strategy that enables fallback to English.

```go
func NewTranslationSetFromConfig(log *logrus.Entry, configLanguage string) (*TranslationSet, error) {
    // 1. discover the list of supported language codes (all JSON files)
    languageCodes, _ := getSupportedLanguageCodes()

    // 2. "auto" → detect OS language using jibber_jabber
    if configLanguage == "auto" {
        language := detectLanguage(jibber_jabber.DetectIETF)
        // pick the first supported code that matches the detected one
        for _, code := range languageCodes {
            if strings.HasPrefix(language, code) {
                return newTranslationSet(log, code)
            }
        }
        // fallback to English if not supported
        return EnglishTranslationSet(), nil
    }

    // 3. explicit language request
    switch configLanguage {
    case "en":
        return EnglishTranslationSet(), nil
    default:
        if slices.Contains(languageCodes, configLanguage) {
            return newTranslationSet(log, configLanguage)
        }
        return nil, errors.New("Language not found: " + configLanguage)
    }
}

```

The `getSupportedLanguageCodes` function reads the embedded directory `translations/*.json`—declared via a **`//go:embed`** directive at the top of the file—and returns the file base-names without the `.json` suffix. This dynamic discovery allows the system to recognize new languages automatically when JSON files are added to the repository.

### Auto-Detection and Language Selection

When the user configures `"auto"` as the language, lazygit invokes **`detectLanguage`**, a thin wrapper around `jibber_jabber.DetectIETF` to determine the operating system's locale. If the detector fails, it returns the POSIX `"C"` locale, which the caller treats as English. The system then iterates through supported language codes, selecting the first match based on prefix comparison (e.g., `zh-CN` matching `zh`), or falling back to English if no compatible translation exists.

### Merging Language Files with English Defaults

The **`newTranslationSet`** function implements the fallback strategy by merging language-specific data onto the English base:

```go
func newTranslationSet(log *logrus.Entry, language string) (*TranslationSet, error) {
    log.Info("language: " + language)

    baseSet := EnglishTranslationSet() // start with English
    if language != "en" {
        // read the JSON file that contains only the translated strings
        translationSet, err := readLanguageFile(language)
        if err != nil {
            return nil, err
        }
        // merge, overriding only the fields present in the JSON
        err = mergo.Merge(baseSet, *translationSet, mergo.WithOverride)
        if err != nil {
            return nil, err
        }
    }
    return baseSet, nil
}

```

The **`readLanguageFile`** function loads the JSON from the embedded filesystem (`embedFS.ReadFile`) and unmarshals it into a `TranslationSet`. Using **`mergo.Merge`** with the `mergo.WithOverride` option ensures that language-specific values replace English defaults only for fields present in the JSON file. Any missing translations automatically retain their English values, creating a seamless partial-localization experience.

## UI Integration and Context Propagation

When the main GUI initializes in **[pkg/gui/gui.go]** (around line 445), the translation set is instantiated once and stored in the common context:

```go
tr, err := i18n.NewTranslationSetFromConfig(gui.Log, userConfig.Gui.Language)

```

The resulting **`*i18n.TranslationSet`** (`tr`) is passed to every presentation function via the `Common.Tr` field. UI components reference these fields directly when rendering strings:

```go
func (c *BranchContext) GetDisplayStrings(tr *i18n.TranslationSet) []string {
    return []string{
        tr.BranchesTitle,
        // …
    }
}

```

This propagation pattern ensures consistent localization across all views while maintaining compile-time safety—attempting to reference a non-existent translation field results in a compilation error rather than a runtime missing string.

## Generating and Updating Translation Files

The repository includes a helper binary at **[cmd/i18n/main.go]** that automates translation file maintenance. This tool can dump the current `EnglishTranslationSet()` to a JSON file ([`en.json`](https://github.com/jesseduffield/lazygit/blob/main/en.json)) and merge it with existing translations, ensuring that new UI strings are automatically available for localization. When adding support for a new language, developers simply copy the generated [`en.json`](https://github.com/jesseduffield/lazygit/blob/main/en.json) to `pkg/i18n/translations/<lang>.json`, translate the values, and commit the file—the `NewTranslationSetFromConfig` logic automatically discovers and loads it on the next application start.

Key files in the i18n architecture include:

- **[pkg/i18n/i18n.go]** – Loader, auto-detect, and merge logic
- **[pkg/i18n/english.go]** – Definition of `TranslationSet` and English defaults
- **pkg/i18n/translations/*.json** – Language-specific JSON files (e.g., **[zh-CN.json]**, **[nl.json]**)
- **[cmd/i18n/main.go]** – Helper CLI to emit and merge JSON translation files
- **[pkg/gui/gui.go]** – Instantiates the translation set at startup and stores it in the common context

## Summary

- **Type-Safe Structure**: The `TranslationSet` struct in [`pkg/i18n/english.go`](https://github.com/jesseduffield/lazygit/blob/main/pkg/i18n/english.go) provides compile-time guarantees by defining a typed field for every user-facing string.
- **Automatic Fallback**: The merging strategy using `mergo.Merge` with `mergo.WithOverride` ensures untranslated strings automatically fall back to English defaults.
- **Embedded Resources**: Translation files are embedded using `//go:embed`, eliminating runtime filesystem dependencies and packaging all languages into the binary.
- **Dynamic Discovery**: The system automatically detects supported languages by reading embedded JSON filenames, requiring no code changes to add new locales.
- **Auto-Detection**: OS language detection via `jibber_jabber` enables zero-configuration localization when users set `language: auto` in their config.

## Frequently Asked Questions

### How does lazygit handle missing translations for new UI strings?

When a language-specific JSON file lacks a translation for a newly added UI string, the system automatically retains the English value from the base `TranslationSet`. Because `newTranslationSet` starts with `EnglishTranslationSet()` and only overrides fields present in the JSON file via `mergo.Merge`, missing keys simply preserve their English defaults without causing runtime errors or blank strings.

### Can lazygit automatically detect my operating system's language?

Yes. When the configuration specifies `language: auto`, lazygit invokes `jibber_jabber.DetectIETF` through the `detectLanguage` wrapper to determine the OS locale. The system then matches this detected language against available translation files; if no match exists or detection fails (returning the POSIX `"C"` locale), the application gracefully falls back to English.

### How do I add a new language to lazygit?

To add a new language, create a JSON file at `pkg/i18n/translations/<lang>.json` containing the translated strings (using the helper tool in [`cmd/i18n/main.go`](https://github.com/jesseduffield/lazygit/blob/main/cmd/i18n/main.go) to generate a template from the English set). Place only the translated fields in the file—omitting a field triggers automatic fallback to English. Once committed, `getSupportedLanguageCodes` automatically discovers the file, making the language available via the `language` configuration option without modifying the loader logic.

### Are translations compiled into the lazygit binary?

Yes. The `//go:embed translations/*.json` directive embeds all translation files directly into the compiled binary. At runtime, `readLanguageFile` accesses these resources through an embedded filesystem (`embedFS`), ensuring that lazygit operates without external file dependencies and that all localizations are available immediately on first launch.