@djangocfg/i18n 2.1.217 → 2.1.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -159
- package/dist/cli/index.mjs +3 -366
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +4 -8
- package/src/cli/commands/index.ts +0 -2
- package/src/cli/index.ts +0 -7
- package/src/cli/utils/index.ts +0 -1
- package/src/cli/utils/locales.ts +2 -1
- package/src/cli/commands/sync.ts +0 -233
- package/src/cli/commands/translate.ts +0 -208
- package/src/cli/utils/translator.ts +0 -97
package/README.md
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# @djangocfg/i18n
|
|
2
2
|
|
|
3
|
-
Lightweight, type-safe i18n library
|
|
3
|
+
Lightweight, type-safe i18n library for @djangocfg packages.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **17 languages**
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **Works standalone** - Components work without provider
|
|
7
|
+
- **17 languages** — en, ru, ko, ja, de, fr, zh, it, es, nl, ar, tr, pt-BR, pl, sv, no, da
|
|
8
|
+
- **Type-safe** — Full TypeScript support with autocomplete
|
|
9
|
+
- **CLI included** — Manage locales from the terminal
|
|
10
|
+
- **Works standalone** — Components work without provider
|
|
12
11
|
|
|
13
12
|
## Installation
|
|
14
13
|
|
|
@@ -21,12 +20,10 @@ pnpm add @djangocfg/i18n
|
|
|
21
20
|
```tsx
|
|
22
21
|
import { I18nProvider, useT, ru } from '@djangocfg/i18n'
|
|
23
22
|
|
|
24
|
-
// Wrap your app
|
|
25
23
|
<I18nProvider locale="ru" translations={ru}>
|
|
26
24
|
<App />
|
|
27
25
|
</I18nProvider>
|
|
28
26
|
|
|
29
|
-
// Use in components
|
|
30
27
|
function MyComponent() {
|
|
31
28
|
const t = useT()
|
|
32
29
|
return <span>{t('ui.form.save')}</span>
|
|
@@ -35,8 +32,6 @@ function MyComponent() {
|
|
|
35
32
|
|
|
36
33
|
## Subpath Imports (Server Components)
|
|
37
34
|
|
|
38
|
-
For Next.js server components, use subpath imports to avoid React Context issues:
|
|
39
|
-
|
|
40
35
|
```tsx
|
|
41
36
|
// ✅ Server-safe (no React Context)
|
|
42
37
|
import { en, ru, ko } from '@djangocfg/i18n/locales'
|
|
@@ -54,85 +49,21 @@ import { I18nProvider, useT } from '@djangocfg/i18n'
|
|
|
54
49
|
|
|
55
50
|
## CLI
|
|
56
51
|
|
|
57
|
-
Built-in CLI with LLM translation support.
|
|
58
|
-
|
|
59
|
-
### Translate Text
|
|
60
|
-
|
|
61
52
|
```bash
|
|
62
|
-
#
|
|
63
|
-
pnpm i18n translate "Hello World" --to ru,ko,ja
|
|
64
|
-
# => ru: Привет, мир
|
|
65
|
-
# => ko: 안녕하세요, 세계
|
|
66
|
-
# => ja: こんにちは、世界
|
|
67
|
-
|
|
68
|
-
# Output as JSON
|
|
69
|
-
pnpm i18n translate "Save" --to ru,ko --json
|
|
70
|
-
# => {"en":"Save","ru":"Сохранить","ko":"저장"}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Translate Locale File
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
# Translate entire en.ts to Russian
|
|
77
|
-
pnpm i18n translate ru
|
|
78
|
-
|
|
79
|
-
# Or explicit
|
|
80
|
-
pnpm i18n translate --file --to ru --from en
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
### Sync Missing Keys
|
|
84
|
-
|
|
85
|
-
```bash
|
|
86
|
-
# Show missing keys
|
|
87
|
-
pnpm i18n sync --dry
|
|
88
|
-
|
|
89
|
-
# Add with [TODO] placeholders
|
|
90
|
-
pnpm i18n sync
|
|
91
|
-
|
|
92
|
-
# Sync with LLM translation
|
|
93
|
-
pnpm i18n sync --translate
|
|
94
|
-
|
|
95
|
-
# Sync specific locales
|
|
96
|
-
pnpm i18n sync --to ru,ko --translate
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Other Commands
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
# List/search keys
|
|
103
|
-
pnpm i18n list # All keys
|
|
104
|
-
pnpm i18n list tour # Search pattern
|
|
105
|
-
pnpm i18n list -v # With values
|
|
106
|
-
|
|
107
|
-
# Check missing keys
|
|
53
|
+
# Check missing keys across locales
|
|
108
54
|
pnpm i18n check
|
|
109
55
|
|
|
110
|
-
#
|
|
111
|
-
pnpm i18n
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
pnpm i18n translate --stats
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Working with App Locales
|
|
118
|
-
|
|
119
|
-
CLI supports any locales directory (.ts or .json files):
|
|
56
|
+
# List / search keys
|
|
57
|
+
pnpm i18n list
|
|
58
|
+
pnpm i18n list tour # filter by pattern
|
|
59
|
+
pnpm i18n list -v # with values
|
|
120
60
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
pnpm i18n translate "Dashboard" --to ru,ko -d ../../apps/hub/i18n/locales
|
|
124
|
-
|
|
125
|
-
# Sync with LLM translation
|
|
126
|
-
pnpm i18n sync -d ../../apps/hub/i18n/locales --translate
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Environment Variables
|
|
61
|
+
# Add key to all locales at once
|
|
62
|
+
pnpm i18n add "tools.new" '{"en":"New","ru":"Новый"}'
|
|
130
63
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
OPENAI_API_KEY=your-key # OpenAI
|
|
135
|
-
ANTHROPIC_API_KEY=your-key # Anthropic
|
|
64
|
+
# Work with app locales in another directory
|
|
65
|
+
pnpm i18n check -d ../../apps/hub/i18n/locales
|
|
66
|
+
pnpm i18n list -d ../../apps/hub/i18n/locales
|
|
136
67
|
```
|
|
137
68
|
|
|
138
69
|
## Hooks
|
|
@@ -168,37 +99,10 @@ function LocaleSwitcher() {
|
|
|
168
99
|
}
|
|
169
100
|
```
|
|
170
101
|
|
|
171
|
-
### useTypedT<T>()
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
import { useTypedT } from '@djangocfg/i18n'
|
|
175
|
-
import type { I18nTranslations } from '@djangocfg/i18n'
|
|
176
|
-
|
|
177
|
-
function MyComponent() {
|
|
178
|
-
const t = useTypedT<I18nTranslations>()
|
|
179
|
-
return <span>{t('ui.form.save')}</span> // OK
|
|
180
|
-
// t('ui.form.typo') // Compile error!
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
102
|
## Type-safe next-intl Integration
|
|
185
103
|
|
|
186
104
|
Override `useTranslations` in your app's `global.d.ts` to get compile-time key validation.
|
|
187
105
|
|
|
188
|
-
### 1. Merge translations (flat, no namespace)
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
// i18n/request.ts
|
|
192
|
-
import { en as baseEn } from '@djangocfg/i18n/locales';
|
|
193
|
-
import { en as appEn } from './locales';
|
|
194
|
-
|
|
195
|
-
const locales = {
|
|
196
|
-
en: { ...baseEn, ...appEn },
|
|
197
|
-
};
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### 2. Add `global.d.ts` to your app root
|
|
201
|
-
|
|
202
106
|
```ts
|
|
203
107
|
// global.d.ts
|
|
204
108
|
type _Messages = import('@djangocfg/i18n').I18nTranslations &
|
|
@@ -216,68 +120,25 @@ declare module 'next-intl' {
|
|
|
216
120
|
}
|
|
217
121
|
```
|
|
218
122
|
|
|
219
|
-
### 3. Include in tsconfig.json
|
|
220
|
-
|
|
221
|
-
```json
|
|
222
|
-
{ "include": ["global.d.ts", "app/**/*.ts", ...] }
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Now invalid keys and namespaces produce compile errors:
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
const t = useTranslations('machines');
|
|
229
|
-
t('title'); // OK
|
|
230
|
-
t('dialogs.delete.title'); // OK
|
|
231
|
-
t('NONEXISTENT'); // Error!
|
|
232
|
-
|
|
233
|
-
useTranslations('BOGUS'); // Error!
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
> **Why not `use-intl` AppConfig?** The standard `declare module 'use-intl' { interface AppConfig }` augmentation doesn't propagate through pnpm's nested `node_modules`. Overriding `useTranslations` in `next-intl` directly works reliably.
|
|
237
|
-
|
|
238
|
-
### Exported utility types
|
|
239
|
-
|
|
240
|
-
| Type | Description |
|
|
241
|
-
|------|-------------|
|
|
242
|
-
| `NestedKeyOf<T>` | All dot-separated paths (leaves + namespaces) |
|
|
243
|
-
| `NestedValueOf<T, P>` | Resolve value type by dot path |
|
|
244
|
-
| `NamespaceKeys<T, A>` | Paths resolving to objects (valid namespaces) |
|
|
245
|
-
| `MessageKeys<T, A>` | Paths resolving to strings (valid keys) |
|
|
246
|
-
| `IntlTranslator<M, NS>` | Type-safe translator with `t()`, `rich()`, `has()`, `raw()` |
|
|
247
|
-
|
|
248
123
|
## Extending Translations
|
|
249
124
|
|
|
250
|
-
### Spread merge (recommended for next-intl apps)
|
|
251
|
-
|
|
252
125
|
```ts
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
126
|
+
// Spread merge (recommended for next-intl)
|
|
127
|
+
import { en as baseEn } from '@djangocfg/i18n/locales'
|
|
128
|
+
const messages = { ...baseEn, ...appEn }
|
|
256
129
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
```tsx
|
|
130
|
+
// Deep merge with overrides
|
|
260
131
|
import { mergeTranslations, ru } from '@djangocfg/i18n'
|
|
261
|
-
|
|
262
132
|
const customRu = mergeTranslations(ru, {
|
|
263
133
|
ui: { select: { placeholder: 'Выберите...' } },
|
|
264
134
|
})
|
|
265
135
|
```
|
|
266
136
|
|
|
267
|
-
## Built-in Locales
|
|
268
|
-
|
|
269
|
-
```tsx
|
|
270
|
-
import { en, ru, ko, ja, de, fr, zh, it, es, nl, ar, tr, ptBR, pl, sv, no, da } from '@djangocfg/i18n'
|
|
271
|
-
```
|
|
272
|
-
|
|
273
137
|
## Interpolation
|
|
274
138
|
|
|
275
139
|
```tsx
|
|
276
140
|
t('ui.pagination.showing', { from: 1, to: 10, total: 100 })
|
|
277
141
|
// => "1-10 of 100"
|
|
278
|
-
|
|
279
|
-
t('ui.select.moreItems', { count: 5 })
|
|
280
|
-
// => "+5 more"
|
|
281
142
|
```
|
|
282
143
|
|
|
283
144
|
## Translation Key Paths
|
|
@@ -292,7 +153,7 @@ tools.* - Heavy tools (tour, upload, code, image)
|
|
|
292
153
|
|
|
293
154
|
## Works Without Provider
|
|
294
155
|
|
|
295
|
-
Components using `useT()`
|
|
156
|
+
Components using `useT()` fall back to English defaults when used outside a provider.
|
|
296
157
|
|
|
297
158
|
## License
|
|
298
159
|
|
package/dist/cli/index.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import { consola } from 'consola';
|
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import { createJiti } from 'jiti';
|
|
7
|
-
import { createLLMClient, createTranslator } from '@djangocfg/llm';
|
|
8
7
|
|
|
9
8
|
var jiti = createJiti(import.meta.url);
|
|
10
9
|
function detectLocaleConfig(dir) {
|
|
@@ -53,7 +52,8 @@ async function loadLocale(config, locale) {
|
|
|
53
52
|
try {
|
|
54
53
|
const module = await jiti.import(filePath);
|
|
55
54
|
const exportName = getExportName(locale);
|
|
56
|
-
|
|
55
|
+
const m = module;
|
|
56
|
+
return m[exportName] ?? m["default"] ?? {};
|
|
57
57
|
} catch (error) {
|
|
58
58
|
consola.error(`Failed to load ${locale}:`, error);
|
|
59
59
|
return {};
|
|
@@ -101,32 +101,6 @@ function getValueAtPath(obj, keyPath) {
|
|
|
101
101
|
}
|
|
102
102
|
return current;
|
|
103
103
|
}
|
|
104
|
-
var DEFAULT_API_KEY = "test-api-key";
|
|
105
|
-
var translator = null;
|
|
106
|
-
function getTranslator() {
|
|
107
|
-
if (!translator) {
|
|
108
|
-
const apiKey = process.env.SDKROUTER_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY || DEFAULT_API_KEY;
|
|
109
|
-
const llm = createLLMClient({
|
|
110
|
-
provider: "sdkrouter",
|
|
111
|
-
apiKey
|
|
112
|
-
});
|
|
113
|
-
translator = createTranslator(llm);
|
|
114
|
-
}
|
|
115
|
-
return translator;
|
|
116
|
-
}
|
|
117
|
-
async function translateText(text, targetLocale, sourceLocale = "en") {
|
|
118
|
-
const t = getTranslator();
|
|
119
|
-
return t.translateText(text, targetLocale, { sourceLanguage: sourceLocale });
|
|
120
|
-
}
|
|
121
|
-
async function translateJson(data, targetLocale, sourceLocale = "en") {
|
|
122
|
-
const t = getTranslator();
|
|
123
|
-
const result = await t.translate(data, targetLocale, { sourceLanguage: sourceLocale });
|
|
124
|
-
return result.data;
|
|
125
|
-
}
|
|
126
|
-
function getTranslatorStats() {
|
|
127
|
-
if (!translator) return null;
|
|
128
|
-
return translator.getStats();
|
|
129
|
-
}
|
|
130
104
|
|
|
131
105
|
// src/cli/commands/list.ts
|
|
132
106
|
var listCommand = defineCommand({
|
|
@@ -413,341 +387,6 @@ ${baseIndent}},`;
|
|
|
413
387
|
function escapeQuotes(value) {
|
|
414
388
|
return value.replace(/'/g, "\\'");
|
|
415
389
|
}
|
|
416
|
-
var translateCommand = defineCommand({
|
|
417
|
-
meta: {
|
|
418
|
-
name: "translate",
|
|
419
|
-
description: "Translate text or locale file using LLM"
|
|
420
|
-
},
|
|
421
|
-
args: {
|
|
422
|
-
text: {
|
|
423
|
-
type: "positional",
|
|
424
|
-
description: 'Text to translate or locale code (e.g., "Hello" or "ru")',
|
|
425
|
-
required: false
|
|
426
|
-
},
|
|
427
|
-
dir: {
|
|
428
|
-
type: "string",
|
|
429
|
-
alias: "d",
|
|
430
|
-
description: "Locales directory"
|
|
431
|
-
},
|
|
432
|
-
from: {
|
|
433
|
-
type: "string",
|
|
434
|
-
alias: "f",
|
|
435
|
-
description: "Source locale (default: en)",
|
|
436
|
-
default: "en"
|
|
437
|
-
},
|
|
438
|
-
to: {
|
|
439
|
-
type: "string",
|
|
440
|
-
alias: "t",
|
|
441
|
-
description: 'Target locales (comma-separated, e.g., "ru,ko,ja")'
|
|
442
|
-
},
|
|
443
|
-
json: {
|
|
444
|
-
type: "boolean",
|
|
445
|
-
alias: "j",
|
|
446
|
-
description: "Output as JSON",
|
|
447
|
-
default: false
|
|
448
|
-
},
|
|
449
|
-
file: {
|
|
450
|
-
type: "boolean",
|
|
451
|
-
description: "Translate entire locale file",
|
|
452
|
-
default: false
|
|
453
|
-
},
|
|
454
|
-
stats: {
|
|
455
|
-
type: "boolean",
|
|
456
|
-
alias: "s",
|
|
457
|
-
description: "Show translation cache stats",
|
|
458
|
-
default: false
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
async run({ args }) {
|
|
462
|
-
if (args.stats) {
|
|
463
|
-
const stats = getTranslatorStats();
|
|
464
|
-
if (stats) {
|
|
465
|
-
consola.info("Translation Cache Stats:");
|
|
466
|
-
consola.log(` Memory size: ${stats.memorySize}`);
|
|
467
|
-
consola.log(` Hits: ${stats.hits}`);
|
|
468
|
-
consola.log(` Misses: ${stats.misses}`);
|
|
469
|
-
if (stats.languagePairs.length > 0) {
|
|
470
|
-
consola.log(" Language pairs:");
|
|
471
|
-
for (const { pair, translations } of stats.languagePairs) {
|
|
472
|
-
consola.log(` ${pair}: ${translations} translations`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
} else {
|
|
476
|
-
consola.info("No translations performed yet.");
|
|
477
|
-
}
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const localesDir = args.dir ? path.resolve(process.cwd(), args.dir) : getDefaultLocalesDir();
|
|
481
|
-
try {
|
|
482
|
-
const config = detectLocaleConfig(localesDir);
|
|
483
|
-
const sourceLocale = args.from || "en";
|
|
484
|
-
let targetLocales;
|
|
485
|
-
if (args.to) {
|
|
486
|
-
targetLocales = args.to.split(",").map((l) => l.trim());
|
|
487
|
-
} else {
|
|
488
|
-
targetLocales = config.locales.filter((l) => l !== sourceLocale);
|
|
489
|
-
}
|
|
490
|
-
if (args.file || args.text && config.locales.includes(args.text)) {
|
|
491
|
-
const targetLocale = args.text || targetLocales[0];
|
|
492
|
-
if (!targetLocale) {
|
|
493
|
-
consola.error("Please specify target locale");
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
consola.info(`Translating ${sourceLocale} -> ${targetLocale}`);
|
|
497
|
-
consola.info(`Directory: ${localesDir}`);
|
|
498
|
-
const sourceData = await loadLocale(config, sourceLocale);
|
|
499
|
-
const startTime = Date.now();
|
|
500
|
-
const topLevelKeys = Object.keys(sourceData);
|
|
501
|
-
const translated = {};
|
|
502
|
-
for (const section of topLevelKeys) {
|
|
503
|
-
const sectionData = sourceData[section];
|
|
504
|
-
consola.info(` [${section}] translating...`);
|
|
505
|
-
const sectionTranslated = await translateJson(
|
|
506
|
-
sectionData,
|
|
507
|
-
targetLocale,
|
|
508
|
-
sourceLocale
|
|
509
|
-
);
|
|
510
|
-
translated[section] = sectionTranslated;
|
|
511
|
-
consola.success(` [${section}] done`);
|
|
512
|
-
}
|
|
513
|
-
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
514
|
-
consola.success(`Translated in ${elapsed}s`);
|
|
515
|
-
if (config.fileExtension === ".ts") {
|
|
516
|
-
const exportName = getExportName(targetLocale);
|
|
517
|
-
const { content } = readLocaleFile(config, targetLocale);
|
|
518
|
-
const jsonStr = JSON.stringify(translated, null, 2);
|
|
519
|
-
const newContent = content.replace(
|
|
520
|
-
/(export const \w+[^=]*=\s*)\{[\s\S]*$/,
|
|
521
|
-
`$1${jsonStr};
|
|
522
|
-
`
|
|
523
|
-
);
|
|
524
|
-
const finalContent = newContent !== content ? newContent : `import type { DocsTranslations } from './en';
|
|
525
|
-
|
|
526
|
-
export const ${exportName}: DocsTranslations = ${jsonStr};
|
|
527
|
-
`;
|
|
528
|
-
writeLocaleFile(config, targetLocale, finalContent);
|
|
529
|
-
consola.success(`Written to ${targetLocale}.ts`);
|
|
530
|
-
} else {
|
|
531
|
-
writeLocaleFile(config, targetLocale, JSON.stringify(translated, null, 2));
|
|
532
|
-
consola.success(`Written to ${targetLocale}.json`);
|
|
533
|
-
}
|
|
534
|
-
if (args.json) {
|
|
535
|
-
console.log(JSON.stringify(translated, null, 2));
|
|
536
|
-
} else {
|
|
537
|
-
const sample = JSON.stringify(translated, null, 2).slice(0, 300);
|
|
538
|
-
consola.log(sample + (sample.length >= 300 ? "\n..." : ""));
|
|
539
|
-
}
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
const text = args.text;
|
|
543
|
-
if (!text) {
|
|
544
|
-
consola.error("Please provide text to translate or use --file flag");
|
|
545
|
-
consola.log("");
|
|
546
|
-
consola.log("Examples:");
|
|
547
|
-
consola.log(' pnpm i18n translate "Hello World" --to ru,ko');
|
|
548
|
-
consola.log(" pnpm i18n translate --file --to ru");
|
|
549
|
-
consola.log(" pnpm i18n translate ru # Translate en.ts to ru");
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
consola.info(`Translating: "${text}"`);
|
|
553
|
-
consola.log("");
|
|
554
|
-
const translations = {
|
|
555
|
-
[sourceLocale]: text
|
|
556
|
-
};
|
|
557
|
-
for (const locale of targetLocales) {
|
|
558
|
-
try {
|
|
559
|
-
const translated = await translateText(text, locale, sourceLocale);
|
|
560
|
-
translations[locale] = translated;
|
|
561
|
-
consola.log(` ${locale}: ${translated}`);
|
|
562
|
-
} catch (error) {
|
|
563
|
-
consola.warn(` ${locale}: [FAILED] ${error instanceof Error ? error.message : error}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
consola.log("");
|
|
567
|
-
if (args.json) {
|
|
568
|
-
console.log(JSON.stringify(translations, null, 2));
|
|
569
|
-
} else {
|
|
570
|
-
consola.log("Copy-paste JSON:");
|
|
571
|
-
console.log(JSON.stringify(translations));
|
|
572
|
-
}
|
|
573
|
-
} catch (error) {
|
|
574
|
-
consola.error(error);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
var syncCommand = defineCommand({
|
|
579
|
-
meta: {
|
|
580
|
-
name: "sync",
|
|
581
|
-
description: "Sync missing keys from base locale to all others using LLM"
|
|
582
|
-
},
|
|
583
|
-
args: {
|
|
584
|
-
dir: {
|
|
585
|
-
type: "string",
|
|
586
|
-
alias: "d",
|
|
587
|
-
description: "Locales directory"
|
|
588
|
-
},
|
|
589
|
-
base: {
|
|
590
|
-
type: "string",
|
|
591
|
-
alias: "b",
|
|
592
|
-
description: "Base locale",
|
|
593
|
-
default: "en"
|
|
594
|
-
},
|
|
595
|
-
to: {
|
|
596
|
-
type: "string",
|
|
597
|
-
alias: "t",
|
|
598
|
-
description: "Target locales (comma-separated). Default: all except base"
|
|
599
|
-
},
|
|
600
|
-
dry: {
|
|
601
|
-
type: "boolean",
|
|
602
|
-
description: "Dry run - show what would be changed",
|
|
603
|
-
default: false
|
|
604
|
-
},
|
|
605
|
-
translate: {
|
|
606
|
-
type: "boolean",
|
|
607
|
-
alias: "T",
|
|
608
|
-
description: "Translate missing values using LLM (default: add [TODO] placeholders)",
|
|
609
|
-
default: false
|
|
610
|
-
},
|
|
611
|
-
stats: {
|
|
612
|
-
type: "boolean",
|
|
613
|
-
alias: "s",
|
|
614
|
-
description: "Show translation stats after sync",
|
|
615
|
-
default: false
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
async run({ args }) {
|
|
619
|
-
const localesDir = args.dir ? path.resolve(process.cwd(), args.dir) : getDefaultLocalesDir();
|
|
620
|
-
try {
|
|
621
|
-
const config = detectLocaleConfig(localesDir);
|
|
622
|
-
const baseLocale = args.base || "en";
|
|
623
|
-
if (!config.locales.includes(baseLocale)) {
|
|
624
|
-
consola.error(`Base locale "${baseLocale}" not found.`);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
let targetLocales;
|
|
628
|
-
if (args.to) {
|
|
629
|
-
targetLocales = args.to.split(",").map((l) => l.trim());
|
|
630
|
-
} else {
|
|
631
|
-
targetLocales = config.locales.filter((l) => l !== baseLocale);
|
|
632
|
-
}
|
|
633
|
-
consola.info(`Syncing locales from ${baseLocale}`);
|
|
634
|
-
consola.info(`Directory: ${localesDir}`);
|
|
635
|
-
consola.info(`Targets: ${targetLocales.join(", ")}`);
|
|
636
|
-
if (args.dry) consola.info("(Dry run - no changes will be made)");
|
|
637
|
-
if (args.translate) consola.info("(LLM batch translation enabled)");
|
|
638
|
-
consola.log("");
|
|
639
|
-
const baseTranslations = await loadLocale(config, baseLocale);
|
|
640
|
-
const baseKeys = getAllKeys(baseTranslations);
|
|
641
|
-
let totalAdded = 0;
|
|
642
|
-
let totalTranslated = 0;
|
|
643
|
-
for (const locale of targetLocales) {
|
|
644
|
-
if (!config.locales.includes(locale)) {
|
|
645
|
-
consola.warn(`Locale "${locale}" not found, skipping`);
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
const translations = await loadLocale(config, locale);
|
|
649
|
-
const missing = [];
|
|
650
|
-
for (const { path: keyPath, value } of baseKeys) {
|
|
651
|
-
const localeValue = getValueAtPath(translations, keyPath);
|
|
652
|
-
if (localeValue === void 0) {
|
|
653
|
-
missing.push({ path: keyPath, value });
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (missing.length === 0) {
|
|
657
|
-
consola.success(`${locale}: All keys present`);
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
consola.warn(`${locale}: ${missing.length} missing keys`);
|
|
661
|
-
if (args.dry) {
|
|
662
|
-
for (const { path: keyPath, value } of missing.slice(0, 5)) {
|
|
663
|
-
consola.log(` + ${keyPath}: ${JSON.stringify(value)}`);
|
|
664
|
-
}
|
|
665
|
-
if (missing.length > 5) {
|
|
666
|
-
consola.log(` ... and ${missing.length - 5} more`);
|
|
667
|
-
}
|
|
668
|
-
continue;
|
|
669
|
-
}
|
|
670
|
-
const { content } = readLocaleFile(config, locale);
|
|
671
|
-
if (config.fileExtension === ".json") {
|
|
672
|
-
const json = JSON.parse(content);
|
|
673
|
-
if (args.translate) {
|
|
674
|
-
const toTranslate = {};
|
|
675
|
-
for (const { path: keyPath, value } of missing) {
|
|
676
|
-
if (typeof value === "string") {
|
|
677
|
-
toTranslate[keyPath] = value;
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
if (Object.keys(toTranslate).length > 0) {
|
|
681
|
-
consola.info(` Translating ${Object.keys(toTranslate).length} texts...`);
|
|
682
|
-
const startTime = Date.now();
|
|
683
|
-
try {
|
|
684
|
-
const translated = await translateJson(toTranslate, locale, baseLocale);
|
|
685
|
-
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
686
|
-
consola.success(` Translated in ${elapsed}s`);
|
|
687
|
-
for (const { path: keyPath, value } of missing) {
|
|
688
|
-
if (typeof value === "string" && translated[keyPath]) {
|
|
689
|
-
setNestedValue2(json, keyPath.split("."), translated[keyPath]);
|
|
690
|
-
totalTranslated++;
|
|
691
|
-
} else {
|
|
692
|
-
const fallback = typeof value === "string" ? `[TODO] ${value}` : JSON.stringify(value);
|
|
693
|
-
setNestedValue2(json, keyPath.split("."), fallback);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
} catch (error) {
|
|
697
|
-
consola.error(` Translation failed: ${error instanceof Error ? error.message : error}`);
|
|
698
|
-
for (const { path: keyPath, value } of missing) {
|
|
699
|
-
const fallback = typeof value === "string" ? `[TODO] ${value}` : JSON.stringify(value);
|
|
700
|
-
setNestedValue2(json, keyPath.split("."), fallback);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
for (const { path: keyPath, value } of missing) {
|
|
706
|
-
const newValue = typeof value === "string" ? `[TODO] ${value}` : JSON.stringify(value);
|
|
707
|
-
setNestedValue2(json, keyPath.split("."), newValue);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
writeLocaleFile(config, locale, JSON.stringify(json, null, 2));
|
|
711
|
-
totalAdded += missing.length;
|
|
712
|
-
consola.success(` Added ${missing.length} keys`);
|
|
713
|
-
} else {
|
|
714
|
-
consola.warn(` TypeScript sync not fully implemented. Use JSON or 'add' command.`);
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
consola.log("");
|
|
719
|
-
if (args.dry) {
|
|
720
|
-
consola.info("Dry run complete. Use without --dry to apply changes.");
|
|
721
|
-
} else {
|
|
722
|
-
consola.success(`Sync complete. Added ${totalAdded} keys total.`);
|
|
723
|
-
if (args.translate && totalTranslated > 0) {
|
|
724
|
-
consola.info(`Translated ${totalTranslated} values using LLM (batch mode).`);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (args.stats) {
|
|
728
|
-
const stats = getTranslatorStats();
|
|
729
|
-
if (stats && stats.memorySize > 0) {
|
|
730
|
-
consola.log("");
|
|
731
|
-
consola.info("Translation Cache:");
|
|
732
|
-
consola.log(` Cached: ${stats.memorySize} | Hits: ${stats.hits} | Misses: ${stats.misses}`);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
} catch (error) {
|
|
736
|
-
consola.error(error);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
});
|
|
740
|
-
function setNestedValue2(obj, keys, value) {
|
|
741
|
-
let current = obj;
|
|
742
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
743
|
-
const key = keys[i];
|
|
744
|
-
if (!(key in current) || typeof current[key] !== "object") {
|
|
745
|
-
current[key] = {};
|
|
746
|
-
}
|
|
747
|
-
current = current[key];
|
|
748
|
-
}
|
|
749
|
-
current[keys[keys.length - 1]] = value;
|
|
750
|
-
}
|
|
751
390
|
|
|
752
391
|
// src/cli/index.ts
|
|
753
392
|
var main = defineCommand({
|
|
@@ -759,9 +398,7 @@ var main = defineCommand({
|
|
|
759
398
|
subCommands: {
|
|
760
399
|
list: listCommand,
|
|
761
400
|
check: checkCommand,
|
|
762
|
-
add: addCommand
|
|
763
|
-
translate: translateCommand,
|
|
764
|
-
sync: syncCommand
|
|
401
|
+
add: addCommand
|
|
765
402
|
},
|
|
766
403
|
setup() {
|
|
767
404
|
consola.box("i18n CLI");
|