@shipi18n/cli 1.1.1 → 1.1.3
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 +5 -0
- package/bin/shipi18n.js +5 -2
- package/package.json +1 -1
- package/src/commands/init.js +443 -0
- package/src/commands/translate.js +2 -0
- package/src/lib/api.js +4 -1
package/README.md
CHANGED
|
@@ -97,6 +97,11 @@ shipi18n translate <input> [options]
|
|
|
97
97
|
- `-o, --output <dir>` - Output directory (default: `./locales`)
|
|
98
98
|
- `--api-key <key>` - API key (overrides config)
|
|
99
99
|
- `--preserve-placeholders` - Preserve placeholders (default: `true`)
|
|
100
|
+
- `--html-handling <mode>` - How to handle HTML in source text (default: `none`)
|
|
101
|
+
- `none` - Leave HTML as-is
|
|
102
|
+
- `strip` - Remove all HTML tags
|
|
103
|
+
- `decode` - Decode HTML entities (`&` → `&`)
|
|
104
|
+
- `preserve` - Keep HTML tags and translate text between them
|
|
100
105
|
- `--no-fallback` - Disable fallback to source for missing translations
|
|
101
106
|
- `--no-regional-fallback` - Disable regional fallback (e.g., pt-BR → pt)
|
|
102
107
|
|
package/bin/shipi18n.js
CHANGED
|
@@ -5,6 +5,7 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { translateCommand } from '../src/commands/translate.js';
|
|
6
6
|
import { keysCommand } from '../src/commands/keys.js';
|
|
7
7
|
import { configCommand } from '../src/commands/config.js';
|
|
8
|
+
import { initCommand } from '../src/commands/init.js';
|
|
8
9
|
import { readFileSync } from 'fs';
|
|
9
10
|
import { dirname, join } from 'path';
|
|
10
11
|
import { fileURLToPath } from 'url';
|
|
@@ -25,13 +26,14 @@ program
|
|
|
25
26
|
.version(packageJson.version, '-v, --version', 'Output the current version')
|
|
26
27
|
.addHelpText('after', `
|
|
27
28
|
${chalk.cyan('Examples:')}
|
|
29
|
+
$ shipi18n init
|
|
28
30
|
$ shipi18n translate en.json --target es,fr,de
|
|
29
31
|
$ shipi18n keys list
|
|
30
32
|
$ shipi18n config set apiKey sk_live_...
|
|
31
33
|
|
|
32
34
|
${chalk.cyan('Get started:')}
|
|
33
|
-
1.
|
|
34
|
-
2.
|
|
35
|
+
1. Run ${chalk.yellow('shipi18n init')} to detect your i18n setup
|
|
36
|
+
2. Sign up at ${chalk.underline('https://shipi18n.com')} and get your API key
|
|
35
37
|
3. Run: ${chalk.yellow('shipi18n config set apiKey YOUR_KEY')}
|
|
36
38
|
4. Translate: ${chalk.yellow('shipi18n translate en.json --target es,fr')}
|
|
37
39
|
|
|
@@ -39,6 +41,7 @@ ${chalk.gray('Documentation: https://shipi18n.com/docs/cli')}
|
|
|
39
41
|
`);
|
|
40
42
|
|
|
41
43
|
// Add commands
|
|
44
|
+
initCommand(program);
|
|
42
45
|
translateCommand(program);
|
|
43
46
|
keysCommand(program);
|
|
44
47
|
configCommand(program);
|
package/package.json
CHANGED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { join, dirname, basename, extname } from 'path';
|
|
5
|
+
import inquirer from 'inquirer';
|
|
6
|
+
|
|
7
|
+
// Known i18n frameworks and their signatures
|
|
8
|
+
const I18N_FRAMEWORKS = {
|
|
9
|
+
'i18next': {
|
|
10
|
+
packages: ['i18next', 'react-i18next', 'next-i18next', 'i18next-http-backend', 'i18next-browser-languagedetector'],
|
|
11
|
+
placeholderPattern: 'i18next',
|
|
12
|
+
},
|
|
13
|
+
'react-intl': {
|
|
14
|
+
packages: ['react-intl', 'formatjs', '@formatjs/intl'],
|
|
15
|
+
placeholderPattern: 'icu',
|
|
16
|
+
},
|
|
17
|
+
'vue-i18n': {
|
|
18
|
+
packages: ['vue-i18n', '@intlify/vue-i18n'],
|
|
19
|
+
placeholderPattern: 'i18next',
|
|
20
|
+
},
|
|
21
|
+
'next-intl': {
|
|
22
|
+
packages: ['next-intl'],
|
|
23
|
+
placeholderPattern: 'icu',
|
|
24
|
+
},
|
|
25
|
+
'lingui': {
|
|
26
|
+
packages: ['@lingui/core', '@lingui/react', '@lingui/macro'],
|
|
27
|
+
placeholderPattern: 'icu',
|
|
28
|
+
},
|
|
29
|
+
'polyglot': {
|
|
30
|
+
packages: ['node-polyglot'],
|
|
31
|
+
placeholderPattern: 'printf',
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Common locale directory patterns
|
|
36
|
+
const LOCALE_DIR_PATTERNS = [
|
|
37
|
+
'locales', 'locale', 'lang', 'langs', 'languages', 'i18n',
|
|
38
|
+
'translations', 'messages', 'public/locales', 'src/locales',
|
|
39
|
+
'src/i18n', 'src/translations', 'app/locales', 'assets/locales'
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Common language codes
|
|
43
|
+
const LANGUAGE_CODES = [
|
|
44
|
+
'en', 'es', 'fr', 'de', 'it', 'pt', 'ja', 'ko', 'zh', 'ru', 'ar', 'hi', 'nl', 'pl', 'tr', 'vi', 'th',
|
|
45
|
+
'en-US', 'en-GB', 'es-ES', 'es-MX', 'fr-FR', 'fr-CA', 'de-DE', 'pt-BR', 'pt-PT', 'zh-CN', 'zh-TW'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detect i18n framework from package.json
|
|
50
|
+
*/
|
|
51
|
+
function detectFramework(packageJson) {
|
|
52
|
+
const allDeps = {
|
|
53
|
+
...(packageJson.dependencies || {}),
|
|
54
|
+
...(packageJson.devDependencies || {})
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const detected = [];
|
|
58
|
+
|
|
59
|
+
for (const [framework, config] of Object.entries(I18N_FRAMEWORKS)) {
|
|
60
|
+
const matchedPackages = config.packages.filter(pkg => allDeps[pkg]);
|
|
61
|
+
if (matchedPackages.length > 0) {
|
|
62
|
+
detected.push({
|
|
63
|
+
framework,
|
|
64
|
+
confidence: matchedPackages.length / config.packages.length,
|
|
65
|
+
matchedPackages,
|
|
66
|
+
placeholderPattern: config.placeholderPattern
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
detected.sort((a, b) => b.confidence - a.confidence);
|
|
72
|
+
return { detected, primary: detected[0] || null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Recursively get all files in a directory
|
|
77
|
+
*/
|
|
78
|
+
function getAllFiles(dirPath, basePath = '', files = []) {
|
|
79
|
+
if (!existsSync(dirPath)) return files;
|
|
80
|
+
|
|
81
|
+
const items = readdirSync(dirPath);
|
|
82
|
+
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
// Skip node_modules, .git, etc.
|
|
85
|
+
if (item === 'node_modules' || item === '.git' || item === 'dist' || item === 'build') {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const fullPath = join(dirPath, item);
|
|
90
|
+
const relativePath = basePath ? `${basePath}/${item}` : item;
|
|
91
|
+
|
|
92
|
+
if (statSync(fullPath).isDirectory()) {
|
|
93
|
+
getAllFiles(fullPath, relativePath, files);
|
|
94
|
+
} else {
|
|
95
|
+
files.push(relativePath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Detect locale file structure
|
|
104
|
+
*/
|
|
105
|
+
function detectFileStructure(files) {
|
|
106
|
+
const result = {
|
|
107
|
+
localeDirectories: [],
|
|
108
|
+
sourceLanguage: null,
|
|
109
|
+
targetLanguages: [],
|
|
110
|
+
namespaces: [],
|
|
111
|
+
fileFormat: 'json',
|
|
112
|
+
structure: 'flat'
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Find locale directories
|
|
116
|
+
const dirCounts = {};
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
const parts = file.split('/');
|
|
119
|
+
for (let i = 0; i < parts.length; i++) {
|
|
120
|
+
const dir = parts.slice(0, i + 1).join('/');
|
|
121
|
+
const dirName = parts[i].toLowerCase();
|
|
122
|
+
if (LOCALE_DIR_PATTERNS.includes(dirName) || LANGUAGE_CODES.includes(parts[i])) {
|
|
123
|
+
dirCounts[dir] = (dirCounts[dir] || 0) + 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const sortedDirs = Object.entries(dirCounts).sort((a, b) => b[1] - a[1]);
|
|
129
|
+
if (sortedDirs.length > 0) {
|
|
130
|
+
result.localeDirectories = sortedDirs.slice(0, 3).map(([dir]) => dir);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Find language files
|
|
134
|
+
const langFiles = files.filter(f => {
|
|
135
|
+
const filename = f.split('/').pop();
|
|
136
|
+
const ext = filename.split('.').pop();
|
|
137
|
+
const name = filename.replace(`.${ext}`, '');
|
|
138
|
+
return (ext === 'json' || ext === 'yaml' || ext === 'yml') &&
|
|
139
|
+
(LANGUAGE_CODES.includes(name) || LANGUAGE_CODES.includes(name.toLowerCase()));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const languages = new Set();
|
|
143
|
+
const namespaces = new Set();
|
|
144
|
+
|
|
145
|
+
for (const file of langFiles) {
|
|
146
|
+
const parts = file.split('/');
|
|
147
|
+
const filename = parts.pop();
|
|
148
|
+
const ext = filename.split('.').pop();
|
|
149
|
+
const name = filename.replace(`.${ext}`, '');
|
|
150
|
+
|
|
151
|
+
if (LANGUAGE_CODES.includes(name) || LANGUAGE_CODES.includes(name.toLowerCase())) {
|
|
152
|
+
languages.add(name.toLowerCase());
|
|
153
|
+
result.structure = 'flat';
|
|
154
|
+
} else {
|
|
155
|
+
const parentDir = parts[parts.length - 1];
|
|
156
|
+
if (parentDir && (LANGUAGE_CODES.includes(parentDir) || LANGUAGE_CODES.includes(parentDir.toLowerCase()))) {
|
|
157
|
+
languages.add(parentDir.toLowerCase());
|
|
158
|
+
namespaces.add(name);
|
|
159
|
+
result.structure = 'nested';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (ext === 'yaml' || ext === 'yml') {
|
|
164
|
+
result.fileFormat = 'yaml';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
result.targetLanguages = Array.from(languages).filter(l => l !== 'en');
|
|
169
|
+
result.sourceLanguage = languages.has('en') ? 'en' : Array.from(languages)[0] || 'en';
|
|
170
|
+
result.namespaces = Array.from(namespaces);
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Detect placeholder patterns from translation content
|
|
177
|
+
*/
|
|
178
|
+
function detectPlaceholderPatterns(content) {
|
|
179
|
+
const PLACEHOLDER_PATTERNS = {
|
|
180
|
+
i18next: { regex: /\{\{[^}]+\}\}/g },
|
|
181
|
+
icu: { regex: /\{[a-zA-Z_][a-zA-Z0-9_]*(?:,\s*(?:number|date|time|plural|select|selectordinal))?[^}]*\}/g },
|
|
182
|
+
printf: { regex: /%[sd@]|%\d+\$[sd]/g },
|
|
183
|
+
ruby: { regex: /%\{[^}]+\}/g }
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const flatContent = flattenObject(content);
|
|
187
|
+
const values = Object.values(flatContent).filter(v => typeof v === 'string');
|
|
188
|
+
|
|
189
|
+
const patternCounts = {};
|
|
190
|
+
|
|
191
|
+
for (const [patternName, patternConfig] of Object.entries(PLACEHOLDER_PATTERNS)) {
|
|
192
|
+
patternCounts[patternName] = 0;
|
|
193
|
+
for (const value of values) {
|
|
194
|
+
const matches = value.match(patternConfig.regex);
|
|
195
|
+
if (matches) {
|
|
196
|
+
patternCounts[patternName] += matches.length;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sortedPatterns = Object.entries(patternCounts)
|
|
202
|
+
.filter(([_, count]) => count > 0)
|
|
203
|
+
.sort((a, b) => b[1] - a[1]);
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
patterns: patternCounts,
|
|
207
|
+
primary: sortedPatterns[0]?.[0] || 'icu'
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function flattenObject(obj, prefix = '') {
|
|
212
|
+
const result = {};
|
|
213
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
214
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
215
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
216
|
+
Object.assign(result, flattenObject(value, newKey));
|
|
217
|
+
} else {
|
|
218
|
+
result[newKey] = value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate GitHub Action workflow
|
|
226
|
+
*/
|
|
227
|
+
function generateGitHubActionWorkflow({ sourceDir, targetLanguages, sourceLanguage }) {
|
|
228
|
+
return `name: Translate
|
|
229
|
+
|
|
230
|
+
on:
|
|
231
|
+
push:
|
|
232
|
+
branches: [main]
|
|
233
|
+
paths:
|
|
234
|
+
- '${sourceDir}/**'
|
|
235
|
+
workflow_dispatch:
|
|
236
|
+
|
|
237
|
+
jobs:
|
|
238
|
+
translate:
|
|
239
|
+
runs-on: ubuntu-latest
|
|
240
|
+
steps:
|
|
241
|
+
- uses: actions/checkout@v4
|
|
242
|
+
|
|
243
|
+
- name: Translate locale files
|
|
244
|
+
uses: shipi18n/shipi18n-github-action@v1
|
|
245
|
+
with:
|
|
246
|
+
api-key: \${{ secrets.SHIPI18N_API_KEY }}
|
|
247
|
+
source-dir: '${sourceDir}'
|
|
248
|
+
target-languages: '${targetLanguages.join(',')}'
|
|
249
|
+
source-language: '${sourceLanguage}'
|
|
250
|
+
incremental: 'true'
|
|
251
|
+
verify: 'true'
|
|
252
|
+
create-pr: 'true'
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function initCommand(program) {
|
|
257
|
+
program
|
|
258
|
+
.command('init')
|
|
259
|
+
.description('Analyze your project and generate shipi18n configuration')
|
|
260
|
+
.option('-y, --yes', 'Skip prompts and use detected defaults')
|
|
261
|
+
.option('--no-workflow', 'Skip GitHub Action workflow generation')
|
|
262
|
+
.action(async (options) => {
|
|
263
|
+
logger.log('');
|
|
264
|
+
logger.log(chalk.cyan.bold('🔍 Shipi18n Project Analyzer'));
|
|
265
|
+
logger.log(chalk.gray('Detecting your i18n setup...'));
|
|
266
|
+
logger.log('');
|
|
267
|
+
|
|
268
|
+
const cwd = process.cwd();
|
|
269
|
+
|
|
270
|
+
// Step 1: Read package.json
|
|
271
|
+
let packageJson = null;
|
|
272
|
+
let frameworkResult = { detected: [], primary: null };
|
|
273
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
274
|
+
|
|
275
|
+
if (existsSync(packageJsonPath)) {
|
|
276
|
+
try {
|
|
277
|
+
packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
278
|
+
frameworkResult = detectFramework(packageJson);
|
|
279
|
+
|
|
280
|
+
if (frameworkResult.primary) {
|
|
281
|
+
logger.success(`Framework: ${chalk.cyan(frameworkResult.primary.framework)}`);
|
|
282
|
+
logger.log(chalk.gray(` Detected packages: ${frameworkResult.primary.matchedPackages.join(', ')}`));
|
|
283
|
+
} else {
|
|
284
|
+
logger.warn('No i18n framework detected in package.json');
|
|
285
|
+
}
|
|
286
|
+
} catch (e) {
|
|
287
|
+
logger.warn('Could not parse package.json');
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
logger.warn('No package.json found');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Step 2: Scan file structure
|
|
294
|
+
logger.log('');
|
|
295
|
+
const files = getAllFiles(cwd);
|
|
296
|
+
const fileStructure = detectFileStructure(files);
|
|
297
|
+
|
|
298
|
+
if (fileStructure.localeDirectories.length > 0) {
|
|
299
|
+
logger.success(`Locale directory: ${chalk.cyan(fileStructure.localeDirectories[0])}`);
|
|
300
|
+
logger.log(chalk.gray(` Structure: ${fileStructure.structure}`));
|
|
301
|
+
logger.log(chalk.gray(` Format: ${fileStructure.fileFormat}`));
|
|
302
|
+
} else {
|
|
303
|
+
logger.warn('No locale directory detected');
|
|
304
|
+
fileStructure.localeDirectories = ['locales'];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (fileStructure.sourceLanguage) {
|
|
308
|
+
logger.success(`Source language: ${chalk.cyan(fileStructure.sourceLanguage)}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (fileStructure.targetLanguages.length > 0) {
|
|
312
|
+
logger.success(`Target languages: ${chalk.cyan(fileStructure.targetLanguages.join(', '))}`);
|
|
313
|
+
} else {
|
|
314
|
+
logger.info('No target languages detected - using defaults (es, fr, de)');
|
|
315
|
+
fileStructure.targetLanguages = ['es', 'fr', 'de'];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (fileStructure.namespaces.length > 0) {
|
|
319
|
+
logger.log(chalk.gray(` Namespaces: ${fileStructure.namespaces.join(', ')}`));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Step 3: Detect placeholder patterns from sample file
|
|
323
|
+
let placeholderResult = { primary: 'icu' };
|
|
324
|
+
|
|
325
|
+
if (fileStructure.localeDirectories.length > 0) {
|
|
326
|
+
const localeDir = fileStructure.localeDirectories[0];
|
|
327
|
+
const sourceFile = files.find(f =>
|
|
328
|
+
f.startsWith(localeDir) &&
|
|
329
|
+
f.endsWith('.json') &&
|
|
330
|
+
(f.includes('/en') || f.includes('/en.') || f.endsWith('en.json'))
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (sourceFile) {
|
|
334
|
+
try {
|
|
335
|
+
const content = JSON.parse(readFileSync(join(cwd, sourceFile), 'utf8'));
|
|
336
|
+
placeholderResult = detectPlaceholderPatterns(content);
|
|
337
|
+
logger.success(`Placeholder format: ${chalk.cyan(placeholderResult.primary)}`);
|
|
338
|
+
} catch (e) {
|
|
339
|
+
// Use framework default if available
|
|
340
|
+
if (frameworkResult.primary?.placeholderPattern) {
|
|
341
|
+
placeholderResult.primary = frameworkResult.primary.placeholderPattern;
|
|
342
|
+
logger.info(`Placeholder format: ${chalk.cyan(placeholderResult.primary)} (from framework)`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logger.log('');
|
|
349
|
+
|
|
350
|
+
// Build configuration
|
|
351
|
+
const sourceDir = fileStructure.structure === 'nested'
|
|
352
|
+
? `${fileStructure.localeDirectories[0]}/${fileStructure.sourceLanguage || 'en'}`
|
|
353
|
+
: fileStructure.localeDirectories[0];
|
|
354
|
+
|
|
355
|
+
const config = {
|
|
356
|
+
sourceLanguage: fileStructure.sourceLanguage || 'en',
|
|
357
|
+
targetLanguages: fileStructure.targetLanguages.length > 0
|
|
358
|
+
? fileStructure.targetLanguages
|
|
359
|
+
: ['es', 'fr', 'de'],
|
|
360
|
+
sourceDir,
|
|
361
|
+
outputDir: fileStructure.localeDirectories[0] || 'locales',
|
|
362
|
+
fileFormat: fileStructure.fileFormat || 'json',
|
|
363
|
+
placeholderFormat: placeholderResult.primary,
|
|
364
|
+
incremental: true,
|
|
365
|
+
verify: true,
|
|
366
|
+
selfCorrect: false
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Confirmation or auto-accept
|
|
370
|
+
let confirmed = options.yes;
|
|
371
|
+
|
|
372
|
+
if (!confirmed) {
|
|
373
|
+
logger.log(chalk.cyan.bold('📝 Proposed Configuration:'));
|
|
374
|
+
logger.log('');
|
|
375
|
+
logger.log(JSON.stringify(config, null, 2));
|
|
376
|
+
logger.log('');
|
|
377
|
+
|
|
378
|
+
const { proceed } = await inquirer.prompt([{
|
|
379
|
+
type: 'confirm',
|
|
380
|
+
name: 'proceed',
|
|
381
|
+
message: 'Create shipi18n.config.json with these settings?',
|
|
382
|
+
default: true
|
|
383
|
+
}]);
|
|
384
|
+
confirmed = proceed;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!confirmed) {
|
|
388
|
+
logger.info('Cancelled. Run again with different options or manually create config.');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Write config file
|
|
393
|
+
const configPath = join(cwd, 'shipi18n.config.json');
|
|
394
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
395
|
+
logger.success(`Created ${chalk.cyan('shipi18n.config.json')}`);
|
|
396
|
+
|
|
397
|
+
// GitHub Action workflow
|
|
398
|
+
if (options.workflow !== false) {
|
|
399
|
+
let createWorkflow = options.yes;
|
|
400
|
+
|
|
401
|
+
if (!createWorkflow) {
|
|
402
|
+
const { workflow } = await inquirer.prompt([{
|
|
403
|
+
type: 'confirm',
|
|
404
|
+
name: 'workflow',
|
|
405
|
+
message: 'Create GitHub Action workflow for automatic translations?',
|
|
406
|
+
default: true
|
|
407
|
+
}]);
|
|
408
|
+
createWorkflow = workflow;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (createWorkflow) {
|
|
412
|
+
const workflowDir = join(cwd, '.github', 'workflows');
|
|
413
|
+
const workflowPath = join(workflowDir, 'translate.yml');
|
|
414
|
+
|
|
415
|
+
// Create directories if needed
|
|
416
|
+
const { mkdirSync } = await import('fs');
|
|
417
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
418
|
+
|
|
419
|
+
const workflow = generateGitHubActionWorkflow({
|
|
420
|
+
sourceDir: config.sourceDir,
|
|
421
|
+
targetLanguages: config.targetLanguages,
|
|
422
|
+
sourceLanguage: config.sourceLanguage
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
writeFileSync(workflowPath, workflow);
|
|
426
|
+
logger.success(`Created ${chalk.cyan('.github/workflows/translate.yml')}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Next steps
|
|
431
|
+
logger.log('');
|
|
432
|
+
logger.log(chalk.cyan.bold('🚀 Next Steps:'));
|
|
433
|
+
logger.log('');
|
|
434
|
+
logger.log(` 1. Get your API key at ${chalk.underline('https://shipi18n.com')}`);
|
|
435
|
+
logger.log(` 2. Add secret: ${chalk.yellow('SHIPI18N_API_KEY')} to your GitHub repository`);
|
|
436
|
+
logger.log(` 3. Push changes to trigger automatic translations`);
|
|
437
|
+
logger.log('');
|
|
438
|
+
logger.log(chalk.gray('Or translate manually:'));
|
|
439
|
+
logger.log(` ${chalk.yellow(`shipi18n config set apiKey YOUR_KEY`)}`);
|
|
440
|
+
logger.log(` ${chalk.yellow(`shipi18n translate ${config.sourceDir} --target ${config.targetLanguages.join(',')}`)}`);
|
|
441
|
+
logger.log('');
|
|
442
|
+
});
|
|
443
|
+
}
|
|
@@ -15,6 +15,7 @@ export function translateCommand(program) {
|
|
|
15
15
|
.option('-o, --output <dir>', 'Output directory', './locales')
|
|
16
16
|
.option('--api-key <key>', 'API key (overrides config)')
|
|
17
17
|
.option('--preserve-placeholders', 'Preserve placeholders like {name}, {{value}}, etc.', true)
|
|
18
|
+
.option('--html-handling <mode>', 'How to handle HTML in source text: none, strip, decode, preserve', 'none')
|
|
18
19
|
.option('--no-fallback', 'Disable fallback to source language for missing translations')
|
|
19
20
|
.option('--no-regional-fallback', 'Disable regional fallback (e.g., pt-BR -> pt)')
|
|
20
21
|
.option('-i, --incremental', 'Only translate new/missing keys (skip existing translations)')
|
|
@@ -131,6 +132,7 @@ export function translateCommand(program) {
|
|
|
131
132
|
sourceLanguage,
|
|
132
133
|
targetLanguages,
|
|
133
134
|
preservePlaceholders: options.preservePlaceholders,
|
|
135
|
+
htmlHandling: options.htmlHandling,
|
|
134
136
|
fallback: {
|
|
135
137
|
fallbackToSource: options.fallback !== false,
|
|
136
138
|
regionalFallback: options.regionalFallback !== false,
|
package/src/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
dotenv.config();
|
|
3
3
|
|
|
4
|
-
const API_BASE_URL = process.env.SHIPI18N_API_URL || 'https://
|
|
4
|
+
const API_BASE_URL = process.env.SHIPI18N_API_URL || 'https://ydjkwckq3f.execute-api.us-east-1.amazonaws.com';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Shipi18n API Client
|
|
@@ -19,6 +19,7 @@ export class Shipi18nAPI {
|
|
|
19
19
|
* @param {string} options.sourceLanguage - Source language code
|
|
20
20
|
* @param {string[]} options.targetLanguages - Target language codes
|
|
21
21
|
* @param {boolean} options.preservePlaceholders - Preserve placeholders
|
|
22
|
+
* @param {string} options.htmlHandling - How to handle HTML: none, strip, decode, preserve
|
|
22
23
|
* @param {Object} options.fallback - Fallback options
|
|
23
24
|
* @param {boolean} options.fallback.fallbackToSource - Use source content when translation missing (default: true)
|
|
24
25
|
* @param {boolean} options.fallback.regionalFallback - Enable pt-BR -> pt fallback (default: true)
|
|
@@ -29,6 +30,7 @@ export class Shipi18nAPI {
|
|
|
29
30
|
sourceLanguage = 'en',
|
|
30
31
|
targetLanguages,
|
|
31
32
|
preservePlaceholders = true,
|
|
33
|
+
htmlHandling = 'none',
|
|
32
34
|
fallback = {}
|
|
33
35
|
}) {
|
|
34
36
|
if (!this.apiKey) {
|
|
@@ -59,6 +61,7 @@ export class Shipi18nAPI {
|
|
|
59
61
|
sourceLanguage,
|
|
60
62
|
targetLanguages: JSON.stringify(processedTargets),
|
|
61
63
|
preservePlaceholders: String(preservePlaceholders),
|
|
64
|
+
htmlHandling,
|
|
62
65
|
}),
|
|
63
66
|
});
|
|
64
67
|
|