第 8 章 · 03 i18n 16 语言/遥测/AGENTS.md


第 8 章 · 03 i18n 16 语言/遥测/AGENTS.md

本节摘要:工程保障舱的收官三件套。i18n:src/i18n/translation.json 约 1.8MB、41,569 行,收录 2445 个 UI 键,每个键在全部支持语言(含 zh-CN 与 zh-TW)中都有译文——产品有完整中文界面;make-i18n-translations.cjs 把这份"键 → {语言 → 文案}"的宽表,按语言拆成 public/locales/<lng>/openhands.json,并生成 I18nKey 枚举;运行时 i18next-http-backend 按需加载语言包,1.8MB 不会进主包。遥测:src/services/telemetry.ts 898 行,PostHog 客户端的唯一持有者——事件分级(install 事件先于同意发送)、多层退出开关(VITE_DO_NOT_TRACK、浏览器 DNT、运行时注入)、防广告拦截器的反向代理域名。AGENTS.md:全仓库 120KB 的 AI 协作规范——仓库地图、API 访问规则、遥测架构、live/mock-LLM 测试框架、PR 规范全在其中,它本身是"如何为 AI 贡献者写工程文档"的绝佳教材。另外补上 winston 日志层。注:教程规划口径称"16 语言",当前源码实测为 15 种语言全量翻译,本节以源码为准。

内容来源:原项目 src/i18n/translation.json(前 50 行结构与统计)、src/i18n/index.tsscripts/make-i18n-translations.cjs(53 行全文)、src/services/telemetry.ts(头部与关键函数)、AGENTS.mdscripts/logger.mjs,精读整理。

⚠️ 注意:translation.json 是编辑源,不是运行时资源。直接改 public/locales/ 下的产物会被下次 make-i18n 覆盖;src/i18n/declaration.ts 头部写着"this file generate by script, don't modify it manually!!!"。一切改动回到宽表,产物交给脚本。

学习目标

阅读完本节,你应当能够:

  1. 描述 translation.json 的"键 → 语言映射"宽表结构,与 SECTION$KEY 的命名组织方式。
  2. 讲清 make-i18n 的两份产物(按语言拆分的 locales 包 + I18nKey 枚举)以及为什么 npm test 前要先跑它。
  3. 解释 i18next-http-backend 的运行时加载路径,与"1.8MB 不进主包"的工程理由。
  4. 复述遥测的分级哲学(install 先于同意、会话事件须授权)与四层退出开关。
  5. 概述 AGENTS.md 的章节结构与"给 AI 贡献者的 README"这一定位,并能从中翻查任意工程约定。

一、i18n:2445 个键的全量翻译

1.1 宽表结构:一行一键,一列一语言

translation.json 的组织是"键 → 各语言译文"的宽表,看前两个键就能懂:

{ "COMMON$PATTERN": { "ar": "النمط:", "de": "Muster:", "en": "Pattern:", "fr": "Motif :", "ja": "パターン:", "ko-KR": "패턴:", "uk": "Шаблон:", "zh-CN": "模式:", "zh-TW": "模式:" }, "COMMON$INCLUDE": { "ar": "تضمين:", "de": "Einschließen:", "en": "Include:", "fr": "Inclure :", "ja": "対象:", "ko-KR": "포함:", "uk": "Включити:", "zh-CN": "包含:", "zh-TW": "包含:" } }

两个设计决定值得注意:

  • 键名带分区前缀:COMMON$PATTERNLANDING$TITLE 这类 区$键 命名把 2445 个键按 UI 区域分组——搜一个界面文案,先看前缀就知道属于哪个功能区,键空间不会退化成扁平的字符串海。
  • 插值用双花括号:"COMMON$RESULTS": { ..., "en": "Results ({{count}})", "zh-CN": "结果 ({{count}})" }——i18next 标准插值语法,count 等参数由组件在调用时传入,译文永远只含占位符,不拼字符串。

实测统计:2445 个键 × 15 种语言(ar/ca/de/en/es/fr/it/ja/ko-KR/no/pt/tr/uk/zh-CN/zh-TW)全量覆盖——不存在"某语言缺这缺那"的半吊子状态,简繁中文都是一等公民。这也是为什么第 7 章、第 8 章反复出现的 i18next/no-literal-string lint 规则敢设为 error:所有 UI 文案都有家可归,组件里写死英文才是违规。

1.2 生成管线:53 行脚本,两份产物

scripts/make-i18n-translations.cjs 全文只有一个职责——把宽表"转置":

