Communitygithub.com

Nelsutro/agent-skills

生产级Web应用国际化与本地化指导。用于添加多语言、翻译UI、提取硬编码字符串、处理复数/性别/ICU消息、区域路由、RTL布局、本地化日期/数字/货币、hreflang/SEO或验证翻译目录。框架无关的指南,适用于React、Next.js、Vue、Svelte、Astro、Remix和原生JS,并提供next-intl、react-i18next、FormatJS和Lingui示例。

agent-skills 是什么?

这是一个专注于Web应用国际化(i18n)和本地化(l10n)的全面技能,旨在帮助开发者将国际化作为架构设计的一部分,而非项目收尾的翻译工作。该技能支持从审计现有代码、设计架构到实现多语言路由、处理复数/性别语法、格式化日期/数字/货币,以及优化RTL布局和SEO的全流程。它适用于React、Next.js、Vue、Svelte、Astro、Remix等主流框架,并提供了next-intl、react-i18next、FormatJS和Lingui等库的具体示例。核心原则包括永不拼接翻译片段、使用语义化键而非英文字符串、为所有用户可见字符串外部化、利用Intl API进行格式化,以及确保服务器端区域状态按请求隔离。技能还包含CI验证、防止常见陷阱(如缓存在CDN中混合用户偏好)以及详细的审查清单,确保生产级质量。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Nelsutro/agent-skills/tree/HEAD/web-i18n

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Web 国际化

国际化是一种架构设计,而不是项目末期的翻译工作。目标是构建一个能正确渲染任何区域设置的UI,无需特殊处理:没有拼接的句子,没有硬编码的格式,没有因德语文本长35%或阿拉伯语翻转方向而导致布局破坏的情况。

何时使用本技能

当用户提出以下要求时使用:

  • 添加语言、语言切换器或区域路由
  • 翻译应用、提取字符串或设置翻译工作流
  • 修复复数、性别或“1 item / 2 items”相关的错误
  • 按区域设置格式化日期、数字、货币、相对时间或列表
  • 支持RTL(阿拉伯语、希伯来语、波斯语、乌尔都语)
  • 添加hreflang、本地化元数据或多语言站点地图
  • 审查现有的i18n配置、缺失的键或翻译漂移
  • 在next-intl、react-i18next、FormatJS、Lingui或TMS之间做出选择

相关参考资料位于references/,脚本位于scripts/。仅读取任务所需的内容。路径相对于本技能所在的目录(技能加载时,代理运行时会打印该路径)。

不可妥协的原则

  1. 绝不通过拼接翻译片段来构建句子。 使用带占位符的完整消息进行翻译:"{count} new messages",而不是 t('new') + ' ' + count + ' ' + t('messages')。
  2. 绝不使用英文字符串作为键。 键是稳定的标识符(inbox.unreadCount),这样文案修改不会导致查找失败或使所有翻译失效。
  3. 每个面向用户的字符串都必须外部化,包括 aria-label、alt、title、placeholder、验证错误、空状态、提示、邮件、推送通知和PDF输出。
  4. 绝不硬编码格式。 不使用 toFixed(2) + '$',不手动写 MM/DD/YYYY,不使用英文月份名称,不假设24小时制。应使用 Intl。
  5. 区域设置是一个BCP 47标签,而不仅仅是语言。pt-BR 和 pt-PT、zh-Hans 和 zh-Hant、es-MX 和 es-ES 是不同的产品。请存储完整的标签。
  6. 服务器上的区域状态必须按请求范围隔离。 在SSR期间,绝不要将当前区域设置放在模块级别的可变变量中;并发请求会导致语言互相泄漏。
  7. 如果翻译来自TMS、社区或CMS,则视为不受信任的输入。 在未进行安全处理的情况下,绝不要将其传递给 innerHTML/dangerouslySetInnerHTML。

工作流

1. 修改代码前先审计

  • 在提出建议前,先检测所使用的技术栈、路由器和现有的i18n库。
  • 查找硬编码的字符串:
    node scripts/scan-hardcoded.mjs src
    
    将输出视为候选列表,而非确定结果:跳过测试夹具、常量、URL和面向开发者的日志。
  • 清点当前已交付的每个区域设置以及目录的存放位置。
  • 优先识别风险最高的区域:认证、结账、错误页面、邮件和法律声明。

