Language Packs
Hermes ships 17 UI languages (display.language). The list is pluggable: a language pack is a
plugin (or a folder in your Hermes home) that adds a new language or overrides the wording of an
existing one — for the Python side (CLI approval prompts, gateway replies, tool verbs, tips), the
hermes --tui interface and the Desktop app, all from one pack.
What a pack translates is the static UI text. Agent responses, tool output, logs and slash-command names stay as they are; to make the agent itself answer in another language, say so in your prompt.
Where languages come from
When Hermes looks up a string it walks these layers, top first, and takes the first hit:
- Plugin language packs — every installed plugin that declares
provides_locales(the last one loaded wins when two packs cover the same key). - Your overlay —
<HERMES_HOME>/locales/<lang>.yaml(per profile: each profile home has its ownlocales/folder). - Bundled — the
locales/<lang>.yamlfiles shipped with Hermes. - The same three layers for English, then the raw key.
Every layer may be partial: a pack or overlay only needs the keys it changes.
hermes config set display.language <id> accepts any id one of these layers supplies. Unknown ids
are refused with the list of available languages:
$ hermes config set display.language pl
✗ Unknown language 'pl' for display.language. Available: en, af, ar, de, es, ...
Install a language pack plugin (hermes plugins install <pack>) or drop <HERMES_HOME>/locales/<id>.yaml to add one.
Using a pack
hermes plugins install https://github.com/teknium1/hermes-lang-pl # or a catalog name
hermes config set display.language pl
Restart running gateways/TUIs so they pick up the pack. The TUI and Desktop language switchers list
bundled languages, your overlay languages and every installed pack by their native name (endonym).
The HERMES_LANGUAGE entry in .env still overrides display.language.
Writing a pack
A pack needs no Python code. Minimal layout:
hermes-lang-pl/
plugin.yaml
locales/
pl.yaml # core: Python-side strings (approval.*, gateway.*, cli.*, display.*, slash.*, tips.*, ...)
pl.tui.yaml # optional: hermes --tui strings
pl.desktop.yaml # optional: Hermes Desktop strings
plugin.yaml:
name: hermes-lang-pl
version: 1.0.0
description: Polish language pack for Hermes
provides_locales:
- id: pl # lowercase BCP-47-style tag: pl, pt-br, zh-hant
endonym: Polski # shown in language switchers
rtl: false # true for right-to-left scripts (flips the Desktop layout)
provides_locales entries can also be plain ids (- pl); the endonym then defaults to the id.
When the manifest declares provides_locales, the plugin loader registers every
locales/<lang>[.tui|.desktop].yaml automatically.
Files and keys
pl.yaml(core) mirrors the structure of the bundledlocales/en.yaml. Nested YAML flattens to dotted keys (approval.denied,gateway.goal_cleared). Copyen.yaml, translate the values, delete what you do not want to override.pl.tui.yaml/pl.desktop.yamlmirror the English catalogs of the TUI and Desktop apps. The key sets are exported tolocales/_keys.tui.jsonandlocales/_keys.desktop.jsonin the Hermes repo, which is what the validator checks against.- YAML values must be text. A number, list,
true/falseor empty value is rejected. - Never use YAML reserved words (
on,off,yes,no) as keys.
Placeholders
- Core (Python) strings use named placeholders exactly as English does:
"⏳ Draining {count} active agent(s)...". Keep every{name}from the English value; a missing or misspelled placeholder makes Hermes fall back to the untranslated string for that key. - TUI and Desktop entries whose English value is a function (it takes arguments) are written in YAML
as strings with positional placeholders:
"{0} of {1} sessions".
Validate
hermes plugins validate ./hermes-lang-pl
The validator checks that every declared id has locales/<id>.yaml, that each file parses and is
text-only (a non-text value is an error), and warns — listing them — about keys that do not
exist in the English catalog for that surface (they are harmless but do nothing). The .tui.yaml /
.desktop.yaml checks run when the corresponding _keys.*.json export is present.
Register from Python (optional)
A plugin that already has Python code can register catalogs itself:
from pathlib import Path
def register(ctx):
here = Path(__file__).parent
ctx.register_locale("pl", here / "locales" / "pl.yaml", endonym="Polski")
ctx.register_locale("pl", {"approval": {"denied": " ✗ Odrzucono"}}) # dict, nested or flat
ctx.register_locale("pl", here / "locales" / "pl.tui.yaml", surface="tui")
ctx.register_locale_dir(here / "locales") # everything at once
register_locale(lang, source, *, endonym=None, rtl=False, surface="core") takes a YAML path or a
mapping; surface is core, tui or desktop. Registration is undone when the plugin unloads and
never changes display.language.
Personal overrides without a plugin
Drop a partial file into your Hermes home:
# ~/.hermes/locales/en.yaml — only the keys you want to change
approval:
denied: " ✗ Nope."
The overlay applies to that profile only (~/.hermes/profiles/<name>/locales/ for a named profile).
Overlay files for a language Hermes does not bundle make that language selectable too. Edits are read
on the next start or the next display.language change.
Troubleshooting
Unknown language ... for display.language— the pack is not installed or not enabled (hermes plugins list), or the id inprovides_localesdoes not match the file name (plneedslocales/pl.yaml).- A translated string still shows in English — the key is not in the English catalog (run
hermes plugins validate; unknown keys are listed), or the placeholder set differs from English. - Language listed but the TUI/Desktop is still English — the pack has no
.tui.yaml/.desktop.yaml; those surfaces render English plus whatever the pack provides. For the 16 bundled languages the TUI ships its ownlocales/<lang>.tui.yamlin the Hermes tree (the Desktop bundles its translations in-app), so a pack for one of those only needs the keys it wants to override.