本教程带你从零构建一个名为 i18n-extract 的 Claude Code 自定义技能,它能扫描源码中散落的硬编码用户文案,自动抽成翻译 key、改写成 t('...') 调用、并播种多语言翻译文件。适合已写过几个 Skill、想掌握「目录结构 + 支撑文件 + 内嵌脚本 + 权限预授权 + 描述调优」完整闭环的开发者。学完你能独立造出任何「知识库 + 脚本」型技能。
${CLAUDE_PROJECT_DIR} 替换、计划任务抑制等特性需要该版本)很多人会把所有规范都塞进 CLAUDE.md,结果文件膨胀到几千行,每次会话都全量加载、持续烧 token。官方文档对二者的分工给了一条清晰的判据:
当你反复把同一段指令、清单或多步流程粘贴进对话,或者 CLAUDE.md 的某一节从「事实」长成了「流程」时,就把它抽成 Skill。与 CLAUDE.md 不同,Skill 的正文只有在被使用时才加载,长篇参考资料在用不到时几乎零成本。
i18n 国际化正是这种典型场景:它不是一条「事实」(像「我们用 React 18」),而是一套多步流程(扫字符串 → 提 key → 改代码 → 生翻译文件 → 校验),偶尔才用一次,但一旦用起来要执行十几个精确步骤。把它做成 Skill,平时只占一行 description,触发时才把整套流程连同脚本、词表一起灌进上下文。
判断清单(满足任意一条就值得抽成 Skill):
| 信号 | 例子 |
|---|---|
| 反复粘贴同一套步骤 | 每次发版前手动跑国际化检查清单 |
| 需要附带脚本/数据文件 | 一个 Python 抽取器 + 一份术语对照表 |
| 要按需加载、平时不占上下文 | 国际化只在接入新语言时才做 |
| 想让 Claude 自动判断何时触发 | 用户说「把这些文案翻译成英文」时自动启用 |
每个 Skill 就是一个目录,入口文件必须叫 SKILL.md,由两部分组成:
--- 之间):告诉 Claude「这是什么、何时用、谁触发」。官方给的最小骨架长这样:
---
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。
最终目录结构:
~/.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/。
mkdir -p ~/.claude/skills/i18n-extract/{references,data,scripts}
先写一个能跑的最小 SKILL.md,验证触发链路通了再迭代:
---
description: 抽取代码里的硬编码用户文案,生成 i18n 翻译 key 并改写调用。
---
扫描当前项目,找出所有写死的用户可见字符串,提议翻译 key。
在任意项目里 claude 启动后问一句「帮我把代码里写死的中文抽成翻译 key」。如果它自动启用或你能 /i18n-extract 调出,骨架就通了。
预期结果:输入
/应能在补全里看到i18n-extract;问「抽文案」时 Claude 会自动加载该技能。若没出现,多半是 YAML 解析失败——加--debug启动可看到解析报错。
description 是技能的「广告位」:Claude 拿它和你的话做语义匹配。差的描述会导致两类问题——不触发(关键词没对上用户原话)或误触发(描述太宽,啥都往里套)。
官方提示:把最关键的用例放最前面,且 description + when_to_use 合计上限 1536 字符。重写如下:
---
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 的三条经验:
when_to_use,命中率最高。正文里只放「总览 + 导航」,把长篇规则、术语表挪到 references/ 和 data/,Claude 按需读取。这能省下大量常驻 token(官方建议 SKILL.md 控制在 500 行内)。
references/extraction-rules.md(节选):
# 用户可见文案判定规则
## 必须抽取
- 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:
# Key 命名约定
结构:scope.context.element[.state]
- common.button.submit
- auth.login.placeholder.email
- order.detail.status.pending
全小写,点分层级,禁止中文/空格。
data/glossary.yml(术语对照表,脚本播种翻译时优先套用):
submit: 提交
cancel: 取消
confirm: 确认
loading: 加载中
email: 邮箱
password: 密码
welcome: 欢迎
然后在 SKILL.md 里挂上导航指针:
## 参考资料
- 判定哪些字符串算「用户可见」:见 [references/extraction-rules.md](references/extraction-rules.md)
- key 怎么命名:见 [references/key-naming.md](references/key-naming.md)
- 术语优先翻译:使用 [data/glossary.yml](data/glossary.yml)
${CLAUDE_SKILL_DIR} 定位国际化最容易踩的坑是 Claude 「猜」字符串——它看不到全仓每个 JSX 节点。正确做法是让脚本先扫一遍,把真实结果灌进 prompt。这就是动态上下文注入 + 内嵌脚本的组合拳。
scripts/extract_strings.py(仅标准库,可独立运行):
#!/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 正文里用 !`...` 把扫描结果直接注入(注意:反引号注入只在行首或空白后才会执行):
## 第 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} 让脚本路径不依赖当前工作目录;多行命令必须用 ```! 围栏而非内联 !` `;注入只跑一遍,命令输出不会被二次扫描。
技能要连读带改还要跑脚本,默认每步都弹权限确认很烦。allowed-tools 在技能激活期间给指定工具免审批通行证:
allowed-tools: Bash(python3 *) Read Edit Write Grep Glob
它只「授予」不「限制」——没列的工具仍按你全局权限规则走。对项目级 .claude/skills/ 里的技能,首次需通过工作区信任对话框才生效,所以审阅团队仓库的技能时务必看一眼它的 allowed-tools,一个技能可以给自己授予很宽的工具权限。
技能「被触发」不等于「做对了」。官方推荐的验证方法是基线对比:准备几条真实 prompt,分别在「开启技能」和「禁用技能」的新会话里各跑一遍,对比结果。一定要用新会话,否则你写技能时残留的上下文会掩盖指令的漏洞。
手动流程可以,但更省事的是装官方 skill-creator 插件自动化这条循环:
/plugin install skill-creator@claude-plugins-official
/reload-plugins
然后说「用 skill-creator 评估我的 i18n-extract 技能」。它会:
evals/evals.jsongrading.jsonbenchmark.json对 i18n 技能,典型 eval 用例包括:正向(「把这些中文按钮文案抽成翻译 key」应触发并输出 key)、负向(「把后端日志里的 ERROR 翻译一下」不应触发、或触发后应明确拒绝抽取日志)。
假设有 src/Login.jsx:
export function Login() {
return (
<form>
<h2>用户登录</h2>
<input placeholder="请输入邮箱" />
<input placeholder="请输入密码" type="password" />
<button onClick={() => alert('登录成功')}>提交</button>
</form>
);
}
在项目根执行 /i18n-extract,预期会发生:
用户登录、请输入邮箱、请输入密码、登录成功、提交 五条。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>
);
}
submit→提交)播种 locales/:// 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" }
verify_keys.py 比对两个 locale 文件 key 集合一致,输出 ✅ key 对齐。整个过程从硬编码到双语言可发布,一条 /i18n-extract 搞定。
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 文件时自动激活,进一步防误触发。PreToolUse 校验 Edit/Write 不许再引入新的硬编码中文,把「不许裸写文案」从软约束变硬护栏。.claude-plugin/plugin.json,技能连脚本一起进团队市场,命名空间 my-plugin:i18n-extract。code.claude.com/docs/en/skillsplatform.claude.com/docs/en/agents-and-tools/agent-skills/best-practicesagentskills.io/skill-creation/evaluating-skillsgithub.com/alirezarezvani/claude-skills 的 progressive_disclosure_principles.md| 维度 | 评估 |
|---|---|
| 适合训练营 | 中级「Claude Code 进阶开发营」「Agent 工程实战营」 |
| 前置要求 | 学员需先做过至少一个 Hello World 级 Skill |
| 建议教学时长 | 3.5–4 小时(含动手改造自己的项目) |
| 实操占比 | 约 60% 代码编写 + 40% 概念与调优 |
| 可裁剪点 | 时间紧可跳过 evals 量化与 context: fork 进阶,保留前 5 步即出可用技能 |
| 考核点 | 学员能独立造出一个「自带脚本 + 数据表 + 精准 description」的领域技能,并通过基线对比验证 |
[!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 等场景全覆盖。
