<!-- title: 造i18n技能：硬编码转多语言 -->

# 造i18n技能：硬编码转多语言

## 概述

本教程带你从零构建一个名为 `i18n-extract` 的 Claude Code 自定义技能，它能扫描源码中散落的硬编码用户文案，自动抽成翻译 key、改写成 `t('...')` 调用、并播种多语言翻译文件。适合已写过几个 Skill、想掌握「目录结构 + 支撑文件 + 内嵌脚本 + 权限预授权 + 描述调优」完整闭环的开发者。学完你能独立造出任何「知识库 + 脚本」型技能。

## 前置条件

- Claude Code v2.1.196 及以上（`${CLAUDE_PROJECT_DIR}` 替换、计划任务抑制等特性需要该版本）
- 已装 Python 3（示例脚本仅用标准库，无需 pip 安装）
- 一个含硬编码文案的前端项目（React/Vue/原生 JS 均可）用于练手
- 读过官方「Extend Claude with skills」页面，理解 Skill 与 CLAUDE.md 的边界

## 一、先搞懂：什么时候该造一个 Skill，而不是写进 CLAUDE.md

很多人会把所有规范都塞进 `CLAUDE.md`，结果文件膨胀到几千行，每次会话都全量加载、持续烧 token。官方文档对二者的分工给了一条清晰的判据：

> 当你反复把同一段指令、清单或多步流程粘贴进对话，或者 CLAUDE.md 的某一节从「事实」长成了「流程」时，就把它抽成 Skill。与 CLAUDE.md 不同，Skill 的正文只有在被使用时才加载，长篇参考资料在用不到时几乎零成本。

i18n 国际化正是这种典型场景：它不是一条「事实」（像「我们用 React 18」），而是一套多步流程（扫字符串 → 提 key → 改代码 → 生翻译文件 → 校验），偶尔才用一次，但一旦用起来要执行十几个精确步骤。把它做成 Skill，平时只占一行 description，触发时才把整套流程连同脚本、词表一起灌进上下文。

判断清单（满足任意一条就值得抽成 Skill）：

| 信号 | 例子 |
| :--- | :--- |
| 反复粘贴同一套步骤 | 每次发版前手动跑国际化检查清单 |
| 需要附带脚本/数据文件 | 一个 Python 抽取器 + 一份术语对照表 |
| 要按需加载、平时不占上下文 | 国际化只在接入新语言时才做 |
| 想让 Claude 自动判断何时触发 | 用户说「把这些文案翻译成英文」时自动启用 |

## 二、解剖 SKILL.md：frontmatter + 正文两段式

每个 Skill 就是一个目录，入口文件必须叫 `SKILL.md`，由两部分组成：

1. **YAML frontmatter**（`---` 之间）：告诉 Claude「这是什么、何时用、谁触发」。
2. **Markdown 正文**：Claude 真正执行的指令，可引用目录内的其它文件。

官方给的最小骨架长这样：

```yaml
---
description: Summarizes uncommitted changes and flags anything risky.
---
## Current changes
!`git diff HEAD`
## Instructions
Summarize the changes above in two or three bullet points...
```

关键 frontmatter 字段（全部可选，只有 `description` 强烈推荐）：

| 字段 | 作用 |
| :--- | :--- |
| `name` | 显示名，默认用目录名 |
| `description` | Claude 用来判断何时自动加载；**关键用例放最前面** |
| `when_to_use` | 触发短语/示例，拼接到 description 后，共享 1536 字符上限 |
| `allowed-tools` | 技能激活时免审批可用的工具 |
| `disable-model-invocation` | 设 `true` 则只能手动 `/` 调用 |
| `user-invocable` | 设 `false` 则从 `/` 菜单隐藏，只给 Claude 自动用 |
| `argument-hint` / `arguments` | 自动补全提示与具名位置参数 |
| `model` / `effort` | 激活期间覆盖会话模型与思考强度 |

正文里还能用一组替换变量，本教程会用到三个：

- `${CLAUDE_SKILL_DIR}` —— 当前 Skill 所在目录，挂脚本时用它定位，跟安装层级无关。
- `${CLAUDE_PROJECT_DIR}` —— 项目根目录（v2.1.196+）。
- `$ARGUMENTS` / `$0` `$1` —— 用户 `/i18n-extract` 后面跟的参数。