const fs = require("fs"); const path = require("path"); const i18n = require("../src/i18n/translation.json"); const namespace = "openhands"; // { [lang]: { [key]: content } } const translationMap = {}; Object.entries(i18n).forEach(([key, transMap]) => { Object.entries(transMap).forEach(([lang, content]) => { if (!translationMap[lang]) { translationMap[lang] = {}; } translationMap[lang][key] = content; }); }); // remove old locales directory const localesPath = path.join(__dirname, "../public/locales"); if (fs.existsSync(localesPath)) { fs.rmSync(localesPath, { recursive: true }); } // write translation files Object.entries(translationMap).forEach(([lang, transMap]) => { const filePath = path.join( __dirname, `../public/locales/${lang}/${namespace}.json`, ); if (!fs.existsSync(filePath)) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } fs.writeFileSync(filePath, JSON.stringify(transMap, null, 2)); }); // write translation key enum const transKeys = Object.keys(translationMap.en); fs.writeFileSync( path.join(__dirname, "../src/i18n/declaration.ts"), ` // this file generate by script, don't modify it manually!!! export enum I18nKey { ${transKeys.map((key) => ` ${key} = "${key}",`).join("\n")} }`.trim() + "\n", );

三步:遍历宽表把"键 → 语言 → 文案"转置成"语言 → 键 → 文案";删旧目录后按语言写 public/locales/<lang>/openhands.json;最后以英文键序为基准生成 declaration.tsI18nKey 枚举。第二份产物是点睛之笔——业务代码里写 t(I18nKey.LANDING$TITLE) 而不是裸字符串,拼错键名编译期报错;新增文案必须先进宽表再重新生成,枚举与翻译永远同步。这也解释了前两节反复看到的怪现象:npm testnpm run dev、甚至变异测试都要先跑 make-i18n——没生成枚举,引用它的代码根本编译不过。

1.3 运行时:i18next-http-backend 按需加载

src/i18n/index.ts 展示加载策略:

