Hermes Agent 源码解析
第 19 讲:国际化与多语言支持
基于 Hermes Agent v0.16.0 源码 · 2026-07-02
一、为什么需要国际化
Hermes Agent 是一个面向全球开发者的工具——从 CLI 到 Telegram/Discord 网关,从 TUI 到桌面应用,用户遍布世界各地。如果所有提示信息都是英文的,非英语用户的使用体验会大打折扣。
但 Hermes 的国际化策略非常克制:它只翻译 Hermes 自身产生的静态用户界面消息(如审批提示、网关命令回复),而不翻译 Agent 生成的回复、日志、工具输出或命令描述。这一设计选择背后有深刻的原因。
💡 设计哲学:薄切片(Thin Slice)
只翻译最高影响的静态字符串——审批提示、网关斜杠命令回复、重启排空通知。Agent 生成的内容、日志、工具输出保持英文。
二、i18n 模块架构总览
Hermes 的国际化系统集中在 agent/i18n.py(302 行),不依赖任何第三方 i18n 框架(如 Babel、gettext),而是用纯 Python + PyYAML 实现了一套轻量级翻译引擎。
用户调用 t("gateway.reset.header_default")
语言解析链
get_language() → 环境 → 配置 → 默认
目录加载
_load_catalog(lang) → YAML → 扁平化
键值查找 + 回退
t(key, lang, **kwargs) → 翻译结果
三、语言解析链:四级优先级
Hermes 的语言解析遵循严格的优先级链,确保测试、开发和生产环境都能正确获取用户语言偏好。
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1(最高) | t() 的 lang 参数 | 显式指定语言,覆盖一切 |
| 2 | HERMES_LANGUAGE 环境变量 | 测试/快速切换用 |
| 3 | config.yaml 的 display.language | 用户配置,进程级缓存 |
| 4(默认) | "en"(英文) | 兜底默认值 |
📄 agent/i18n.py (第 241-249 行)
def get_language() -> str:
"""Resolve the active language using env > config > default order."""
env_lang = os.environ.get("HERMES_LANGUAGE")
if env_lang:
return _normalize_lang(env_lang)
cfg_lang = _config_language_cached()
if cfg_lang:
return cfg_lang
return DEFAULT_LANGUAGE
注意 _config_language_cached() 使用了 @lru_cache(maxsize=1) 装饰器——因为 t() 在热路径上调用(每次审批提示、每次网关回复),反复读取 YAML 配置会浪费性能。当配置变更时(如设置向导后),调用 reset_language_cache() 即可刷新。
四、语言别名系统:50+ 种输入映射到 16 种语言
Hermes 支持 16 种语言目录,但用户输入的语言标识可以非常灵活。系统内置了 _LANGUAGE_ALIASES 映射表,将自然语言名、BCP-47 区域标签、甚至非拉丁字符都映射到标准语言代码。
| 语言 | 支持别名 | 目录文件 |
|---|---|---|
| 简体中文 | chinese, mandarin, zh-cn, zh-hans | zh.yaml |
| 繁体中文 | zh-tw, zh-hk, traditional-chinese | zh-hant.yaml |
| 日语 | japanese, jp, ja-jp | ja.yaml |
| 德语 | german, deutsch, de-de, de-at | de.yaml |
| 西班牙语 | spanish, español, es-mx, es-ar | es.yaml |
| 法语 | french, français, fr-fr, fr-ca | fr.yaml |
| 乌克兰语 | ukrainian, українська, ua | uk.yaml |
| 俄语 | russian, русский, ru-ru | ru.yaml |
| 韩语 | korean, 한국어, ko-kr | ko.yaml |
| 葡萄牙语 | portuguese, português, pt-br | pt.yaml |
| 土耳其语 | turkish, türkçe, tr-tr | tr.yaml |
| 其他 | 意大利语、爱尔兰语、匈牙利语、南非语 | it/ga/hu/af.yaml |
📄 agent/i18n.py (第 43-82 行)
SUPPORTED_LANGUAGES: tuple[str, ...] = (
"en", "zh", "zh-hant", "ja", "de", "es", "fr", "tr", "uk",
"af", "ko", "it", "ga", "pt", "ru", "hu",
)
_LANGUAGE_ALIASES: dict[str, str] = {
"english": "en", "en-us": "en", "en-gb": "en",
"chinese": "zh", "mandarin": "zh", "zh-cn": "zh",
"zh-tw": "zh-hant", "zh-hk": "zh-hant",
"japanese": "ja", "jp": "ja", "ja-jp": "ja",
"german": "de", "deutsch": "de",
"spanish": "es", "español": "es",
"french": "fr", "français": "fr",
"ukrainian": "uk", "українська": "uk",
"korean": "ko", "한국어": "ko",
"russian": "ru", "русский": "ru",
# ... 共 50+ 条映射
}
亮点:系统甚至支持非拉丁字符输入(如 українська、한국어、русский),用户可以直接用自己母语的语言名来设置。
五、YAML 目录加载与扁平化
每个语言目录存放在 locales/<lang>.yaml,采用嵌套 YAML 结构提高可读性,但运行时会被扁平化为点号分隔的键值对。
📄 目录文件结构示例 (locales/en.yaml)
# 嵌套 YAML(人类可读)
approval:
dangerous_header: "⚠️ DANGEROUS COMMAND: {description}"
choose_long: " [o]nce | [s]ession | [a]lways | [d]eny"
gateway:
model:
switched: "Model switched to `{model}`"
provider_label: "Provider: {provider}"
agents:
header: "🤖 **Active Agents & Tasks**"
加载时,<_flatten_into> 递归将嵌套结构扁平化:
📄 agent/i18n.py (第 200-207 行)
def _flatten_into(node: Any, prefix: str, out: dict[str, str]) -> None:
if isinstance(node, dict):
for key, value in node.items():
child_key = f"{prefix}.{key}" if prefix else str(key)
_flatten_into(value, child_key, out)
elif isinstance(node, str):
out[prefix] = node
# Non-string, non-dict leaves are ignored -- catalogs are text-only.
扁平化后,gateway.model.switched 这样的点号路径可以直接在 t() 中查找。
目录位置解析:三级回退链
_locales_dir() 函数实现了三级回退链,确保在任何安装方式下都能找到语言目录:
HERMES_BUNDLED_LOCALES 环境变量
Nix 包管理器 / 密封安装包使用
↓ 不存在则回退
<repo-root>/locales
源码安装 / pip install -e
↓ 不存在则回退
sysconfig(data|purelib|platlib)/locales
pip wheel 安装
六、t() 翻译函数:三级回退机制
t() 是 i18n 模块的核心入口函数,实现了三级回退确保永远不会崩溃:
第 1 级:目标语言目录查找
在 zh.yaml 中查找 "gateway.reset.header_default"
↓ 未找到 → 回退
第 2 级:英文目录查找
在 en.yaml 中查找同一键
↓ 仍未找到 → 回退
第 3 级:返回原始键路径
返回 "gateway.reset.header_default" 本身
📄 agent/i18n.py (第 252-293 行)
def t(key: str, lang: str | None = None, **format_kwargs: Any) -> str:
target = _normalize_lang(lang) if lang else get_language()
catalog = _load_catalog(target)
value = catalog.get(key)
if value is None and target != DEFAULT_LANGUAGE:
# Fall through to English rather than showing a key path.
value = _load_catalog(DEFAULT_LANGUAGE).get(key)
if value is None:
# Last-ditch: return the key itself. A broken catalog
# should not crash anything; it just looks ugly.
logger.debug("i18n miss: key=%r lang=%r", key, target)
value = key
if format_kwargs:
try:
return value.format(**format_kwargs)
except (KeyError, IndexError, ValueError) as exc:
logger.warning("i18n format failed: %s", exc)
return value
return value
占位符格式化:翻译字符串中的 {placeholder} 通过 str.format() 替换。例如 t("gateway.draining", count=3) 会将 {count} 替换为 3。
七、目录覆盖范围与实际使用
英文目录 en.yaml 是翻译的事实来源(source of truth),共 386 行,涵盖以下功能域:
| 功能域 | 键前缀 | 示例 |
|---|---|---|
| 审批提示 | approval.* | 危险命令确认、选择菜单 |
| 网关命令 | gateway.* | /reset、/model、/agents 等回复 |
| 模型切换 | gateway.model.* | 模型信息展示标签 |
| 代理状态 | gateway.agents.* | 活跃代理与任务列表 |
| 上下文压缩 | gateway.compress.* | 压缩状态与错误消息 |
| 对话分支 | gateway.branch.* | 分支创建与切换消息 |
| 调试与状态 | gateway.debug.* | 调试报告上传状态 |
| MCP 重载 | gateway.reload_mcp.* | MCP 服务器重载通知 |
这些翻译在 gateway/run.py、gateway/slash_commands.py 和 tools/approval.py 中被大量调用。以 slash_commands.py 为例,/reset 命令就使用了 6 个不同的 i18n 键来处理不同场景(默认标题、新会话标题、AI 生成标题、标题错误等)。
八、测试保障:目录一致性测试
tests/agent/test_i18n.py(229 行)提供了完善的测试保障,确保翻译质量:
🔹 目录文件存在性测试:每个支持的语言必须有对应的 YAML 文件
🔹 键一致性测试:每个非英文目录必须与英文目录有完全相同的键集合(不能多也不能少)
🔹 占位符一致性测试:翻译文本中的 {placeholder} 必须与英文完全一致(防止拼写错误导致运行时 KeyError)
🔹 语言解析测试:验证别名映射、环境变量覆盖、默认回退等
🔹 目录回退链测试:验证三级回退机制(目标语言 → 英文 → 原始键)
🔹 安装路径回退测试:验证 Nix、pip wheel、源码安装三种场景下的目录解析
📄 tests/agent/test_i18n.py (第 44-74 行)
@pytest.mark.parametrize("lang", [l for l in SUPPORTED_LANGUAGES if l != "en"])
def test_catalog_keys_match_english(lang: str):
"""Every non-English catalog must have exactly the same key set."""
en_keys = set(_flatten(_load_raw("en")).keys())
lang_keys = set(_flatten(_load_raw(lang)).keys())
missing = en_keys - lang_keys
extra = lang_keys - en_keys
assert not missing, f"{lang}.yaml missing keys: {sorted(missing)}"
assert not extra, f"{lang}.yaml has keys not in en.yaml: {sorted(extra)}"
@pytest.mark.parametrize("lang", list(SUPPORTED_LANGUAGES))
def test_catalog_placeholders_match_english(lang: str):
"""Every translated value must use the same {placeholder} tokens."""
placeholder_re = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}")
for key, en_value in en_flat.items():
en_placeholders = set(placeholder_re.findall(en_value))
lang_placeholders = set(placeholder_re.findall(lang_flat.get(key, "")))
assert en_placeholders == lang_placeholders
九、配置与用户设置
用户可以通过 config.yaml 的 display.language 字段设置界面语言:
📄 config.yaml 配置示例
display:
language: zh # 简体中文
# 支持: en, zh, zh-hant, ja, de, es, fr, tr, uk,
# af, ko, it, ga, pt, ru, hu
# 未知值回退到 en
也可以通过环境变量快速切换(测试/临时使用):
📄 环境变量方式
# 临时切换为日语
export HERMES_LANGUAGE=ja
hermes chat
# 或直接指定
HERMES_LANGUAGE=zh hermes tools
十、设计亮点与不足
亮点
🔹 零外部依赖:不依赖 Babel/gettext,只用 PyYAML,减少安装包体积
🔹 三级回退:目标语言 → 英文 → 原始键,确保永不崩溃
🔹 50+ 别名映射:支持自然语言名、BCP-47 标签、非拉丁字符输入
🔹 进程级缓存:目录和配置都做了缓存,热路径不影响性能
🔹 目录一致性测试:强制所有语言目录与英文保持键一致
🔹 三级安装路径回退:Nix / pip wheel / 源码安装都能正确找到目录
不足
🔹 覆盖范围有限:只翻译静态消息,Agent 生成的内容仍是英文
🔹 无复数形式支持:没有 clDRule 或 nplurals 机制,复数通过占位符手动处理
🔹 无 RTL 支持:没有阿拉伯语/希伯来语等从右到左语言的目录
🔹 手动翻译维护:没有 CI 自动检测新键,需要开发者手动同步所有目录
十一、总结
Hermes Agent 的国际化系统是一个克制但精妙的设计。它不追求全面翻译,而是聚焦在最影响用户体验的静态界面上,用 302 行代码实现了 16 种语言的支持。三级回退机制确保系统在任何情况下都不会崩溃,而完善的测试保障则维护了翻译质量。
下一讲我们将进行 Hermes Agent 系列的总结与实战指南,回顾整个系列的核心要点。
📦 本讲核心文件
agent/i18n.py(302 行)— i18n 引擎
locales/(16 个 YAML 文件)— 语言目录
tests/agent/test_i18n.py(229 行)— 一致性测试
hermes_cli/config.py(display.language)— 语言配置
← 第 18 讲:Hermes Agent 源码-TUI、Dashboard 与桌面应用
第 20 讲:Hermes Agent 总结与实战指南 →
关注公众号获取更多技术干货
源码:https://github.com/NousResearch/hermes-agent
夜雨聆风