Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/tomevault-io/skills-registry --skill i18n-specialist명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
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.