ilib-lint 2.9.1 → 2.9.2

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.9.1",
3
+ "version": "2.9.2",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "module": "./src/index.js",
@@ -71,10 +71,10 @@
71
71
  "micromatch": "^4.0.7",
72
72
  "options-parser": "^0.4.0",
73
73
  "xml-js": "^1.6.11",
74
- "ilib-lint-common": "^3.2.0",
75
74
  "ilib-common": "^1.1.6",
75
+ "ilib-lint-common": "^3.2.0",
76
76
  "ilib-locale": "^1.2.4",
77
- "ilib-tools-common": "^1.14.0"
77
+ "ilib-tools-common": "^1.15.0"
78
78
  },
79
79
  "scripts": {
80
80
  "coverage": "pnpm test -- --coverage",
package/src/Project.js CHANGED
@@ -324,9 +324,10 @@ class Project extends DirItem {
324
324
  }
325
325
  }
326
326
  }
327
- this.formatter = fmtMgr.get(this.options?.opt?.formatter || this.options.formatter || "ansi-console-formatter");
327
+ const formatterName = this.options?.opt?.formatter || this.options.formatter || "ansi-console-formatter";
328
+ this.formatter = fmtMgr.get(formatterName);
328
329
  if (!this.formatter) {
329
- logger.error(`Could not find formatter ${options.formatter}. Aborting...`);
330
+ logger.error(`Could not find formatter ${formatterName}. Aborting...`);
330
331
  process.exit(3);
331
332
  }
332
333
 
@@ -2,7 +2,7 @@
2
2
  * ResourceICUPluralTranslation.js - rule to check formatjs/ICU style plurals
3
3
  * in the target string actually have translations
4
4
  *
5
- * Copyright © 2023-2024 JEDLSoft
5
+ * Copyright © 2023-2025 JEDLSoft
6
6
  *
7
7
  * Licensed under the Apache License, Version 2.0 (the "License");
8
8
  * you may not use this file except in compliance with the License.
@@ -100,6 +100,25 @@ class ResourceICUPluralTranslation extends ResourceRule {
100
100
  return result;
101
101
  }
102
102
 
103
+ /**
104
+ * Reconstruct the string but only give the text nodes of the given tree so we can
105
+ * see if there is anything to translate.
106
+ * @private
107
+ * @param {Object} nodes the top of the tree to reconstruct
108
+ * @returns {string} the text of the tree
109
+ */
110
+ textNodes(nodes) {
111
+ let result = "";
112
+
113
+ for (let i = 0; i < nodes.length; i++) {
114
+ if (nodes[i].type === 0) {
115
+ result += nodes[i].value;
116
+ }
117
+ }
118
+
119
+ return result.trim();
120
+ }
121
+
103
122
  /**
104
123
  * Traverse an array of ast nodes to find any embedded selects or plurals
105
124
  * or tags, and then process those separately.
@@ -144,6 +163,11 @@ class ResourceICUPluralTranslation extends ResourceRule {
144
163
  const sourcePluralCat = sourcePlural.options[sourceCategory];
145
164
  if (!sourcePluralCat) return; // nothing to check!
146
165
 
166
+ // Only compare the source and target if there is some text there to
167
+ // translate. This will avoid the false positives for the situation where
168
+ // the only thing in the plural category string is just a {variable}.
169
+ if (this.textNodes(sourcePluralCat.value).length === 0) return;
170
+
147
171
  const sourceStr = this.reconstruct(sourcePluralCat.value).replace(/\s+/g, " ").trim();
148
172
  const targetStr = this.reconstruct(targetPlural.options[category].value).replace(/\s+/g, " ").trim();
149
173
  let result = [];
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * ResourceICUPlurals.js - rule to check formatjs/ICU style plurals in the target string
3
3
  *
4
- * Copyright © 2022-2023 JEDLSoft
4
+ * Copyright © 2022-2023, 2025 JEDLSoft
5
5
  *
6
6
  * Licensed under the Apache License, Version 2.0 (the "License");
7
7
  * you may not use this file except in compliance with the License.
@@ -20,34 +20,42 @@
20
20
  import { IntlMessageFormat } from 'intl-messageformat';
21
21
  import Locale from 'ilib-locale';
22
22
  import { Result } from 'ilib-lint-common';
23
+ import { getLanguagePluralCategories } from 'ilib-tools-common';
23
24
 
24
25
  import ResourceRule from './ResourceRule.js';
25
26
 
26
- // all the plural categories from CLDR
27
- const allCategories = ["zero", "one", "two", "few", "many", "other"];
27
+ /**
28
+ * Get the difference of two sets. That is, return a set that contains
29
+ * all the items in set1 that are not in set2.
30
+ * @private
31
+ * @param {Set<string>} set1 The first set
32
+ * @param {Set<string>} set2 The second set
33
+ * @returns {Set<string>} The difference of the two sets
34
+ */
35
+ function difference(set1, set2) {
36
+ const result = new Set();
37
+ set1.forEach(item => {
38
+ if (!set2.has(item)) {
39
+ result.add(item);
40
+ }
41
+ });
42
+ return result;
43
+ }
28
44
 
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" ]
45
+ /**
46
+ * Get the union of two sets. That is, return a set that contains
47
+ * all the items in set1 and set2.
48
+ * @private
49
+ * @param {Set<string>} set1 The first set
50
+ * @param {Set<string>} set2 The second set
51
+ * @returns {Set<string>} The union of the two sets
52
+ */
53
+ function union(set1, set2) {
54
+ const result = new Set(set1);
55
+ set2.forEach(item => {
56
+ result.add(item);
57
+ });
58
+ return result;
51
59
  }
