Repository Wiki
jason5ng32/MyIP

翻译维护、辅助脚本与贡献流程

MyIP 将翻译贡献组织为“英文基准、完整键结构、允许逐步补齐的 beta 语言包”,并通过 pnpm 脚本完成骨架生成、同步、进度检查和提交前验证。本页说明这些工具如何配合,以及贡献者从修改资源到提交 PR 的实际路径。

目的与范围

本页覆盖 UI 翻译和 README 翻译的维护约定、语言脚手架的核心算法、相关辅助命令、测试门禁与贡献流程。不展开应用的语言切换组件、后端 IP 数据查询、部署及缓存清理机制;这些内容应分别阅读对应架构和部署说明。项目的完整开发导览可参阅 Developer Guide。

证据范围:脚手架分析依据实际读取的 i18n-scaffold.js;运行时回退和测试规则依据仓库的 TRANSLATING.md。本页没有直接核验运行时加载器、测试实现或全部辅助脚本,因此不将这些指南描述扩展为未经验证的内部 API、异常或并发保证。

概述

翻译维护有三个相互独立的维度:

  • 结构完整性:主语言包必须保留与英文相同的键,暂不翻译的值使用 "",而不是删除键。
  • 内容完成度:beta 主包允许空字符串;full 语言的主包、隐私文案和安全检查清单必须完整。
  • 持续维护责任:升级为 full 后,后续用户可见文案变更也必须同步维护该语言,并满足更新历史要求。

这一设计让翻译 PR 的主要差异表现为 "" 变为译文,减少结构变动带来的审阅噪声。主包允许逐键回退,但两个可选数据集采用整文件回退,因此不能使用同样的“半完成文件直接发布”策略。参见 TRANSLATING.md。

工具架构

Loading diagram...

Sources: package.json、i18n-scaffold.js、i18n-scaffold.js

这里的主数据是提交进 Git 的 JSON 文件和语言注册表,而非数据库记录。脚手架通过自身模块位置计算仓库根目录,使用 node:fs 的同步文件操作;flattenPack 在已读取的新建流程中用于输出键数量。图中的状态报告脚本只有命令入口经过直接核验,不表示已经检查其内部统计实现。

翻译资源与发布约束

主包、可选包和更新历史

资源beta 语言full 语言维护重点
主语言包必须具有全部英文键,值可为 ""不允许未译空值不新增拼错的键,不删除未译键
隐私文案可以不提供;提供就必须完整必须提供且完整不提交半完成文件
安全检查清单可以不提供;提供就必须完整必须提供且完整slug、priority 保持英文数据值
更新历史免除 beta 语言要求需要补齐历史记录晋升前检查全部条目

资源布局和约束见 TRANSLATING.md;full 语言的晋升条件见 TRANSLATING.md。

按照翻译指南,主包缺失或为空的字符串沿“当前语言 → 已注册基础语言 → 英文”逐键回退,例如 zh-TW → zh → en。更新历史按条目回退,隐私文案与安全检查清单按整个文件回退。运行时能够容忍缺失键,不代表源文件可以缺失键:提交门禁仍要求主包键集合与英文一致。

注册信息及自动猜测

新增语言需要主包以及 locale-registry.js 中的一条记录。脚手架对注册记录的生成逻辑如下:

javascript
1export const buildRegistryEntry = (code) => ({ 2 code, 3 nativeName: nativeNameFor(code), 4 flag: flagFor(code).flag, 5 apiTag: code, 6 htmlLang: code, 7 status: 'beta', 8});

Source: i18n-scaffold.js

字段类型脚手架初值贡献者需要检查的内容
codestring传入代码与 JSON 文件名一致,区分默认语言和区域变体
nativeNamestringIntl.DisplayNames 推导并将首字符大写是否符合母语习惯;查询失败时退回代码
flagstring区域子标签、内置映射或小写代码是否为有效的两字母旗帜代码
apiTagstring原样使用 code是否应改为上游支持的近似语言标签
htmlLangstring原样使用 code是否准确表达语言、地区或文字系统
statusstring'beta'不自行将新语言标为完整维护语言

推导实现见 i18n-scaffold.js。特别注意:apiTag 不是自动选出的最接近上游语言,实现只是赋值为 code。指南列出的上游标签为 en、de、es、fr、ja、pt-BR、ru、zh-CN;没有相近标签时,地名显示英文属于预期情况。注册顺序同时影响语言选择器顺序,参见 TRANSLATING.md。

新增语言的控制流

1. 校验代码并选择创建模式

runNew(code, flags) 首先拒绝缺少语言代码的调用,再提取 --privacy 和 --checklist。如果任一标志存在,就进入可选包创建流程;否则创建主包和注册记录。带可选标志的调用不会同时替你创建主包,所以应先完成普通新建。