还有一个极其重要的语法：`` !`命令` `` **动态上下文注入**。它在正文发给 Claude **之前**先把命令跑一遍、用输出替换占位符。下文会用它把脚本扫描结果直接灌进 prompt。

## 三、实战：从零造 i18n-extract 技能

最终目录结构：

```text
~/.claude/skills/i18n-extract/
├── SKILL.md                      # 入口：流程总览 + 导航
├── references/
│   ├── extraction-rules.md       # 什么算「用户可见文案」
│   └── key-naming.md             # key 命名约定
├── data/
│   └── glossary.yml              # 通用术语中英对照
└── scripts/
    ├── extract_strings.py        # 硬编码字符串抽取器
    └── verify_keys.py            # 翻译文件缺 key 校验
```

放在 `~/.claude/skills/` 下是「个人级」，对你所有项目生效；想只对某个项目生效，就放该项目的 `.claude/skills/`。

### 步骤 1：建目录、写最小骨架

```bash
mkdir -p ~/.claude/skills/i18n-extract/{references,data,scripts}
```

先写一个能跑的最小 `SKILL.md`，验证触发链路通了再迭代：

```markdown
---
description: 抽取代码里的硬编码用户文案，生成 i18n 翻译 key 并改写调用。
---
扫描当前项目，找出所有写死的用户可见字符串，提议翻译 key。
```

在任意项目里 `claude` 启动后问一句「帮我把代码里写死的中文抽成翻译 key」。如果它自动启用或你能 `/i18n-extract` 调出，骨架就通了。

> **预期结果**：输入 `/` 应能在补全里看到 `i18n-extract`；问「抽文案」时 Claude 会自动加载该技能。若没出现，多半是 YAML 解析失败——加 `--debug` 启动可看到解析报错。

### 步骤 2：打磨 description，让它精准触发

description 是技能的「广告位」：Claude 拿它和你的话做语义匹配。差的描述会导致两类问题——**不触发**（关键词没对上用户原话）或**误触发**（描述太宽，啥都往里套）。

官方提示：把最关键的用例放最前面，且 `description` + `when_to_use` 合计上限 1536 字符。重写如下：

```yaml
---
description: >
  扫描前端源码中写死的用户可见文案（中文/英文），抽取为 i18n 翻译 key，
  把硬编码改写成 t('key') 调用，并按术语表播种多语言翻译文件。
  Use when the user wants to internationalize a project, extract hardcoded
  UI strings, add multi-language support, generate translation files,
  or translate user-facing text into other languages.
  Triggers: "国际化"、"i18n"、"多语言"、"这些文案翻译"、"硬编码文案"、"提取翻译 key"。
disable-model-invocation: false
allowed-tools: Bash(python3 *) Read Edit Write Grep Glob
---
```

写 description 的三条经验：

1. **中英混合 + 触发词列表示例**：把用户可能说的原话（中英文）都塞进 `when_to_use`，命中率最高。
2. **动词开头说清产出**：「抽取…改写…播种…」比「一个 i18n 工具」具体得多。
3. **限定作用域防误触**：加「前端源码」「用户可见文案」，避免在改后端日志时被误启用。

### 步骤 3：加支撑文件，做渐进披露

正文里只放「总览 + 导航」，把长篇规则、术语表挪到 `references/` 和 `data/`，Claude 按需读取。这能省下大量常驻 token（官方建议 SKILL.md 控制在 500 行内）。

`references/extraction-rules.md`（节选）：

```markdown
# 用户可见文案判定规则
## 必须抽取
- JSX 文本节点：<button>提交</button>
- 表单属性：placeholder、title、aria-label、alt
- 弹窗/通知：alert(message)、toast(message)、console.warn 之外的用户提示
- 路由/菜单 label、表头、按钮文案

## 不要抽取
- console.log / 调试日志
- API 路径、CSS 类名、事件名
- 已在 t() / i18n() / $t() 内的 key
- 注释里的中文
```

`references/key-naming.md`：

```markdown
# Key 命名约定
结构：scope.context.element[.state]
- common.button.submit
- auth.login.placeholder.email
- order.detail.status.pending
全小写，点分层级，禁止中文/空格。
```

`data/glossary.yml`（术语对照表，脚本播种翻译时优先套用）：

```yaml
submit: 提交
cancel: 取消
confirm: 确认
loading: 加载中
email: 邮箱
password: 密码
welcome: 欢迎
```

然后在 `SKILL.md` 里挂上导航指针：

