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.15.0",
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.17.0"
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 {object} [options.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?.param?.except) ? options.param.except : [];
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 {object} [options.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?.param?.except) ? options.param.except : [];
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 {object} [options.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?.param?.except) ? options.param.except : [];
46
+ ];
47
+ this.exceptions = Array.isArray(options?.except) ? options.except : [];
49
48
  }
50
49
 
51
50
  /**