internationalization
13
总安装量
7
周安装量
#24284
全站排名
安装命令
npx skills add https://github.com/miles990/claude-software-skills --skill internationalization
Agent 安装分布
claude-code
5
codex
5
antigravity
5
gemini-cli
4
windsurf
4
Skill 文档
åéåèæ¬å°å Internationalization & Localization
è®ä½ çæç¨ç¨å¼èµ°åå ¨ä¸ç
æ ¸å¿æ¦å¿µ
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â i18n vs l10n â
â â
â åéå (Internationalization - i18n) â
â ââ è¨è¨æ¯æ´å¤èªè¨çæ¶æ§ â
â ââ æ½é¢å¯ç¿»è¯çå串 â
â ââ èçæ¥æãæ¸åãè²¨å¹£æ ¼å¼ â
â ââ 䏿¬¡æ§å·¥ç¨å·¥ä½ â
â â
â æ¬å°å (Localization - l10n) â
â ââ éå°ç¹å®èªè¨/å°åçç¿»è¯ â
â ââ æå驿ï¼åçãé¡è²ã符èï¼ â
â ââ æ³è¦éµå¾ªï¼é±ç§ãå
§å®¹ï¼ â
â ââ æçºæ§å·¥ä½ â
â â
â i18n = è®æç¨ãè½ãæ¯æ´å¤èªè¨ â
â l10n = è®æç¨ã實éãæ¯æ´æèªè¨ â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
é©ç¨å ´æ¯
- æ°å°æ¡çå¤èªè¨æ¶æ§è¨è¨
- ç¾æå°æ¡æ·»å å¤èªè¨æ¯æ´
- ç¿»è¯å·¥ä½æµç¨åªå
- RTL (å³å°å·¦) èªè¨æ¯æ´
- æ¥æ/æ¸å/è²¨å¹£æ ¼å¼å
i18n æ¶æ§è¨è¨
ç¿»è¯æªæ¡çµæ§
æ¨è¦çµæ§ï¼æèªè¨åé¡
locales/
âââ en/
â âââ common.json # å
±ç¨è©å½
â âââ home.json # é¦é
â âââ settings.json # è¨å®é
âââ zh-TW/
â âââ common.json
â âââ home.json
â âââ settings.json
âââ ja/
âââ common.json
âââ home.json
âââ settings.json
æ¿ä»£çµæ§ï¼æåè½åé¡
locales/
âââ common/
â âââ en.json
â âââ zh-TW.json
â âââ ja.json
âââ settings/
âââ en.json
âââ zh-TW.json
âââ ja.json
ç¿»è¯ Key å½åè¦ç¯
## å½ååå
1. **é層å¼å½å**
â
`settings.notifications.email.title`
â `settingsNotificationsEmailTitle`
2. **èªæåªå
æ¼ä½ç½®**
â
`action.save`, `action.cancel`
â `button1`, `button2`
3. **é¿å
縮寫**
â
`error.network.timeout`
â `err.net.to`
4. **使ç¨è±æå°å¯«**
â
`user.profile.avatar`
â `User.Profile.Avatar`
## 常ç¨åç¶´
| åç¶´ | ç¨é | ç¯ä¾ |
|------|------|------|
| `action.` | å使é | `action.submit`, `action.delete` |
| `label.` | è¡¨å®æ¨ç±¤ | `label.email`, `label.password` |
| `error.` | é¯èª¤è¨æ¯ | `error.required`, `error.invalid` |
| `message.` | ä¸è¬è¨æ¯ | `message.success`, `message.loading` |
| `title.` | é 颿¨é¡ | `title.home`, `title.settings` |
| `hint.` | æç¤ºæå | `hint.password_format` |
ä¸»æµæ¡æ¶å¯¦ä½
React (react-i18next)
// i18n.ts - é
ç½®
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
supportedLngs: ['en', 'zh-TW', 'ja'],
ns: ['common', 'home', 'settings'],
defaultNS: 'common',
interpolation: {
escapeValue: false,
},
detection: {
order: ['querystring', 'cookie', 'localStorage', 'navigator'],
caches: ['localStorage', 'cookie'],
},
});
// 使ç¨
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t, i18n } = useTranslation();
return (
<div>
<h1>{t('title.welcome')}</h1>
<p>{t('message.greeting', { name: 'John' })}</p>
<button onClick={() => i18n.changeLanguage('zh-TW')}>
䏿
</button>
</div>
);
}
Vue (vue-i18n)
// i18n.ts
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {
en: { /* ... */ },
'zh-TW': { /* ... */ },
},
});
// 使ç¨
<template>
<h1>{{ $t('title.welcome') }}</h1>
<p>{{ $t('message.greeting', { name: 'John' }) }}</p>
</template>
<script setup>
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
</script>
Next.js (next-intl)
// middleware.ts
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'zh-TW', 'ja'],
defaultLocale: 'en',
localePrefix: 'as-needed',
});
// app/[locale]/layout.tsx
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
export default async function LocaleLayout({
children,
params: { locale }
}) {
const messages = await getMessages();
return (
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
);
}
// 使ç¨
import { useTranslations } from 'next-intl';
function Page() {
const t = useTranslations('home');
return <h1>{t('title')}</h1>;
}
è¤éç¿»è¯èç
è¤æ¸å½¢å¼ (Pluralization)
// en.json
{
"items": {
"zero": "No items",
"one": "{{count}} item",
"other": "{{count}} items"
}
}
// zh-TW.json (䏿ç¡è¤æ¸è®å)
{
"items": "{{count}} åé
ç®"
}
// 使ç¨
t('items', { count: 5 }) // "5 items" / "5 åé
ç®"
æå¼èæ ¼å¼å
{
"greeting": "Hello, {{name}}!",
"date": "Today is {{date, datetime}}",
"price": "Price: {{amount, currency}}",
"percent": "{{value, percent}} complete"
}
// ä½¿ç¨ Intl API
t('date', { date: new Date() });
t('price', { amount: 99.99 });
t('percent', { value: 0.85 });
å·¢çèå¼ç¨
{
"common": {
"app_name": "MyApp"
},
"welcome": "Welcome to $t(common.app_name)!",
"footer": "© 2024 $t(common.app_name). All rights reserved."
}
æ¥ææéèç
// ä½¿ç¨ Intl.DateTimeFormat
const formatDate = (date: Date, locale: string) => {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
};
formatDate(new Date(), 'en-US'); // "January 7, 2024"
formatDate(new Date(), 'zh-TW'); // "2024å¹´1æ7æ¥"
formatDate(new Date(), 'ja-JP'); // "2024å¹´1æ7æ¥"
// ç¸å°æé
const formatRelative = (date: Date, locale: string) => {
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
const diff = Math.round((date.getTime() - Date.now()) / 86400000);
return rtf.format(diff, 'day');
};
formatRelative(yesterday, 'en'); // "yesterday"
formatRelative(yesterday, 'zh-TW'); // "æ¨å¤©"
æ¸åè貨幣
// æ¸åæ ¼å¼å
const formatNumber = (num: number, locale: string) => {
return new Intl.NumberFormat(locale).format(num);
};
formatNumber(1234567.89, 'en-US'); // "1,234,567.89"
formatNumber(1234567.89, 'de-DE'); // "1.234.567,89"
formatNumber(1234567.89, 'zh-TW'); // "1,234,567.89"
// è²¨å¹£æ ¼å¼å
const formatCurrency = (amount: number, currency: string, locale: string) => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
};
formatCurrency(99.99, 'USD', 'en-US'); // "$99.99"
formatCurrency(99.99, 'TWD', 'zh-TW'); // "NT$99.99"
formatCurrency(99.99, 'JPY', 'ja-JP'); // "ï¿¥100"
RTL èªè¨æ¯æ´
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â RTL (Right-to-Left) æ¯æ´ â
â â
â RTL èªè¨ï¼é¿æä¼¯èªãå¸ä¼¯ä¾èªãæ³¢æ¯èªãçç¾é½èª â
â â
â HTML è¨å® â
â <html lang="ar" dir="rtl"> â
â â
â CSS éè¼¯å±¬æ§ â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â LTR å±¬æ§ â éè¼¯å±¬æ§ â â
â â margin-left â margin-inline-start â â
â â margin-right â margin-inline-end â â
â â padding-left â padding-inline-start â â
â â text-align:left â text-align: start â â
â â float: left â float: inline-start â â
â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
â â
â èªåç¿»è½ â
â â¢ åæ¨æ¹åï¼ç®é ãå°èªï¼ â
â â¢ è¡¨å®æ¬ä½é åº â
â â¢ æ»¾åæ¹å â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
RTL CSS ç¯ä¾
/* ä½¿ç¨ CSS éè¼¯å±¬æ§ */
.card {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-inline-start: 3px solid blue;
}
/* éå° RTL èª¿æ´ */
[dir="rtl"] .icon-arrow {
transform: scaleX(-1);
}
/* ä½¿ç¨ :dir() å½é¡ */
.nav:dir(rtl) {
flex-direction: row-reverse;
}
ç¿»è¯å·¥ä½æµç¨
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â ç¿»è¯å·¥ä½æµç¨ â
â â
â éç¼è
ç¿»è¯äººå¡ â
â â â â
â â 1. æ°å¢ key + é è¨æå â â
â ââââââââââââââââââââââââââââââââ â
â â â â
â â 2. 忥å°ç¿»è¯å¹³å° â â
â ââââââââââââââââââââââââââââââââ â
â â â â
â â 3. ç¿»è¯ + å¯©æ ¸ â â
â âââââââââââââââââââââââââââââââ⤠â
â â â â
â â 4. æåç¿»è¯æªæ¡ â â
â ââââââââââââââââââââââââââââââââ â
â â â â
â â 5. 測試 + ç¼å¸ â â
â â¼ â â
â â
â å·¥å
·æ¨è¦ï¼ â
â ⢠Lokalise, Crowdin, Phrase â
â ⢠Weblate (éæº) â
â ⢠POEditor â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ç¿»è¯å質ä¿è
## ç¿»è¯ Checklist
### æè¡æª¢æ¥
- [ ] ææ key é½æç¿»è¯
- [ ] æå¼è®æ¸æ£ç¢º
- [ ] è¤æ¸å½¢å¼å®æ´
- [ ] ç¹æ®åå
æ£ç¢ºè½ç¾©
- [ ] HTML æ¨ç±¤å®æ´
### å
§å®¹æª¢æ¥
- [ ] ç¿»è¯æºç¢º
- [ ] èªæ°£ä¸è´
- [ ] å°æåè©çµ±ä¸
- [ ] é·åº¦é©ä¸ï¼UI 䏿º¢åºï¼
- [ ] æåé©å®
### èªå忏¬è©¦
```typescript
// 檢æ¥éºæ¼ç¿»è¯
function checkMissingTranslations(
defaultLocale: object,
targetLocale: object,
path: string = ''
) {
const missing: string[] = [];
for (const key in defaultLocale) {
const currentPath = path ? `${path}.${key}` : key;
if (!(key in targetLocale)) {
missing.push(currentPath);
} else if (typeof defaultLocale[key] === 'object') {
missing.push(...checkMissingTranslations(
defaultLocale[key],
targetLocale[key],
currentPath
));
}
}
return missing;
}
常è¦åé¡è解決
| åé¡ | è§£æ±ºæ¹æ¡ |
|---|---|
| ç¿»è¯å¾æåå¤ªé· | é ç空éã使ç¨ç¸®å¯«ãèª¿æ´ UI |
| å°æåè©ä¸ä¸è´ | 建ç«è¡èªè¡¨ (Glossary) |
| ç¿»è¯éºæ¼ | CI èªåæª¢æ¥ |
| ä¸ä¸æä¸æ¸ | çºç¿»è¯è æä¾æªåå說æ |
| 硬編碼å串 | ESLint è¦åæª¢æ¥ |
| åæ å §å®¹ç¿»è¯ | ä½¿ç¨æå¼ï¼é¿å åä¸²æ¼æ¥ |
æè½åªå
## ç¿»è¯è¼å
¥çç¥
### 1. æéè¼å
¥
åªè¼å
¥ç¶åèªè¨ï¼åææåè¼å
¥
```typescript
const loadLocale = async (locale: string) => {
const messages = await import(`./locales/${locale}.json`);
i18n.addResourceBundle(locale, 'translation', messages);
};
2. å½å空éåå²
æé é¢/åè½åå²ç¿»è¯æªæ¡
// åªè¼å
¥é¦é éè¦çç¿»è¯
await i18n.loadNamespaces(['common', 'home']);
3. é è¼å ¥å¸¸ç¨èªè¨
// é è¼å
¥å¯è½ä½¿ç¨çèªè¨
const preloadLocales = ['en', 'zh-TW'];
preloadLocales.forEach(locale => {
i18n.loadLanguages(locale);
});
4. å¿«åçç¥
ä½¿ç¨ Service Worker å¿«åç¿»è¯æªæ¡
## å·¥å
·æ¨è¦
### ç¿»è¯ç®¡çå¹³å°
- **Lokalise** - éç¼è
åå¥½ï¼æ¯æ´å¤ç¨®æ ¼å¼
- **Crowdin** - 社群翻è¯ï¼éæºå°æ¡å
è²»
- **Phrase** - 伿¥ç´ï¼å¼·å¤§ç工使µç¨
### éç¼å·¥å
·
- **i18n Ally** - VS Code æ´å±ï¼å³æé 覽
- **eslint-plugin-i18n** - 檢æ¥ç¡¬ç·¨ç¢¼å串
- **FormatJS** - å¼·å¤§çæ ¼å¼åå·¥å
·é
### 測試工å
·
- **pseudolocalization** - 彿¬å°å測試
- **i18next-parser** - èªåæå key
## æä½³å¯¦è¸ç¸½çµ
```markdown
## DO â
1. å¾å°æ¡éå§å°±èæ
® i18n
2. 使ç¨èªæåç key å½å
3. é¿å
åä¸²æ¼æ¥ï¼ä½¿ç¨æå¼
4. çºç¿»è¯è
æä¾ä¸ä¸æ
5. èªååç¿»è¯æª¢æ¥
6. æ¸¬è©¦æææ¯æ´çèªè¨
## DON'T â
1. 硬編碼任ä½ç¨æ¶å¯è¦æå
2. åè¨ææèªè¨é·åº¦ç¸å
3. å¨ key ä¸ä½¿ç¨é è¨æå
4. 忽ç¥è¤æ¸åæ§å¥è®å
5. æå管çç¿»è¯åæ¥
6. å¿½ç¥ RTL èªè¨éæ±