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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +231 -0
  3. package/docs/AnsiConsoleFormatter.html +467 -0
  4. package/docs/Formatter.html +577 -0
  5. package/docs/Formatter.js.html +135 -0
  6. package/docs/FormatterFactory.html +191 -0
  7. package/docs/FormatterFactory.js.html +109 -0
  8. package/docs/Parser.html +483 -0
  9. package/docs/Parser.js.html +122 -0
  10. package/docs/ParserFactory.js.html +109 -0
  11. package/docs/Plugin.html +847 -0
  12. package/docs/Plugin.js.html +168 -0
  13. package/docs/PluginManager.html +541 -0
  14. package/docs/PluginManager.js.html +125 -0
  15. package/docs/ResourceICUPlurals.html +278 -0
  16. package/docs/ResourceQuoteStyle.html +278 -0
  17. package/docs/ResourceRegExpChecker.html +295 -0
  18. package/docs/ResourceUniqueKeys.html +278 -0
  19. package/docs/Result.html +263 -0
  20. package/docs/Result.js.html +130 -0
  21. package/docs/Rule.html +774 -0
  22. package/docs/Rule.js.html +230 -0
  23. package/docs/RuleSet.html +760 -0
  24. package/docs/RuleSet.js.html +153 -0
  25. package/docs/SourceFile.html +826 -0
  26. package/docs/SourceFile.js.html +232 -0
  27. package/docs/XliffParser.html +396 -0
  28. package/docs/XliffPlugin.html +472 -0
  29. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  30. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  31. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  32. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  33. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  34. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  35. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  36. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  37. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  38. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  39. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  40. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  44. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  45. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  46. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  47. package/docs/formatters_AnsiConsoleFormatter.js.html +147 -0
  48. package/docs/global.html +448 -0
  49. package/docs/ilibLint.md +1013 -0
  50. package/docs/index.html +81 -0
  51. package/docs/plugins_XliffParser.js.html +129 -0
  52. package/docs/plugins_XliffPlugin.js.html +129 -0
  53. package/docs/rules_ResourceICUPlurals.js.html +297 -0
  54. package/docs/rules_ResourceQuoteStyle.js.html +238 -0
  55. package/docs/rules_ResourceRegExpChecker.js.html +248 -0
  56. package/docs/rules_ResourceUniqueKeys.js.html +144 -0
  57. package/docs/scripts/collapse.js +20 -0
  58. package/docs/scripts/linenumber.js +25 -0
  59. package/docs/scripts/nav.js +12 -0
  60. package/docs/scripts/polyfill.js +4 -0
  61. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  62. package/docs/scripts/prettify/lang-css.js +2 -0
  63. package/docs/scripts/prettify/prettify.js +28 -0
  64. package/docs/scripts/search.js +83 -0
  65. package/docs/styles/jsdoc.css +765 -0
  66. package/docs/styles/prettify.css +79 -0
  67. package/docs/walk.js.html +214 -0
  68. package/log4js.json +21 -0
  69. package/package.json +83 -0
  70. package/src/Formatter.js +66 -0
  71. package/src/FormatterFactory.js +41 -0
  72. package/src/Parser.js +53 -0
  73. package/src/ParserFactory.js +41 -0
  74. package/src/Plugin.js +99 -0
  75. package/src/PluginManager.js +56 -0
  76. package/src/Result.js +62 -0
  77. package/src/Rule.js +162 -0
  78. package/src/RuleSet.js +84 -0
  79. package/src/SourceFile.js +163 -0
  80. package/src/formatters/AnsiConsoleFormatter.js +78 -0
  81. package/src/index.js +213 -0
  82. package/src/plugins/XliffParser.js +60 -0
  83. package/src/plugins/XliffPlugin.js +60 -0
  84. package/src/rules/ResourceICUPlurals.js +229 -0
  85. package/src/rules/ResourceQuoteStyle.js +170 -0
  86. package/src/rules/ResourceRegExpChecker.js +179 -0
  87. package/src/rules/ResourceUniqueKeys.js +76 -0
  88. package/src/rules/utils.js +78 -0
  89. 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;