ilib-lint 1.0.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/LICENSE +201 -0
- package/README.md +231 -0
- package/docs/AnsiConsoleFormatter.html +467 -0
- package/docs/Formatter.html +577 -0
- package/docs/Formatter.js.html +135 -0
- package/docs/FormatterFactory.html +191 -0
- package/docs/FormatterFactory.js.html +109 -0
- package/docs/Parser.html +483 -0
- package/docs/Parser.js.html +122 -0
- package/docs/ParserFactory.js.html +109 -0
- package/docs/Plugin.html +847 -0
- package/docs/Plugin.js.html +168 -0
- package/docs/PluginManager.html +541 -0
- package/docs/PluginManager.js.html +125 -0
- package/docs/ResourceICUPlurals.html +278 -0
- package/docs/ResourceQuoteStyle.html +278 -0
- package/docs/ResourceRegExpChecker.html +295 -0
- package/docs/ResourceUniqueKeys.html +278 -0
- package/docs/Result.html +263 -0
- package/docs/Result.js.html +130 -0
- package/docs/Rule.html +774 -0
- package/docs/Rule.js.html +230 -0
- package/docs/RuleSet.html +760 -0
- package/docs/RuleSet.js.html +153 -0
- package/docs/SourceFile.html +826 -0
- package/docs/SourceFile.js.html +232 -0
- package/docs/XliffParser.html +396 -0
- package/docs/XliffPlugin.html +472 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/formatters_AnsiConsoleFormatter.js.html +147 -0
- package/docs/global.html +448 -0
- package/docs/ilibLint.md +1013 -0
- package/docs/index.html +81 -0
- package/docs/plugins_XliffParser.js.html +129 -0
- package/docs/plugins_XliffPlugin.js.html +129 -0
- package/docs/rules_ResourceICUPlurals.js.html +297 -0
- package/docs/rules_ResourceQuoteStyle.js.html +238 -0
- package/docs/rules_ResourceRegExpChecker.js.html +248 -0
- package/docs/rules_ResourceUniqueKeys.js.html +144 -0
- package/docs/scripts/collapse.js +20 -0
- package/docs/scripts/linenumber.js +25 -0
- package/docs/scripts/nav.js +12 -0
- package/docs/scripts/polyfill.js +4 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
- package/docs/scripts/prettify/lang-css.js +2 -0
- package/docs/scripts/prettify/prettify.js +28 -0
- package/docs/scripts/search.js +83 -0
- package/docs/styles/jsdoc.css +765 -0
- package/docs/styles/prettify.css +79 -0
- package/docs/walk.js.html +214 -0
- package/log4js.json +21 -0
- package/package.json +83 -0
- package/src/Formatter.js +66 -0
- package/src/FormatterFactory.js +41 -0
- package/src/Parser.js +53 -0
- package/src/ParserFactory.js +41 -0
- package/src/Plugin.js +99 -0
- package/src/PluginManager.js +56 -0
- package/src/Result.js +62 -0
- package/src/Rule.js +162 -0
- package/src/RuleSet.js +84 -0
- package/src/SourceFile.js +163 -0
- package/src/formatters/AnsiConsoleFormatter.js +78 -0
- package/src/index.js +213 -0
- package/src/plugins/XliffParser.js +60 -0
- package/src/plugins/XliffPlugin.js +60 -0
- package/src/rules/ResourceICUPlurals.js +229 -0
- package/src/rules/ResourceQuoteStyle.js +170 -0
- package/src/rules/ResourceRegExpChecker.js +179 -0
- package/src/rules/ResourceUniqueKeys.js +76 -0
- package/src/rules/utils.js +78 -0
- package/src/walk.js +146 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ResourceQuoteStyle.js - rule to check quotes in the target string
|
|
3
|
+
*
|
|
4
|
+
* Copyright © 2022 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 LocaleInfo from 'ilib-localeinfo';
|
|
21
|
+
|
|
22
|
+
import Rule from '../Rule.js';
|
|
23
|
+
import Result from '../Result.js';
|
|
24
|
+
|
|
25
|
+
let LICache = {};
|
|
26
|
+
|
|
27
|
+
// superset of all the start and end chars used in CLDR
|
|
28
|
+
const quoteChars = "«»‘“”„「」’‚‹›『』\"\'";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @class Represent an ilib-lint rule.
|
|
32
|
+
*/
|
|
33
|
+
class ResourceQuoteStyle extends Rule {
|
|
34
|
+
constructor(options) {
|
|
35
|
+
super(options);
|
|
36
|
+
this.name = "resource-quote-style";
|
|
37
|
+
this.description = "Ensure that the proper quote characters are used in translated resources";
|
|
38
|
+
this.sourceLocale = (options && options.sourceLocale) || "en-US";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getRuleType() {
|
|
42
|
+
return "resource";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @override
|
|
47
|
+
*/
|
|
48
|
+
match(options) {
|
|
49
|
+
const { locale, resource, file } = options || {};
|
|
50
|
+
let li = LICache[locale];
|
|
51
|
+
const _this = this;
|
|
52
|
+
|
|
53
|
+
if (!li) {
|
|
54
|
+
li = new LocaleInfo(locale);
|
|
55
|
+
LICache[locale] = li;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let sourceLI = LICache[this.sourceLocale];
|
|
59
|
+
if (!sourceLI) {
|
|
60
|
+
sourceLI = new LocaleInfo(this.sourceLocale);
|
|
61
|
+
LICache[this.sourceLocale] = sourceLI;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const srcQuoteStart = sourceLI.getDelimiterQuotationStart();
|
|
65
|
+
const srcAltQuoteStart = sourceLI.info.delimiter.alternateQuotationStart;
|
|
66
|
+
|
|
67
|
+
const srcQuoteEnd = sourceLI.getDelimiterQuotationEnd();
|
|
68
|
+
const srcAltQuoteEnd = sourceLI.info.delimiter.alternateQuotationEnd;
|
|
69
|
+
|
|
70
|
+
const tarQuoteStart = li.getDelimiterQuotationStart();
|
|
71
|
+
const tarAltQuoteStart = li.info.delimiter.alternateQuotationStart;
|
|
72
|
+
|
|
73
|
+
const tarQuoteEnd = li.getDelimiterQuotationEnd();
|
|
74
|
+
const tarAltQuoteEnd = li.info.delimiter.alternateQuotationEnd;
|
|
75
|
+
|
|
76
|
+
// if the source uses ASCII quotes, then the target could have ASCII or native quotes
|
|
77
|
+
const srcQuotesAscii = new RegExp(`((^|\\W)['"]\\p{Letter}|\\p{Letter}['"](\\W|$))`, "gu");
|
|
78
|
+
const srcQuotesNative = new RegExp(`((^|\\W)[${srcQuoteStart}${srcAltQuoteStart}]\\p{Letter}|\\p{Letter}[${srcQuoteEnd}${srcAltQuoteEnd}](\\W|$))`, "gu");
|
|
79
|
+
|
|
80
|
+
// if the source contains native quotes, then the target should also have native quotes
|
|
81
|
+
const tarQuotesAll = new RegExp(`((^|\\W)[${tarQuoteStart}${tarAltQuoteStart}'"]\\p{Letter}|\\p{Letter}[${tarQuoteEnd}${tarAltQuoteEnd}'"](\\W|$))`, "gu");
|
|
82
|
+
const tarQuotesNative = new RegExp(`((^|\\W)[${tarQuoteStart}${tarAltQuoteStart}]\\p{Letter}|\\p{Letter}[${tarQuoteEnd}${tarAltQuoteEnd}](\\W|$))`, "gu");
|
|
83
|
+
|
|
84
|
+
const nonQuoteChars = `([${
|
|
85
|
+
quoteChars.
|
|
86
|
+
replace(srcQuoteStart, "").
|
|
87
|
+
replace(srcAltQuoteStart, "").
|
|
88
|
+
replace(tarQuoteStart, "").
|
|
89
|
+
replace(tarAltQuoteStart, "").
|
|
90
|
+
replace(srcQuoteEnd, "").
|
|
91
|
+
replace(srcAltQuoteEnd, "").
|
|
92
|
+
replace(tarQuoteEnd, "").
|
|
93
|
+
replace(tarAltQuoteEnd, "")}])`;
|
|
94
|
+
const re = new RegExp(nonQuoteChars, "g");
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
function checkString(src, tar) {
|
|
100
|
+
srcQuotesAscii.lastIndex = 0;
|
|
101
|
+
tarQuotesAll.lastIndex = 0;
|
|
102
|
+
if ((src.match(srcQuotesAscii) && !tar.match(tarQuotesAll)) ||
|
|
103
|
+
(src.match(srcQuotesNative) && !tar.match(tarQuotesNative))) {
|
|
104
|
+
const matches = re.exec(tar);
|
|
105
|
+
let value = {
|
|
106
|
+
severity: "warning",
|
|
107
|
+
id: resource.getKey(),
|
|
108
|
+
source: src,
|
|
109
|
+
rule: _this,
|
|
110
|
+
pathName: file
|
|
111
|
+
};
|
|
112
|
+
if (matches) {
|
|
113
|
+
value.highlight = `Target: ${tar.replace(re, "<e0>$1</e0>")}`;
|
|
114
|
+
value.description = `Quote style for the the locale ${locale} should be ${tarQuoteStart}text${tarQuoteEnd}`;
|
|
115
|
+
} else {
|
|
116
|
+
value.highlight = `Target: ${tar}<e0></e0>`;
|
|
117
|
+
value.description = `Quotes are missing in the target. Quote style for the the locale ${locale} should be ${tarQuoteStart}text${tarQuoteEnd}`;
|
|
118
|
+
}
|
|
119
|
+
if (typeof(options.lineNumber) !== 'undefined') {
|
|
120
|
+
value.lineNumber = options.lineNumber;
|
|
121
|
+
}
|
|
122
|
+
return new Result(value);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (resource.getType()) {
|
|
127
|
+
case 'string':
|
|
128
|
+
const tarString = resource.getTarget();
|
|
129
|
+
if (tarString) {
|
|
130
|
+
return checkString(resource.getSource(), tarString);
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'array':
|
|
135
|
+
const srcArray = resource.getSource();
|
|
136
|
+
const tarArray = resource.getTarget();
|
|
137
|
+
if (tarArray) {
|
|
138
|
+
return srcArray.map((item, i) => {
|
|
139
|
+
if (i < tarArray.length && tarArray[i]) {
|
|
140
|
+
return checkString(srcArray[i], tarArray[i]);
|
|
141
|
+
}
|
|
142
|
+
}).filter(element => {
|
|
143
|
+
return element;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
|
|
148
|
+
case 'plural':
|
|
149
|
+
const srcPlural = resource.getSource();
|
|
150
|
+
const tarPlural = resource.getTarget();
|
|
151
|
+
if (tarPlural) {
|
|
152
|
+
const hasQuotes = categories.find(category => {
|
|
153
|
+
return (srcPlural[category] && srcPlural[category].contains(srcQuote));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (hasQuotes) {
|
|
157
|
+
return categories.map(category => {
|
|
158
|
+
return checkString(srcPlural.other, tarPlural[category]);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// no match
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default ResourceQuoteStyle;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ResourceRegExpChecker.js - rule to check if URLs in the source string also
|
|
3
|
+
* appear in the target string
|
|
4
|
+
*
|
|
5
|
+
* Copyright © 2022 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 Rule from '../Rule.js';
|
|
22
|
+
import Result from '../Result.js';
|
|
23
|
+
import { stripPlurals } from './utils.js';
|
|
24
|
+
|
|
25
|
+
function findMissing(source, target) {
|
|
26
|
+
let missing = [];
|
|
27
|
+
for (var i = 0; i < source.length; i++) {
|
|
28
|
+
if (target.indexOf(source[i]) < 0) {
|
|
29
|
+
missing.push(source[i]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return missing;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @class Resource checker class that checks that any regular expressions
|
|
37
|
+
* that matches in the source also appears in the translation.
|
|
38
|
+
*/
|
|
39
|
+
class ResourceRegExpChecker extends Rule {
|
|
40
|
+
/**
|
|
41
|
+
* Construct a new regular expression-based resource checker.
|
|
42
|
+
*
|
|
43
|
+
* The options must contain the following required properties:
|
|
44
|
+
*
|
|
45
|
+
* - name - a unique name for this rule
|
|
46
|
+
* - description - a one-line description of what this rule checks for.
|
|
47
|
+
* Example: "Check that URLs in the source also appear in the target"
|
|
48
|
+
* - note - a one-line note that will be printed on screen when the
|
|
49
|
+
* check fails. Example: "The URL {matchString} did not appear in the
|
|
50
|
+
* the target." (Currently, matchString is the only replacement
|
|
51
|
+
* param that is supported.)
|
|
52
|
+
* - regexps - an array of strings that encode regular expressions to
|
|
53
|
+
* look for
|
|
54
|
+
*/
|
|
55
|
+
constructor(options) {
|
|
56
|
+
super(options);
|
|
57
|
+
|
|
58
|
+
if (!options || !options.name || !options.description || !options.note || !options.regexps) {
|
|
59
|
+
throw "Missing required options for the ResourceRegExpChecker constructor";
|
|
60
|
+
}
|
|
61
|
+
["name", "description", "regexps", "note", "sourceLocale"].forEach(prop => {
|
|
62
|
+
this[prop] = options[prop];
|
|
63
|
+
});
|
|
64
|
+
this.sourceLocale = this.sourceLocale || "en-US";
|
|
65
|
+
|
|
66
|
+
// this may throw if you got to the syntax wrong:
|
|
67
|
+
this.re = this.regexps.map(regexp => new RegExp(regexp, "g"));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getRuleType() {
|
|
71
|
+
return "resource";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @override
|
|
76
|
+
*/
|
|
77
|
+
match(options) {
|
|
78
|
+
const { locale, resource, file } = options || {};
|
|
79
|
+
const _this = this;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
function checkString(re, src, tar) {
|
|
85
|
+
re.lastIndex = 0;
|
|
86
|
+
let sourceMatches = [];
|
|
87
|
+
const strippedSrc = stripPlurals(src);
|
|
88
|
+
const strippedTar = stripPlurals(tar);
|
|
89
|
+
|
|
90
|
+
let match = re.exec(strippedSrc);
|
|
91
|
+
while (match) {
|
|
92
|
+
sourceMatches.push(match[0]);
|
|
93
|
+
match = re.exec(strippedSrc);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (sourceMatches.length > 0) {
|
|
97
|
+
// contains URLs, so check the target
|
|
98
|
+
re.lastIndex = 0;
|
|
99
|
+
let targetMatches = [];
|
|
100
|
+
match = re.exec(strippedTar);
|
|
101
|
+
while (match) {
|
|
102
|
+
targetMatches.push(match[0]);
|
|
103
|
+
match = re.exec(strippedTar);
|
|
104
|
+
}
|
|
105
|
+
const missing = findMissing(sourceMatches, targetMatches);
|
|
106
|
+
if (missing.length > 0) {
|
|
107
|
+
return missing.map(missing => {
|
|
108
|
+
let value = {
|
|
109
|
+
severity: "error",
|
|
110
|
+
id: resource.getKey(),
|
|
111
|
+
source: src,
|
|
112
|
+
rule: _this,
|
|
113
|
+
pathName: file,
|
|
114
|
+
highlight:`Target: ${tar}<e0></e0>`,
|
|
115
|
+
description: _this.note.replace(/\{matchString\}/g, missing)
|
|
116
|
+
};
|
|
117
|
+
if (typeof(options.lineNumber) !== 'undefined') {
|
|
118
|
+
value.lineNumber = options.lineNumber;
|
|
119
|
+
}
|
|
120
|
+
return new Result(value);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkRegExps(src, tar) {
|
|
127
|
+
let results = [];
|
|
128
|
+
_this.re.forEach(re => {
|
|
129
|
+
results = results.concat(checkString(re, src, tar));
|
|
130
|
+
});
|
|
131
|
+
results = results.filter(result => result);
|
|
132
|
+
return results && results.length ? results : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
switch (resource.getType()) {
|
|
136
|
+
case 'string':
|
|
137
|
+
const tarString = resource.getTarget();
|
|
138
|
+
if (tarString) {
|
|
139
|
+
return checkRegExps(resource.getSource(), tarString);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case 'array':
|
|
144
|
+
const srcArray = resource.getSource();
|
|
145
|
+
const tarArray = resource.getTarget();
|
|
146
|
+
if (tarArray) {
|
|
147
|
+
return srcArray.map((item, i) => {
|
|
148
|
+
if (i < tarArray.length && tarArray[i]) {
|
|
149
|
+
return checkRegExps(srcArray[i], tarArray[i]);
|
|
150
|
+
}
|
|
151
|
+
}).filter(element => {
|
|
152
|
+
return element;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'plural':
|
|
158
|
+
const srcPlural = resource.getSource();
|
|
159
|
+
const tarPlural = resource.getTarget();
|
|
160
|
+
if (tarPlural) {
|
|
161
|
+
const hasQuotes = categories.find(category => {
|
|
162
|
+
return (srcPlural[category] && srcPlural[category].contains(srcQuote));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (hasQuotes) {
|
|
166
|
+
return categories.map(category => {
|
|
167
|
+
return checkRegExps(srcPlural.other, tarPlural[category]);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// no match
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export default ResourceRegExpChecker;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ResourceUniqueKeys.js - rule to check quotes in the target string
|
|
3
|
+
*
|
|
4
|
+
* Copyright © 2022 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 LocaleInfo from 'ilib-localeinfo';
|
|
21
|
+
import { TranslationSet } from 'ilib-tools-common';
|
|
22
|
+
|
|
23
|
+
import Rule from '../Rule.js';
|
|
24
|
+
import Result from '../Result.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @class Represent an ilib-lint rule.
|
|
28
|
+
*/
|
|
29
|
+
class ResourceUniqueKeys extends Rule {
|
|
30
|
+
constructor(options) {
|
|
31
|
+
super(options);
|
|
32
|
+
this.name = "resource-unique-keys";
|
|
33
|
+
this.description = "Ensure that the keys are unique within a locale across all resource files";
|
|
34
|
+
this.sourceLocale = (options && options.sourceLocale) || "en-US";
|
|
35
|
+
|
|
36
|
+
this.ts = new TranslationSet();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getRuleType() {
|
|
40
|
+
return "resource";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @override
|
|
45
|
+
*/
|
|
46
|
+
match(options) {
|
|
47
|
+
const { locale, resource, file } = options || {};
|
|
48
|
+
|
|
49
|
+
const hash = resource.hashKey();
|
|
50
|
+
const other = this.ts.get(hash);
|
|
51
|
+
|
|
52
|
+
if (other) {
|
|
53
|
+
let value = {
|
|
54
|
+
severity: "error",
|
|
55
|
+
id: resource.getKey(),
|
|
56
|
+
rule: this,
|
|
57
|
+
pathName: file,
|
|
58
|
+
highlight: `Also defined in this file: ${other.resfile}`,
|
|
59
|
+
description: `Key is not unique within locale ${locale}.`,
|
|
60
|
+
locale
|
|
61
|
+
};
|
|
62
|
+
if (typeof(options.lineNumber) !== 'undefined') {
|
|
63
|
+
value.lineNumber = options.lineNumber;
|
|
64
|
+
}
|
|
65
|
+
return new Result(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
resource.resfile = file;
|
|
69
|
+
this.ts.add(resource);
|
|
70
|
+
|
|
71
|
+
// no result
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default ResourceUniqueKeys;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* utils.js - utility functions for the rules
|
|
3
|
+
*
|
|
4
|
+
* Copyright © 2022 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 { IntlMessageFormat } from 'intl-messageformat';
|
|
21
|
+
|
|
22
|
+
function processNode(node) {
|
|
23
|
+
let text = "";
|
|
24
|
+
switch (node.type) {
|
|
25
|
+
case 0:
|
|
26
|
+
text = node.value;
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
// Actual parameter
|
|
30
|
+
case 1:
|
|
31
|
+
text = `{${node.value}}`;
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case 6:
|
|
35
|
+
if (node.options) {
|
|
36
|
+
text = Object.keys(node.options).map(category => {
|
|
37
|
+
return concatText(node.options[category].value);
|
|
38
|
+
}).join(" ");
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (node.children) {
|
|
44
|
+
text += concatText(node.children);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function concatText(ast) {
|
|
51
|
+
if (!ast) return "";
|
|
52
|
+
|
|
53
|
+
let result = "";
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(ast)) {
|
|
56
|
+
result += ast.map(node => {
|
|
57
|
+
return processNode(node);
|
|
58
|
+
}).join(" ");
|
|
59
|
+
} else if (typeof(ast) === "Object") {
|
|
60
|
+
result = processNode(node);
|
|
61
|
+
} else if (typeof(ast) === "string") {
|
|
62
|
+
result = ast;
|
|
63
|
+
} // else just ignore
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stripPlurals(str, locale) {
|
|
69
|
+
try {
|
|
70
|
+
const imf = new IntlMessageFormat(str, locale);
|
|
71
|
+
const ast = imf.getAst();
|
|
72
|
+
|
|
73
|
+
return concatText(ast).replace(/\s+/g, " ");
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// punt
|
|
76
|
+
return str;
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/walk.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* walk.js - walk a directory tree
|
|
3
|
+
*
|
|
4
|
+
* Copyright © 2022 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 fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import log4js from 'log4js';
|
|
23
|
+
import mm from 'micromatch';
|
|
24
|
+
|
|
25
|
+
import SourceFile from './SourceFile.js';
|
|
26
|
+
|
|
27
|
+
const logger = log4js.getLogger("ilib-lint.walk");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Recursively walk a directory and return a list of files and directories
|
|
31
|
+
* within that directory. The walk is controlled via a list of exclude and
|
|
32
|
+
* include patterns. Each pattern should be a micromatch pattern like this:
|
|
33
|
+
*
|
|
34
|
+
* <code>
|
|
35
|
+
* "*.json"
|
|
36
|
+
* </code>
|
|
37
|
+
*
|
|
38
|
+
* The full path to every file and directory in the top-level directory will
|
|
39
|
+
* be included, unless it matches an exclude pattern, it which case, it will be
|
|
40
|
+
* excluded from the output. However, if the path
|
|
41
|
+
* also matches an include pattern, it will still be included nonetheless. The
|
|
42
|
+
* idea is that you can exclude a whole category of files (like all json files),
|
|
43
|
+
* but include specific ones. For example, you may exclude all json files, but
|
|
44
|
+
* still want to include the "config.json" file.<p>
|
|
45
|
+
|
|
46
|
+
* The options parameter may include any of the following optional properties:
|
|
47
|
+
*
|
|
48
|
+
* <ul>
|
|
49
|
+
* <li><i>quiet</i> (boolean) - whether or not to give output while walking
|
|
50
|
+
* the directory tree
|
|
51
|
+
* <li><i>excludes</i> (Array of strings) - A list of micromatch patterns to
|
|
52
|
+
* exclude from the output. If a pattern matches a directory, that directory
|
|
53
|
+
* will not be recursively searched.
|
|
54
|
+
* <li><i>includes</i> (Array of strings) - A list of micromatch patterns to
|
|
55
|
+
* include in the walk. If a pattern matches both an exclude and an include, the
|
|
56
|
+
* include will override the exclude.
|
|
57
|
+
* </ul>
|
|
58
|
+
*
|
|
59
|
+
* @param {String} root Directory to walk
|
|
60
|
+
* @param {Object} options Options controlling how this walk happens. (See
|
|
61
|
+
* the description for more details.)
|
|
62
|
+
* @returns {Array.<SourceFile>} an array of file names in the directory, filtered
|
|
63
|
+
* by the the excludes and includes list
|
|
64
|
+
*/
|
|
65
|
+
function walk(root, options) {
|
|
66
|
+
let results = [], projectRoot = false, newProject, list;
|
|
67
|
+
|
|
68
|
+
if (typeof(root) !== "string") {
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { config, quiet = false } = options || {};
|
|
73
|
+
const includes = config && config.paths ? Object.keys(config.paths) : ["**"];
|
|
74
|
+
let excludes = config && config.excludes;
|
|
75
|
+
let pathName, relPath, included, stat, glob;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (fs.existsSync(root)) {
|
|
79
|
+
stat = fs.statSync(root);
|
|
80
|
+
if (stat && stat.isDirectory()) {
|
|
81
|
+
list = fs.readdirSync(root);
|
|
82
|
+
if (!quiet) logger.trace("Searching " + root);
|
|
83
|
+
|
|
84
|
+
if (list && list.length !== 0) {
|
|
85
|
+
list.sort().forEach((file) => {
|
|
86
|
+
if (file === "." || file === "..") {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pathName = path.join(root, file);
|
|
91
|
+
included = true;
|
|
92
|
+
|
|
93
|
+
if (excludes) {
|
|
94
|
+
if (!quiet) logger.trace(`There are excludes. Relpath is ${pathName}`);
|
|
95
|
+
included = !mm.isMatch(pathName, excludes);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (included) {
|
|
99
|
+
results = results.concat(walk(pathName, options));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// file
|
|
105
|
+
included = false;
|
|
106
|
+
|
|
107
|
+
if (includes) {
|
|
108
|
+
if (!quiet) logger.trace(`There are includes.`);
|
|
109
|
+
mm.match(root, includes, {
|
|
110
|
+
onMatch: (params) => {
|
|
111
|
+
if (!glob && params.isMatch) {
|
|
112
|
+
glob = params.glob;
|
|
113
|
+
excludes = config && ((config.paths && config.paths[glob] && config.paths[glob].excludes) || excludes);
|
|
114
|
+
included = excludes ? !mm.isMatch(root, excludes) : true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (included) {
|
|
121
|
+
if (!quiet) logger.trace(`${pathName} ... included`);
|
|
122
|
+
glob = glob || "**";
|
|
123
|
+
results.push(new SourceFile({
|
|
124
|
+
filePath: root,
|
|
125
|
+
settings: config.paths && config.paths[glob]
|
|
126
|
+
}));
|
|
127
|
+
} else {
|
|
128
|
+
if (!quiet) logger.trace(`${pathName} ... excluded`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
if (!quiet) logger.warn(`File ${pathName} does not exist.`);
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// if the readdirSync did not work, it's maybe a file?
|
|
136
|
+
if (fs.existsSync(root)) {
|
|
137
|
+
return [new SourceFile({
|
|
138
|
+
filePath: root
|
|
139
|
+
})];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export default walk;
|