@i18n-agent/cli 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/LICENSE +21 -0
- package/README.md +107 -0
- package/bin/i18nagent.js +6 -0
- package/docs/BINARY_DISTRIBUTION.md +108 -0
- package/package.json +44 -0
- package/src/api-client.js +127 -0
- package/src/commands/analyze.js +58 -0
- package/src/commands/credits.js +27 -0
- package/src/commands/download.js +53 -0
- package/src/commands/languages.js +33 -0
- package/src/commands/login.js +26 -0
- package/src/commands/logout.js +19 -0
- package/src/commands/resume.js +30 -0
- package/src/commands/status.js +44 -0
- package/src/commands/translate.js +301 -0
- package/src/commands/tui.js +91 -0
- package/src/commands/upload.js +115 -0
- package/src/config.js +45 -0
- package/src/index.js +156 -0
- package/src/namespace-detector.js +362 -0
- package/src/output.js +39 -0
- package/src/prompts.js +65 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { requireApiKey } from '../config.js';
|
|
2
|
+
import { callTool } from '../api-client.js';
|
|
3
|
+
import { formatOutput, formatProgress, print } from '../output.js';
|
|
4
|
+
|
|
5
|
+
export async function statusAction(jobId, opts) {
|
|
6
|
+
if (!jobId) {
|
|
7
|
+
console.error('Error: jobId is required');
|
|
8
|
+
console.error('Usage: i18nagent status <jobId>');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const apiKey = requireApiKey();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const result = await callTool('check_translation_status', {
|
|
16
|
+
jobId,
|
|
17
|
+
pageSize: opts.pageSize ? parseInt(opts.pageSize) : 50,
|
|
18
|
+
}, apiKey, { timeout: 30000 });
|
|
19
|
+
|
|
20
|
+
print(formatOutput(result, {
|
|
21
|
+
json: opts.json,
|
|
22
|
+
template: (data) => {
|
|
23
|
+
const lines = [`Job: ${jobId}`];
|
|
24
|
+
lines.push(`Status: ${data.status || 'unknown'}`);
|
|
25
|
+
if (data.progress !== undefined) {
|
|
26
|
+
lines.push(`Progress: ${formatProgress(data.progress, 100)}`);
|
|
27
|
+
}
|
|
28
|
+
if (data.elapsedTime) {
|
|
29
|
+
lines.push(`Elapsed: ${data.elapsedTime}`);
|
|
30
|
+
}
|
|
31
|
+
if (data.completedLanguages?.length) {
|
|
32
|
+
lines.push(`Completed: ${data.completedLanguages.join(', ')}`);
|
|
33
|
+
}
|
|
34
|
+
if (data.status === 'completed') {
|
|
35
|
+
lines.push('\nDownload with: i18nagent download ' + jobId);
|
|
36
|
+
}
|
|
37
|
+
return lines.join('\n');
|
|
38
|
+
}
|
|
39
|
+
}));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(`Error: ${error.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { requireApiKey, loadConfig, getConfigPath } from '../config.js';
|
|
4
|
+
import { callTool } from '../api-client.js';
|
|
5
|
+
import { formatOutput, formatProgress, print, printErr } from '../output.js';
|
|
6
|
+
import { promptText, promptChoice } from '../prompts.js';
|
|
7
|
+
import { detectNamespaceFromPath } from '../namespace-detector.js';
|
|
8
|
+
|
|
9
|
+
function isFilePath(input) {
|
|
10
|
+
if (!input) return false;
|
|
11
|
+
if (fs.existsSync(input)) return true;
|
|
12
|
+
const ext = path.extname(input);
|
|
13
|
+
if (ext && !input.includes(' ')) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseLanguages(langStr) {
|
|
18
|
+
if (!langStr) return null;
|
|
19
|
+
return langStr.split(',').map(l => l.trim()).filter(Boolean);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function interactiveTranslate(opts) {
|
|
23
|
+
const modeIdx = await promptChoice('What would you like to translate?', [
|
|
24
|
+
'Text',
|
|
25
|
+
'File',
|
|
26
|
+
]);
|
|
27
|
+
if (modeIdx === null) return;
|
|
28
|
+
|
|
29
|
+
if (modeIdx === 0) {
|
|
30
|
+
const text = await promptText('Enter text to translate: ');
|
|
31
|
+
if (!text) return;
|
|
32
|
+
const lang = await promptText('Target language(s) (comma-separated, e.g. es,fr): ');
|
|
33
|
+
if (!lang) return;
|
|
34
|
+
opts.lang = lang;
|
|
35
|
+
return translateText([text], opts);
|
|
36
|
+
} else {
|
|
37
|
+
const filePath = await promptText('File path: ');
|
|
38
|
+
if (!filePath) return;
|
|
39
|
+
const lang = await promptText('Target language(s) (comma-separated, e.g. es,fr): ');
|
|
40
|
+
if (!lang) return;
|
|
41
|
+
opts.lang = lang;
|
|
42
|
+
return translateFile(filePath, opts);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function translateAction(input, opts) {
|
|
47
|
+
if (!input) {
|
|
48
|
+
return interactiveTranslate(opts);
|
|
49
|
+
}
|
|
50
|
+
if (isFilePath(input)) {
|
|
51
|
+
return translateFile(input, opts);
|
|
52
|
+
} else {
|
|
53
|
+
return translateText([input], opts);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function translateTextAction(text, opts) {
|
|
58
|
+
if (!text) {
|
|
59
|
+
const input = await promptText('Enter text to translate: ');
|
|
60
|
+
if (!input) return;
|
|
61
|
+
text = input;
|
|
62
|
+
}
|
|
63
|
+
return translateText([text], opts);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function translateFileAction(filePath, opts) {
|
|
67
|
+
if (!filePath) {
|
|
68
|
+
const input = await promptText('File path: ');
|
|
69
|
+
if (!input) return;
|
|
70
|
+
filePath = input;
|
|
71
|
+
}
|
|
72
|
+
return translateFile(filePath, opts);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function translateText(texts, opts) {
|
|
76
|
+
const apiKey = requireApiKey();
|
|
77
|
+
const languages = parseLanguages(opts.lang);
|
|
78
|
+
if (!languages) {
|
|
79
|
+
printErr('Error: --lang is required (e.g. --lang es,fr,ja)');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const args = {
|
|
84
|
+
texts,
|
|
85
|
+
targetLanguages: languages,
|
|
86
|
+
};
|
|
87
|
+
if (opts.source) args.sourceLanguage = opts.source;
|
|
88
|
+
if (opts.audience) args.targetAudience = opts.audience;
|
|
89
|
+
if (opts.industry) args.industry = opts.industry;
|
|
90
|
+
if (opts.context) args.context = opts.context;
|
|
91
|
+
if (opts.namespace) args.namespace = opts.namespace;
|
|
92
|
+
if (opts.pseudo) args.pseudoTranslation = true;
|
|
93
|
+
if (opts.skipWarnings) args.skipWarnings = true;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const result = await callTool('translate_text', args, apiKey, {
|
|
97
|
+
timeout: texts.join('').length > 50000 ? 600000 : 300000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (result.status === 'processing' && result.jobId) {
|
|
101
|
+
printErr(`Job started: ${result.jobId}`);
|
|
102
|
+
printErr('Polling for completion...\n');
|
|
103
|
+
return pollAndDownload(result.jobId, apiKey, opts);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
print(formatOutput(result, {
|
|
107
|
+
json: opts.json,
|
|
108
|
+
template: formatTranslationResult,
|
|
109
|
+
}));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
print(JSON.stringify({ error: error.message }));
|
|
113
|
+
} else {
|
|
114
|
+
printErr(`Error: ${error.message}`);
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function translateFile(filePath, opts) {
|
|
121
|
+
const apiKey = requireApiKey();
|
|
122
|
+
const languages = parseLanguages(opts.lang);
|
|
123
|
+
if (!languages) {
|
|
124
|
+
printErr('Error: --lang is required (e.g. --lang es,fr,ja)');
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!fs.existsSync(filePath)) {
|
|
129
|
+
printErr(`Error: File not found: ${filePath}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
134
|
+
const ext = path.extname(filePath).slice(1) || 'auto';
|
|
135
|
+
|
|
136
|
+
let namespace = opts.namespace;
|
|
137
|
+
if (!namespace) {
|
|
138
|
+
const config = loadConfig(getConfigPath());
|
|
139
|
+
namespace = config.defaultNamespace;
|
|
140
|
+
}
|
|
141
|
+
if (!namespace) {
|
|
142
|
+
const detection = detectNamespaceFromPath(path.resolve(filePath));
|
|
143
|
+
if (detection.suggestion && detection.confidence > 0.5) {
|
|
144
|
+
namespace = detection.suggestion;
|
|
145
|
+
printErr(`Auto-detected namespace: "${namespace}"`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!namespace) {
|
|
149
|
+
namespace = await promptText('Namespace (required for file translation): ');
|
|
150
|
+
if (!namespace) {
|
|
151
|
+
printErr('Error: namespace is required for file translation');
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const args = {
|
|
157
|
+
fileContent: content,
|
|
158
|
+
filePath,
|
|
159
|
+
fileType: ext,
|
|
160
|
+
targetLanguages: languages,
|
|
161
|
+
namespace,
|
|
162
|
+
};
|
|
163
|
+
if (opts.source) args.sourceLanguage = opts.source;
|
|
164
|
+
if (opts.audience) args.targetAudience = opts.audience;
|
|
165
|
+
if (opts.industry) args.industry = opts.industry;
|
|
166
|
+
if (opts.context) args.context = opts.context;
|
|
167
|
+
if (opts.pseudo) args.pseudoTranslation = true;
|
|
168
|
+
if (opts.skipWarnings) args.skipWarnings = true;
|
|
169
|
+
|
|
170
|
+
printErr(`Translating ${path.basename(filePath)} -> ${languages.join(', ')}...`);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const result = await callTool('translate_file', args, apiKey, {
|
|
174
|
+
timeout: content.length > 50000 ? 600000 : 300000,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (result.jobId) {
|
|
178
|
+
printErr(`Job ID: ${result.jobId}\n`);
|
|
179
|
+
return pollAndDownload(result.jobId, apiKey, opts);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
print(formatOutput(result, { json: opts.json }));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (opts.json) {
|
|
185
|
+
print(JSON.stringify({ error: error.message }));
|
|
186
|
+
} else {
|
|
187
|
+
printErr(`Error: ${error.message}`);
|
|
188
|
+
}
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function pollAndDownload(jobId, apiKey, opts) {
|
|
194
|
+
const startTime = Date.now();
|
|
195
|
+
const maxWait = 600000;
|
|
196
|
+
|
|
197
|
+
while (Date.now() - startTime < maxWait) {
|
|
198
|
+
try {
|
|
199
|
+
const status = await callTool('check_translation_status', {
|
|
200
|
+
jobId,
|
|
201
|
+
pageSize: 50,
|
|
202
|
+
}, apiKey, { timeout: 30000 });
|
|
203
|
+
|
|
204
|
+
const progress = status.progress || 0;
|
|
205
|
+
const statusText = status.status || 'unknown';
|
|
206
|
+
|
|
207
|
+
if (!opts.json) {
|
|
208
|
+
const bar = formatProgress(progress, 100);
|
|
209
|
+
const completed = status.completedLanguages?.join(', ') || '';
|
|
210
|
+
printErr(`\r ${bar} | ${statusText}${completed ? ' | Done: ' + completed : ''} `);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (statusText === 'completed' || progress === 100) {
|
|
214
|
+
if (!opts.json) printErr('\n');
|
|
215
|
+
return downloadResults(jobId, apiKey, opts);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (statusText === 'failed') {
|
|
219
|
+
if (!opts.json) printErr('\n');
|
|
220
|
+
const msg = status.error || 'Translation failed';
|
|
221
|
+
printErr(`Error: ${msg}`);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
226
|
+
} catch (error) {
|
|
227
|
+
printErr(`\nError checking status: ${error.message}`);
|
|
228
|
+
printErr(`Check manually: i18nagent status ${jobId}`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
printErr(`\nTimeout waiting for job. Check status: i18nagent status ${jobId}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function downloadResults(jobId, apiKey, opts) {
|
|
238
|
+
try {
|
|
239
|
+
const result = await callTool('download_translations', { jobId }, apiKey, { timeout: 60000 });
|
|
240
|
+
|
|
241
|
+
const outputDir = opts.output || `./i18n-translations-${jobId}`;
|
|
242
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
243
|
+
|
|
244
|
+
const filesWritten = [];
|
|
245
|
+
|
|
246
|
+
if (result.downloadUrls) {
|
|
247
|
+
for (const [lang, url] of Object.entries(result.downloadUrls)) {
|
|
248
|
+
const response = await fetch(url);
|
|
249
|
+
const content = await response.text();
|
|
250
|
+
const ext = result.fileName?.split('.').pop() || 'json';
|
|
251
|
+
const fp = path.join(outputDir, `${lang}.${ext}`);
|
|
252
|
+
fs.writeFileSync(fp, content);
|
|
253
|
+
filesWritten.push(fp);
|
|
254
|
+
}
|
|
255
|
+
} else if (result.translations) {
|
|
256
|
+
for (const [lang, content] of Object.entries(result.translations)) {
|
|
257
|
+
const ext = result.fileName?.split('.').pop() || 'json';
|
|
258
|
+
const fp = path.join(outputDir, `${lang}.${ext}`);
|
|
259
|
+
fs.writeFileSync(fp, content);
|
|
260
|
+
filesWritten.push(fp);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (opts.json) {
|
|
265
|
+
print(JSON.stringify({ jobId, outputDir, filesWritten }, null, 2));
|
|
266
|
+
} else {
|
|
267
|
+
print(`Translation complete! Files saved to:\n${filesWritten.map(f => ` ${f}`).join('\n')}`);
|
|
268
|
+
}
|
|
269
|
+
} catch (error) {
|
|
270
|
+
printErr(`Error downloading: ${error.message}`);
|
|
271
|
+
printErr(`Download manually: i18nagent download ${jobId}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatTranslationResult(data) {
|
|
277
|
+
if (!data) return 'No result';
|
|
278
|
+
const lines = ['Translation complete\n'];
|
|
279
|
+
|
|
280
|
+
if (data.translations) {
|
|
281
|
+
if (Array.isArray(data.translations)) {
|
|
282
|
+
for (const t of data.translations) {
|
|
283
|
+
lines.push(` ${t.language || t.lang}: "${t.text || t.translation}"`);
|
|
284
|
+
}
|
|
285
|
+
} else if (typeof data.translations === 'object') {
|
|
286
|
+
for (const [lang, texts] of Object.entries(data.translations)) {
|
|
287
|
+
if (Array.isArray(texts)) {
|
|
288
|
+
lines.push(` ${lang}: ${texts.map(t => `"${t}"`).join(', ')}`);
|
|
289
|
+
} else {
|
|
290
|
+
lines.push(` ${lang}: "${texts}"`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (data.creditsUsed !== undefined) {
|
|
297
|
+
lines.push(`\n Credits used: ${data.creditsUsed}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return lines.join('\n');
|
|
301
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { promptChoice, promptText } from '../prompts.js';
|
|
5
|
+
import { translateAction } from './translate.js';
|
|
6
|
+
import { statusAction } from './status.js';
|
|
7
|
+
import { creditsAction } from './credits.js';
|
|
8
|
+
import { loginAction } from './login.js';
|
|
9
|
+
import { printErr } from '../output.js';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
|
|
13
|
+
function getVersion() {
|
|
14
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
16
|
+
return pkg.version;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function tuiAction() {
|
|
20
|
+
console.error(`\ni18n-agent CLI v${getVersion()}\n`);
|
|
21
|
+
|
|
22
|
+
while (true) {
|
|
23
|
+
const choice = await promptChoice('What would you like to do?', [
|
|
24
|
+
'Translate text',
|
|
25
|
+
'Translate file',
|
|
26
|
+
'Check job status',
|
|
27
|
+
'View credits',
|
|
28
|
+
'Upload translations',
|
|
29
|
+
'Settings (login/logout)',
|
|
30
|
+
'Exit',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
if (choice === null || choice === 6) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
switch (choice) {
|
|
39
|
+
case 0: {
|
|
40
|
+
const text = await promptText('\nEnter text to translate: ');
|
|
41
|
+
if (!text) break;
|
|
42
|
+
const lang = await promptText('Target language(s) (e.g. es,fr): ');
|
|
43
|
+
if (!lang) break;
|
|
44
|
+
await translateAction(text, { lang });
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
case 1: {
|
|
48
|
+
const filePath = await promptText('\nFile path: ');
|
|
49
|
+
if (!filePath) break;
|
|
50
|
+
const lang = await promptText('Target language(s) (e.g. es,fr): ');
|
|
51
|
+
if (!lang) break;
|
|
52
|
+
await translateAction(filePath, { lang });
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
case 2: {
|
|
56
|
+
const jobId = await promptText('\nJob ID: ');
|
|
57
|
+
if (!jobId) break;
|
|
58
|
+
await statusAction(jobId, {});
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 3: {
|
|
62
|
+
await creditsAction({});
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case 4: {
|
|
66
|
+
printErr('\nUse: i18nagent upload <file> --source <lang> --target <lang>');
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case 5: {
|
|
70
|
+
const settingChoice = await promptChoice('\nSettings:', [
|
|
71
|
+
'Login (save API key)',
|
|
72
|
+
'Logout (remove API key)',
|
|
73
|
+
'Back',
|
|
74
|
+
]);
|
|
75
|
+
if (settingChoice === 0) await loginAction({});
|
|
76
|
+
if (settingChoice === 1) {
|
|
77
|
+
const { logoutAction } = await import('./logout.js');
|
|
78
|
+
await logoutAction();
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
printErr(`\nError: ${error.message}\n`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.error('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.error('Goodbye!');
|
|
91
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { requireApiKey, loadConfig, getConfigPath } from '../config.js';
|
|
4
|
+
import { callTool, uploadFile } from '../api-client.js';
|
|
5
|
+
import { formatOutput, print, printErr } from '../output.js';
|
|
6
|
+
import { detectNamespaceFromPath } from '../namespace-detector.js';
|
|
7
|
+
import { promptText } from '../prompts.js';
|
|
8
|
+
|
|
9
|
+
export async function uploadAction(filePath, opts) {
|
|
10
|
+
if (!filePath) {
|
|
11
|
+
filePath = await promptText('File path: ');
|
|
12
|
+
if (!filePath) return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const apiKey = requireApiKey();
|
|
16
|
+
|
|
17
|
+
if (!opts.source) {
|
|
18
|
+
console.error('Error: --source is required (source language code, e.g. en)');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
if (!opts.target) {
|
|
22
|
+
console.error('Error: --target is required (target language code, e.g. es)');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(filePath)) {
|
|
27
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let namespace = opts.namespace;
|
|
32
|
+
if (!namespace) {
|
|
33
|
+
const config = loadConfig(getConfigPath());
|
|
34
|
+
namespace = config.defaultNamespace;
|
|
35
|
+
}
|
|
36
|
+
if (!namespace) {
|
|
37
|
+
const detection = detectNamespaceFromPath(path.resolve(filePath));
|
|
38
|
+
if (detection.suggestion && detection.confidence > 0.5) {
|
|
39
|
+
namespace = detection.suggestion;
|
|
40
|
+
printErr(`Auto-detected namespace: "${namespace}"`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (!namespace) {
|
|
44
|
+
namespace = await promptText('Namespace: ');
|
|
45
|
+
if (!namespace) {
|
|
46
|
+
console.error('Error: namespace is required');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
52
|
+
const fileName = path.basename(filePath);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
printErr(`Uploading ${fileName}...`);
|
|
56
|
+
|
|
57
|
+
const result = await uploadFile(
|
|
58
|
+
`/namespaces/${namespace}/translations/upload`,
|
|
59
|
+
{ sourceLocale: opts.source, targetLocale: opts.target },
|
|
60
|
+
{ name: 'file', content, fileName },
|
|
61
|
+
apiKey,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
print(formatOutput(result, {
|
|
65
|
+
json: opts.json,
|
|
66
|
+
template: (data) => {
|
|
67
|
+
const lines = ['Upload successful\n'];
|
|
68
|
+
lines.push(` Namespace: ${namespace}`);
|
|
69
|
+
lines.push(` File: ${fileName}`);
|
|
70
|
+
lines.push(` Languages: ${opts.source} -> ${opts.target}`);
|
|
71
|
+
if (data.pairsStored) lines.push(` Pairs stored: ${data.pairsStored}`);
|
|
72
|
+
if (data.pairsUpdated) lines.push(` Pairs updated: ${data.pairsUpdated}`);
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`Error: ${error.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function uploadsListAction(opts) {
|
|
83
|
+
const apiKey = requireApiKey();
|
|
84
|
+
|
|
85
|
+
if (!opts.namespace) {
|
|
86
|
+
console.error('Error: --namespace is required');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const args = { namespace: opts.namespace };
|
|
92
|
+
if (opts.source) args.sourceLocale = opts.source;
|
|
93
|
+
if (opts.target) args.targetLocale = opts.target;
|
|
94
|
+
|
|
95
|
+
const result = await callTool('list_uploaded_translations', args, apiKey);
|
|
96
|
+
|
|
97
|
+
print(formatOutput(result, {
|
|
98
|
+
json: opts.json,
|
|
99
|
+
template: (data) => {
|
|
100
|
+
const files = data.files || data.translations || data;
|
|
101
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
102
|
+
return `No uploaded translations found in namespace "${opts.namespace}"`;
|
|
103
|
+
}
|
|
104
|
+
const lines = [`Uploaded translations in "${opts.namespace}":\n`];
|
|
105
|
+
for (const f of files) {
|
|
106
|
+
lines.push(` ${f.fileName || f.name} | ${f.sourceLocale} -> ${f.targetLocale} | ${f.translationCount || f.pairs || '?'} pairs`);
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
}));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(`Error: ${error.message}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
export function getConfigPath() {
|
|
6
|
+
return path.join(os.homedir(), '.config', 'i18nagent', 'config.json');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function loadConfig(configPath) {
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(configPath)) return {};
|
|
12
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveConfig(configPath, updates) {
|
|
19
|
+
const existing = loadConfig(configPath);
|
|
20
|
+
const merged = { ...existing, ...updates };
|
|
21
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
22
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getApiKey(configPath) {
|
|
27
|
+
if (process.env.I18N_AGENT_API_KEY) {
|
|
28
|
+
return process.env.I18N_AGENT_API_KEY;
|
|
29
|
+
}
|
|
30
|
+
const config = loadConfig(configPath || getConfigPath());
|
|
31
|
+
return config.apiKey || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function requireApiKey(configPath) {
|
|
35
|
+
const key = getApiKey(configPath);
|
|
36
|
+
if (!key) {
|
|
37
|
+
console.error('Error: No API key found.\n');
|
|
38
|
+
console.error('Set your API key using one of:');
|
|
39
|
+
console.error(' i18nagent login');
|
|
40
|
+
console.error(' export I18N_AGENT_API_KEY=your-key-here\n');
|
|
41
|
+
console.error('Get your API key at: https://app.i18nagent.ai');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|