ilib-lint 2.21.3 → 2.21.4

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.4",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -82,9 +82,9 @@
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",
87
+ "ilib-tools-common": "^1.22.0",
88
88
  "ilib-xliff": "^1.4.1"
89
89
  },
90
90
  "scripts": {
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
 
@@ -173,7 +173,7 @@ class ResourceSentenceEnding extends ResourceRule {
173
173
  super(options);
174
174
  this.name = "resource-sentence-ending";
175
175
  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";
176
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-sentence-ending.md";
177
177
 
178
178
  // Get the parameter from the options
179
179
  const param = this.getParam() || {};
@@ -438,6 +438,40 @@ class ResourceSentenceEnding extends ResourceRule {
438
438
  return stripped.endsWith(expected);
439
439
  }
440
440
 
441
+ /**
442
+ * Detect whether a string uses "person quotation" style, where the quoted
443
+ * content is a direct speech quotation introduced by a comma.
444
+ *
445
+ * Person quotation: She said, "A quotation!"
446
+ * Call-out (default): select 'Manual Zoom.'
447
+ *
448
+ * @param {string} str
449
+ * @returns {boolean}
450
+ */
451
+ static isPersonQuotation(str) {
452
+ if (!str) return false;
453
+ const trimmed = str.trim();
454
+ const quoteChars = ResourceSentenceEnding.allQuoteChars;
455
+
456
+ const lastChar = trimmed.charAt(trimmed.length - 1);
457
+ if (!quoteChars.includes(lastChar)) return false;
458
+
459
+ let openPos = -1;
460
+ for (let i = trimmed.length - 2; i >= 0; i--) {
461
+ if (quoteChars.includes(trimmed.charAt(i))) {
462
+ openPos = i;
463
+ break;
464
+ }
465
+ }
466
+ if (openPos <= 0) return false;
467
+
468
+ let beforeQuote = openPos - 1;
469
+ while (beforeQuote >= 0 && /\s/.test(trimmed.charAt(beforeQuote))) {
470
+ beforeQuote--;
471
+ }
472
+ return beforeQuote >= 0 && trimmed.charAt(beforeQuote) === ',';
473
+ }
474
+
441
475
  /**
442
476
  * Get the last quoted string in the input, or null if none found.
443
477
  * Handles all quote types in allQuoteChars.
@@ -974,12 +1008,20 @@ class ResourceSentenceEnding extends ResourceRule {
974
1008
  let highlight = '';
975
1009
  let description = '';
976
1010
 
1011
+ const targetTrimmed = target?.trim() || '';
1012
+ const targetEndsWithQuote = quoteChars.includes(targetTrimmed.charAt(targetTrimmed.length - 1));
1013
+
977
1014
  let lastSentence;
978
- if (sourceEndsWithQuote) {
979
- // Use the last quoted string in the target
1015
+ if (sourceEndsWithQuote &&
1016
+ (ResourceSentenceEnding.isPersonQuotation(sourceTrimmed) || targetEndsWithQuote)) {
1017
+ // Person quotation (e.g. She said, "Hello!") or both source and target
1018
+ // end with a quote — compare quoted content
980
1019
  lastSentence = ResourceSentenceEnding.getLastQuotedString(target) || target.trim();
1020
+ } else if (sourceEndsWithQuote) {
1021
+ // Call-out / reference where the target does not end with a quote —
1022
+ // compare overall target ending, not a sentence fragment
1023
+ lastSentence = target.trim();
981
1024
  } else {
982
- // Use the full target string
983
1025
  lastSentence = this.getLastSentenceFromContent(target, targetLocaleObj);
984
1026
  }
985
1027
 
@@ -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*$"