国际化与本地化

让 Agent 用 ICU MessageFormat、RTL 布局与区域格式处理文案,约定键名与回退链,避免拼接式翻译与硬编码语种。

本页为 Agent 提供国际化完整实施参考:i18next/vue-i18n 完整配置、英文/中文/阿拉伯语复数规则处理、Intl API 日期数字货币格式化、i18next-scanner 提取工具配置,以及 RTL 支持的 CSS 逻辑属性对比。

键名与默认语言文件结构在 SKILL 中约定;禁止散落硬编码字符串;动态插值使用命名占位符({name})以便译者重排语序;复数用库内置 plural categories。

  • 语言切换持久化与 SEO hreflang 策略分开说明。
  • 图片与法律文案若不可译需列例外与责任方。
  • 翻译记忆与术语表版本与代码分支对齐。

概述与原则

对 Agent:先读仓库约定的资源目录(如 locales/messages/)与默认语言,再改文案或补键;禁止把可见字符串写回组件而不走资源层。

  • 键名稳定、语义化,与路由/特性域对齐,避免把英文句子当键。
  • 同一语义在不同界面复用同一键,减少 TM 碎片与不一致。
  • CI:缺失键、占位符不一致、非法 BCP 47 标签应可自动失败。

i18next/vue-i18n 配置与复数规则

i18next 完整配置

// i18n/index.ts
import i18next from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import HttpBackend from 'i18next-http-backend'

i18next
  .use(HttpBackend)
  .use(LanguageDetector)
  .init({
    fallbackLng: 'en',
    supportedLngs: ['en', 'zh', 'ar'],
    ns: ['common', 'errors', 'dashboard'],
    defaultNS: 'common',
    backend: { loadPath: '/locales/{{lng}}/{{ns}}.json' },
    detection: { order: ['localStorage', 'navigator'], caches: ['localStorage'] },
    interpolation: { escapeValue: false }, // React/Vue 自行转义
  })

复数规则处理(英文/中文/阿拉伯语的不同规则):

// locales/en/common.json — 英文:one / other
{
  "message_count": "{{count}} message",
  "message_count_plural": "{{count}} messages"
}

// locales/zh/common.json — 中文:无复数形式(只有 other)
{
  "message_count": "{{count}} 条消息",
  "message_count_plural": "{{count}} 条消息"
}

// locales/ar/common.json — 阿拉伯语:zero/one/two/few/many/other
{
  "message_count_zero": "لا رسائل",
  "message_count_one": "رسالة واحدة",
  "message_count_two": "رسالتان",
  "message_count_few": "{{count}} رسائل",
  "message_count_many": "{{count}} رسالة",
  "message_count_other": "{{count}} رسالة"
}

// 使用(i18next 自动按 locale 选分支)
t('message_count', { count: 5 })  // → "5 messages"

Intl API 日期/数字/货币格式化

// utils/formatters.ts
export function formatDate(date: Date, locale: string): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric', month: 'long', day: 'numeric'
  }).format(date)
  // zh-CN: "2026年4月11日"  | en-US: "April 11, 2026"
}

export function formatCurrency(amount: number, locale: string, currency: string): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency', currency,
    minimumFractionDigits: 2,
  }).format(amount)
  // zh-CN + CNY: "¥1,234.56"  | en-US + USD: "$1,234.56"
  // ar-SA + SAR: "١٬٢٣٤٫٥٦ ر.س."
}

export function formatRelativeTime(diffMs: number, locale: string): string {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
  const diffSec = Math.round(diffMs / 1000)
  if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second')
  const diffMin = Math.round(diffSec / 60)
  if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute')
  return rtf.format(Math.round(diffMin / 60), 'hour')
}

i18next-scanner 配置文件(自动提取翻译键):

// i18next-scanner.config.js
module.exports = {
  input: ['src/**/*.{ts,tsx,vue}'],
  output: './',
  options: {
    func: { list: ['t', 'i18next.t', '$t'], extensions: ['.ts', '.tsx', '.vue'] },
    lngs: ['en', 'zh', 'ar'],
    ns: ['common', 'errors', 'dashboard'],
    defaultLng: 'en',
    defaultNS: 'common',
    resource: {
      loadPath: 'public/locales/{{lng}}/{{ns}}.json',
      savePath: 'public/locales/{{lng}}/{{ns}}.json',
    },
    defaultValue: '__STRING_NOT_TRANSLATED__',
    keySeparator: '.', nsSeparator: ':',
  },
}
// 运行:npx i18next-scanner

RTL 支持:CSS 逻辑属性 vs 物理属性

CSS 逻辑属性(推荐)vs 物理属性(避免)

/* ❌ 物理属性:RTL 下方向错误,需额外覆盖 */
.card {
  margin-left: 1rem;
  padding-right: 0.5rem;
  border-left: 2px solid blue;
  text-align: left;
}
[dir="rtl"] .card {          /* 需要额外写 RTL 覆盖 */
  margin-left: 0;
  margin-right: 1rem;
  padding-right: 0;
  padding-left: 0.5rem;
  border-left: none;
  border-right: 2px solid blue;
  text-align: right;
}

/* ✅ 逻辑属性:自动适应 dir="rtl",无需覆盖 */
.card {
  margin-inline-start: 1rem;  /* LTR = left, RTL = right */
  padding-inline-end: 0.5rem; /* LTR = right, RTL = left */
  border-inline-start: 2px solid blue;
  text-align: start;          /* 随书写方向自动对齐 */
}

/* RTL 完整对照表 */
/* margin-left        → margin-inline-start  */
/* margin-right       → margin-inline-end    */
/* padding-left       → padding-inline-start */
/* padding-right      → padding-inline-end   */
/* left / right       → inset-inline-start / inset-inline-end */
/* border-left        → border-inline-start  */
/* float: left        → float: inline-start  */

HTML dir 属性与动态切换

// 动态设置文档方向
const RTL_LANGS = ['ar', 'he', 'fa', 'ur']

function applyLocale(locale: string) {
  const isRtl = RTL_LANGS.some(l => locale.startsWith(l))
  document.documentElement.setAttribute('dir', isRtl ? 'rtl' : 'ltr')
  document.documentElement.setAttribute('lang', locale)
}

// 禁止自动镜像的例外(如播放控件、进度条)
// 用 [dir="rtl"] .no-flip { transform: scaleX(-1) scaleX(-1) = 不翻转 }
// 或直接 writing-mode 控制
.playback-icon { unicode-bidi: normal; }
  • 混合方向字符串用 unicode-bidi: isolate<bdi> 包裹数字/用户名。
  • 表单、时间轴、图表坐标轴在 RTL 下需单独验收。

回退链与缺失键

约定显式顺序,例如 zh-TW → zh → enpt-BR → pt → en;开发环境可警告、生产环境可记录遥测,避免静默显示键名。

  • 缺失键:构建或运行时列出键路径;禁止把英文默认值藏在组件里作为「隐形回退」。
  • 部分更新:新键先合入默认语言再发翻译包,避免其它语种空文件被误当成「已翻译」。
  • 服务端渲染:首屏 locale 与客户端 hydration 一致,防止闪语或 SEO 错语。
SKILL 提示:写明「回退到哪一层语言、缺失时是否显示键、是否上报」,并与框架的 fallbackLocale / 等价配置名称对齐。

抽取与校验流程

将「读约定 → 改资源 → 占位符/键检查 → 伪本地化或 CI」写进 SKILL,便于模型按顺序执行。

  [ 读 SKILL:键命名、目录、默认语言、回退链 ]
                    │
                    ▼
            [ 扫描:硬编码可见串 / 未走 i18n API ]
                    │
                    ▼
            [ 抽取或补键;ICU 表达复数与格式 ]
                    │
           ┌────────┴────────┐
           ▼                 ▼
    [ RTL:dir + 逻辑属性 ]   [ 区域:日期货币时区一致 ]
           │                 │
           └────────┬──────────┘
                    ▼
            [ 占位符集合:基准语言 vs 译文对齐 ]
                    │
                    ▼
         [ CI / 本地:缺失键、伪本地化、截断 ]
                    │
           ┌────────┴────────┐
           ▼                 ▼
        [ 通过 ]         [ 修键或修译文再跑 ]

键名与占位符小工具

下方为轻量校验:键名建议为点分小写段(可含数字);占位符比对简单 ICU 风格命名占位 {name}(不解析完整 plural 块)。仅作辅助,以项目真实 linter 为准。


            

SKILL 片段

可直接复制为技能正文骨架,再替换为仓库真实路径与脚本。

---
name: i18n-l10n-cn
description: 抽取文案、ICU 复数与区域格式,校验键名、占位符与回退链
---
# 规则
- 禁止硬编码用户可见字符串;全部通过 t(key) / $t(key) 调用
- 键名点分小写,按功能域分 namespace(common/errors/dashboard)
- 复数用库 plural categories;禁止代码里写 if (n === 1)
- 货币/日期/数字用 Intl API 或库内置格式化方法
- RTL 布局用 CSS 逻辑属性(margin-inline-start 等),不写 dir 覆盖
- 回退链:zh-TW → zh → en;缺失键在开发环境警告,生产记遥测

# 步骤
1. 读 SKILL:键命名约定、namespace 目录、默认语言、回退链配置
2. 运行 i18next-scanner 扫描未抽取字符串
3. 提取/补键到 locales/{lng}/{ns}.json
4. 复数:按 CLDR plural rules 写 _zero/_one/_two/_few/_many/_other 变体
5. 日期/货币:用 Intl.DateTimeFormat / Intl.NumberFormat 替换硬编码格式
6. RTL:将 margin-left/right 改为 margin-inline-start/end;验证 dir=rtl 布局
7. CI:运行占位符一致性检查,阻断缺失键合并

返回技能库 更多技能入口