@shipi18n/cli 1.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/README.md ADDED
@@ -0,0 +1,385 @@
1
+ # @shipi18n/cli
2
+
3
+ Command-line tool for translating locale files with [Shipi18n](https://shipi18n.com).
4
+
5
+ > **🚀 Translate JSON files in seconds** - One command, multiple languages!
6
+
7
+ ## Features
8
+
9
+ - ✅ **Translate JSON files** to 100+ languages with one command
10
+ - ✅ **Preserve JSON structure** - Nested objects, arrays, everything
11
+ - ✅ **Placeholder preservation** - Keep `{name}`, `{{value}}`, `%s`, etc. intact
12
+ - ✅ **Key-based pricing** - 100 free translation keys (unlimited characters!)
13
+ - ✅ **Language limits enforced** - FREE: 3 languages, STARTER: 10, PRO: unlimited
14
+ - ✅ **Config file support** - Save settings in `~/.shipi18n/config.yml`
15
+ - ✅ **Translation Memory** - Manage keys with `shipi18n keys` commands
16
+ - ✅ **Beautiful output** - Colored, formatted terminal output
17
+
18
+ ## Quick Start
19
+
20
+ ### 1. Install
21
+
22
+ ```bash
23
+ npm install -g @shipi18n/cli
24
+ ```
25
+
26
+ ### 2. Get Your Free API Key
27
+
28
+ Sign up at [shipi18n.com](https://shipi18n.com) - it takes 30 seconds!
29
+
30
+ **Free tier includes:**
31
+ - 100 translation keys
32
+ - 3 languages
33
+ - 10 requests/minute
34
+ - Unlimited characters
35
+
36
+ ### 3. Configure
37
+
38
+ ```bash
39
+ shipi18n config set apiKey YOUR_API_KEY
40
+ ```
41
+
42
+ ### 4. Translate!
43
+
44
+ ```bash
45
+ shipi18n translate en.json --target es,fr,de
46
+ ```
47
+
48
+ Done! You now have `es.json`, `fr.json`, and `de.json` in your `./locales` folder.
49
+
50
+ ## Installation
51
+
52
+ ### Global (recommended)
53
+
54
+ ```bash
55
+ npm install -g @shipi18n/cli
56
+ ```
57
+
58
+ ### Local project
59
+
60
+ ```bash
61
+ npm install --save-dev @shipi18n/cli
62
+ ```
63
+
64
+ Then use via npx:
65
+ ```bash
66
+ npx shipi18n translate en.json --target es,fr
67
+ ```
68
+
69
+ ## Usage
70
+
71
+ ### Translate Command
72
+
73
+ Translate a JSON locale file to multiple languages:
74
+
75
+ ```bash
76
+ shipi18n translate <input> [options]
77
+ ```
78
+
79
+ **Options:**
80
+ - `-t, --target <languages>` - Target languages (comma-separated, default: `es,fr`)
81
+ - `-s, --source <language>` - Source language (default: `en`)
82
+ - `-o, --output <dir>` - Output directory (default: `./locales`)
83
+ - `--api-key <key>` - API key (overrides config)
84
+ - `--preserve-placeholders` - Preserve placeholders (default: `true`)
85
+
86
+ **Examples:**
87
+
88
+ ```bash
89
+ # Basic usage
90
+ shipi18n translate en.json --target es,fr
91
+
92
+ # Custom output directory
93
+ shipi18n translate en.json --target es,fr,de --output ./translations
94
+
95
+ # Specify source language
96
+ shipi18n translate ja.json --source ja --target en,es
97
+
98
+ # Use inline API key
99
+ shipi18n translate en.json --target es --api-key sk_live_...
100
+ ```
101
+
102
+ ### Keys Management
103
+
104
+ Manage your translation keys in Translation Memory:
105
+
106
+ ```bash
107
+ # List all saved keys
108
+ shipi18n keys list
109
+
110
+ # Export keys to JSON
111
+ shipi18n keys export --format json --output keys.json
112
+
113
+ # Delete a specific key
114
+ shipi18n keys delete <keyId>
115
+ ```
116
+
117
+ ### Configuration
118
+
119
+ Manage CLI settings:
120
+
121
+ ```bash
122
+ # Show current configuration
123
+ shipi18n config get
124
+
125
+ # Set API key
126
+ shipi18n config set apiKey YOUR_KEY
127
+
128
+ # Set default target languages
129
+ shipi18n config set targetLanguages es,fr,de
130
+
131
+ # Initialize config file with defaults
132
+ shipi18n config init
133
+ ```
134
+
135
+ ### Help
136
+
137
+ ```bash
138
+ # General help
139
+ shipi18n --help
140
+
141
+ # Command-specific help
142
+ shipi18n translate --help
143
+ shipi18n keys --help
144
+ shipi18n config --help
145
+ ```
146
+
147
+ ## Configuration File
148
+
149
+ The CLI stores settings in `~/.shipi18n/config.yml`:
150
+
151
+ ```yaml
152
+ apiKey: sk_live_your_api_key_here
153
+ sourceLanguage: en
154
+ targetLanguages:
155
+ - es
156
+ - fr
157
+ - de
158
+ outputDir: ./locales
159
+ saveKeys: true
160
+ ```
161
+
162
+ **Priority:** Environment variables > Config file > Command-line options
163
+
164
+ ## Environment Variables
165
+
166
+ You can also configure via environment variables:
167
+
168
+ ```bash
169
+ export SHIPI18N_API_KEY=sk_live_your_api_key_here
170
+ export SHIPI18N_SOURCE_LANG=en
171
+ export SHIPI18N_TARGET_LANGS=es,fr,de
172
+ export SHIPI18N_OUTPUT_DIR=./locales
173
+ ```
174
+
175
+ ## Supported Languages
176
+
177
+ Shipi18n supports **100+ languages** including:
178
+
179
+ **Popular:**
180
+ - 🇪🇸 Spanish (es)
181
+ - 🇫🇷 French (fr)
182
+ - 🇩🇪 German (de)
183
+ - 🇯🇵 Japanese (ja)
184
+ - 🇨🇳 Chinese Simplified (zh)
185
+ - 🇨🇳 Chinese Traditional (zh-TW)
186
+ - 🇵🇹 Portuguese (pt)
187
+ - 🇷🇺 Russian (ru)
188
+ - 🇰🇷 Korean (ko)
189
+ - 🇮🇹 Italian (it)
190
+
191
+ [See full list of 100+ supported languages](https://shipi18n.com/docs/languages)
192
+
193
+ ## Pricing
194
+
195
+ | Tier | Price | Keys | Languages | Rate Limit |
196
+ |------|-------|------|-----------|------------|
197
+ | **FREE** | $0/mo | 100 | 3 | 10 req/min |
198
+ | **STARTER** | $9/mo | 500 | 10 | 60 req/min |
199
+ | **PRO** | $29/mo | 10K | 100+ | 300 req/min |
200
+ | **ENTERPRISE** | Custom | Unlimited | Custom | 1000+ req/min |
201
+
202
+ **What's a "key"?** Each unique translation path (e.g., `app.welcome`) counts as one key. Translating to multiple languages doesn't multiply the count!
203
+
204
+ ## Examples
205
+
206
+ ### Real-World Workflow
207
+
208
+ ```bash
209
+ # Your project structure
210
+ my-app/
211
+ ├── locales/
212
+ │ └── en.json # ✅ You have this
213
+ └── src/
214
+
215
+ # Translate to multiple languages
216
+ $ shipi18n translate locales/en.json --target es,fr,de,ja
217
+
218
+ # Result
219
+ my-app/
220
+ ├── locales/
221
+ │ ├── en.json # ✅ Original
222
+ │ ├── es.json # ✅ Spanish
223
+ │ ├── fr.json # ✅ French
224
+ │ ├── de.json # ✅ German
225
+ │ └── ja.json # ✅ Japanese
226
+ └── src/
227
+ ```
228
+
229
+ ### Input File (`en.json`)
230
+
231
+ ```json
232
+ {
233
+ "app": {
234
+ "title": "My Application",
235
+ "welcome": "Welcome, {username}!",
236
+ "description": "This is a demo"
237
+ },
238
+ "auth": {
239
+ "login": "Log In",
240
+ "logout": "Log Out"
241
+ }
242
+ }
243
+ ```
244
+
245
+ ### Output (`es.json`)
246
+
247
+ ```json
248
+ {
249
+ "app": {
250
+ "title": "Mi Aplicación",
251
+ "welcome": "¡Bienvenido, {username}!",
252
+ "description": "Esta es una demostración"
253
+ },
254
+ "auth": {
255
+ "login": "Iniciar Sesión",
256
+ "logout": "Cerrar Sesión"
257
+ }
258
+ }
259
+ ```
260
+
261
+ Notice how:
262
+ - ✅ JSON structure is preserved
263
+ - ✅ Placeholders like `{username}` are kept intact
264
+ - ✅ Only values are translated, keys stay in English
265
+
266
+ ## CI/CD Integration
267
+
268
+ ### GitHub Actions
269
+
270
+ ```yaml
271
+ name: Translate Locales
272
+ on: [push]
273
+
274
+ jobs:
275
+ translate:
276
+ runs-on: ubuntu-latest
277
+ steps:
278
+ - uses: actions/checkout@v3
279
+
280
+ - name: Setup Node.js
281
+ uses: actions/setup-node@v3
282
+ with:
283
+ node-version: '18'
284
+
285
+ - name: Install Shipi18n CLI
286
+ run: npm install -g @shipi18n/cli
287
+
288
+ - name: Translate
289
+ env:
290
+ SHIPI18N_API_KEY: ${{ secrets.SHIPI18N_API_KEY }}
291
+ run: shipi18n translate locales/en.json --target es,fr,de
292
+
293
+ - name: Commit translations
294
+ run: |
295
+ git config user.name "github-actions"
296
+ git config user.email "github-actions@github.com"
297
+ git add locales/
298
+ git commit -m "Update translations" || echo "No changes"
299
+ git push
300
+ ```
301
+
302
+ ### NPM Scripts
303
+
304
+ Add to your `package.json`:
305
+
306
+ ```json
307
+ {
308
+ "scripts": {
309
+ "translate": "shipi18n translate locales/en.json --target es,fr,de",
310
+ "translate:dev": "shipi18n translate locales/en.json --target es",
311
+ "translate:all": "shipi18n translate locales/en.json --target es,fr,de,ja,zh,pt,ru,ko"
312
+ }
313
+ }
314
+ ```
315
+
316
+ Then run:
317
+ ```bash
318
+ npm run translate
319
+ ```
320
+
321
+ ## Troubleshooting
322
+
323
+ ### "API key not found"
324
+
325
+ ```bash
326
+ # Set your API key
327
+ shipi18n config set apiKey YOUR_KEY
328
+
329
+ # Or use environment variable
330
+ export SHIPI18N_API_KEY=YOUR_KEY
331
+
332
+ # Get your key at https://shipi18n.com
333
+ ```
334
+
335
+ ### "Language limit exceeded"
336
+
337
+ The FREE tier allows 3 languages. Upgrade your plan:
338
+ - **STARTER** ($9/mo) - 10 languages
339
+ - **PRO** ($29/mo) - 100+ languages
340
+
341
+ ### "Rate limit exceeded"
342
+
343
+ Wait a minute or upgrade your plan for higher rate limits.
344
+
345
+ ### "Invalid JSON"
346
+
347
+ Make sure your input file is valid JSON:
348
+ ```bash
349
+ # Validate JSON
350
+ cat en.json | jq .
351
+ ```
352
+
353
+ ## Development
354
+
355
+ ```bash
356
+ # Clone the repo
357
+ git clone https://github.com/Shipi18n/shipi18n-cli.git
358
+ cd shipi18n-cli
359
+
360
+ # Install dependencies
361
+ npm install
362
+
363
+ # Test locally
364
+ node bin/shipi18n.js translate test.json --target es,fr
365
+
366
+ # Link globally for testing
367
+ npm link
368
+ shipi18n --help
369
+ ```
370
+
371
+ ## License
372
+
373
+ MIT
374
+
375
+ ## Links
376
+
377
+ - [Shipi18n Website](https://shipi18n.com)
378
+ - [Documentation](https://shipi18n.com/docs)
379
+ - [API Reference](https://shipi18n.com/docs/api)
380
+ - [GitHub](https://github.com/Shipi18n/shipi18n-cli)
381
+ - [Support](https://github.com/Shipi18n/shipi18n-cli/issues)
382
+
383
+ ---
384
+
385
+ Built with ❤️ by [Shipi18n](https://shipi18n.com) - Smart translation API for developers
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
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 { readFileSync } from 'fs';
9
+ import { dirname, join } from 'path';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ // Read package.json for version
16
+ const packageJson = JSON.parse(
17
+ readFileSync(join(__dirname, '../package.json'), 'utf8')
18
+ );
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('shipi18n')
24
+ .description('🌍 Translate your locale files with Shipi18n')
25
+ .version(packageJson.version, '-v, --version', 'Output the current version')
26
+ .addHelpText('after', `
27
+ ${chalk.cyan('Examples:')}
28
+ $ shipi18n translate en.json --target es,fr,de
29
+ $ shipi18n keys list
30
+ $ shipi18n config set apiKey sk_live_...
31
+
32
+ ${chalk.cyan('Get started:')}
33
+ 1. Sign up at ${chalk.underline('https://shipi18n.com')}
34
+ 2. Get your API key (free tier: 100 keys, 3 languages)
35
+ 3. Run: ${chalk.yellow('shipi18n config set apiKey YOUR_KEY')}
36
+ 4. Translate: ${chalk.yellow('shipi18n translate en.json --target es,fr')}
37
+
38
+ ${chalk.gray('Documentation: https://shipi18n.com/docs/cli')}
39
+ `);
40
+
41
+ // Add commands
42
+ translateCommand(program);
43
+ keysCommand(program);
44
+ configCommand(program);
45
+
46
+ // Parse arguments
47
+ program.parse(process.argv);
48
+
49
+ // Show help if no command provided
50
+ if (!process.argv.slice(2).length) {
51
+ program.outputHelp();
52
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@shipi18n/cli",
3
+ "version": "1.0.0",
4
+ "description": "Command-line tool for translating locale files with Shipi18n",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "bin",
9
+ "src/commands",
10
+ "src/lib",
11
+ "src/utils",
12
+ "src/index.js",
13
+ "README.md"
14
+ ],
15
+ "bin": {
16
+ "shipi18n": "./bin/shipi18n.js"
17
+ },
18
+ "scripts": {
19
+ "dev": "node bin/shipi18n.js",
20
+ "build": "echo 'No build step needed for now'",
21
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
22
+ "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch"
23
+ },
24
+ "keywords": [
25
+ "translation",
26
+ "i18n",
27
+ "internationalization",
28
+ "localization",
29
+ "cli",
30
+ "shipi18n"
31
+ ],
32
+ "author": "Shipi18n",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "commander": "^11.1.0",
36
+ "chalk": "^5.3.0",
37
+ "dotenv": "^16.3.1",
38
+ "yaml": "^2.3.4",
39
+ "ora": "^7.0.1",
40
+ "inquirer": "^9.2.12"
41
+ },
42
+ "devDependencies": {
43
+ "jest": "^29.7.0"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/Shipi18n/shipi18n-cli.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/Shipi18n/shipi18n-cli/issues"
54
+ },
55
+ "homepage": "https://shipi18n.com"
56
+ }
@@ -0,0 +1,94 @@
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
+ }
@@ -0,0 +1,128 @@
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
+ }
@@ -0,0 +1,103 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join, parse, dirname } from 'path';
3
+ import chalk from 'chalk';
4
+ import { Shipi18nAPI } from '../lib/api.js';
5
+ import { getConfig } from '../lib/config.js';
6
+ import { logger, formatError } from '../utils/logger.js';
7
+
8
+ export function translateCommand(program) {
9
+ program
10
+ .command('translate <input>')
11
+ .description('Translate a JSON locale file to multiple languages')
12
+ .option('-t, --target <languages>', 'Target languages (comma-separated)', 'es,fr')
13
+ .option('-s, --source <language>', 'Source language', 'en')
14
+ .option('-o, --output <dir>', 'Output directory', './locales')
15
+ .option('--api-key <key>', 'API key (overrides config)')
16
+ .option('--preserve-placeholders', 'Preserve placeholders like {name}, {{value}}, etc.', true)
17
+ .action(async (input, options) => {
18
+ const spinner = logger.spinner('Translating...');
19
+
20
+ try {
21
+ // Get config
22
+ const config = getConfig();
23
+ const apiKey = options.apiKey || config.apiKey;
24
+
25
+ if (!apiKey) {
26
+ spinner.fail();
27
+ logger.error('API key not found');
28
+ logger.info('Set your API key:');
29
+ logger.log(` ${chalk.yellow('shipi18n config set apiKey YOUR_KEY')}`);
30
+ logger.log(` ${chalk.gray('Get your free key at https://shipi18n.com')}`);
31
+ process.exit(1);
32
+ }
33
+
34
+ // Read input file
35
+ if (!existsSync(input)) {
36
+ spinner.fail();
37
+ logger.error(`Input file not found: ${input}`);
38
+ process.exit(1);
39
+ }
40
+
41
+ const fileContent = readFileSync(input, 'utf8');
42
+ let json;
43
+ try {
44
+ json = JSON.parse(fileContent);
45
+ } catch (error) {
46
+ spinner.fail();
47
+ logger.error(`Invalid JSON in ${input}: ${error.message}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ // Parse target languages
52
+ const targetLanguages = options.target.split(',').map(lang => lang.trim());
53
+ const sourceLanguage = options.source;
54
+
55
+ spinner.text = `Translating to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}...`;
56
+
57
+ // Translate
58
+ const api = new Shipi18nAPI(apiKey);
59
+ const translations = await api.translateJSON({
60
+ json,
61
+ sourceLanguage,
62
+ targetLanguages,
63
+ preservePlaceholders: options.preservePlaceholders,
64
+ });
65
+
66
+ spinner.succeed(chalk.green(`Translated to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}!`));
67
+
68
+ // Save translated files
69
+ const outputDir = options.output;
70
+ if (!existsSync(outputDir)) {
71
+ mkdirSync(outputDir, { recursive: true });
72
+ }
73
+
74
+ let savedCount = 0;
75
+ for (const [langCode, content] of Object.entries(translations)) {
76
+ if (langCode === 'warnings') continue;
77
+
78
+ const outputFile = join(outputDir, `${langCode}.json`);
79
+ writeFileSync(outputFile, JSON.stringify(content, null, 2), 'utf8');
80
+ logger.success(`Saved: ${chalk.cyan(outputFile)}`);
81
+ savedCount++;
82
+ }
83
+
84
+ // Show warnings if any
85
+ if (translations.warnings && translations.warnings.length > 0) {
86
+ logger.warn('Warnings:');
87
+ translations.warnings.forEach(warning => {
88
+ logger.log(` ${chalk.yellow('•')} ${warning.message}`);
89
+ });
90
+ }
91
+
92
+ logger.log('');
93
+ logger.log(chalk.green(`✨ Successfully translated ${savedCount} file${savedCount > 1 ? 's' : ''}!`));
94
+ logger.log(chalk.gray(` Output: ${outputDir}`));
95
+
96
+ } catch (error) {
97
+ spinner.fail();
98
+ logger.log('');
99
+ logger.log(formatError(error));
100
+ process.exit(1);
101
+ }
102
+ });
103
+ }
package/src/lib/api.js ADDED
@@ -0,0 +1,135 @@
1
+ import dotenv from 'dotenv';
2
+ dotenv.config();
3
+
4
+ const API_BASE_URL = process.env.SHIPI18N_API_URL || 'https://x9527l3blg.execute-api.us-east-1.amazonaws.com';
5
+
6
+ /**
7
+ * Shipi18n API Client
8
+ */
9
+ export class Shipi18nAPI {
10
+ constructor(apiKey) {
11
+ this.apiKey = apiKey || process.env.SHIPI18N_API_KEY;
12
+ this.baseUrl = API_BASE_URL;
13
+ }
14
+
15
+ /**
16
+ * Translate JSON file
17
+ */
18
+ async translateJSON({ json, sourceLanguage = 'en', targetLanguages, preservePlaceholders = true }) {
19
+ if (!this.apiKey) {
20
+ throw new Error('API key is required. Set SHIPI18N_API_KEY or run: shipi18n config set apiKey YOUR_KEY');
21
+ }
22
+
23
+ const jsonString = typeof json === 'string' ? json : JSON.stringify(json);
24
+
25
+ const response = await fetch(`${this.baseUrl}/api/translate`, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'Content-Type': 'application/json',
29
+ 'X-API-Key': this.apiKey,
30
+ },
31
+ body: JSON.stringify({
32
+ inputMethod: 'text',
33
+ text: jsonString,
34
+ sourceLanguage,
35
+ targetLanguages: JSON.stringify(targetLanguages),
36
+ preservePlaceholders: String(preservePlaceholders),
37
+ }),
38
+ });
39
+
40
+ if (!response.ok) {
41
+ const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
42
+ const error = new Error(errorData.error?.message || errorData.message || `Translation failed: ${response.statusText}`);
43
+ error.code = errorData.error?.code;
44
+ error.status = response.status;
45
+ throw error;
46
+ }
47
+
48
+ const result = await response.json();
49
+
50
+ // Parse JSON strings back to objects
51
+ const parsed = {};
52
+ for (const [lang, jsonStr] of Object.entries(result)) {
53
+ if (lang === 'warnings') {
54
+ parsed.warnings = jsonStr;
55
+ continue;
56
+ }
57
+ try {
58
+ parsed[lang] = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;
59
+ } catch (e) {
60
+ parsed[lang] = jsonStr;
61
+ }
62
+ }
63
+
64
+ return parsed;
65
+ }
66
+
67
+ /**
68
+ * List translation keys
69
+ */
70
+ async listKeys() {
71
+ if (!this.apiKey) {
72
+ throw new Error('API key is required');
73
+ }
74
+
75
+ const response = await fetch(`${this.baseUrl}/api/keys`, {
76
+ method: 'GET',
77
+ headers: {
78
+ 'X-API-Key': this.apiKey,
79
+ },
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
84
+ throw new Error(errorData.error?.message || errorData.message || 'Failed to list keys');
85
+ }
86
+
87
+ return response.json();
88
+ }
89
+
90
+ /**
91
+ * Delete a translation key
92
+ */
93
+ async deleteKey(keyId) {
94
+ if (!this.apiKey) {
95
+ throw new Error('API key is required');
96
+ }
97
+
98
+ const response = await fetch(`${this.baseUrl}/api/keys/${keyId}`, {
99
+ method: 'DELETE',
100
+ headers: {
101
+ 'X-API-Key': this.apiKey,
102
+ },
103
+ });
104
+
105
+ if (!response.ok) {
106
+ const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
107
+ throw new Error(errorData.error?.message || errorData.message || 'Failed to delete key');
108
+ }
109
+
110
+ return response.json();
111
+ }
112
+
113
+ /**
114
+ * Export translation keys
115
+ */
116
+ async exportKeys(format = 'json') {
117
+ if (!this.apiKey) {
118
+ throw new Error('API key is required');
119
+ }
120
+
121
+ const response = await fetch(`${this.baseUrl}/api/keys/export/${format}`, {
122
+ method: 'GET',
123
+ headers: {
124
+ 'X-API-Key': this.apiKey,
125
+ },
126
+ });
127
+
128
+ if (!response.ok) {
129
+ const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
130
+ throw new Error(errorData.error?.message || errorData.message || 'Failed to export keys');
131
+ }
132
+
133
+ return response.json();
134
+ }
135
+ }
@@ -0,0 +1,74 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import YAML from 'yaml';
5
+
6
+ const CONFIG_DIR = join(homedir(), '.shipi18n');
7
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.yml');
8
+
9
+ /**
10
+ * Get configuration from file or environment variables
11
+ */
12
+ export function getConfig() {
13
+ const config = {
14
+ apiKey: process.env.SHIPI18N_API_KEY,
15
+ sourceLanguage: process.env.SHIPI18N_SOURCE_LANG || 'en',
16
+ targetLanguages: process.env.SHIPI18N_TARGET_LANGS?.split(','),
17
+ outputDir: process.env.SHIPI18N_OUTPUT_DIR || './locales',
18
+ saveKeys: process.env.SHIPI18N_SAVE_KEYS === 'true',
19
+ };
20
+
21
+ // Try to read from config file
22
+ if (existsSync(CONFIG_FILE)) {
23
+ try {
24
+ const fileContent = readFileSync(CONFIG_FILE, 'utf8');
25
+ const fileConfig = YAML.parse(fileContent);
26
+
27
+ // Merge with priority: env vars > config file
28
+ Object.keys(fileConfig).forEach((key) => {
29
+ if (config[key] === undefined || config[key] === null) {
30
+ config[key] = fileConfig[key];
31
+ }
32
+ });
33
+ } catch (error) {
34
+ console.warn(`Warning: Could not read config file: ${error.message}`);
35
+ }
36
+ }
37
+
38
+ return config;
39
+ }
40
+
41
+ /**
42
+ * Save configuration to file
43
+ */
44
+ export function saveConfig(config) {
45
+ try {
46
+ // Create directory if it doesn't exist
47
+ if (!existsSync(CONFIG_DIR)) {
48
+ mkdirSync(CONFIG_DIR, { recursive: true });
49
+ }
50
+
51
+ const yamlContent = YAML.stringify(config);
52
+ writeFileSync(CONFIG_FILE, yamlContent, 'utf8');
53
+ return true;
54
+ } catch (error) {
55
+ throw new Error(`Failed to save config: ${error.message}`);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Get a specific config value
61
+ */
62
+ export function getConfigValue(key) {
63
+ const config = getConfig();
64
+ return config[key];
65
+ }
66
+
67
+ /**
68
+ * Set a specific config value
69
+ */
70
+ export function setConfigValue(key, value) {
71
+ const config = getConfig();
72
+ config[key] = value;
73
+ saveConfig(config);
74
+ }
@@ -0,0 +1,46 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+
4
+ export const logger = {
5
+ success: (message) => {
6
+ console.log(chalk.green('✓'), message);
7
+ },
8
+
9
+ error: (message) => {
10
+ console.log(chalk.red('✗'), message);
11
+ },
12
+
13
+ warn: (message) => {
14
+ console.log(chalk.yellow('⚠'), message);
15
+ },
16
+
17
+ info: (message) => {
18
+ console.log(chalk.blue('ℹ'), message);
19
+ },
20
+
21
+ log: (message) => {
22
+ console.log(message);
23
+ },
24
+
25
+ spinner: (text) => {
26
+ return ora(text).start();
27
+ },
28
+ };
29
+
30
+ export function formatError(error) {
31
+ if (error.code === 'ENOTFOUND') {
32
+ return chalk.red('Network error: Could not connect to Shipi18n API');
33
+ }
34
+
35
+ if (error.message.includes('Language limit exceeded')) {
36
+ return chalk.red(error.message) + '\n' +
37
+ chalk.yellow('💡 Upgrade your plan at https://shipi18n.com to translate to more languages');
38
+ }
39
+
40
+ if (error.message.includes('API key')) {
41
+ return chalk.red(error.message) + '\n' +
42
+ chalk.yellow('💡 Get your free API key at https://shipi18n.com or run: shipi18n config set apiKey YOUR_KEY');
43
+ }
44
+
45
+ return chalk.red(error.message);
46
+ }