ilib-lint 2.14.0 → 2.15.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilib-lint",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -71,8 +71,8 @@
71
71
  "micromatch": "^4.0.7",
72
72
  "options-parser": "^0.4.0",
73
73
  "xml-js": "^1.6.11",
74
- "ilib-common": "^1.1.6",
75
74
  "ilib-lint-common": "^3.4.0",
75
+ "ilib-common": "^1.1.6",
76
76
  "ilib-locale": "^1.2.4",
77
77
  "ilib-tools-common": "^1.17.0"
78
78
  },
@@ -45,6 +45,8 @@ import ResourceXML from '../rules/ResourceXML.js';
45
45
  import ResourceCamelCase from '../rules/ResourceCamelCase.js';
46
46
  import ResourceSnakeCase from '../rules/ResourceSnakeCase.js';
47
47
  import ResourceKebabCase from '../rules/ResourceKebabCase.js';
48
+ import ResourceGNUPrintfMatch from '../rules/ResourceGNUPrintfMatch.js';
49
+ import ResourceReturnChar from '../rules/ResourceReturnChar.js';
48
50
  import StringFixer from './string/StringFixer.js';
49
51
  import ResourceFixer from './resource/ResourceFixer.js';
50
52
 
@@ -55,7 +57,7 @@ export const regexRules = [
55
57
  name: "resource-url-match",
56
58
  description: "Ensure that URLs that appear in the source string are also used in the translated string",
57
59
  note: "URL '{matchString}' from the source string does not appear in the target string",
58
- regexps: [ "((https?|github|ftps?|mailto|file|data|irc):\\/\\/)([\\da-zA-Z\\.-]+)\\.([a-zA-Z\\.]{2,6})([\\/\w\\.-]*)*\\/?" ],
60
+ regexps: [ "((https?|github|ftps?|mailto|file|data|irc):\\/\\/)([\\da-zA-Z\\.-]+)\\.([a-zA-Z\\.]{2,6})([\\/#\\?=%&\\w\\.-]*)*[\\/#\\?=%&\\w-]" ],
59
61
  link: "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-url-match.md"
60
62
  },
61
63
  {
@@ -368,6 +370,14 @@ export const regexRules = [
368
370
  note: "The numbered parameter '{{matchString}}' from the source string does not appear in the target string",
369
371
  regexps: [ "\\{\\s*(?<match>\\d[^}]*?)\\s*\\}" ],
370
372
  link: "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint-javascript/docs/resource-csharp-numbered-params.md"
373
+ },
374
+ {
375
+ type: "resource-matcher",
376
+ name: "resource-tap-named-params",
377
+ description: "Ensure that named parameters in Tap I18n that appear in the source string are also used in the translated string",
378
+ note: "The named parameter '__{matchString}__' from the source string does not appear in the target string",
379
+ regexps: [ "__(?<match>[a-zA-Z_][a-zA-Z0-9_.]*?)__" ],
380
+ link: "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-tap-named-params.md"
371
381
  }
372
382
  ];
373
383
 
@@ -401,6 +411,11 @@ export const builtInRulesets = {
401
411
  "resource-no-space-with-fullwidth-punctuation": true,
402
412
  },
403
413
 
414
+ gnu: {
415
+ // GNU printf style parameter matching
416
+ "resource-gnu-printf-match": true,
417
+ },
418
+
404
419
  source: {
405
420
  "resource-source-icu-plural-syntax": true,
406
421
  "resource-source-icu-plural-categories": true,
@@ -421,6 +436,12 @@ export const builtInRulesets = {
421
436
  },
422
437
  "csharp": {
423
438
  "resource-csharp-numbered-params": true
439
+ },
440
+ "windows": {
441
+ "resource-return-char": true
442
+ },
443
+ "tap": {
444
+ "resource-tap-named-params": true
424
445
  }
425
446
  };
426
447
 