validateNewCode 使用正则限定 xx、xx-YY 或 xx-Xxxx 形式,例如 zh、pt-BR、sr-Latn。这不是接受任意 BCP-47 标签的通用解析器。格式不符立即返回错误;已注册代码也会产生错误。首次引入某个基础语言却使用区域代码,以及无法推导旗帜,只产生提示而不直接阻止创建。见 i18n-scaffold.js。

2. 递归生成骨架

javascript
1export const buildSkeleton = (reference, keepKey = () => false) => { 2 const walk = (node, key) => { 3 if (typeof node === 'string') return keepKey(key) ? node : ''; 4 if (Array.isArray(node)) return node.map((child) => walk(child, key)); 5 if (node && typeof node === 'object') { 6 return Object.fromEntries(Object.entries(node).map(([child_key, child]) => [child_key, walk(child, child_key)])); 7 } 8 return node; 9 }; 10 return walk(reference, ''); 11};

Source: i18n-scaffold.js

算法保留对象层级、数组顺序及非字符串数据,只清空需要翻译的字符串。可选的 keepKey 接收当前键名,而非完整路径;安全检查清单用它保留 slug 和 priority。这样生成的骨架既可用于逐步填写文案,也不会先把 URL 标识和优先级等数据清空。

3. 写入主包,再修改注册表

Loading diagram...

Source: i18n-scaffold.js、i18n-scaffold.js