2. 确定架构(书面记录)

在编写代码前回答以下问题:

决策项默认值说明
区域策略路径前缀 /es/SEO和缓存效果最佳。仅在确有理由时才使用子域名/国别顶级域。
默认区域源语言不要在未经用户覆盖的情况下,自动将首次访问者重定向到猜测的区域。
区域检测URL → Cookie → Accept-Language → 默认URL优先,这样共享链接不会改变语言。
目录格式每个区域每个命名空间一个JSON文件按需加载命名空间;切勿将所有区域设置都发送到客户端。
消息语法ICU MessageFormat可跨库和TMS移植;支持复数/选择/序数。
键样式feature.component.element稳定、可全局搜索、不包含英文字符串。
翻译数据源TMS或代码仓库,二选一只能选择一个;在CI中单向同步。
回退区域链 es-MX → es → en在开发环境中记录回退情况以发现缺口。

3. 外部化并组织消息

  • 将字符串替换为语义化键,并将原文复制到基础区域目录中。
  • 每个完整句子使用一个键。如果句子包含链接或粗体字,应使用富文本插值而不是拆分句子。
  • 占位符应使用命名方式({userName}),不要使用位置索引({0})。
  • 除非库明确支持类型化标签,否则不要在目录中放入HTML。
  • 按产品面而不是按页面分组:checkout.json、auth.json、errors.json。

关于复数、选择、序数和富文本的规则,请参阅 references/icu.md。

4. 配置区域路由和渲染

  • 从路由中提取区域设置,验证其是否在支持列表中,对未知区域应返回404而不是静默回退。
  • 提供语言切换器,并保留当前路径和查询字符串。
  • 在服务器渲染的文档中设置 <html lang="..."> 和 <html dir="...">。
  • 仅加载当前活动区域的目录以及默认回退目录。
  • 确保同一目录可同时用于服务端和客户端渲染;避免重复或分叉的翻译逻辑。

框架特定的设置和陷阱:references/frameworks.md。 路由、检测、hreflang和站点地图:references/routing-seo.md。

5. 处理区域敏感值

使用 Intl(或框架的封装),绝不手动格式化:

new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
new Intl.DateTimeFormat(locale, { dateStyle: 'medium', timeZone }).format(date)
new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(-3, 'day')
new Intl.ListFormat(locale, { type: 'conjunction' }).format(items)
new Intl.PluralRules(locale).select(count)
new Intl.Collator(locale).compare(a, b)
  • 时间戳以UTC存储;在用户的时区渲染,而不是服务器的时区。
  • 货币与区域设置不是同一回事:法国用户可能用美元支付。
  • 使用 Intl.Collator 进行排序/搜索,而不是使用 < 或不指定区域的 localeCompare()。
  • 大小写转换:使用 toLocaleUpperCase(locale),因为土耳其语中的 i 会变成 İ。

6. 考虑文本扩展和RTL

  • 假设文本长度增加30–40%(德语、芬兰语、俄语),避免固定宽度的按钮或单行标签。
  • 让容器可以扩展,允许换行,并通过伪本地化进行测试:
    node scripts/pseudo-localize.mjs locales/en.json --out locales/en-XA.json
    
  • 使用逻辑CSS属性(margin-inline-start、padding-inline、inset-inline)替代 left/right。
  • 镜像方向性图标(箭头、V形符号、进度条),但不要镜像Logo、时钟、媒体控件或对勾。
  • 将用户生成的内联内容包裹在 <bdi> 或 dir="auto" 中。

完整的RTL检查清单:references/rtl.md。

7. 在CI中设置翻译质量检查

至少要在目录结构无效时构建失败:

node scripts/check-locales.mjs locales --base en

该命令会检查缺失/多余的键、空值、占位符不匹配以及破损的ICU语法。在可用时,通过库的CLI添加ICU编译:

npx formatjs compile-folder --ast locales/en locales/compiled/en

