Skip to content

Commit 1659da2

Browse files
ericyangpanclaude
andcommitted
refactor: migrate i18n directory from messages/ to locales/
Rename the messages/ directory to locales/ to better align with Next.js internationalization conventions and improve clarity. - Rename messages/*.json to locales/*.json (en, de, ko, zh-Hans) - Update i18n skill documentation to reflect new directory structure - Update sync and translate scripts to use locales/ path - Update i18n request configuration to import from locales/ - Update all locale file structures for consistency - Add language names to cspell dictionary 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2a7ec3a commit 1659da2

File tree

9 files changed

+183
-175
lines changed

9 files changed

+183
-175
lines changed

.claude/skills/i18n/SKILL.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ description: Internationalization management tool for syncing and translating la
55

66
# I18n Management Skill
77

8-
Manage multilingual content in the `messages/` directory. This skill provides tools to synchronize language files with the English reference and assist with translations.
8+
Manage multilingual content in the `locales/` directory. This skill provides tools to synchronize language files with the English reference and assist with translations.
99

1010
## Overview
1111

12-
This project uses `next-intl` for internationalization with JSON message files stored in `messages/`:
12+
This project uses `next-intl` for internationalization with JSON message files stored in `locales/`:
1313

14-
- `messages/en.json` - English (source of truth)
15-
- `messages/zh-Hans.json` - Simplified Chinese
14+
- `locales/en.json` - English (source of truth)
15+
- `locales/zh-Hans.json` - Simplified Chinese
1616
- Additional language files can be added
1717

1818
All language files must maintain the same nested key structure as `en.json`.
@@ -25,7 +25,7 @@ Synchronize all language files with `en.json` as the source of truth.
2525

2626
**What it does:**
2727

28-
- Scans all `.json` files in `messages/` directory
28+
- Scans all `.json` files in `locales/` directory
2929
- Compares each file's keys with `en.json`
3030
- **Adds missing keys** with English text as placeholder (needs translation)
3131
- **Removes extra keys** not present in `en.json`
@@ -195,7 +195,7 @@ export const locales = ['en', 'zh-Hans', 'ja'] as const; // Add 'ja'
195195
2. Create an empty JSON file or copy `en.json`:
196196

197197
```bash
198-
cp messages/en.json messages/ja.json
198+
cp locales/en.json locales/ja.json
199199
```
200200

201201
3. Run sync to ensure structure matches:
@@ -224,7 +224,7 @@ Please run the i18n translate command for ja
224224
**Problem:** Script says "file not found"
225225

226226
- **Solution:** Ensure you're running from project root
227-
- Check that `messages/` directory exists
227+
- Check that `locales/` directory exists
228228

229229
**Problem:** Keys are out of sync after adding new content
230230

@@ -245,7 +245,7 @@ This skill is designed to work with the project's `next-intl` setup:
245245
```typescript
246246
// src/i18n/request.ts
247247
export default getRequestConfig(async ({ locale }) => ({
248-
messages: (await import(`../../messages/${locale}.json`)).default,
248+
messages: (await import(`../../locales/${locale}.json`)).default,
249249
}));
250250
```
251251

.claude/skills/i18n/scripts/sync.mjs

Lines changed: 75 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* Sync all language files with en.json as source of truth
55
*
66
* This script:
7-
* 1. Reads messages/en.json as the reference
8-
* 2. Scans all other .json files in messages/
7+
* 1. Reads locales/en.json as the reference
8+
* 2. Scans all other .json files in locales/
99
* 3. Adds missing keys (with English text as placeholder)
1010
* 4. Removes extra keys not present in en.json
1111
* 5. Preserves JSON structure and formatting
@@ -30,8 +30,8 @@ const colors = {
3030

3131
// Get project root (4 levels up from .claude/skills/i18n/scripts/)
3232
const PROJECT_ROOT = path.resolve(__dirname, '../../../../')
33-
const MESSAGES_DIR = path.join(PROJECT_ROOT, 'messages')
34-
const EN_FILE = path.join(MESSAGES_DIR, 'en.json')
33+
const LOCALES_DIR = path.join(PROJECT_ROOT, 'locales')
34+
const EN_FILE = path.join(LOCALES_DIR, 'en.json')
3535

3636
/**
3737
* Recursively get all keys from a nested object
@@ -103,6 +103,68 @@ function deleteValueByPath(obj, path) {
103103
}
104104
}
105105

106+
/**
107+
* Recursively rebuild object with same structure and order as reference
108+
* @param {Object} reference - The reference object (en.json)
109+
* @param {Object} target - The target object to sync
110+
* @param {Array} added - Array to track added keys
111+
* @param {Array} removed - Array to track removed keys
112+
* @param {string} prefix - Current key path prefix
113+
* @returns {Object} Rebuilt object with same structure as reference
114+
*/
115+
function rebuildWithSameOrder(reference, target, added, removed, prefix = '') {
116+
const result = {}
117+
118+
// Iterate through reference keys in order
119+
for (const [key, refValue] of Object.entries(reference)) {
120+
const fullKey = prefix ? `${prefix}.${key}` : key
121+
122+
if (refValue !== null && typeof refValue === 'object' && !Array.isArray(refValue)) {
123+
// It's a nested object
124+
if (
125+
key in target &&
126+
typeof target[key] === 'object' &&
127+
target[key] !== null &&
128+
!Array.isArray(target[key])
129+
) {
130+
// Recursively rebuild nested object
131+
result[key] = rebuildWithSameOrder(refValue, target[key], added, removed, fullKey)
132+
} else {
133+
// Missing nested object, use reference structure
134+
result[key] = rebuildWithSameOrder(refValue, {}, added, removed, fullKey)
135+
// Track all leaf keys as added
136+
const leafKeys = getAllKeys(refValue, fullKey)
137+
added.push(...leafKeys)
138+
}
139+
} else {
140+
// It's a leaf value
141+
if (key in target) {
142+
// Use target's translation
143+
result[key] = target[key]
144+
} else {
145+
// Missing key, use English as placeholder
146+
result[key] = refValue
147+
added.push(fullKey)
148+
}
149+
}
150+
}
151+
152+
// Track removed keys (keys in target but not in reference)
153+
for (const key in target) {
154+
const fullKey = prefix ? `${prefix}.${key}` : key
155+
if (!(key in reference)) {
156+
if (typeof target[key] === 'object' && target[key] !== null && !Array.isArray(target[key])) {
157+
const leafKeys = getAllKeys(target[key], fullKey)
158+
removed.push(...leafKeys)
159+
} else {
160+
removed.push(fullKey)
161+
}
162+
}
163+
}
164+
165+
return result
166+
}
167+
106168
/**
107169
* Sync a target language file with the English reference
108170
* @param {string} targetFile - Path to the target language file
@@ -111,33 +173,15 @@ function deleteValueByPath(obj, path) {
111173
*/
112174
function syncLanguageFile(targetFile, enData) {
113175
const targetData = JSON.parse(fs.readFileSync(targetFile, 'utf-8'))
114-
const enKeys = getAllKeys(enData)
115-
const targetKeys = getAllKeys(targetData)
116176

117177
const added = []
118178
const removed = []
119179

120-
// Add missing keys from en.json
121-
for (const key of enKeys) {
122-
if (!targetKeys.includes(key)) {
123-
const enValue = getValueByPath(enData, key)
124-
setValueByPath(targetData, key, enValue)
125-
added.push(key)
126-
}
127-
}
128-
129-
// Remove extra keys not in en.json
130-
for (const key of targetKeys) {
131-
if (!enKeys.includes(key)) {
132-
deleteValueByPath(targetData, key)
133-
removed.push(key)
134-
}
135-
}
180+
// Rebuild target with same structure and order as en.json
181+
const syncedData = rebuildWithSameOrder(enData, targetData, added, removed)
136182

137-
// Write back with consistent formatting (2 spaces indentation)
138-
if (added.length > 0 || removed.length > 0) {
139-
fs.writeFileSync(targetFile, `${JSON.stringify(targetData, null, 2)}\n`, 'utf-8')
140-
}
183+
// Always write to ensure correct order
184+
fs.writeFileSync(targetFile, `${JSON.stringify(syncedData, null, 2)}\n`, 'utf-8')
141185

142186
return { added, removed }
143187
}
@@ -148,9 +192,9 @@ function syncLanguageFile(targetFile, enData) {
148192
function main() {
149193
console.log(`${colors.cyan}🔄 Syncing language files with en.json...${colors.reset}\n`)
150194

151-
// Check if messages directory exists
152-
if (!fs.existsSync(MESSAGES_DIR)) {
153-
console.error(`${colors.red}Messages directory not found: ${MESSAGES_DIR}${colors.reset}`)
195+
// Check if locales directory exists
196+
if (!fs.existsSync(LOCALES_DIR)) {
197+
console.error(`${colors.red}Locales directory not found: ${LOCALES_DIR}${colors.reset}`)
154198
process.exit(1)
155199
}
156200

@@ -164,9 +208,9 @@ function main() {
164208

165209
// Get all language files except en.json
166210
const files = fs
167-
.readdirSync(MESSAGES_DIR)
211+
.readdirSync(LOCALES_DIR)
168212
.filter(file => file.endsWith('.json') && file !== 'en.json')
169-
.map(file => path.join(MESSAGES_DIR, file))
213+
.map(file => path.join(LOCALES_DIR, file))
170214

171215
if (files.length === 0) {
172216
console.log(`${colors.yellow}⚠ No other language files found to sync${colors.reset}`)

.claude/skills/i18n/scripts/translate.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Translate language file with Claude Code assistance
55
*
66
* This script:
7-
* 1. Reads messages/en.json and messages/<locale>.json
7+
* 1. Reads locales/en.json and locales/<locale>.json
88
* 2. Identifies keys that need translation (currently in English)
99
* 3. Outputs translation tasks for Claude Code to perform
1010
* 4. Reads translated content from stdin
@@ -31,8 +31,8 @@ const colors = {
3131

3232
// Get project root (4 levels up from .claude/skills/i18n/scripts/)
3333
const PROJECT_ROOT = path.resolve(__dirname, '../../../../')
34-
const MESSAGES_DIR = path.join(PROJECT_ROOT, 'messages')
35-
const EN_FILE = path.join(MESSAGES_DIR, 'en.json')
34+
const LOCALES_DIR = path.join(PROJECT_ROOT, 'locales')
35+
const EN_FILE = path.join(LOCALES_DIR, 'en.json')
3636

3737
// Locale display names
3838
const LOCALE_NAMES = {
@@ -228,7 +228,7 @@ function main() {
228228
process.exit(1)
229229
}
230230

231-
const targetFile = path.join(MESSAGES_DIR, `${locale}.json`)
231+
const targetFile = path.join(LOCALES_DIR, `${locale}.json`)
232232

233233
console.log(`${colors.cyan}🌐 Translation Assistant for ${locale}${colors.reset}\n`)
234234

cspell.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,22 @@
2020
"CLIs",
2121
"cmdk",
2222
"Entwicklungsworkflows",
23+
"Español",
24+
"Français",
2325
"hrefs",
2426
"Integrationen",
2527
"Junie",
2628
"Kimi",
2729
"Kiro",
2830
"Kode",
2931
"Neovate",
32+
"Português",
3033
"Qoder",
3134
"QoderCLI",
3235
"Qwen",
3336
"RLHF",
3437
"Roo",
38+
"Русский",
3539
"shareAI",
3640
"TRAE"
3741
],

messages/de.json renamed to locales/de.json

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
{
2+
"common": {
3+
"comparison": "Comparison"
4+
},
25
"stacks": {
3-
"aiCodingStack": "AI Coding Stack",
46
"ides": "IDEs",
57
"clis": "CLIs",
68
"extensions": "Erweiterungen",
79
"models": "Modelle",
810
"modelProviders": "Modellanbieter",
9-
"vendors": "Anbieter",
10-
"comparison": "Vergleich"
11+
"vendors": "Anbieter"
1112
},
1213
"community": {
1314
"github": "GitHub",
@@ -21,19 +22,13 @@
2122
"openrouter": "OpenRouter"
2223
},
2324
"header": {
24-
"docs": "Dokumentation",
25-
"articles": "Artikel",
26-
"curatedCollections": "Kuratierte Sammlungen",
27-
"comparison": "Vergleich",
28-
"features": "Funktionen",
29-
"manifesto": "Manifest",
3025
"aiCodingStack": "AI Coding Stack",
31-
"collections": "Sammlungen",
26+
"manifesto": "Manifest",
3227
"landscape": "Landschaft",
33-
"aiCodingLandscape": "KI-Coding-Landschaft",
34-
"aiCodingLandscapeDesc": "Visualisieren Sie das gesamte KI-Coding-Ökosystem",
28+
"ranking": "Ranking",
3529
"openSourceRank": "Open Source Ranking",
3630
"openSourceRankDesc": "Erkunden Sie Open-Source-KI-Coding-Projekte nach GitHub-Sternen",
31+
"collections": "Sammlungen",
3732
"developmentTools": "Entwicklungstools",
3833
"intelligence": "Intelligenz",
3934
"toggleMenu": "Menü umschalten",
@@ -42,6 +37,22 @@
4237
"search": "Suchen",
4338
"searchPlaceholder": "Suchen..."
4439
},
40+
"footer": {
41+
"tagline": "Ihr KI-Coding-Ökosystem-Hub.",
42+
"openSource": "Open-Source-KI-Coding-Metadaten-Repository.",
43+
"resources": "Ressourcen",
44+
"documentation": "Dokumentation",
45+
"docs": "Docs",
46+
"articles": "Articles",
47+
"curatedCollections": "Curated Collections",
48+
"faq": "FAQ",
49+
"community": "Community",
50+
"copyright": "© 2025 AI Coding Stack • Mit ❤︎ erstellt • Open Source",
51+
"darkMode": "Dunkelmodus",
52+
"lightMode": "Hellmodus",
53+
"selectLanguage": "Sprache auswählen",
54+
"toggleTheme": "Thema umschalten"
55+
},
4556
"search": {
4657
"title": "Suchergebnisse",
4758
"placeholder": "KI-Coding-Tools durchsuchen...",
@@ -61,20 +72,6 @@
6172
"vendors": "Anbieter"
6273
}
6374
},
64-
"footer": {
65-
"aicodingstack": "AI Coding Stack",
66-
"tagline": "Ihr KI-Coding-Ökosystem-Hub.",
67-
"openSource": "Open-Source-KI-Coding-Metadaten-Repository.",
68-
"resources": "Ressourcen",
69-
"documentation": "Dokumentation",
70-
"community": "Community",
71-
"copyright": "© 2025 AI Coding Stack • Mit ❤︎ erstellt • Open Source",
72-
"darkMode": "Dunkelmodus",
73-
"lightMode": "Hellmodus",
74-
"faq": "FAQ",
75-
"selectLanguage": "Sprache auswählen",
76-
"toggleTheme": "Thema umschalten"
77-
},
7875
"pages": {
7976
"home": {
8077
"meta": {
@@ -410,11 +407,6 @@
410407
"articles": "Featured Artikel",
411408
"tools": "Ökosystem-Tools"
412409
},
413-
"stackSidebar": {
414-
"title": "STACKS",
415-
"label": "AI Coding Stack",
416-
"overview": "Übersicht"
417-
},
418410
"filterSortBar": {
419411
"search": "Nach Namen suchen...",
420412
"sort": "Sortieren:",

0 commit comments

Comments
 (0)