<!-- title: 造i18n提取Skill：扫中文秒出多语言包 -->

# 造 i18n 提取 Skill：扫源码中文秒出多语言包

## 概述

本教程带你从零构建一个名为 `i18n-extract` 的自定义 Skill：它扫描项目源码里写死的中文文案，自动抽取成 `zh-CN.json` / `en-US.json` 多语言资源文件，并产出待人工确认的歧义清单。你将掌握 Skill 的目录结构、`SKILL.md` frontmatter 写法、渐进式披露（progressive disclosure）、捆绑脚本与 LLM 的分工模式，以及 `${CLAUDE_SKILL_DIR}` 等字符串替换机制。适合会写命令行、做过前端/全栈项目、想让 Claude 替自己处理重复性工程任务的开发者。

## 前置条件

- 已安装 Claude Code（CLI 版本 `v2.1.196+`，下文涉及 `${CLAUDE_PROJECT_DIR}` 等较新特性）
- 本机装有 Node.js（脚本用 Node 跑，无需第三方依赖）
- 一个含中文文案的真实前端/全栈项目（JS/TS/JSX/TSX/Vue 均可），用于实战
- 了解 JSON、正则表达式基础

---

## 一、先理解 Skill 的本质：渐进式披露

很多人把 Skill 等同于"一段提示词"，这只对了一半。Skill 的真正工程价值在于**渐进式披露**（progressive disclosure）——让 Claude 在大部分时间里只花极少 token，在真正需要时才加载完整能力。

它分三层运作：

| 层级 | 加载时机 | 进入上下文的内容 | Token 成本 |
|------|---------|----------------|-----------|
| 元数据层 | 会话启动 | 仅 `name` + `description`（描述被截断在 1,536 字符内） | 极低 |
| 指令层 | 被 `/调用` 或模型判定相关时 | 完整 `SKILL.md` 正文 | 中 |
| 参考层 | 正文里显式 `Read` 时 | `reference/*.md`、模板等 | 按需 |

官方文档把这个机制说得很直白：**"一旦某个 Skill 被加载，它的内容会在后续轮次里持续留在上下文中，所以每一行都是反复发生的 token 成本。"** 这就是为什么我们要把"机械扫描"这种脏活交给脚本，把"键名命名 + 翻译"这种需要判断的活留给模型——脚本只是被执行、不进上下文，而 `SKILL.md` 正文要尽量精简。

理解这一点后，我们设计 `i18n-extract` 的分工：

- **脚本 `scan_zh.js`**（被执行，不进上下文）：用正则把所有含中文的字符串字面量机械找出来，输出 JSON。
- **`SKILL.md` 正文**（进上下文）：告诉模型拿到 JSON 后怎么去重、怎么按约定生成键名、怎么写两个语言包、怎么出报告。
- **`reference/key-conventions.md`**（按需加载）：键名命名规范，仅在模型需要时被 `Read`。

这种分工是本 Skill 的灵魂，也是判断一个 Skill 设计是否优秀的试金石。

---

## 二、创建 Skill 目录结构

Skill 放在哪里，决定了它的作用域。官方约定：

- `~/.claude/skills/<name>/`：个人级，所有项目可用
- `<project>/.claude/skills/<name>/`：项目级，随仓库共享给团队（需先接受工作区信任对话框）
- 父目录与嵌套子目录的 `.claude/skills/` 会被自动发现，天然支持 monorepo

我们做一个**个人级** Skill，目录结构如下：

```
~/.claude/skills/i18n-extract/
├── SKILL.md                      # 入口，必须存在
├── scripts/
│   └── scan_zh.js                # 捆绑脚本：机械扫描中文
├── reference/
│   └── key-conventions.md        # 参考文件：键名命名规范（按需加载）
└── templates/
    └── locale.json               # 输出模板
```

注意：**`SKILL.md` 是唯一必须的文件**，其余都是可选的"支撑文件"。官方建议把 `SKILL.md` 控制在 500 行以内，大段参考材料拆到独立文件、并在正文里用 Markdown 链接引用，好让模型知道"什么时候该去读它"。

---

## 三、写 `SKILL.md`：frontmatter 与正文

