@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 +385 -0
- package/bin/shipi18n.js +52 -0
- package/package.json +56 -0
- package/src/commands/config.js +94 -0
- package/src/commands/keys.js +128 -0
- package/src/commands/translate.js +103 -0
- package/src/lib/api.js +135 -0
- package/src/lib/config.js +74 -0
- package/src/utils/logger.js +46 -0
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
|
package/bin/shipi18n.js
ADDED
|
@@ -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
|
+
}
|