@shipi18n/cli 1.1.5 → 2.0.0

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/bin/shipi18n.js CHANGED
@@ -1,55 +1,35 @@
1
1
  #!/usr/bin/env node
2
+ import { Command } from 'commander'
3
+ import chalk from 'chalk'
4
+ import { readFileSync } from 'node:fs'
5
+ import { dirname, join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { translateCommand } from '../src/commands/translate.js'
2
8
 
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import { translateCommand } from '../src/commands/translate.js';
6
- import { keysCommand } from '../src/commands/keys.js';
7
- import { configCommand } from '../src/commands/config.js';
8
- import { initCommand } from '../src/commands/init.js';
9
- import { readFileSync } from 'fs';
10
- import { dirname, join } from 'path';
11
- import { fileURLToPath } from 'url';
12
-
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = dirname(__filename);
15
-
16
- // Read package.json for version
17
- const packageJson = JSON.parse(
18
- readFileSync(join(__dirname, '../package.json'), 'utf8')
19
- );
20
-
21
- const program = new Command();
9
+ const __dirname = dirname(fileURLToPath(import.meta.url))
10
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
22
11
 
12
+ const program = new Command()
23
13
  program
24
14
  .name('shipi18n')
25
- .description('šŸŒ Translate your locale files with Shipi18n')
26
- .version(packageJson.version, '-v, --version', 'Output the current version')
27
- .addHelpText('after', `
15
+ .description('šŸŒ Open-source i18n translation — bring your own LLM key')
16
+ .version(pkg.version, '-v, --version')
17
+ .addHelpText(
18
+ 'after',
19
+ `
28
20
  ${chalk.cyan('Examples:')}
29
- $ shipi18n init
21
+ $ export ANTHROPIC_API_KEY=sk-ant-...
30
22
  $ shipi18n translate en.json --target es,fr,de
31
- $ shipi18n keys list
32
- $ shipi18n config set apiKey sk_live_...
33
-
34
- ${chalk.cyan('Get started:')}
35
- 1. Run ${chalk.yellow('shipi18n init')} to detect your i18n setup
36
- 2. Sign up at ${chalk.underline('https://shipi18n.com')} and get your API key
37
- 3. Run: ${chalk.yellow('shipi18n config set apiKey YOUR_KEY')}
38
- 4. Translate: ${chalk.yellow('shipi18n translate en.json --target es,fr')}
39
-
40
- ${chalk.gray('Documentation: https://shipi18n.com/docs/cli')}
41
- `);
23
+ $ shipi18n translate en.json -p openai --target ja --incremental
42
24
 
43
- // Add commands
44
- initCommand(program);
45
- translateCommand(program);
46
- keysCommand(program);
47
- configCommand(program);
25
+ ${chalk.cyan('Bring your own LLM:')}
26
+ Set ${chalk.yellow('ANTHROPIC_API_KEY')} (default) or use ${chalk.yellow('-p openai')} with ${chalk.yellow('OPENAI_API_KEY')}.
27
+ No Shipi18n account or hosted API — your keys, your models. Apache-2.0.
48
28
 
49
- // Parse arguments
50
- program.parse(process.argv);
29
+ ${chalk.gray('https://github.com/Shipi18n/shipi18n')}
30
+ `
31
+ )
51
32
 
52
- // Show help if no command provided
53
- if (!process.argv.slice(2).length) {
54
- program.outputHelp();
55
- }
33
+ translateCommand(program)
34
+ program.parse(process.argv)
35
+ if (!process.argv.slice(2).length) program.outputHelp()
package/package.json CHANGED
@@ -1,71 +1,61 @@
1
1
  {
2
2
  "name": "@shipi18n/cli",
3
- "version": "1.1.5",
4
- "description": "Command-line tool for translating locale files with Shipi18n",
5
- "main": "src/index.js",
3
+ "version": "2.0.0",
4
+ "description": "Open-source, bring-your-own-LLM i18n translation CLI. Translate locale files with your own OpenAI/Anthropic key.",
6
5
  "type": "module",
6
+ "bin": {
7
+ "shipi18n": "./bin/shipi18n.js"
8
+ },
7
9
  "files": [
8
10
  "bin",
9
- "src/commands",
10
- "src/lib",
11
- "src/utils",
12
- "src/index.js",
11
+ "src",
12
+ "LICENSE",
13
+ "NOTICE",
13
14
  "README.md",
14
- "NOTICE"
15
+ "CHANGELOG.md"
15
16
  ],
16
- "bin": {
17
- "shipi18n": "./bin/shipi18n.js"
18
- },
19
- "scripts": {
20
- "dev": "node bin/shipi18n.js",
21
- "build": "echo 'No build step needed for now'",
22
- "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
23
- "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
24
- "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage"
17
+ "publishConfig": {
18
+ "access": "public"
25
19
  },
26
20
  "keywords": [
27
21
  "i18n",
22
+ "cli",
28
23
  "translation",
24
+ "llm",
25
+ "openai",
26
+ "anthropic",
29
27
  "localization",
30
- "internationalization",
31
- "locale",
32
- "translate",
33
- "cli",
34
- "i18next",
35
- "react-intl",
36
- "json-translation",
37
- "locale-files",
38
- "multilingual",
39
- "l10n",
40
- "ai-translation",
41
- "shipi18n"
28
+ "bring-your-own-key"
42
29
  ],
43
- "author": "Shipi18n",
44
30
  "license": "Apache-2.0",
31
+ "author": "Shipi18n",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/Shipi18n/shipi18n.git",
35
+ "directory": "packages/cli"
36
+ },
45
37
  "dependencies": {
46
- "archiver": "^7.0.1",
47
38
  "chalk": "^5.3.0",
48
- "commander": "^11.1.0",
49
- "dotenv": "^16.3.1",
50
- "inquirer": "^9.2.12",
51
- "ora": "^7.0.1",
52
- "yaml": "^2.3.4"
39
+ "commander": "^12.0.0",
40
+ "ora": "^8.0.1",
41
+ "@shipi18n/core": "^2.0.0"
53
42
  },
54
- "devDependencies": {
55
- "jest": "^29.7.0"
43
+ "peerDependencies": {
44
+ "@anthropic-ai/sdk": ">=0.30.0",
45
+ "openai": ">=4.0.0"
56
46
  },
57
- "engines": {
58
- "node": ">=18.0.0"
47
+ "peerDependenciesMeta": {
48
+ "@anthropic-ai/sdk": {
49
+ "optional": true
50
+ },
51
+ "openai": {
52
+ "optional": true
53
+ }
59
54
  },
60
- "repository": {
61
- "type": "git",
62
- "url": "https://github.com/Shipi18n/shipi18n-cli.git"
63
- },
64
- "bugs": {
65
- "url": "https://github.com/Shipi18n/shipi18n-cli/issues"
55
+ "devDependencies": {
56
+ "jest": "^29.7.0"
66
57
  },
67
- "homepage": "https://shipi18n.com",
68
- "publishConfig": {
69
- "access": "public"
58
+ "scripts": {
59
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests"
70
60
  }
71
- }
61
+ }
@@ -1,331 +1,88 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream } from 'fs';
2
- import { join, parse, dirname } from 'path';
3
- import chalk from 'chalk';
4
- import archiver from 'archiver';
5
- import { Shipi18nAPI } from '../lib/api.js';
6
- import { getConfig } from '../lib/config.js';
7
- import { logger, formatError } from '../utils/logger.js';
8
- import { flattenObject, unflattenObject, deepMerge, findMissingKeys } from '../utils/incremental.js';
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import chalk from 'chalk'
4
+ import ora from 'ora'
5
+ import { translateJSON } from '@shipi18n/core'
6
+
7
+ const PROVIDER_ENV = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY' }
9
8
 
10
9
  export function translateCommand(program) {
11
10
  program
12
11
  .command('translate <input>')
13
- .description('Translate a JSON locale file to multiple languages')
12
+ .description('Translate a JSON locale file to other languages using your own LLM')
14
13
  .option('-t, --target <languages>', 'Target languages (comma-separated)', 'es,fr')
15
14
  .option('-s, --source <language>', 'Source language', 'en')
16
15
  .option('-o, --output <dir>', 'Output directory', './locales')
17
- .option('--api-key <key>', 'API key (overrides config)')
18
- .option('--preserve-placeholders', 'Preserve placeholders like {name}, {{value}}, etc.', true)
19
- .option('--html-handling <mode>', 'How to handle HTML in source text: none, strip, decode, preserve', 'none')
20
- .option('--no-fallback', 'Disable fallback to source language for missing translations')
21
- .option('--no-regional-fallback', 'Disable regional fallback (e.g., pt-BR -> pt)')
22
- .option('-i, --incremental', 'Only translate new/missing keys (skip existing translations)')
23
- .option('--skip-keys <keys>', 'Keys to skip from translation (comma-separated exact paths)')
24
- .option('--skip-paths <patterns>', 'Paths to skip using wildcards (comma-separated, e.g., "states.*,config.*.secret")')
25
- .option('--context-file <path>', 'JSON file with context annotations for disambiguation (e.g., {"close": "button - dismiss"})')
26
- .option('--zip [filename]', 'Output translations as ZIP file (default: translations.zip)')
16
+ .option('-p, --provider <name>', 'LLM provider: anthropic | openai', 'anthropic')
17
+ .option('--api-key <key>', 'LLM API key (else read from provider env var)')
18
+ .option('--model <model>', 'Override the provider default model')
19
+ .option('-i, --incremental', 'Only translate new/missing keys (reuse existing output files)')
27
20
  .action(async (input, options) => {
28
- const spinner = logger.spinner('Translating...');
29
-
21
+ const provider = options.provider
22
+ if (!PROVIDER_ENV[provider]) {
23
+ console.error(chalk.red(`Unknown provider '${provider}'. Use 'anthropic' or 'openai'.`))
24
+ process.exit(1)
25
+ }
26
+ const apiKey = options.apiKey || process.env[PROVIDER_ENV[provider]]
27
+ if (!apiKey) {
28
+ console.error(chalk.red(`No API key. Set ${chalk.yellow(PROVIDER_ENV[provider])} or pass --api-key.`))
29
+ console.error(chalk.gray(`This tool uses YOUR own ${provider} key — no account or Shipi18n key needed.`))
30
+ process.exit(1)
31
+ }
32
+ if (!existsSync(input)) {
33
+ console.error(chalk.red(`Input file not found: ${input}`))
34
+ process.exit(1)
35
+ }
36
+ let content
30
37
  try {
31
- // Get config
32
- const config = getConfig();
33
- const apiKey = options.apiKey || config.apiKey;
34
-
35
- if (!apiKey) {
36
- spinner.fail();
37
- logger.error('API key not found');
38
- logger.info('Set your API key:');
39
- logger.log(` ${chalk.yellow('shipi18n config set apiKey YOUR_KEY')}`);
40
- logger.log(` ${chalk.gray('Get your free key at https://shipi18n.com')}`);
41
- process.exit(1);
42
- }
43
-
44
- // Read input file
45
- if (!existsSync(input)) {
46
- spinner.fail();
47
- logger.error(`Input file not found: ${input}`);
48
- process.exit(1);
49
- }
38
+ content = JSON.parse(readFileSync(input, 'utf8'))
39
+ } catch (e) {
40
+ console.error(chalk.red(`Invalid JSON in ${input}: ${e.message}`))
41
+ process.exit(1)
42
+ }
50
43
 
51
- const fileContent = readFileSync(input, 'utf8');
52
- let json;
44
+ const targets = options.target.split(',').map((s) => s.trim()).filter(Boolean)
45
+ mkdirSync(options.output, { recursive: true })
46
+ console.log(chalk.cyan(`\nšŸŒ Translating ${input} (${options.source} → ${targets.join(', ')}) via ${provider}\n`))
47
+
48
+ let hadWarnings = false
49
+ for (const to of targets) {
50
+ const spinner = ora(`Translating to ${to}...`).start()
51
+ const outPath = join(options.output, `${to}.json`)
52
+ const existing =
53
+ options.incremental && existsSync(outPath)
54
+ ? JSON.parse(readFileSync(outPath, 'utf8'))
55
+ : undefined
53
56
  try {
54
- json = JSON.parse(fileContent);
55
- } catch (error) {
56
- spinner.fail();
57
- logger.error(`Invalid JSON in ${input}: ${error.message}`);
58
- process.exit(1);
59
- }
60
-
61
- // Parse target languages
62
- const targetLanguages = options.target.split(',').map(lang => lang.trim());
63
- const sourceLanguage = options.source;
64
- const outputDir = options.output;
65
- const inputFileName = parse(input).name;
66
-
67
- // Parse skip options
68
- const skipKeys = options.skipKeys
69
- ? options.skipKeys.split(',').map(k => k.trim())
70
- : [];
71
- const skipPaths = options.skipPaths
72
- ? options.skipPaths.split(',').map(p => p.trim())
73
- : [];
74
-
75
- if (skipKeys.length > 0 || skipPaths.length > 0) {
76
- logger.info(`Skipping ${skipKeys.length + skipPaths.length} key/pattern(s) from translation`);
77
- }
78
-
79
- // Parse context annotations file
80
- let contextAnnotations = {};
81
- if (options.contextFile) {
82
- if (!existsSync(options.contextFile)) {
83
- spinner.fail();
84
- logger.error(`Context file not found: ${options.contextFile}`);
85
- process.exit(1);
86
- }
87
- try {
88
- const contextContent = readFileSync(options.contextFile, 'utf8');
89
- contextAnnotations = JSON.parse(contextContent);
90
- const contextCount = Object.keys(contextAnnotations).length;
91
- logger.info(`Loaded ${contextCount} context annotation(s) from ${options.contextFile}`);
92
- } catch (error) {
93
- spinner.fail();
94
- logger.error(`Invalid JSON in context file: ${error.message}`);
95
- process.exit(1);
96
- }
97
- }
98
-
99
- // Incremental mode: load existing translations and find missing keys
100
- let jsonToTranslate = json;
101
- const existingTranslations = {};
102
- let incrementalStats = { total: 0, existing: 0, toTranslate: 0 };
103
-
104
- if (options.incremental) {
105
- spinner.text = 'Checking existing translations...';
106
-
107
- const sourceKeyCount = Object.keys(flattenObject(json)).length;
108
- incrementalStats.total = sourceKeyCount;
109
-
110
- // Load existing translations for each target language
111
- for (const lang of targetLanguages) {
112
- const targetFile = join(outputDir, lang, `${inputFileName}.json`);
113
- const altTargetFile = join(outputDir, `${lang}.json`);
114
-
115
- let existingFile = null;
116
- if (existsSync(targetFile)) {
117
- existingFile = targetFile;
118
- } else if (existsSync(altTargetFile)) {
119
- existingFile = altTargetFile;
120
- }
121
-
122
- if (existingFile) {
123
- try {
124
- const existingContent = readFileSync(existingFile, 'utf8');
125
- existingTranslations[lang] = JSON.parse(existingContent);
126
- } catch (e) {
127
- logger.warn(`Could not parse ${existingFile}, will re-translate`);
128
- }
129
- }
130
- }
131
-
132
- // Find keys that need translation (missing from ANY target language)
133
- const allMissingKeys = {};
134
- for (const lang of targetLanguages) {
135
- const existing = existingTranslations[lang] || {};
136
- const missing = findMissingKeys(json, existing);
137
- const missingFlat = flattenObject(missing);
138
-
139
- for (const [key, value] of Object.entries(missingFlat)) {
140
- if (!(key in allMissingKeys)) {
141
- allMissingKeys[key] = value;
142
- }
143
- }
57
+ const { result, stats } = await translateJSON({
58
+ content,
59
+ from: options.source,
60
+ to,
61
+ provider,
62
+ apiKey,
63
+ model: options.model,
64
+ existing,
65
+ })
66
+ writeFileSync(outPath, JSON.stringify(result, null, 2) + '\n', 'utf8')
67
+ const warn = stats.placeholderWarnings.length
68
+ if (warn) hadWarnings = true
69
+ spinner.succeed(
70
+ `${to} → ${chalk.green(outPath)} ` +
71
+ chalk.gray(`(${stats.translated} translated, ${stats.reused} reused` +
72
+ (warn ? chalk.yellow(`, ${warn} placeholder warnings`) : '') + ')')
73
+ )
74
+ for (const w of stats.placeholderWarnings) {
75
+ console.log(chalk.yellow(` ⚠ ${w.path}: placeholder drift — missing ${JSON.stringify(w.missing)}`))
144
76
  }
145
-
146
- const missingKeyCount = Object.keys(allMissingKeys).length;
147
- incrementalStats.existing = sourceKeyCount - missingKeyCount;
148
- incrementalStats.toTranslate = missingKeyCount;
149
-
150
- if (missingKeyCount === 0) {
151
- spinner.succeed(chalk.green('All translations up to date!'));
152
- logger.log('');
153
- logger.log(chalk.gray(` ${sourceKeyCount} key${sourceKeyCount !== 1 ? 's' : ''} already translated`));
154
- return;
155
- }
156
-
157
- jsonToTranslate = unflattenObject(allMissingKeys);
158
- spinner.text = `Translating ${missingKeyCount} new key${missingKeyCount !== 1 ? 's' : ''} to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}...`;
159
- logger.log('');
160
- logger.info(`Incremental mode: ${chalk.cyan(missingKeyCount)} new key${missingKeyCount !== 1 ? 's' : ''} to translate (${incrementalStats.existing} already exist)`);
161
- } else {
162
- spinner.text = `Translating to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}...`;
163
- }
164
-
165
- // Translate with fallback support
166
- const api = new Shipi18nAPI(apiKey);
167
- const translations = await api.translateJSON({
168
- json: jsonToTranslate,
169
- sourceLanguage,
170
- targetLanguages,
171
- preservePlaceholders: options.preservePlaceholders,
172
- htmlHandling: options.htmlHandling,
173
- fallback: {
174
- fallbackToSource: options.fallback !== false,
175
- regionalFallback: options.regionalFallback !== false,
176
- },
177
- skipKeys,
178
- skipPaths,
179
- contextAnnotations,
180
- });
181
-
182
- const keyCount = Object.keys(flattenObject(jsonToTranslate)).length;
183
- spinner.succeed(chalk.green(`Translated ${keyCount} key${keyCount !== 1 ? 's' : ''} to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}!`));
184
-
185
- // Save translated files
186
- if (!existsSync(outputDir)) {
187
- mkdirSync(outputDir, { recursive: true });
77
+ } catch (e) {
78
+ spinner.fail(`${to}: ${e.message}`)
79
+ process.exitCode = 1
188
80
  }
189
-
190
- let savedCount = 0;
191
-
192
- // Prepare translations for output (filter metadata, apply merging)
193
- const outputTranslations = {};
194
- for (const [langCode, content] of Object.entries(translations)) {
195
- if (langCode === 'warnings' || langCode === 'fallbackInfo' || langCode === 'namespaceInfo' || langCode === 'skipped' || langCode === 'contextEnhanced') continue;
196
-
197
- let finalContent = content;
198
- if (options.incremental && existingTranslations[langCode]) {
199
- finalContent = deepMerge(existingTranslations[langCode], content);
200
- }
201
- outputTranslations[langCode] = finalContent;
202
- }
203
-
204
- if (options.zip) {
205
- // ZIP output mode
206
- const zipFileName = typeof options.zip === 'string' ? options.zip : 'translations.zip';
207
- const zipPath = join(outputDir, zipFileName);
208
-
209
- await new Promise((resolve, reject) => {
210
- const output = createWriteStream(zipPath);
211
- const archive = archiver('zip', { zlib: { level: 9 } });
212
-
213
- output.on('close', resolve);
214
- archive.on('error', reject);
215
-
216
- archive.pipe(output);
217
-
218
- for (const [langCode, content] of Object.entries(outputTranslations)) {
219
- archive.append(JSON.stringify(content, null, 2), { name: `${langCode}.json` });
220
- savedCount++;
221
- }
222
-
223
- archive.finalize();
224
- });
225
-
226
- logger.success(`Saved: ${chalk.cyan(zipPath)} (${savedCount} file${savedCount !== 1 ? 's' : ''})`);
227
- } else {
228
- // Individual files mode
229
- for (const [langCode, content] of Object.entries(outputTranslations)) {
230
- const outputFile = join(outputDir, `${langCode}.json`);
231
- writeFileSync(outputFile, JSON.stringify(content, null, 2), 'utf8');
232
- logger.success(`Saved: ${chalk.cyan(outputFile)}${options.incremental ? chalk.gray(' (merged)') : ''}`);
233
- savedCount++;
234
- }
235
- }
236
-
237
- // Show fallback info if any fallbacks were used
238
- if (translations.fallbackInfo && translations.fallbackInfo.used) {
239
- const fallbackInfo = translations.fallbackInfo;
240
- logger.log('');
241
- logger.info('Fallback information:');
242
-
243
- // Regional fallbacks
244
- if (Object.keys(fallbackInfo.regionalFallbacks).length > 0) {
245
- for (const [lang, baseLang] of Object.entries(fallbackInfo.regionalFallbacks)) {
246
- logger.log(` ${chalk.blue('•')} ${lang} → ${baseLang} ${chalk.gray('(regional fallback)')}`);
247
- }
248
- }
249
-
250
- // Languages that fell back to source
251
- if (fallbackInfo.languagesFallbackToSource.length > 0) {
252
- for (const lang of fallbackInfo.languagesFallbackToSource) {
253
- logger.log(` ${chalk.yellow('•')} ${lang} → ${sourceLanguage} ${chalk.gray('(source fallback)')}`);
254
- }
255
- }
256
-
257
- // Keys that used fallback
258
- if (Object.keys(fallbackInfo.keysFallback).length > 0) {
259
- for (const [lang, keys] of Object.entries(fallbackInfo.keysFallback)) {
260
- logger.log(` ${chalk.yellow('•')} ${lang}: ${keys.length} key${keys.length > 1 ? 's' : ''} used fallback`);
261
- if (keys.length <= 5) {
262
- keys.forEach(key => {
263
- logger.log(` ${chalk.gray('- ' + key)}`);
264
- });
265
- }
266
- }
267
- }
268
- }
269
-
270
- // Show skipped keys info if any
271
- if (translations.skipped && translations.skipped.count > 0) {
272
- logger.log('');
273
- logger.info(`Skipped ${translations.skipped.count} key${translations.skipped.count > 1 ? 's' : ''} from translation:`);
274
- const keysToShow = translations.skipped.keys.slice(0, 10);
275
- keysToShow.forEach(key => {
276
- logger.log(` ${chalk.gray('•')} ${key}`);
277
- });
278
- if (translations.skipped.keys.length > 10) {
279
- logger.log(` ${chalk.gray(`... and ${translations.skipped.keys.length - 10} more`)}`);
280
- }
281
- }
282
-
283
- // Show context-enhanced keys info if any
284
- if (translations.contextEnhanced && translations.contextEnhanced.count > 0) {
285
- logger.log('');
286
- logger.info(`${chalk.cyan('šŸŽÆ')} ${translations.contextEnhanced.count} key${translations.contextEnhanced.count > 1 ? 's' : ''} translated with context annotations:`);
287
- const keysToShow = translations.contextEnhanced.keys.slice(0, 10);
288
- keysToShow.forEach(key => {
289
- logger.log(` ${chalk.cyan('•')} ${key}`);
290
- });
291
- if (translations.contextEnhanced.keys.length > 10) {
292
- logger.log(` ${chalk.gray(`... and ${translations.contextEnhanced.keys.length - 10} more`)}`);
293
- }
294
- }
295
-
296
- // Show legal content warning with key details
297
- const legalWarning = translations.warnings?.find(w => w.type === 'legal_content');
298
- if (legalWarning?.details?.keys?.length > 0) {
299
- logger.log('');
300
- logger.warn(`${chalk.yellow('āš ļø')} Legal content detected - review these keys:`);
301
- legalWarning.details.keys.forEach(key => {
302
- logger.log(` ${chalk.yellow('•')} ${key}`);
303
- });
304
- if (legalWarning.details.count > 10) {
305
- logger.log(` ${chalk.gray(`... and ${legalWarning.details.count - 10} more`)}`);
306
- }
307
- logger.log(` ${chalk.gray('Machine-translated legal text may not be legally binding.')}`);
308
- }
309
-
310
- // Show other warnings if any (exclude legal_content since we showed it above)
311
- const otherWarnings = translations.warnings?.filter(w => w.type !== 'legal_content') || [];
312
- if (otherWarnings.length > 0) {
313
- logger.log('');
314
- logger.warn('Warnings:');
315
- otherWarnings.forEach(warning => {
316
- logger.log(` ${chalk.yellow('•')} ${warning.message}`);
317
- });
318
- }
319
-
320
- logger.log('');
321
- logger.log(chalk.green(`✨ Successfully translated ${savedCount} file${savedCount > 1 ? 's' : ''}!`));
322
- logger.log(chalk.gray(` Output: ${outputDir}`));
323
-
324
- } catch (error) {
325
- spinner.fail();
326
- logger.log('');
327
- logger.log(formatError(error));
328
- process.exit(1);
329
81
  }
330
- });
82
+ console.log(
83
+ hadWarnings
84
+ ? chalk.yellow('\nDone, with placeholder warnings — review the flagged keys.')
85
+ : chalk.green('\nāœ“ Done.')
86
+ )
87
+ })
331
88
  }