@l10nmonster/helpers-lqaboss 3.0.0-alpha.9 → 3.1.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 CHANGED
@@ -1,3 +1,45 @@
1
+ ## @l10nmonster/helpers-lqaboss [3.1.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-lqaboss@3.1.0...@l10nmonster/helpers-lqaboss@3.1.1) (2025-12-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Improve type definitions and checks ([826b412](https://public-github/l10nmonster/l10nmonster/commit/826b412f0f7e761d404165a243b0c2b26c416ac1))
7
+
8
+ ## @l10nmonster/helpers-lqaboss [3.1.1](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-lqaboss@3.1.0...@l10nmonster/helpers-lqaboss@3.1.1) (2025-12-23)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Improve type definitions and checks ([826b412](https://public-github/l10nmonster/l10nmonster/commit/826b412f0f7e761d404165a243b0c2b26c416ac1))
14
+
15
+
16
+
17
+
18
+
19
+ ### Dependencies
20
+
21
+ * **@l10nmonster/core:** upgraded to 3.1.1
22
+
23
+ # @l10nmonster/helpers-lqaboss [3.1.0](https://public-github/l10nmonster/l10nmonster/compare/@l10nmonster/helpers-lqaboss@3.0.0...@l10nmonster/helpers-lqaboss@3.1.0) (2025-12-20)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * Calibrate log severities ([2b3350a](https://public-github/l10nmonster/l10nmonster/commit/2b3350a3123abb91e7f91a9c1864daeb6275c3ad))
29
+ * **helpers-lqaboss:** Fix typo ([209dd07](https://public-github/l10nmonster/l10nmonster/commit/209dd071724c6f15464a1be0ebf140f23efc3f56))
30
+ * **helpers-lqaboss:** Move lqaboss ingestion to tm syncdown ([ebda63f](https://public-github/l10nmonster/l10nmonster/commit/ebda63f3b1651b44265ba62ce0f4ff876e9c97ed))
31
+ * **helpers-lqaboss:** Relax LQABossTmStore glob to allow for custom flow names ([5568c81](https://public-github/l10nmonster/l10nmonster/commit/5568c81d55f07c95d4af337904bc0367f5f71d68))
32
+ * **helpers-lqaboss:** Support old jobs without updatedAt property ([7c8cf75](https://public-github/l10nmonster/l10nmonster/commit/7c8cf759dfe9df379f537ef8a278b76949c19783))
33
+ * **lqaboss:** Support new response type ([ecaec02](https://public-github/l10nmonster/l10nmonster/commit/ecaec029b509d88267ac66374c5993c1537737c0))
34
+ * **server:** Fix cart cleanup ([9bbcab9](https://public-github/l10nmonster/l10nmonster/commit/9bbcab93e1fd20aeb09f59c828665159f091f37c))
35
+
36
+
37
+ ### Features
38
+
39
+ * **core:** Major refactor ([6992ee4](https://public-github/l10nmonster/l10nmonster/commit/6992ee4d74ad2e25afef6220f92f2e72dfd02457))
40
+ * Improve LQA Boss ([fcb0818](https://public-github/l10nmonster/l10nmonster/commit/fcb0818181f1a7bd46764596c9d2b8d8f362375c))
41
+ * **lqaboss:** Support for new Chrome extension ([dc6f86f](https://public-github/l10nmonster/l10nmonster/commit/dc6f86f417dde5e5942bdad2c81c0fbbac59fb80))
42
+
1
43
  # Changelog
2
44
 
3
45
  All notable changes to this project will be documented in this file.
package/flowCapture.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable complexity */
1
2
  import JSZip from 'jszip';
2
3
  import puppeteer from 'puppeteer';
3
4
  import { logInfo, logVerbose } from '@l10nmonster/core';
@@ -24,7 +25,7 @@ async function extractTextAndMetadataInPageContext() {
24
25
 
25
26
  const textElements = [];
26
27
  const START_MARKER_REGEX = /(?<![''<])\u200B([\uFE00-\uFE0F]+)/g;
27
- const END_MARKER = '\u200B';
28
+ const END_MARKER = '\u200C';
28
29
 
29
30
  if (!document.body) {
30
31
  return { error: 'Document body not found.' };
@@ -67,7 +68,9 @@ async function extractTextAndMetadataInPageContext() {
67
68
  if (decodedJsonMetadata && decodedJsonMetadata.trim() !== '') {
68
69
  parsedMetadata = JSON.parse(decodedJsonMetadata);
69
70
  }
70
- } catch (e) { parsedMetadata.decodingError = e.message; }
71
+ } catch (e) {
72
+ parsedMetadata.decodingError = e.message;
73
+ }
71
74
 
72
75
  textElements.push({
73
76
  text: activeSegment.text,
@@ -109,7 +112,9 @@ async function extractTextAndMetadataInPageContext() {
109
112
  if (decodedJsonMetadata && decodedJsonMetadata.trim() !== '') {
110
113
  parsedMetadata = JSON.parse(decodedJsonMetadata);
111
114
  }
112
- } catch (e) { parsedMetadata.decodingError = e.message; }
115
+ } catch (e) {
116
+ parsedMetadata.decodingError = e.message;
117
+ }
113
118
 
114
119
  textElements.push({
115
120
  text: capturedText,
@@ -221,12 +226,8 @@ export class FlowSnapshotter {
221
226
  const job = {
222
227
  sourceLang: tm.sourceLang,
223
228
  targetLang: tm.targetLang,
224
- tus: [],
229
+ tus: Object.values(await tm.getEntries(Array.from(guids))),
225
230
  };
226
- guids.forEach(guid => {
227
- const tu = tm.getEntryByGuid(guid);
228
- tu && job.tus.push(tu);
229
- });
230
231
  if (job.tus.length > 0) {
231
232
  zip.file('job.json', JSON.stringify(job, null, 2));
232
233
  }
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { LQABossActions } from './lqabossActions.js';
2
2
  export { LQABossProvider } from './lqabossProvider.js';
3
3
  export { LQABossTmStore } from './lqabossTmStore.js';
4
+ export { createLQABossRoutes } from './lqabossRoutes.js';
package/lqabossActions.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { lqaboss_capture } from './lqabossCapture.js';
2
2
 
3
- export class LQABossActions {
4
- static name = 'lqaboss';
5
- static help = {
3
+ /**
4
+ * CLI actions for LQA Boss integration.
5
+ * @type {import('@l10nmonster/core').L10nAction}
6
+ */
7
+ export const LQABossActions = {
8
+ name: 'lqaboss',
9
+ help: {
6
10
  description: 'Actions to integrate with LQA Boss.',
7
- };
8
-
9
- static subActions = [ lqaboss_capture ];
10
- }
11
+ },
12
+ subActions: [ lqaboss_capture ],
13
+ };
package/lqabossCapture.js CHANGED
@@ -3,8 +3,13 @@ import readline from 'readline';
3
3
  import { consoleLog } from '@l10nmonster/core';
4
4
  import { FlowSnapshotter } from './flowCapture.js';
5
5
 
6
- export class lqaboss_capture {
7
- static help = {
6
+ /**
7
+ * CLI action for creating an LQA Boss flow.
8
+ * @type {import('@l10nmonster/core').L10nAction}
9
+ */
10
+ export const lqaboss_capture = {
11
+ name: 'lqaboss_capture',
12
+ help: {
8
13
  description: 'create an lqaboss flow.',
9
14
  arguments: [
10
15
  ['<url>', 'the url of the page to capture'],
@@ -13,29 +18,30 @@ export class lqaboss_capture {
13
18
  options: [
14
19
  [ '--lang <srcLang,tgtLang>', 'source and target language pair' ],
15
20
  ],
16
- };
21
+ },
17
22
 
18
- static async action(mm, options) {
19
- if (!options.url || !options.flowName) {
23
+ async action(mm, options) {
24
+ const { url, flowName, lang } = /** @type {{ url?: string, flowName?: string, lang?: string | string[] }} */ (options);
25
+ if (!url || !flowName) {
20
26
  throw new Error('You must specify a url and a flowName');
21
27
  }
22
- const langPairs = options.lang ? (Array.isArray(options.lang) ? options.lang : options.lang.split(',')) : null;
28
+ const langPairs = lang ? (Array.isArray(lang) ? lang : lang.split(',')) : null;
23
29
  let tm;
24
30
  if (langPairs) {
25
31
  const [ sourceLang, targetLang ] = langPairs;
26
32
  tm = mm.tmm.getTM(sourceLang, targetLang);
27
33
  }
28
34
  // Run the capture flow
29
- const lqaBossBuffer = await runCapture(options.url, options.flowName, tm);
35
+ const lqaBossBuffer = await runCapture(url, flowName, tm);
30
36
  if (lqaBossBuffer) {
31
- const filename = `${options.flowName.replace(/[^a-z0-9_.-]/gi, '_')}.lqaboss`;
37
+ const filename = `${flowName.replace(/[^a-z0-9_.-]/gi, '_')}.lqaboss`;
32
38
  await fs.promises.writeFile(filename, lqaBossBuffer);
33
39
  consoleLog`Flow successfully saved as ${filename}`;
34
40
  } else {
35
41
  console.log('No pages were captured. Nothing to save.');
36
42
  }
37
- }
38
- }
43
+ },
44
+ };
39
45
 
40
46
  async function runCapture(startUrl, flowNameBase, tm) {
41
47
  const snapShotter = new FlowSnapshotter(startUrl, flowNameBase);
@@ -1,10 +1,13 @@
1
1
  import JSZip from 'jszip';
2
- import { providers, logVerbose, styleString, opsManager } from '@l10nmonster/core';
2
+ import path from 'path';
3
+ import { readFileSync } from 'fs';
4
+ import { providers, logVerbose, styleString, opsManager, getBaseDir } from '@l10nmonster/core';
3
5
 
4
6
  /**
5
7
  * @typedef {object} LQABossProviderOptions
6
- * @extends BaseTranslationProvider
7
8
  * @property {Object} delegate - Required file store delegate implementing file operations
9
+ * @property {string} [urlPrefix] - Prefix for the LQA Boss URL
10
+ * @property {string} [qualityFile] - Path to a quality model JSON file
8
11
  */
9
12
 
10
13
  /**
@@ -12,15 +15,19 @@ import { providers, logVerbose, styleString, opsManager } from '@l10nmonster/cor
12
15
  */
13
16
  export class LQABossProvider extends providers.BaseTranslationProvider {
14
17
  #storageDelegate;
18
+ urlPrefix;
19
+ #qualityFilePath;
15
20
  #opNames = {};
16
21
 
17
22
  /**
18
23
  * Initializes a new instance of the LQABossProvider class.
19
24
  * @param {LQABossProviderOptions} options - Configuration options for the provider.
20
25
  */
21
- constructor({ delegate, ...options }) {
26
+ constructor({ delegate, urlPrefix, qualityFile, ...options }) {
22
27
  super(options);
23
28
  this.#storageDelegate = delegate;
29
+ this.urlPrefix = urlPrefix;
30
+ qualityFile && (this.#qualityFilePath = path.resolve(getBaseDir(), qualityFile));
24
31
  this.#opNames.startReviewOp = `${this.id}.startReviewOp`;
25
32
  opsManager.registerOp(this.startReviewOp.bind(this), { opName: this.#opNames.startReviewOp, idempotent: false });
26
33
  }
@@ -31,26 +38,30 @@ export class LQABossProvider extends providers.BaseTranslationProvider {
31
38
  }
32
39
 
33
40
  async startReviewOp(op) {
34
- const { tus, ...jobResponse } = op.args.job;
41
+ const filename = op.args.job.jobName ?
42
+ `${op.args.job.jobName.replace(/\s+/g, '_')}-${op.args.job.jobGuid.substring(0, 5)}.lqaboss` :
43
+ `${op.args.job.jobGuid}.lqaboss`;
44
+ const jobRequest = {
45
+ ...op.args.job,
46
+ statusDescription: `Created LQA Boss file ${this.urlPrefix ? `at ${this.urlPrefix}/${filename}` : `: ${filename}`}`,
47
+ providerData: { quality: this.quality },
48
+ };
35
49
  const zip = new JSZip();
36
- zip.file('job.json', JSON.stringify({
37
- ...jobResponse,
38
- tus: tus.map(tu => ({
39
- rid: tu.rid,
40
- sid: tu.sid,
41
- guid: tu.guid,
42
- nsrc: tu.nsrc,
43
- notes: tu.notes,
44
- ntgt: tu.ntgt,
45
- q: this.quality,
46
- })),
47
- }, null, 2));
50
+ zip.file('job.json', JSON.stringify(jobRequest, null, 2));
51
+
52
+ // Add quality model if configured
53
+ if (this.#qualityFilePath) {
54
+ const qualityModel = JSON.parse(readFileSync(this.#qualityFilePath, 'utf-8'));
55
+ zip.file('quality.json', JSON.stringify(qualityModel, null, 2));
56
+ }
57
+
48
58
  const buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } });
49
- const filename = `${jobResponse.jobGuid}.lqaboss`;
50
59
  await this.#storageDelegate.saveFile(filename, buffer);
51
- logVerbose`Saved LQABoss file ${filename} with ${tus.length} guids and ${buffer.length} bytes`;
52
- jobResponse.tus = []; // remove tus so that job is cancelled and won't be stored
53
- return jobResponse;
60
+ logVerbose`Saved LQABoss file ${filename} with ${jobRequest.tus.length} guids and ${buffer.length} bytes`;
61
+ return {
62
+ ...jobRequest,
63
+ tus: [], // remove tus so that job is cancelled and won't be stored
64
+ };
54
65
  }
55
66
 
56
67
  async info() {
@@ -0,0 +1,35 @@
1
+ import { logInfo, logVerbose, logWarn } from '@l10nmonster/core';
2
+
3
+ /** @typedef {import('@l10nmonster/core').MonsterManager} MonsterManager */
4
+
5
+ /**
6
+ * Creates LQABoss route handlers for the server extension mechanism.
7
+ * @param {MonsterManager} mm - MonsterManager instance.
8
+ * @returns {Array<[string, string, Function]>} Route definitions.
9
+ */
10
+ export function createLQABossRoutes(mm) {
11
+ return [
12
+ ['post', '/lookup', async (req, res) => {
13
+ logInfo`LQABossRoute:/lookup`;
14
+ try {
15
+ const { sourceLang, targetLang, segments } = req.body;
16
+ const tm = mm.tmm.getTM(sourceLang, targetLang);
17
+ const guids = new Set(segments.map(segment => segment.g));
18
+ let tus = [];
19
+ if (guids.size > 0) {
20
+ tus = await tm.queryByGuids(Array.from(guids));
21
+ }
22
+ const guidMap = new Map(tus.map(tu => [ tu.guid, tu ]));
23
+ const results = segments.map(segment => guidMap.get(segment.g) ?? {});
24
+ logVerbose`Matched ${tus.length} segments out of ${guids.size}`;
25
+ res.json({ results });
26
+ } catch (error) {
27
+ logWarn`Error in LQABossRoute:/lookup: ${error.message}`;
28
+ res.status(500).json({
29
+ error: 'Failed to lookup translation memory',
30
+ message: error.message
31
+ });
32
+ }
33
+ }]
34
+ ]
35
+ }
package/lqabossTmStore.js CHANGED
@@ -1,19 +1,22 @@
1
- import { logInfo, logVerbose, utils } from '@l10nmonster/core';
1
+ import { utils } from '@l10nmonster/core';
2
+
3
+ /** @typedef {import('@l10nmonster/core').TMStore} TMStore */
2
4
 
3
5
  /**
4
6
  * Adapter class to expose LQABoss completion files as a TM store.
5
- *
6
- * @class LQABossTmStore
7
+ * @implements {TMStore}
7
8
  */
8
9
  export class LQABossTmStore {
9
10
  id;
10
11
  #storageDelegate;
11
12
  #tm;
12
13
 
14
+ /** @type {'readonly'} */
13
15
  get access() {
14
16
  return 'readonly';
15
17
  }
16
18
 
19
+ /** @type {'job'} */
17
20
  get partitioning() {
18
21
  return 'job';
19
22
  }
@@ -39,9 +42,13 @@ export class LQABossTmStore {
39
42
  this.#tm = {};
40
43
  const files = await this.#storageDelegate.listAllFiles();
41
44
  for (const [ fileName ] of files) {
42
- if (fileName.length === 26 && fileName.endsWith('.json')) {
45
+ if (fileName.endsWith('.json')) {
43
46
  const job = JSON.parse(await this.#storageDelegate.getFile(fileName));
44
- const ts = job.updatedAt ? new Date(job.updatedAt).getTime() : new Date().getTime();
47
+ if (!job.sourceLang || !job.targetLang || !job.jobGuid) {
48
+ continue;
49
+ }
50
+ !job.updatedAt && (job.updatedAt = '2025-08-29T21:29:36.269Z'); // workaround for old jobs that don't have an updatedAt
51
+ const ts = new Date(job.updatedAt).getTime();
45
52
  job.tus = job.tus.map(tu => ({ ...tu, ts }));
46
53
  this.#tm[job.sourceLang] ??= {};
47
54
  this.#tm[job.sourceLang][job.targetLang] ??= {};
@@ -52,8 +59,13 @@ export class LQABossTmStore {
52
59
  return this.#tm;
53
60
  }
54
61
 
62
+ /**
63
+ * @returns {Promise<[string, string][]>}
64
+ */
55
65
  async getAvailableLangPairs() {
56
66
  const tm = await this.#getTM();
67
+
68
+ /** @type {[string, string][]} */
57
69
  const pairs = [];
58
70
  for (const [ sourceLang, targets ] of Object.entries(tm)) {
59
71
  for (const targetLang of Object.keys(targets)) {
@@ -69,21 +81,34 @@ export class LQABossTmStore {
69
81
  return Object.entries(blocks).map(([ jobGuid, job ]) => [ jobGuid, job.updatedAt ]);
70
82
  }
71
83
 
84
+ /**
85
+ * @param {string} sourceLang
86
+ * @param {string} targetLang
87
+ * @param {string[]} blockIds
88
+ * @returns {AsyncGenerator<import('@l10nmonster/core').JobPropsTusPair>}
89
+ */
72
90
  async *getTmBlocks(sourceLang, targetLang, blockIds) {
73
91
  const tm = await this.#getTM();
74
92
  const blocks = tm[sourceLang]?.[targetLang] ?? {};
75
93
  for (const jobGuid of blockIds) {
76
94
  const job = blocks[jobGuid];
77
95
  if (job) {
96
+ // @ts-ignore - type inference issue with TU properties
78
97
  yield* utils.getIteratorFromJobPair(job, job);
79
98
  }
80
99
  }
81
100
  }
82
101
 
102
+ /**
103
+ * @param {string} sourceLang
104
+ * @param {string} targetLang
105
+ * @returns {Promise<import('@l10nmonster/core').TMStoreTOC>}
106
+ */
83
107
  async getTOC(sourceLang, targetLang) {
84
108
  const tm = await this.#getTM();
85
109
  const blocks = tm[sourceLang]?.[targetLang] ?? {};
86
- const toc = { v: 1, sourceLang, targetLang, blocks: {} };
110
+ /** @type {import('@l10nmonster/core').TMStoreTOC} */
111
+ const toc = { v: 1, sourceLang, targetLang, blocks: {}, storedBlocks: [] };
87
112
  for (const [ jobGuid, job ] of Object.entries(blocks)) {
88
113
  toc.blocks[jobGuid] = { blockName: jobGuid, modified: job.updatedAt, jobs: [ [ jobGuid, job.updatedAt ] ] };
89
114
  }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
- "name": "@l10nmonster/helpers-lqaboss",
3
- "version": "3.0.0-alpha.9",
4
- "description": "LQA Boss helper for L10n Monster",
5
- "main": "index.js",
6
- "type": "module",
7
- "scripts": {
8
- "start": "node index.js",
9
- "test": "node --test test/*.test.js"
10
- },
11
- "dependencies": {
12
- "jszip": "^3.10.1",
13
- "puppeteer": "^24"
14
- },
15
- "peerDependencies": {
16
- "@l10nmonster/core": "^3.0.0-alpha.0"
17
- }
2
+ "name": "@l10nmonster/helpers-lqaboss",
3
+ "version": "3.1.1",
4
+ "description": "LQA Boss helper for L10n Monster",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node index.js",
9
+ "test": "node --test test/*.test.js",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "jszip": "^3.10.1",
14
+ "puppeteer": "^24"
15
+ },
16
+ "peerDependencies": {
17
+ "@l10nmonster/core": "3.1.1"
18
+ }
18
19
  }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "include": [
4
+ "*.js",
5
+ "**/*.js"
6
+ ],
7
+ "exclude": [
8
+ "node_modules",
9
+ "**/node_modules",
10
+ "test/**",
11
+ "tests/**",
12
+ "**/*.test.js",
13
+ "**/*.spec.js",
14
+ "dist/**",
15
+ "ui/**",
16
+ "types/**"
17
+ ]
18
+ }
package/.releaserc.json DELETED
@@ -1,31 +0,0 @@
1
- {
2
- "branches": [
3
- "main",
4
- {
5
- "name": "next",
6
- "prerelease": "alpha"
7
- },
8
- {
9
- "name": "beta",
10
- "prerelease": "beta"
11
- }
12
- ],
13
- "tagFormat": "@l10nmonster/helpers-lqaboss@${version}",
14
- "plugins": [
15
- "@semantic-release/commit-analyzer",
16
- "@semantic-release/release-notes-generator",
17
- {
18
- "path": "@semantic-release/changelog",
19
- "changelogFile": "CHANGELOG.md"
20
- },
21
- {
22
- "path": "@semantic-release/npm",
23
- "npmPublish": true
24
- },
25
- {
26
- "path": "@semantic-release/git",
27
- "assets": ["CHANGELOG.md", "package.json"],
28
- "message": "chore(release): @l10nmonster/helpers-lqaboss@${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
29
- }
30
- ]
31
- }