本教程带你从零构建一个名为 i18n-extract 的自定义 Skill:它扫描项目源码里写死的中文文案,自动抽取成 zh-CN.json / en-US.json 多语言资源文件,并产出待人工确认的歧义清单。你将掌握 Skill 的目录结构、SKILL.md frontmatter 写法、渐进式披露(progressive disclosure)、捆绑脚本与 LLM 的分工模式,以及 ${CLAUDE_SKILL_DIR} 等字符串替换机制。适合会写命令行、做过前端/全栈项目、想让 Claude 替自己处理重复性工程任务的开发者。
v2.1.196+,下文涉及 ${CLAUDE_PROJECT_DIR} 等较新特性)很多人把 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 放在哪里,决定了它的作用域。官方约定:
~/.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 正文(给模型的指令)。
---
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)
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。
#!/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,你会得到:
[
{ "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/` 整文件扫描,或接受这层简化、在报告里标注"跨行模板串需人工补充"。scanFile 里先按 // 和 /* */ 粗剥离。@babel/parser),教程阶段用正则够用且零依赖。把会膨胀的内容挪出 SKILL.md,是控制 token 成本的核心动作。
reference/key-conventions.md(按需加载,正文里用链接引用,模型只在需要命名规则时才 Read 它):
# 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(给模型看输出长什么样):
{
"scope.section.key": "译文(保留 {placeholder} 占位符)"
}
这两份文件分别示范了 Skill 的两类支撑文件:reference(被读取的知识)与 template(被填写的格式)。SKILL.md 保持精简、只负责导航,是它们能各司其职的前提。
Skill 在 ~/.claude/skills/ 下增删改会话内即时生效,无需重启(仅顶层全新目录才需重启一次)。测试分三步:
1. 直接验证脚本(先确保机械层正确):
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 → 产出报告。
常见问题:
description 触发词;或它本来就该手动触发,那就保持现状,用 / 主动调。disable-model-invocation: true,只允许你手动 / 调用。/ 菜单、只当背景知识:加 user-invocable: false。SKILL.md 文本即时生效;若 Skill 同时是 plugin,改了 hooks/、.mcp.json 才需 /reload-plugins。/i18n-extract 即可恢复完整指令。假设项目 my-app/src 里有这些写死的中文:
// 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:
{
"account.title": "个人中心",
"account.logout.btn": "退出登录",
"home.welcome.title": "欢迎回来,{name}",
"home.balance.tip": "当前余额:{balance} 元"
}
locales/en-US.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:
{
"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 正文铁律的功劳,证明指令确实在持续约束模型行为。整个流程从一条命令到三份语言包,耗时以秒计。
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
图中绿色节点是"被执行、不进上下文"的脚本层,橙色是产物,蓝色是按需加载的参考层——三层分工一目了然,正是渐进式披露的工程兑现。
locales/*.json,发现重复键时在报告里报警。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}",让模型只在编辑这些文件时才考虑触发,进一步收敛误触发。[!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 等场景全覆盖。