```markdown
## 参考资料
- 判定哪些字符串算「用户可见」：见 [references/extraction-rules.md](references/extraction-rules.md)
- key 怎么命名：见 [references/key-naming.md](references/key-naming.md)
- 术语优先翻译：使用 [data/glossary.yml](data/glossary.yml)
```

### 步骤 4：挂抽取脚本，用 `${CLAUDE_SKILL_DIR}` 定位

国际化最容易踩的坑是 Claude 「猜」字符串——它看不到全仓每个 JSX 节点。正确做法是让脚本先扫一遍，把真实结果灌进 prompt。这就是动态上下文注入 + 内嵌脚本的组合拳。

`scripts/extract_strings.py`（仅标准库，可独立运行）：

```python
#!/usr/bin/env python3
"""扫描前端源码，列出疑似硬编码的用户可见文案。"""
import re, json, sys
from pathlib import Path

SKIP_DIRS = {'.git', 'node_modules', 'dist', 'build', '.next', '__pycache__'}
EXTS = {'.js', '.jsx', '.ts', '.tsx', '.vue'}
# JSX 文本节点 / 常见用户可见属性
PATTERNS = [
    re.compile(r'>([\u4e00-\u9fa5][^<]{0,40})<'),                 # <tag>中文</tag>
    re.compile(r'placeholder="([^"]{1,60})"'),
    re.compile(r'title="([^"]{1,60})"'),
    re.compile(r'aria-label="([^"]{1,60})"'),
    re.compile(r'alt="([^"]{1,60})"'),
    re.compile(r"""alert\(['"]([^'"]{1,60})['"]\)"""),
]

def scan(root: Path):
    hits = []
    for p in root.rglob('*'):
        if any(part in SKIP_DIRS for part in p.parts):
            continue
        if p.suffix not in EXTS:
            continue
        for i, line in enumerate(p.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
            for pat in PATTERNS:
                for m in pat.finditer(line):
                    val = m.group(1).strip()
                    if val and not re.match(r'^[a-zA-Z0-9_\-/.]+$', val):  # 排除纯标识符
                        hits.append({'value': val, 'file': str(p), 'line': i})
    # 按值去重
    seen, uniq = set(), []
    for h in hits:
        if h['value'] not in seen:
            seen.add(h['value']); uniq.append(h)
    return uniq

if __name__ == '__main__':
    root = Path(sys.argv[1]).resolve()
    print(json.dumps(scan(root), ensure_ascii=False, indent=2))
```

在 `SKILL.md` 正文里用 `` !`...` `` 把扫描结果直接注入（注意：反引号注入只在行首或空白后才会执行）：

````markdown
## 第 1 步：扫描硬编码文案
先运行抽取脚本，结果已注入下方：

```!
python3 ${CLAUDE_SKILL_DIR}/scripts/extract_strings.py ${CLAUDE_PROJECT_DIR}
```

## 第 2 步：生成 key 并改写
- 对上面每条 `value`，按 [key-naming.md](references/key-naming.md) 生成 key
- 用 Edit 把对应位置的硬编码替换为 `t('scope.context.xxx')`
- 翻译时优先套用 [glossary.yml](data/glossary.yml) 中的固定译法

## 第 3 步：播种翻译文件
生成/更新 `locales/zh-CN.json` 与 `locales/en-US.json`，结构为 `{ "common.button.submit": "提交" }`。

## 第 4 步：校验完整性
```bash
python3 ${CLAUDE_SKILL_DIR}/scripts/verify_keys.py ${CLAUDE_PROJECT_DIR}/locales
```
````