@@ -491,6 +512,8 @@ class BuiltinPlugin extends Plugin {
491
512
  ResourceCamelCase,
492
513
  ResourceSnakeCase,
493
514
  ResourceKebabCase,
515
+ ResourceGNUPrintfMatch,
516
+ ResourceReturnChar,
494
517
  ...regexRules
495
518
  ];
496
519
  }
@@ -0,0 +1,206 @@
1
+ /*
2
+ * ResourceGNUPrintfMatch.js - rule to check if GNU printf-style parameters in the source string
3
+ * also appear in the target string with the same format specifiers
4
+ *
5
+ * Copyright © 2025 JEDLSoft
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ *
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */
20
+
21
+ import { Result } from 'ilib-lint-common';
22
+ import { Resource } from 'ilib-tools-common';
23
+ import ResourceRule from './ResourceRule.js';
24
+
25
+ /**
26
+ * @class Represent an ilib-lint rule.
27
+ */
28
+ class ResourceGNUPrintfMatch extends ResourceRule {
29
+ /**
30
+ * Make a new rule instance.
31
+ * @constructor
32
+ */
33
+ constructor(options) {
34
+ super(options);
35
+ this.name = "resource-gnu-printf-match";
36
+ this.description = "Test that GNU printf-style substitution parameters match in the source and target strings.";
37
+ this.sourceLocale = (options && options.sourceLocale) || "en-US";
38
+ this.link = "https://github.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-gnu-printf-match.md";
39
+ }
40
+
41
+ /**
42
+ * Extract GNU printf-style parameters from a string.
43
+ * Supports positional parameters (%1$s, %2$d), width/precision from arguments (%*s, %.*f),
44
+ * and GNU extensions (%m, %'d, %I, etc.) as well as Swift/Objective-C %@ specifiers
45
+ * @private
46
+ * @param {string} str the string to extract parameters from
47
+ * @returns {Array<string>} array of parameter strings found
48
+ */
49
+ extractParameters(str) {
50
+ if (!str || typeof str !== 'string') return [];
51
+
52
+ // GNU printf regex pattern:
53
+ // % - literal percent
54
+ // (?:(\d+)\$)? - optional positional parameter (1$, 2$, etc.)
55
+ // (?:(\d+))? - optional width/precision from argument (*)
56
+ // (?:\.(?:\*|\d+))? - optional precision (.* or .123)
57
+ // (?:[hlL]|hh|ll)? - optional length modifier (h, l, L, hh, ll)
58
+ // [diouxXfFeEgGaAcCsSpn%m'#0I@] - format specifier including GNU extensions and Swift/Objective-C @
59
+ const gnuPrintfRegex =
60
+ /%(?:(\d+)\$)?(?:(\*))?(?:\.(?:\*|\d+))?(?:[hlL]|hh|ll)?[diouxXfFeEgGaAcCsSpn%m'#0I@]/g;
61
+
62
+ const matches = [];
63
+ let match;
64
+
65
+ while ((match = gnuPrintfRegex.exec(str)) !== null) {
66
+ matches.push(match[0]);
67
+ }
68
+
69
+ return matches;
70
+ }
71
+
72
+ /**
73
+ * Check a string pair for GNU printf parameter mismatches.
74
+ * @override
75
+ * @param {Object} params parameters for the string matching
76
+ * @param {String|undefined} params.source the source string to match against
77
+ * @param {String|undefined} params.target the target string to match
78
+ * @param {String} params.file the file path where the resources came from
79
+ * @param {Resource} params.resource the resource that contains the source and/or target string
80
+ * @param {number} [params.index] if the resource being tested is an array resource, this represents the index of this string in the array
81
+ * @param {string} [params.category] if the resource being tested is a plural resource, this represents the plural category of this string
82
+ * @returns {Result|Array.<Result>|undefined} any results found in this string or undefined if no problems were found
83
+ */
84
+ matchString({source, target, file, resource, index, category}) {
85
+ if (!source || !target) return;
86
+
87
+ const sourceParams = this.extractParameters(source);
88
+ const targetParams = this.extractParameters(target);
89
+
90
+ if (sourceParams.length === 0 && targetParams.length === 0) return;
91
+
92
+ const results = [];
93
+
94
+ // Ensure required fields are available
95
+ const resourceKey = resource.getKey();
96
+ if (!resourceKey) {
97
+ return;
98
+ }
99
+ if (!file) {
100
+ return;
101
+ }
102
+
103
+ // Get location information from the resource
104
+ const location = resource.getLocation();
105
+ const lineNumber = location?.line;
106
+ const charNumber = location?.char;
107
+
108
+ // Count occurrences of each parameter
109
+ function countParams(params) {
110
+ const counts = {};
111
+ for (const param of params) {
112
+ counts[param] = (counts[param] || 0) + 1;
113
+ }
114
+ return counts;
115
+ }
116
+ const sourceCounts = countParams(sourceParams);
117
+ const targetCounts = countParams(targetParams);
118
+
119
+ // Check for missing parameters in target (by count)
120
+ // Create separate Result for each different missing parameter
121
+ for (const param of Object.keys(sourceCounts)) {
122
+ const missingCount = sourceCounts[param] - (targetCounts[param] || 0);
123
+ if (missingCount > 0) {
124
+ const resultFields = {
125
+ severity: /** @type {const} */ ("error"),
126
+ description: `Source string GNU printf parameter ${param} not found in the target string.`,
127
+ rule: this,
128
+ id: resourceKey,
129
+ source: source,
130
+ highlight: `<e0>${target}</e0>`,
131
+ pathName: file,
132
+ lineNumber: lineNumber,
133
+ charNumber: charNumber,
134
+ };
135
+ results.push(new Result(resultFields));
136
+ }
137
+ }
138
+
139
+ // Check for extra parameters in target (by count)
140
+ // Group extra parameters by type to handle multiple of same type together
141
+ const extraParamsByType = {};
142
+ for (const param of Object.keys(targetCounts)) {
143
+ const extraCount = targetCounts[param] - (sourceCounts[param] || 0);
144
+ if (extraCount > 0) {
145
+ extraParamsByType[param] = extraCount;
146
+ }
147
+ }
148
+
149
+ // Create separate Result for each different extra parameter type
150
+ for (const [param, extraCount] of Object.entries(extraParamsByType)) {
151
+ let highlight = target;
152
+
153
+ if (extraCount === 1) {
154
+ // Single extra parameter - highlight the rightmost occurrence
155
+ const lastIndex = highlight.lastIndexOf(param);
156
+ if (lastIndex !== -1) {
157
+ highlight =
158
+ highlight.substring(0, lastIndex) +
159
+ `<e0>${param}</e0>` +
160
+ highlight.substring(lastIndex + param.length);
161
+ }
162
+ } else {
163
+ // Multiple extra parameters of same type - highlight only the last N occurrences (left-to-right)
164
+ // Find all indices of the param in the string
165
+ let allIndices = [];
166
+ let searchStart = 0;
167
+ while (true) {
168
+ const idx = highlight.indexOf(param, searchStart);
169
+ if (idx === -1) break;
170
+ allIndices.push(idx);
171
+ searchStart = idx + param.length;
172
+ }
173
+ // Only tag the last 'extraCount' occurrences
174
+ const indices = allIndices.slice(-extraCount);
175
+ // Apply tags in left-to-right order, adjusting for offset as we insert tags
176
+ let offset = 0;
177
+ for (let i = 0; i < indices.length; i++) {
178
+ const idx = indices[i] + offset;
179
+ const tag = `<e${i}>${param}</e${i}>`;
180
+ highlight =
181
+ highlight.substring(0, idx) +
182
+ tag +
183
+ highlight.substring(idx + param.length);
184
+ offset += tag.length - param.length;
185
+ }
186
+ }
187
+
188
+ const resultFields = {
189
+ severity: /** @type {const} */ ("error"),
190
+ description: `Extra target string GNU printf parameter ${param} not found in the source string.`,
191
+ rule: this,
192
+ id: resourceKey,
193
+ source: source,
194
+ highlight: highlight,
195
+ pathName: file,
196
+ lineNumber: lineNumber,
197
+ charNumber: charNumber,
198
+ };
199
+ results.push(new Result(resultFields));
200
+ }
201
+
202
+ return results.length > 0 ? results : undefined;
203
+ }
204
+ }
205
+
206
+ export default ResourceGNUPrintfMatch;
@@ -0,0 +1,106 @@
1
+ /*
2
+ * ResourceReturnChar.js - Rule to check that return character counts match between source and target
3
+ *
4
+ * Copyright © 2023-2024 JEDLSoft
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ *
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ import ResourceRule from './ResourceRule.js';
21
+
22
+ /**
23
+ * Rule to check that the number of return characters (CR, LF, CRLF) in the source
24
+ * string matches the number in the target string. This is important for Windows
25
+ * applications where return characters are used for formatting output.
26
+ *
27
+ * @example
28
+ * // Source: "Line 1\nLine 2\nLine 3" (2 newlines)
29
+ * // Target: "Line 1\nLine 2" (1 newline) - ERROR
30
+ * // Target: "Line 1\nLine 2\nLine 3" (2 newlines) - OK
31
+ */
32
+ export default class ResourceReturnChar extends ResourceRule {
33
+ constructor(options) {
34
+ super(options);
35
+ this.name = "resource-return-char";
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";
38
+ this.type = "resource";
39
+ }
40
+
41
+ /**
42
+ * Count return characters in a string, handling CR, LF, and CRLF sequences
43
+ * @param {string} str - The string to count return characters in
44
+ * @returns {number} - The number of return characters
45
+ */
46
+ countReturnChars(str) {
47
+ if (!str) return 0;
48
+
49
+ let count = 0;
50
+ let i = 0;
51
+
52
+ while (i < str.length) {
53
+ if (str[i] === '\r' && i + 1 < str.length && str[i + 1] === '\n') {
54
+ // CRLF sequence
55
+ count++;
56
+ i += 2;
57
+ } else if (str[i] === '\r' || str[i] === '\n') {
58
+ // Single CR or LF
59
+ count++;
60
+ i++;
61
+ } else {
62
+ i++;
63
+ }
64
+ }
65
+
66
+ return count;
67
+ }
68
+
69
+ /**
70
+ * Match a resource string to check if return character counts match
71
+ * @param {Object} params - Parameters for the match
72
+ * @param {string} params.source - The source string
73
+ * @param {string} params.target - The target string
74
+ * @param {Object} params.resource - The resource object
75
+ * @param {string} params.file - The file path
76
+ * @returns {Object|undefined} - Result object if there's a mismatch, undefined otherwise
77
+ */
78
+ matchString({ source, target, resource, file }) {
79
+ if (!source || !target) {
80
+ return;
81
+ }
82
+
83
+ const sourceReturns = this.countReturnChars(source);
84
+ const targetReturns = this.countReturnChars(target);
85
+
86
+ if (sourceReturns !== targetReturns) {
87
+ return {
88
+ rule: this,
89
+ severity: "error",
90
+ id: "return-char-count-mismatch",
91
+ pathName: file,
92
+ source: source,
93
+ target: target,
94
+ highlight: `Source has ${sourceReturns} return character(s), target has ${targetReturns}`,
95
+ description: `Return character count mismatch: source has ${sourceReturns} return character(s), target has ${targetReturns}. This may cause formatting issues in Windows applications.`,
96
+ lineNumber: resource?.lineNumber,
97
+ charNumber: resource?.charNumber,
98
+ endLineNumber: resource?.endLineNumber,
99
+ endCharNumber: resource?.endCharNumber,
100
+ locale: resource?.targetLocale
101
+ };
102
+ }
103
+
104
+ return;
105
+ }
106
+ }