ilib-lint 2.21.3 → 2.21.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -608,17 +608,11 @@ ilib-lint plugins from v1 of ilib-lint to v2.
608
608
 
609
609
  ## License
610
610
 
611
- Copyright © 2022-2025, JEDLSoft
611
+ Copyright © 2022-2026, JEDLSoft
612
612
 
613
- Licensed under the Apache License, Version 2.0 (the "License");
614
- you may not use this file except in compliance with the License.
615
- You may obtain a copy of the License at
613
+ This package is released under the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0). The full license text is available in the [LICENSE](https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/LICENSE) file in the ilib-mono repository on GitHub.
616
614
 
617
- http://www.apache.org/licenses/LICENSE-2.0
615
+ ## Release Notes
618
616
 
619
- Unless required by applicable law or agreed to in writing, software
620
- distributed under the License is distributed on an "AS IS" BASIS,
621
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
617
+ See [CHANGELOG.md](https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/CHANGELOG.md).
622
618
 
623
- See the License for the specific language governing permissions and
624
- limitations under the License.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilib-lint",
3
- "version": "2.21.3",
3
+ "version": "2.21.5",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -77,15 +77,15 @@
77
77
  "options-parser": "^0.4.0",
78
78
  "xml-js": "^1.6.11",
79
79
  "ilib-xliff-webos": "^1.0.11",
80
- "ilib-casemapper": "^1.0.2",
81
80
  "ilib-common": "^1.1.7",
81
+ "ilib-casemapper": "^1.0.2",
82
82
  "ilib-ctype": "^1.3.0",
83
83
  "ilib-lint-common": "^3.7.0",
84
84
  "ilib-locale": "^1.4.0",
85
- "ilib-localematcher": "^1.3.3",
85
+ "ilib-localematcher": "^1.3.4",
86
86
  "ilib-scriptinfo": "^1.0.0",
87
- "ilib-tools-common": "^1.21.4",
88
- "ilib-xliff": "^1.4.1"
87
+ "ilib-xliff": "^1.4.1",
88
+ "ilib-tools-common": "^1.22.0"
89
89
  },
