Skip to content

Commit f3f7831

Browse files
ericyangpanclaude
andcommitted
test: update validation tests and i18n skill documentation
Update translation alignment tests, reference validation tests, and URL accessibility tests. Enhance i18n skill documentation with improved workflow and examples. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6a0f086 commit f3f7831

File tree

4 files changed

+145
-41
lines changed

4 files changed

+145
-41
lines changed

.claude/skills/i18n/SKILL.md

Lines changed: 127 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,60 @@ translations/
2424
│ ├── articles.json
2525
│ ├── curated-collections.json
2626
│ ├── stacks.json
27-
│ └── comparison.json
27+
│ ├── comparison.json
28+
│ ├── landscape.json
29+
│ ├── open-source-rank.json
30+
│ └── search.json
2831
├── de/ # German
32+
├── es/ # Spanish
33+
├── fr/ # French
34+
├── id/ # Indonesian
35+
├── ja/ # Japanese
36+
├── ko/ # Korean
37+
├── pt/ # Portuguese
38+
├── ru/ # Russian
39+
├── tr/ # Turkish
2940
├── zh-Hans/ # Simplified Chinese
30-
└── ko/ # Korean
41+
└── zh-Hant/ # Traditional Chinese
3142
```
3243

3344
**Important:** Each locale must maintain the exact same file structure and JSON key order as `en/`.
3445

46+
## I18n Architecture
47+
48+
The project has **two separate internationalization systems** that share the same 12 locale configuration:
49+
50+
### 1. UI Translation System (next-intl)
51+
- **Purpose:** Translates static UI strings (buttons, labels, page content, etc.)
52+
- **Location:** `translations/{locale}/*.json`
53+
- **Usage:** Via `useTranslations()` hook or `getTranslations()` server function
54+
- **Managed by:** This skill's sync and translate commands
55+
56+
### 2. Manifest Translation System
57+
- **Purpose:** Translates manifest data (IDEs, CLIs, models, providers, etc.)
58+
- **Location:** `manifests/**/*.json` (in each manifest file's `translations` field)
59+
- **Usage:** Via `localizeManifestItem()` and `localizeManifestItems()` functions
60+
- **Managed by:** Manual editing of manifest files or manifest automation tools
61+
62+
**This skill manages only the UI Translation System.** For manifest translations, edit the manifest JSON files directly or use the manifest-automation skill.
63+
3564
## Enabled Locales
3665

3766
Currently enabled locales in `src/i18n/config.ts`:
3867
- `en` - English (source of truth)
3968
- `de` - Deutsch (German)
40-
- `zh-Hans` - 简体中文 (Simplified Chinese)
69+
- `es` - Español (Spanish)
70+
- `fr` - Français (French)
71+
- `id` - Bahasa Indonesia (Indonesian)
72+
- `ja` - 日本語 (Japanese)
4173
- `ko` - 한국어 (Korean)
74+
- `pt` - Português (Portuguese)
75+
- `ru` - Русский (Russian)
76+
- `tr` - Türkçe (Turkish)
77+
- `zh-Hans` - 简体中文 (Simplified Chinese)
78+
- `zh-Hant` - 繁體中文 (Traditional Chinese)
4279

43-
Additional locale directories may exist but are not enabled in the config.
80+
All 12 locales are fully enabled and must be maintained in sync.
4481

4582
## Subcommands
4683

@@ -117,13 +154,13 @@ Generate translation tasks for Claude Code to translate missing content.
117154
When you need to translate content, use this command in Claude Code:
118155

119156
```
120-
Please run the i18n translate command for zh-Hans
157+
Please run the i18n translate command for ja
121158
```
122159

123160
Claude Code will execute:
124161

125162
```bash
126-
node .claude/skills/i18n/scripts/translate.mjs zh-Hans
163+
node .claude/skills/i18n/scripts/translate.mjs ja
127164
```
128165

129166
**Workflow:**
@@ -145,13 +182,13 @@ node .claude/skills/i18n/scripts/translate.mjs zh-Hans
145182
**Output Example:**
146183

147184
```
148-
🌐 Translation Assistant for zh-Hans
185+
🌐 Translation Assistant for ja
149186
150187
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
151188
📝 Translation Task
152189
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
153190
154-
Target Language: 简体中文 (Simplified Chinese)
191+
Target Language: 日本語 (Japanese)
155192
Entries to translate: 15
156193
157194
Files affected:
@@ -168,13 +205,15 @@ Files affected:
168205
169206
Content to translate:
170207
208+
```json
171209
{
172210
"components.languageSwitcher.english": "English",
173211
"pages.home.hero.title": "Welcome to AI Coding Stack",
174212
"shared.navigation.docs": "Documentation",
175213
...
176214
}
177215
```
216+
```
178217
179218
---
180219
@@ -205,7 +244,16 @@ Each locale has:
205244
// translations/en/index.ts
206245
import components from './components.json'
207246
import articles from './pages/articles.json'
208-
// ... other imports
247+
import comparison from './pages/comparison.json'
248+
import curatedCollections from './pages/curated-collections.json'
249+
import docs from './pages/docs.json'
250+
import home from './pages/home.json'
251+
import landscape from './pages/landscape.json'
252+
import manifesto from './pages/manifesto.json'
253+
import openSourceRank from './pages/open-source-rank.json'
254+
import search from './pages/search.json'
255+
import stacks from './pages/stacks.json'
256+
import shared from './shared.json'
209257
210258
const messages = {
211259
shared,
@@ -216,8 +264,11 @@ const messages = {
216264
docs,
217265
articles,
218266
curatedCollections,
219-
...stacks,
267+
stacks,
220268
comparison,
269+
landscape,
270+
openSourceRank,
271+
search,
221272
},
222273
}
223274
@@ -244,45 +295,59 @@ Becomes: `pages.home.hero.title = "Welcome"`
244295

245296
### Adding a New Language
246297

298+
**Note:** The project currently supports 12 locales. To add a new locale (e.g., Italian 'it'):
299+
247300
1. Add the locale to `src/i18n/config.ts`:
248301

249302
```typescript
250-
export const locales = ['en', 'de', 'zh-Hans', 'ko', 'ja'] as const; // Add 'ja'
303+
export const locales = [
304+
'en', 'de', 'es', 'fr', 'id', 'ja', 'ko', 'pt', 'ru', 'tr', 'zh-Hans', 'zh-Hant',
305+
'it' // Add new locale
306+
] as const;
251307
```
252308

253309
2. Update locale names:
254310

255311
```typescript
256312
export const localeNames: Record<Locale, string> = {
257-
// ...
258-
ja: '日本語',
313+
// ... existing locales
314+
it: 'Italiano',
259315
}
260316