import Backend from "i18next-http-backend"; import LanguageDetector from "i18next-browser-languagedetector"; import { initReactI18next } from "react-i18next"; export const OPENHANDS_I18N_NAMESPACE = "openhands"; const initializeI18n = (instance: I18nInstance) => { // ... const initPromise = instance .use(Backend) .use(LanguageDetector) .use(initReactI18next) .init({ fallbackLng: "en", supportedLngs: AvailableLanguages.map((lang) => lang.value), ns: [OPENHANDS_I18N_NAMESPACE], defaultNS: OPENHANDS_I18N_NAMESPACE, backend: { loadPath: buildAgentCanvasPath("/locales/{{lng}}/{{ns}}.json"), }, interpolation: { escapeValue: false }, }); // ... };

关键在 loadPath:语言包运行时/locales/<语言>/openhands.json 拉取(路径还经 buildAgentCanvasPath 处理,兼容第 9 章会讲的 /canvas 子路径部署)。用户用中文,浏览器只下载中文那一份(几十 KB 到一两百 KB);1.8MB 的全量宽表从不进 bundle。文件头部的注释还讲了另一半故事:library 消费者如果不想用 HTTP 后端,可以从 resources.ts re-export 内嵌资源(Rollup 会在没人引用时把它摇掉)——同一份翻译,app 走网络、库走打包,两种形态一个来源。

💡 驾驶舱要点:这套 i18n 的完成度体现在"闭环":文案改动只进宽表一处;脚本产出语言包与类型枚举;枚举让键名拼写编译期受检;lint 禁止裸字符串堵住漏网;HTTP 后端保证运行时只付当前语言的成本。每个环节都在把"翻译遗漏"从运行时丢字幕,变成编译/CI 阶段的硬错误。

二、PostHog 遥测:898 行的事件宪法

2.1 分级哲学:install 先行,余者须同意

telemetry.ts 头部注释就是一份跟踪政策宣言:

/** * TRACKING PHILOSOPHY: * - Install event (canvas_install): Sent immediately on first use, regardless of consent. * This is anonymous and contains no PII - just basic browser info and a random ID. * - Session/custom events: Only sent after user grants consent via the consent modal. * - Users can opt out of all future tracking by declining consent. * * ADBLOCKER BYPASS: telemetry is routed through OpenHands' reverse proxy * (z.openhands.dev) ... */

落地代码在 trackInstall():

export async function trackInstall(): Promise<void> { // Respect hard opt-out via environment variable or browser setting if (isDoNotTrackEnabled()) { return; } // Already sent install event (persisted in localStorage - survives app relaunches) if (hasFirstUseSent()) { return; } const posthog = await initializePostHogClient(true); if (!posthog || isDoNotTrackEnabled()) { return; } // Capture the install event posthog.capture("canvas_install", { platform: typeof navigator !== "undefined" ? navigator.platform : "unknown", user_agent: typeof navigator !== "undefined" ? navigator.userAgent : "unknown", referrer: typeof document !== "undefined" ? document.referrer : "", url_origin: typeof window !== "undefined" ? window.location.origin : "", embedded: typeof window !== "undefined" && window.self !== window.top, }); markFirstUseSent(); // Restore opt-out state if user hasn't granted consent yet const currentConsent = getTelemetryConsent(); if (currentConsent !== "granted") { posthog.opt_out_capturing(); } }

细读这份"例外中的例外":install 事件虽先于同意发送,但(1)只发一次(localStorage 记账,跨会话有效);(2)载荷仅平台/UA/来源等环境字段加随机 ID,无 PII;(3)发完立刻检查同意状态,未授权马上 opt_out_capturing() 恢复静默。除此之外的一切事件(trackEventuseTelemetry 生命周期)都必须以 getTelemetryConsent() === "granted" 为前提。

2.2 退出开关的四层与运行时配置

不想被跟踪的用户有四条退路,代码全部落实:

开关 层级 效果
VITE_DO_NOT_TRACK=1 构建环境变量 install 事件也不发
浏览器 Do Not Track 设置 浏览器 同上,isDoNotTrackEnabled() 统一判定
window.__AGENT_CANVAS_DO_NOT_TRACK__ = true 运行时注入 静态服务器可按 AGENT_CANVAS_DISABLE_TELEMETRY=1 注入;此 flag 下 PostHog 客户端从不初始化,后端镜像来的同意也无法把它打开
拒绝同意弹窗 产品 UI install 已发,后续全停

还有一层给嵌入方的:configureTelemetry(config | false) 允许 npm 消费者运行时替换 apiKey/apiHost/uiHost,或传 false 硬关。默认 key 来自 config/defaults.jsontelemetry 段,数据经自家反向代理 z.openhands.dev 中转以绕开广告拦截器。AGENTS.md 的"Tracking / Analytics Architecture"一节(近 80 行)进一步规定:telemetry.ts 是唯一能摸 agent-canvas 命名 PostHog 客户端的模块;React 组件禁止裸调 posthog.capture(),一律经 useTracking 的类型化函数;事件字典(如 onboarding_link_clickedlink_id/destination_type/surface 枚举属性)明文列出,"新埋点必须复用既有契约,不许每个按钮造一个事件"。遥测在这个仓库里不是随手 capture,而是一套有宪法、有字典、有唯一入口的子系统。

2.3 winston 日志

scripts/logger.mjs 提供 dev 脚本与 Electron 端的文件日志:winston 按天滚动写到 <状态目录>/logs/agent-canvas.YYYY-MM-DD.log(保留 7 天),写前剥离 ANSI 颜色码;winston 用动态 import 加载并在解析失败时降级为 no-op——打包后的桌面应用剥掉 node_modules 也不至于崩日志。前端侧另有 src/utils/event-logger.ts 记录事件流。日志与遥测分工明确:遥测回答"用户怎么用",日志回答"这台机器怎么了"。

三、AGENTS.md:120KB 的 AI 协作规范

3.1 它是什么:README 的进化形态

AGENTS.md 是 CLAUDE.md/AGENTS.md 约定的仓库级 AI 指令文件——AI 编码代理进入仓库后读的第一份文档。这个仓库把它写到了 120,363 字节(约 714 行)的规模,目录横跨:

  • General/Repository Map——三仓库分工表(第 7 章第 02 节引用过)与"什么改动落在哪"的裁决规则;
  • Cross-Repository Boundaries——跨仓库依赖方向;
  • PR Description Human Check——PR 描述的人审规范;
  • Tracking/Analytics Architecture——上一节所述遥测宪法全文,含事件字典与环境变量;
  • Runtime Services in Dev Stacks——/server_info.runtime_services 的 JSON 形状与注入 agent 系统提示的 <RUNTIME_SERVICES> 块样例;
  • Live E2E / Mock-LLM E2E Test Framework——上一节两大测试框架的完整操作模型(admin API、目录布局、选择性执行、PR 报告,甚至"改了框架必须同 PR 更新本节"的元规则);
  • Testing Rules——TDD/AAA 约定;
  • API Access Rules——第 7 章的两条铁律;
  • No Magic Strings——i18n 键、命名常量、可辨识联合标签三条字符串纪律。

注意一个特征:它不是教程式文档,而是规则 + 理由 + 例外清单的规范文体。每条规则都写清"为什么"(如 live 测试为何不进普通 e2e:凭据与不确定性)、"违反了会怎样"(CI 守卫击落)、"哪里例外"(白名单三个文件)。AI 代理需要的正是这种零歧义的硬约束——比喻和愿景对模型没有约束力,"violation breaks CI"才有。

3.2 为什么说它是教材

这份文件值得每个团队精读,不因为你用 OpenHands,而因为它示范了 agent 协作时代的工程文档该怎么写:

  1. 可执行的规则优先于建议。"必须走 typescript-client"配的是能跑的守卫测试;"live 测试必须隔离"配的是配置文件的 ignore 模式。规则后面永远跟着强制机制,文档与机器纪律一一对应。
  2. 给决策留理由。entrypoint 里为什么 sleep & wait 而不是 wait -n、公共模式静态服务器为什么不注册编辑器路由(Referer 泄漏分析)——AI 与人一样需要 why 才能在新场景正确外推。
  3. 元规则保证文档不过期。"When changing any part of this framework — update this AGENTS.md section in the same PR"——改框架的 PR 必须同步改文档,过期文档比没有文档更危险。
  4. 为非人类读者优化结构。表格化的仓库分工、枚举化的属性字典、分节的规则编号,都是让 token 有限的读者快速定位的写法。

传统 README 写给人("这个项目是什么、怎么上手"),AGENTS.md 写给 agent 与人的混合团队("这个仓库的边界在哪、什么不能做、做了会被什么拦住")。当你的仓库每天有 AI 提交的 PR,这份文件就是新"同事"的入职手册——OpenHands 把它当核心基础设施维护,120KB 不是冗长,是把协作成本前置成了文档成本。

💡 驾驶舱要点:工程保障舱至此完整:单测/MSW/变异(逻辑与数据流)、mock-LLM e2e/TESTING_MATRIX(全链路与交付物)、i18n(全球用户)、遥测(产品度量)、winston(运行诊断)、AGENTS.md(人类与 AI 的协作纪律)。六种设施共同回答一个问题——当贡献者从"人"扩展为"人 + AI agent",怎么让每一次改动都安全落地。这是 Agent Canvas 作为"agent 时代产品"在自身工程实践上的自洽:用 agent 协作的方式,开发一个给 agent 用的驾驶舱。

本节要点回顾

  1. 宽表结构:translation.json 约 1.8MB/41,569 行,2445 个键 × 15 种语言全量翻译(规划口径 16,源码实测 15,含 zh-CN/zh-TW);键名 区$键 分区组织,插值用 {{count}}
  2. 生成管线:make-i18n 转置宽表 → 按语言写 public/locales/<lng>/openhands.json + 生成 I18nKey 枚举;枚举让键名拼错编译期报错,这也是所有测试/构建命令前置 make-i18n 的原因;产物禁止手改。
  3. 运行时加载:i18next-http-backend 从 /locales/{{lng}}/{{ns}}.json 按需拉取(路径经 base-path 处理),主包不含翻译;library 形态可走内嵌 resources 的 re-export。
  4. 遥测宪法:telemetry.ts 898 行是 PostHog 客户端唯一持有者;install 事件先于同意但一次性、无 PII、发后即恢复退出;四层退出开关(环境变量/DNT/运行时注入/拒绝弹窗)+ configureTelemetry 运行时覆盖;组件禁止裸 capture,事件按字典复用契约。
  5. winston 日志:dev 脚本与桌面端的按天滚动文件日志,动态加载失败自动降级 no-op。
  6. AGENTS.md:120KB/714 行的 AI 协作规范,覆盖仓库地图、API 规则、遥测、两大 e2e 框架、字符串纪律;特征是"规则 + 强制机制 + 理由 + 例外";它是"为 AI 贡献者写工程文档"的范本,也是 README 在 agent 协作时代的进化形态。

下一节:第 9 章 · 01 五种部署形态——离开保障舱,进入停机坪:npm 全局包、Docker 三合一、Electron 三平台、Helm 与 Vercel,看这个产品怎么交付到用户手里。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U