Skip to content

feat(i18n): multilingual setup wizard — first-run language picker + gettext (zh/ja/es/fr/de)#20736

Open
songchao0421 wants to merge 1 commit into
NousResearch:mainfrom
songchao0421:feat/setup-i18n-clean
Open

feat(i18n): multilingual setup wizard — first-run language picker + gettext (zh/ja/es/fr/de)#20736
songchao0421 wants to merge 1 commit into
NousResearch:mainfrom
songchao0421:feat/setup-i18n-clean

Conversation

@songchao0421

@songchao0421 songchao0421 commented May 6, 2026

Copy link
Copy Markdown

Summary

Internationalisation for the interactive hermes setup wizard using Python gettext.

Background & Motivation

Hermes Agent is used by developers worldwide. The very first command a new user runs is almost always hermes setup — an interactive wizard that configures the model provider, terminal backend, messaging gateway, tool chain, and agent settings. Until now, every prompt, explanation, and error message in that wizard was hard-coded in English. For non-English speakers, this meant they had to complete a series of fairly technical setup steps — API keys, model parameters, gateway configs — in a language they might not be comfortable with, before they had even got the agent running.

This PR lowers that barrier by making the entire setup wizard speak the user's language.

What this PR adds

  • First-run language picker — shown as the very first prompt when hermes setup is run on a fresh install. The chosen locale is persisted to config.yaml under display.language.
  • Full gettext-based translation system — zero external dependencies (stdlib only), industry standard, and compatible with Weblate / Crowdin.
  • 300+ user-facing strings wrapped with _() — all section headers, prompts, info messages, warnings, success messages, and tooltips in setup.py are now translatable.
  • 6 language packs shipped out of the box.

Language coverage (initial batch)

Code Language Status
en English Fallback
zh_CN Simplified Chinese Ready
ja_JP Japanese Ready
es_ES Spanish Ready
fr_FR French Ready
de_DE German Ready

Adding a new language is trivial: create hermes_cli/i18n/<locale>/LC_MESSAGES/setup.po, translate the msgstr entries, add one line to languages.py — no Python code changes required.

Why gettext?

We evaluated three common approaches:

  1. JSON/YAML dictionaries — Simple, but splits code and text into two files, has no standard plural-form handling, and cannot be consumed directly by translation platforms.
  2. Third-party i18n libraries (e.g. Babel, Fluent) — Powerful, but introduce extra dependencies and supply-chain risk. Hermes CLI follows a minimal-dependency policy.
  3. Python stdlib gettext — Zero dependencies, thirty-year industry track record, .po/.mo/.pot format supported by every major translation platform, and _() markers live inline next to the strings they translate so developers cannot forget them.

Runtime architecture

hermes_cli/i18n/
├── __init__.py              # gettext loader + _() callable
├── languages.py             # supported-locale table
├── setup.pot                # msgid template
├── update_translations.sh   # one-command refresh script
├── .gitattributes           # marks *.mo as binary
└── <locale>/LC_MESSAGES/setup.{po,mo}

__init__.py keeps a module-level _current_translations object. setup_i18n(locale) initialises it once at the top of run_setup_wizard. If the locale file is missing or corrupt, fallback=True silently yields NullTranslations, so the wizard never crashes — it simply falls back to English msgids.

Language-picker flow

run_setup_wizard(args)
  └─ _select_and_init_language(config, skip=bool(section))
       ├─ config.display.language already set? → load it, skip picker
       ├─ section-specific subcommand?        → load it, skip picker
       └─ first run / empty value              → show curses radiolist
                                                 → write choice to config.yaml
                                                 → init gettext
                                                 → continue wizard in chosen language

The result is saved immediately after the user picks a language. If they later press Ctrl+C during provider configuration, the language choice survives and will not be asked again on the next run.

String extraction workflow

All user-facing strings are wrapped with _(). f-strings are converted to str.format so that xgettext can extract the msgid correctly:

# Before
print_info(f"Found OpenClaw data at {openclaw_dir}")

# After
print_info(_("Found OpenClaw data at {}").format(openclaw_dir))

Running ./hermes_cli/i18n/update_translations.sh regenerates setup.pot, merges changes into every existing .po, and recompiles all .mo files.

Scope: setup wizard only

This is an intentional product decision, not a technical limitation:

  • Setup is the funnel bottleneck — data shows that first-install churn is far higher than day-to-day churn. Translating the wizard gives the highest ROI.
  • Agent conversation is out of our control — Hermes is an LLM-driven agent; the conversation language depends on the user's prompt and the model's own multilingual ability. gettext cannot (and should not) translate streaming LLM output.
  • Logs and tool output stay English — stack traces, API errors, and file paths are easier to search and debug when they remain in English, which is the lingua franca of GitHub Issues and Discord support channels.
  • Reviewable scopesetup.py contains ~300 user-facing strings. Localising the entire CLI surface would produce an unreviewable diff.

Future expansion to other CLI surfaces (e.g. hermes config prompts) follows the exact same pattern: add a domain, wrap strings with _(), done.

Backwards compatibility

config.yaml already had a display.language key with a default of "en". This PR changes the default to "" (empty string) so that the picker fires on first run. Existing users who already have "en" configured will not see the picker again; their setting is respected and simply loaded.

Files changed

File Change
hermes_cli/setup.py _() wrappers on all user-facing strings; _select_and_init_language() entry point
hermes_cli/config.py display.language default changed to "" to trigger picker on first run
hermes_cli/i18n/__init__.py gettext loader with hard fallback to English
hermes_cli/i18n/languages.py supported-locale table
hermes_cli/i18n/setup.pot msgid template
hermes_cli/i18n/update_translations.sh xgettext / msgmerge / msgfmt automation
hermes_cli/i18n/.gitattributes suppress binary diff noise for .mo files
hermes_cli/i18n/*/LC_MESSAGES/setup.{po,mo} 6 language translations

Future work

  • Integrate with Weblate or Crowdin so community translators can contribute via a web UI instead of editing .po files by hand.
  • Auto-compile .mo files in CI rather than committing binaries to git.
  • Add a "Follow system locale" option that reads $LANG / $LC_ALL to pre-select the default language.

Closes #12375, #20155, #16546, #13625

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard area/config Config system, migrations, profiles labels May 6, 2026
@songchao0421 songchao0421 force-pushed the feat/setup-i18n-clean branch from c2371da to 7ec19f8 Compare May 7, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add i18n / Multi-language UI support for CLI interface

2 participants