261317
export const localeToOgLocale: Record<Locale, string> = {
262-
// ...
263-
ja: 'ja_JP',
318+
// ... existing locales
319+
it: 'it_IT',
320+
}
321+
```
322+
323+
3. Add to translate.mjs LOCALE_NAMES (`.claude/skills/i18n/scripts/translate.mjs`):
324+
325+
```javascript
326+
const LOCALE_NAMES = {
327+
// ... existing locales
328+
it: 'Italiano (Italian)',
264329
}
265330
```
266331

267-
3. Create the locale directory structure:
332+
4. Create the locale directory structure:
268333

269334
```bash
270-
mkdir -p translations/ja/pages
271-
cp translations/en/index.ts translations/ja/index.ts
272-
cp translations/en/*.json translations/ja/
273-
cp translations/en/pages/*.json translations/ja/pages/
335+
mkdir -p translations/it/pages
336+
cp translations/en/index.ts translations/it/index.ts
337+
cp translations/en/*.json translations/it/
338+
cp translations/en/pages/*.json translations/it/pages/
274339
```
275340

276-
4. Run sync to ensure structure matches:
341+
5. Run sync to ensure structure matches:
277342

278343
```
279344
Please run the i18n sync command
280345
```
281346

282-
5. Run translate to generate translation tasks:
347+
6. Run translate to generate translation tasks:
283348

284349
```
285-
Please run the i18n translate command for ja
350+
Please run the i18n translate command for it
286351
```
287352

288353
## Best Practices
@@ -329,7 +394,45 @@ const rawMessages = (await import(`../../translations/${locale}/index.ts`)).defa
329394
const messages = resolveReferences(rawMessages)
330395
```
331396

332-
The JSON files are loaded through the index.ts for each locale, and the `resolveReferences` function handles `@:path` reference syntax.
397+
The JSON files are loaded through the index.ts for each locale, and the `resolveReferences` function handles reference syntax.
398+
399+
### Reference Resolution
400+
401+
The project supports **reference syntax** for reusing translations:
402+
403+
**Basic Reference:** `@:path.to.key`
404+
```json
405+
{
406+
"shared": {
407+
"appName": "AI Coding Stack",
408+
"welcome": "Welcome to @:shared.appName"
409+
}
410+
}
411+
// Result: "Welcome to AI Coding Stack"
412+
```
413+
414+
**Reference with Modifiers:** `@.modifier:path.to.key`
415+
416+
Supported modifiers:
417+
- `@.upper:path` - Convert to UPPERCASE
418+
- `@.lower:path` - Convert to lowercase
419+
- `@.capitalize:path` - Capitalize first letter
420+
421+
```json
422+
{
423+
"terms": {
424+
"documentation": "documentation",
425+
"title": "@.capitalize:terms.documentation Guide"
426+
}
427+
}
428+
// Result: "Documentation Guide"
429+
```
430+
431+
**Important:**
432+
- References are resolved at runtime by `src/i18n/lib-core.ts`
433+
- Circular references are detected and will throw an error
434+
- References can be nested (references within references)
435+
- Keep reference syntax intact during translation
333436

334437
## License
335438

tests/validate/translations.en.alignment.test.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,7 @@ function extractReferencePath(value: string): string | null {
135135
* Collect all leaf values by their key paths.
136136
* Returns a map of key paths to their actual values.
137137
*/
138-
function collectValueMap(
139-
value: unknown,
140-
prefix: string,
141-
map: Map<string, unknown>
142-
): void {
138+
function collectValueMap(value: unknown, prefix: string, map: Map<string, unknown>): void {
143139
if (value === null || typeof value !== 'object') return
144140

145141
if (Array.isArray(value)) {
@@ -205,16 +201,16 @@ function validateValueAlignment(
205201
if (!localeIsRef) {
206202
failures.push(
207203
`[${locale}] ${filePath}:${keyPath}\n` +
208-
` English uses reference: ${enValue}\n` +
209-
` Locale uses direct value: "${localeValue}"\n` +
210-
` Expected: ${enValue}`
204+
` English uses reference: ${enValue}\n` +
205+
` Locale uses direct value: "${localeValue}"\n` +
206+
` Expected: ${enValue}`
211207
)
212208
} else if (extractReferencePath(localeValue as string) !== enRefPath) {
213209
failures.push(
214210
`[${locale}] ${filePath}:${keyPath}\n` +
215-
` English reference: ${enValue}\n` +
216-
` Locale reference: ${localeValue}\n` +
217-
` Expected same reference path`
211+
` English reference: ${enValue}\n` +
212+
` Locale reference: ${localeValue}\n` +
213+
` Expected same reference path`
218214
)
219215
}
220216
}
@@ -223,9 +219,9 @@ function validateValueAlignment(
223219
if (localeIsRef) {
224220
failures.push(
225221
`[${locale}] ${filePath}:${keyPath}\n` +
226-
` English uses direct value: "${enValue}"\n` +
227-
` Locale uses reference: ${localeValue}\n` +
228-
` Expected: a direct translation (not a reference)`
222+
` English uses direct value: "${enValue}"\n` +
223+
` Locale uses reference: ${localeValue}\n` +
224+
` Expected: a direct translation (not a reference)`
229225
)
230226
}
231227
}
@@ -311,7 +307,12 @@ function validateEnglishAlignment(rootDir: string): string[] {
311307
}
312308

313309
// Check value alignment for this file
314-
if (!fileDiff.onlyInA.length && !fileDiff.onlyInB.length && !keyDiff.onlyInA.length && !keyDiff.onlyInB.length) {
310+
if (
311+
!fileDiff.onlyInA.length &&
312+
!fileDiff.onlyInB.length &&
313+
!keyDiff.onlyInA.length &&
314+
!keyDiff.onlyInB.length
315+
) {
315316
const valueFailures = validateValueAlignment(translationsDir, locale, file)
316317
failures.push(...valueFailures)
317318
}

tests/validate/translations.refs.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { describe, it } from 'vitest'
55

66
// TypeScript can import .mjs because allowJs=true and moduleResolution=bundler.
77
// These exports are the source of truth for reference parsing/resolution.
8-
import { extractReferences, getValueByPath, resolveReference } from '../../src/i18n/lib-core.mjs'
8+
import { extractReferences, getValueByPath, resolveReference } from '../../src/i18n/lib-core'
99

1010
type Reference = { match: string; modifier?: string; path: string }
1111

tests/validate/urls.accessibility.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ function extractUrlsFromManifestItem(
114114
const docsUrl = extractUrlField(item, 'docsUrl', manifestFile, itemId)
115115
if (docsUrl) urls.push(docsUrl)
116116

117-
if (['clis', 'ides', 'extensions'].includes(manifestType)) {
117+
if (['clis', 'ides', 'extensions', 'models'].includes(manifestType)) {
118118
const githubUrl = extractUrlField(item, 'githubUrl', manifestFile, itemId)
119119
if (githubUrl) urls.push(githubUrl)
120120

0 commit comments

Comments
 (0)