`SKILL.md` 由两段组成：顶部的 YAML frontmatter（配置行为）和下面的 Markdown 正文（给模型的指令）。

### 完整文件

```markdown
---
name: i18n-extract
description: 扫描源码中的硬编码中文文案，抽取为 i18n 多语言资源文件（zh-CN.json / en-US.json）并生成歧义清单。当用户提到"国际化 / i18n / 多语言 / 文案抽取 / 把中文抽出来 / 抽成多语言包"时使用。
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/scan_zh.js *) Read Write Edit
argument-hint: [扫描目录] [目标语言...]
---

# i18n 文案提取

把源码里写死的中文，抽取成可维护的多语言资源。
**机械扫描交给脚本，键名命名与翻译交给模型。**

## 工作流

1. 运行捆绑扫描脚本，拿到全部中文文案（脚本输出 JSON 数组）：

   !\`${CLAUDE_SKILL_DIR}/scripts/scan_zh.js "$ARGUMENTS"

   输出形如：`[{ "file": "src/Home.jsx", "line": 12, "text": "欢迎回来" }, ...]`

2. 对每条 `text` 去重；遇到同文不同义（按钮文案 vs 标题），拆成不同键。

3. 按 reference/key-conventions.md 的约定生成语义化键名（如 `home.welcome.title`）。

4. 写入 `locales/zh-CN.json`：键名 → 原始中文（**保持原文不翻译**）。

5. 写入 `locales/<目标语言>.json`（默认 `en-US.json`）：键名 → 对应译文。
   占位符 `{name}` / `%s` / `{0}` 必须**原样保留**。

6. 输出报告：抽取总条数、去重后键数、以及"上下文不足、需人工确认"的歧义文案清单。

## 铁律
- **不要**自动把源码里的文案改写成 `t('key')`，除非用户明确要求。改写有风险，先让用户 review 资源文件。
- 翻译时保持简洁、贴近 UI 语境；按钮用动词，标题用名词短语。
- 键名全小写、点分层级、不超过 4 层。

## 附加资源
- 完整键名命名规范见 [reference/key-conventions.md](reference/key-conventions.md)
```

### frontmatter 字段逐条解释

- **`name`**：显示名。注意它**不决定**你输入的命令名——个人级 Skill 的命令名取自目录名，所以这里叫 `i18n-extract`，你输入的就是 `/i18n-extract`。
- **`description`**：**唯一强烈建议填的字段**。模型就靠它判断"现在该不该加载这个 Skill"，所以要把"做什么 + 何时用"写清楚，把高频触发词（国际化、i18n、文案抽取）放进去。这段会在会话启动时就进上下文，但有 1,536 字符上限。
- **`allowed-tools`**：让模型在 Skill 激活期间免确认地使用列出的工具。这里用 `${CLAUDE_SKILL_DIR}` 指向捆绑脚本，保证无论从哪个工作目录调用都能定位到 `scan_zh.js`。
- **`argument-hint`**：自动补全时显示的参数提示。

### 正文里的两个关键机制

第一是 **`!`<命令>` 动态注入**。这种语法会在 Skill 内容发送给模型**之前**先跑命令，把输出**原地替换**掉占位符。于是模型拿到的不是"请运行 scan_zh.js"，而是脚本已经执行完的真实 JSON 结果。这是 Skill 区别于普通提示词的杀手锏——把"现场取数"固化进工作流。

第二是 **`$ARGUMENTS` 字符串替换**。`/i18n-extract src en-US ja-JP` 执行时，`$ARGUMENTS` 会被替换成 `src en-US ja-JP`，整段传给脚本和模型。`argument-hint` 里的 `[目标语言...]` 提示用户可以一次指定多个语言。

> 常见问题：**Skill 没被自动触发？** 90% 是 `description` 写得不到位。把"何时用"的触发词写具体，别只写"处理国际化"。

---

## 四、写扫描脚本 `scan_zh.js`

这是机械活，不该让模型一边猜一边扫。脚本职责单一：遍历目录、找出含中文的字符串字面量、输出结构化 JSON。

```js
#!/usr/bin/env node
// scripts/scan_zh.js
// 扫描目录下源码文件中的中文字符串字面量，输出 JSON 数组。
// 用法: node scan_zh.js <目录> [--ext=js,jsx,ts,tsx,vue]
const fs = require('fs');
const path = require('path');