insertRegistryLine 并不解析 JavaScript AST,而是在源码中寻找特定形状的 export const LOCALES = [ 数组,然后在结束位置前插入文本。如果注册表声明格式被改到无法匹配,函数会抛出 Error。因此注册表格式也是该工具的隐含接口,调整格式时应同步检查脚手架。

4. 创建可选数据集

addExtraPacks 先对所有目标数据集收集错误:语言必须已经注册、主包必须存在、目标文件不能已存在。校验通过后,再依次读取对应基准文件、生成骨架并写入。清单骨架保留数据键;隐私骨架没有这一保留规则。工具明确提示:这些文件在所有文案填满之前会使测试失败,不准备完成时应删除新生成文件。见 i18n-scaffold.js 和 i18n-scaffold.js。

同步、进度检查与测试门禁

同步保留什么,删除什么

syncPack(reference, existing, prefix = '', report = { added: [], removed: [] }) 是可直接阅读和测试的纯逻辑部分。它不是对象的普通浅合并,而是以基准结构重建目标包:

  1. 将非对象的旧值视为没有可复用子结构。
  2. 检查旧对象的键,记录基准已删除的路径;数组则按参考长度判断被移除的索引。
  3. 按基准对象顺序或数组顺序遍历。
  4. 对子对象递归,并共享同一个差异报告对象。
  5. 非字符串叶子采用基准值;已有字符串原样保留,包括原有 ""。
  6. 缺少字符串或旧值类型不符时写入 "",并记录新增路径。
  7. 返回重建后的 pack,以及 added、removed 路径数组。

核心保留规则如下:

javascript
1 const merge = (key, child) => { 2 const current = source[key]; 3 if (child && typeof child === 'object') return syncPack(child, current, at(key), report).pack; 4 if (typeof child !== 'string') return child; // non-copy leaf: keep en's value 5 if (typeof current === 'string') return current; // the translation, or a "" already there 6 report.added.push(at(key)); 7 return ''; 8 };

Source: i18n-scaffold.js

完整算法见 i18n-scaffold.js。它保留仍在基准结构中的字符串译文,但不会判断译文是否因英文语义变化而过时,也不会保留英文已删除的键。因此“同步不覆盖译文”不等于“同步不会删除任何内容”。

根据翻译指南,pnpm i18n-sync 会同步主包;隐私文案和清单只报告差异,不自动写入空值。这避免工具自行制造一个违反完整性门禁的可选文件。此处 CLI 整体行为依据 TRANSLATING.md,不推断未读取的同步执行器细节。

进度报告与通过测试不是一回事

指南把 pnpm i18n-status 定义为报告命令:显示各语言、各数据集的完成百分比、待翻译键,并统计与英文逐字相同的值。英文相同值可能是产品名或 MTR 之类的术语,不应机械地认定为遗漏。报告命令不是质量门禁,也不能证明译文准确。见 TRANSLATING.md。

指南列出的测试职责为:

检查门禁含义
主包键集合必须与英文完全一致,多键、少键都不允许
插值占位符译文不能引入英文未提供的占位符;名字必须保持一致
清单数据键slug、priority 不翻译
可选文件空值隐私文案和清单不能包含未译空字符串
full 状态三类文件必须齐全,主包也不能留空值
注册表及更新历史检查注册行形状,并要求 full 语言维护历史

规则来源为 TRANSLATING.md。特别注意,占位符规则描述的是英文集合的子集约束,本页不把它夸大为“必须使用英文全部占位符”。本次未运行测试,也未直接读取测试实现。

用法示例

下列命令均直接摘自仓库指南,不是新编写的命令配方。

最小新增语言

bash
pnpm i18n-new es-MX # your language's code — see "Naming the code" below

Source: TRANSLATING.md

该示例展示区域语言代码的用法,并不表示 es-MX 已经注册。完成后检查生成的注册字段,先翻译主包中最有价值的页面和导航文案;其他键保留空字符串。

为已有语言生成完整数据集骨架

bash
pnpm i18n-new es-MX --privacy --checklist # either flag, or both

Source: TRANSLATING.md

这一步必须在主包和注册记录已经存在之后执行。生成不等于完成:两个文件都应翻译完整后再提交。

本地检查与界面预览

bash
1pnpm install 2pnpm test # the hard gate — must be green 3pnpm i18n-status # progress report — never fails, just tells you where you are 4pnpm dev # then pick your language in Preferences

Source: TRANSLATING.md

预览时可通过偏好设置选择语言。指南也支持 ?hl=<code>,但已保存的语言偏好优先于查询参数;调试“查询参数为什么没有切换语言”时应先检查这个条件。见 TRANSLATING.md。

命令与环境配置

开发环境

package.json 固定 packageManager 为 pnpm@12.4.2,Node 引擎表达式为 ^24.15.0 || >=26.0.0。贡献指南概括为 Node.js 24+,但安装环境应遵循包声明的更精确范围,而不是认为任意 24.x 或所有更高主版本都自动满足要求。

命令实际入口或组合本页相关用途
pnpm i18n-newnode scripts/i18n-scaffold.js new新建语言或可选数据集
pnpm i18n-syncnode scripts/i18n-scaffold.js sync主包结构对齐
pnpm i18n-statusnode scripts/i18n-status.js翻译进度报告
pnpm testnode --import ./tests/setup.js --test tests/*.test.jsNode 测试门禁
pnpm checkpnpm run test && pnpm run build测试通过后执行生产构建
pnpm devconcurrently 启动 Vite 和 nodemon 后端本地预览
pnpm fetch-faviconsnode scripts/fetch-favicons.js连通性列表图标补齐
pnpm dns-checknode scripts/check-dns-resolvers.jsDNS 检查脚本入口;内部行为未核验
pnpm purge-indexnode scripts/purge-index-cache.js缓存清理入口;运维细节不在本页展开

命令定义见 package.json。其中 check 使用 &&,意味着测试失败时不会继续构建。build 对应 vite build,并配置了 postbuild:仅在清理脚本存在时启动子进程,将其状态码或兜底值 1 用作退出码。因此生产构建校验也可能涉及构建后的脚本阶段,不只是前端编译。

CLI 选项

入口或字段类型默认或省略行为说明
i18n-new 的语言代码string无默认;缺少则抛错按脚手架支持的代码格式传入
--privacy标志不选中生成已注册语言的隐私文案骨架
--checklist标志不选中生成清单骨架并保留数据键
i18n-status --localestring本次未核验默认实现指南展示按语言查看进度
i18n-status --limit数值参数本次未核验默认值指南示例使用 30 控制显示数量

新建参数依据 i18n-scaffold.js,状态参数依据 TRANSLATING.md。不将示例参数值视为默认配置。

脚手架函数参考

这些是 JavaScript 模块中的导出函数,不是 HTTP API;源码没有 TypeScript 类型声明。下表按真实签名说明其输入和返回结构。

签名输入与返回边界
buildSkeleton(reference, keepKey = () => false)递归输入结构和键保留谓词;返回新骨架非字符串叶子原样保留
isChecklistDataKey(key)键名;返回 boolean仅匹配 slug 或 priority
syncPack(reference, existing, prefix = '', report = { added: [], removed: [] })基准、旧包、路径前缀、累计报告;返回 { pack, added, removed }递归共享并修改报告数组
nativeNameFor(code)语言代码;返回名称字符串捕获 Intl.DisplayNames 错误并退回代码
flagFor(code)语言代码;返回 { flag, guessed }guessed: false 表示需要人工检查
buildRegistryEntry(code)语言代码;返回六字段注册对象固定为 beta,不验证上游支持
formatRegistryLine(entry)注册对象;返回 JavaScript 文本行通过字符串模板拼接
insertRegistryLine(source, line)注册表文本和待插入行;返回新文本无法找到 LOCALES 数组时抛出 Error
validateNewCode(code, registered = LOCALE_CODES)代码和已注册列表;返回 { errors, warnings }返回校验结果,由调用者决定是否抛错
validateExtraPack(code, dir, { registered, hasMainPack, exists })代码、目录和前置条件;返回错误字符串数组防止缺少主包或覆盖可选包

实现依据:i18n-scaffold.js。这些函数以有效的语言代码、注册对象或 JSON 结构为预期输入,不应被解释为对任意 JavaScript 值都具有完整防御式校验。

贡献流程

分支、实现和提交前验证

  1. Fork 仓库,从 dev 建分支,PR 也提交到 dev。main 接受来自 dev 的发布合并;本页源码链接使用 main 不代表贡献目标分支是 main。
  2. 使用 pnpm 安装依赖,不引入 npm 或 yarn 的竞争锁文件。
  3. 非小型改动先开 issue 讨论;每个 PR 聚焦一个关注点。
  4. 阅读根目录及所修改子系统的规范,再实现修改。
  5. 非视觉逻辑变更在同一个 PR 中提供测试;UI 改动说明需要审阅的视觉效果。
  6. 执行 pnpm check,rebase 到最新 dev,提交说明“改了什么、为什么改”。

流程和规范见 CONTRIBUTING.md 及 CONTRIBUTING.md。代码约定包括仅使用 JavaScript、新函数使用 const 箭头语法、新文件带用途头注释;后端日志使用共享 pino logger,不在 API 与 common 目录使用 console.*。脚手架自身的终端输出不应被误判为该后端目录规则的例外设计。

README 翻译与 UI 翻译分开维护

README 社区翻译以英文 README.md 为权威来源,新文件按 README_<LANG>.md 命名。开头使用目标语言注明“社区维护,英文为准”,并在现有 README 顶部语言切换行增加入口;代码块、URL 和徽章不变。开始前先检查是否已有对应语言请求。UI 语言则走语言包与注册表流程,两者不是同一个发布机制。参见 CONTRIBUTING.md。

数据类贡献及辅助工具

DNS 解析器数据贡献应按数据文件头部规则填写,并通过对应数据测试。连通性站点列表成员需要提交 64px PNG 图标;贡献指南说明,本地 pnpm test 可以自动下载缺失图标,也可使用 pnpm fetch-favicons。CI 保持离线,只检查图标是否存在,因此自动下载成功后仍必须把生成的 PNG 纳入提交。测试通常不访问真实上游,本地缺失图标下载是明确例外。见 CONTRIBUTING.md 和 CONTRIBUTING.md。

失败模式、并发与维护注意事项

情况已验证行为或指南约束建议处理
新建代码格式错误、已注册或主包已存在创建流程在写入前抛出 Error检查代码;已有语言直接维护,不重复新建
可选包已有文件或缺少主包先收集错误并阻止创建先注册语言,不使用新建命令覆盖译文
注册表源码格式不匹配insertRegistryLine 抛出 Error检查生成主包和注册表是否处于不一致状态
可选包未翻译完整指南规定测试失败完成整个文件,或撤回该文件
与英文结构漂移指南规定多键、少键失败主包同步后审阅 diff;可选包手动处理
地名仍显示英文上游本地化标签有限检查 apiTag,不要仅据此判断 UI 翻译失效
输入框出现地址自动填充指南指出 iOS QuickType 会受地址类措辞影响占位文案避免对应语言的“地址”表达

失败前置条件见 i18n-scaffold.js 和 i18n-scaffold.js;翻译边界见 TRANSLATING.md。

文件一致性与并发

主包与注册表是两次顺序写入,不是事务:主包先落盘,随后才读取、插入并写回注册表。第二步失败时,前一步不会在已读取代码中自动回滚。可选包也逐个写入;前置校验全部通过不等于后续磁盘操作全部成功。

已读取的新建路径没有锁、临时文件替换或重试逻辑。同步文件 API 只让单进程中的步骤顺序执行,不能保证两个并发脚手架进程之间互斥。操作建议是串行运行语言生成工具,并在提交前检查 JSON 与注册表是否同时更新;不要把文件存在检查视为跨进程安全保证。写入依据见 i18n-scaffold.js 和 i18n-scaffold.js。

性能与扩展点

骨架生成和包同步都递归遍历基准结构,成本主要来自 JSON 大小、树深度和磁盘读写;没有必要把它们当作线上请求路径调优。同步通过重建对象维持英文顺序,可降低资源文件审阅噪声,但仍需人工检查语义变更。

扩展时优先使用现有纯函数边界:新增“数据而非文案”的保留规则可从 keepKey 切入;新增数据集应检查 DATASETS,同时修改 runNew 中当前写死的 privacy、checklist 选项选择列表。仅增加数据集配置并不会自动开放新的 CLI 标志。规则位置见 i18n-scaffold.js 和 i18n-scaffold.js。

相关链接