ilib-lint 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilib-lint",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
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-common": "^1.1.6",
75
74
  "ilib-lint-common": "^3.4.0",
76
75
  "ilib-tools-common": "^1.17.0",
77
- "ilib-locale": "^1.2.4"
76
+ "ilib-locale": "^1.2.4",
77
+ "ilib-common": "^1.1.6"
78
78
  },
79
79
  "scripts": {
80
80
  "coverage": "pnpm test -- --coverage",
@@ -44,6 +44,7 @@ import ResourceSourceICUUnexplainedParams from '../rules/ResourceSourceICUUnexpl
44
44
  import ResourceXML from '../rules/ResourceXML.js';
45
45
  import ResourceCamelCase from '../rules/ResourceCamelCase.js';
46
46
  import ResourceSnakeCase from '../rules/ResourceSnakeCase.js';
47
+ import ResourceKebabCase from '../rules/ResourceKebabCase.js';
47
48
  import StringFixer from './string/StringFixer.js';
48
49
  import ResourceFixer from './resource/ResourceFixer.js';
49
50
 
@@ -271,6 +272,7 @@ export const builtInRulesets = {
271
272
  "resource-xml": true,
272
273
  "resource-snake-case": true,
273
274
  "resource-camel-case": true,
275
+ "resource-kebab-case": true,
274
276
 
275
277
  // declarative rules from above
276
278
  "resource-url-match": true,
@@ -365,6 +367,7 @@ class BuiltinPlugin extends Plugin {
365
367
  ResourceXML,
366
368
  ResourceCamelCase,
367
369
  ResourceSnakeCase,
370
+ ResourceKebabCase,
368
371
  ...regexRules
369
372
  ];
370
373
  }
@@ -0,0 +1,162 @@
1
+ /*
2
+ * ResourceFullwidthPunctuationSubsetFixer.test.js - test the fixer for fullwidth punctuation characters
3
+ *
4
+ * Copyright © 2025 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 { ResourceString, ResourcePlural, ResourceArray } from 'ilib-tools-common';
21
+ import { IntermediateRepresentation, SourceFile } from "ilib-lint-common";
22
+ import ResourceFixer from "../../../src/plugins/resource/ResourceFixer.js";
23
+ import { regexRules } from '../../../src/plugins/BuiltinPlugin.js';
24
+
25
+ const rule = regexRules.find(rule => rule.name === "resource-no-fullwidth-punctuation-subset");
26
+
27
+ describe("ResourceFullwidthPunctuationSubsetFixer", () => {
28
+ test("fixes fullwidth punctuation in simple string", () => {
29
+ const fixer = new ResourceFixer();
30
+ const resource = new ResourceString({
31
+ key: "test.key",
32
+ source: "Really? Yes! 100%",
33
+ target: "本当? はい! 100%",
34
+ });
35
+ const source = getSource();
36
+ const representation = getIntermediateRepresentation({ source, resource });
37
+ const fixes = getFixesForStringResource(resource);
38
+
39
+ fixer.applyFixes(representation, fixes);
40
+
41
+ expect(resource.getTarget()).toBe("本当? はい! 100%");
42
+ });
43
+
44
+ test("fixes fullwidth punctuation in plural string", () => {
45
+ const fixer = new ResourceFixer();
46
+ const resource = new ResourcePlural({
47
+ key: "test.key",
48
+ source: {
49
+ one: "Really?",
50
+ other: "Really? Yes! 100%"
51
+ },
52
+ target: {
53
+ other: "本当? はい! 100%"
54
+ },
55
+ });
56
+ const source = getSource()
57
+ const representation = getIntermediateRepresentation({ source, resource })
58
+ const fixes = getFixesForObjectResource(resource);
59
+
60
+ fixer.applyFixes(representation, fixes);
61
+
62
+ expect(resource.getTarget().other).toBe("本当? はい! 100%");
63
+ });
64
+
65
+ test("fixes fullwidth punctuation in array string", () => {
66
+ const fixer = new ResourceFixer();
67
+ const resource = new ResourceArray({
68
+ key: "test.key",
69
+ source: ["Really?", "Yes! 100%"],
70
+ target: ["本当?", "はい! 100%"],
71
+ });
72
+ const source = getSource()
73
+ const representation = getIntermediateRepresentation({ source, resource })
74
+ const fixes = getFixesForArrayResource(resource);
75
+
76
+ fixer.applyFixes(representation, fixes);
77
+
78
+ expect(resource.getTarget()).toEqual(["本当?", "はい! 100%"]);
79
+ });
80
+ });
81
+
82
+
83
+ function getIntermediateRepresentation({ source, resource }) {
84
+ return new IntermediateRepresentation({
85
+ sourceFile: source,
86
+ type: "resource",
87
+ ir: [resource],
88
+ dirty: false
89
+ })
90
+ }
91
+
92
+ function getSource() {
93
+ return new SourceFile("test.xliff", {
94
+ sourceLocale: "en-US",
95
+ type: "resource"
96
+ });
97
+ }
98
+
99
+ function getFixesForStringResource(resource) {
100
+ if (!rule || !rule.fixes) return [];
101
+ return rule.fixes.map(fix => {
102
+ const target = resource.getTarget()
103
+ const fullwidthChar = String.fromCharCode(parseInt(fix.search.slice(2), 16));
104
+ const position = target.indexOf(fullwidthChar);
105
+ const hasFullwidthChar = position !== -1;
106
+
107
+ if (hasFullwidthChar) {
108
+ return ResourceFixer.createFix({
109
+ resource,
110
+ commands: [
111
+ ResourceFixer.createStringCommand(position, 1, fix.replace)
112
+ ]
113
+ });
114
+ }
115
+ return null;
116
+ }).filter(fix => fix !== null);
117
+ }
118
+
119
+ function getFixesForObjectResource(resource) {
120
+ if (!rule || !rule.fixes) return [];
121
+ return rule.fixes.map(fix => {
122
+ const target = resource.getTarget().other
123
+ const fullwidthChar = String.fromCharCode(parseInt(fix.search.slice(2), 16));
124
+ const position = target.indexOf(fullwidthChar);
125
+ const hasFullwidthChar = position !== -1;
126
+
127
+ if (hasFullwidthChar) {
128
+ return ResourceFixer.createFix({
129
+ resource,
130
+ category: "other",
131
+ commands: [
132
+ ResourceFixer.createStringCommand(position, 1, fix.replace)
133
+ ]
134
+ });
135
+ }
136
+ return null;
137
+ }).filter(fix => fix !== null)
138
+ }
139
+
140
+ function getFixesForArrayResource(resource) {
141
+ if (!rule || !rule.fixes) return [];
142
+ const targets = resource.getTarget()
143
+
144
+ return targets.flatMap((target, index) => {
145
+ return rule.fixes.map(fix => {
146
+ const fullwidthChar = String.fromCharCode(parseInt(fix.search.slice(2), 16));
147
+ const position = target.indexOf(fullwidthChar);
148
+ const hasFullwidthChar = position !== -1;
149
+
150
+ if (hasFullwidthChar) {
151
+ return ResourceFixer.createFix({
152
+ resource,
153
+ index,
154
+ commands: [
155
+ ResourceFixer.createStringCommand(position, 1, fix.replace)
156
+ ]
157
+ });
158
+ }
159
+ return null;
160
+ }).filter(fix => fix !== null);
161
+ });
162
+ }
@@ -1,5 +1,6 @@
1
1
  import ResourceRule from './ResourceRule.js';
2
2
  import {Result} from 'ilib-lint-common';
3
+ import ResourceFixer from '../plugins/resource/ResourceFixer.js';
3
4
 
4
5
  /** @ignore @typedef {import('ilib-tools-common').Resource} Resource */
5
6
 
@@ -49,7 +50,7 @@ class ResourceCamelCase extends ResourceRule {
49
50
  }
50
51
 
51
52
  if (source !== target) {
52
- return new Result({
53
+ const result = new Result({
53
54
  severity: "error",
54
55
  id: resource.getKey(),
55
56
  source,
@@ -58,10 +59,29 @@ class ResourceCamelCase extends ResourceRule {
58
59
  locale: resource.sourceLocale,
59
60
  pathName: file,
60
61
  highlight: `<e0>${target}</e0>`
61
- })
62
+ });
63
+ result.fix = this.getFix(resource, source);
64
+
65
+ return result;
62
66
  }
63
67
  }
64
68
 
69
+ /**
70
+ * Get the fix for ResourceCamelCase rule
71
+ * @param {Resource} resource The resource to fix
72
+ * @param {string} source The source string that should be used in the target
73
+ * @returns {import('../plugins/resource/ResourceFix.js').default} The fix for ResourceCamelCase rule
74
+ */
75
+ getFix(resource, source) {
76
+ const command = ResourceFixer.createStringCommand(0, resource.getTarget().length, source);
77
+
78
+ return ResourceFixer.createFix({
79
+ resource,
80
+ target: true,
81
+ commands: [command]
82
+ });
83
+ }
84
+
65
85
  /**
66
86
  * @public
67
87
  * @param {string} string A non-empty string to check.
@@ -0,0 +1,102 @@
1
+ import ResourceRule from './ResourceRule.js';
2
+ import {Result} from 'ilib-lint-common';
3
+ import ResourceFixer from '../plugins/resource/ResourceFixer.js';
4
+
5
+ /** @ignore @typedef {import('ilib-tools-common').Resource} Resource */
6
+
7
+ /**
8
+ * @classdesc Class representing an ilib-lint programmatic rule for linting kebab cased strings.
9
+ * @class
10
+ */
11
+ class ResourceKebabCase extends ResourceRule {
12
+ /**
13
+ * Create a ResourceKebabCase rule instance.
14
+ * @param {object} options
15
+ * @param {object} [options.param]
16
+ * @param {string[]} [options.param.except] An array of strings to exclude from the rule.
17
+ */
18
+ constructor(options) {
19
+ super(options);
20
+
21
+ this.name = "resource-kebab-case";
22
+ this.description = "Ensure that when source strings contain only kebab case and no whitespace, then the targets are the same";
23
+ this.link = "https://gihub.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-kebab-case.md";
24
+ this.regexps = [
25
+ "^\\s*[a-zA-Z0-9]*(-[a-zA-Z0-9]+)+\\s*$",
26
+ "^\\s*[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*-\\s*$"
27
+ ];
28
+ this.exceptions = Array.isArray(options?.param?.except) ? options.param.except : [];
29
+ }
30
+
31
+ /**
32
+ * Check if a source string is in kebab case and if the target string is the same as the source.
33
+ * @override
34
+ * @param {{source: (String|undefined), target: (String|undefined), file: String, resource: Resource}} params
35
+ * @returns {Result|undefined} A Result with severity 'error' if the source string is in kebab case and target string is not the same as the source string, otherwise undefined.
36
+ */
37
+ matchString({source, target, file, resource}) {
38
+ if (!source || !target) {
39
+ return;
40
+ }
41
+
42
+ const isException = this.exceptions.includes(source);
43
+ if (isException) {
44
+ return;
45
+ }
46
+
47
+ const isKebabCase = this.isKebabCase(source);
48
+ if (!isKebabCase) {
49
+ return;
50
+ }
51
+
52
+ if (source !== target) {
53
+ const result = new Result({
54
+ severity: "error",
55
+ id: resource.getKey(),
56
+ source,
57
+ description: "Do not translate the source string if it consists solely of kebab cased strings and/or digits. Please update the target string so it matches the source string.",
58
+ rule: this,
59
+ locale: resource.sourceLocale,
60
+ pathName: file,
61
+ highlight: `<e0>${target}</e0>`
62
+ });
63
+ result.fix = this.getFix(resource, source);
64
+ return result;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get the fix for this rule
70
+ * @param {Resource} resource the resource to fix
71
+ * @param {string} source the source string that should be used in the target
72
+ * @returns {import('../plugins/resource/ResourceFix.js').default} the fix for this rule
73
+ */
74
+ getFix(resource, source) {
75
+ const command = ResourceFixer.createStringCommand(0, resource.getTarget().length, source);
76
+ return ResourceFixer.createFix({
77
+ resource,
78
+ target: true,
79
+ commands: [command]
80
+ });
81
+ }
82
+
83
+ /**
84
+ * @public
85
+ * @param {string} string A non-empty string to check.
86
+ * @returns {boolean} Returns true for a string that is in kebab case (matches one of the regular expressions declared in the constructor).
87
+ * Otherwise, returns false.
88
+ */
89
+ isKebabCase(string) {
90
+ const trimmed = string.trim();
91
+ for (const regexp of this.regexps) {
92
+ const match = RegExp(regexp).test(trimmed);
93
+
94
+ if (match) {
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ }
101
+
102
+ export default ResourceKebabCase;
@@ -1,5 +1,6 @@
1
1
  import ResourceRule from './ResourceRule.js';
2
2
  import {Result} from 'ilib-lint-common';
3
+ import ResourceFixer from '../plugins/resource/ResourceFixer.js';
3
4
 
4
5
  /** @ignore @typedef {import('ilib-tools-common').Resource} Resource */
5
6
 
@@ -49,7 +50,7 @@ class ResourceSnakeCase extends ResourceRule {
49
50
  }
50
51
 
51
52
  if (source !== target) {
52
- return new Result({
53
+ const result = new Result({
53
54
  severity: "error",
54
55
  id: resource.getKey(),
55
56
  source,
@@ -58,10 +59,29 @@ class ResourceSnakeCase extends ResourceRule {
58
59
  locale: resource.sourceLocale,
59
60
  pathName: file,
60
61
  highlight: `<e0>${target}</e0>`
61
- })
62
+ });
63
+ result.fix = this.getFix(resource, source);
64
+
65
+ return result;
62
66
  }
63
67
  }
64
68
 
69
+ /**
70
+ * Get the fix for ResourceSnakeCase rule
71
+ * @param {Resource} resource the resource to fix
72
+ * @param {string} source the source string that should be used in the target
73
+ * @returns {import('../plugins/resource/ResourceFix.js').default} the fix for ResourceSnakeCase rule
74
+ */
75
+ getFix(resource, source) {
76
+ const command = ResourceFixer.createStringCommand(0, resource.getTarget().length, source);
77
+
78
+ return ResourceFixer.createFix({
79
+ resource,
80
+ target: true,
81
+ commands: [command]
82
+ });
83
+ }
84
+
65
85
  /**
66
86
  * @public
67
87
  * @param {string} string A non-empty string to check.