const root = process.argv[2] || '.';
const extArg = process.argv.find(a => a.startsWith('--ext='));
const exts = (extArg ? extArg.split('=')[1] : 'js,jsx,ts,tsx,vue').split(',');
const SKIP = ['node_modules', '.git', 'dist', 'build', '.next', 'locales'];

// 匹配含至少一个中文字符的字符串字面量（单引号/双引号/模板串）
const ZH_IN_STR = /([\u4e00-\u9fa5])/;

const results = [];

function walk(dir) {
  let entries;
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
  catch { return; }
  for (const e of entries) {
    if (SKIP.includes(e.name)) continue;
    const full = path.join(dir, e.name);
    if (e.isDirectory()) walk(full);
    else if (exts.some(x => e.name.endsWith('.' + x))) scanFile(full);
  }
}

function scanFile(file) {
  const lines = fs.readFileSync(file, 'utf8').split('\n');
  lines.forEach((line, i) => {
    // 每行新建一个带 g 标志的正则，避免 lastIndex 跨行污染
    const re = /(['"`])([^'"`\n]*[\u4e00-\u9fa5][^'"`\n]*)\1/g;
    let m;
    while ((m = re.exec(line))) {
      results.push({
        file: path.relative(root, file),
        line: i + 1,
        text: m[2].trim()
      });
    }
  });
}

walk(root);
console.log(JSON.stringify(results, null, 2));
```

### 预期输出

在测试项目根目录跑 `node ~/.claude/skills/i18n-extract/scripts/scan_zh.js src`，你会得到：

```json
[
  { "file": "src/App.jsx", "line": 8,  "text": "个人中心" },
  { "file": "src/Home.jsx", "line": 12, "text": "欢迎回来，{name}" },
  { "file": "src/Home.jsx", "line": 31, "text": "退出登录" }
]
```

这份 JSON 就是模型后续所有工作的**唯一数据源**——去重、命名、翻译全部基于它进行。

### 常见问题与解决

- **漏抽模板串里的多行中文？** 正则用了 `[^'"`\n]` 排除了换行，跨行模板串会断。解决：对模板串单独用 `/\u4e00-\u9fa5/` 整文件扫描，或接受这层简化、在报告里标注"跨行模板串需人工补充"。
- **把注释里的中文也算进去了？** 这是 feature 不是 bug——注释里的中文往往也该翻译。若不想，可在 `scanFile` 里先按 `//` 和 `/* */` 粗剥离。
- **引号内还嵌着引号的串匹配错乱？** 字面量正则的天然短板。生产级方案是上 AST 解析（如 `@babel/parser`），教程阶段用正则够用且零依赖。

---

## 五、写参考文件与模板

把会膨胀的内容挪出 `SKILL.md`，是控制 token 成本的核心动作。

`reference/key-conventions.md`（**按需加载**，正文里用链接引用，模型只在需要命名规则时才 `Read` 它）：

```markdown
# i18n 键名命名约定

- 结构：`模块.区域.用途`，例如 `user.profile.editTitle`
- 段落用全小写驼峰（lowerCamelCase），用 `.` 分层，最多 4 层
- 后缀语义化：
  - 按钮用 `.btn`（`auth.login.btn`）
  - 标题用 `.title`（`home.welcome.title`）
  - 提示/说明用 `.tip`（`form.email.tip`）
  - 校验错误用 `.err`（`form.email.err`）
- 含变量的键，值里用 `{var}` 占位，例如 `"home.welcome.title": "欢迎回来，{name}"`
- 同一文案出现在多个模块，各自建立独立键，避免跨模块耦合
```

`templates/locale.json`（给模型看输出长什么样）：

```json
{
  "scope.section.key": "译文（保留 {placeholder} 占位符）"
}
```

这两份文件分别示范了 Skill 的两类支撑文件：**reference**（被读取的知识）与 **template**（被填写的格式）。`SKILL.md` 保持精简、只负责导航，是它们能各司其职的前提。

---

## 六、测试、调用与调试

Skill 在 `~/.claude/skills/` 下增删改**会话内即时生效**，无需重启（仅顶层全新目录才需重启一次）。测试分三步：

**1. 直接验证脚本**（先确保机械层正确）：

```bash
node ~/.claude/skills/i18n-extract/scripts/scan_zh.js /path/to/project/src
```

**2. 验证 Skill 被识别**：在 Claude Code 里输入 `/`，应能在列表看到 `i18n-extract`，参数提示显示 `[扫描目录] [目标语言...]`。

**3. 端到端调用**：

```
/i18n-extract src en-US ja-JP
```

模型会：先跑 `scan_zh.js src` 拿到 JSON → 去重并按约定命名 → 写出 `locales/zh-CN.json`、`locales/en-US.json`、`locales/ja-JP.json` → 产出报告。

**常见问题**：

- **Skill 一直不自动触发**：强化 `description` 触发词；或它本来就该手动触发，那就保持现状，用 `/` 主动调。
- **想禁止模型擅自触发**（比如有副作用的 Skill）：加 `disable-model-invocation: true`，只允许你手动 `/` 调用。
- **想隐藏进 `/` 菜单、只当背景知识**：加 `user-invocable: false`。
- **重新加载**：改 `SKILL.md` 文本即时生效；若 Skill 同时是 plugin，改了 `hooks/`、`.mcp.json` 才需 `/reload-plugins`。
- **压缩后 Skill "失灵"**：auto-compaction 后每个 Skill 只保留最近一次调用且共享 25,000 token 预算，老的会被丢。重跑一次 `/i18n-extract` 即可恢复完整指令。

---

## 七、实战案例：把一个 React 项目抽成三语言包

假设项目 `my-app/src` 里有这些写死的中文：

```jsx
// src/App.jsx
<h1>个人中心</h1>
<button>退出登录</button>

// src/Home.jsx
<p>欢迎回来，{name}</p>
<span>当前余额：{balance} 元</span>
```

执行 `/i18n-extract src en-US ja-JP`，模型产出：

`locales/zh-CN.json`：
```json
{
  "account.title": "个人中心",
  "account.logout.btn": "退出登录",
  "home.welcome.title": "欢迎回来，{name}",
  "home.balance.tip": "当前余额：{balance} 元"
}
```

`locales/en-US.json`：
```json
{
  "account.title": "My Account",
  "account.logout.btn": "Sign Out",
  "home.welcome.title": "Welcome back, {name}",
  "home.balance.tip": "Current balance: {balance}"
}
```

`locales/ja-JP.json`：
```json
{
  "account.title": "マイアカウント",
  "account.logout.btn": "ログアウト",
  "home.welcome.title": "おかえりなさい、{name}",
  "home.balance.tip": "現在の残高：{balance}"
}
```

**报告**（模型在对话里输出）：
```
扫描：2 个文件，抽取 4 条中文文案
去重后键数：4（无重复）
语言包：zh-CN.json / en-US.json / ja-JP.json 已写入 locales/
歧义待确认：0
提示：源码尚未改写为 t('key')，review 资源文件后可用
     /i18n-extract-replace 一键替换（需你授权）。
```

`{name}`、`{balance}` 占位符在三种语言里都被原样保留——这是 `SKILL.md` 正文铁律的功劳，证明指令确实在持续约束模型行为。整个流程从一条命令到三份语言包，耗时以秒计。

---

## 工作流程图

```mermaid
flowchart TD
    A["用户输入<br/>/i18n-extract src en-US ja-JP"] --> B{"会话启动时<br/>仅加载 description"}
    B -->|"$ARGUMENTS 替换 + 脚本预执行"| C["scan_zh.js 机械扫描<br/>输出中文文案 JSON"]
    C --> D["模型读取 JSON<br/>去重 + 判断歧义"]
    D --> E{"需要命名规范?"}
    E -->|是| F["按需 Read<br/>reference/key-conventions.md"]
    E -->|否| G["生成语义化键名"]
    F --> G
    G --> H["写 zh-CN.json<br/>键名→原文"]
    G --> I["写 en-US / ja-JP.json<br/>键名→译文"]
    H --> J["输出报告<br/>条数/键数/歧义清单"]
    I --> J
    J --> K["用户 review 后<br/>决定是否改写源码"]

    style C fill:#e8f5e9
    style H fill:#fff3e0
    style I fill:#fff3e0
    style F fill:#e3f2fd
```

图中绿色节点是"被执行、不进上下文"的脚本层，橙色是产物，蓝色是按需加载的参考层——三层分工一目了然，正是渐进式披露的工程兑现。

---

## 进阶：还能怎么强化这个 Skill

- **键名冲突检测**：扫描既有 `locales/*.json`，发现重复键时在报告里报警。
- **接入 AST**：把 `scan_zh.js` 升级为 `@babel/parser` 解析，精准命中 JSX 文本节点、模板字面量，告别正则盲区。
- **一键改写模式**：另开一个 `disable-model-invocation: true` 的 `i18n-replace` Skill，在用户授权后把源码中文替换为 `t('key')` 调用并自动 `import` ——把高风险动作和低风险抽取**物理隔离**到两个 Skill。
- **堆叠调用**：新版支持 `/a /b 参数` 一次性加载多个 Skill，可让 `/i18n-extract` 和 `/code-review` 串联，抽完文案顺便审一遍改动。
- **`paths` 限定**：加 `paths: "**/*.{jsx,tsx,vue}"`，让模型只在编辑这些文件时才考虑触发，进一步收敛误触发。

---

## 延伸阅读

- **Claude Code 官方文档 · Extend Claude with skills** — 最权威的 frontmatter 字段表、字符串替换、支撑文件机制来源。
- **anthropics/claude-code · skill-development/SKILL.md** — 官方自己写"如何写好 Skill"的 Skill，强调强触发描述、渐进式披露与祈使句写作。
- **AnhTu.dev · Claude Code Skills 2026 — Progressive Disclosure** — 工程团队视角的三层渐进式披露深度解读。
- **DesignRevision · Claude Code Skills: The Complete Guide (2026)** — 从"一个文件夹 + SKILL.md + 好描述"起步的完整实战指南。

---

## 训练营适用性评估

- **适用类型**：前端/全栈工程提效训练营、AI 辅助开发实战营、Claude Code 进阶训练营。不适合纯零基础——需要正则、Node、命令行基础。
- **教学时长**：建议 90–120 分钟。其中 20 分钟讲渐进式披露原理、40 分钟动手搭 Skill（脚本 + SKILL.md）、30 分钟跑实战案例与调参、20 分钟做进阶改造练习。
- **考核点**：学员能独立产出一个**扫描脚本 + 精简 SKILL.md + 按需 reference** 三件套，并说清"为什么这么分工"。
- **可复用骨架**：本教程的"脚本取数 → 正文指挥 → reference 知识"模式可直接迁移到 SQL 迁移生成、Changelog 抽取、依赖体检等任何"机械扫 + 语义加工"型 Skill，举一反三价值高。

---

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方 Skill 开发文档，frontmatter 字段、字符串替换、支撑文件与渐进式披露的权威来源。
2. [skill-development/SKILL.md — anthropics/claude-code (GitHub)](https://github.com/anthropics/claude-code/blob/main/plugins/plugin-dev/skills/skill-development/SKILL.md) — 官方"如何写好 Skill"的元 Skill，强触发描述与祈使句写作规范。
3. [Claude Code Skills 2026 — Progressive Disclosure — AnhTu.dev](https://anhtu.dev/claude-code-skills-2026-progressive-disclosure-and-how-to-standardise-workflow-for-engineering-teams-1062) — 三层渐进式披露的工程团队深度解读。
4. [Claude Agent Skills Framework: Build Specialized AI Agents — DigitalApplied](https://www.digitalapplied.com/blog/claude-agent-skills-framework-guide) — SKILL.md 结构与跨端（Claude.ai / Code / API）框架指南。
5. [Claude Code Skills: The Complete Guide (2026) — DesignRevision](https://designrevision.com/blog/claude-code-skills) — 从零到生产的 Skill 完整实战指南。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
