@djangocfg/i18n 2.1.216 → 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 CHANGED
@@ -1,14 +1,13 @@
1
1
  # @djangocfg/i18n
2
2
 
3
- Lightweight, type-safe i18n library with LLM-powered translation CLI.
3
+ Lightweight, type-safe i18n library for @djangocfg packages.
4
4
 
5
5
  ## Features
6
6
 
7
- - **17 languages** - en, ru, ko, ja, de, fr, zh, it, es, nl, ar, tr, pt-BR, pl, sv, no, da
8
- - **LLM Translation** - Translate with GPT-4o-mini via CLI
9
- - **Type-safe** - Full TypeScript support with autocomplete
10
- - **CLI included** - Manage and translate locales
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
- # Translate to multiple languages
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
- # Add key to all locales
111
- pnpm i18n add "tools.new" '{"en":"New","ru":"Новый"}'
112
-
113
- # Show translation cache stats
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
- ```bash
122
- # Hub app
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
- ```bash
132
- # Optional - uses built-in test key by default
133
- SDKROUTER_API_KEY=your-key # SDKRouter (recommended)
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
- import { en as baseEn } from '@djangocfg/i18n/locales';
254
- const messages = { ...baseEn, ...appEn };
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
- ### mergeTranslations() (deep merge with overrides)
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()` work without provider - they fall back to English defaults.
156
+ Components using `useT()` fall back to English defaults when used outside a provider.
296
157
 
297
158
  ## License
298
159
 
@@ -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
- return module[exportName] ?? module.default ?? {};
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");