README 多语言链接智能插入方法详解(GitHub 文档翻译工具项目方案)

README 翻完,多语言入口却不出现——不起眼,但很影响使用:翻译任务生成了 README.zh-CN.md、README.ja.md,可主仓库的 README 里不会自动出现指向这些文件的链接,用户得自己翻目录才能找到对应语言版本。最初想用固定的链接文本硬编码进模板,结果仓库结构一换(比如 README 挪进 docs/ 目录、文件名变成小写)链接就失效。于是我给工具加了一个智能插入模块,让它在不同仓库结构下都能正确识别与插入。README 多语言链接([English](README.en.md) · [中文](README.zh.md))看似简单,但要让工具在不同仓库结构下都能正确识别与插入,需要做:识别 README 路径、解析现有语言标签、构造新链接、保持格式一致、跳过已存在条目。

一、目标

动手之前先把目标列清楚,避免后面实现时跑偏:

  • 自动在 README 顶部/底部插入多语言导航;
  • 不破坏原文档结构;
  • 支持自定义顺序与格式;
  • 与现有翻译任务兼容。

其中“不破坏原文档结构”这条我放在首位。翻译工具的定位是辅助,用户自己写的 README 才是主体,插链接时把别人的排版弄乱,再好的功能也没人敢用。所以后面所有实现都以“最小侵入”为前提,能只加一行就绝不重排整段。

二、链接格式

常见的链接形式有两种,先看它们各自的适用场景:

格式 用途
文字徽章 顶部用徽章卡片
文字列表 标题下方一行

本项目用 text 列表,兼容更多主题:

[English](README.en.md) · [中文](README.zh-CN.md) · [日本語](README.ja.md)

徽章方案视觉效果更好,但依赖 shields.io 这类外部服务,主题不匹配时样式会很突兀;文字列表不依赖任何外部服务,在 GitHub 和各代码托管平台上渲染一致,可访问性也更好。权衡下来选了 text 列表,后面所有解析、去重逻辑都围绕这种格式实现。

三、识别 README 路径

仓库里 README 的命名并不统一,识别要按优先级探测。候选路径按优先级:

README.md
README.zh.md
README.zh-CN.md
readme.md
docs/README.md

匹配规则:

  • 大小写不敏感;
  • 允许 docs/ 前缀;
  • 不区分点分隔与下划线分隔。

这里容易踩的坑是大小写:Linux 环境下 Readme.md 和 README.md 是两个文件,直接用字符串比较会漏掉,必须统一小写后再比。优先级从根目录的主 README 开始,命中即停,避免同时存在多个文件时选错基准。

四、解析现有语言标签

插入前先摸清 README 里已经有哪些链接,用正则把 Markdown 链接的 URL 全部收出来:

const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
const existing = new Set<string>();
for (const m of content.matchAll(linkRegex)) existing.add(m[2]);

新生成的链接若 path 已存在则跳过。

解析这一步决定“智能”程度。如果只无脑追加,同一个语言链接会被插入两次,README 越改越乱。这里把已有链接的 URL 收进 Set,构造新链接时查重。用 Set 而不是数组,是因为语言多了之后判断是否存在的耗时是常数,不随链接数量增长。

五、构造新链接

针对每个目标语言,根据源 README 的位置推导目标文件路径:

function buildLink(lang: string, sourcePath: string) {
  const dir = path.dirname(sourcePath);
  const base = path.basename(sourcePath, ".md");
  const target = path.join(dir, `${base}.${lang}.md`);
  return { text: langDisplay[lang], path: target };
}

langDisplay 把 ISO code 映射为显示名,如 zh-CN → 中文。

构造链接的核心是让目标路径跟着源 README 走。源文件在根目录,目标就在根目录;源文件在 docs/ 下,目标也保持在 docs/ 下。path.dirname 和 path.basename 负责拆解路径,这样不管 README 放哪一层,链接都不会指错地方。

六、插入位置策略

插入位置同样可配置,三种策略对应不同的 README 风格:

策略 描述
top-after-title H1 后第一行
top-insert-line 文档最顶部
bottom-append 文末追加

默认 top-after-title,不破坏徽章区。插入前检查该行是否已经存在语言列表,避免重复。

三个策略怎么选:带徽章区的 README 用 top-after-title 更合适,插在 H1 之后、徽章之前,视觉上不抢焦点;没有徽章的简单 README 用 top-insert-line,一进页面就能看到语言切换;想完全不打扰正文就用 bottom-append。默认值选 top-after-title,是因为多数项目都有徽章区,这个位置兼顾了可见度和安全性。

