ilib-lint 2.16.0 → 2.16.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.16.
|
|
3
|
+
"version": "2.16.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"module": "./src/index.js",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"jest": "^29.7.0",
|
|
61
61
|
"jsdoc": "^4.0.3",
|
|
62
62
|
"jsdoc-to-markdown": "^8.0.3",
|
|
63
|
-
"typescript": "^5.5.4"
|
|
63
|
+
"typescript": "^5.5.4",
|
|
64
|
+
"@ilib-mono/e2e-test": "^0.0.0"
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"@formatjs/intl": "^2.10.4",
|
|
@@ -72,15 +73,17 @@
|
|
|
72
73
|
"options-parser": "^0.4.0",
|
|
73
74
|
"xml-js": "^1.6.11",
|
|
74
75
|
"ilib-common": "^1.1.6",
|
|
75
|
-
"ilib-
|
|
76
|
+
"ilib-ctype": "^1.2.2",
|
|
76
77
|
"ilib-locale": "^1.2.4",
|
|
77
|
-
"ilib-
|
|
78
|
+
"ilib-lint-common": "^3.4.0",
|
|
79
|
+
"ilib-tools-common": "^1.19.0"
|
|
78
80
|
},
|
|
79
81
|
"scripts": {
|
|
80
82
|
"coverage": "pnpm test -- --coverage",
|
|
81
83
|
"test": "pnpm test:jest",
|
|
82
84
|
"test:jest": "LANG=en_US.UTF8 node --trace-warnings --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
83
85
|
"test:watch": "pnpm test:jest --watch",
|
|
86
|
+
"test:e2e": "LANG=en_US.UTF8 node --trace-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --config test-e2e/jest.config.cjs",
|
|
84
87
|
"debug": "LANG=en_US.UTF8 node --experimental-vm-modules --inspect-brk node_modules/jest/bin/jest.js -i",
|
|
85
88
|
"lint": "node src/index.js",
|
|
86
89
|
"clean": "git clean -f -d src test",
|
|
@@ -56,8 +56,8 @@ class AnsiConsoleFormatter extends Formatter {
|
|
|
56
56
|
output += ` Source: ${result.source}\n`;
|
|
57
57
|
}
|
|
58
58
|
output += ` ${result.highlight}
|
|
59
|
-
Auto-fix: ${result.fix
|
|
60
|
-
Rule (${result
|
|
59
|
+
Auto-fix: ${!result.fix ? "unavailable" : result.fix.applied ? "\u001B[92mapplied\u001B[0m" : "\u001B[91mnot applied\u001B[0m"}
|
|
60
|
+
Rule (${result?.rule?.getName()}): ${result?.rule?.getDescription()}
|
|
61
61
|
`;
|
|
62
62
|
if (result.locale) {
|
|
63
63
|
output += ` Locale: ${result.locale}\n`;
|
|
@@ -68,7 +68,7 @@ class AnsiConsoleFormatter extends Formatter {
|
|
|
68
68
|
output = output.replace(/<e\d\/>/g, "\u001B[91m␣\u001B[0m");
|
|
69
69
|
output = output.replace(/<e\d>/g, "\u001B[91m");
|
|
70
70
|
output = output.replace(/<\/e\d>/g, "\u001B[0m");
|
|
71
|
-
if (typeof(result
|
|
71
|
+
if (typeof(result?.rule?.getLink) === 'function' && result?.rule?.getLink()) {
|
|
72
72
|
output += ` More info: ${result.rule.getLink()}\n`;
|
|
73
73
|
}
|
|
74
74
|
return output;
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
import { Result } from 'ilib-lint-common';
|
|
35
35
|
import ResourceRule from './ResourceRule.js';
|
|
36
36
|
import Locale from 'ilib-locale';
|
|
37
|
+
import LocaleInfo from 'ilib-localeinfo';
|
|
37
38
|
import ResourceFixer from '../plugins/resource/ResourceFixer.js';
|
|
39
|
+
import { isPunct, isSpace } from 'ilib-ctype';
|
|
38
40
|
|
|
39
41
|
/** @ignore @typedef {import("ilib-tools-common").Resource} Resource */
|
|
40
42
|
|
|
@@ -62,57 +64,77 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
62
64
|
|
|
63
65
|
// Initialize custom punctuation mappings from configuration
|
|
64
66
|
this.customPunctuationMap = {};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
if (options && typeof options === 'object' && !Array.isArray(options)) {
|
|
68
|
+
// options 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
|
+
// Custom maps are stored by language, not locale, so that they apply to all locales of
|
|
72
|
+
// that language.
|
|
73
|
+
for (const locale in options) {
|
|
74
|
+
const localeObj = new Locale(locale);
|
|
75
|
+
|
|
76
|
+
// only process config for valid locales
|
|
77
|
+
if (localeObj.isValid()) {
|
|
78
|
+
const language = localeObj.getLanguage();
|
|
79
|
+
// locale must have a language code
|
|
80
|
+
if (!language) continue;
|
|
81
|
+
// Apply locale-specific defaults for any locale that usesthis language
|
|
82
|
+
const localeDefaults = this.getLocaleDefaults(language);
|
|
83
|
+
this.customPunctuationMap[language] = {
|
|
84
|
+
...localeDefaults,
|
|
85
|
+
...options[locale]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/**
|
|
80
|
-
* Check if the given string ends with any of the
|
|
93
|
+
* Check if the given string ends with any of the configured punctuation patterns
|
|
81
94
|
* @param {string} str - The string to check
|
|
95
|
+
* @param {Locale} locale - The locale code of the string
|
|
82
96
|
* @returns {Object|null} - Object with type and original punctuation, or null if no match
|
|
83
97
|
*/
|
|
84
|
-
getEndingPunctuation(str) {
|
|
98
|
+
getEndingPunctuation(str, locale) {
|
|
85
99
|
if (!str || typeof str !== 'string') return null;
|
|
86
100
|
|
|
87
101
|
const trimmed = str.trim();
|
|
88
102
|
if (!trimmed) return null;
|
|
89
103
|
|
|
90
|
-
//
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{
|
|
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
|
-
}
|
|
104
|
+
// Strip trailing quotes and whitespace to find the actual ending punctuation
|
|
105
|
+
const stripped = ResourceSentenceEnding.stripTrailingQuotesAndWhitespace(trimmed);
|
|
106
|
+
if (!stripped) return null;
|
|
107
|
+
|
|
108
|
+
// Check for ellipsis first (three dots or Unicode ellipsis)
|
|
109
|
+
if (stripped.endsWith('...')) {
|
|
110
|
+
return { type: 'ellipsis', original: '...' };
|
|
111
|
+
}
|
|
112
|
+
if (stripped.endsWith('…')) {
|
|
113
|
+
return { type: 'ellipsis', original: '…' };
|
|
113
114
|
}
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
// Check if the last character is punctuation using isPunct
|
|
117
|
+
const lastChar = stripped.charAt(stripped.length - 1);
|
|
118
|
+
if (!isPunct(lastChar)) return null;
|
|
119
|
+
|
|
120
|
+
// Determine the punctuation type based on the character
|
|
121
|
+
let type = 'period'; // default
|
|
122
|
+
let original = lastChar;
|
|
123
|
+
|
|
124
|
+
// Check for specific punctuation types
|
|
125
|
+
if (lastChar === this.getExpectedPunctuation(locale, 'question')) {
|
|
126
|
+
type = 'question';
|
|
127
|
+
} else if (lastChar === this.getExpectedPunctuation(locale, 'exclamation')) {
|
|
128
|
+
type = 'exclamation';
|
|
129
|
+
} else if (lastChar === this.getExpectedPunctuation(locale, 'colon')) {
|
|
130
|
+
type = 'colon';
|
|
131
|
+
} else if (lastChar === this.getExpectedPunctuation(locale, 'period')) {
|
|
132
|
+
type = 'period';
|
|
133
|
+
} else {
|
|
134
|
+
type = 'unknown';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { type, original };
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
/**
|
|
@@ -132,6 +154,18 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
132
154
|
if (language === 'en' && type === 'ellipsis') {
|
|
133
155
|
return defaults['ellipsis'];
|
|
134
156
|
}
|
|
157
|
+
// Get locale-specific defaults for this language
|
|
158
|
+
const localeDefaults = this.getLocaleDefaults(language);
|
|
159
|
+
const result = localeDefaults[type] || this.getDefaultPunctuation(type);
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get locale-specific defaults for a given language
|
|
165
|
+
* @param {string} language - The language code
|
|
166
|
+
* @returns {Object} - The locale-specific defaults for the language
|
|
167
|
+
*/
|
|
168
|
+
getLocaleDefaults(language) {
|
|
135
169
|
const punctuationMap = {
|
|
136
170
|
'ja': { 'period': '。', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
137
171
|
'zh': { 'period': '。', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
@@ -147,8 +181,26 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
147
181
|
'kn': { 'period': '।', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' },
|
|
148
182
|
'km': { 'period': '។', 'question': '?', 'exclamation': '!', 'ellipsis': '…', 'colon': ':' }
|
|
149
183
|
};
|
|
150
|
-
|
|
151
|
-
|
|
184
|
+
return punctuationMap[language] || defaults;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get a regex that matches all expected punctuation for a given locale
|
|
189
|
+
* @param {Locale} localeObj locale of the punctuation
|
|
190
|
+
* @returns {string} regex string that matches all expected punctuation for the locale
|
|
191
|
+
*/
|
|
192
|
+
getExpectedPunctuationRegex(localeObj) {
|
|
193
|
+
const language = localeObj.getLanguage();
|
|
194
|
+
let config;
|
|
195
|
+
if (language) {
|
|
196
|
+
config = this.customPunctuationMap[language];
|
|
197
|
+
if (!config) {
|
|
198
|
+
config = this.getLocaleDefaults(language);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
config = defaults;
|
|
202
|
+
}
|
|
203
|
+
return Object.values(config).join('').replace(/\./g, '\\.').replace(/\?/g, '\\?');
|
|
152
204
|
}
|
|
153
205
|
|
|
154
206
|
/**
|
|
@@ -203,46 +255,71 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
203
255
|
}
|
|
204
256
|
|
|
205
257
|
/**
|
|
206
|
-
* Get the last
|
|
207
|
-
*
|
|
258
|
+
* Get the last quoted string in the input, or null if none found.
|
|
259
|
+
* Handles all quote types in allQuoteChars.
|
|
208
260
|
* @param {string} str
|
|
209
|
-
* @returns {string}
|
|
261
|
+
* @returns {string|null}
|
|
210
262
|
*/
|
|
211
|
-
static
|
|
212
|
-
if (!str) return
|
|
213
|
-
|
|
214
|
-
// If there's a trailing quote, search backwards for the matching start quote
|
|
263
|
+
static getLastQuotedString(str) {
|
|
264
|
+
if (!str) return null;
|
|
215
265
|
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return ResourceSentenceEnding.getLastSentenceFromContent(quotedContent);
|
|
266
|
+
let lastOpen = -1, lastClose = -1;
|
|
267
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
268
|
+
if (quoteChars.includes(str[i])) {
|
|
269
|
+
lastClose = i;
|
|
270
|
+
// Find the matching opening quote
|
|
271
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
272
|
+
if (quoteChars.includes(str[j])) {
|
|
273
|
+
lastOpen = j;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
227
276
|
}
|
|
277
|
+
break;
|
|
228
278
|
}
|
|
279
|
+
|
|
229
280
|
}
|
|
281
|
+
if (lastOpen !== -1 && lastClose !== -1 && lastOpen < lastClose) {
|
|
282
|
+
return str.substring(lastOpen + 1, lastClose);
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get the last sentence from a string, handling quoted content.
|
|
289
|
+
* For source strings: only return quoted content if the string ends with a quote; otherwise return the full string.
|
|
290
|
+
* For target strings: return the last quoted content anywhere in the string, or the full string if no quotes.
|
|
291
|
+
* @param {string} str
|
|
292
|
+
* @param {boolean} isSource - true if this is a source string, false if target string
|
|
293
|
+
* @param {Locale} targetLocaleObj - locale of the punctuation
|
|
294
|
+
* @returns {string}
|
|
295
|
+
*/
|
|
296
|
+
getLastSentence(str, isSource, targetLocaleObj) {
|
|
297
|
+
if (!str) return str;
|
|
298
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
299
|
+
const trimmedStr = str.trim();
|
|
230
300
|
|
|
231
|
-
|
|
232
|
-
|
|
301
|
+
const lastQuotedString = ResourceSentenceEnding.getLastQuotedString(trimmedStr);
|
|
302
|
+
if (lastQuotedString !== null) {
|
|
303
|
+
return lastQuotedString;
|
|
304
|
+
} else {
|
|
305
|
+
return this.getLastSentenceFromContent(trimmedStr, targetLocaleObj);
|
|
306
|
+
}
|
|
233
307
|
}
|
|
234
308
|
|
|
235
309
|
/**
|
|
236
310
|
* Get the last sentence from content without considering outer quotes
|
|
237
311
|
* @param {string} content
|
|
312
|
+
* @param {Locale} targetLocaleObj
|
|
238
313
|
* @returns {string}
|
|
239
314
|
*/
|
|
240
|
-
|
|
315
|
+
getLastSentenceFromContent(content, targetLocaleObj) {
|
|
241
316
|
if (!content) return content;
|
|
242
317
|
// Only treat .!?。?! as sentence-ending punctuation, not ¿ or ¡
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
318
|
+
const allSentenceEnding = this.getExpectedPunctuationRegex(targetLocaleObj);
|
|
319
|
+
const sentenceEndingRegex = new RegExp(`[^${allSentenceEnding}]+\\p{P}?\\w*$`, 'gu');
|
|
320
|
+
const match = sentenceEndingRegex.exec(content);
|
|
321
|
+
if (match !== null && match.length > 0) {
|
|
322
|
+
let lastSentence = match[0].trim();
|
|
246
323
|
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
247
324
|
const lastChar = lastSentence.charAt(lastSentence.length - 1);
|
|
248
325
|
// If the last sentence ends with a quote, try to extract the last quoted segment
|
|
@@ -266,15 +343,35 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
266
343
|
return content.trim();
|
|
267
344
|
}
|
|
268
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Get Unicode code for a character
|
|
348
|
+
* @param {string} char - The character to get the Unicode code for
|
|
349
|
+
* @returns {string} - The Unicode code in format "U+XXXX"
|
|
350
|
+
*/
|
|
351
|
+
static getUnicodeCode(char) {
|
|
352
|
+
if (!char || char.length === 0) return '';
|
|
353
|
+
const code = char.charCodeAt(0).toString(16).toUpperCase().padStart(4, '0');
|
|
354
|
+
return `U+${code}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Get Unicode codes for a string
|
|
359
|
+
* @param {string} str - The string to get Unicode codes for
|
|
360
|
+
* @returns {string} - The Unicode codes in format "U+XXXX U+YYYY U+ZZZZ"
|
|
361
|
+
*/
|
|
362
|
+
static getUnicodeCodes(str) {
|
|
363
|
+
if (!str) return '';
|
|
364
|
+
return str.split('').map(char => ResourceSentenceEnding.getUnicodeCode(char)).join(' ');
|
|
365
|
+
}
|
|
366
|
+
|
|
269
367
|
/**
|
|
270
368
|
* Check if Spanish target has the correct inverted punctuation at the beginning of the last sentence
|
|
271
|
-
* @param {string} source - The source string
|
|
272
369
|
* @param {string} lastSentence - The last sentence of the target string (already stripped of quotes)
|
|
273
370
|
* @param {string} sourceEndingType - The type of ending punctuation in source
|
|
274
371
|
* @returns {boolean} - True if Spanish target has correct inverted punctuation at start of last sentence
|
|
275
372
|
*/
|
|
276
|
-
hasCorrectSpanishInvertedPunctuation(
|
|
277
|
-
if (!
|
|
373
|
+
hasCorrectSpanishInvertedPunctuation(lastSentence, sourceEndingType) {
|
|
374
|
+
if (!lastSentence || typeof lastSentence !== 'string') return false;
|
|
278
375
|
// Only check for questions and exclamations
|
|
279
376
|
if (sourceEndingType !== 'question' && sourceEndingType !== 'exclamation') {
|
|
280
377
|
return true; // Not applicable for other punctuation types
|
|
@@ -282,7 +379,7 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
282
379
|
// Strip any leading quote characters before checking for inverted punctuation
|
|
283
380
|
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
284
381
|
let strippedSentence = lastSentence;
|
|
285
|
-
while (strippedSentence.length > 0 && quoteChars.includes(strippedSentence.charAt(0))) {
|
|
382
|
+
while (strippedSentence.length > 0 && (quoteChars.includes(strippedSentence.charAt(0)) || isSpace(strippedSentence.charAt(0)))) {
|
|
286
383
|
strippedSentence = strippedSentence.slice(1);
|
|
287
384
|
}
|
|
288
385
|
// Check for inverted punctuation at the beginning of the stripped last sentence
|
|
@@ -293,26 +390,26 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
293
390
|
/**
|
|
294
391
|
* Find the position of the incorrect punctuation in the target string
|
|
295
392
|
* @param {string} target - The target string
|
|
393
|
+
* @param {string} lastSentence - The last sentence from the target
|
|
296
394
|
* @param {string} incorrectPunctuation - The incorrect punctuation to find
|
|
297
395
|
* @returns {Object|null} - Object with position and length, or null if not found
|
|
298
396
|
*/
|
|
299
|
-
findIncorrectPunctuationPosition(target, incorrectPunctuation) {
|
|
300
|
-
if (!target || !incorrectPunctuation) return null;
|
|
397
|
+
findIncorrectPunctuationPosition(target, lastSentence, incorrectPunctuation) {
|
|
398
|
+
if (!target || !lastSentence || !incorrectPunctuation) return null;
|
|
301
399
|
|
|
302
|
-
|
|
303
|
-
if (!stripped) return null;
|
|
304
|
-
|
|
305
|
-
// Find the position of the incorrect punctuation at the end
|
|
400
|
+
// Find the position of the incorrect punctuation in the last sentence
|
|
306
401
|
const punctuationLength = incorrectPunctuation.length;
|
|
307
|
-
const endPosition =
|
|
308
|
-
|
|
309
|
-
if (endPosition >= 0 &&
|
|
310
|
-
//
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
402
|
+
const endPosition = lastSentence.trimEnd().length - punctuationLength;
|
|
403
|
+
|
|
404
|
+
if (endPosition >= 0 && lastSentence.trimEnd().substring(endPosition) === incorrectPunctuation) {
|
|
405
|
+
// Find the position of the last sentence within the target string
|
|
406
|
+
const lastSentenceStart = target.lastIndexOf(lastSentence);
|
|
407
|
+
if (lastSentenceStart !== -1) {
|
|
408
|
+
return {
|
|
409
|
+
position: lastSentenceStart + endPosition,
|
|
410
|
+
length: punctuationLength
|
|
411
|
+
};
|
|
412
|
+
}
|
|
316
413
|
}
|
|
317
414
|
|
|
318
415
|
return null;
|
|
@@ -324,14 +421,55 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
324
421
|
* @param {string} target - The target string
|
|
325
422
|
* @param {string} incorrectPunctuation - The incorrect punctuation
|
|
326
423
|
* @param {string} correctPunctuation - The correct punctuation
|
|
327
|
-
* @returns {Object|
|
|
424
|
+
* @returns {Object|undefined} - The fix object or undefined if no fix can be created
|
|
328
425
|
*/
|
|
329
|
-
createPunctuationFix(resource, target, incorrectPunctuation, correctPunctuation) {
|
|
330
|
-
|
|
331
|
-
|
|
426
|
+
createPunctuationFix(resource, target, incorrectPunctuation, correctPunctuation, index, category, targetLocaleObj) {
|
|
427
|
+
// Get the last sentence to find the position
|
|
428
|
+
const lastSentence = this.getLastSentence(target, false, targetLocaleObj);
|
|
429
|
+
|
|
430
|
+
// If we're adding punctuation (incorrectPunctuation is empty), add it at the end
|
|
431
|
+
if (!incorrectPunctuation && correctPunctuation) {
|
|
432
|
+
return ResourceFixer.createFix({
|
|
433
|
+
resource,
|
|
434
|
+
index,
|
|
435
|
+
category,
|
|
436
|
+
commands: [
|
|
437
|
+
ResourceFixer.createStringCommand(
|
|
438
|
+
target.length,
|
|
439
|
+
0,
|
|
440
|
+
correctPunctuation
|
|
441
|
+
)
|
|
442
|
+
]
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// If we're removing punctuation (correctPunctuation is empty), remove it
|
|
447
|
+
if (incorrectPunctuation && !correctPunctuation) {
|
|
448
|
+
const positionInfo = this.findIncorrectPunctuationPosition(target, lastSentence, incorrectPunctuation);
|
|
449
|
+
if (!positionInfo) return undefined;
|
|
450
|
+
|
|
451
|
+
return ResourceFixer.createFix({
|
|
452
|
+
resource,
|
|
453
|
+
index,
|
|
454
|
+
category,
|
|
455
|
+
commands: [
|
|
456
|
+
ResourceFixer.createStringCommand(
|
|
457
|
+
positionInfo.position,
|
|
458
|
+
positionInfo.length,
|
|
459
|
+
correctPunctuation
|
|
460
|
+
)
|
|
461
|
+
]
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Normal case: replacing punctuation
|
|
466
|
+
const positionInfo = this.findIncorrectPunctuationPosition(target, lastSentence, incorrectPunctuation);
|
|
467
|
+
if (!positionInfo) return undefined;
|
|
332
468
|
|
|
333
469
|
return ResourceFixer.createFix({
|
|
334
470
|
resource,
|
|
471
|
+
index,
|
|
472
|
+
category,
|
|
335
473
|
commands: [
|
|
336
474
|
ResourceFixer.createStringCommand(
|
|
337
475
|
positionInfo.position,
|
|
@@ -342,6 +480,34 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
342
480
|
});
|
|
343
481
|
}
|
|
344
482
|
|
|
483
|
+
/**
|
|
484
|
+
* Create a fix to insert the correct starting punctuation for a Spanish question or exclamation
|
|
485
|
+
* at the beginning of the last sentence.
|
|
486
|
+
*
|
|
487
|
+
* @param {Resource} resource - The resource object
|
|
488
|
+
* @param {string} target - The target string
|
|
489
|
+
* @param {string} lastSentence - The last sentence from the target
|
|
490
|
+
* @param {string} correctPunctuation - The correct punctuation
|
|
491
|
+
* @returns {Object|undefined} - The fix object or undefined if no fix can be created
|
|
492
|
+
*/
|
|
493
|
+
createFixForSpanishInvertedPunctuation(resource, target, lastSentence, correctPunctuation, index, category, targetLocaleObj) {
|
|
494
|
+
const lastSentenceStart = target.lastIndexOf(lastSentence);
|
|
495
|
+
|
|
496
|
+
return ResourceFixer.createFix({
|
|
497
|
+
resource,
|
|
498
|
+
index,
|
|
499
|
+
category,
|
|
500
|
+
commands: [
|
|
501
|
+
// insert the correct punctuation at the beginning of the last sentence
|
|
502
|
+
ResourceFixer.createStringCommand(
|
|
503
|
+
lastSentenceStart,
|
|
504
|
+
0,
|
|
505
|
+
correctPunctuation
|
|
506
|
+
)
|
|
507
|
+
]
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
345
511
|
/**
|
|
346
512
|
* Match the source and target strings for sentence ending punctuation issues
|
|
347
513
|
* @param {Object} params - Parameters object
|
|
@@ -349,81 +515,194 @@ class ResourceSentenceEnding extends ResourceRule {
|
|
|
349
515
|
* @param {string} params.target - The target string
|
|
350
516
|
* @param {Resource} params.resource - The resource object
|
|
351
517
|
* @param {string} params.file - The file path
|
|
352
|
-
* @
|
|
518
|
+
* @param {number} [params.index] - Index for array/plural resources
|
|
519
|
+
* @param {string} [params.category] - Category for plural resources
|
|
520
|
+
* @returns {Result|undefined} - Result object if there's an issue, undefined otherwise
|
|
353
521
|
*/
|
|
354
|
-
matchString({ source, target, resource, file }) {
|
|
355
|
-
if (!source || !target
|
|
356
|
-
|
|
357
|
-
if (!sourceEnding) return undefined;
|
|
522
|
+
matchString({ source, target, resource, file, index, category }) {
|
|
523
|
+
if (!source || !target) return undefined;
|
|
524
|
+
|
|
358
525
|
const targetLocale = resource.getTargetLocale();
|
|
359
526
|
if (!targetLocale) return undefined;
|
|
360
|
-
const
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
527
|
+
const targetLocaleObj = new Locale(targetLocale);
|
|
528
|
+
const targetLanguage = targetLocaleObj.getLanguage();
|
|
529
|
+
if (!targetLanguage) return undefined;
|
|
530
|
+
|
|
531
|
+
const sourceLocale = resource.getSourceLocale();
|
|
532
|
+
if (!sourceLocale) return undefined;
|
|
533
|
+
const sourceLocaleObj = new Locale(sourceLocale);
|
|
534
|
+
const sourceLanguage = sourceLocaleObj.getLanguage();
|
|
535
|
+
if (!sourceLanguage) return undefined;
|
|
536
|
+
|
|
537
|
+
const optionalPunctuationLanguages = ['th', 'lo', 'my', 'km', 'vi', 'id', 'ms', 'tl', 'jv', 'su'];
|
|
538
|
+
const isOptionalPunctuationLanguage = optionalPunctuationLanguages.includes(targetLanguage);
|
|
539
|
+
|
|
540
|
+
// Get the ending punctuation from source and target
|
|
541
|
+
const sourceEnding = this.getEndingPunctuation(source, sourceLocaleObj);
|
|
542
|
+
|
|
543
|
+
// Determine if the source ends with a quote
|
|
544
|
+
const sourceTrimmed = source.trim();
|
|
545
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
546
|
+
const sourceEndsWithQuote = quoteChars.includes(sourceTrimmed.charAt(sourceTrimmed.length - 1));
|
|
547
|
+
|
|
548
|
+
let lastSentence;
|
|
549
|
+
if (sourceEndsWithQuote) {
|
|
550
|
+
// Use the last quoted string in the target
|
|
551
|
+
lastSentence = ResourceSentenceEnding.getLastQuotedString(target) || target.trim();
|
|
552
|
+
} else {
|
|
553
|
+
// Use the full target string
|
|
554
|
+
lastSentence = this.getLastSentenceFromContent(target, targetLocaleObj);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const targetEnding = this.getEndingPunctuation(lastSentence, targetLocaleObj);
|
|
558
|
+
|
|
559
|
+
// Case 1: Source has no punctuation but target does
|
|
560
|
+
if (!sourceEnding && targetEnding) {
|
|
561
|
+
const unicodeCode = ResourceSentenceEnding.getUnicodeCodes(targetEnding.original);
|
|
562
|
+
const positionInfo = this.findIncorrectPunctuationPosition(target, lastSentence, targetEnding.original);
|
|
563
|
+
let highlight = '';
|
|
564
|
+
if (positionInfo) {
|
|
565
|
+
const beforePunctuation = target.substring(0, positionInfo.position);
|
|
566
|
+
const afterPunctuation = target.substring(positionInfo.position + positionInfo.length);
|
|
567
|
+
highlight = `${beforePunctuation}<e0>${targetEnding.original} (${unicodeCode})</e0>${afterPunctuation}`;
|
|
373
568
|
}
|
|
374
569
|
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
570
|
+
// Add prefix for array/plural resources
|
|
571
|
+
if (index !== undefined) {
|
|
572
|
+
highlight = `Target[${index}]: ${highlight}`;
|
|
573
|
+
} else if (category) {
|
|
574
|
+
highlight = `Target(${category}): ${highlight}`;
|
|
380
575
|
}
|
|
381
576
|
|
|
382
|
-
// Spanish target is missing inverted punctuation at the beginning
|
|
383
577
|
return new Result({
|
|
384
578
|
rule: this,
|
|
385
579
|
severity: "warning",
|
|
386
580
|
id: "sentence-ending-punctuation",
|
|
387
|
-
description: `
|
|
581
|
+
description: `Extra sentence ending punctuation "${targetEnding.original}" (${unicodeCode}) for ${targetLocale} locale`,
|
|
388
582
|
source: source,
|
|
389
|
-
highlight:
|
|
583
|
+
highlight: highlight,
|
|
390
584
|
pathName: file,
|
|
585
|
+
fix: this.createPunctuationFix(resource, target, targetEnding.original, '', index, category, targetLocaleObj),
|
|
391
586
|
lineNumber: typeof(resource['lineNumber']) !== 'undefined' ? resource['lineNumber'] : undefined
|
|
392
587
|
});
|
|
393
588
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
589
|
+
|
|
590
|
+
// Case 2: Source has punctuation but target doesn't
|
|
591
|
+
if (sourceEnding && !targetEnding && !isOptionalPunctuationLanguage && sourceEnding.type !== 'unknown') {
|
|
592
|
+
const expectedPunctuation = this.getExpectedPunctuation(targetLocaleObj, sourceEnding.type);
|
|
593
|
+
if (!expectedPunctuation) return undefined;
|
|
594
|
+
|
|
595
|
+
const unicodeCode = ResourceSentenceEnding.getUnicodeCodes(expectedPunctuation);
|
|
596
|
+
let highlight = `${lastSentence}<e0></e0>`;
|
|
597
|
+
|
|
598
|
+
// Add prefix for array/plural resources
|
|
599
|
+
if (index !== undefined) {
|
|
600
|
+
highlight = `Target[${index}]: ${highlight}`;
|
|
601
|
+
} else if (category) {
|
|
602
|
+
highlight = `Target(${category}): ${highlight}`;
|
|
407
603
|
}
|
|
604
|
+
|
|
605
|
+
return new Result({
|
|
606
|
+
rule: this,
|
|
607
|
+
severity: "warning",
|
|
608
|
+
id: "sentence-ending-punctuation",
|
|
609
|
+
description: `Missing sentence ending punctuation for ${targetLocale} locale. It should be "${expectedPunctuation}" (${unicodeCode})`,
|
|
610
|
+
source: source,
|
|
611
|
+
highlight: highlight,
|
|
612
|
+
pathName: file,
|
|
613
|
+
fix: this.createPunctuationFix(resource, target, '', expectedPunctuation, index, category, targetLocaleObj),
|
|
614
|
+
lineNumber: typeof(resource['lineNumber']) !== 'undefined' ? resource['lineNumber'] : undefined
|
|
615
|
+
});
|
|
408
616
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (
|
|
412
|
-
|
|
617
|
+
|
|
618
|
+
// Case 3: Both source and target have punctuation, but they don't match
|
|
619
|
+
if (sourceEnding && targetEnding && sourceEnding.type !== 'unknown') {
|
|
620
|
+
const expectedPunctuation = this.getExpectedPunctuation(targetLocaleObj, sourceEnding.type);
|
|
621
|
+
if (!expectedPunctuation) return undefined;
|
|
622
|
+
|
|
623
|
+
// For Spanish, check for inverted punctuation at the beginning
|
|
624
|
+
if (targetLanguage === 'es' && (sourceEnding.type === 'question' || sourceEnding.type === 'exclamation')) {
|
|
625
|
+
if (!this.hasCorrectSpanishInvertedPunctuation(lastSentence, sourceEnding.type)) {
|
|
626
|
+
// Spanish target is missing inverted punctuation at the beginning
|
|
627
|
+
const quoteChars = ResourceSentenceEnding.allQuoteChars;
|
|
628
|
+
let quotedContentStart = -1;
|
|
629
|
+
for (let i = 0; i < target.length; i++) {
|
|
630
|
+
if (quoteChars.includes(target.charAt(i))) {
|
|
631
|
+
quotedContentStart = i + 1;
|
|
632
|
+
break;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
let highlight;
|
|
637
|
+
if (quotedContentStart !== -1) {
|
|
638
|
+
const beforeQuote = target.substring(0, quotedContentStart);
|
|
639
|
+
const afterQuote = target.substring(quotedContentStart);
|
|
640
|
+
highlight = `${beforeQuote}<e0/>${afterQuote}`;
|
|
641
|
+
} else {
|
|
642
|
+
highlight = `<e0/>${target}`;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Add prefix for array/plural resources
|
|
646
|
+
if (index !== undefined) {
|
|
647
|
+
highlight = `Target[${index}]: ${highlight}`;
|
|
648
|
+
} else if (category) {
|
|
649
|
+
highlight = `Target(${category}): ${highlight}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const invertedChar = sourceEnding.type === 'question' ? '¿' : '¡';
|
|
653
|
+
const unicodeCode = ResourceSentenceEnding.getUnicodeCode(invertedChar);
|
|
654
|
+
return new Result({
|
|
655
|
+
rule: this,
|
|
656
|
+
severity: "warning",
|
|
657
|
+
id: "sentence-ending-punctuation",
|
|
658
|
+
description: `Spanish ${sourceEnding.type} should start with "${invertedChar}" (${unicodeCode}) for ${targetLocale} locale`,
|
|
659
|
+
source: source,
|
|
660
|
+
highlight: highlight,
|
|
661
|
+
pathName: file,
|
|
662
|
+
fix: this.createFixForSpanishInvertedPunctuation(resource, target, lastSentence, invertedChar, index, category, targetLocaleObj),
|
|
663
|
+
lineNumber: typeof(resource['lineNumber']) !== 'undefined' ? resource['lineNumber'] : undefined
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check if the target punctuation matches the expected punctuation for the locale
|
|
669
|
+
if (targetEnding.type === sourceEnding.type && targetEnding.original === expectedPunctuation) {
|
|
670
|
+
return undefined; // Punctuation matches, no issue
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Target has different punctuation than expected
|
|
674
|
+
const unicodeCode = ResourceSentenceEnding.getUnicodeCodes(targetEnding.original);
|
|
675
|
+
const expectedUnicode = ResourceSentenceEnding.getUnicodeCodes(expectedPunctuation);
|
|
676
|
+
const positionInfo = this.findIncorrectPunctuationPosition(target, lastSentence, targetEnding.original);
|
|
677
|
+
let highlight = '';
|
|
678
|
+
if (positionInfo) {
|
|
679
|
+
const beforePunctuation = target.substring(0, positionInfo.position);
|
|
680
|
+
const afterPunctuation = target.substring(positionInfo.position + positionInfo.length);
|
|
681
|
+
highlight = `${beforePunctuation}<e0>${targetEnding.original} (${unicodeCode})</e0>${afterPunctuation}`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Add prefix for array/plural resources
|
|
685
|
+
if (index !== undefined) {
|
|
686
|
+
highlight = `Target[${index}]: ${highlight}`;
|
|
687
|
+
} else if (category) {
|
|
688
|
+
highlight = `Target(${category}): ${highlight}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return new Result({
|
|
692
|
+
rule: this,
|
|
693
|
+
severity: "warning",
|
|
694
|
+
id: "sentence-ending-punctuation",
|
|
695
|
+
description: `Sentence ending punctuation should be "${expectedPunctuation}" (${expectedUnicode}) for ${targetLocale} locale, not "${targetEnding.original}" (${unicodeCode})`,
|
|
696
|
+
source: source,
|
|
697
|
+
highlight: highlight,
|
|
698
|
+
pathName: file,
|
|
699
|
+
fix: this.createPunctuationFix(resource, target, targetEnding.original, expectedPunctuation, index, category, targetLocaleObj),
|
|
700
|
+
lineNumber: typeof(resource['lineNumber']) !== 'undefined' ? resource['lineNumber'] : undefined
|
|
701
|
+
});
|
|
413
702
|
}
|
|
414
703
|
|
|
415
|
-
return
|
|
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
|
-
});
|
|
704
|
+
return undefined;
|
|
426
705
|
}
|
|
427
706
|
}
|
|
428
707
|
|
|
429
|
-
export default ResourceSentenceEnding;
|
|
708
|
+
export default ResourceSentenceEnding;
|