ilib-lint 2.15.0 → 2.16.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.
|
|
3
|
+
"version": "2.16.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-lint-common": "^3.4.0",
|
|
75
74
|
"ilib-common": "^1.1.6",
|
|
75
|
+
"ilib-lint-common": "^3.4.0",
|
|
76
76
|
"ilib-locale": "^1.2.4",
|
|
77
|
-
"ilib-tools-common": "^1.
|
|
77
|
+
"ilib-tools-common": "^1.18.0"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"coverage": "pnpm test -- --coverage",
|
|
@@ -31,6 +31,7 @@ import JsonFormatter from '../formatters/JsonFormatter.js';
|
|
|
31
31
|
import ResourceICUPlurals from '../rules/ResourceICUPlurals.js';
|
|
32
32
|
import ResourceICUPluralTranslation from '../rules/ResourceICUPluralTranslation.js';
|
|
33
33
|
import ResourceQuoteStyle from '../rules/ResourceQuoteStyle.js';
|
|
34
|
+
import ResourceSentenceEnding from '../rules/ResourceSentenceEnding.js';
|
|
34
35
|
import ResourceUniqueKeys from '../rules/ResourceUniqueKeys.js';
|
|
35
36
|
import ResourceEdgeWhitespace from '../rules/ResourceEdgeWhitespace.js';
|
|
36
37
|
import ResourceCompleteness from '../rules/ResourceCompleteness.js';
|
|
@@ -440,6 +441,9 @@ export const builtInRulesets = {
|
|
|
440
441
|
"windows": {
|
|
441
442
|
"resource-return-char": true
|
|
442
443
|
},
|
|
444
|
+
"punctuation-checks": {
|
|
445
|
+
"resource-sentence-ending": true
|
|
446
|
+
},
|
|
443
447
|
"tap": {
|
|
444
448
|
"resource-tap-named-params": true
|
|
445
449
|
}
|
|
@@ -498,6 +502,7 @@ class BuiltinPlugin extends Plugin {
|
|
|
498
502
|
ResourceICUPlurals,
|
|
499
503
|
ResourceICUPluralTranslation,
|
|
500
504
|
ResourceQuoteStyle,
|
|
505
|
+
ResourceSentenceEnding,
|
|
501
506
|
ResourceUniqueKeys,
|
|
502
507
|
ResourceEdgeWhitespace,
|
|
503
508
|
ResourceCompleteness,
|
|
@@ -32,8 +32,7 @@ class ResourceCamelCase extends ResourceRule {
|
|
|
32
32
|
/**
|
|
33
33
|
* Create a ResourceCamelCase rule instance.
|
|
34
34
|
* @param {object} options
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {string[]} [options.param.except] An array of strings to exclude from the rule.
|
|
35
|
+
* @param {string[]} [options.except] An array of strings to exclude from the rule.
|
|
37
36
|
*/
|
|
38
37
|
constructor(options) {
|
|
39
38
|
super(options);
|
|
@@ -45,7 +44,7 @@ class ResourceCamelCase extends ResourceRule {
|
|
|
45
44
|
"^\\s*[a-z\\d]+([A-Z][a-z\\d]+)+\\s*$",
|
|
46
45
|
"^\\s*[A-Z][a-z\\d]+([A-Z][a-z\\d]+)+\\s*$",
|
|
47
46
|
];
|
|
48
|
-
this.exceptions = Array.isArray(options?.
|
|
47
|
+
this.exceptions = Array.isArray(options?.except) ? options.except : [];
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
/**
|
|
@@ -12,8 +12,7 @@ class ResourceKebabCase extends ResourceRule {
|
|
|
12
12
|
/**
|
|
13
13
|
* Create a ResourceKebabCase rule instance.
|
|
14
14
|
* @param {object} options
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {string[]} [options.param.except] An array of strings to exclude from the rule.
|
|
15
|
+
* @param {string[]} [options.except] An array of strings to exclude from the rule.
|
|
17
16
|
*/
|
|
18
17
|
constructor(options) {
|
|
19
18
|
super(options);
|
|
@@ -25,7 +24,7 @@ class ResourceKebabCase extends ResourceRule {
|
|
|
25
24
|
"^\\s*[a-zA-Z0-9]*(-[a-zA-Z0-9]+)+\\s*$",
|
|
26
25
|
"^\\s*[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*-\\s*$"
|
|
27
26
|
];
|
|
28
|
-
this.exceptions = Array.isArray(options?.
|
|
27
|
+
this.exceptions = Array.isArray(options?.except) ? options.except : [];
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
/**
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ResourceSentenceEnding.js - rule to check sentence-ending punctuation in the target string
|
|
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
|
+
/**
|
|
21
|
+
* ResourceSentenceEnding - Checks that sentence-ending punctuation is appropriate for the target locale
|
|
22
|
+
*
|
|
23
|
+
* This rule checks if the source string ends with certain punctuation marks and ensures
|
|
24
|
+
* the target uses the locale-appropriate equivalent.
|
|
25
|
+
*
|
|
26
|
+
* Examples:
|
|
27
|
+
* - English period (.) should become Japanese maru (。) in Japanese
|
|
28
|
+
* - English question mark (?) should become Japanese question mark (?) in Japanese
|
|
29
|
+
* - English exclamation mark (!) should become Japanese exclamation mark (!) in Japanese
|
|
30
|
+
* - English ellipsis (...) should become Japanese ellipsis (…) in Japanese
|
|
31
|
+
* - English colon (:) should become Japanese colon (:) in Japanese
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { Result } from 'ilib-lint-common';
|
|
35
|
+
import ResourceRule from './ResourceRule.js';
|
|
36
|
+
import Locale from 'ilib-locale';
|
|
37
|
+
import ResourceFixer from '../plugins/resource/ResourceFixer.js';
|
|
38
|
+
|
|
39
|
+
/** @ignore @typedef {import("ilib-tools-common").Resource} Resource */
|
|
40
|
+
|
|
41
|
+
/** @ignore
|
|
42
|
+
* Default punctuation for each punctuation type
|
|
43
|
+
*/
|
|
44
|
+
const defaults = {
|
|
45
|
+
'period': '.',
|
|
46
|
+
'question': '?',
|
|
47
|
+
'exclamation': '!',
|
|
48
|
+
'ellipsis': '…',
|
|
49
|
+
'colon': ':'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @class ResourceSentenceEnding
|
|
54
|
+
* @extends ResourceRule
|
|
55
|
+
*/
|
|
56
|
+
class ResourceSentenceEnding extends ResourceRule {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super(options);
|
|
59
|
+
this.name = "resource-sentence-ending";
|
|
60
|
+
this.description = "Checks that sentence-ending punctuation is appropriate for the locale of the target string and matches the punctuation in the source string";
|
|
61
|
+
this.link = "https://github.com/iLib-js/ilib-lint/blob/main/docs/resource-sentence-ending.md";
|
|
62
|
+
|
|
63
|
+
// Initialize custom punctuation mappings from configuration
|
|
64
|
+
this.customPunctuationMap = {};
|
|
65
|
+
|
|
66
|
+
if (options && options.param) {
|
|
67
|
+
if (typeof options.param === 'object' && !Array.isArray(options.param)) {
|
|
68
|
+
// param is an object with locale codes as keys and punctuation mappings as values
|
|
69
|
+
// Merge the default punctuation with the custom punctuation so that the custom
|
|
70
|
+
// punctuation overrides the default and we don't have to specify all punctuation types.
|
|
71
|
+
this.customPunctuationMap = {
|
|
72
|
+
...defaults,
|
|
73
|
+
...options.param
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if the given string ends with any of the specified punctuation patterns
|
|
81
|
+
* @param {string} str - The string to check
|
|
82
|
+
* @returns {Object|null} - Object with type and original punctuation, or null if no match
|
|
83
|
+
*/
|
|
84
|
+
getEndingPunctuation(str) {
|
|
85
|
+
if (!str || typeof str !== 'string') return null;
|
|
86
|
+
|
|
87
|
+
const trimmed = str.trim();
|
|
88
|
+
if (!trimmed) return null;
|
|
89
|
+
|
|
90
|
+
// Patterns to match, in order of specificity (longer patterns first)
|
|
91
|
+
const patterns = [
|
|
92
|
+
// Ellipsis patterns (three dots or Unicode ellipsis)
|
|
93
|
+
{ regex: /\.{3}$/, type: 'ellipsis', original: '...' },
|
|
94
|
+
{ regex: /…$/, type: 'ellipsis', original: '…' },
|
|
95
|
+
|
|
96
|
+
// Punctuation followed by quotes
|
|
97
|
+
{ regex: /\.["']$/, type: 'period', original: trimmed.slice(-2) },
|
|
98
|
+
{ regex: /\?["']$/, type: 'question', original: trimmed.slice(-2) },
|
|
99
|
+
{ regex: /!["']$/, type: 'exclamation', original: trimmed.slice(-2) },
|
|
100
|
+
{ regex: /:["']$/, type: 'colon', original: trimmed.slice(-2) },
|
|
101
|
+
|
|
102
|
+
// Single punctuation marks
|
|
103
|
+
{ regex: /\.$/, type: 'period', original: '.' },
|
|
104
|
+
{ regex: /\?$/, type: 'question', original: '?' },
|
|
105
|
+
{ regex: /!$/, type: 'exclamation', original: '!' },
|
|
106
|
+
{ regex: /:$/, type: 'colon', original: ':' }
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const pattern of patterns) {
|
|
110
|
+
if (pattern.regex.test(trimmed)) {
|
|
111
|
+
return pattern;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the expected punctuation for the given locale and punctuation type
|
|
120
|
+
* @param {Locale} localeObj - The parsed locale object
|
|
121
|
+
* @param {string} type - The punctuation type
|
|
122
|
+
* @returns {string|null} - The expected punctuation for the locale, or null if punctuation is optional
|
|
123
|
+
*/
|
|
124
|
+
getExpectedPunctuation(localeObj, type) {
|
|
125
|
+
const language = localeObj.getLanguage();
|
|
126
|
+
if (!language) return null;
|
|
127
|
+
// Custom config
|
|
128
|
+
if (this.customPunctuationMap[language] && this.customPunctuationMap[language][type]) {
|
|
129
|
+
return this.customPunctuationMap[language][type];
|
|
130
|
+
}
|
|
131
|
+
// For English ellipsis, only accept the default (Unicode ellipsis) in the target
|
|
132
|
+
if (language === 'en' && type === 'ellipsis') {
|
|
133
|
+
return defaults['ellipsis'];
|
|
134
|
+
}
|
|
135
|
+
const punctuationMap = {
|
|
136
|
+
'ja': { 'period': '。', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
137
|
+
'zh': { 'period': '。', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
138
|
+
'el': { 'period': '.', 'question': ';', 'exclamation': '!', 'ellipsis': '...', 'colon': ':' },
|
|
139
|
+
'ar': { 'period': '.', 'question': '؟', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
140
|
+
'bo': { 'period': '།', 'question': '།', 'exclamation': '།', 'ellipsis': '…', 'colon': '།' },
|
|
141
|
+
'am': { 'period': '።', 'question': '፧', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
142
|
+
'ur': { 'period': '۔', 'question': '؟', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
143
|
+
'as': { 'period': '।', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
144
|
+
'hi': { 'period': '।', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
145
|
+
'or': { 'period': '।', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
146
|
+
'pa': { 'period': '।', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
147
|
+
'kn': { 'period': '।', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
148
|
+
'km': { 'period': '។', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' }
|
|
149
|
+
};
|
|
150
|
+
const result = punctuationMap[language]?.[type] || this.getDefaultPunctuation(type);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get default punctuation (Western/English style)
|
|
156
|
+
* @param {string} type - The punctuation type
|
|
157
|
+
* @returns {string} - The default punctuation
|
|
158
|
+
*/
|
|
159
|
+
getDefaultPunctuation(type) {
|
|
160
|
+
return defaults[type] || defaults['period'];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Superset of quote characters from ResourceQuoteStyle.js, plus ASCII quotes
|
|
164
|
+
static allQuoteChars = '"' + "'" + "«»‘“”„「」’‚‹›『』";
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Find the last non-quote, non-whitespace character, and return the substring up to and including it.
|
|
168
|
+
* This is used to check the actual sentence-ending punctuation before any trailing quotes or spaces.
|
|
169
|
+
* @param {string} str
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
static stripTrailingQuotesAndWhitespace(str) {
|
|
173
|
+
if (!str) return str;
|
|
174
|
+
// Find the last non-quote, non-whitespace character
|
|
175
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
176
|
+
// This regex matches trailing quotes and whitespace
|
|
177
|
+
const regex = new RegExp(`[${quoteChars.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\s]+$`, 'u');
|
|
178
|
+
// Remove trailing quotes/whitespace
|
|
179
|
+
let trimmed = str.replace(regex, '');
|
|
180
|
+
// Now, find the last non-quote, non-whitespace character
|
|
181
|
+
let i = trimmed.length - 1;
|
|
182
|
+
while (i >= 0 && (quoteChars.includes(trimmed[i]) || /\s/.test(trimmed[i]))) {
|
|
183
|
+
i--;
|
|
184
|
+
}
|
|
185
|
+
return trimmed.substring(0, i + 1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if the target string has the expected ending punctuation
|
|
190
|
+
* @param {string} target - The target string
|
|
191
|
+
* @param {string|string[]} expected - The expected punctuation(s)
|
|
192
|
+
* @param {string} original - The original punctuation from source
|
|
193
|
+
* @returns {boolean} - True if the target has the expected ending
|
|
194
|
+
*/
|
|
195
|
+
hasExpectedEnding(target, expected, original) {
|
|
196
|
+
if (!target || typeof target !== 'string') return false;
|
|
197
|
+
const stripped = target.trim();
|
|
198
|
+
if (!stripped) return false;
|
|
199
|
+
if (Array.isArray(expected)) {
|
|
200
|
+
return expected.some(e => stripped.endsWith(e));
|
|
201
|
+
}
|
|
202
|
+
return stripped.endsWith(expected);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get the last sentence from a string, using period, question, exclamation, or colon as delimiters.
|
|
207
|
+
* For quoted content, extract the actual sentence within the quotes.
|
|
208
|
+
* @param {string} str
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
static getLastSentence(str) {
|
|
212
|
+
if (!str) return str;
|
|
213
|
+
|
|
214
|
+
// If there's a trailing quote, search backwards for the matching start quote
|
|
215
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
216
|
+
const trimmedStr = str.trim();
|
|
217
|
+
const lastChar = trimmedStr.charAt(trimmedStr.length - 1);
|
|
218
|
+
|
|
219
|
+
if (quoteChars.includes(lastChar)) {
|
|
220
|
+
// Find the last matching opening quote
|
|
221
|
+
for (let i = trimmedStr.length - 2; i >= 0; i--) {
|
|
222
|
+
if (quoteChars.includes(trimmedStr.charAt(i))) {
|
|
223
|
+
// Extract content inside the last pair of quotes
|
|
224
|
+
const quotedContent = trimmedStr.substring(i + 1, trimmedStr.length - 1);
|
|
225
|
+
// Now find the last sentence within the quoted content
|
|
226
|
+
return ResourceSentenceEnding.getLastSentenceFromContent(quotedContent);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// If no quotes, use the current algorithm directly
|
|
232
|
+
return ResourceSentenceEnding.getLastSentenceFromContent(trimmedStr);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the last sentence from content without considering outer quotes
|
|
237
|
+
* @param {string} content
|
|
238
|
+
* @returns {string}
|
|
239
|
+
*/
|
|
240
|
+
static getLastSentenceFromContent(content) {
|
|
241
|
+
if (!content) return content;
|
|
242
|
+
// Only treat .!?。?! as sentence-ending punctuation, not ¿ or ¡
|
|
243
|
+
const sentences = content.match(/[^.!?。?!]+[.!?。?!]+(?:\s+|$)/gu);
|
|
244
|
+
if (sentences && sentences.length > 0) {
|
|
245
|
+
let lastSentence = sentences[sentences.length - 1].trim();
|
|
246
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
247
|
+
const lastChar = lastSentence.charAt(lastSentence.length - 1);
|
|
248
|
+
// If the last sentence ends with a quote, try to extract the last quoted segment
|
|
249
|
+
if (quoteChars.includes(lastChar)) {
|
|
250
|
+
// Find the last matching opening quote
|
|
251
|
+
for (let i = lastSentence.length - 2; i >= 0; i--) {
|
|
252
|
+
if (quoteChars.includes(lastSentence.charAt(i))) {
|
|
253
|
+
// Extract content inside the last pair of quotes
|
|
254
|
+
return lastSentence.substring(i + 1, lastSentence.length - 1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// If the last sentence is entirely quoted, extract the content inside the quotes
|
|
259
|
+
const firstChar = lastSentence.charAt(0);
|
|
260
|
+
if (quoteChars.includes(firstChar) && quoteChars.includes(lastChar) && lastSentence.length > 1) {
|
|
261
|
+
lastSentence = lastSentence.substring(1, lastSentence.length - 1);
|
|
262
|
+
}
|
|
263
|
+
return lastSentence;
|
|
264
|
+
}
|
|
265
|
+
// If not matched, return the whole string
|
|
266
|
+
return content.trim();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if Spanish target has the correct inverted punctuation at the beginning of the last sentence
|
|
271
|
+
* @param {string} source - The source string
|
|
272
|
+
* @param {string} lastSentence - The last sentence of the target string (already stripped of quotes)
|
|
273
|
+
* @param {string} sourceEndingType - The type of ending punctuation in source
|
|
274
|
+
* @returns {boolean} - True if Spanish target has correct inverted punctuation at start of last sentence
|
|
275
|
+
*/
|
|
276
|
+
hasCorrectSpanishInvertedPunctuation(source, lastSentence, sourceEndingType) {
|
|
277
|
+
if (!source || !lastSentence || typeof lastSentence !== 'string') return false;
|
|
278
|
+
// Only check for questions and exclamations
|
|
279
|
+
if (sourceEndingType !== 'question' && sourceEndingType !== 'exclamation') {
|
|
280
|
+
return true; // Not applicable for other punctuation types
|
|
281
|
+
}
|
|
282
|
+
// Strip any leading quote characters before checking for inverted punctuation
|
|
283
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
284
|
+
let strippedSentence = lastSentence;
|
|
285
|
+
while (strippedSentence.length > 0 && quoteChars.includes(strippedSentence.charAt(0))) {
|
|
286
|
+
strippedSentence = strippedSentence.slice(1);
|
|
287
|
+
}
|
|
288
|
+
// Check for inverted punctuation at the beginning of the stripped last sentence
|
|
289
|
+
const expectedInverted = sourceEndingType === 'question' ? '¿' : '¡';
|
|
290
|
+
return strippedSentence.startsWith(expectedInverted);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Find the position of the incorrect punctuation in the target string
|
|
295
|
+
* @param {string} target - The target string
|
|
296
|
+
* @param {string} incorrectPunctuation - The incorrect punctuation to find
|
|
297
|
+
* @returns {Object|null} - Object with position and length, or null if not found
|
|
298
|
+
*/
|
|
299
|
+
findIncorrectPunctuationPosition(target, incorrectPunctuation) {
|
|
300
|
+
if (!target || !incorrectPunctuation) return null;
|
|
301
|
+
|
|
302
|
+
const stripped = ResourceSentenceEnding.stripTrailingQuotesAndWhitespace(target.trim());
|
|
303
|
+
if (!stripped) return null;
|
|
304
|
+
|
|
305
|
+
// Find the position of the incorrect punctuation at the end
|
|
306
|
+
const punctuationLength = incorrectPunctuation.length;
|
|
307
|
+
const endPosition = stripped.length - punctuationLength;
|
|
308
|
+
|
|
309
|
+
if (endPosition >= 0 && stripped.substring(endPosition) === incorrectPunctuation) {
|
|
310
|
+
// Calculate the position in the original target string
|
|
311
|
+
const originalEndPosition = target.length - (target.trim().length - stripped.length) - punctuationLength;
|
|
312
|
+
return {
|
|
313
|
+
position: originalEndPosition,
|
|
314
|
+
length: punctuationLength
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Create a fix to replace the incorrect punctuation with the correct one
|
|
323
|
+
* @param {Resource} resource - The resource object
|
|
324
|
+
* @param {string} target - The target string
|
|
325
|
+
* @param {string} incorrectPunctuation - The incorrect punctuation
|
|
326
|
+
* @param {string} correctPunctuation - The correct punctuation
|
|
327
|
+
* @returns {Object|null} - The fix object or null if no fix can be created
|
|
328
|
+
*/
|
|
329
|
+
createPunctuationFix(resource, target, incorrectPunctuation, correctPunctuation) {
|
|
330
|
+
const positionInfo = this.findIncorrectPunctuationPosition(target, incorrectPunctuation);
|
|
331
|
+
if (!positionInfo) return null;
|
|
332
|
+
|
|
333
|
+
return ResourceFixer.createFix({
|
|
334
|
+
resource,
|
|
335
|
+
commands: [
|
|
336
|
+
ResourceFixer.createStringCommand(
|
|
337
|
+
positionInfo.position,
|
|
338
|
+
positionInfo.length,
|
|
339
|
+
correctPunctuation
|
|
340
|
+
)
|
|
341
|
+
]
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Match the source and target strings for sentence ending punctuation issues
|
|
347
|
+
* @param {Object} params - Parameters object
|
|
348
|
+
* @param {string} params.source - The source string
|
|
349
|
+
* @param {string} params.target - The target string
|
|
350
|
+
* @param {Resource} params.resource - The resource object
|
|
351
|
+
* @param {string} params.file - The file path
|
|
352
|
+
* @returns {Result|undefined} - A Result object if there's an issue, undefined otherwise
|
|
353
|
+
*/
|
|
354
|
+
matchString({ source, target, resource, file }) {
|
|
355
|
+
if (!source || !target || !resource) return undefined;
|
|
356
|
+
const sourceEnding = this.getEndingPunctuation(source);
|
|
357
|
+
if (!sourceEnding) return undefined;
|
|
358
|
+
const targetLocale = resource.getTargetLocale();
|
|
359
|
+
if (!targetLocale) return undefined;
|
|
360
|
+
const localeObj = new Locale(targetLocale);
|
|
361
|
+
const language = localeObj.getLanguage();
|
|
362
|
+
if (!language) return undefined;
|
|
363
|
+
const optionalPunctuationLanguages = ['th', 'lo', 'my'];
|
|
364
|
+
if (optionalPunctuationLanguages.includes(language)) return undefined;
|
|
365
|
+
// Pass sourceEnding.original to getExpectedPunctuation for ellipsis
|
|
366
|
+
const expectedPunctuation = this.getExpectedPunctuation(localeObj, sourceEnding.type);
|
|
367
|
+
if (!expectedPunctuation) return undefined;
|
|
368
|
+
|
|
369
|
+
const strippedTarget = ResourceSentenceEnding.stripTrailingQuotesAndWhitespace(target.trim());
|
|
370
|
+
if (this.hasExpectedEnding(strippedTarget, expectedPunctuation, sourceEnding.original)) {
|
|
371
|
+
if (language !== 'es') {
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// For Spanish, also check for inverted punctuation at the beginning of the last sentence
|
|
376
|
+
// Call getLastSentence with the original target to properly handle quoted content
|
|
377
|
+
const lastSentence = ResourceSentenceEnding.getLastSentence(target);
|
|
378
|
+
if (this.hasCorrectSpanishInvertedPunctuation(source, lastSentence, sourceEnding.type)) {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Spanish target is missing inverted punctuation at the beginning
|
|
383
|
+
return new Result({
|
|
384
|
+
rule: this,
|
|
385
|
+
severity: "warning",
|
|
386
|
+
id: "sentence-ending-punctuation",
|
|
387
|
+
description: `Spanish ${sourceEnding.type} should start with "${sourceEnding.type === 'question' ? '¿' : '¡'}" for ${targetLocale} locale`,
|
|
388
|
+
source: source,
|
|
389
|
+
highlight: `Spanish ${sourceEnding.type} should start with "${sourceEnding.type === 'question' ? '¿' : '¡'}"`,
|
|
390
|
+
pathName: file,
|
|
391
|
+
lineNumber: typeof(resource['lineNumber']) !== 'undefined' ? resource['lineNumber'] : undefined
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
let actualPunctuation = null;
|
|
395
|
+
const patterns = [
|
|
396
|
+
{ regex: /\.{3}$/, punctuation: '...' },
|
|
397
|
+
{ regex: /…$/, punctuation: '…' },
|
|
398
|
+
{ regex: /\.$/, punctuation: '.' },
|
|
399
|
+
{ regex: /\?$/, punctuation: '?' },
|
|
400
|
+
{ regex: /!$/, punctuation: '!' },
|
|
401
|
+
{ regex: /:$/, punctuation: ':' }
|
|
402
|
+
];
|
|
403
|
+
for (const pattern of patterns) {
|
|
404
|
+
if (pattern.regex.test((target || '').trim())) {
|
|
405
|
+
actualPunctuation = pattern.punctuation;
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// For English ellipsis, prefer the form used in the source for the fix
|
|
410
|
+
let fix = null;
|
|
411
|
+
if (actualPunctuation && actualPunctuation !== expectedPunctuation) {
|
|
412
|
+
fix = this.createPunctuationFix(resource, target, actualPunctuation, expectedPunctuation);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return new Result({
|
|
416
|
+
rule: this,
|
|
417
|
+
severity: "warning",
|
|
418
|
+
id: "sentence-ending-punctuation",
|
|
419
|
+
description: `Sentence ending punctuation should be "${expectedPunctuation}" for ${targetLocale} locale, not "${(actualPunctuation ?? sourceEnding.original)}"`,
|
|
420
|
+
source: source,
|
|
421
|
+
highlight: '',
|
|
422
|
+
pathName: file,
|
|
423
|
+
fix,
|
|
424
|
+
lineNumber: typeof(resource['lineNumber']) !== 'undefined' ? resource['lineNumber'] : undefined
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export default ResourceSentenceEnding;
|
|
@@ -32,20 +32,19 @@ class ResourceSnakeCase extends ResourceRule {
|
|
|
32
32
|
/**
|
|
33
33
|
* Create a ResourceSnakeCase rule instance.
|
|
34
34
|
* @param {object} options
|
|
35
|
-
* @param {
|
|
36
|
-
* @param {string[]} [options.param.except] An array of strings to exclude from the rule.
|
|
35
|
+
* @param {string[]} [options.except] An array of strings to exclude from the rule.
|
|
37
36
|
*/
|
|
38
37
|
constructor(options) {
|
|
39
38
|
super(options);
|
|
40
39
|
|
|
41
40
|
this.name = "resource-snake-case";
|
|
42
41
|
this.description = "Ensure that when source strings contain only snake case and no whitespace, then the targets are the same";
|
|
43
|
-
this.link = "https://gihub.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-snake-case.md"
|
|
42
|
+
this.link = "https://gihub.com/iLib-js/ilib-mono/blob/main/packages/ilib-lint/docs/resource-snake-case.md";
|
|
44
43
|
this.regexps = [
|
|
45
44
|
"^\\s*[a-zA-Z0-9]*(_[a-zA-Z0-9]+)+\\s*$",
|
|
46
45
|
"^\\s*[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*_\\s*$"
|
|
47
|
-
]
|
|
48
|
-
this.exceptions = Array.isArray(options?.
|
|
46
|
+
];
|
|
47
|
+
this.exceptions = Array.isArray(options?.except) ? options.except : [];
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
/**
|