@shipi18n/cli 1.0.0 โ†’ 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,24 @@
1
1
  # @shipi18n/cli
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@shipi18n/cli)](https://www.npmjs.com/package/@shipi18n/cli)
4
+ [![npm downloads](https://img.shields.io/npm/dw/@shipi18n/cli)](https://www.npmjs.com/package/@shipi18n/cli)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![GitHub last commit](https://img.shields.io/github/last-commit/Shipi18n/shipi18n-cli)](https://github.com/Shipi18n/shipi18n-cli)
7
+ [![CI](https://github.com/Shipi18n/shipi18n-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Shipi18n/shipi18n-cli/actions)
8
+ [![codecov](https://codecov.io/gh/Shipi18n/shipi18n-cli/branch/main/graph/badge.svg)](https://codecov.io/gh/Shipi18n/shipi18n-cli)
9
+
3
10
  Command-line tool for translating locale files with [Shipi18n](https://shipi18n.com).
4
11
 
5
12
  > **๐Ÿš€ Translate JSON files in seconds** - One command, multiple languages!
6
13
 
14
+ ## Why Shipi18n CLI?
15
+
16
+ - **Stop copy-pasting into Google Translate** - One command translates to 100+ languages
17
+ - **Placeholders stay intact** - `{name}`, `{{count}}`, `%s` are preserved automatically
18
+ - **Works with your existing setup** - Drop into any React, Vue, Next.js, i18next project
19
+ - **90-day Translation Memory** - Same content? Cached. No extra cost.
20
+ - **Free tier included** - 100 keys, 3 languages, no credit card required
21
+
7
22
  ## Features
8
23
 
9
24
  - โœ… **Translate JSON files** to 100+ languages with one command
@@ -82,6 +97,8 @@ shipi18n translate <input> [options]
82
97
  - `-o, --output <dir>` - Output directory (default: `./locales`)
83
98
  - `--api-key <key>` - API key (overrides config)
84
99
  - `--preserve-placeholders` - Preserve placeholders (default: `true`)
100
+ - `--no-fallback` - Disable fallback to source for missing translations
101
+ - `--no-regional-fallback` - Disable regional fallback (e.g., pt-BR โ†’ pt)
85
102
 
86
103
  **Examples:**
87
104
 
@@ -97,6 +114,48 @@ shipi18n translate ja.json --source ja --target en,es
97
114
 
98
115
  # Use inline API key
99
116
  shipi18n translate en.json --target es --api-key sk_live_...
117
+
118
+ # Translate with regional variants (pt-BR will fallback to pt if needed)
119
+ shipi18n translate en.json --target es,pt-BR,zh-TW
120
+
121
+ # Disable fallback (strict mode - fail if translation missing)
122
+ shipi18n translate en.json --target es --no-fallback
123
+ ```
124
+
125
+ ### Fallback Behavior
126
+
127
+ By default, the CLI handles missing translations gracefully:
128
+
129
+ | Scenario | Default Behavior |
130
+ |----------|-----------------|
131
+ | Missing translation for a language | Uses source content (English) |
132
+ | Missing regional variant (pt-BR) | Falls back to base language (pt), then source |
133
+ | Missing translation for a key | Fills from source content |
134
+
135
+ **Fallback output example:**
136
+ ```
137
+ โœ“ Translated to 3 languages!
138
+ โœ“ Saved: ./locales/es.json
139
+ โœ“ Saved: ./locales/pt-BR.json
140
+ โœ“ Saved: ./locales/zh-TW.json
141
+
142
+ Fallback information:
143
+ โ€ข pt-BR โ†’ pt (regional fallback)
144
+ โ€ข zh-TW โ†’ en (source fallback)
145
+ โ€ข es: 2 keys used fallback
146
+ - checkout.terms
147
+ - checkout.privacy
148
+
149
+ โœจ Successfully translated 3 files!
150
+ ```
151
+
152
+ **Disable fallback:**
153
+ ```bash
154
+ # Strict mode - no fallback to source
155
+ shipi18n translate en.json --target es --no-fallback
156
+
157
+ # Disable regional fallback only (pt-BR won't fall back to pt)
158
+ shipi18n translate en.json --target pt-BR --no-regional-fallback
100
159
  ```
101
160
 
102
161
  ### Keys Management
@@ -368,18 +427,40 @@ npm link
368
427
  shipi18n --help
369
428
  ```
370
429
 
371
- ## License
430
+ ## Documentation & Resources
372
431
 
373
- MIT
432
+ ๐Ÿ“š **Full Documentation:** [shipi18n.com/integrations/cli](https://shipi18n.com/integrations/cli)
374
433
 
375
- ## Links
434
+ | Resource | Link |
435
+ |----------|------|
436
+ | **Getting Started** | [shipi18n.com](https://shipi18n.com) |
437
+ | **API Reference** | [shipi18n.com/api](https://shipi18n.com/api) |
438
+ | **i18next Best Practices** | [shipi18n.com/integrations/react](https://shipi18n.com/integrations/react) |
439
+ | **Blog & Tutorials** | [shipi18n.com/blog](https://shipi18n.com/blog) |
376
440
 
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)
441
+ ## Related Packages
442
+
443
+ | Package | Description |
444
+ |---------|-------------|
445
+ | [@shipi18n/api](https://www.npmjs.com/package/@shipi18n/api) | Node.js SDK for programmatic use |
446
+ | [vite-plugin-shipi18n](https://www.npmjs.com/package/vite-plugin-shipi18n) | Vite plugin for build-time translation |
447
+ | [i18next-shipi18n-backend](https://www.npmjs.com/package/i18next-shipi18n-backend) | i18next backend for dynamic loading |
448
+ | [shipi18n-github-action](https://github.com/marketplace/actions/shipi18n-auto-translate) | GitHub Action for CI/CD |
449
+
450
+ ## Examples
451
+
452
+ - [Node.js Example](https://github.com/Shipi18n/shipi18n-nodejs-example) - Basic usage examples
453
+ - [Vue Example](https://github.com/Shipi18n/shipi18n-vue-example) - Vue 3 + vue-i18n integration
454
+
455
+ ## License
456
+
457
+ MIT
382
458
 
383
459
  ---
384
460
 
385
- Built with โค๏ธ by [Shipi18n](https://shipi18n.com) - Smart translation API for developers
461
+ <p align="center">
462
+ <a href="https://shipi18n.com">shipi18n.com</a> ยท
463
+ <a href="https://github.com/Shipi18n">GitHub</a> ยท
464
+ <a href="https://shipi18n.com/pricing">Pricing</a>
465
+ </p>
466
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipi18n/cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Command-line tool for translating locale files with Shipi18n",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -19,14 +19,24 @@
19
19
  "dev": "node bin/shipi18n.js",
20
20
  "build": "echo 'No build step needed for now'",
21
21
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
22
- "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch"
22
+ "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch",
23
+ "test:coverage": "NODE_OPTIONS='--experimental-vm-modules' jest --coverage"
23
24
  },
24
25
  "keywords": [
25
- "translation",
26
26
  "i18n",
27
- "internationalization",
27
+ "translation",
28
28
  "localization",
29
+ "internationalization",
30
+ "locale",
31
+ "translate",
29
32
  "cli",
33
+ "i18next",
34
+ "react-intl",
35
+ "json-translation",
36
+ "locale-files",
37
+ "multilingual",
38
+ "l10n",
39
+ "ai-translation",
30
40
  "shipi18n"
31
41
  ],
32
42
  "author": "Shipi18n",
@@ -14,6 +14,8 @@ export function translateCommand(program) {
14
14
  .option('-o, --output <dir>', 'Output directory', './locales')
15
15
  .option('--api-key <key>', 'API key (overrides config)')
16
16
  .option('--preserve-placeholders', 'Preserve placeholders like {name}, {{value}}, etc.', true)
17
+ .option('--no-fallback', 'Disable fallback to source language for missing translations')
18
+ .option('--no-regional-fallback', 'Disable regional fallback (e.g., pt-BR -> pt)')
17
19
  .action(async (input, options) => {
18
20
  const spinner = logger.spinner('Translating...');
19
21
 
@@ -54,13 +56,17 @@ export function translateCommand(program) {
54
56
 
55
57
  spinner.text = `Translating to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}...`;
56
58
 
57
- // Translate
59
+ // Translate with fallback support
58
60
  const api = new Shipi18nAPI(apiKey);
59
61
  const translations = await api.translateJSON({
60
62
  json,
61
63
  sourceLanguage,
62
64
  targetLanguages,
63
65
  preservePlaceholders: options.preservePlaceholders,
66
+ fallback: {
67
+ fallbackToSource: options.fallback !== false,
68
+ regionalFallback: options.regionalFallback !== false,
69
+ },
64
70
  });
65
71
 
66
72
  spinner.succeed(chalk.green(`Translated to ${targetLanguages.length} language${targetLanguages.length > 1 ? 's' : ''}!`));
@@ -73,7 +79,7 @@ export function translateCommand(program) {
73
79
 
74
80
  let savedCount = 0;
75
81
  for (const [langCode, content] of Object.entries(translations)) {
76
- if (langCode === 'warnings') continue;
82
+ if (langCode === 'warnings' || langCode === 'fallbackInfo' || langCode === 'namespaceInfo') continue;
77
83
 
78
84
  const outputFile = join(outputDir, `${langCode}.json`);
79
85
  writeFileSync(outputFile, JSON.stringify(content, null, 2), 'utf8');
@@ -81,8 +87,42 @@ export function translateCommand(program) {
81
87
  savedCount++;
82
88
  }
83
89
 
90
+ // Show fallback info if any fallbacks were used
91
+ if (translations.fallbackInfo && translations.fallbackInfo.used) {
92
+ const fallbackInfo = translations.fallbackInfo;
93
+ logger.log('');
94
+ logger.info('Fallback information:');
95
+
96
+ // Regional fallbacks
97
+ if (Object.keys(fallbackInfo.regionalFallbacks).length > 0) {
98
+ for (const [lang, baseLang] of Object.entries(fallbackInfo.regionalFallbacks)) {
99
+ logger.log(` ${chalk.blue('โ€ข')} ${lang} โ†’ ${baseLang} ${chalk.gray('(regional fallback)')}`);
100
+ }
101
+ }
102
+
103
+ // Languages that fell back to source
104
+ if (fallbackInfo.languagesFallbackToSource.length > 0) {
105
+ for (const lang of fallbackInfo.languagesFallbackToSource) {
106
+ logger.log(` ${chalk.yellow('โ€ข')} ${lang} โ†’ ${sourceLanguage} ${chalk.gray('(source fallback)')}`);
107
+ }
108
+ }
109
+
110
+ // Keys that used fallback
111
+ if (Object.keys(fallbackInfo.keysFallback).length > 0) {
112
+ for (const [lang, keys] of Object.entries(fallbackInfo.keysFallback)) {
113
+ logger.log(` ${chalk.yellow('โ€ข')} ${lang}: ${keys.length} key${keys.length > 1 ? 's' : ''} used fallback`);
114
+ if (keys.length <= 5) {
115
+ keys.forEach(key => {
116
+ logger.log(` ${chalk.gray('- ' + key)}`);
117
+ });
118
+ }
119
+ }
120
+ }
121
+ }
122
+
84
123
  // Show warnings if any
85
124
  if (translations.warnings && translations.warnings.length > 0) {
125
+ logger.log('');
86
126
  logger.warn('Warnings:');
87
127
  translations.warnings.forEach(warning => {
88
128
  logger.log(` ${chalk.yellow('โ€ข')} ${warning.message}`);
package/src/lib/api.js CHANGED
@@ -14,14 +14,39 @@ export class Shipi18nAPI {
14
14
 
15
15
  /**
16
16
  * Translate JSON file
17
+ * @param {Object} options
18
+ * @param {Object|string} options.json - JSON content to translate
19
+ * @param {string} options.sourceLanguage - Source language code
20
+ * @param {string[]} options.targetLanguages - Target language codes
21
+ * @param {boolean} options.preservePlaceholders - Preserve placeholders
22
+ * @param {Object} options.fallback - Fallback options
23
+ * @param {boolean} options.fallback.fallbackToSource - Use source content when translation missing (default: true)
24
+ * @param {boolean} options.fallback.regionalFallback - Enable pt-BR -> pt fallback (default: true)
25
+ * @param {string} options.fallback.fallbackLanguage - Custom fallback language
17
26
  */
18
- async translateJSON({ json, sourceLanguage = 'en', targetLanguages, preservePlaceholders = true }) {
27
+ async translateJSON({
28
+ json,
29
+ sourceLanguage = 'en',
30
+ targetLanguages,
31
+ preservePlaceholders = true,
32
+ fallback = {}
33
+ }) {
19
34
  if (!this.apiKey) {
20
35
  throw new Error('API key is required. Set SHIPI18N_API_KEY or run: shipi18n config set apiKey YOUR_KEY');
21
36
  }
22
37
 
38
+ const {
39
+ fallbackToSource = true,
40
+ regionalFallback = true,
41
+ fallbackLanguage,
42
+ } = fallback;
43
+
44
+ const sourceContent = typeof json === 'string' ? JSON.parse(json) : json;
23
45
  const jsonString = typeof json === 'string' ? json : JSON.stringify(json);
24
46
 
47
+ // Process regional languages - add base languages for fallback
48
+ const { processedTargets, regionalMap } = this.processRegionalLanguages(targetLanguages, regionalFallback);
49
+
25
50
  const response = await fetch(`${this.baseUrl}/api/translate`, {
26
51
  method: 'POST',
27
52
  headers: {
@@ -32,7 +57,7 @@ export class Shipi18nAPI {
32
57
  inputMethod: 'text',
33
58
  text: jsonString,
34
59
  sourceLanguage,
35
- targetLanguages: JSON.stringify(targetLanguages),
60
+ targetLanguages: JSON.stringify(processedTargets),
36
61
  preservePlaceholders: String(preservePlaceholders),
37
62
  }),
38
63
  });
@@ -50,8 +75,8 @@ export class Shipi18nAPI {
50
75
  // Parse JSON strings back to objects
51
76
  const parsed = {};
52
77
  for (const [lang, jsonStr] of Object.entries(result)) {
53
- if (lang === 'warnings') {
54
- parsed.warnings = jsonStr;
78
+ if (lang === 'warnings' || lang === 'namespaceInfo') {
79
+ parsed[lang] = jsonStr;
55
80
  continue;
56
81
  }
57
82
  try {
@@ -61,7 +86,177 @@ export class Shipi18nAPI {
61
86
  }
62
87
  }
63
88
 
64
- return parsed;
89
+ // Apply fallback logic
90
+ return this.applyFallbacks(
91
+ parsed,
92
+ sourceContent,
93
+ targetLanguages,
94
+ sourceLanguage,
95
+ fallbackToSource,
96
+ regionalFallback,
97
+ fallbackLanguage,
98
+ regionalMap
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Process regional language codes for fallback support
104
+ */
105
+ processRegionalLanguages(targetLanguages, regionalFallback) {
106
+ const regionalMap = {};
107
+ const processedTargets = [];
108
+ const baseLanguagesAdded = new Set();
109
+
110
+ for (const lang of targetLanguages) {
111
+ if (lang.includes('-') && regionalFallback) {
112
+ const baseLang = lang.split('-')[0];
113
+ regionalMap[lang] = baseLang;
114
+
115
+ if (!baseLanguagesAdded.has(baseLang) && !targetLanguages.includes(baseLang)) {
116
+ processedTargets.push(baseLang);
117
+ baseLanguagesAdded.add(baseLang);
118
+ }
119
+ }
120
+
121
+ if (!processedTargets.includes(lang)) {
122
+ processedTargets.push(lang);
123
+ }
124
+ }
125
+
126
+ return { processedTargets, regionalMap };
127
+ }
128
+
129
+ /**
130
+ * Apply fallback logic to translation results
131
+ */
132
+ applyFallbacks(result, sourceContent, targetLanguages, sourceLanguage, fallbackToSource, regionalFallback, fallbackLanguage, regionalMap) {
133
+ const fallbackInfo = {
134
+ used: false,
135
+ languagesFallbackToSource: [],
136
+ regionalFallbacks: {},
137
+ keysFallback: {},
138
+ };
139
+
140
+ for (const lang of targetLanguages) {
141
+ const translation = result[lang];
142
+
143
+ // Case 1: Entire language missing
144
+ if (!translation || Object.keys(translation).length === 0) {
145
+ // Try regional fallback first
146
+ if (regionalFallback && regionalMap[lang]) {
147
+ const baseLang = regionalMap[lang];
148
+ const baseTranslation = result[baseLang];
149
+
150
+ if (baseTranslation && Object.keys(baseTranslation).length > 0) {
151
+ result[lang] = { ...baseTranslation };
152
+ fallbackInfo.used = true;
153
+ fallbackInfo.regionalFallbacks[lang] = baseLang;
154
+ continue;
155
+ }
156
+ }
157
+
158
+ // Fall back to source
159
+ if (fallbackToSource) {
160
+ result[lang] = { ...sourceContent };
161
+ fallbackInfo.used = true;
162
+ fallbackInfo.languagesFallbackToSource.push(lang);
163
+ }
164
+ continue;
165
+ }
166
+
167
+ // Case 2: Check for missing keys
168
+ if (fallbackToSource && typeof translation === 'object') {
169
+ const missingKeys = this.findMissingKeys(sourceContent, translation);
170
+
171
+ if (missingKeys.length > 0) {
172
+ fallbackInfo.used = true;
173
+ fallbackInfo.keysFallback[lang] = missingKeys;
174
+
175
+ for (const key of missingKeys) {
176
+ const fallbackValue = this.getNestedValue(sourceContent, key);
177
+
178
+ // Try regional fallback first
179
+ if (regionalFallback && regionalMap[lang]) {
180
+ const baseLang = regionalMap[lang];
181
+ const baseTranslation = result[baseLang];
182
+ const baseValue = baseTranslation ? this.getNestedValue(baseTranslation, key) : undefined;
183
+
184
+ if (baseValue !== undefined) {
185
+ this.setNestedValue(translation, key, baseValue);
186
+ continue;
187
+ }
188
+ }
189
+
190
+ if (fallbackValue !== undefined) {
191
+ this.setNestedValue(translation, key, fallbackValue);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ if (fallbackInfo.used) {
199
+ result.fallbackInfo = fallbackInfo;
200
+ }
201
+
202
+ return result;
203
+ }
204
+
205
+ /**
206
+ * Find missing keys in translation
207
+ */
208
+ findMissingKeys(source, translation, prefix = '') {
209
+ const missing = [];
210
+
211
+ for (const key of Object.keys(source)) {
212
+ const fullKey = prefix ? `${prefix}.${key}` : key;
213
+ const sourceValue = source[key];
214
+ const translationValue = translation[key];
215
+
216
+ if (translationValue === undefined || translationValue === null || translationValue === '') {
217
+ missing.push(fullKey);
218
+ } else if (
219
+ typeof sourceValue === 'object' &&
220
+ sourceValue !== null &&
221
+ !Array.isArray(sourceValue) &&
222
+ typeof translationValue === 'object' &&
223
+ translationValue !== null
224
+ ) {
225
+ missing.push(...this.findMissingKeys(sourceValue, translationValue, fullKey));
226
+ }
227
+ }
228
+
229
+ return missing;
230
+ }
231
+
232
+ /**
233
+ * Get nested value from object using dot notation
234
+ */
235
+ getNestedValue(obj, path) {
236
+ return path.split('.').reduce((current, key) => {
237
+ if (current && typeof current === 'object' && key in current) {
238
+ return current[key];
239
+ }
240
+ return undefined;
241
+ }, obj);
242
+ }
243
+
244
+ /**
245
+ * Set nested value in object using dot notation
246
+ */
247
+ setNestedValue(obj, path, value) {
248
+ const keys = path.split('.');
249
+ let current = obj;
250
+
251
+ for (let i = 0; i < keys.length - 1; i++) {
252
+ const key = keys[i];
253
+ if (!(key in current) || typeof current[key] !== 'object') {
254
+ current[key] = {};
255
+ }
256
+ current = current[key];
257
+ }
258
+
259
+ current[keys[keys.length - 1]] = value;
65
260
  }
66
261
 
67
262
  /**