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,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* AnsiConsoleFormatter.js - Formats result output
|
|
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
|
+
import log4js from 'log4js';
|
|
20
|
+
|
|
21
|
+
import Formatter from '../Formatter.js';
|
|
22
|
+
|
|
23
|
+
var logger = log4js.getLogger("ilib-lint.formatters.AnsiConsoleFormatter");
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @class Represent an output formatter for an ANSI console/terminal
|
|
27
|
+
* @abstract
|
|
28
|
+
*/
|
|
29
|
+
class AnsiConsoleFormatter extends Formatter {
|
|
30
|
+
/**
|
|
31
|
+
* Construct an formatter instance.
|
|
32
|
+
*/
|
|
33
|
+
constructor() {
|
|
34
|
+
super();
|
|
35
|
+
this.name = "ansi-console-formatter";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return a general description of the formatter for use in help output.
|
|
40
|
+
*
|
|
41
|
+
* @returns {String} a general description of the formatter
|
|
42
|
+
*/
|
|
43
|
+
getDescription() {
|
|
44
|
+
return "Formats results for an ANSI terminal/console with colors.";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format the given result with the current formatter and return the
|
|
49
|
+
* formatted string.
|
|
50
|
+
*
|
|
51
|
+
* @abstract
|
|
52
|
+
* @param {Result} result the result to format
|
|
53
|
+
* @returns {String} the formatted result
|
|
54
|
+
*/
|
|
55
|
+
format(result) {
|
|
56
|
+
if (!result) return;
|
|
57
|
+
let output = "";
|
|
58
|
+
const startColor = (result.severity === "error" ? "\u001B[91m" : "\u001B[33m");
|
|
59
|
+
output += `${result.pathName}${typeof(result.lineNumber) === "number" ? ('(' + result.lineNumber + ')') : ""}:
|
|
60
|
+
${startColor}${result.description}\u001B[0m\n`;
|
|
61
|
+
if (result.id) {
|
|
62
|
+
output += ` Key: ${result.id}\n`;
|
|
63
|
+
}
|
|
64
|
+
if (result.source) {
|
|
65
|
+
output += ` Source: ${result.source}\n`;
|
|
66
|
+
}
|
|
67
|
+
output += ` ${result.highlight}
|
|
68
|
+
Rule (${result.rule.getName()}): ${result.rule.getDescription()}
|
|
69
|
+
`;
|
|
70
|
+
// output ascii terminal escape sequences
|
|
71
|
+
output = output.replace(/<e\d><\/e\d>/g, "\u001B[91m \u001B[0m");
|
|
72
|
+
output = output.replace(/<e\d>/g, "\u001B[91m");
|
|
73
|
+
output = output.replace(/<\/e\d>/g, "\u001B[0m");
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default AnsiConsoleFormatter;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* index.js - main program of ilib-lint
|
|
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 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
|
|
23
|
+
import OptionsParser from 'options-parser';
|
|
24
|
+
import Locale from 'ilib-locale';
|
|
25
|
+
import { JSUtils, Utils, Path } from 'ilib-common';
|
|
26
|
+
import json5 from 'json5';
|
|
27
|
+
import log4js from 'log4js';
|
|
28
|
+
|
|
29
|
+
import walk from './walk.js';
|
|
30
|
+
import ResourceICUPlurals from './rules/ResourceICUPlurals.js';
|
|
31
|
+
import ResourceQuoteStyle from './rules/ResourceQuoteStyle.js';
|
|
32
|
+
import ResourceRegExpChecker from './rules/ResourceRegExpChecker.js';
|
|
33
|
+
import ResourceUniqueKeys from './rules/ResourceUniqueKeys.js';
|
|
34
|
+
import FormatterFactory from './FormatterFactory.js';
|
|
35
|
+
import RuleSet from './RuleSet.js';
|
|
36
|
+
|
|
37
|
+
const __dirname = Path.dirname(Path.fileUriToPath(import.meta.url));
|
|
38
|
+
log4js.configure(path.join(__dirname, '..', 'log4js.json'));
|
|
39
|
+
|
|
40
|
+
var logger = log4js.getLogger("ilib-lint.root");
|
|
41
|
+
|
|
42
|
+
const optionConfig = {
|
|
43
|
+
help: {
|
|
44
|
+
short: "h",
|
|
45
|
+
help: "This help message",
|
|
46
|
+
showHelp: {
|
|
47
|
+
banner: 'Usage: ilib-lint [-h] [options] path [path ...]',
|
|
48
|
+
output: logger.info
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
config: {
|
|
52
|
+
short: "c",
|
|
53
|
+
help: "Give an explicit path to a configuration file instead of trying to find it in the current directory."
|
|
54
|
+
},
|
|
55
|
+
errorsOnly: {
|
|
56
|
+
short: "e",
|
|
57
|
+
flag: true,
|
|
58
|
+
"default": false,
|
|
59
|
+
help: "Only return errors and supress warnings"
|
|
60
|
+
},
|
|
61
|
+
locales: {
|
|
62
|
+
short: "l",
|
|
63
|
+
"default": "en-AU,en-CA,en-GB,en-IN,en-NG,en-PH,en-PK,en-US,en-ZA,de-DE,fr-CA,fr-FR,es-AR,es-ES,es-MX,id-ID,it-IT,ja-JP,ko-KR,pt-BR,ru-RU,tr-TR,vi-VN,zxx-XX,zh-Hans-CN,zh-Hant-HK,zh-Hant-TW,zh-Hans-SG",
|
|
64
|
+
help: "Locales you want your app to support. Value is a comma-separated list of BCP-47 style locale tags. Default: the top 20 locales on the internet by traffic."
|
|
65
|
+
},
|
|
66
|
+
sourceLocale: {
|
|
67
|
+
short: "s",
|
|
68
|
+
"default": "en-US",
|
|
69
|
+
help: "Default locale used to interpret the strings in the source code or the source strings in resource files."
|
|
70
|
+
},
|
|
71
|
+
quiet: {
|
|
72
|
+
short: "q",
|
|
73
|
+
flag: true,
|
|
74
|
+
help: "Produce no progress output during the run, except for error messages. Instead exit with a return value. Zero indicates no errors, and a positive exit value indicates errors."
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const options = OptionsParser.parse(optionConfig);
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
if (options.args.length < 1) {
|
|
82
|
+
logger.info("Error: missing path parameter");
|
|
83
|
+
OptionsParser.help(optionConfig, {
|
|
84
|
+
banner: 'Usage: iilib-lint [-h] [options] path [path ...]',
|
|
85
|
+
output: logger.info
|
|
86
|
+
});
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
if (!options.opt.quiet) logger.info("ilib-lint - Copyright (c) 2022 JEDLsoft, All rights reserved.");
|
|
92
|
+
|
|
93
|
+
let paths = options.args;
|
|
94
|
+
if (paths.length === 0) {
|
|
95
|
+
paths.push(".");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (options.opt.locales) {
|
|
99
|
+
options.opt.locales = options.opt.locales.split(/,/g);
|
|
100
|
+
}
|
|
101
|
+
// normalize the locale specs
|
|
102
|
+
options.opt.locales = options.opt.locales.map(spec => {
|
|
103
|
+
let loc = new Locale(spec);
|
|
104
|
+
if (!loc.getLanguage()) {
|
|
105
|
+
loc = new Locale("und", loc.getRegion(), loc.getVariant(), loc.getScript());
|
|
106
|
+
}
|
|
107
|
+
return loc.getSpec();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// used if no explicit config is found or given
|
|
111
|
+
const defaultConfig = {
|
|
112
|
+
"name": "ilib-lint",
|
|
113
|
+
"locales": [
|
|
114
|
+
"en-US",
|
|
115
|
+
"de-DE",
|
|
116
|
+
"ja-JP",
|
|
117
|
+
"ko-KR"
|
|
118
|
+
],
|
|
119
|
+
"paths": {
|
|
120
|
+
"**/*.json": {
|
|
121
|
+
"locales": [
|
|
122
|
+
"en-US",
|
|
123
|
+
"de-DE",
|
|
124
|
+
"ja-JP"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
"**/*.xliff": {
|
|
128
|
+
"rules": {
|
|
129
|
+
"resource-icu-plurals": true,
|
|
130
|
+
"resource-quote-style": true,
|
|
131
|
+
"resource-url-match": true,
|
|
132
|
+
"resource-named-params": "localeOnly"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"excludes": [
|
|
137
|
+
"**/.git",
|
|
138
|
+
"**/node_modules",
|
|
139
|
+
"**/.svn",
|
|
140
|
+
"package.json",
|
|
141
|
+
"package-lock.json"
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let config = {};
|
|
146
|
+
if (options.opt.config) {
|
|
147
|
+
if (!fs.existsSync(options.opt.config)) {
|
|
148
|
+
logger.warn(`Config file ${options.opt.config} does not exist. Aborting...`);
|
|
149
|
+
process.exit(2);
|
|
150
|
+
}
|
|
151
|
+
const data = fs.readFileSync(options.opt.config, "utf-8");
|
|
152
|
+
config = json5.parse(data);
|
|
153
|
+
} else {
|
|
154
|
+
config = defaultConfig;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!options.opt.quiet) logger.debug(`Scanning input paths: ${JSON.stringify(paths)}`);
|
|
158
|
+
|
|
159
|
+
let files = [];
|
|
160
|
+
|
|
161
|
+
paths.forEach(pathName => {
|
|
162
|
+
files = files.concat(walk(pathName, {
|
|
163
|
+
quiet: options.opt.quiet,
|
|
164
|
+
config
|
|
165
|
+
}));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const rules = {
|
|
169
|
+
url: {
|
|
170
|
+
name: "resource-url-match",
|
|
171
|
+
description: "Ensure that URLs that appear in the source string are also used in the translated string",
|
|
172
|
+
note: "URL '{matchString}' from the source string does not appear in the target string",
|
|
173
|
+
regexps: [ "((https?|github|ftps?|mailto|file|data|irc):\\/\\/)([\\da-zA-Z\\.-]+)\\.([a-zA-Z\\.]{2,6})([\\/\w\\.-]*)*\\/?" ]
|
|
174
|
+
},
|
|
175
|
+
namedParams: {
|
|
176
|
+
name: "resource-named-params",
|
|
177
|
+
description: "Ensure that named parameters that appear in the source string are also used in the translated string",
|
|
178
|
+
note: "The named parameter '{matchString}' from the source string does not appear in the target string",
|
|
179
|
+
regexps: [ "\\{\\w+\\}" ]
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const defaultRules = new RuleSet([
|
|
184
|
+
new ResourceICUPlurals(),
|
|
185
|
+
new ResourceQuoteStyle(),
|
|
186
|
+
new ResourceRegExpChecker(rules.url),
|
|
187
|
+
new ResourceRegExpChecker(rules.namedParams),
|
|
188
|
+
new ResourceUniqueKeys()
|
|
189
|
+
]);
|
|
190
|
+
const fmt = FormatterFactory(options.opt);
|
|
191
|
+
|
|
192
|
+
// main loop
|
|
193
|
+
let exitValue = 0;
|
|
194
|
+
|
|
195
|
+
files.forEach(file => {
|
|
196
|
+
logger.trace(`Examining ${file.filePath}`);
|
|
197
|
+
file.parse();
|
|
198
|
+
const issues = file.findIssues(defaultRules, options.opt.locales);
|
|
199
|
+
issues.forEach(issue => {
|
|
200
|
+
const str = fmt.format(issue);
|
|
201
|
+
if (str) {
|
|
202
|
+
if (issue.severity === "error") {
|
|
203
|
+
logger.error(str);
|
|
204
|
+
exitValue = 2;
|
|
205
|
+
} else if (!options.opt.errorsOnly) {
|
|
206
|
+
logger.warn(str);
|
|
207
|
+
exitValue = Math.max(exitValue, 1);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
process.exit(exitValue);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* XliffParser.js - common SPI for parser plugins
|
|
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 { ResourceXliff } from 'ilib-tools-common';
|
|
22
|
+
|
|
23
|
+
import Parser from '../Parser.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @class common SPI for parser plugins
|
|
27
|
+
* @abstract
|
|
28
|
+
*/
|
|
29
|
+
class XliffParser extends Parser {
|
|
30
|
+
/**
|
|
31
|
+
* Construct a new plugin.
|
|
32
|
+
*/
|
|
33
|
+
constructor(options) {
|
|
34
|
+
super(options);
|
|
35
|
+
this.path = options.filePath;
|
|
36
|
+
this.xliff = new ResourceXliff({
|
|
37
|
+
path: options.filePath
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse the current file into an intermediate representation.
|
|
43
|
+
*/
|
|
44
|
+
parse() {
|
|
45
|
+
const data = fs.readFileSync(this.path, "utf-8");
|
|
46
|
+
this.xliff.parse(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* For a "resource" type of plugin, this returns a list of Resource instances
|
|
51
|
+
* that result from parsing the file.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Array.<Resource>} list of Resource instances in this file
|
|
54
|
+
*/
|
|
55
|
+
getResources() {
|
|
56
|
+
return this.xliff.getResources();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default XliffParser;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* XliffPlugin.js - plugin that can parse an Xliff file
|
|
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 Plugin from '../Plugin.js';
|
|
21
|
+
import XliffParser from './XliffParser.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @class Plugin that can parse XLIFF files
|
|
25
|
+
*/
|
|
26
|
+
class XliffPlugin extends Plugin {
|
|
27
|
+
/**
|
|
28
|
+
*
|
|
29
|
+
*/
|
|
30
|
+
constructor(options) {
|
|
31
|
+
super(options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @override
|
|
36
|
+
*/
|
|
37
|
+
getType() {
|
|
38
|
+
return "parser";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @override
|
|
43
|
+
*/
|
|
44
|
+
getExtensions() {
|
|
45
|
+
return ["xliff", "xlif", "xlf"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* For a "parser" type of plugin, this returns a list of Parser classes
|
|
50
|
+
* that this plugin implements.
|
|
51
|
+
*
|
|
52
|
+
* @returns {Array.<Parser>} list of Parser classes implemented by this
|
|
53
|
+
* plugin
|
|
54
|
+
*/
|
|
55
|
+
getParsers() {
|
|
56
|
+
return [XliffParser];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default XliffPlugin;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ResourceICUPlurals.js - rule to check formatjs/ICU style plurals 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 { IntlMessageFormat } from 'intl-messageformat';
|
|
21
|
+
import Locale from 'ilib-locale';
|
|
22
|
+
|
|
23
|
+
import Rule from '../Rule.js';
|
|
24
|
+
import Result from '../Result.js';
|
|
25
|
+
|
|
26
|
+
// all the plural categories from CLDR
|
|
27
|
+
const allCategories = ["zero", "one", "two", "few", "many", "other"];
|
|
28
|
+
|
|
29
|
+
// Map the language to the set of plural categories that the language
|
|
30
|
+
// uses. If the language is not listed below, it uses the default
|
|
31
|
+
// list of plurals: "one" and "other"
|
|
32
|
+
const categoriesForLang = {
|
|
33
|
+
"ja": [ "other" ],
|
|
34
|
+
"zh": [ "other" ],
|
|
35
|
+
"ko": [ "other" ],
|
|
36
|
+
"th": [ "other" ],
|
|
37
|
+
"lv": [ "zero", "one", "other" ],
|
|
38
|
+
"ga": [ "one", "two", "other" ],
|
|
39
|
+
"ro": [ "one", "few", "other" ],
|
|
40
|
+
"lt": [ "one", "few", "other" ],
|
|
41
|
+
"ru": [ "one", "few", "other" ],
|
|
42
|
+
"uk": [ "one", "few", "other" ],
|
|
43
|
+
"be": [ "one", "few", "other" ],
|
|
44
|
+
"sr": [ "one", "few", "other" ],
|
|
45
|
+
"hr": [ "one", "few", "other" ],
|
|
46
|
+
"cs": [ "one", "few", "other" ],
|
|
47
|
+
"sk": [ "one", "few", "other" ],
|
|
48
|
+
"pl": [ "one", "few", "other" ],
|
|
49
|
+
"sl": [ "one", "two", "few", "other" ],
|
|
50
|
+
"ar": [ "zero", "one", "two", "few", "many", "other" ]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @class Represent an ilib-lint rule.
|
|
55
|
+
*/
|
|
56
|
+
class ResourceICUPlurals extends Rule {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super(options);
|
|
59
|
+
this.name = "resource-icu-plurals";
|
|
60
|
+
this.description = "Ensure that plurals in translated resources have the correct syntax";
|
|
61
|
+
this.sourceLocale = (options && options.sourceLocale) || "en-US";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getRuleType() {
|
|
65
|
+
return "resource";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
checkPluralCategories(ast, neededCategories, isSource, stringToCheck, key, pathName, source) {
|
|
72
|
+
let value = [];
|
|
73
|
+
for (let i = 0; i < ast.length; i++) {
|
|
74
|
+
const opts = ast[i].options;
|
|
75
|
+
if (opts) {
|
|
76
|
+
// check if any of the needed categories are missing
|
|
77
|
+
const missing = neededCategories.filter(category => {
|
|
78
|
+
return typeof(opts[category]) === 'undefined';
|
|
79
|
+
});
|
|
80
|
+
if ( missing && missing.length ) {
|
|
81
|
+
let opts = {
|
|
82
|
+
severity: "error",
|
|
83
|
+
rule: this,
|
|
84
|
+
description: `Missing plural categories in ${isSource ? "source" : "target"} string: ${missing.join(", ")}. Expecting these: ${neededCategories.join(", ")}`,
|
|
85
|
+
id: key,
|
|
86
|
+
highlight: `${isSource ? "Source" : "Target"}: ${stringToCheck}<e0></e0>`,
|
|
87
|
+
pathName,
|
|
88
|
+
source
|
|
89
|
+
};
|
|
90
|
+
value.push(new Result(opts));
|
|
91
|
+
}
|
|
92
|
+
for (let category in opts) {
|
|
93
|
+
if ( opts[category] && Array.isArray(opts[category].value) ) {
|
|
94
|
+
value = value.concat(this.checkPluralCategories(opts[category].value, neededCategories, isSource, stringToCheck, key, pathName, source));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// now check the other way around. That is, if the categories that exist are not needed.
|
|
98
|
+
if (!isSource) {
|
|
99
|
+
const extras = Object.keys(opts).filter(category => {
|
|
100
|
+
return neededCategories.indexOf(category) < 0;
|
|
101
|
+
});
|
|
102
|
+
if (extras && extras.length) {
|
|
103
|
+
let opts = {
|
|
104
|
+
severity: "warning",
|
|
105
|
+
rule: this,
|
|
106
|
+
description: `Extra plural categories in ${isSource ? "source" : "target"} string: ${extras.join(", ")}. Expecting only these: ${neededCategories.join(", ")}`,
|
|
107
|
+
id: key,
|
|
108
|
+
highlight: `${isSource ? "Source" : "Target"}: ${stringToCheck}<e0></e0>`,
|
|
109
|
+
pathName,
|
|
110
|
+
source
|
|
111
|
+
};
|
|
112
|
+
value.push(new Result(opts));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
checkString(src, tar, file, resource, sourceLocale, targetLocale, lineNumber) {
|
|
121
|
+
const sLoc = new Locale(sourceLocale);
|
|
122
|
+
const tLoc = new Locale(targetLocale);
|
|
123
|
+
let results;
|
|
124
|
+
let problems = [];
|
|
125
|
+
let sourceCategories = [];
|
|
126
|
+
try {
|
|
127
|
+
const imf = new IntlMessageFormat(src, sourceLocale);
|
|
128
|
+
let categories = categoriesForLang[sLoc.getLanguage()] || [ "one", "other" ];
|
|
129
|
+
// look in the abstract syntax tree for the categories that were parsed out and make
|
|
130
|
+
// sure the required ones are there
|
|
131
|
+
const ast = imf.getAst();
|
|
132
|
+
problems = problems.concat(this.checkPluralCategories(ast, categories, true, src, resource.getKey(), file, resource.getSource()));
|
|
133
|
+
if ( ast[0] && ast[0].options ) {
|
|
134
|
+
sourceCategories = Object.keys(ast[0].options).filter(category => {
|
|
135
|
+
// if it is not one of the standard categories, it is a special one, so search for it
|
|
136
|
+
// in the target too
|
|
137
|
+
return allCategories.indexOf(category) < 0;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {
|
|
141
|
+
let value = {
|
|
142
|
+
pathName: file,
|
|
143
|
+
severity: "error",
|
|
144
|
+
rule: this,
|
|
145
|
+
description: `Incorrect plural or select syntax in source string: ${e}`,
|
|
146
|
+
id: resource.getKey(),
|
|
147
|
+
highlight: `Source: ${src.substring(0, e.location.end.offset)}<e0>${src.substring(e.location.end.offset)}</e0>`,
|
|
148
|
+
pathName: file
|
|
149
|
+
};
|
|
150
|
+
if (typeof(lineNumber) !== 'undefined') {
|
|
151
|
+
value.lineNumber = lineNumber + e.location.end.line - 1;
|
|
152
|
+
}
|
|
153
|
+
problems.push(new Result(value));
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const imf = new IntlMessageFormat(tar, targetLocale);
|
|
157
|
+
let categories = categoriesForLang[tLoc.getLanguage()] || [ "one", "other" ];
|
|
158
|
+
if (sourceCategories.length) {
|
|
159
|
+
categories = categories.concat(sourceCategories);
|
|
160
|
+
}
|
|
161
|
+
// look in the abstract syntax tree for the categories that were parsed out and make
|
|
162
|
+
// sure the required ones are there
|
|
163
|
+
const ast = imf.getAst();
|
|
164
|
+
problems = problems.concat(this.checkPluralCategories(ast, categories, false, tar, resource.getKey(), file, resource.getSource()));
|
|
165
|
+
} catch (e) {
|
|
166
|
+
let value = {
|
|
167
|
+
severity: "error",
|
|
168
|
+
description: `Incorrect plural or select syntax in target string: ${e}`,
|
|
169
|
+
rule: this,
|
|
170
|
+
id: resource.getKey(),
|
|
171
|
+
source: src,
|
|
172
|
+
highlight: `Target: ${tar.substring(0, e.location.end.offset)}<e0>${tar.substring(e.location.end.offset)}</e0>`,
|
|
173
|
+
pathName: file
|
|
174
|
+
};
|
|
175
|
+
if (typeof(lineNumber) !== 'undefined') {
|
|
176
|
+
value.lineNumber = lineNumber + e.location.end.line - 1;
|
|
177
|
+
}
|
|
178
|
+
problems.push(new Result(value));
|
|
179
|
+
}
|
|
180
|
+
return problems.length < 2 ? problems[0] : problems;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @override
|
|
185
|
+
*/
|
|
186
|
+
match(options) {
|
|
187
|
+
const { resource, file } = options;
|
|
188
|
+
const sourceLocale = this.sourceLocale;
|
|
189
|
+
let problems = [];
|
|
190
|
+
|
|
191
|
+
switch (resource.getType()) {
|
|
192
|
+
case 'string':
|
|
193
|
+
const tarString = resource.getTarget();
|
|
194
|
+
if (tarString) {
|
|
195
|
+
return this.checkString(resource.getSource(), tarString, file, resource, sourceLocale, options.locale, options.lineNumber);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
|
|
199
|
+
case 'array':
|
|
200
|
+
const srcArray = resource.getSource();
|
|
201
|
+
const tarArray = resource.getTarget();
|
|
202
|
+
if (tarArray) {
|
|
203
|
+
return srcArray.map((item, i) => {
|
|
204
|
+
if (i < tarArray.length && tarArray[i]) {
|
|
205
|
+
return this.checkString(srcArray[i], tarArray[i], file, resource, sourceLocale, options.locale, options.lineNumber);
|
|
206
|
+
}
|
|
207
|
+
}).filter(element => {
|
|
208
|
+
return element;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'plural':
|
|
214
|
+
const srcPlural = resource.getSource();
|
|
215
|
+
const tarPlural = resource.getTarget();
|
|
216
|
+
if (tarPlural) {
|
|
217
|
+
return categories.map(category => {
|
|
218
|
+
return this.checkString(srcPlural.other, tarPlural[category], file, resource, sourceLocale, options.locale, options.lineNumber);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// no match
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default ResourceICUPlurals;
|