注意三个易错点：`${CLAUDE_SKILL_DIR}` 让脚本路径不依赖当前工作目录；多行命令必须用 ` ```! ` 围栏而非内联 `` !` ` ``；注入只跑一遍，命令输出不会被二次扫描。

### 步骤 5：用 allowed-tools 预授权，省掉审批弹窗

技能要连读带改还要跑脚本，默认每步都弹权限确认很烦。`allowed-tools` 在技能激活期间给指定工具免审批通行证：

```yaml
allowed-tools: Bash(python3 *) Read Edit Write Grep Glob
```

它只「授予」不「限制」——没列的工具仍按你全局权限规则走。对项目级 `.claude/skills/` 里的技能，首次需通过工作区信任对话框才生效，所以**审阅团队仓库的技能时务必看一眼它的 `allowed-tools`**，一个技能可以给自己授予很宽的工具权限。

### 步骤 6：测试、迭代、用 evals 量化

技能「被触发」不等于「做对了」。官方推荐的验证方法是**基线对比**：准备几条真实 prompt，分别在「开启技能」和「禁用技能」的新会话里各跑一遍，对比结果。**一定要用新会话**，否则你写技能时残留的上下文会掩盖指令的漏洞。

手动流程可以，但更省事的是装官方 `skill-creator` 插件自动化这条循环：

```text
/plugin install skill-creator@claude-plugins-official
/reload-plugins
```

然后说「用 skill-creator 评估我的 i18n-extract 技能」。它会：

- 把 prompt、输入文件、期望行为存进 `evals/evals.json`
- 每个用例起一个干净 subagent 跑，记录 token 与耗时
- 按断言判 pass/fail，写 `grading.json`
- 汇总「有技能 vs 无技能」的通过率/耗时/token 到 `benchmark.json`
- 还能做两个版本的盲测 A/B，确认你这次改动是真改进再提交
- 自动生成 should-trigger / should-not-trigger prompt，测描述是否误触发，并提议 description 改写

对 i18n 技能，典型 eval 用例包括：正向（「把这些中文按钮文案抽成翻译 key」应触发并输出 key）、负向（「把后端日志里的 ERROR 翻译一下」不应触发、或触发后应明确拒绝抽取日志）。

## 四、端到端案例：把一个迷你 React 项目国际化

假设有 `src/Login.jsx`：

```jsx
export function Login() {
  return (
    <form>
      <h2>用户登录</h2>
      <input placeholder="请输入邮箱" />
      <input placeholder="请输入密码" type="password" />
      <button onClick={() => alert('登录成功')}>提交</button>
    </form>
  );
}
```

在项目根执行 `/i18n-extract`，预期会发生：

1. 脚本注入扫描结果：`用户登录`、`请输入邮箱`、`请输入密码`、`登录成功`、`提交` 五条。
2. Claude 生成 key 并改写源码：

```jsx
export function Login() {
  const { t } = useTranslation();
  return (
    <form>
      <h2>{t('auth.login.title')}</h2>
      <input placeholder={t('auth.login.placeholder.email')} />
      <input placeholder={t('auth.login.placeholder.password')} type="password" />
      <button onClick={() => alert(t('auth.login.success'))}>{t('common.button.submit')}</button>
    </form>
  );
}
```

3. 套用 glossary（`submit→提交`）播种 `locales/`：

```json
// locales/zh-CN.json
{ "auth.login.title": "用户登录",
  "auth.login.placeholder.email": "请输入邮箱",
  "auth.login.placeholder.password": "请输入密码",
  "auth.login.success": "登录成功",
  "common.button.submit": "提交" }
// locales/en-US.json
{ "auth.login.title": "User Login",
  "auth.login.placeholder.email": "Enter your email",
  "auth.login.placeholder.password": "Enter your password",
  "auth.login.success": "Login successful",
  "common.button.submit": "Submit" }
```

4. `verify_keys.py` 比对两个 locale 文件 key 集合一致，输出 `✅ key 对齐`。

整个过程从硬编码到双语言可发布，一条 `/i18n-extract` 搞定。

## 五、技能加载与触发的完整生命周期

```mermaid
flowchart TD
    A[会话启动] --> B[扫描 skills 目录]
    B --> C[只把 name+description 装进上下文<br/>正文暂不加载]
    C --> D{用户说话 / 输入斜杠命令}
    D -->|语义匹配 description| E[Claude 决定自动加载]
    D -->|/i18n-extract| F[手动加载]
    E --> G[注入一次 SKILL.md 正文]
    F --> G
    G --> H[先跑 !`...` 动态注入<br/>脚本扫描结果]
    H --> I[Claude 读 references/data 按需取用]
    I --> J[用 allowed-tools 免审批执行]
    J --> K[正文留在上下文<br/>后续轮次持续生效]
    K --> L{上下文压缩?}
    L -->|是| M[每个技能保留前 5000 token<br/>共享 25000 token 预算]
    L -->|否| K
