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.
|
|
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})([
|
|
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
|
+
}
|