90
90
  "scripts": {
91
91
  "coverage": "LANG=en_US.UTF8 node --trace-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
package/src/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  * index.js - main program of ilib-lint
4
4
  *
5
- * Copyright © 2022-2025 JEDLSoft
5
+ * Copyright © 2022-2026 JEDLSoft
6
6
  *
7
7
  * Licensed under the Apache License, Version 2.0 (the "License");
8
8
  * you may not use this file except in compliance with the License.
@@ -178,7 +178,7 @@ if (options.opt.quiet) {
178
178
  logger.level = "debug";
179
179
  }
180
180
 
181
- logger.info("ilib-lint - Copyright (c) 2022-2025 JEDLsoft, All rights reserved.");
181
+ logger.info("ilib-lint - Copyright (c) 2022-2026 JEDLsoft, All rights reserved.");
182
182
 
183
183
  let paths = options.args;
184
184
  if (paths.length === 0) {
@@ -169,7 +169,7 @@ export const regexRules = [
169
169
  regexps: [
170
170
  "(\\p{L}+('\\p{L}+)+)" // word boundary + word chars + quote + word chars + word boundary (e.g., it's, don't, d'l'homme)
171
171
  ],
172
- link: "https://github.com/iLib-js/ilib-lint/blob/main/docs/resource-apostrophe.md",
172
+ link: "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-apostrophe.md",
173
173
  fixes: [
174
174
  { search: "'", replace: "\u2019" }
175
175
  ]
@@ -39,7 +39,7 @@ class ResourceCamelCase extends ResourceRule {
39
39
 
40
40
  this.name = "resource-camel-case";
41
41
  this.description = "Ensure that when source strings contain only camel case and no whitespace, then the targets are the same";
42
- this.link = "https://gihub.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-camel-case.md";
42
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-camel-case.md";
43
43
  this.regexps = [
44
44
  "^\\s*[a-z\\d]+([A-Z][a-z\\d]+)+\\s*$",
45
45
  "^\\s*[A-Z][a-z\\d]+([A-Z][a-z\\d]+)+\\s*$",
@@ -19,7 +19,7 @@ class ResourceKebabCase extends ResourceRule {
19
19
 
20
20
  this.name = "resource-kebab-case";
21
21
  this.description = "Ensure that when source strings contain only kebab case and no whitespace, then the targets are the same";
22
- this.link = "https://gihub.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-kebab-case.md";
22
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-kebab-case.md";
23
23
 
24
24
  const param = this.getParam() || {};
25
25
  this.exceptions = Array.isArray(param?.except) ? param.except : [];
@@ -236,14 +236,17 @@ class ResourceQuoteStyle extends ResourceRule {
236
236
  }
237
237
  let match;
238
238
  let commands = [];
239
+ const startQuotePositions = new Set();
239
240
 
240
241
  while ((match = startQuote.exec(tar)) !== null) {
241
242
  // now that we have found a start quote, find it in the matched string so
242
243
  // that we can get the index into that string
243
244
  const offset = match[0].indexOf(match[3]);
245
+ const pos = match.index + offset;
246
+ startQuotePositions.add(pos);
244
247
 
245
248
  commands.push(ResourceFixer.createStringCommand(
246
- match.index + offset,
249
+ pos,
247
250
  1,
248
251
  correctQuoteStart
249
252
  ));
@@ -253,9 +256,13 @@ class ResourceQuoteStyle extends ResourceRule {
253
256
  // now that we have found an end quote, find it in the matched string so
254
257
  // that we can get the index into that string
255
258
  const offset = match[0].indexOf(match[3]);
259
+ const pos = match.index + offset;
260
+ // skip positions already handled by startQuote to avoid overlapping commands
261
+ // (can occur when a quote char is surrounded by CJK letters on both sides)
262
+ if (startQuotePositions.has(pos)) continue;
256
263
 
257
264
  commands.push(ResourceFixer.createStringCommand(
258
- match.index + offset,
265
+ pos,
259
266
  1,
260
267
  correctQuoteEnd
261
268
  ));
@@ -34,7 +34,7 @@ export default class ResourceReturnChar extends ResourceRule {
34
34
  super(options);
35
35
  this.name = "resource-return-char";
36
36
  this.description = "Checks that the number of return characters (CR, LF, CRLF) in the source matches the target";
37
- this.link = "https://github.com/iLib-js/ilib-lint/blob/main/docs/resource-return-char.md";
37
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-return-char.md";
38
38
  this.type = "resource";
39
39
  }
40
40
 
@@ -102,9 +102,11 @@ const punctuationMap = {
102
102
 
103
103
  /**
104
104
  * @ignore
105
- * @typedef {{minimumLength?: number}} ResourceSentenceEndingFixedOptions
105
+ * @typedef {{minimumLength?: number, exceptions?: string[]}} ResourceSentenceEndingFixedOptions
106
106
  * @property {number} [minimumLength=10] - Minimum length of source string before the rule is applied.
107
107
  * Strings shorter than this length will be skipped (useful for avoiding false positives on abbreviations).
108
+ * @property {string[]} [exceptions] - Array of source strings to skip checking for ALL locales.
109
+ * Useful for handling special cases that should be globally excluded from sentence-ending punctuation checks.
108
110
  */
109
111
 
110
112
  /**
@@ -120,7 +122,7 @@ class ResourceSentenceEnding extends ResourceRule {
120
122
  /**
121
123
  * Constructs a new ResourceSentenceEnding rule instance.
122
124
  *
123
- * @param {ResourceSentenceEndingOptions} [options] - Configuration options for the rule
125
+ * @param {{ param?: ResourceSentenceEndingOptions, sourceLocale?: string, getLogger?: Function }} [options] - Configuration options for the rule
124
126
  *
125
127
  * @example
126
128
  * // Basic usage with default settings
@@ -173,7 +175,7 @@ class ResourceSentenceEnding extends ResourceRule {
173
175
  super(options);
174
176
  this.name = "resource-sentence-ending";
175
177
  this.description = "Checks that sentence-ending punctuation is appropriate for the locale of the target string and matches the punctuation in the source string";
176
- this.link = "https://github.com/iLib-js/ilib-lint/blob/main/docs/resource-sentence-ending.md";
178
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-sentence-ending.md";
177
179
 
178
180
  // Get the parameter from the options
179
181
  const param = this.getParam() || {};
@@ -181,9 +183,16 @@ class ResourceSentenceEnding extends ResourceRule {
181
183
  // Initialize minimum length configuration
182
184
  this.minimumLength = Math.max(0, param?.minimumLength ?? 10);
183
185
 
186
+ // Initialize global exceptions (apply to all locales), deduplicated via Set
187
+ this.globalExceptions = new Set(
188
+ Array.isArray(param?.exceptions)
189
+ ? param.exceptions.map((/** @type {string} */ e) => e.toLowerCase().trim())
190
+ : []
191
+ );
192
+
184
193
  // Initialize custom punctuation mappings from configuration
185
194
  this.customPunctuationMap = {};
186
- // Initialize exception lists from configuration
195
+ // Initialize locale-specific exception lists from configuration
187
196
  this.exceptionsMap = {};
188
197
 
189
198
  if (param && typeof param === 'object' && !Array.isArray(param)) {
@@ -211,9 +220,11 @@ class ResourceSentenceEnding extends ResourceRule {
211
220
  ...punctuationMappings
212
221
  };
213
222
 
214
- // Store exceptions separately
223
+ // Store exceptions as a Set to deduplicate
215
224
  if (exceptions && Array.isArray(exceptions)) {
216
- this.exceptionsMap[language] = exceptions;
225
+ this.exceptionsMap[language] = new Set(
226
+ exceptions.map((/** @type {string} */ e) => e.toLowerCase().trim())
227
+ );
217
228
  }
218
229
  }
219
230
  }
@@ -438,6 +449,40 @@ class ResourceSentenceEnding extends ResourceRule {
438
449
  return stripped.endsWith(expected);
439
450
  }
440
451
 
452
+ /**
453
+ * Detect whether a string uses "person quotation" style, where the quoted
454
+ * content is a direct speech quotation introduced by a comma.
455
+ *
456
+ * Person quotation: She said, "A quotation!"
457
+ * Call-out (default): select 'Manual Zoom.'
458
+ *
459
+ * @param {string} str
460
+ * @returns {boolean}
461
+ */
462
+ static isPersonQuotation(str) {
463
+ if (!str) return false;
464
+ const trimmed = str.trim();
465
+ const quoteChars = ResourceSentenceEnding.allQuoteChars;
466
+
467
+ const lastChar = trimmed.charAt(trimmed.length - 1);
468
+ if (!quoteChars.includes(lastChar)) return false;
469
+
470
+ let openPos = -1;
471
+ for (let i = trimmed.length - 2; i >= 0; i--) {
472
+ if (quoteChars.includes(trimmed.charAt(i))) {
473
+ openPos = i;
474
+ break;
475
+ }
476
+ }
477
+ if (openPos <= 0) return false;
478
+
479
+ let beforeQuote = openPos - 1;
480
+ while (beforeQuote >= 0 && /\s/.test(trimmed.charAt(beforeQuote))) {
481
+ beforeQuote--;
482
+ }
483
+ return beforeQuote >= 0 && trimmed.charAt(beforeQuote) === ',';
484
+ }
485
+
441
486
  /**
442
487
  * Get the last quoted string in the input, or null if none found.
443
488
  * Handles all quote types in allQuoteChars.
@@ -948,12 +993,17 @@ class ResourceSentenceEnding extends ResourceRule {
948
993
  }
949
994
  }
950
995
 
951
- // Exception 3: Check if source is in exception list
996
+ const normalizedSource = source.toLowerCase().trim();
997
+
998
+ // Exception 3: Check if source is in global exception list (all locales)
999
+ if (this.globalExceptions.has(normalizedSource)) {
1000
+ return undefined;
1001
+ }
1002
+
1003
+ // Exception 4: Check if source is in locale-specific exception list
952
1004
  const exceptions = this.exceptionsMap[targetLanguage];
953
- if (exceptions) {
954
- if (exceptions.some(exception => exception.toLowerCase().trim() === source.toLowerCase().trim())) {
955
- return undefined;
956
- }
1005
+ if (exceptions?.has(normalizedSource)) {
1006
+ return undefined;
957
1007
  }
958
1008
 
959
1009
  const optionalPunctuationLanguages = ['th', 'lo', 'my', 'km', 'vi', 'id', 'ms', 'tl', 'jv', 'su'];
@@ -974,12 +1024,20 @@ class ResourceSentenceEnding extends ResourceRule {
974
1024
  let highlight = '';
975
1025
  let description = '';
976
1026
 
1027
+ const targetTrimmed = target?.trim() || '';
1028
+ const targetEndsWithQuote = quoteChars.includes(targetTrimmed.charAt(targetTrimmed.length - 1));
1029
+
977
1030
  let lastSentence;
978
- if (sourceEndsWithQuote) {
979
- // Use the last quoted string in the target
1031
+ if (sourceEndsWithQuote &&
1032
+ (ResourceSentenceEnding.isPersonQuotation(sourceTrimmed) || targetEndsWithQuote)) {
1033
+ // Person quotation (e.g. She said, "Hello!") or both source and target
1034
+ // end with a quote — compare quoted content
980
1035
  lastSentence = ResourceSentenceEnding.getLastQuotedString(target) || target.trim();
1036
+ } else if (sourceEndsWithQuote) {
1037
+ // Call-out / reference where the target does not end with a quote —
1038
+ // compare overall target ending, not a sentence fragment
1039
+ lastSentence = target.trim();
981
1040
  } else {
982
- // Use the full target string
983
1041
  lastSentence = this.getLastSentenceFromContent(target, targetLocaleObj);
984
1042
  }
985
1043
 
@@ -39,7 +39,7 @@ class ResourceSnakeCase extends ResourceRule {
39
39
 
40
40
  this.name = "resource-snake-case";
41
41
  this.description = "Ensure that when source strings contain only snake case and no whitespace, then the targets are the same";
42
- this.link = "https://gihub.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-snake-case.md";
42
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-snake-case.md";
43
43
  this.regexps = [
44
44
  "^\\s*[a-zA-Z0-9]*(_[a-zA-Z0-9]+)+\\s*$",
45
45
  "^\\s*[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*_\\s*$"