@shipi18n/cli 1.1.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/NOTICE +7 -1
- package/README.md +26 -568
- package/bin/shipi18n.js +25 -45
- package/package.json +40 -50
- package/src/commands/translate.js +73 -316
- package/src/commands/config.js +0 -94
- package/src/commands/init.js +0 -443
- package/src/commands/keys.js +0 -128
- package/src/lib/api.js +0 -342
- package/src/lib/config.js +0 -74
- package/src/utils/incremental.js +0 -90
- package/src/utils/logger.js +0 -46
package/src/lib/api.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import dotenv from 'dotenv';
|
|
2
|
-
dotenv.config();
|
|
3
|
-
|
|
4
|
-
const API_BASE_URL = process.env.SHIPI18N_API_URL || 'https://ydjkwckq3f.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
|
-
* @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 {string} options.htmlHandling - How to handle HTML: none, strip, decode, preserve
|
|
23
|
-
* @param {Object} options.fallback - Fallback options
|
|
24
|
-
* @param {boolean} options.fallback.fallbackToSource - Use source content when translation missing (default: true)
|
|
25
|
-
* @param {boolean} options.fallback.regionalFallback - Enable pt-BR -> pt fallback (default: true)
|
|
26
|
-
* @param {string} options.fallback.fallbackLanguage - Custom fallback language
|
|
27
|
-
* @param {string[]} options.skipKeys - Exact key paths to skip from translation
|
|
28
|
-
* @param {string[]} options.skipPaths - Glob patterns to skip (e.g., "nav.*", "config.*.secret")
|
|
29
|
-
* @param {Object} options.contextAnnotations - Per-key context hints for disambiguation
|
|
30
|
-
*/
|
|
31
|
-
async translateJSON({
|
|
32
|
-
json,
|
|
33
|
-
sourceLanguage = 'en',
|
|
34
|
-
targetLanguages,
|
|
35
|
-
preservePlaceholders = true,
|
|
36
|
-
htmlHandling = 'none',
|
|
37
|
-
fallback = {},
|
|
38
|
-
skipKeys = [],
|
|
39
|
-
skipPaths = [],
|
|
40
|
-
contextAnnotations = {},
|
|
41
|
-
}) {
|
|
42
|
-
if (!this.apiKey) {
|
|
43
|
-
throw new Error('API key is required. Set SHIPI18N_API_KEY or run: shipi18n config set apiKey YOUR_KEY');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const {
|
|
47
|
-
fallbackToSource = true,
|
|
48
|
-
regionalFallback = true,
|
|
49
|
-
fallbackLanguage,
|
|
50
|
-
} = fallback;
|
|
51
|
-
|
|
52
|
-
const sourceContent = typeof json === 'string' ? JSON.parse(json) : json;
|
|
53
|
-
const jsonString = typeof json === 'string' ? json : JSON.stringify(json);
|
|
54
|
-
|
|
55
|
-
// Process regional languages - add base languages for fallback
|
|
56
|
-
const { processedTargets, regionalMap } = this.processRegionalLanguages(targetLanguages, regionalFallback);
|
|
57
|
-
|
|
58
|
-
const response = await fetch(`${this.baseUrl}/api/translate`, {
|
|
59
|
-
method: 'POST',
|
|
60
|
-
headers: {
|
|
61
|
-
'Content-Type': 'application/json',
|
|
62
|
-
'X-API-Key': this.apiKey,
|
|
63
|
-
},
|
|
64
|
-
body: JSON.stringify({
|
|
65
|
-
inputMethod: 'text',
|
|
66
|
-
text: jsonString,
|
|
67
|
-
sourceLanguage,
|
|
68
|
-
targetLanguages: JSON.stringify(processedTargets),
|
|
69
|
-
preservePlaceholders: String(preservePlaceholders),
|
|
70
|
-
htmlHandling,
|
|
71
|
-
skipKeys,
|
|
72
|
-
skipPaths,
|
|
73
|
-
contextAnnotations,
|
|
74
|
-
}),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!response.ok) {
|
|
78
|
-
const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
79
|
-
const error = new Error(errorData.error?.message || errorData.message || `Translation failed: ${response.statusText}`);
|
|
80
|
-
error.code = errorData.error?.code;
|
|
81
|
-
error.status = response.status;
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const result = await response.json();
|
|
86
|
-
|
|
87
|
-
// Parse JSON strings back to objects
|
|
88
|
-
const parsed = {};
|
|
89
|
-
for (const [lang, jsonStr] of Object.entries(result)) {
|
|
90
|
-
if (lang === 'warnings' || lang === 'namespaceInfo' || lang === 'skipped' || lang === 'contextEnhanced') {
|
|
91
|
-
parsed[lang] = jsonStr;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
parsed[lang] = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;
|
|
96
|
-
} catch (e) {
|
|
97
|
-
parsed[lang] = jsonStr;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Apply fallback logic
|
|
102
|
-
return this.applyFallbacks(
|
|
103
|
-
parsed,
|
|
104
|
-
sourceContent,
|
|
105
|
-
targetLanguages,
|
|
106
|
-
sourceLanguage,
|
|
107
|
-
fallbackToSource,
|
|
108
|
-
regionalFallback,
|
|
109
|
-
fallbackLanguage,
|
|
110
|
-
regionalMap
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Process regional language codes for fallback support
|
|
116
|
-
*/
|
|
117
|
-
processRegionalLanguages(targetLanguages, regionalFallback) {
|
|
118
|
-
const regionalMap = {};
|
|
119
|
-
const processedTargets = [];
|
|
120
|
-
const baseLanguagesAdded = new Set();
|
|
121
|
-
|
|
122
|
-
for (const lang of targetLanguages) {
|
|
123
|
-
if (lang.includes('-') && regionalFallback) {
|
|
124
|
-
const baseLang = lang.split('-')[0];
|
|
125
|
-
regionalMap[lang] = baseLang;
|
|
126
|
-
|
|
127
|
-
if (!baseLanguagesAdded.has(baseLang) && !targetLanguages.includes(baseLang)) {
|
|
128
|
-
processedTargets.push(baseLang);
|
|
129
|
-
baseLanguagesAdded.add(baseLang);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!processedTargets.includes(lang)) {
|
|
134
|
-
processedTargets.push(lang);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return { processedTargets, regionalMap };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Apply fallback logic to translation results
|
|
143
|
-
*/
|
|
144
|
-
applyFallbacks(result, sourceContent, targetLanguages, sourceLanguage, fallbackToSource, regionalFallback, fallbackLanguage, regionalMap) {
|
|
145
|
-
const fallbackInfo = {
|
|
146
|
-
used: false,
|
|
147
|
-
languagesFallbackToSource: [],
|
|
148
|
-
regionalFallbacks: {},
|
|
149
|
-
keysFallback: {},
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
for (const lang of targetLanguages) {
|
|
153
|
-
const translation = result[lang];
|
|
154
|
-
|
|
155
|
-
// Case 1: Entire language missing
|
|
156
|
-
if (!translation || Object.keys(translation).length === 0) {
|
|
157
|
-
// Try regional fallback first
|
|
158
|
-
if (regionalFallback && regionalMap[lang]) {
|
|
159
|
-
const baseLang = regionalMap[lang];
|
|
160
|
-
const baseTranslation = result[baseLang];
|
|
161
|
-
|
|
162
|
-
if (baseTranslation && Object.keys(baseTranslation).length > 0) {
|
|
163
|
-
result[lang] = { ...baseTranslation };
|
|
164
|
-
fallbackInfo.used = true;
|
|
165
|
-
fallbackInfo.regionalFallbacks[lang] = baseLang;
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Fall back to source
|
|
171
|
-
if (fallbackToSource) {
|
|
172
|
-
result[lang] = { ...sourceContent };
|
|
173
|
-
fallbackInfo.used = true;
|
|
174
|
-
fallbackInfo.languagesFallbackToSource.push(lang);
|
|
175
|
-
}
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Case 2: Check for missing keys
|
|
180
|
-
if (fallbackToSource && typeof translation === 'object') {
|
|
181
|
-
const missingKeys = this.findMissingKeys(sourceContent, translation);
|
|
182
|
-
|
|
183
|
-
if (missingKeys.length > 0) {
|
|
184
|
-
fallbackInfo.used = true;
|
|
185
|
-
fallbackInfo.keysFallback[lang] = missingKeys;
|
|
186
|
-
|
|
187
|
-
for (const key of missingKeys) {
|
|
188
|
-
const fallbackValue = this.getNestedValue(sourceContent, key);
|
|
189
|
-
|
|
190
|
-
// Try regional fallback first
|
|
191
|
-
if (regionalFallback && regionalMap[lang]) {
|
|
192
|
-
const baseLang = regionalMap[lang];
|
|
193
|
-
const baseTranslation = result[baseLang];
|
|
194
|
-
const baseValue = baseTranslation ? this.getNestedValue(baseTranslation, key) : undefined;
|
|
195
|
-
|
|
196
|
-
if (baseValue !== undefined) {
|
|
197
|
-
this.setNestedValue(translation, key, baseValue);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (fallbackValue !== undefined) {
|
|
203
|
-
this.setNestedValue(translation, key, fallbackValue);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (fallbackInfo.used) {
|
|
211
|
-
result.fallbackInfo = fallbackInfo;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return result;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Find missing keys in translation
|
|
219
|
-
*/
|
|
220
|
-
findMissingKeys(source, translation, prefix = '') {
|
|
221
|
-
const missing = [];
|
|
222
|
-
|
|
223
|
-
for (const key of Object.keys(source)) {
|
|
224
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
225
|
-
const sourceValue = source[key];
|
|
226
|
-
const translationValue = translation[key];
|
|
227
|
-
|
|
228
|
-
if (translationValue === undefined || translationValue === null || translationValue === '') {
|
|
229
|
-
missing.push(fullKey);
|
|
230
|
-
} else if (
|
|
231
|
-
typeof sourceValue === 'object' &&
|
|
232
|
-
sourceValue !== null &&
|
|
233
|
-
!Array.isArray(sourceValue) &&
|
|
234
|
-
typeof translationValue === 'object' &&
|
|
235
|
-
translationValue !== null
|
|
236
|
-
) {
|
|
237
|
-
missing.push(...this.findMissingKeys(sourceValue, translationValue, fullKey));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return missing;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Get nested value from object using dot notation
|
|
246
|
-
*/
|
|
247
|
-
getNestedValue(obj, path) {
|
|
248
|
-
return path.split('.').reduce((current, key) => {
|
|
249
|
-
if (current && typeof current === 'object' && key in current) {
|
|
250
|
-
return current[key];
|
|
251
|
-
}
|
|
252
|
-
return undefined;
|
|
253
|
-
}, obj);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Set nested value in object using dot notation
|
|
258
|
-
*/
|
|
259
|
-
setNestedValue(obj, path, value) {
|
|
260
|
-
const keys = path.split('.');
|
|
261
|
-
let current = obj;
|
|
262
|
-
|
|
263
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
264
|
-
const key = keys[i];
|
|
265
|
-
if (!(key in current) || typeof current[key] !== 'object') {
|
|
266
|
-
current[key] = {};
|
|
267
|
-
}
|
|
268
|
-
current = current[key];
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
current[keys[keys.length - 1]] = value;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* List translation keys
|
|
276
|
-
*/
|
|
277
|
-
async listKeys() {
|
|
278
|
-
if (!this.apiKey) {
|
|
279
|
-
throw new Error('API key is required');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const response = await fetch(`${this.baseUrl}/api/keys`, {
|
|
283
|
-
method: 'GET',
|
|
284
|
-
headers: {
|
|
285
|
-
'X-API-Key': this.apiKey,
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
if (!response.ok) {
|
|
290
|
-
const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
291
|
-
throw new Error(errorData.error?.message || errorData.message || 'Failed to list keys');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return response.json();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Delete a translation key
|
|
299
|
-
*/
|
|
300
|
-
async deleteKey(keyId) {
|
|
301
|
-
if (!this.apiKey) {
|
|
302
|
-
throw new Error('API key is required');
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const response = await fetch(`${this.baseUrl}/api/keys/${keyId}`, {
|
|
306
|
-
method: 'DELETE',
|
|
307
|
-
headers: {
|
|
308
|
-
'X-API-Key': this.apiKey,
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
if (!response.ok) {
|
|
313
|
-
const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
314
|
-
throw new Error(errorData.error?.message || errorData.message || 'Failed to delete key');
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return response.json();
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Export translation keys
|
|
322
|
-
*/
|
|
323
|
-
async exportKeys(format = 'json') {
|
|
324
|
-
if (!this.apiKey) {
|
|
325
|
-
throw new Error('API key is required');
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const response = await fetch(`${this.baseUrl}/api/keys/export/${format}`, {
|
|
329
|
-
method: 'GET',
|
|
330
|
-
headers: {
|
|
331
|
-
'X-API-Key': this.apiKey,
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
if (!response.ok) {
|
|
336
|
-
const errorData = await response.json().catch(() => ({ error: { message: response.statusText } }));
|
|
337
|
-
throw new Error(errorData.error?.message || errorData.message || 'Failed to export keys');
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return response.json();
|
|
341
|
-
}
|
|
342
|
-
}
|
package/src/lib/config.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/incremental.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utilities for incremental translation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Flatten a nested object into dot-notation keys
|
|
7
|
-
* @param {Object} obj - Nested object
|
|
8
|
-
* @param {string} prefix - Key prefix (used for recursion)
|
|
9
|
-
* @returns {Object} Flattened object with dot-notation keys
|
|
10
|
-
*/
|
|
11
|
-
export function flattenObject(obj, prefix = '') {
|
|
12
|
-
const result = {};
|
|
13
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
14
|
-
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
15
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
16
|
-
Object.assign(result, flattenObject(value, newKey));
|
|
17
|
-
} else {
|
|
18
|
-
result[newKey] = value;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Unflatten dot-notation keys back into nested object
|
|
26
|
-
* @param {Object} obj - Flattened object with dot-notation keys
|
|
27
|
-
* @returns {Object} Nested object
|
|
28
|
-
*/
|
|
29
|
-
export function unflattenObject(obj) {
|
|
30
|
-
const result = {};
|
|
31
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
32
|
-
const keys = key.split('.');
|
|
33
|
-
let current = result;
|
|
34
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
35
|
-
if (!current[keys[i]]) {
|
|
36
|
-
current[keys[i]] = {};
|
|
37
|
-
}
|
|
38
|
-
current = current[keys[i]];
|
|
39
|
-
}
|
|
40
|
-
current[keys[keys.length - 1]] = value;
|
|
41
|
-
}
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Deep merge two objects
|
|
47
|
-
* @param {Object} target - Target object
|
|
48
|
-
* @param {Object} source - Source object to merge in
|
|
49
|
-
* @returns {Object} Merged object
|
|
50
|
-
*/
|
|
51
|
-
export function deepMerge(target, source) {
|
|
52
|
-
const result = { ...target };
|
|
53
|
-
for (const [key, value] of Object.entries(source)) {
|
|
54
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
55
|
-
result[key] = deepMerge(result[key] || {}, value);
|
|
56
|
-
} else {
|
|
57
|
-
result[key] = value;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Find keys that exist in source but not in target
|
|
65
|
-
* @param {Object} sourceJson - Source JSON object
|
|
66
|
-
* @param {Object} targetJson - Target JSON object
|
|
67
|
-
* @returns {Object} Object containing only missing keys
|
|
68
|
-
*/
|
|
69
|
-
export function findMissingKeys(sourceJson, targetJson) {
|
|
70
|
-
const sourceFlat = flattenObject(sourceJson);
|
|
71
|
-
const targetFlat = flattenObject(targetJson);
|
|
72
|
-
|
|
73
|
-
const missingKeys = {};
|
|
74
|
-
for (const [key, value] of Object.entries(sourceFlat)) {
|
|
75
|
-
if (!(key in targetFlat)) {
|
|
76
|
-
missingKeys[key] = value;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return unflattenObject(missingKeys);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Count the number of leaf keys in a nested object
|
|
85
|
-
* @param {Object} obj - Object to count keys in
|
|
86
|
-
* @returns {number} Number of leaf keys
|
|
87
|
-
*/
|
|
88
|
-
export function countKeys(obj) {
|
|
89
|
-
return Object.keys(flattenObject(obj)).length;
|
|
90
|
-
}
|
package/src/utils/logger.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
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
|
-
}
|