@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.
@@ -1,94 +0,0 @@
1
- import chalk from 'chalk';
2
- import { getConfig, setConfigValue, saveConfig } from '../lib/config.js';
3
- import { logger } from '../utils/logger.js';
4
-
5
- export function configCommand(program) {
6
- const config = program.command('config')
7
- .description('Manage CLI configuration');
8
-
9
- // Get config
10
- config
11
- .command('get [key]')
12
- .description('Get configuration value(s)')
13
- .action((key) => {
14
- const currentConfig = getConfig();
15
-
16
- if (key) {
17
- const value = currentConfig[key];
18
- if (value !== undefined && value !== null) {
19
- logger.log(`${chalk.cyan(key)}: ${value}`);
20
- } else {
21
- logger.warn(`Config key "${key}" not found`);
22
- }
23
- } else {
24
- logger.log(chalk.cyan('Current configuration:'));
25
- logger.log('');
26
- Object.entries(currentConfig).forEach(([k, v]) => {
27
- if (v !== undefined && v !== null) {
28
- // Mask API key for security
29
- if (k === 'apiKey' && v) {
30
- logger.log(` ${chalk.yellow(k)}: ${v.substring(0, 12)}...`);
31
- } else {
32
- logger.log(` ${chalk.yellow(k)}: ${v}`);
33
- }
34
- }
35
- });
36
- logger.log('');
37
- logger.log(chalk.gray('Config file: ~/.shipi18n/config.yml'));
38
- }
39
- });
40
-
41
- // Set config
42
- config
43
- .command('set <key> <value>')
44
- .description('Set configuration value')
45
- .action((key, value) => {
46
- try {
47
- // Parse value if it's a boolean or array
48
- let parsedValue = value;
49
- if (value === 'true') parsedValue = true;
50
- if (value === 'false') parsedValue = false;
51
- if (value.includes(',')) parsedValue = value.split(',').map(v => v.trim());
52
-
53
- setConfigValue(key, parsedValue);
54
- logger.success(`Set ${chalk.cyan(key)} = ${parsedValue}`);
55
-
56
- // Show next steps for API key
57
- if (key === 'apiKey') {
58
- logger.log('');
59
- logger.info('API key saved! Try translating a file:');
60
- logger.log(` ${chalk.yellow('shipi18n translate en.json --target es,fr')}`);
61
- }
62
- } catch (error) {
63
- logger.error(`Failed to set config: ${error.message}`);
64
- process.exit(1);
65
- }
66
- });
67
-
68
- // Init config
69
- config
70
- .command('init')
71
- .description('Initialize configuration file with defaults')
72
- .action(() => {
73
- try {
74
- const defaultConfig = {
75
- apiKey: '',
76
- sourceLanguage: 'en',
77
- targetLanguages: ['es', 'fr', 'de'],
78
- outputDir: './locales',
79
- saveKeys: true,
80
- };
81
-
82
- saveConfig(defaultConfig);
83
- logger.success('Created config file: ~/.shipi18n/config.yml');
84
- logger.log('');
85
- logger.info('Next steps:');
86
- logger.log(` 1. Get your API key at ${chalk.cyan('https://shipi18n.com')}`);
87
- logger.log(` 2. Set your API key: ${chalk.yellow('shipi18n config set apiKey YOUR_KEY')}`);
88
- logger.log(` 3. Translate: ${chalk.yellow('shipi18n translate en.json --target es,fr')}`);
89
- } catch (error) {
90
- logger.error(`Failed to initialize config: ${error.message}`);
91
- process.exit(1);
92
- }
93
- });
94
- }
@@ -1,443 +0,0 @@
1
- import chalk from 'chalk';
2
- import { logger } from '../utils/logger.js';
3
- import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
4
- import { join, dirname, basename, extname } from 'path';
5
- import inquirer from 'inquirer';
6
-
7
- // Known i18n frameworks and their signatures
8
- const I18N_FRAMEWORKS = {
9
- 'i18next': {
10
- packages: ['i18next', 'react-i18next', 'next-i18next', 'i18next-http-backend', 'i18next-browser-languagedetector'],
11
- placeholderPattern: 'i18next',
12
- },
13
- 'react-intl': {
14
- packages: ['react-intl', 'formatjs', '@formatjs/intl'],
15
- placeholderPattern: 'icu',
16
- },
17
- 'vue-i18n': {
18
- packages: ['vue-i18n', '@intlify/vue-i18n'],
19
- placeholderPattern: 'i18next',
20
- },
21
- 'next-intl': {
22
- packages: ['next-intl'],
23
- placeholderPattern: 'icu',
24
- },
25
- 'lingui': {
26
- packages: ['@lingui/core', '@lingui/react', '@lingui/macro'],
27
- placeholderPattern: 'icu',
28
- },
29
- 'polyglot': {
30
- packages: ['node-polyglot'],
31
- placeholderPattern: 'printf',
32
- }
33
- };
34
-
35
- // Common locale directory patterns
36
- const LOCALE_DIR_PATTERNS = [
37
- 'locales', 'locale', 'lang', 'langs', 'languages', 'i18n',
38
- 'translations', 'messages', 'public/locales', 'src/locales',
39
- 'src/i18n', 'src/translations', 'app/locales', 'assets/locales'
40
- ];
41
-
42
- // Common language codes
43
- const LANGUAGE_CODES = [
44
- 'en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'ko', 'zh', 'ru', 'ar', 'hi', 'nl', 'pl', 'tr', 'vi', 'th',
45
- 'en-US', 'en-GB', 'es-ES', 'es-MX', 'fr-FR', 'fr-CA', 'de-DE', 'pt-BR', 'pt-PT', 'zh-CN', 'zh-TW'
46
- ];
47
-
48
- /**
49
- * Detect i18n framework from package.json
50
- */
51
- function detectFramework(packageJson) {
52
- const allDeps = {
53
- ...(packageJson.dependencies || {}),
54
- ...(packageJson.devDependencies || {})
55
- };
56
-
57
- const detected = [];
58
-
59
- for (const [framework, config] of Object.entries(I18N_FRAMEWORKS)) {
60
- const matchedPackages = config.packages.filter(pkg => allDeps[pkg]);
61
- if (matchedPackages.length > 0) {
62
- detected.push({
63
- framework,
64
- confidence: matchedPackages.length / config.packages.length,
65
- matchedPackages,
66
- placeholderPattern: config.placeholderPattern
67
- });
68
- }
69
- }
70
-
71
- detected.sort((a, b) => b.confidence - a.confidence);
72
- return { detected, primary: detected[0] || null };
73
- }
74
-
75
- /**
76
- * Recursively get all files in a directory
77
- */
78
- function getAllFiles(dirPath, basePath = '', files = []) {
79
- if (!existsSync(dirPath)) return files;
80
-
81
- const items = readdirSync(dirPath);
82
-
83
- for (const item of items) {
84
- // Skip node_modules, .git, etc.
85
- if (item === 'node_modules' || item === '.git' || item === 'dist' || item === 'build') {
86
- continue;
87
- }
88
-
89
- const fullPath = join(dirPath, item);
90
- const relativePath = basePath ? `${basePath}/${item}` : item;
91
-
92
- if (statSync(fullPath).isDirectory()) {
93
- getAllFiles(fullPath, relativePath, files);
94
- } else {
95
- files.push(relativePath);
96
- }
97
- }
98
-
99
- return files;
100
- }
101
-
102
- /**
103
- * Detect locale file structure
104
- */
105
- function detectFileStructure(files) {
106
- const result = {
107
- localeDirectories: [],
108
- sourceLanguage: null,
109
- targetLanguages: [],
110
- namespaces: [],
111
- fileFormat: 'json',
112
- structure: 'flat'
113
- };
114
-
115
- // Find locale directories
116
- const dirCounts = {};
117
- for (const file of files) {
118
- const parts = file.split('/');
119
- for (let i = 0; i < parts.length; i++) {
120
- const dir = parts.slice(0, i + 1).join('/');
121
- const dirName = parts[i].toLowerCase();
122
- if (LOCALE_DIR_PATTERNS.includes(dirName) || LANGUAGE_CODES.includes(parts[i])) {
123
- dirCounts[dir] = (dirCounts[dir] || 0) + 1;
124
- }
125
- }
126
- }
127
-
128
- const sortedDirs = Object.entries(dirCounts).sort((a, b) => b[1] - a[1]);
129
- if (sortedDirs.length > 0) {
130
- result.localeDirectories = sortedDirs.slice(0, 3).map(([dir]) => dir);
131
- }
132
-
133
- // Find language files
134
- const langFiles = files.filter(f => {
135
- const filename = f.split('/').pop();
136
- const ext = filename.split('.').pop();
137
- const name = filename.replace(`.${ext}`, '');
138
- return (ext === 'json' || ext === 'yaml' || ext === 'yml') &&
139
- (LANGUAGE_CODES.includes(name) || LANGUAGE_CODES.includes(name.toLowerCase()));
140
- });
141
-
142
- const languages = new Set();
143
- const namespaces = new Set();
144
-
145
- for (const file of langFiles) {
146
- const parts = file.split('/');
147
- const filename = parts.pop();
148
- const ext = filename.split('.').pop();
149
- const name = filename.replace(`.${ext}`, '');
150
-
151
- if (LANGUAGE_CODES.includes(name) || LANGUAGE_CODES.includes(name.toLowerCase())) {
152
- languages.add(name.toLowerCase());
153
- result.structure = 'flat';
154
- } else {
155
- const parentDir = parts[parts.length - 1];
156
- if (parentDir && (LANGUAGE_CODES.includes(parentDir) || LANGUAGE_CODES.includes(parentDir.toLowerCase()))) {
157
- languages.add(parentDir.toLowerCase());
158
- namespaces.add(name);
159
- result.structure = 'nested';
160
- }
161
- }
162
-
163
- if (ext === 'yaml' || ext === 'yml') {
164
- result.fileFormat = 'yaml';
165
- }
166
- }
167
-
168
- result.targetLanguages = Array.from(languages).filter(l => l !== 'en');
169
- result.sourceLanguage = languages.has('en') ? 'en' : Array.from(languages)[0] || 'en';
170
- result.namespaces = Array.from(namespaces);
171
-
172
- return result;
173
- }
174
-
175
- /**
176
- * Detect placeholder patterns from translation content
177
- */
178
- function detectPlaceholderPatterns(content) {
179
- const PLACEHOLDER_PATTERNS = {
180
- i18next: { regex: /\{\{[^}]+\}\}/g },
181
- icu: { regex: /\{[a-zA-Z_][a-zA-Z0-9_]*(?:,\s*(?:number|date|time|plural|select|selectordinal))?[^}]*\}/g },
182
- printf: { regex: /%[sd@]|%\d+\$[sd]/g },
183
- ruby: { regex: /%\{[^}]+\}/g }
184
- };
185
-
186
- const flatContent = flattenObject(content);
187
- const values = Object.values(flatContent).filter(v => typeof v === 'string');
188
-
189
- const patternCounts = {};
190
-
191
- for (const [patternName, patternConfig] of Object.entries(PLACEHOLDER_PATTERNS)) {
192
- patternCounts[patternName] = 0;
193
- for (const value of values) {
194
- const matches = value.match(patternConfig.regex);
195
- if (matches) {
196
- patternCounts[patternName] += matches.length;
197
- }
198
- }
199
- }
200
-
201
- const sortedPatterns = Object.entries(patternCounts)
202
- .filter(([_, count]) => count > 0)
203
- .sort((a, b) => b[1] - a[1]);
204
-
205
- return {
206
- patterns: patternCounts,
207
- primary: sortedPatterns[0]?.[0] || 'icu'
208
- };
209
- }
210
-
211
- function flattenObject(obj, prefix = '') {
212
- const result = {};
213
- for (const [key, value] of Object.entries(obj)) {
214
- const newKey = prefix ? `${prefix}.${key}` : key;
215
- if (value && typeof value === 'object' && !Array.isArray(value)) {
216
- Object.assign(result, flattenObject(value, newKey));
217
- } else {
218
- result[newKey] = value;
219
- }
220
- }
221
- return result;
222
- }
223
-
224
- /**
225
- * Generate GitHub Action workflow
226
- */
227
- function generateGitHubActionWorkflow({ sourceDir, targetLanguages, sourceLanguage }) {
228
- return `name: Translate
229
-
230
- on:
231
- push:
232
- branches: [main]
233
- paths:
234
- - '${sourceDir}/**'
235
- workflow_dispatch:
236
-
237
- jobs:
238
- translate:
239
- runs-on: ubuntu-latest
240
- steps:
241
- - uses: actions/checkout@v4
242
-
243
- - name: Translate locale files
244
- uses: shipi18n/shipi18n-github-action@v1
245
- with:
246
- api-key: \${{ secrets.SHIPI18N_API_KEY }}
247
- source-dir: '${sourceDir}'
248
- target-languages: '${targetLanguages.join(',')}'
249
- source-language: '${sourceLanguage}'
250
- incremental: 'true'
251
- verify: 'true'
252
- create-pr: 'true'
253
- `;
254
- }
255
-
256
- export function initCommand(program) {
257
- program
258
- .command('init')
259
- .description('Analyze your project and generate shipi18n configuration')
260
- .option('-y, --yes', 'Skip prompts and use detected defaults')
261
- .option('--no-workflow', 'Skip GitHub Action workflow generation')
262
- .action(async (options) => {
263
- logger.log('');
264
- logger.log(chalk.cyan.bold('🔍 Shipi18n Project Analyzer'));
265
- logger.log(chalk.gray('Detecting your i18n setup...'));
266
- logger.log('');
267
-
268
- const cwd = process.cwd();
269
-
270
- // Step 1: Read package.json
271
- let packageJson = null;
272
- let frameworkResult = { detected: [], primary: null };
273
- const packageJsonPath = join(cwd, 'package.json');
274
-
275
- if (existsSync(packageJsonPath)) {
276
- try {
277
- packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
278
- frameworkResult = detectFramework(packageJson);
279
-
280
- if (frameworkResult.primary) {
281
- logger.success(`Framework: ${chalk.cyan(frameworkResult.primary.framework)}`);
282
- logger.log(chalk.gray(` Detected packages: ${frameworkResult.primary.matchedPackages.join(', ')}`));
283
- } else {
284
- logger.warn('No i18n framework detected in package.json');
285
- }
286
- } catch (e) {
287
- logger.warn('Could not parse package.json');
288
- }
289
- } else {
290
- logger.warn('No package.json found');
291
- }
292
-
293
- // Step 2: Scan file structure
294
- logger.log('');
295
- const files = getAllFiles(cwd);
296
- const fileStructure = detectFileStructure(files);
297
-
298
- if (fileStructure.localeDirectories.length > 0) {
299
- logger.success(`Locale directory: ${chalk.cyan(fileStructure.localeDirectories[0])}`);
300
- logger.log(chalk.gray(` Structure: ${fileStructure.structure}`));
301
- logger.log(chalk.gray(` Format: ${fileStructure.fileFormat}`));
302
- } else {
303
- logger.warn('No locale directory detected');
304
- fileStructure.localeDirectories = ['locales'];
305
- }
306
-
307
- if (fileStructure.sourceLanguage) {
308
- logger.success(`Source language: ${chalk.cyan(fileStructure.sourceLanguage)}`);
309
- }
310
-
311
- if (fileStructure.targetLanguages.length > 0) {
312
- logger.success(`Target languages: ${chalk.cyan(fileStructure.targetLanguages.join(', '))}`);
313
- } else {
314
- logger.info('No target languages detected - using defaults (es, fr, de)');
315
- fileStructure.targetLanguages = ['es', 'fr', 'de'];
316
- }
317
-
318
- if (fileStructure.namespaces.length > 0) {
319
- logger.log(chalk.gray(` Namespaces: ${fileStructure.namespaces.join(', ')}`));
320
- }
321
-
322
- // Step 3: Detect placeholder patterns from sample file
323
- let placeholderResult = { primary: 'icu' };
324
-
325
- if (fileStructure.localeDirectories.length > 0) {
326
- const localeDir = fileStructure.localeDirectories[0];
327
- const sourceFile = files.find(f =>
328
- f.startsWith(localeDir) &&
329
- f.endsWith('.json') &&
330
- (f.includes('/en') || f.includes('/en.') || f.endsWith('en.json'))
331
- );
332
-
333
- if (sourceFile) {
334
- try {
335
- const content = JSON.parse(readFileSync(join(cwd, sourceFile), 'utf8'));
336
- placeholderResult = detectPlaceholderPatterns(content);
337
- logger.success(`Placeholder format: ${chalk.cyan(placeholderResult.primary)}`);
338
- } catch (e) {
339
- // Use framework default if available
340
- if (frameworkResult.primary?.placeholderPattern) {
341
- placeholderResult.primary = frameworkResult.primary.placeholderPattern;
342
- logger.info(`Placeholder format: ${chalk.cyan(placeholderResult.primary)} (from framework)`);
343
- }
344
- }
345
- }
346
- }
347
-
348
- logger.log('');
349
-
350
- // Build configuration
351
- const sourceDir = fileStructure.structure === 'nested'
352
- ? `${fileStructure.localeDirectories[0]}/${fileStructure.sourceLanguage || 'en'}`
353
- : fileStructure.localeDirectories[0];
354
-
355
- const config = {
356
- sourceLanguage: fileStructure.sourceLanguage || 'en',
357
- targetLanguages: fileStructure.targetLanguages.length > 0
358
- ? fileStructure.targetLanguages
359
- : ['es', 'fr', 'de'],
360
- sourceDir,
361
- outputDir: fileStructure.localeDirectories[0] || 'locales',
362
- fileFormat: fileStructure.fileFormat || 'json',
363
- placeholderFormat: placeholderResult.primary,
364
- incremental: true,
365
- verify: true,
366
- selfCorrect: false
367
- };
368
-
369
- // Confirmation or auto-accept
370
- let confirmed = options.yes;
371
-
372
- if (!confirmed) {
373
- logger.log(chalk.cyan.bold('📝 Proposed Configuration:'));
374
- logger.log('');
375
- logger.log(JSON.stringify(config, null, 2));
376
- logger.log('');
377
-
378
- const { proceed } = await inquirer.prompt([{
379
- type: 'confirm',
380
- name: 'proceed',
381
- message: 'Create shipi18n.config.json with these settings?',
382
- default: true
383
- }]);
384
- confirmed = proceed;
385
- }
386
-
387
- if (!confirmed) {
388
- logger.info('Cancelled. Run again with different options or manually create config.');
389
- return;
390
- }
391
-
392
- // Write config file
393
- const configPath = join(cwd, 'shipi18n.config.json');
394
- writeFileSync(configPath, JSON.stringify(config, null, 2));
395
- logger.success(`Created ${chalk.cyan('shipi18n.config.json')}`);
396
-
397
- // GitHub Action workflow
398
- if (options.workflow !== false) {
399
- let createWorkflow = options.yes;
400
-
401
- if (!createWorkflow) {
402
- const { workflow } = await inquirer.prompt([{
403
- type: 'confirm',
404
- name: 'workflow',
405
- message: 'Create GitHub Action workflow for automatic translations?',
406
- default: true
407
- }]);
408
- createWorkflow = workflow;
409
- }
410
-
411
- if (createWorkflow) {
412
- const workflowDir = join(cwd, '.github', 'workflows');
413
- const workflowPath = join(workflowDir, 'translate.yml');
414
-
415
- // Create directories if needed
416
- const { mkdirSync } = await import('fs');
417
- mkdirSync(workflowDir, { recursive: true });
418
-
419
- const workflow = generateGitHubActionWorkflow({
420
- sourceDir: config.sourceDir,
421
- targetLanguages: config.targetLanguages,
422
- sourceLanguage: config.sourceLanguage
423
- });
424
-
425
- writeFileSync(workflowPath, workflow);
426
- logger.success(`Created ${chalk.cyan('.github/workflows/translate.yml')}`);
427
- }
428
- }
429
-
430
- // Next steps
431
- logger.log('');
432
- logger.log(chalk.cyan.bold('🚀 Next Steps:'));
433
- logger.log('');
434
- logger.log(` 1. Get your API key at ${chalk.underline('https://shipi18n.com')}`);
435
- logger.log(` 2. Add secret: ${chalk.yellow('SHIPI18N_API_KEY')} to your GitHub repository`);
436
- logger.log(` 3. Push changes to trigger automatic translations`);
437
- logger.log('');
438
- logger.log(chalk.gray('Or translate manually:'));
439
- logger.log(` ${chalk.yellow(`shipi18n config set apiKey YOUR_KEY`)}`);
440
- logger.log(` ${chalk.yellow(`shipi18n translate ${config.sourceDir} --target ${config.targetLanguages.join(',')}`)}`);
441
- logger.log('');
442
- });
443
- }
@@ -1,128 +0,0 @@
1
- import chalk from 'chalk';
2
- import { Shipi18nAPI } from '../lib/api.js';
3
- import { getConfig } from '../lib/config.js';
4
- import { logger, formatError } from '../utils/logger.js';
5
- import { writeFileSync } from 'fs';
6
-
7
- export function keysCommand(program) {
8
- const keys = program.command('keys')
9
- .description('Manage translation keys');
10
-
11
- // List keys
12
- keys
13
- .command('list')
14
- .description('List all translation keys')
15
- .option('--api-key <key>', 'API key (overrides config)')
16
- .action(async (options) => {
17
- const spinner = logger.spinner('Fetching keys...');
18
-
19
- try {
20
- const config = getConfig();
21
- const apiKey = options.apiKey || config.apiKey;
22
-
23
- if (!apiKey) {
24
- spinner.fail();
25
- logger.error('API key not found. Run: shipi18n config set apiKey YOUR_KEY');
26
- process.exit(1);
27
- }
28
-
29
- const api = new Shipi18nAPI(apiKey);
30
- const result = await api.listKeys();
31
-
32
- spinner.succeed(chalk.green(`Found ${result.keys?.length || 0} keys`));
33
-
34
- if (!result.keys || result.keys.length === 0) {
35
- logger.info('No translation keys found');
36
- logger.log(chalk.gray(' Create keys by translating JSON files with --save-keys flag'));
37
- return;
38
- }
39
-
40
- // Display keys in a table
41
- logger.log('');
42
- result.keys.forEach((key, index) => {
43
- logger.log(chalk.cyan(`${index + 1}. ${key.keyName}`));
44
- logger.log(chalk.gray(` Source: ${key.sourceValue}`));
45
- logger.log(chalk.gray(` Languages: ${Object.keys(key.translations || {}).join(', ')}`));
46
- logger.log('');
47
- });
48
-
49
- logger.log(chalk.gray(`Total: ${result.keys.length} keys | Limit: ${result.limit || 'unlimited'}`));
50
-
51
- } catch (error) {
52
- spinner.fail();
53
- logger.log(formatError(error));
54
- process.exit(1);
55
- }
56
- });
57
-
58
- // Delete key
59
- keys
60
- .command('delete <keyId>')
61
- .description('Delete a translation key')
62
- .option('--api-key <key>', 'API key (overrides config)')
63
- .action(async (keyId, options) => {
64
- const spinner = logger.spinner(`Deleting key ${keyId}...`);
65
-
66
- try {
67
- const config = getConfig();
68
- const apiKey = options.apiKey || config.apiKey;
69
-
70
- if (!apiKey) {
71
- spinner.fail();
72
- logger.error('API key not found. Run: shipi18n config set apiKey YOUR_KEY');
73
- process.exit(1);
74
- }
75
-
76
- const api = new Shipi18nAPI(apiKey);
77
- await api.deleteKey(keyId);
78
-
79
- spinner.succeed(chalk.green(`Deleted key: ${keyId}`));
80
-
81
- } catch (error) {
82
- spinner.fail();
83
- logger.log(formatError(error));
84
- process.exit(1);
85
- }
86
- });
87
-
88
- // Export keys
89
- keys
90
- .command('export')
91
- .description('Export all translation keys')
92
- .option('-f, --format <format>', 'Export format (json, csv)', 'json')
93
- .option('-o, --output <file>', 'Output file')
94
- .option('--api-key <key>', 'API key (overrides config)')
95
- .action(async (options) => {
96
- const spinner = logger.spinner(`Exporting keys as ${options.format}...`);
97
-
98
- try {
99
- const config = getConfig();
100
- const apiKey = options.apiKey || config.apiKey;
101
-
102
- if (!apiKey) {
103
- spinner.fail();
104
- logger.error('API key not found. Run: shipi18n config set apiKey YOUR_KEY');
105
- process.exit(1);
106
- }
107
-
108
- const api = new Shipi18nAPI(apiKey);
109
- const result = await api.exportKeys(options.format);
110
-
111
- if (options.output) {
112
- const content = options.format === 'json'
113
- ? JSON.stringify(result, null, 2)
114
- : result;
115
- writeFileSync(options.output, content, 'utf8');
116
- spinner.succeed(chalk.green(`Exported to: ${options.output}`));
117
- } else {
118
- spinner.succeed(chalk.green('Export complete'));
119
- console.log(JSON.stringify(result, null, 2));
120
- }
121
-
122
- } catch (error) {
123
- spinner.fail();
124
- logger.log(formatError(error));
125
- process.exit(1);
126
- }
127
- });
128
- }