```

两个要记住的机制：正文一旦加载，**整段会留在上下文直到会话结束**（所以正文要精炼、说做什么而非怎么做）；压缩时每个被调过的技能保留最近一次的前 5000 token，多个共享 25000 上限，**最近调用的优先保留**，老旧的可能被整段丢掉——所以压缩后若感觉技能「失效」，重新 `/i18n-extract` 一次即可恢复全文。

## 六、常见坑与排查

| 症状 | 原因 | 解决 |
| :--- | :--- | :--- |
| `/` 菜单里找不到技能 | YAML 解析失败，元数据为空 | `claude --debug` 启动看解析报错，修缩进/引号 |
| 该触发却不触发 | description 关键词没对上用户原话 | 补 `when_to_use` 触发短语，关键用例前置 |
| 不该触发却乱触发 | description 太宽泛 | 收窄作用域词（如「前端」「用户可见」） |
| 描述被截断 | 技能太多超出预算（约上下文窗口的 1%） | `/doctor` 看哪些被裁；`skillOverrides` 把低优先级设 `name-only` |
| 脚本路径找不到 | 用了相对路径 | 改用 `${CLAUDE_SKILL_DIR}/scripts/...` |
| `` !`cmd` `` 没执行 | `!` 前有非空白字符（如 `KEY=!\`cmd\``） | 让 `!` 处于行首或空白后；多行用 ` ```! ` 围栏 |
| 动态注入被禁用 | 全局设了 `disableSkillShellExecution` | 该策略把注入替换为禁用提示文本，改设置或换纯指令 |
| 压缩后技能「失效」 | 正文被压缩预算裁掉 | 重新 `/i18n-extract` 调用恢复全文 |

排查触发问题最有用的一招：直接问 Claude「你有哪些可用的 skills？」确认它能看到 `i18n-extract` 及其完整 description。

## 进阶方向

- **`context: fork` + `agent: Explore`**：让扫描在隔离 subagent 里跑，主对话不被几千行扫描结果污染，结果汇总回传。
- **`paths: src/**/*.jsx`**：限定只在改 JSX 文件时自动激活，进一步防误触发。
- **挂 hooks**：在 `PreToolUse` 校验 `Edit`/`Write` 不许再引入新的硬编码中文，把「不许裸写文案」从软约束变硬护栏。
- **打成 Plugin 发团队**：加 `.claude-plugin/plugin.json`，技能连脚本一起进团队市场，命名空间 `my-plugin:i18n-extract`。

## 延伸阅读

- 官方 Skills 总览（frontmatter 全字段、生命周期、子代理运行）：`code.claude.com/docs/en/skills`
- Skill 编写最佳实践（跨 Claude 产品的通用指导）：`platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices`
- skill-creator 插件与 evals 文件格式：`agentskills.io/skill-creation/evaluating-skills`
- 渐进披露原则与何时拆分参考文件：`github.com/alirezarezvani/claude-skills` 的 progressive_disclosure_principles.md

## 训练营适用性评估

| 维度 | 评估 |
| :--- | :--- |
| 适合训练营 | 中级「Claude Code 进阶开发营」「Agent 工程实战营」 |
| 前置要求 | 学员需先做过至少一个 Hello World 级 Skill |
| 建议教学时长 | 3.5–4 小时（含动手改造自己的项目） |
| 实操占比 | 约 60% 代码编写 + 40% 概念与调优 |
| 可裁剪点 | 时间紧可跳过 evals 量化与 `context: fork` 进阶，保留前 5 步即出可用技能 |
| 考核点 | 学员能独立造出一个「自带脚本 + 数据表 + 精准 description」的领域技能，并通过基线对比验证 |

## 参考来源

1. [Extend Claude with skills — Claude Code Docs](https://code.claude.com/docs/en/skills) — 官方技能总览，frontmatter 全字段表、动态上下文注入、生命周期与触发控制
2. [Skill authoring best practices — Claude Platform Docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) — 跨产品的技能编写指导，description 与正文写作规范
3. [Evaluating skill output quality — agentskills.io](https://agentskills.io/skill-creation/evaluating-skills) — evals 文件格式与 skill-creator 迭代工作流
4. [Progressive Disclosure Architecture — DeepWiki](https://deepwiki.com/spences10/claude-skills-cli/2.1-progressive-disclosure-architecture) — 渐进披露在技能目录结构中的工程化实践
5. [Core Concepts and Progressive Disclosure — Anthropic Cookbooks](https://deepwiki.com/anthropics/claude-cookbooks/4.1-core-concepts-and-progressive-disclosure) — Skills 系统核心架构与按需加载机制解析

> [!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)
