@l10nmonster/cli 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ ## Getting started
2
+
3
+ ### Installation
4
+
5
+ ```sh
6
+ git clone git@github.com:l10nmonster/l10nmonster.git
7
+ cd l10nmonster
8
+ npm i
9
+ npm link
10
+ ```
11
+
12
+ Eventually there will be a binary for each platform, but this is still under heavy development.
13
+
14
+ ## Basic Operation
15
+
16
+ ```sh
17
+ l10n push
18
+ ```
19
+ It will re-read all your source content, figure out what needs translation, and send it to your translator.
20
+
21
+ ```sh
22
+ l10n status
23
+ ```
24
+ It will give you an overview of the state of translation of your project.
25
+
26
+ ```sh
27
+ l10n analyze
28
+ ```
29
+ It will analyze your sources and report insights like repeated content in different files and keys.
30
+
31
+ ```sh
32
+ l10n grandfather -q 80
33
+ ```
34
+ TODO: update to Grandfather provider. For all missing translations, it will extract translations from the current translated files and, if present, import them at the specified quality level. This assume translations are faithful translations of the current source (i.e. they didn't become outdated if the source has changed). This is probably only used at the beginning, in order to establish a baseline. Afterwards, translated files are always recreated from the TM and overwritten.
35
+
36
+ ```sh
37
+ l10n leverage -q 70 -u 60
38
+ ```
39
+ TODO: update to Repetition provider. For all missing translations, it will look into the TM for translations of the exact same source text but in different resources, while matching or not the string id (called respectively qualified and unqualified repetition). Since reusing translations may lead to a loss of quality, you can choose what quality levels to assign to your specific content. Leveraging can be done on a regular basis before pushing content to translation, or never if it's not safe to do so.
40
+
41
+ ```sh
42
+ l10n pull
43
+ ```
44
+ If there are pending translations, it will check if they became available and it will fetch them.
45
+
46
+ ```sh
47
+ l10n translate
48
+ ```
49
+ It will generate translated files based on the latest sources and translations in the TM.
50
+
51
+ ### Working files
52
+
53
+ L10n Monster maintains its working files in a hidden `.l10nmonster` directory at the root of the project. Working files are source-control friendly (json files with newlines) and can be checked in. On the other hand, they can also be destroyed and recreated on the fly if all you want to preserve is translations in your current files.
54
+
55
+ ## Demo
56
+
57
+ ![Demo screen](tty.gif)
58
+
59
+ ## Basic Configuration
60
+
61
+ At the root of your project there should be a file named `l10nmonster.cjs`. You can create it by hand, or you can use `l10n init` and use one of the configurators to get up and running in no time. Well, that's the plan, it's not implemented yet!
62
+
63
+ The configuration must export a class that once instantiated provides the following properties:
64
+
65
+ * `sourceLang`: the default source language
66
+ * `minimumQuality`: this is the minimum required quality for a string to be considered translated (anything below triggers a request to translate)
67
+ * `source`: a source adapter to read input resources from
68
+ * `resourceFilter`: a filter to process the specific resource format
69
+ * `translationProvider`: a connector to the translation vendor
70
+ * `target`: a target adapter to write translated resources to
71
+ * `adapters`, `filters`, `translators`: built-in helpers (see below)
72
+ * TODO: add the other properties that can be defined
73
+
74
+ ## Advanced CLI
75
+
76
+ The CLI support additional options to control its behavior:
77
+
78
+ * `-a, --arg <string>`: this is a user-defined argument that allows to customize the user config behavior
79
+ * `-v, --verbose`: output additional debug information
80
+
81
+ Some commands also allow additional options. For more information type `l10n help <command>`.
82
+
83
+ ## Advanced Configuration
84
+
85
+ There is also additional functionality in the configuration that can be useful, especially in environments with larger teams.
86
+
87
+ The the following properties can optionally be defined:
88
+
89
+ * `jobStore`: a durable persistence adapter to store translations
90
+ * `translationProvider`: this can also be a function that given a job request returns the desired vendor (e.g. `(job) => job.targetLang === 'piggy' ? piggyTranslator : xliffTranslator`)
91
+ * TODO: add the other properties that can be defined
92
+
93
+ ### JSON Job Store
94
+
95
+ ```js
96
+ this.jobStore = new stores.JsonJobStore({
97
+ jobsDir: 'translationJobs',
98
+ });
99
+ ```
100
+
101
+ The JSON job store is appropriate for small dev teams where all translations are managed by a single person and there little possibility of conflicts among members. Translation jobs are stored locally in JSON file in a specified folder.
102
+
103
+ * `jobsDir` is the directory containing translation jobs. It should be kept (e.g. checked into git) as it is needed to regenerate translated resources in a reliable way.
@@ -0,0 +1,689 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // l10nCommands.js
30
+ var l10nCommands_exports = {};
31
+ __export(l10nCommands_exports, {
32
+ builtInCmds: () => builtInCmds,
33
+ runL10nMonster: () => runL10nMonster
34
+ });
35
+ module.exports = __toCommonJS(l10nCommands_exports);
36
+ var path = __toESM(require("path"), 1);
37
+ var util = __toESM(require("node:util"), 1);
38
+ var winston = __toESM(require("winston"), 1);
39
+ var import_core8 = require("@l10nmonster/core");
40
+
41
+ // analyze.js
42
+ var import_fs = require("fs");
43
+ var import_core = require("@l10nmonster/core");
44
+
45
+ // shared.js
46
+ var import_helpers = require("@l10nmonster/helpers");
47
+ var consoleColor = {
48
+ red: "\x1B[31m",
49
+ yellow: "\x1B[33m",
50
+ green: "\x1B[32m",
51
+ reset: "\x1B[0m",
52
+ dim: "\x1B[2m",
53
+ bright: "\x1B[1m"
54
+ };
55
+ function printContent(contentPairs) {
56
+ for (const [prj, uc] of Object.entries(contentPairs)) {
57
+ console.log(`Project: ${prj}`);
58
+ for (const [rid, content] of Object.entries(uc)) {
59
+ console.log(` \u2023 ${rid}`);
60
+ for (const [sid, str] of Object.entries(content)) {
61
+ console.log(` \u2219 ${consoleColor.dim}${sid}:${consoleColor.reset} ${str.color}${str.confidence ? `[${str.confidence.toFixed(2)}] ` : ""}${sid === str.txt ? "\u2263" : str.txt}${consoleColor.reset}`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ function printRequest(req) {
67
+ const untranslatedContent = {};
68
+ for (const tu of req.tus) {
69
+ const prj = tu.prj || "default";
70
+ untranslatedContent[prj] ??= {};
71
+ untranslatedContent[prj][tu.rid] ??= {};
72
+ const confidence = 1;
73
+ untranslatedContent[prj][tu.rid][tu.sid] = {
74
+ confidence,
75
+ txt: import_helpers.utils.flattenNormalizedSourceV1(tu.nsrc)[0],
76
+ // eslint-disable-next-line no-nested-ternary
77
+ color: confidence <= 0.1 ? consoleColor.red : confidence <= 0.2 ? consoleColor.yellow : consoleColor.green
78
+ };
79
+ }
80
+ printContent(untranslatedContent);
81
+ }
82
+ function printResponse(req, res, showPair) {
83
+ const translations = res.tus.reduce((p, c) => (p[c.guid] = c.ntgt, p), {});
84
+ let matchedTranslations = 0;
85
+ const translatedContent = {};
86
+ for (const tu of req.tus) {
87
+ const prj = tu.prj || "default";
88
+ translatedContent[prj] ??= {};
89
+ translatedContent[prj][tu.rid] ??= {};
90
+ if (translations[tu.guid]) {
91
+ const key = showPair ? import_helpers.utils.flattenNormalizedSourceV1(tu.nsrc)[0] : tu.sid;
92
+ translatedContent[prj][tu.rid][key] = {
93
+ txt: import_helpers.utils.flattenNormalizedSourceV1(translations[tu.guid])[0],
94
+ color: consoleColor.green
95
+ };
96
+ matchedTranslations++;
97
+ }
98
+ }
99
+ if (req.tus.length !== res.tus.length || req.tus.length !== matchedTranslations) {
100
+ console.log(`${consoleColor.red}${req.tus.length} TU in request, ${res.tus.length} TU in response, ${matchedTranslations} matching translations${consoleColor.reset}`);
101
+ }
102
+ printContent(translatedContent);
103
+ }
104
+
105
+ // analyze.js
106
+ var analyze = class {
107
+ static help = {
108
+ description: "content reports and validation.",
109
+ arguments: [
110
+ ["[analyzer]", "name of the analyzer to run"],
111
+ ["[params...]", "optional parameters to the analyzer"]
112
+ ],
113
+ options: [
114
+ ["-l, --lang <language>", "target language to analyze (if TM analyzer)"],
115
+ ["--filter <filter>", "use the specified tu filter"],
116
+ ["--output <filename>", "filename to write the analysis to)"]
117
+ ]
118
+ };
119
+ static async action(monsterManager, options) {
120
+ try {
121
+ if (options.analyzer) {
122
+ const analysis = await (0, import_core.analyzeCmd)(monsterManager, options.analyzer, options.params, options.lang, options.filter);
123
+ const header = analysis.head;
124
+ if (options.output) {
125
+ const rows = header ? [header, ...analysis.body].map((row) => row.join(",")) : analysis.body;
126
+ rows.push("\n");
127
+ (0, import_fs.writeFileSync)(options.output, rows.join("\n"));
128
+ } else {
129
+ if (header) {
130
+ const groups = analysis.groupBy;
131
+ let previousGroup;
132
+ for (const row of analysis.body) {
133
+ const columns = row.map((col, idx) => [col, idx]);
134
+ if (groups) {
135
+ const currentGroup = columns.filter(([col, idx]) => groups.includes(header[idx]));
136
+ const currentGroupSmashed = currentGroup.map(([col, idx]) => col).join("|");
137
+ if (currentGroupSmashed !== previousGroup) {
138
+ previousGroup = currentGroupSmashed;
139
+ console.log(currentGroup.map(([col, idx]) => `${consoleColor.dim}${header[idx]}: ${consoleColor.reset}${consoleColor.bright}${col}${consoleColor.reset}`).join(" "));
140
+ }
141
+ }
142
+ const currentData = columns.filter(([col, idx]) => (!groups || !groups.includes(header[idx])) && col !== null && col !== void 0);
143
+ console.log(currentData.map(([col, idx]) => ` ${consoleColor.dim}${header[idx]}: ${consoleColor.reset}${col}`).join(""));
144
+ }
145
+ } else {
146
+ console.log(analysis.body.join("\n"));
147
+ }
148
+ }
149
+ } else {
150
+ console.log("Available analyzers:");
151
+ for (const [name, analyzer] of Object.entries(monsterManager.analyzers)) {
152
+ console.log(` ${typeof analyzer.prototype.processSegment === "function" ? "(src)" : " (tu)"} ${consoleColor.bright}${name} ${analyzer.helpParams ?? ""}${consoleColor.reset} ${analyzer.help}`);
153
+ }
154
+ }
155
+ } catch (e) {
156
+ console.error(`Failed to analyze: ${e.stack || e}`);
157
+ }
158
+ }
159
+ };
160
+
161
+ // job.js
162
+ var import_core2 = require("@l10nmonster/core");
163
+ var job = class {
164
+ static help = {
165
+ description: "show request/response/pairs of a job or push/delete jobs.",
166
+ arguments: [
167
+ ["<operation>", "operation to perform on job", ["req", "res", "pairs", "push", "delete"]]
168
+ ],
169
+ requiredOptions: [
170
+ ["-g, --jobGuid <guid>", "guid of job"]
171
+ ]
172
+ };
173
+ static async action(monsterManager, options) {
174
+ const op = options.operation;
175
+ const jobGuid = options.jobGuid;
176
+ if (op === "req") {
177
+ const req = await monsterManager.jobStore.getJobRequest(jobGuid);
178
+ if (req) {
179
+ console.log(`Showing request of job ${jobGuid} ${req.sourceLang} -> ${req.targetLang}`);
180
+ printRequest(req);
181
+ } else {
182
+ console.error("Could not fetch the specified job");
183
+ }
184
+ } else if (op === "res") {
185
+ const req = await monsterManager.jobStore.getJobRequest(jobGuid);
186
+ const res = await monsterManager.jobStore.getJob(jobGuid);
187
+ if (req && res) {
188
+ console.log(`Showing response of job ${jobGuid} ${req.sourceLang} -> ${req.targetLang} (${res.translationProvider}) ${res.status}`);
189
+ printResponse(req, res);
190
+ } else {
191
+ console.error("Could not fetch the specified job");
192
+ }
193
+ } else if (op === "pairs") {
194
+ const req = await monsterManager.jobStore.getJobRequest(jobGuid);
195
+ const res = await monsterManager.jobStore.getJob(jobGuid);
196
+ if (req && res) {
197
+ console.log(`Showing source-target pairs of job ${jobGuid} ${req.sourceLang} -> ${req.targetLang} (${res.translationProvider}) ${res.status}`);
198
+ printResponse(req, res, true);
199
+ } else {
200
+ console.error("Could not fetch the specified job");
201
+ }
202
+ } else if (op === "push") {
203
+ console.log(`Pushing job ${jobGuid}...`);
204
+ try {
205
+ const pushResponse = await (0, import_core2.jobPushCmd)(monsterManager, jobGuid);
206
+ console.log(`${pushResponse.num.toLocaleString()} translations units requested -> status: ${pushResponse.status}`);
207
+ } catch (e) {
208
+ console.error(`Failed to push job: ${e.stack ?? e}`);
209
+ }
210
+ } else if (op === "delete") {
211
+ console.log(`Deleting job ${jobGuid}...`);
212
+ try {
213
+ const res = await monsterManager.jobStore.getJob(jobGuid);
214
+ if (res) {
215
+ console.error(`Can only delete blocked/failed jobs. This job has status: ${res.status}`);
216
+ } else {
217
+ await monsterManager.jobStore.deleteJobRequest(jobGuid);
218
+ }
219
+ } catch (e) {
220
+ console.error(`Failed to push job: ${e.stack ?? e}`);
221
+ }
222
+ } else {
223
+ console.error(`Invalid operation: ${op}`);
224
+ }
225
+ }
226
+ };
227
+
228
+ // jobs.js
229
+ var import_core3 = require("@l10nmonster/core");
230
+ var jobs = class {
231
+ static help = {
232
+ description: "unfinished jobs status.",
233
+ options: [
234
+ ["-l, --lang <language>", "only get jobs for the target language"]
235
+ ]
236
+ };
237
+ static async action(monsterManager, options) {
238
+ const limitToLang = options.lang;
239
+ const jobs2 = await (0, import_core3.jobsCmd)(monsterManager, { limitToLang });
240
+ for (const [lang, jobManifests] of Object.entries(jobs2)) {
241
+ if (jobManifests.length > 0) {
242
+ console.log(`Target language ${consoleColor.bright}${lang}${consoleColor.reset}:`);
243
+ for (const mf of jobManifests) {
244
+ const numUnits = mf.inflight?.length ?? mf.tus?.length ?? 0;
245
+ const lastModified = new Date(mf.updatedAt);
246
+ console.log(` Job ${mf.jobGuid}: status ${consoleColor.bright}${mf.status}${consoleColor.reset} ${numUnits.toLocaleString()} ${mf.sourceLang} units with ${mf.translationProvider} - ${lastModified.toDateString()} ${lastModified.toLocaleTimeString()}`);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ };
252
+
253
+ // monster.js
254
+ var monster = class {
255
+ static help = {
256
+ description: "test configuration and warm up caches",
257
+ options: [
258
+ ["-l, --lang <language>", "target languages to warm up"]
259
+ ]
260
+ };
261
+ static async action(monsterManager, options) {
262
+ console.log(
263
+ " _.------. .----.__\n / \\_. ._ /---.__ \\\n | O O |\\\\___ //| / `\\ |\n | .vvvvv. | ) `(/ | | o o \\|\n / | | |/ \\ | /| ./| .vvvvv. |\\\n / `^^^^^' / _ _ `|_ || / /| | | | \\\n ./ /| | O) O ) \\|| //' | `^vvvv' |/\\\\\n / / | \\ / | | ~ \\ | \\\\\n \\ / | / \\ Y /' | \\ | | ~\n `' | _ | `._/' | | \\ 7 /\n _.-'-' `-'-'| |`-._/ / \\ _ / . |\n __.-' \\ \\ . / \\_. \\ -|_/\\/ `--.|_\n --' \\ \\ | / | | `-\n \\uU \\UU/ | / :F_P:"
264
+ );
265
+ console.time("Initialization time");
266
+ const resourceHandles = await monsterManager.rm.getResourceHandles();
267
+ const targetLangs = monsterManager.getTargetLangs(options.lang);
268
+ console.log(`Resources: ${resourceHandles.length}`);
269
+ console.log(`Possible languages: ${targetLangs.join(", ")}`);
270
+ console.log("Translation Memories:");
271
+ const availableLangPairs = (await monsterManager.jobStore.getAvailableLangPairs()).sort();
272
+ for (const [sourceLang, targetLang] of availableLangPairs) {
273
+ const tm = await monsterManager.tmm.getTM(sourceLang, targetLang);
274
+ console.log(` - ${sourceLang} / ${targetLang} (${tm.guids.length} entries)`);
275
+ }
276
+ console.timeEnd("Initialization time");
277
+ const printCapabilities = (cap) => `${Object.entries(cap).map(([cmd, available]) => `${available ? consoleColor.green : consoleColor.red}${cmd}`).join(" ")}${consoleColor.reset}`;
278
+ console.log(`
279
+ Your config allows the following commands: ${printCapabilities(monsterManager.capabilities)}`);
280
+ if (Object.keys(monsterManager.capabilitiesByChannel).length > 1) {
281
+ Object.entries(monsterManager.capabilitiesByChannel).forEach(([channel, cap]) => console.log(` - ${channel}: ${printCapabilities(cap)}`));
282
+ }
283
+ }
284
+ };
285
+
286
+ // pull.js
287
+ var import_core4 = require("@l10nmonster/core");
288
+ var pull = class {
289
+ static help = {
290
+ description: "receive outstanding translation jobs.",
291
+ options: [
292
+ ["--partial", "commit partial deliveries"],
293
+ ["-l, --lang <language>", "only get jobs for the target language"]
294
+ ]
295
+ };
296
+ static async action(monsterManager, options) {
297
+ const limitToLang = options.lang;
298
+ const partial = options.partial;
299
+ console.log(`Pulling pending translations...`);
300
+ const stats = await (0, import_core4.pullCmd)(monsterManager, { limitToLang, partial });
301
+ console.log(`Checked ${stats.numPendingJobs.toLocaleString()} pending jobs, ${stats.doneJobs.toLocaleString()} done jobs, ${stats.newPendingJobs.toLocaleString()} pending jobs created, ${stats.translatedStrings.toLocaleString()} translated strings found`);
302
+ }
303
+ };
304
+
305
+ // push.js
306
+ var import_core5 = require("@l10nmonster/core");
307
+ var push = class {
308
+ static help = {
309
+ description: "push source content upstream (send to translation).",
310
+ options: [
311
+ ["-l, --lang <language>", "target language to push"],
312
+ ["--filter <filter>", "use the specified tu filter"],
313
+ ["--driver <untranslated|source|tm|job:jobGuid>", "driver of translations need to be pushed (default: untranslated)"],
314
+ ["--leverage", "eliminate internal repetitions from untranslated driver"],
315
+ ["--refresh", "refresh existing translations without requesting new ones"],
316
+ ["--provider <name,...>", "use the specified translation providers"],
317
+ ["--instructions <instructions>", "send the specified translation instructions"],
318
+ ["--dryrun", "simulate translating and compare with existing translations"]
319
+ ]
320
+ };
321
+ static async action(monsterManager, options) {
322
+ const limitToLang = options.lang;
323
+ const tuFilter = options.filter;
324
+ const driverOption = options.driver ?? "untranslated";
325
+ const driver = {};
326
+ if (driverOption.indexOf("job:") === 0) {
327
+ driver.jobGuid = driverOption.split(":")[1];
328
+ } else if (["untranslated", "source", "tm"].includes(driverOption)) {
329
+ driver[driverOption] = true;
330
+ } else {
331
+ throw `invalid ${driverOption} driver`;
332
+ }
333
+ const refresh = options.refresh;
334
+ const leverage = options.leverage;
335
+ const dryRun = options.dryrun;
336
+ const instructions = options.instructions;
337
+ console.log(`Pushing content upstream...${dryRun ? " (dry run)" : ""}`);
338
+ try {
339
+ if (dryRun) {
340
+ const status2 = await (0, import_core5.pushCmd)(monsterManager, { limitToLang, tuFilter, driver, refresh, leverage, dryRun, instructions });
341
+ for (const langStatus of status2) {
342
+ console.log(`
343
+ Dry run of ${langStatus.sourceLang} -> ${langStatus.targetLang} push:`);
344
+ printRequest(langStatus);
345
+ }
346
+ } else {
347
+ const providerList = (options.provider ?? "default").split(",");
348
+ for (const provider of providerList) {
349
+ const translationProviderName = provider.toLowerCase() === "default" ? void 0 : provider;
350
+ const status2 = await (0, import_core5.pushCmd)(monsterManager, { limitToLang, tuFilter, driver, refresh, translationProviderName, leverage, dryRun, instructions });
351
+ if (status2.length > 0) {
352
+ for (const ls of status2) {
353
+ if (ls.minimumJobSize === void 0) {
354
+ console.log(`job ${ls.jobGuid} with ${ls.num.toLocaleString()} translations received for language ${consoleColor.bright}${ls.targetLang}${consoleColor.reset} from provider ${consoleColor.bright}${ls.provider}${consoleColor.reset} -> status: ${consoleColor.bright}${ls.status}${consoleColor.reset}`);
355
+ } else {
356
+ console.log(`${ls.num.toLocaleString()} translations units for language ${ls.targetLang} not sent to provider ${consoleColor.bright}${ls.provider}${consoleColor.reset} because you need at least ${ls.minimumJobSize}`);
357
+ }
358
+ }
359
+ } else {
360
+ console.log("Nothing to push!");
361
+ break;
362
+ }
363
+ }
364
+ }
365
+ } catch (e) {
366
+ console.error(`Failed to push: ${e.stack || e}`);
367
+ }
368
+ }
369
+ };
370
+
371
+ // snap.js
372
+ var import_core6 = require("@l10nmonster/core");
373
+ var snap = class {
374
+ static help = {
375
+ description: "commits a snapshot of sources in normalized format.",
376
+ options: [
377
+ ["--maxSegments <number>", "threshold to break up snapshots into chunks"]
378
+ ]
379
+ };
380
+ static async action(monsterManager, options) {
381
+ console.log(`Taking a snapshot of sources...`);
382
+ const numSources = await (0, import_core6.snapCmd)(monsterManager, options);
383
+ console.log(`${numSources} sources committed`);
384
+ }
385
+ };
386
+
387
+ // status.js
388
+ var import_fs2 = require("fs");
389
+ var import_core7 = require("@l10nmonster/core");
390
+ function computeTotals(totals, partial) {
391
+ for (const [k, v] of Object.entries(partial)) {
392
+ if (typeof v === "object") {
393
+ totals[k] ??= {};
394
+ computeTotals(totals[k], v);
395
+ } else {
396
+ totals[k] ??= 0;
397
+ totals[k] += v;
398
+ }
399
+ }
400
+ }
401
+ function printLeverage(leverage, detailed) {
402
+ const totalStrings = leverage.translated + leverage.pending + leverage.untranslated + leverage.internalRepetitions;
403
+ detailed && console.log(` - total strings for target language: ${totalStrings.toLocaleString()} (${leverage.translatedWords.toLocaleString()} translated words)`);
404
+ for (const [q, num] of Object.entries(leverage.translatedByQ).sort((a, b) => b[1] - a[1])) {
405
+ detailed && console.log(` - translated strings @ quality ${q}: ${num.toLocaleString()}`);
406
+ }
407
+ leverage.pending && console.log(` - strings pending translation: ${leverage.pending.toLocaleString()} (${leverage.pendingWords.toLocaleString()} words)`);
408
+ leverage.untranslated && console.log(` - untranslated unique strings: ${leverage.untranslated.toLocaleString()} (${leverage.untranslatedChars.toLocaleString()} chars - ${leverage.untranslatedWords.toLocaleString()} words - $${(leverage.untranslatedWords * 0.2).toFixed(2)})`);
409
+ leverage.internalRepetitions && console.log(` - untranslated repeated strings: ${leverage.internalRepetitions.toLocaleString()} (${leverage.internalRepetitionWords.toLocaleString()} words)`);
410
+ }
411
+ var status = class {
412
+ static help = {
413
+ description: "translation status of content.",
414
+ options: [
415
+ ["-l, --lang <language>", "only get status of target language"],
416
+ ["-a, --all", "show information for all projects, not just untranslated ones"],
417
+ ["--output <filename>", "write status to the specified file"]
418
+ ]
419
+ };
420
+ static async action(monsterManager, options) {
421
+ const limitToLang = options.lang;
422
+ const all = Boolean(options.all);
423
+ const output = options.output;
424
+ const status2 = await (0, import_core7.statusCmd)(monsterManager, { limitToLang });
425
+ if (output) {
426
+ (0, import_fs2.writeFileSync)(output, JSON.stringify(status2, null, " "), "utf8");
427
+ } else {
428
+ console.log(`${consoleColor.reset}${status2.numSources.toLocaleString()} translatable resources`);
429
+ for (const [lang, langStatus] of Object.entries(status2.lang)) {
430
+ console.log(`
431
+ ${consoleColor.bright}Language ${lang}${consoleColor.reset} (minimum quality: ${langStatus.leverage.minimumQuality})`);
432
+ const totals = {};
433
+ const prjLeverage = Object.entries(langStatus.leverage.prjLeverage).sort((a, b) => a[0] > b[0] ? 1 : -1);
434
+ for (const [prj, leverage] of prjLeverage) {
435
+ computeTotals(totals, leverage);
436
+ const untranslated = leverage.pending + leverage.untranslated + leverage.internalRepetitions;
437
+ if (leverage.translated + untranslated > 0) {
438
+ (all || untranslated > 0) && console.log(` Project: ${consoleColor.bright}${prj}${consoleColor.reset}`);
439
+ printLeverage(leverage, all);
440
+ }
441
+ }
442
+ if (prjLeverage.length > 1) {
443
+ console.log(` Total:`);
444
+ printLeverage(totals, true);
445
+ }
446
+ }
447
+ }
448
+ return status2;
449
+ }
450
+ };
451
+
452
+ // tmexport.js
453
+ var fs = __toESM(require("fs/promises"), 1);
454
+ var import_helpers2 = require("@l10nmonster/helpers");
455
+ var tmexport = class {
456
+ static help = {
457
+ description: "export translation memory as a json job.",
458
+ options: [
459
+ ["-l, --lang <language>", "target language to export"],
460
+ ["--filter <filter>", "use the specified tu filter"],
461
+ ["--prjsplit", "split target files by project"]
462
+ ]
463
+ };
464
+ static async action(monsterManager, options) {
465
+ const prjsplit = options.prjsplit;
466
+ console.log(`Exporting TM for ${consoleColor.bright}${options.lang ? options.lang : "all languages"}${consoleColor.reset}...`);
467
+ let tuFilterFunction;
468
+ if (options.filter) {
469
+ tuFilterFunction = monsterManager.tuFilters[import_helpers2.utils.fixCaseInsensitiveKey(monsterManager.tuFilters, options.filter)];
470
+ if (!tuFilterFunction) {
471
+ throw `Couldn't find ${options.filter} tu filter`;
472
+ }
473
+ }
474
+ const files = [];
475
+ const desiredTargetLangs = new Set(monsterManager.getTargetLangs(options.lang));
476
+ const availableLangPairs = (await monsterManager.jobStore.getAvailableLangPairs()).filter((pair) => desiredTargetLangs.has(pair[1]));
477
+ for (const [sourceLang, targetLang] of availableLangPairs) {
478
+ const tusByPrj = {};
479
+ const tm = await monsterManager.tmm.getTM(sourceLang, targetLang);
480
+ tm.guids.forEach((guid) => {
481
+ const tu = tm.getEntryByGuid(guid);
482
+ if (!tuFilterFunction || tuFilterFunction(tu)) {
483
+ if (!prjsplit || !l10nmonster.prj || l10nmonster.prj.includes(tu.prj)) {
484
+ const prj = prjsplit && tu?.prj || "default";
485
+ tusByPrj[prj] ??= [];
486
+ tusByPrj[prj].push(tu);
487
+ }
488
+ }
489
+ });
490
+ for (const [prj, tus] of Object.entries(tusByPrj)) {
491
+ const jobGuid = `tmexport_${prjsplit ? `${prj}_` : ""}${sourceLang}_${targetLang}`;
492
+ const jobReq = {
493
+ sourceLang,
494
+ targetLang,
495
+ jobGuid,
496
+ updatedAt: (l10nmonster.regression ? /* @__PURE__ */ new Date("2022-05-30T00:00:00.000Z") : /* @__PURE__ */ new Date()).toISOString(),
497
+ status: "created",
498
+ tus: []
499
+ };
500
+ const jobRes = {
501
+ ...jobReq,
502
+ translationProvider: "TMExport",
503
+ status: "done",
504
+ tus: []
505
+ };
506
+ for (const tu of tus) {
507
+ try {
508
+ jobReq.tus.push(l10nmonster.TU.asSource(tu));
509
+ } catch (e) {
510
+ l10nmonster.logger.info(e.stack ?? e);
511
+ }
512
+ if (tu.inflight) {
513
+ l10nmonster.logger.info(`Warning: in-flight translation unit ${tu.guid} can't be exported`);
514
+ } else {
515
+ try {
516
+ jobRes.tus.push(l10nmonster.TU.asTarget(tu));
517
+ } catch (e) {
518
+ l10nmonster.logger.info(e.stack ?? e);
519
+ }
520
+ }
521
+ }
522
+ const filename = `TMExport_${sourceLang}_${targetLang}_job_${jobGuid}`;
523
+ await fs.writeFile(`${filename}-req.json`, JSON.stringify(jobReq, null, " "), "utf8");
524
+ await fs.writeFile(`${filename}-done.json`, JSON.stringify(jobRes, null, " "), "utf8");
525
+ files.push(filename);
526
+ }
527
+ }
528
+ console.log(`Generated files: ${files.join(", ")}`);
529
+ }
530
+ };
531
+
532
+ // translate.js
533
+ function computeDelta(currentTranslations, newTranslations) {
534
+ const delta = [];
535
+ const newGstrMap = Object.fromEntries(newTranslations.segments.map((seg) => [seg.sid, seg.gstr]));
536
+ const seenIds = /* @__PURE__ */ new Set();
537
+ for (const seg of currentTranslations.segments) {
538
+ seenIds.add(seg.sid);
539
+ const newGstr = newGstrMap[seg.sid];
540
+ if (seg.gstr !== newGstr) {
541
+ delta.push({ id: seg.sid, l: seg.gstr, r: newGstr });
542
+ }
543
+ }
544
+ newTranslations.segments.filter((seg) => !seenIds.has(seg.sid)).forEach((seg) => delta.push({ id: seg.sid, r: seg.gstr }));
545
+ return delta;
546
+ }
547
+ async function compareToExisting(monsterManager, resHandle, targetLang, translatedRes) {
548
+ let currentTranslations;
549
+ let delta;
550
+ const channel = monsterManager.rm.getChannel(resHandle.channel);
551
+ try {
552
+ currentTranslations = await channel.getExistingTranslatedResource(resHandle, targetLang);
553
+ if (translatedRes) {
554
+ const newTranslations = await channel.makeResourceHandleFromObject(resHandle).loadResourceFromRaw(translatedRes, { isSource: false });
555
+ delta = computeDelta(currentTranslations, newTranslations);
556
+ }
557
+ } catch (e) {
558
+ l10nmonster.logger.verbose(`Couldn't fetch ${targetLang} resource for ${resHandle.channel}:${resHandle.id}: ${e.stack ?? e}`);
559
+ }
560
+ const bundleChanges = currentTranslations ? translatedRes ? delta.length > 0 ? "changed" : "unchanged" : "deleted" : translatedRes ? "new" : "void";
561
+ return [bundleChanges, delta];
562
+ }
563
+ function printChanges(resHandle, targetLang, bundleChanges, delta) {
564
+ if (bundleChanges === "changed") {
565
+ console.log(`
566
+ ${consoleColor.yellow}Changed translated bundle ${resHandle.channel}:${resHandle.id} for ${targetLang}${consoleColor.reset}`);
567
+ for (const change of delta) {
568
+ change.l !== void 0 && console.log(`${consoleColor.red}- ${change.id}: ${change.l}${consoleColor.reset}`);
569
+ change.r !== void 0 && console.log(`${consoleColor.green}+ ${change.id}: ${change.r}${consoleColor.reset}`);
570
+ }
571
+ } else if (bundleChanges === "new") {
572
+ console.log(`
573
+ ${consoleColor.green}New translated bundle ${resHandle.channel}:${resHandle.id} for ${targetLang}${consoleColor.reset}`);
574
+ } else if (bundleChanges === "deleted") {
575
+ console.log(`
576
+ ${consoleColor.green}Deleted translated bundle ${resHandle.channel}:${resHandle.id} for ${targetLang}${consoleColor.reset}`);
577
+ }
578
+ }
579
+ function printSummary(response) {
580
+ console.log("Translation summary:");
581
+ for (const [lang, langStatus] of Object.entries(response.lang)) {
582
+ const summary = {};
583
+ for (const resourceStatus of langStatus.resourceStatus) {
584
+ summary[resourceStatus.status] = (summary[resourceStatus.status] ?? 0) + 1;
585
+ }
586
+ console.log(` - ${lang}: ${Object.entries(summary).sort().map(([k, v]) => `${k}(${v})`).join(", ")}`);
587
+ }
588
+ }
589
+ var translate = class {
590
+ static help = {
591
+ description: "generate translated resources based on latest source and translations.",
592
+ arguments: [
593
+ ["[mode]", "commit all/changed/none of the translations", ["all", "delta", "dryrun"]]
594
+ ],
595
+ options: [
596
+ ["-l, --lang <language>", "target language to translate"]
597
+ ]
598
+ };
599
+ static async action(monsterManager, options) {
600
+ const mode = (options.mode ?? "all").toLowerCase();
601
+ console.log(`Generating translated resources for ${consoleColor.bright}${options.lang ? options.lang : "all languages"}${consoleColor.reset}... (${mode} mode)`);
602
+ const response = { lang: {} };
603
+ const targetLangs = monsterManager.getTargetLangs(options.lang);
604
+ const allResources = await monsterManager.rm.getAllResources({ keepRaw: true });
605
+ for await (const resHandle of allResources) {
606
+ for (const targetLang of targetLangs) {
607
+ if (resHandle.targetLangs.includes(targetLang) && (l10nmonster.prj === void 0 || l10nmonster.prj.includes(resHandle.prj))) {
608
+ const resourceStatus = { id: resHandle.id };
609
+ const tm = await monsterManager.tmm.getTM(resHandle.sourceLang, targetLang);
610
+ const translatedRes = await resHandle.generateTranslatedRawResource(tm);
611
+ let bundleChanges, delta;
612
+ if (mode === "delta" || mode === "dryrun") {
613
+ [bundleChanges, delta] = await compareToExisting(monsterManager, resHandle, targetLang, translatedRes);
614
+ resourceStatus.status = bundleChanges;
615
+ resourceStatus.delta = delta;
616
+ }
617
+ if (mode === "dryrun") {
618
+ printChanges(resHandle, targetLang, bundleChanges, delta);
619
+ } else if (mode === "all" || bundleChanges === "changed" || bundleChanges === "new" || bundleChanges === "deleted") {
620
+ const translatedResourceId = await monsterManager.rm.getChannel(resHandle.channel).commitTranslatedResource(targetLang, resHandle.id, translatedRes);
621
+ resourceStatus.status = translatedRes === null ? "deleted" : "generated";
622
+ resourceStatus.translatedId = translatedResourceId;
623
+ l10nmonster.logger.verbose(`Committed translated resource: ${translatedResourceId}`);
624
+ } else {
625
+ l10nmonster.logger.verbose(`Delta mode skipped translation of bundle ${resHandle.channel}:${resHandle.id} for ${targetLang}`);
626
+ resourceStatus.status = "skipped";
627
+ }
628
+ response.lang[targetLang] ??= { resourceStatus: [] };
629
+ response.lang[targetLang].resourceStatus.push(resourceStatus);
630
+ }
631
+ }
632
+ }
633
+ printSummary(response);
634
+ return response;
635
+ }
636
+ };
637
+
638
+ // l10nCommands.js
639
+ function createLogger2(verboseOption) {
640
+ const verboseLevel = verboseOption === void 0 || verboseOption === 0 ? "error" : (
641
+ // eslint-disable-next-line no-nested-ternary
642
+ verboseOption === 1 ? "warn" : verboseOption === true || verboseOption === 2 ? "info" : "verbose"
643
+ );
644
+ return winston.createLogger({
645
+ level: verboseLevel,
646
+ transports: [
647
+ new winston.transports.Console({
648
+ format: winston.format.combine(
649
+ winston.format.ms(),
650
+ winston.format.timestamp(),
651
+ winston.format.printf(({ level, message, timestamp, ms }) => `${consoleColor.green}${timestamp.substr(11, 12)} (${ms}) [${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB] ${level}: ${typeof message === "string" ? message : util.inspect(message)}${consoleColor.reset}`)
652
+ )
653
+ })
654
+ ]
655
+ });
656
+ }
657
+ function createHandler(mm, globalOptions, action) {
658
+ return (opts) => action(mm, { ...globalOptions, ...opts });
659
+ }
660
+ var builtInCmds = [analyze, job, jobs, monster, pull, push, snap, status, tmexport, translate];
661
+ async function runL10nMonster(relativePath, globalOptions, cb) {
662
+ const configPath = path.resolve(".", relativePath);
663
+ global.l10nmonster ??= {};
664
+ l10nmonster.logger = createLogger2(globalOptions.verbose);
665
+ l10nmonster.env = process.env;
666
+ const mm = await (0, import_core8.createMonsterManager)(configPath, globalOptions);
667
+ const l10n = {
668
+ withMonsterManager: (cb2) => cb2(mm)
669
+ };
670
+ [...builtInCmds, ...mm.extensionCmds].forEach((Cmd) => l10n[Cmd.name] = createHandler(mm, globalOptions, Cmd.action));
671
+ let response;
672
+ try {
673
+ response = await cb(l10n);
674
+ } catch (e) {
675
+ response = { error: e.stack ?? e };
676
+ } finally {
677
+ mm && await mm.shutdown();
678
+ }
679
+ if (response?.error) {
680
+ throw response.error;
681
+ }
682
+ return response;
683
+ }
684
+ // Annotate the CommonJS export names for ESM import in node:
685
+ 0 && (module.exports = {
686
+ builtInCmds,
687
+ runL10nMonster
688
+ });
689
+ //# sourceMappingURL=l10nCommands.cjs.map
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@l10nmonster/cli",
3
+ "version": "1.0.0",
4
+ "description": "Continuous localization for the rest of us",
5
+ "bin": {
6
+ "l10n": "out/l10n.cjs"
7
+ },
8
+ "exports": {
9
+ "import": "./l10nCommands.js",
10
+ "require": "./out/l10nCommands.cjs"
11
+ },
12
+ "main": "out/l10nCommands.cjs",
13
+ "type": "module",
14
+ "scripts": {
15
+ "esbuild": "esbuild l10nCommands.js --bundle --external:@l10nmonster/core --external:@l10nmonster/helpers --external:commander --external:winston --outfile=out/l10nCommands.cjs --format=cjs --platform=node --target=node18 --sourcemap",
16
+ "esbuild-watch": "npm run esbuild -- --watch",
17
+ "package": "pkg -t node18-macos-x64 --out-path bin out/l10n.cjs"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/l10nmonster/l10nmonster.git"
22
+ },
23
+ "keywords": [
24
+ "translation",
25
+ "localization",
26
+ "l10n",
27
+ "globalization",
28
+ "translation-files"
29
+ ],
30
+ "author": "Diego Lagunas",
31
+ "license": "MIT",
32
+ "bugs": {
33
+ "url": "https://github.com/l10nmonster/l10nmonster/issues"
34
+ },
35
+ "homepage": "https://github.com/l10nmonster/l10nmonster#readme",
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "dependencies": {
40
+ "@l10nmonster/core": "^1",
41
+ "commander": "^10",
42
+ "winston": "^3.7.2"
43
+ },
44
+ "peerDependencies": {
45
+ "@l10nmonster/helpers": "^1"
46
+ },
47
+ "devDependencies": {
48
+ "esbuild": "latest",
49
+ "eslint": "^8",
50
+ "pkg": "^5.4.1"
51
+ }
52
+ }