七、避免破坏 Markdown 结构

插入位置再准,也得防着破坏原文档结构,所以立了几条硬规则:

  • 不在代码块内插入;
  • 不在 HTML 注释内插入;
  • 不在引用块内插入;
  • 检查前后空行,保证渲染清爽。
function isInCodeBlock(lines: string[], index: number) {
  let inFence = false;
  for (let i = 0; i < index; i++) {
    if (/^```/.test(lines[i])) inFence = !inFence;
  }
  return inFence;
}

这条规则是从一次事故里来的。早期版本没做代码块判断,直接把链接行插进了一段 YAML 配置示例里,用户仓库的 CI 配置被改坏,发版直接失败。后来补上 isInCodeBlock 这类守卫函数,插入前逐行扫描,确认目标位置不在围栏、注释和引用块内,才允许写入。

八、与翻译任务协作

链接生成与翻译任务解耦:

  1. 翻译任务生成 README.zh-CN.md 等文件;
  2. README 链接插入作为独立步骤,根据任务结果批量更新主 README;
  3. 如果是首次翻译,没有目标语言文件,链接仍可先插入(指向待生成文件)。

解耦的意义在可测试性。翻译任务可能失败、可能中途取消,链接插入不该跟着一起翻车。把它设计成独立步骤后,翻译成功与否都不影响链接模块的确定性输出。首次翻译时目标文件还不存在,链接也照插不误,指向的文件后续由翻译任务补齐。

九、增量处理

仅当翻译任务新增了目标语言文件时才插入新链接:

  • 比对新旧文件集合;
  • 增量语言集合 → 生成新链接;
  • 提交到 PR 描述中作为说明。

全量重写 README 会带来大量无意义 diff,评审的人要在一堆无关变更里找真正改了什么。增量处理只动新增语言的链接,diff 干净,PR 描述里还能自动列出本次新增的语言,省去手动写说明的步骤。

十、配置项

语言列表、插入位置、分隔符都收敛到配置里,代码只负责执行:

readme_links:
  enabled: true
  position: top-after-title
  separator: " · "
  languages:
    - { code: en,    label: English }
    - { code: zh-CN, label: 中文 }
    - { code: ja,    label: 日本語 }

配置优先的好处是新增一种语言只改 YAML,不碰代码,也不影响其他模块的测试。separator 用 · 分隔不同语言,比竖线更不易与其他 Markdown 语法冲突,渲染出来也美观。

十一、常见边界

真实仓库里总有些边缘情况,逐一处理掉:

  • README 名为 Readme.md:统一小写后比较;
  • 路径含空格:用 url 编码;
  • 仓库使用 docs/ 目录:插入时保留目录;
  • 多 README(如 monorepo):按 glob 列表逐一处理。

这些边界场景都是真实用户反馈回来的。路径含空格的情况最初没想到,链接生成后 Markdown 渲染正常,但点击跳转 404,url 编码后才修复。monorepo 里多个 README 各自处理,用 glob 列出候选再逐个走完整流程,避免只改根目录漏掉子包。

十二、回归测试

改动收敛后,用一组回归用例把容易破坏的行为固定住:

  • 已有语言链接:不被覆盖;
  • 含代码块的 README:不破坏代码块;
  • 多种语言文件:链接按配置顺序输出;
  • 链接大小写:大小写不敏感比较。

回归测试把前面所有坑固化下来。每次改代码前跑一遍,确保已有链接不被覆盖、代码块不被破坏。测试用例不多但都是真实场景,覆盖了路径大小写、代码块守卫、顺序输出三块容易回归的地方。

十三、给同类项目的建议

  1. 链接生成与翻译任务解耦,便于独立测试;
  2. 配置优先,代码只负责执行;
  3. 渲染前先 dry-run,让用户预览变更。

dry-run 是最后加的一条建议,也是相当实用的一条。插入前先生成一份 diff 预览,用户确认改动符合预期再落盘,避免工具自作主张。这三条合起来,链接插入从“会动的工具”变成“可预期的工具”,用户敢放心交给它处理仓库根目录的 README。

常见问题(FAQ)

Q1:链接放在 README 顶部还是底部?

顶部更易被发现,本项目默认顶部。

Q2:链接文本要不要本地化?

要,按 ISO code 映射显示名。

Q3:能否不修改源 README?

可以,改为生成单独的 README.index.md,但效果弱于原地插入。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部