52
60
 
53
61
  /**
@@ -73,31 +81,27 @@ class ResourceICUPlurals extends ResourceRule {
73
81
  // categories that are required according to the language rules
74
82
  let requiredSourceCategories, requiredTargetCategories;
75
83
 
76
- // categories that actually exist in the select which are required by the language rules
77
- let actualRequiredSourceCategories = [], actualRequiredTargetCategories = [];
78
-
79
- // categories that actually exist in the select which are not required by the language rules
80
- let actualNonrequiredSourceCategories = [], actualNonrequiredTargetCategories = [];
81
-
82
84
  if (sourceSelect.node.pluralType === "cardinal") {
83
- requiredSourceCategories = categoriesForLang[srcLocale.getLanguage()] || [ "one", "other" ]
84
- requiredTargetCategories = categoriesForLang[locale.getLanguage()] || [ "one", "other" ];
85
+ requiredSourceCategories = new Set(getLanguagePluralCategories(srcLocale.getLanguage()));
86
+ requiredTargetCategories = new Set(getLanguagePluralCategories(locale.getLanguage()));
85
87
  } else {
86
88
  // for select or selectordinal, only the "other" category is required
87
- requiredSourceCategories = [ "other" ];
88
- requiredTargetCategories = [ "other" ];
89
+ requiredSourceCategories = new Set([ "other" ]);
90
+ requiredTargetCategories = new Set([ "other" ]);
89
91
  }
90
92
 
91
- const allSourceCategories = Object.keys(sourceSelect.node.options);
92
- actualRequiredSourceCategories = allSourceCategories.filter(category => requiredSourceCategories.includes(category));
93
- actualNonrequiredSourceCategories = allSourceCategories.filter(category => !requiredSourceCategories.includes(category));
93
+ const allSourceCategories = new Set(Object.keys(sourceSelect.node.options));
94
+ let actualNonrequiredSourceCategories = difference(allSourceCategories, requiredSourceCategories);
94
95
 
95
- const allTargetCategories = Object.keys(targetSelect.node.options);
96
- actualRequiredTargetCategories = allTargetCategories.filter(category => requiredTargetCategories.includes(category));
97
- actualNonrequiredTargetCategories = allTargetCategories.filter(category => !requiredTargetCategories.includes(category));
96
+ const allTargetCategories = new Set(Object.keys(targetSelect.node.options));
97
+ if (sourceSelect.node.pluralType !== "cardinal") {
98
+ // for select and selectordinal, the target should always have all of the same categories as the source
99
+ requiredTargetCategories = allSourceCategories;
100
+ }
101
+ let actualNonrequiredTargetCategories = difference(allTargetCategories, requiredTargetCategories);
98
102
 
99
103
  // first check the required plural categories
100
- let missing = requiredTargetCategories.filter(category => {
104
+ let missing = Array.from(requiredTargetCategories).filter(category => {
101
105
  if (!targetSelect.node.options[category]) {
102
106
  // if the required category doesn't exist in the target, check if it is required
103
107
  // in the source. If it is required in the source and does not exist there, then
@@ -107,7 +111,7 @@ class ResourceICUPlurals extends ResourceRule {
107
111
  // in the target. If it is not required in the source, then produce a result because
108
112
  // it is required in the target language and it doesn't matter about the source
109
113
  // language.
110
- if (!requiredSourceCategories.includes(category) || sourceSelect.node.options[category]) {
114
+ if (!requiredSourceCategories.has(category) || sourceSelect.node.options[category]) {
111
115
  return true;
112
116
  }
113
117
  } else if (sourceSelect.node.options[category]) {
@@ -135,7 +139,7 @@ class ResourceICUPlurals extends ResourceRule {
135
139
  let opts = {
136
140
  severity: "error",
137
141
  rule: this,
138
- description: `Missing categories in target string: ${missing.join(", ")}. Expecting these: ${requiredTargetCategories.concat(actualNonrequiredSourceCategories).join(", ")}`,
142
+ description: `Missing categories in target string: ${missing.join(", ")}. Expecting these: ${Array.from(union(requiredTargetCategories, actualNonrequiredSourceCategories)).join(", ")}`,
139
143
  id: resource.getKey(),
140
144
  highlight: `Target: ${resource.getTarget()}<e0></e0>`,
141
145
  pathName: resource.getPath(),
@@ -146,34 +150,37 @@ class ResourceICUPlurals extends ResourceRule {
146
150
  }
147
151
 
148
152
  // now deal with the missing non-required categories
149
- missing = actualNonrequiredSourceCategories.filter(category => {
150
- // if it is in the source, but it is not required, it should also be in the target
151
- return !allTargetCategories.includes(category);
152
- });
153
- if (missing.length) {
154
- let opts = {
155
- severity: "warning", // non-required categories get a warning
156
- rule: this,
157
- description: `Missing categories in target string: ${missing.join(", ")}. Expecting these: ${requiredTargetCategories.concat(actualNonrequiredSourceCategories).join(", ")}`,
158
- id: resource.getKey(),
159
- highlight: `Target: ${resource.getTarget()}<e0></e0>`,
160
- pathName: resource.getPath(),
161
- source: resource.getSource(),
162
- locale: resource.getTargetLocale()
163
- };
164
- problems.push(new Result(opts));
165
- }
153
+ if (sourceSelect.node.pluralType === "cardinal") {
154
+ missing = Array.from(actualNonrequiredSourceCategories).filter(category => {
155
+ // if it is in the source, but it is not required, it should also be in the target
156
+ // so give a warning
157
+ return !allTargetCategories.has(category);
158
+ });
159
+ if (missing.length) {
160
+ let opts = {
161
+ severity: "warning", // non-required categories get a warning
162
+ rule: this,
163
+ description: `Missing categories in target string: ${missing.join(", ")}. Expecting these: ${Array.from(union(requiredTargetCategories, actualNonrequiredSourceCategories)).join(", ")}`,
164
+ id: resource.getKey(),
165
+ highlight: `Target: ${resource.getTarget()}<e0></e0>`,
166
+ pathName: resource.getPath(),
167
+ source: resource.getSource(),
168
+ locale: resource.getTargetLocale()
169
+ };
170
+ problems.push(new Result(opts));
171
+ }
172
+ } // else the source categories are already required in the target, so we don't need to check them again
166
173
 
167
174
  // now deal with non-required categories that are in the target but not the source
168
- const extra = actualNonrequiredTargetCategories.filter(category => {
169
- return !allSourceCategories.includes(category);
175
+ const extra = Array.from(actualNonrequiredTargetCategories).filter(category => {
176
+ return !allSourceCategories.has(category);
170
177
  });
171
178
  if (extra.length) {
172
179
  const highlight = resource.getTarget().replace(new RegExp(`(${extra.join("|")})\\s*\\{`, "g"), "<e0>$1</e0> {");
173
180
  let opts = {
174
181
  severity: "warning",
175
182
  rule: this,
176
- description: `Extra categories in target string: ${extra.join(", ")}. Expecting only these: ${requiredTargetCategories.concat(actualNonrequiredSourceCategories).join(", ")}`,
183
+ description: `Extra categories in target string: ${extra.join(", ")}. Expecting only these: ${Array.from(union(requiredTargetCategories, actualNonrequiredSourceCategories)).join(", ")}`,
177
184
  id: resource.getKey(),
178
185
  highlight: `Target: ${highlight}`,
179
186
  pathName: resource.getPath(),
@@ -280,4 +287,4 @@ class ResourceICUPlurals extends ResourceRule {
280
287
  }
281
288
  }
282
289
 
283
- export default ResourceICUPlurals;
290
+ export default ResourceICUPlurals;