@shipi18n/cli 1.1.5 → 2.3.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/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
- }
@@ -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
- }
@@ -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
- }