添加针对复数类别、固定时区下的日期/数字输出以及RTL渲染的测试。请参阅 references/testing.md。

关键决策与陷阱

库的选择

需求选择
Next.js App Router,服务端组件next-intl
React SPA,成熟生态,插件丰富react-i18next
ICU优先,框架无关,CLI提取FormatJS (react-intl)
编译时目录,小型运行时,类型安全Lingui
Vuevue-i18n
Sveltesvelte-i18n 或 paraglide
Astro内置i18n路由 + 消息库
原生JS / 最小化Intl + 小型目录加载器

不要向已有i18n库的应用中引入第二个i18n库。应扩展现有配置。

复数

复数类别因语言而异。英语有 one/other;阿拉伯语有 zero/one/two/few/many/other;日语只有 other。绝不要写 count === 1 ? ... : ...。

"inbox.unread": "{count, plural, =0 {No unread messages} one {# unread message} other {# unread messages}}"

性别与语法一致

使用 select 处理性别变量,并保持变量值语言中立(female, male, neutral, unknown),而不是本地化的词汇:

"profile.greeting": "{gender, select, female {Bienvenida, {name}} male {Bienvenido, {name}} other {Bienvenido/a, {name}}}"

如果某种语言需要在形容词、名词或冠词上进行一致变化,应将语法性别/格作为参数公开,而不是通过字符串拼接。

富文本

不要将 "Read our " + link + " terms" 拆分为三个键。应使用标签插值:

"signup.terms": "I agree to the <link>Terms of Service</link>"
t.rich('signup.terms', { link: (chunks) => <a href="/terms">{chunks}</a> })

SEO

  • 每个区域有一个可索引的URL;绝不要在同一个URL下提供机器翻译的内容。
  • 自引用 hreflang 加上 x-default;使用绝对URL;包含所有区域。
  • 本地化 <title>、meta描述、OG标签和结构化数据。
  • 不要在本地化页面上同时使用 hreflang 和 noindex。

详细信息见 references/routing-seo.md。

缓存

  • 将区域设置在路径中可使CDN缓存变得简单。如果通过请求头协商,发送 Vary: Accept-Language,并预期缓存碎片化。
  • 不要在共享CDN响应中缓存个性化的语言偏好。
  • 保持 Content-Language 与渲染的区域设置一致。

审查清单

在宣布i18n完成之前:

  • 组件、邮件或错误中不存在硬编码的面向用户字符串
  • 没有句子拼接;每条消息都是一个完整的可翻译单元
  • 键是语义化且稳定的;不使用英文字符串作为键
  • 每个占位符在每个区域中都存在;ICU语法可编译
  • 复数使用 Intl.PluralRules/ICU,而不是 count === 1
  • 性别/一致问题通过 select 处理,而不是 "/a" 后缀
  • 日期、数字、货币、相对时间使用 Intl,并显式指定区域/时区
  • <html lang> 和 dir 在首次渲染时(SSR)正确设置,不是在effect中设置
  • 区域切换器保留路径/查询字符串,并能处理深层链接
  • 未知区域返回404,而不是静默回退
  • 布局能承受+35%的扩展和RTL
  • hreflang、规范URL、站点地图和本地化元数据正确
  • 目录验证在CI中运行
  • 回退行为是有意设计且可观察的

反模式

  • t('hello') + ', ' + name —— 破碎的句子
  • key={englishText} —— 翻译后的文本作为键
  • count === 1 ? 'item' : 'items' —— 仅适用于英语的复数逻辑
  • 不带区域或时区的 new Date().toLocaleDateString()
  • 使用 value.toFixed(2) 处理货币
  • 分散在各个组件中的 locale === 'ar' && { direction: 'rtl' },而不是文档级别的 dir
  • 在 useEffect 中设置 <html lang> —— 会导致闪烁并影响爬虫
  • 将所有区域设置加载到初始包中
  • 根据IP自动重定向,且无法覆盖
  • 翻译专有名词、产品名称、代码标识符或URL
  • 机器翻译字符串后未经审查就发布
  • 假设一个目录文件可以容纳几百个以上的键

相关技能