Lazygit Internationalization (i18n) Architecture: TranslationSet and Runtime Merging
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.
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.
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:
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:
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:
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) 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 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
TranslationSetand 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
TranslationSetstruct inpkg/i18n/english.goprovides compile-time guarantees by defining a typed field for every user-facing string. - Automatic Fallback: The merging strategy using
mergo.Mergewithmergo.WithOverrideensures 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_jabberenables zero-configuration localization when users setlanguage: autoin 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →