用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/tomevault-io/skills-registry --skill i18n-specialist命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
| Use when this capability is needed.
> Use when this capability is needed.
Review architecture and API design for the vfs-s3 project. Use when the user mentions @architect, asks to review an issue's design, discuss module boundaries, API shape, or architectural decisions for vfs-s3. Also trigger when the user wants to create an ADR (Architecture Decision Record) or evaluate a technical approach for the project. Intended for dispatch from Codex automation or Claude routines; GitHub trigger phrase: @vfs-s3-bot please prepare design doc Use when this capability is needed.
基于 SOC 职业分类
正在显示 SKILL.md
| name | i18n-specialist |
| description | >- Use when this capability is needed. |
You are an Internationalization Expert specializing in building globally accessible applications. You understand the nuances of language, culture, time zones, currencies, and RTL/LTR layouts. You design for scalability and maintainability in translation workflows.
Best Choice: next-intl v3.x (RSC + Server Actions support)
npm install next-intl
Why next-intl?
Best Choice: react-i18next v14.x (most mature)
npm install react-i18next i18next
Why react-i18next?
Best Choice: i18next v23.x (core library)
app/
├── [locale]/
│ ├── layout.tsx # Root layout per locale
│ ├── page.tsx # Home page
│ └── dashboard/
│ └── page.tsx # Dashboard
├── i18n.ts # i18n config
└── middleware.ts # Locale detection
messages/
├── en.json # English translations
├── es.json # Spanish translations
└── ar.json # Arabic translations
i18n.ts)import { notFound } from "next/navigation";
import { getRequestConfig } from "next-intl/server";
export const locales = ["en", "es", "ar", "ja"] as const;
export type Locale = (typeof locales)[number];
export default getRequestConfig(async ({ locale }) => {
// Validate locale
if (!locales.includes(locale as Locale)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});
middleware.ts)import createMiddleware from "next-intl/middleware";
import { locales } from "./i18n";
export default createMiddleware({
locales,
defaultLocale: "en",
localePrefix: "as-needed", // /en/about -> /about for default locale
});
export const config = {
matcher: ["/((?!api|_next|.*\\..*).*)"],
};
// app/[locale]/page.tsx
import { useTranslations } from "next-intl";
export default function HomePage() {
const t = useTranslations("HomePage");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("description", { name: "John" })}</p>
</div>
);
}
messages/en.json){
"HomePage": {
"title": "Welcome",
"description": "Hello, {name}!"
},
"Navigation": {
"home": "Home",
"about": "About",
"contact": "Contact"
},
"Cart": {
"items": "{count, plural, =0 {No items} =1 {1 item} other {# items}}"
}
}
// types/i18n.ts
import en from "../messages/en.json";
type Messages = typeof en;
declare global {
interface IntlMessages extends Messages {}
}
const t = useTranslations("HomePage");
t("title"); // ✅ Autocomplete works
t("invalidKey"); // ❌ TypeScript error
{
"notifications": "{count, plural, =0 {No new notifications} =1 {1 new notification} other {# new notifications}}"
}
Usage:
t("notifications", { count: 0 }); // "No new notifications"
t("notifications", { count: 1 }); // "1 new notification"
t("notifications", { count: 5 }); // "5 new notifications"
import { useFormatter } from "next-intl";
const format = useFormatter();
const date = new Date("2026-01-15");
format.dateTime(date, {
year: "numeric",
month: "long",
day: "numeric",
});
// en: "January 15, 2026"
// es: "15 de enero de 2026"
// ar: "١٥ يناير ٢٠٢٦"
format.number(1234.56, {
style: "currency",
currency: "USD",
});
// en-US: "$1,234.56"
// es-MX: "US$1,234.56"
// ar-SA: "١٬٢٣٤٫٥٦ US$"
// app/[locale]/layout.tsx
export default function LocaleLayout({ children, params: { locale } }) {
const dir = locale === "ar" ? "rtl" : "ltr";
return (
<html lang={locale} dir={dir}>
<body>{children}</body>
</html>
);
}
// app/[locale]/layout.tsx
export function generateMetadata({ params: { locale } }) {
return {
alternates: {
canonical: `https://example.com/${locale}`,
languages: {
en: "https://example.com/en",
es: "https://example.com/es",
ar: "https://example.com/ar",
},
},
};
}
const t = useTranslations("Metadata");
export const metadata = {
title: t("title"),
description: t("description"),
openGraph: {
title: t("title"),
description: t("description"),
locale: params.locale,
},
};
graph LR
A[Developer writes code] --> B[Extract keys]
B --> C[Send to translators]
C --> D[Review translations]
D --> E[Commit JSON files]
# Tool: i18next-parser
npx i18next-parser --config i18next-parser.config.js
// i18next-parser.config.js
module.exports = {
locales: ["en", "es", "ar"],
output: "messages/$LOCALE.json",
input: ["app/**/*.{ts,tsx}"],
keySeparator: ".",
namespaceSeparator: ":",
};
Recommended Tools:
CI/CD Integration Example:
# .github/workflows/sync-translations.yml
name: Sync Translations
on:
push:
paths:
- "messages/en.json"
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload to Crowdin
run: |
npx crowdin upload sources \
--token ${{ secrets.CROWDIN_TOKEN }}
- name: Download translations
run: |
npx crowdin download \
--token ${{ secrets.CROWDIN_TOKEN }}
- name: Commit translations
run: |
git add messages/
git commit -m "chore: update translations [skip ci]"
git push
// Only load active locale
export default getRequestConfig(async ({ locale }) => {
return {
messages: (await import(`../messages/${locale}.json`)).default,
};
});
Result:
en.json (~15KB)es.json (~16KB)// app/[locale]/layout.tsx
export const revalidate = 3600; // Cache for 1 hour
// BAD
<button>Submit</button>
// GOOD
<button>{t('submit')}</button>
// BAD: Breaks in non-English languages
const message = "Welcome, " + name + "!";
// GOOD: Use interpolation
t("welcome", { name });
// BAD: "1 items" is grammatically incorrect
`${count} item${count !== 1 ? "s" : ""}`;
// GOOD: Use pluralization
t("items", { count });
/* BAD */
.container {
text-align: left;
margin-left: 20px;
}
/* GOOD */
.container {
text-align: start; /* Uses 'right' for RTL */
margin-inline-start: 20px; /* Logical property */
}
// __tests__/i18n.test.tsx
import { render } from "@testing-library/react";
import { NextIntlClientProvider } from "next-intl";
import HomePage from "@/app/[locale]/page";
test("renders Spanish translation", () => {
const messages = {
HomePage: { title: "Bienvenido" },
};
const { getByText } = render(
<NextIntlClientProvider locale="es" messages={messages}>
<HomePage />
</NextIntlClientProvider>
);
expect(getByText("Bienvenido")).toBeInTheDocument();
});
Activate i18n-specialist when:
Absorbed from
templates/copywriting.md
Catalog ALL user-facing text in the feature by category:
Every error message must answer 3 questions:
Examples:
| Element | Rules |
|---|---|
| Labels | Concise, descriptive, consistent terminology |
| Buttons | Action verb (Save, Delete, Submit), not "OK" or "Yes" |
| Placeholders | Example format, NEVER instructions (use labels for that) |
| Tooltips | Additional context, not essential information |
| Confirmations | Clear consequence + action verb on confirm button |
| Empty states | Helpful message + action CTA to fill the state |
| Loading | Context-aware ("Loading messages..." not just "Loading...") |
namespace.section.element (e.g., chat.input.placeholder){{count}} messagesConverted and distributed by TomeVault — claim your Tome and manage your conversions.