@red-hat-developer-hub/translations-cli 0.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/CHANGELOG.md +7 -0
- package/bin/translations-cli +34 -0
- package/dist/commands/clean.cjs.js +130 -0
- package/dist/commands/deploy.cjs.js +53 -0
- package/dist/commands/download.cjs.js +304 -0
- package/dist/commands/generate.cjs.js +1294 -0
- package/dist/commands/index.cjs.js +146 -0
- package/dist/commands/init.cjs.js +115 -0
- package/dist/commands/list.cjs.js +178 -0
- package/dist/commands/setupMemsource.cjs.js +338 -0
- package/dist/commands/status.cjs.js +40 -0
- package/dist/commands/sync.cjs.js +226 -0
- package/dist/commands/upload.cjs.js +506 -0
- package/dist/index.cjs.js +30 -0
- package/dist/lib/errors.cjs.js +36 -0
- package/dist/lib/i18n/analyzeStatus.cjs.js +79 -0
- package/dist/lib/i18n/config.cjs.js +256 -0
- package/dist/lib/i18n/deployTranslations.cjs.js +1213 -0
- package/dist/lib/i18n/extractKeys.cjs.js +418 -0
- package/dist/lib/i18n/formatReport.cjs.js +138 -0
- package/dist/lib/i18n/generateFiles.cjs.js +94 -0
- package/dist/lib/i18n/loadFile.cjs.js +93 -0
- package/dist/lib/i18n/mergeFiles.cjs.js +126 -0
- package/dist/lib/i18n/uploadCache.cjs.js +83 -0
- package/dist/lib/i18n/validateFile.cjs.js +189 -0
- package/dist/lib/paths.cjs.js +51 -0
- package/dist/lib/utils/exec.cjs.js +41 -0
- package/dist/lib/utils/translationUtils.cjs.js +33 -0
- package/dist/lib/version.cjs.js +38 -0
- package/package.json +65 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
var fs = require('fs-extra');
|
|
5
|
+
var chalk = require('chalk');
|
|
6
|
+
var validateFile = require('../lib/i18n/validateFile.cjs.js');
|
|
7
|
+
var config = require('../lib/i18n/config.cjs.js');
|
|
8
|
+
var uploadCache = require('../lib/i18n/uploadCache.cjs.js');
|
|
9
|
+
var exec = require('../lib/utils/exec.cjs.js');
|
|
10
|
+
var translationUtils = require('../lib/utils/translationUtils.cjs.js');
|
|
11
|
+
|
|
12
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
15
|
+
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
16
|
+
var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
|
|
17
|
+
|
|
18
|
+
function detectRepoName(repoPath) {
|
|
19
|
+
const targetPath = repoPath || process.cwd();
|
|
20
|
+
try {
|
|
21
|
+
const gitRepoUrl = exec.safeExecSyncOrThrow(
|
|
22
|
+
"git",
|
|
23
|
+
["config", "--get", "remote.origin.url"],
|
|
24
|
+
{
|
|
25
|
+
cwd: targetPath
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
if (gitRepoUrl) {
|
|
29
|
+
let repoName = gitRepoUrl.replace(/\.git$/, "");
|
|
30
|
+
const lastSlashIndex = repoName.lastIndexOf("/");
|
|
31
|
+
if (lastSlashIndex >= 0) {
|
|
32
|
+
repoName = repoName.substring(lastSlashIndex + 1);
|
|
33
|
+
}
|
|
34
|
+
if (repoName) {
|
|
35
|
+
return repoName;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
return path__default.default.basename(targetPath);
|
|
41
|
+
}
|
|
42
|
+
function extractSprintFromFilename(sourceFile) {
|
|
43
|
+
const sourceBasename = path__default.default.basename(sourceFile, path__default.default.extname(sourceFile));
|
|
44
|
+
let sprintMatch = sourceBasename.match(/^[a-z-]+-(s?\d+)$/i);
|
|
45
|
+
if (!sprintMatch) {
|
|
46
|
+
sprintMatch = sourceBasename.match(/-reference-(s?\d+)$/i);
|
|
47
|
+
}
|
|
48
|
+
return sprintMatch ? sprintMatch[1] : void 0;
|
|
49
|
+
}
|
|
50
|
+
function findGitRoot(sourceDir) {
|
|
51
|
+
let currentDir = sourceDir;
|
|
52
|
+
while (currentDir !== path__default.default.dirname(currentDir)) {
|
|
53
|
+
const gitDir = path__default.default.join(currentDir, ".git");
|
|
54
|
+
try {
|
|
55
|
+
if (fs__default.default.statSync(gitDir).isDirectory()) {
|
|
56
|
+
return currentDir;
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
currentDir = path__default.default.dirname(currentDir);
|
|
61
|
+
}
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
function formatIdentifier(sprintValue) {
|
|
65
|
+
if (!sprintValue) {
|
|
66
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
67
|
+
}
|
|
68
|
+
return sprintValue.startsWith("s") || sprintValue.startsWith("S") ? sprintValue.toLowerCase() : `s${sprintValue}`;
|
|
69
|
+
}
|
|
70
|
+
function generateUploadFileName(sourceFile, customName, sprint) {
|
|
71
|
+
if (customName) {
|
|
72
|
+
const ext2 = path__default.default.extname(sourceFile);
|
|
73
|
+
return customName.endsWith(ext2) ? customName : `${customName}${ext2}`;
|
|
74
|
+
}
|
|
75
|
+
const sprintValue = extractSprintFromFilename(sourceFile);
|
|
76
|
+
const sourceFileAbs = path__default.default.resolve(sourceFile);
|
|
77
|
+
const sourceDir = path__default.default.dirname(sourceFileAbs);
|
|
78
|
+
const repoRoot = findGitRoot(sourceDir);
|
|
79
|
+
const repoName = repoRoot ? detectRepoName(repoRoot) : detectRepoName();
|
|
80
|
+
const identifier = formatIdentifier(sprintValue);
|
|
81
|
+
const ext = path__default.default.extname(sourceFile);
|
|
82
|
+
return `${repoName}-${identifier}${ext}`;
|
|
83
|
+
}
|
|
84
|
+
async function prepareUploadFile(filePath, uploadFileName) {
|
|
85
|
+
const absoluteFilePath = path__default.default.resolve(filePath);
|
|
86
|
+
let fileToUpload = absoluteFilePath;
|
|
87
|
+
let tempFile = null;
|
|
88
|
+
if (uploadFileName && path__default.default.basename(absoluteFilePath) !== uploadFileName) {
|
|
89
|
+
const tempDir = path__default.default.join(path__default.default.dirname(absoluteFilePath), ".i18n-temp");
|
|
90
|
+
await fs__default.default.ensureDir(tempDir);
|
|
91
|
+
tempFile = path__default.default.join(tempDir, uploadFileName);
|
|
92
|
+
await fs__default.default.copy(absoluteFilePath, tempFile);
|
|
93
|
+
fileToUpload = tempFile;
|
|
94
|
+
console.log(
|
|
95
|
+
chalk__default.default.gray(` Created temporary file with name: ${uploadFileName}`)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return { fileToUpload, tempFile };
|
|
99
|
+
}
|
|
100
|
+
function validateMemsourcePrerequisites() {
|
|
101
|
+
if (!exec.commandExists("memsource")) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"memsource CLI not found. Please ensure memsource-cli is installed and ~/.memsourcerc is sourced."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function buildUploadCommandArgs(projectId, targetLanguages, fileToUpload) {
|
|
108
|
+
if (targetLanguages.length === 0) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Target languages are required. Please specify --target-languages or configure them in .i18n.config.json"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return [
|
|
114
|
+
"job",
|
|
115
|
+
"create",
|
|
116
|
+
"--project-id",
|
|
117
|
+
projectId,
|
|
118
|
+
"--target-langs",
|
|
119
|
+
...targetLanguages,
|
|
120
|
+
"--filenames",
|
|
121
|
+
fileToUpload
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
function extractErrorMessage(error) {
|
|
125
|
+
if (!(error instanceof Error)) {
|
|
126
|
+
return "Unknown error";
|
|
127
|
+
}
|
|
128
|
+
let errorMessage = error.message;
|
|
129
|
+
if ("stderr" in error && typeof error.stderr === "object") {
|
|
130
|
+
const stderr = error.stderr;
|
|
131
|
+
if (stderr) {
|
|
132
|
+
const stderrText = stderr.toString("utf-8");
|
|
133
|
+
if (stderrText) {
|
|
134
|
+
errorMessage = stderrText.trim();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return errorMessage;
|
|
139
|
+
}
|
|
140
|
+
async function countKeysFromFile(filePath) {
|
|
141
|
+
try {
|
|
142
|
+
const fileContent = await fs__default.default.readFile(filePath, "utf-8");
|
|
143
|
+
const data = JSON.parse(fileContent);
|
|
144
|
+
return translationUtils.countTranslationKeys(data);
|
|
145
|
+
} catch {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function cleanupTempFile(tempFile) {
|
|
150
|
+
try {
|
|
151
|
+
if (await fs__default.default.pathExists(tempFile)) {
|
|
152
|
+
await fs__default.default.remove(tempFile);
|
|
153
|
+
}
|
|
154
|
+
const tempDir = path__default.default.dirname(tempFile);
|
|
155
|
+
if (await fs__default.default.pathExists(tempDir)) {
|
|
156
|
+
const files = await fs__default.default.readdir(tempDir);
|
|
157
|
+
if (files.length === 0) {
|
|
158
|
+
await fs__default.default.remove(tempDir);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (cleanupError) {
|
|
162
|
+
console.warn(
|
|
163
|
+
chalk__default.default.yellow(
|
|
164
|
+
` Warning: Failed to clean up temporary file: ${cleanupError}`
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function executeMemsourceUpload(args, _fileToUpload) {
|
|
170
|
+
const output = exec.safeExecSyncOrThrow("memsource", args, {
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
stdio: "pipe",
|
|
173
|
+
env: { ...process.env }
|
|
174
|
+
});
|
|
175
|
+
const trimmed = output?.trim();
|
|
176
|
+
if (trimmed) {
|
|
177
|
+
console.log(chalk__default.default.gray(` ${trimmed}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function uploadWithMemsourceCLI(filePath, projectId, targetLanguages, uploadFileName) {
|
|
181
|
+
validateMemsourcePrerequisites();
|
|
182
|
+
const absoluteFilePath = path__default.default.resolve(filePath);
|
|
183
|
+
const { fileToUpload, tempFile } = await prepareUploadFile(
|
|
184
|
+
filePath,
|
|
185
|
+
uploadFileName
|
|
186
|
+
);
|
|
187
|
+
const args = buildUploadCommandArgs(projectId, targetLanguages, fileToUpload);
|
|
188
|
+
try {
|
|
189
|
+
await executeMemsourceUpload(args, fileToUpload);
|
|
190
|
+
const keyCount = await countKeysFromFile(fileToUpload);
|
|
191
|
+
return {
|
|
192
|
+
fileName: uploadFileName || path__default.default.basename(absoluteFilePath),
|
|
193
|
+
keyCount
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const errorMessage = extractErrorMessage(error);
|
|
197
|
+
throw new Error(`memsource CLI upload failed: ${errorMessage}`);
|
|
198
|
+
} finally {
|
|
199
|
+
if (tempFile) {
|
|
200
|
+
await cleanupTempFile(tempFile);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function extractStringOption(value) {
|
|
205
|
+
return value && typeof value === "string" ? value : void 0;
|
|
206
|
+
}
|
|
207
|
+
function validateTmsConfig(tmsUrl, tmsToken, projectId) {
|
|
208
|
+
const tmsUrlStr = extractStringOption(tmsUrl);
|
|
209
|
+
const tmsTokenStr = extractStringOption(tmsToken);
|
|
210
|
+
const projectIdStr = extractStringOption(projectId);
|
|
211
|
+
if (!tmsUrlStr || !tmsTokenStr || !projectIdStr) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return { tmsUrl: tmsUrlStr, tmsToken: tmsTokenStr, projectId: projectIdStr };
|
|
215
|
+
}
|
|
216
|
+
function displayMissingConfigError(tmsUrlStr, tmsTokenStr, projectIdStr) {
|
|
217
|
+
console.error(chalk__default.default.red("\u274C Missing required TMS configuration:"));
|
|
218
|
+
console.error("");
|
|
219
|
+
const missingConfigs = [
|
|
220
|
+
{
|
|
221
|
+
value: tmsUrlStr,
|
|
222
|
+
label: "TMS URL",
|
|
223
|
+
messages: [
|
|
224
|
+
" Set via: --tms-url <url> or I18N_TMS_URL or .i18n.config.json"
|
|
225
|
+
]
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
value: tmsTokenStr,
|
|
229
|
+
label: "TMS Token",
|
|
230
|
+
messages: [
|
|
231
|
+
" Primary: Source ~/.memsourcerc (sets MEMSOURCE_TOKEN)",
|
|
232
|
+
" Fallback: --tms-token <token> or I18N_TMS_TOKEN or ~/.i18n.auth.json"
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
value: projectIdStr,
|
|
237
|
+
label: "Project ID",
|
|
238
|
+
messages: [
|
|
239
|
+
" Set via: --project-id <id> or I18N_TMS_PROJECT_ID or .i18n.config.json"
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
missingConfigs.filter((item) => !item.value).forEach((item) => {
|
|
244
|
+
console.error(chalk__default.default.yellow(` \u2717 ${item.label}`));
|
|
245
|
+
item.messages.forEach((message) => {
|
|
246
|
+
console.error(chalk__default.default.gray(message));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
console.error("");
|
|
250
|
+
console.error(chalk__default.default.blue("\u{1F4CB} Quick Setup Guide:"));
|
|
251
|
+
console.error(chalk__default.default.gray(" 1. Run: translations-cli i18n init"));
|
|
252
|
+
console.error(chalk__default.default.gray(" This creates .i18n.config.json"));
|
|
253
|
+
console.error("");
|
|
254
|
+
console.error(
|
|
255
|
+
chalk__default.default.gray(" 2. Edit .i18n.config.json in your project root:")
|
|
256
|
+
);
|
|
257
|
+
console.error(
|
|
258
|
+
chalk__default.default.gray(
|
|
259
|
+
' - Add your TMS URL (e.g., "https://cloud.memsource.com/web")'
|
|
260
|
+
)
|
|
261
|
+
);
|
|
262
|
+
console.error(chalk__default.default.gray(" - Add your Project ID"));
|
|
263
|
+
console.error("");
|
|
264
|
+
console.error(
|
|
265
|
+
chalk__default.default.gray(" 3. Set up Memsource authentication (recommended):")
|
|
266
|
+
);
|
|
267
|
+
console.error(
|
|
268
|
+
chalk__default.default.gray(" - Run: translations-cli i18n setup-memsource")
|
|
269
|
+
);
|
|
270
|
+
console.error(
|
|
271
|
+
chalk__default.default.gray(
|
|
272
|
+
" - Or manually create ~/.memsourcerc following localization team instructions"
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
console.error(chalk__default.default.gray(" - Then source it: source ~/.memsourcerc"));
|
|
276
|
+
console.error("");
|
|
277
|
+
console.error(
|
|
278
|
+
chalk__default.default.gray(
|
|
279
|
+
" OR use ~/.i18n.auth.json as fallback (run init to create it)"
|
|
280
|
+
)
|
|
281
|
+
);
|
|
282
|
+
console.error("");
|
|
283
|
+
console.error(
|
|
284
|
+
chalk__default.default.gray(" See docs/i18n-commands.md for detailed instructions.")
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
async function validateSourceFile(sourceFile) {
|
|
288
|
+
if (!await fs__default.default.pathExists(sourceFile)) {
|
|
289
|
+
throw new Error(`Source file not found: ${sourceFile}`);
|
|
290
|
+
}
|
|
291
|
+
console.log(chalk__default.default.yellow(`\u{1F50D} Validating ${sourceFile}...`));
|
|
292
|
+
const isValid = await validateFile.validateTranslationFile(sourceFile);
|
|
293
|
+
if (!isValid) {
|
|
294
|
+
throw new Error(`Invalid translation file format: ${sourceFile}`);
|
|
295
|
+
}
|
|
296
|
+
console.log(chalk__default.default.green(`\u2705 Translation file is valid`));
|
|
297
|
+
}
|
|
298
|
+
async function checkFileChangeAndWarn(sourceFile, projectId, tmsUrl, finalUploadFileName, force, cachedEntry) {
|
|
299
|
+
if (force) {
|
|
300
|
+
console.log(
|
|
301
|
+
chalk__default.default.yellow(`\u26A0\uFE0F Force upload enabled - skipping cache check`)
|
|
302
|
+
);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
const fileChanged = await uploadCache.hasFileChanged(sourceFile, projectId, tmsUrl);
|
|
306
|
+
const sameFilename = cachedEntry?.uploadFileName === finalUploadFileName;
|
|
307
|
+
if (!fileChanged && cachedEntry && sameFilename) {
|
|
308
|
+
console.log(
|
|
309
|
+
chalk__default.default.yellow(
|
|
310
|
+
`\u2139\uFE0F File has not changed since last upload (${new Date(
|
|
311
|
+
cachedEntry.uploadedAt
|
|
312
|
+
).toLocaleString()})`
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
console.log(chalk__default.default.gray(` Upload filename: ${finalUploadFileName}`));
|
|
316
|
+
console.log(chalk__default.default.gray(` Skipping upload to avoid duplicate.`));
|
|
317
|
+
console.log(
|
|
318
|
+
chalk__default.default.gray(
|
|
319
|
+
` Use --force to upload anyway, or delete .i18n-cache to clear cache.`
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (!fileChanged && cachedEntry && !sameFilename) {
|
|
325
|
+
console.log(
|
|
326
|
+
chalk__default.default.yellow(
|
|
327
|
+
`\u26A0\uFE0F File content unchanged, but upload filename differs from last upload:`
|
|
328
|
+
)
|
|
329
|
+
);
|
|
330
|
+
console.log(
|
|
331
|
+
chalk__default.default.gray(` Last upload: ${cachedEntry.uploadFileName || "unknown"}`)
|
|
332
|
+
);
|
|
333
|
+
console.log(chalk__default.default.gray(` This upload: ${finalUploadFileName}`));
|
|
334
|
+
console.log(chalk__default.default.gray(` This will create a new job in Memsource.`));
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
async function uploadCommand(opts) {
|
|
339
|
+
console.log(chalk__default.default.blue("\u{1F4E4} Uploading translation reference files to TMS..."));
|
|
340
|
+
const config$1 = await config.loadI18nConfig();
|
|
341
|
+
const mergedOpts = await config.mergeConfigWithOptions(config$1, opts);
|
|
342
|
+
const {
|
|
343
|
+
tmsUrl,
|
|
344
|
+
tmsToken,
|
|
345
|
+
projectId,
|
|
346
|
+
sourceFile,
|
|
347
|
+
targetLanguages,
|
|
348
|
+
uploadFileName,
|
|
349
|
+
uploadFilename,
|
|
350
|
+
// Commander.js converts --upload-filename to uploadFilename
|
|
351
|
+
dryRun = false,
|
|
352
|
+
force = false
|
|
353
|
+
} = mergedOpts;
|
|
354
|
+
const finalUploadFileNameOption = uploadFilename || uploadFileName;
|
|
355
|
+
const tmsConfig = validateTmsConfig(tmsUrl, tmsToken, projectId);
|
|
356
|
+
if (!tmsConfig) {
|
|
357
|
+
const tmsUrlStr = extractStringOption(tmsUrl);
|
|
358
|
+
const tmsTokenStr = extractStringOption(tmsToken);
|
|
359
|
+
const projectIdStr = extractStringOption(projectId);
|
|
360
|
+
displayMissingConfigError(tmsUrlStr, tmsTokenStr, projectIdStr);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
const sourceFileStr = extractStringOption(sourceFile);
|
|
364
|
+
if (!sourceFileStr) {
|
|
365
|
+
console.error(chalk__default.default.red("\u274C Missing required option: --source-file"));
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
await validateSourceFile(sourceFileStr);
|
|
370
|
+
const finalUploadFileName = finalUploadFileNameOption && typeof finalUploadFileNameOption === "string" ? generateUploadFileName(sourceFileStr, finalUploadFileNameOption) : generateUploadFileName(sourceFileStr);
|
|
371
|
+
const cachedEntry = await uploadCache.getCachedUpload(
|
|
372
|
+
sourceFileStr,
|
|
373
|
+
tmsConfig.projectId,
|
|
374
|
+
tmsConfig.tmsUrl
|
|
375
|
+
);
|
|
376
|
+
const shouldProceed = await checkFileChangeAndWarn(
|
|
377
|
+
sourceFileStr,
|
|
378
|
+
tmsConfig.projectId,
|
|
379
|
+
tmsConfig.tmsUrl,
|
|
380
|
+
finalUploadFileName,
|
|
381
|
+
force,
|
|
382
|
+
cachedEntry
|
|
383
|
+
);
|
|
384
|
+
if (!shouldProceed) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (dryRun) {
|
|
388
|
+
simulateUpload(
|
|
389
|
+
tmsConfig.tmsUrl,
|
|
390
|
+
tmsConfig.projectId,
|
|
391
|
+
sourceFileStr,
|
|
392
|
+
finalUploadFileName,
|
|
393
|
+
targetLanguages,
|
|
394
|
+
cachedEntry
|
|
395
|
+
);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await performUpload(
|
|
399
|
+
tmsConfig.tmsUrl,
|
|
400
|
+
tmsConfig.tmsToken,
|
|
401
|
+
tmsConfig.projectId,
|
|
402
|
+
sourceFileStr,
|
|
403
|
+
finalUploadFileName,
|
|
404
|
+
targetLanguages,
|
|
405
|
+
force
|
|
406
|
+
);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error(chalk__default.default.red("\u274C Error uploading translation file:"), error);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function simulateUpload(tmsUrl, projectId, sourceFile, uploadFileName, targetLanguages, cachedEntry) {
|
|
413
|
+
console.log(
|
|
414
|
+
chalk__default.default.yellow("\u{1F50D} Dry run mode - showing what would be uploaded:")
|
|
415
|
+
);
|
|
416
|
+
console.log(chalk__default.default.gray(` TMS URL: ${tmsUrl}`));
|
|
417
|
+
console.log(chalk__default.default.gray(` Project ID: ${projectId}`));
|
|
418
|
+
console.log(chalk__default.default.gray(` Source file: ${sourceFile}`));
|
|
419
|
+
console.log(chalk__default.default.gray(` Upload filename: ${uploadFileName}`));
|
|
420
|
+
console.log(
|
|
421
|
+
chalk__default.default.gray(
|
|
422
|
+
` Target languages: ${targetLanguages || "All configured languages"}`
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
if (cachedEntry) {
|
|
426
|
+
console.log(
|
|
427
|
+
chalk__default.default.gray(
|
|
428
|
+
` Last uploaded: ${new Date(
|
|
429
|
+
cachedEntry.uploadedAt
|
|
430
|
+
).toLocaleString()}`
|
|
431
|
+
)
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function performUpload(tmsUrl, tmsToken, projectId, sourceFile, uploadFileName, targetLanguages, _force) {
|
|
436
|
+
if (!process.env.MEMSOURCE_TOKEN && !tmsToken) {
|
|
437
|
+
console.error(chalk__default.default.red("\u274C MEMSOURCE_TOKEN not found in environment"));
|
|
438
|
+
console.error(chalk__default.default.yellow(" Please source ~/.memsourcerc first:"));
|
|
439
|
+
console.error(chalk__default.default.gray(" source ~/.memsourcerc"));
|
|
440
|
+
console.error(chalk__default.default.gray(" Or set MEMSOURCE_TOKEN environment variable"));
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const config$1 = await config.loadI18nConfig();
|
|
444
|
+
console.log(
|
|
445
|
+
chalk__default.default.yellow(`\u{1F517} Using memsource CLI to upload to project ${projectId}...`)
|
|
446
|
+
);
|
|
447
|
+
let languages = [];
|
|
448
|
+
if (targetLanguages && typeof targetLanguages === "string") {
|
|
449
|
+
languages = targetLanguages.split(",").map((lang) => lang.trim()).filter(Boolean);
|
|
450
|
+
} else if (config$1.languages && Array.isArray(config$1.languages) && config$1.languages.length > 0) {
|
|
451
|
+
languages = config$1.languages;
|
|
452
|
+
console.log(
|
|
453
|
+
chalk__default.default.gray(
|
|
454
|
+
` Using target languages from config: ${languages.join(", ")}`
|
|
455
|
+
)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (languages.length === 0) {
|
|
459
|
+
console.error(chalk__default.default.red("\u274C Target languages are required"));
|
|
460
|
+
console.error(chalk__default.default.yellow(" Please specify one of:"));
|
|
461
|
+
console.error(
|
|
462
|
+
chalk__default.default.gray(" 1. --target-languages it (or other language codes)")
|
|
463
|
+
);
|
|
464
|
+
console.error(
|
|
465
|
+
chalk__default.default.gray(' 2. Add "languages": ["it"] to .i18n.config.json')
|
|
466
|
+
);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
console.log(chalk__default.default.yellow(`\u{1F4E4} Uploading ${sourceFile}...`));
|
|
470
|
+
console.log(chalk__default.default.gray(` Upload filename: ${uploadFileName}`));
|
|
471
|
+
if (languages.length > 0) {
|
|
472
|
+
console.log(chalk__default.default.gray(` Target languages: ${languages.join(", ")}`));
|
|
473
|
+
}
|
|
474
|
+
const uploadResult = await uploadWithMemsourceCLI(
|
|
475
|
+
sourceFile,
|
|
476
|
+
projectId,
|
|
477
|
+
languages,
|
|
478
|
+
uploadFileName
|
|
479
|
+
);
|
|
480
|
+
const fileContent = await fs__default.default.readFile(sourceFile, "utf-8");
|
|
481
|
+
let keyCount = uploadResult.keyCount;
|
|
482
|
+
if (keyCount === 0) {
|
|
483
|
+
try {
|
|
484
|
+
const data = JSON.parse(fileContent);
|
|
485
|
+
keyCount = translationUtils.countTranslationKeys(data);
|
|
486
|
+
} catch {
|
|
487
|
+
keyCount = 0;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
await uploadCache.saveUploadCache(
|
|
491
|
+
sourceFile,
|
|
492
|
+
projectId,
|
|
493
|
+
tmsUrl,
|
|
494
|
+
keyCount,
|
|
495
|
+
uploadFileName
|
|
496
|
+
);
|
|
497
|
+
console.log(chalk__default.default.green(`\u2705 Upload completed successfully!`));
|
|
498
|
+
console.log(chalk__default.default.gray(` File: ${uploadResult.fileName}`));
|
|
499
|
+
console.log(chalk__default.default.gray(` Keys: ${keyCount}`));
|
|
500
|
+
if (languages.length > 0) {
|
|
501
|
+
console.log(chalk__default.default.gray(` Target languages: ${languages.join(", ")}`));
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
exports.uploadCommand = uploadCommand;
|
|
506
|
+
//# sourceMappingURL=upload.cjs.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chalk = require('chalk');
|
|
4
|
+
var commander = require('commander');
|
|
5
|
+
var index = require('./commands/index.cjs.js');
|
|
6
|
+
var errors = require('./lib/errors.cjs.js');
|
|
7
|
+
var version = require('./lib/version.cjs.js');
|
|
8
|
+
|
|
9
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
10
|
+
|
|
11
|
+
var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
|
|
12
|
+
|
|
13
|
+
const main = (argv) => {
|
|
14
|
+
commander.program.name("translations-cli").version(version.version);
|
|
15
|
+
index.registerCommands(commander.program);
|
|
16
|
+
commander.program.on("command:*", () => {
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(chalk__default.default.red(`Invalid command: ${commander.program.args.join(" ")}`));
|
|
19
|
+
console.log();
|
|
20
|
+
commander.program.outputHelp();
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
23
|
+
commander.program.parse(argv);
|
|
24
|
+
};
|
|
25
|
+
process.on("unhandledRejection", (rejection) => {
|
|
26
|
+
const error = rejection instanceof Error ? rejection : new Error(`Unknown rejection: '${rejection}'`);
|
|
27
|
+
errors.exitWithError(error);
|
|
28
|
+
});
|
|
29
|
+
main(process.argv);
|
|
30
|
+
//# sourceMappingURL=index.cjs.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var chalk__default = /*#__PURE__*/_interopDefaultCompat(chalk);
|
|
8
|
+
|
|
9
|
+
class CustomError extends Error {
|
|
10
|
+
get name() {
|
|
11
|
+
return this.constructor.name;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class ExitCodeError extends CustomError {
|
|
15
|
+
code;
|
|
16
|
+
constructor(code, command) {
|
|
17
|
+
super(
|
|
18
|
+
command ? `Command '${command}' exited with code ${code}` : `Child exited with code ${code}`
|
|
19
|
+
);
|
|
20
|
+
this.code = code;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function exitWithError(error) {
|
|
24
|
+
const errorMessage = error instanceof ExitCodeError ? error.message : String(error);
|
|
25
|
+
const exitCode = error instanceof ExitCodeError ? error.code : 1;
|
|
26
|
+
process.stderr.write(`
|
|
27
|
+
${chalk__default.default.red(errorMessage)}
|
|
28
|
+
|
|
29
|
+
`);
|
|
30
|
+
process.exit(exitCode);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
exports.CustomError = CustomError;
|
|
34
|
+
exports.ExitCodeError = ExitCodeError;
|
|
35
|
+
exports.exitWithError = exitWithError;
|
|
36
|
+
//# sourceMappingURL=errors.cjs.js.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
var fs = require('fs-extra');
|
|
5
|
+
var glob = require('glob');
|
|
6
|
+
|
|
7
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
10
|
+
var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
11
|
+
var glob__default = /*#__PURE__*/_interopDefaultCompat(glob);
|
|
12
|
+
|
|
13
|
+
async function analyzeTranslationStatus(options) {
|
|
14
|
+
const { sourceDir, i18nDir, localesDir } = options;
|
|
15
|
+
const sourceFiles = glob__default.default.sync("**/*.{ts,tsx,js,jsx}", {
|
|
16
|
+
cwd: sourceDir,
|
|
17
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
|
|
18
|
+
});
|
|
19
|
+
const referenceFile = path__default.default.join(i18nDir, "reference.json");
|
|
20
|
+
let referenceKeys = [];
|
|
21
|
+
if (await fs__default.default.pathExists(referenceFile)) {
|
|
22
|
+
const referenceData = await fs__default.default.readJson(referenceFile);
|
|
23
|
+
referenceKeys = Object.keys(referenceData.translations || referenceData);
|
|
24
|
+
}
|
|
25
|
+
const languageFiles = await findLanguageFiles(localesDir);
|
|
26
|
+
const languages = languageFiles.map(
|
|
27
|
+
(file) => path__default.default.basename(file, path__default.default.extname(file))
|
|
28
|
+
);
|
|
29
|
+
const languageStats = {};
|
|
30
|
+
const extraKeys = {};
|
|
31
|
+
for (const languageFile of languageFiles) {
|
|
32
|
+
const language = path__default.default.basename(languageFile, path__default.default.extname(languageFile));
|
|
33
|
+
const fileData = await fs__default.default.readJson(languageFile);
|
|
34
|
+
const languageKeys = Object.keys(fileData.translations || fileData);
|
|
35
|
+
const translated = languageKeys.filter((key) => {
|
|
36
|
+
const value = (fileData.translations || fileData)[key];
|
|
37
|
+
return value && value.trim() !== "" && value !== key;
|
|
38
|
+
});
|
|
39
|
+
languageStats[language] = {
|
|
40
|
+
total: referenceKeys.length,
|
|
41
|
+
translated: translated.length,
|
|
42
|
+
completion: referenceKeys.length > 0 ? translated.length / referenceKeys.length * 100 : 0
|
|
43
|
+
};
|
|
44
|
+
extraKeys[language] = languageKeys.filter(
|
|
45
|
+
(key) => !referenceKeys.includes(key)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const missingKeys = referenceKeys.filter((key) => {
|
|
49
|
+
return !languages.some((lang) => {
|
|
50
|
+
const langKeys = Object.keys(languageStats[lang] || {});
|
|
51
|
+
return langKeys.includes(key);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
const totalTranslations = languages.reduce(
|
|
55
|
+
(sum, lang) => sum + (languageStats[lang]?.translated || 0),
|
|
56
|
+
0
|
|
57
|
+
);
|
|
58
|
+
const totalPossible = referenceKeys.length * languages.length;
|
|
59
|
+
const overallCompletion = totalPossible > 0 ? totalTranslations / totalPossible * 100 : 0;
|
|
60
|
+
return {
|
|
61
|
+
sourceFiles,
|
|
62
|
+
totalKeys: referenceKeys.length,
|
|
63
|
+
languages,
|
|
64
|
+
overallCompletion,
|
|
65
|
+
languageStats,
|
|
66
|
+
missingKeys,
|
|
67
|
+
extraKeys
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function findLanguageFiles(localesDir) {
|
|
71
|
+
if (!await fs__default.default.pathExists(localesDir)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const files = await fs__default.default.readdir(localesDir);
|
|
75
|
+
return files.filter((file) => file.endsWith(".json")).map((file) => path__default.default.join(localesDir, file));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
exports.analyzeTranslationStatus = analyzeTranslationStatus;
|
|
79
|
+
//# sourceMappingURL=analyzeStatus.cjs.js.map
|