@twin.org/validate-locales 0.0.2-next.21 → 0.0.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.
@@ -1,14 +1,12 @@
1
- import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import { CLIDisplay, CLIUtils, CLIBase } from '@twin.org/cli-core';
4
- import { readFile } from 'node:fs/promises';
5
- import { I18n, Is, GeneralError, StringHelper } from '@twin.org/core';
6
- import { manual } from '@twin.org/nameof-transformer';
7
- import * as glob from 'glob';
8
- import * as ts from 'typescript';
9
-
10
1
  // Copyright 2024 IOTA Stiftung.
11
2
  // SPDX-License-Identifier: Apache-2.0.
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { CLIDisplay, CLIUtils } from "@twin.org/cli-core";
6
+ import { GeneralError, I18n, Is, StringHelper } from "@twin.org/core";
7
+ import { manual } from "@twin.org/nameof-transformer";
8
+ import * as glob from "glob";
9
+ import * as ts from "typescript";
12
10
  const ERROR_TYPES = [
13
11
  { name: "GeneralError", dynamicPropertyIndex: 2 },
14
12
  { name: "GuardError", dynamicPropertyIndex: -1 },
@@ -19,11 +17,21 @@ const ERROR_TYPES = [
19
17
  { name: "UnauthorizedError", dynamicPropertyIndex: 2 },
20
18
  { name: "NotSupportedError", dynamicPropertyIndex: 3, inbuiltProperties: ["methodName"] },
21
19
  { name: "UnprocessableError", dynamicPropertyIndex: 2 },
22
- { name: "ConflictError", dynamicPropertyIndex: 4, inbuiltProperties: ["conflictId", "conflicts"] }
20
+ {
21
+ name: "ConflictError",
22
+ dynamicPropertyIndex: 4,
23
+ inbuiltProperties: ["conflictId", "conflicts"]
24
+ },
25
+ {
26
+ name: "TooManyRequestsError",
27
+ dynamicPropertyIndex: 4,
28
+ inbuiltProperties: ["requestCount", "nextRequestTime"]
29
+ },
30
+ { name: "ForbiddenError", dynamicPropertyIndex: 2 }
23
31
  ];
24
32
  const SKIP_FILES = ["**/models/**/*.ts"];
25
33
  const SKIP_LITERALS = [
26
- /\d+\.\d+\.\d+(-\w+(\.\w+)*)?$/, // Version string
34
+ /^\d+\.\d+\.\d+(-\w+(\.\w+)*)?(-\d)?$/, // Version string
27
35
  /^[^@]+@[^@]+\.[^@]+$/, // Email string
28
36
  /\.json$/i, // ending in .json
29
37
  /\.js$/i, // ending in .js
@@ -33,7 +41,8 @@ const SKIP_LITERALS = [
33
41
  /\.lock$/i, // ending in .lock
34
42
  /\.toml$/i, // ending in .toml
35
43
  /\.{3}/i, // ...
36
- /@twin\.org/ // starting with @twin.org
44
+ /@twin\.org/, // starting with @twin.org
45
+ /^console.log/i // console.log
37
46
  ];
38
47
  const SKIP_METHODS = [/^generateRest/, /^generateSocket/];
39
48
  const CAPTURE_VARIABLES = [/ROUTES_SOURCE/];
@@ -41,7 +50,7 @@ const CAPTURE_VARIABLES = [/ROUTES_SOURCE/];
41
50
  * Build the root command to be consumed by the CLI.
42
51
  * @param program The command to build on.
43
52
  */
44
- function buildCommandValidateLocales(program) {
53
+ export function buildCommandValidateLocales(program) {
45
54
  program
46
55
  .option(I18n.formatMessage("commands.validate-locales.options.source.param"), I18n.formatMessage("commands.validate-locales.options.source.description"), "src/**/*.ts")
47
56
  .option(I18n.formatMessage("commands.validate-locales.options.locales.param"), I18n.formatMessage("commands.validate-locales.options.locales.description"), "locales/**/*.json")
@@ -57,7 +66,7 @@ function buildCommandValidateLocales(program) {
57
66
  * @param opts.locales The locales glob.
58
67
  * @param opts.ignoreFile The ignore file path.
59
68
  */
60
- async function actionCommandValidateLocales(opts) {
69
+ export async function actionCommandValidateLocales(opts) {
61
70
  opts.source = path.resolve(opts.source);
62
71
  opts.locales = path.resolve(opts.locales);
63
72
  opts.ignoreFile = path.resolve(opts.ignoreFile);
@@ -93,7 +102,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
93
102
  const localeEntries = [];
94
103
  let hasQuoteError = false;
95
104
  let hasUnused = false;
96
- let hasMissing = false;
105
+ let hasFailures = false;
97
106
  for (const localeFile of localeFiles) {
98
107
  const dictionary = await CLIUtils.readJsonFile(localeFile);
99
108
  const locale = path.basename(localeFile, path.extname(localeFile));
@@ -118,27 +127,27 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
118
127
  });
119
128
  }
120
129
  }
121
- let missing = [];
130
+ let failures = [];
122
131
  const captureVariables = {};
123
132
  for (const sourceFile of sourceFiles) {
124
133
  const source = await readFile(sourceFile, "utf8");
125
134
  const sourceTs = ts.createSourceFile(sourceFile, source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
126
- visit(sourceTs, sourceTs, localeEntries, missing, captureVariables);
135
+ visit(sourceTs, sourceTs, localeEntries, failures, captureVariables);
127
136
  }
128
- missing = missing.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
129
- if (missing.length === 0) {
137
+ failures = failures.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
138
+ if (failures.length === 0) {
130
139
  CLIDisplay.write(I18n.formatMessage("commands.validate-locales.labels.noMissingLocaleEntries"));
131
140
  CLIDisplay.break();
132
141
  }
133
142
  else {
134
- hasMissing = true;
135
- for (const missingRef of missing) {
136
- if (missingRef.type === "key") {
143
+ hasFailures = true;
144
+ for (const failureRef of failures) {
145
+ if (failureRef.type === "key") {
137
146
  CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.missingLocaleEntry", {
138
- key: missingRef.key,
139
- source: missingRef.source,
140
- line: missingRef.line,
141
- column: missingRef.column
147
+ key: failureRef.key,
148
+ source: failureRef.source,
149
+ line: failureRef.line,
150
+ column: failureRef.column
142
151
  }));
143
152
  }
144
153
  }
@@ -164,7 +173,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
164
173
  }
165
174
  }
166
175
  }
167
- if (hasMissing || hasUnused || hasQuoteError) {
176
+ if (hasFailures || hasUnused || hasQuoteError) {
168
177
  throw new GeneralError("validateLocales", "validationFailed");
169
178
  }
170
179
  }
@@ -173,43 +182,46 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
173
182
  * @param sourceFile The TypeScript source file for position calculations.
174
183
  * @param node The node to visit.
175
184
  * @param localeEntries The locale entries.
176
- * @param missing The missing entries.
185
+ * @param failures The failure entries.
177
186
  * @param captureVariables The capture variables.
178
187
  */
179
- function visit(sourceFile, node, localeEntries, missing, captureVariables) {
188
+ function visit(sourceFile, node, localeEntries, failures, captureVariables) {
180
189
  let handled = false;
181
190
  if (ts.isNewExpression(node) &&
182
191
  ts.isIdentifier(node.expression) &&
183
192
  ERROR_TYPES.some(errorType => errorType.name === node.expression.getText())) {
184
- processErrorType(sourceFile, node, node.expression.text, localeEntries, missing);
193
+ processErrorType(sourceFile, node, node.expression.text, localeEntries, failures);
185
194
  handled = true;
186
195
  }
187
196
  else if (ts.isStringLiteral(node)) {
188
- processStringLiteral(sourceFile, node, localeEntries, missing);
197
+ processStringLiteral(sourceFile, node, localeEntries, failures);
189
198
  handled = true;
190
199
  }
191
200
  else if (ts.isTemplateExpression(node)) {
192
- processTemplateExpression(sourceFile, node, localeEntries, missing);
201
+ processTemplateExpression(sourceFile, node, localeEntries, failures);
193
202
  handled = true;
194
203
  }
195
204
  else if (ts.isCallExpression(node)) {
196
- handled = processCallExpression(sourceFile, node, localeEntries, missing, captureVariables);
205
+ handled = processCallExpression(sourceFile, node, localeEntries, failures, captureVariables);
197
206
  }
198
207
  else if (ts.isFunctionDeclaration(node)) {
199
- handled = processFunctionDeclaration(sourceFile, node);
208
+ handled = processFunctionDeclaration(sourceFile, node, localeEntries, failures);
200
209
  }
201
210
  else if (ts.isVariableDeclaration(node)) {
202
- handled = processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables);
211
+ handled = processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables);
203
212
  }
204
213
  else if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
205
214
  // Don't care about string in imports/exports
206
215
  handled = true;
207
216
  }
217
+ else if (ts.isObjectLiteralExpression(node)) {
218
+ handled = processObjectLiteralExpression(sourceFile, node, localeEntries, failures);
219
+ }
208
220
  else if (ts.isPropertyAssignment(node)) {
209
- handled = processPropertyAssignment(sourceFile, node, localeEntries, missing);
221
+ handled = processPropertyAssignment(sourceFile, node, localeEntries, failures);
210
222
  }
211
223
  if (!handled) {
212
- ts.forEachChild(node, child => visit(sourceFile, child, localeEntries, missing, captureVariables));
224
+ ts.forEachChild(node, child => visit(sourceFile, child, localeEntries, failures, captureVariables));
213
225
  }
214
226
  }
215
227
  /**
@@ -218,12 +230,12 @@ function visit(sourceFile, node, localeEntries, missing, captureVariables) {
218
230
  * @param node The node to process.
219
231
  * @param errorType The error type.
220
232
  * @param localeEntries The locale entries.
221
- * @param missing The missing entries.
233
+ * @param failures The failure entries.
222
234
  */
223
- function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
235
+ function processErrorType(sourceFile, node, errorType, localeEntries, failures) {
224
236
  const errType = ERROR_TYPES.find(e => e.name === errorType);
225
237
  if (Is.object(errType)) {
226
- const localeKey = localeFromClassAndMessage(node.arguments?.[0], node.arguments?.[1], "error");
238
+ const localeKey = localeFromClassAndMessage(sourceFile, node.arguments?.[0], node.arguments?.[1], "error", failures);
227
239
  if (Is.stringValue(localeKey)) {
228
240
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
229
241
  if (Is.object(localeEntry)) {
@@ -232,11 +244,11 @@ function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
232
244
  if (errType.dynamicPropertyIndex !== -1) {
233
245
  usedProperties.push(...getPropertiesFromNode(node.arguments?.[errType.dynamicPropertyIndex]));
234
246
  }
235
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, missing);
247
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, failures);
236
248
  }
237
249
  }
238
250
  else {
239
- missing.push({
251
+ failures.push({
240
252
  type: "key",
241
253
  key: localeKey,
242
254
  source: path.resolve(sourceFile.fileName),
@@ -268,9 +280,9 @@ function findAndReferenceLocale(localeEntries, entryToMatch) {
268
280
  * @param sourceFile The TypeScript source file for position calculations.
269
281
  * @param node The node to process.
270
282
  * @param localeEntries The locale entries.
271
- * @param missing The missing entries.
283
+ * @param failures The failure entries.
272
284
  */
273
- function processStringLiteral(sourceFile, node, localeEntries, missing) {
285
+ function processStringLiteral(sourceFile, node, localeEntries, failures) {
274
286
  if (node.text.length > 3 &&
275
287
  node.text.includes(".") &&
276
288
  !/[ ()/]/.test(node.text) &&
@@ -283,18 +295,18 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
283
295
  if (localeEntry) {
284
296
  localeEntry.referenced = true;
285
297
  const usedProperties = getPropertiesFromNode(node);
286
- checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, missing);
298
+ checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, failures);
287
299
  }
288
300
  if (!localeEntry && ["validation.", "common."].some(t => node.text.startsWith(t))) {
289
301
  localeEntry = findAndReferenceLocale(localeEntries, `error.${node.text}`);
290
302
  if (localeEntry) {
291
303
  localeEntry.referenced = true;
292
304
  const usedProperties = getPropertiesFromNode(node);
293
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, missing);
305
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, failures);
294
306
  }
295
307
  }
296
308
  if (!localeEntry) {
297
- missing.push({
309
+ failures.push({
298
310
  type: "key",
299
311
  key: node.text,
300
312
  source: path.resolve(sourceFile.fileName),
@@ -309,9 +321,9 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
309
321
  * @param sourceFile The TypeScript source file for position calculations.
310
322
  * @param node The node to process.
311
323
  * @param localeEntries The locale entries.
312
- * @param missing The missing entries.
324
+ * @param failures The failure entries.
313
325
  */
314
- function processTemplateExpression(sourceFile, node, localeEntries, missing) {
326
+ function processTemplateExpression(sourceFile, node, localeEntries, failures) {
315
327
  // This case handles templates like `error.${nameof(Class)}.message`
316
328
  const templateParts = extractTemplatePartsWithExpressions(node);
317
329
  // Join all literal text parts to form a potential locale key
@@ -320,14 +332,14 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
320
332
  let localeEntry = findAndReferenceLocale(localeEntries, key);
321
333
  if (localeEntry) {
322
334
  const usedProperties = getPropertiesFromNode(node);
323
- checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, missing);
335
+ checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, failures);
324
336
  }
325
337
  else if (["validation.", "common."].some(t => key.startsWith(t))) {
326
338
  localeEntry = findAndReferenceLocale(localeEntries, `error.${key}`);
327
339
  if (localeEntry) {
328
340
  localeEntry.referenced = true;
329
341
  const usedProperties = getPropertiesFromNode(node.parent.parent);
330
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, missing);
342
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, failures);
331
343
  }
332
344
  }
333
345
  }
@@ -337,14 +349,17 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
337
349
  * @param sourceFile The TypeScript source file for position calculations.
338
350
  * @param node The node to process.
339
351
  * @param localeEntries The locale entries.
340
- * @param missing The missing entries.
352
+ * @param failures The failure entries.
341
353
  * @param captureVariables The capture variables.
342
354
  * @returns True if processed, false otherwise.
343
355
  */
344
- function processCallExpression(sourceFile, node, localeEntries, missing, captureVariables) {
356
+ function processCallExpression(sourceFile, node, localeEntries, failures, captureVariables) {
345
357
  if (ts.isPropertyAccessExpression(node.expression)) {
346
358
  const functionName = node.expression.name.getText();
347
- if (functionName === "log" &&
359
+ if (node.expression.getText() === "console.log") {
360
+ return true;
361
+ }
362
+ else if (functionName === "log" &&
348
363
  node.arguments.length === 1 &&
349
364
  ts.isObjectLiteralExpression(node.arguments[0])) {
350
365
  let level;
@@ -375,14 +390,14 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
375
390
  }
376
391
  }
377
392
  }
378
- const localeKey = localeFromClassAndMessage(source, message, level);
393
+ const localeKey = localeFromClassAndMessage(sourceFile, source, message, level, failures);
379
394
  if (Is.stringValue(localeKey)) {
380
395
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
381
396
  if (Is.object(localeEntry)) {
382
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
397
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
383
398
  }
384
399
  else {
385
- missing.push({
400
+ failures.push({
386
401
  type: "key",
387
402
  key: localeKey,
388
403
  source: path.resolve(sourceFile.fileName),
@@ -390,6 +405,17 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
390
405
  });
391
406
  }
392
407
  }
408
+ else {
409
+ const messageText = message?.getText();
410
+ if (Is.stringValue(messageText)) {
411
+ failures.push({
412
+ type: "key",
413
+ key: messageText,
414
+ source: path.resolve(sourceFile.fileName),
415
+ ...getSourcePosition(sourceFile, node)
416
+ });
417
+ }
418
+ }
393
419
  return true;
394
420
  }
395
421
  else if (functionName === "formatMessage" &&
@@ -401,10 +427,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
401
427
  const dataNames = getPropertiesFromNode(node.arguments[1]);
402
428
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
403
429
  if (Is.object(localeEntry)) {
404
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
430
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
405
431
  }
406
432
  else {
407
- missing.push({
433
+ failures.push({
408
434
  type: "key",
409
435
  key: localeKey,
410
436
  source: path.resolve(sourceFile.fileName),
@@ -422,10 +448,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
422
448
  * @param sourceFile The TypeScript source file for position calculations.
423
449
  * @param node The node to process.
424
450
  * @param localeEntries The locale entries.
425
- * @param missing The missing entries.
451
+ * @param failures The failure entries.
426
452
  * @returns True if processed, false otherwise.
427
453
  */
428
- function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
454
+ function processFunctionDeclaration(sourceFile, node, localeEntries, failures) {
429
455
  if (Is.object(node.name) &&
430
456
  ts.isIdentifier(node.name) &&
431
457
  SKIP_METHODS.some(re => re.test(node.name?.text ?? ""))) {
@@ -438,11 +464,11 @@ function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
438
464
  * @param sourceFile The TypeScript source file for position calculations.
439
465
  * @param node The node to process.
440
466
  * @param localeEntries The locale entries.
441
- * @param missing The missing entries.
467
+ * @param failures The failure entries.
442
468
  * @param captureVariables The capture variables.
443
469
  * @returns True if processed, false otherwise.
444
470
  */
445
- function processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables) {
471
+ function processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables) {
446
472
  if (Is.object(node.name) &&
447
473
  Is.object(node.initializer) &&
448
474
  ts.isIdentifier(node.name) &&
@@ -454,29 +480,106 @@ function processVariableDeclaration(sourceFile, node, localeEntries, missing, ca
454
480
  return false;
455
481
  }
456
482
  /**
457
- * Process a property assignment node.
483
+ * Process an object literal expression node.
458
484
  * @param sourceFile The TypeScript source file for position calculations.
459
485
  * @param node The node to process.
460
486
  * @param localeEntries The locale entries.
461
- * @param missing The missing entries.
487
+ * @param failures The failure entries.
462
488
  * @returns True if processed, false otherwise.
463
489
  */
464
- function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
465
- if (Is.object(node.name) && ts.isIdentifier(node.name) && node.name.getText() === "message") {
466
- const localeKey = getExpandedText(node.initializer);
467
- if (Is.stringValue(localeKey)) {
468
- let localeEntry = findAndReferenceLocale(localeEntries, localeKey);
469
- if (!Is.object(localeEntry)) {
470
- localeEntry = findAndReferenceLocale(localeEntries, `error.${localeKey}`);
490
+ function processObjectLiteralExpression(sourceFile, node, localeEntries, failures) {
491
+ let sourceNode;
492
+ let prefix;
493
+ let descriptionNode;
494
+ let messageNode;
495
+ for (const prop of node.properties) {
496
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
497
+ switch (prop.name.text) {
498
+ case "source":
499
+ if (ts.isPropertyAccessExpression(prop.initializer) &&
500
+ ts.isIdentifier(prop.initializer.expression)) {
501
+ sourceNode = prop.initializer;
502
+ }
503
+ else if (ts.isStringLiteral(prop.initializer) ||
504
+ ts.isTemplateExpression(prop.initializer)) {
505
+ sourceNode = prop.initializer;
506
+ }
507
+ break;
508
+ case "status":
509
+ if (ts.isPropertyAccessExpression(prop.initializer) &&
510
+ ts.isIdentifier(prop.initializer.expression) &&
511
+ prop.initializer.expression.text === "HealthStatus") {
512
+ prefix = "health";
513
+ }
514
+ break;
515
+ case "level":
516
+ if (!Is.stringValue(prefix)) {
517
+ const levelText = getExpandedText(prop.initializer);
518
+ if (Is.stringValue(levelText)) {
519
+ prefix = levelText;
520
+ }
521
+ }
522
+ break;
523
+ case "description":
524
+ descriptionNode = prop.initializer;
525
+ break;
526
+ case "message":
527
+ messageNode = prop.initializer;
528
+ break;
471
529
  }
472
- if (!Is.object(localeEntry)) {
473
- missing.push({
474
- type: "key",
475
- key: localeKey,
476
- source: path.resolve(sourceFile.fileName),
477
- ...getSourcePosition(sourceFile, node)
478
- });
530
+ }
531
+ }
532
+ if (!sourceNode) {
533
+ return false;
534
+ }
535
+ if (Is.stringValue(prefix)) {
536
+ for (const fieldNode of [descriptionNode, messageNode].filter((n) => n !== undefined)) {
537
+ const localeKey = localeFromClassAndMessage(sourceFile, sourceNode, fieldNode, prefix, failures);
538
+ if (Is.stringValue(localeKey)) {
539
+ const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
540
+ if (!Is.object(localeEntry)) {
541
+ failures.push({
542
+ type: "key",
543
+ key: localeKey,
544
+ source: path.resolve(sourceFile.fileName),
545
+ ...getSourcePosition(sourceFile, fieldNode)
546
+ });
547
+ }
479
548
  }
549
+ }
550
+ }
551
+ return true;
552
+ }
553
+ /**
554
+ * Process a property assignment node.
555
+ * @param sourceFile The TypeScript source file for position calculations.
556
+ * @param node The node to process.
557
+ * @param localeEntries The locale entries.
558
+ * @param failures The failure entries.
559
+ * @returns True if processed, false otherwise.
560
+ */
561
+ function processPropertyAssignment(sourceFile, node, localeEntries, failures) {
562
+ if (Is.object(node.name) && ts.isIdentifier(node.name)) {
563
+ if (node.name.getText() === "message") {
564
+ const localeKey = getExpandedText(node.initializer);
565
+ if (Is.stringValue(localeKey)) {
566
+ let localeEntry = findAndReferenceLocale(localeEntries, localeKey);
567
+ if (!Is.object(localeEntry)) {
568
+ localeEntry = findAndReferenceLocale(localeEntries, `error.${localeKey}`);
569
+ }
570
+ if (!Is.object(localeEntry)) {
571
+ failures.push({
572
+ type: "key",
573
+ key: localeKey,
574
+ source: path.resolve(sourceFile.fileName),
575
+ ...getSourcePosition(sourceFile, node)
576
+ });
577
+ }
578
+ return true;
579
+ }
580
+ }
581
+ else if (node.name.getText() === "property") {
582
+ // This handles cases such as { property: 'someProperty.foo' } like we have in entity conditions
480
583
  return true;
481
584
  }
482
585
  }
@@ -529,7 +632,7 @@ function extractTemplatePartsWithExpressions(node) {
529
632
  * @returns True if the template parts are valid, false otherwise.
530
633
  */
531
634
  function hasValidTemplateContent(templateParts) {
532
- return !templateParts.some(part => /[ #,/:=?|]/.test(part));
635
+ return !templateParts.some(part => /[#,/:=?|]/.test(part));
533
636
  }
534
637
  /**
535
638
  * Expand template parts into a single string, processing nameof expressions.
@@ -573,9 +676,9 @@ function expandTemplatePart(templatePart) {
573
676
  * @param localeEntry The locale entry to check against.
574
677
  * @param key The key in the locale entry.
575
678
  * @param usedProperties The properties used in the code.
576
- * @param missing The missing entries.
679
+ * @param failures The failure entries.
577
680
  */
578
- function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, missing) {
681
+ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, failures) {
579
682
  for (const propName of localeEntry.propertyNames) {
580
683
  const propIndex = usedProperties.indexOf(propName);
581
684
  if (propIndex === -1) {
@@ -587,7 +690,7 @@ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties,
587
690
  line: position.line,
588
691
  column: position.column
589
692
  }));
590
- missing.push({
693
+ failures.push({
591
694
  type: "property",
592
695
  key: localeEntry.key,
593
696
  source: path.resolve(sourceFile.fileName),
@@ -673,66 +776,76 @@ function isSkipLiteral(value) {
673
776
  }
674
777
  /**
675
778
  * Get the locale from the class and message nodes.
779
+ * @param sourceFile The TypeScript source file for position calculations.
676
780
  * @param classNode The class node.
677
781
  * @param messageNode The message node.
678
782
  * @param prefix The prefix for the locale key.
783
+ * @param failures The failure entries.
679
784
  * @returns The locale entry or undefined.
680
785
  */
681
- function localeFromClassAndMessage(classNode, messageNode, prefix) {
786
+ function localeFromClassAndMessage(sourceFile, classNode, messageNode, prefix, failures) {
682
787
  if (!classNode || !messageNode) {
683
788
  return undefined;
684
789
  }
685
790
  const classNameParam = classNode.getText();
686
791
  const classNameParamParts = classNameParam?.split(".");
687
792
  if (Is.array(classNameParamParts)) {
688
- const localeKey = getExpandedText(messageNode);
689
- if (Is.stringValue(localeKey)) {
690
- const localeKeyParts = [];
691
- if (Is.stringValue(prefix)) {
692
- localeKeyParts.push(prefix);
793
+ const messageKey = getExpandedText(messageNode);
794
+ if (Is.stringValue(messageKey)) {
795
+ if (messageKey.includes(" ")) {
796
+ // If the message contains spaces then it is not a key
797
+ // but should be replaced by one
798
+ const position = getSourcePosition(sourceFile, messageNode);
799
+ CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.shouldBeKey", {
800
+ value: messageKey,
801
+ source: path.resolve(sourceFile.fileName),
802
+ line: position.line,
803
+ column: position.column
804
+ }));
805
+ failures.push({
806
+ type: "noKey",
807
+ key: messageKey,
808
+ source: path.resolve(sourceFile.fileName),
809
+ ...position
810
+ });
693
811
  }
694
- // If the key is not fully qualified then add the class name
695
- if (!localeKey.includes(".")) {
696
- localeKeyParts.push(expandTemplatePart(classNameParam));
812
+ else {
813
+ const finalKeyParts = [];
814
+ const classNameExpanded = ts.isTemplateExpression(classNode) || ts.isStringLiteral(classNode)
815
+ ? getExpandedText(classNode)
816
+ : expandTemplatePart(classNameParam);
817
+ const messageKeyParts = messageKey.split(".");
818
+ if (messageKeyParts.length === 2 && classNameExpanded === messageKeyParts[0]) {
819
+ // But if it is fully qualified with exactly two segments and starts with the class name
820
+ // then the class name is redundant and should be removed in the source
821
+ const position = getSourcePosition(sourceFile, messageNode);
822
+ CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.noNeedToQualify", {
823
+ key: messageKey,
824
+ property: classNameParam,
825
+ source: path.resolve(sourceFile.fileName),
826
+ line: position.line,
827
+ column: position.column
828
+ }));
829
+ failures.push({
830
+ type: "qualify",
831
+ key: messageKey,
832
+ source: path.resolve(sourceFile.fileName),
833
+ ...position
834
+ });
835
+ }
836
+ else if (!messageKey.includes(".")) {
837
+ // If the key is not fully qualified then add the class name
838
+ // to the final key
839
+ finalKeyParts.push(classNameExpanded);
840
+ }
841
+ finalKeyParts.push(messageKey);
842
+ if (Is.stringValue(prefix)) {
843
+ finalKeyParts.unshift(prefix);
844
+ }
845
+ return finalKeyParts.join(".");
697
846
  }
698
- localeKeyParts.push(localeKey);
699
- return localeKeyParts.join(".");
700
847
  }
701
848
  }
702
849
  return undefined;
703
850
  }
704
-
705
- // Copyright 2024 IOTA Stiftung.
706
- // SPDX-License-Identifier: Apache-2.0.
707
- /**
708
- * The main entry point for the CLI.
709
- */
710
- class CLI extends CLIBase {
711
- /**
712
- * Run the app.
713
- * @param argv The process arguments.
714
- * @param localesDirectory The directory for the locales, default to relative to the script.
715
- * @param options Additional options.
716
- * @param options.overrideOutputWidth Override the output width.
717
- * @returns The exit code.
718
- */
719
- async run(argv, localesDirectory, options) {
720
- return this.execute({
721
- title: "TWIN Validate Locales",
722
- appName: "validate-locales",
723
- version: "0.0.2-next.21", // x-release-please-version
724
- icon: "⚙️ ",
725
- supportsEnvFiles: false,
726
- overrideOutputWidth: options?.overrideOutputWidth
727
- }, localesDirectory ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "../locales"), argv);
728
- }
729
- /**
730
- * Configure any options or actions at the root program level.
731
- * @param program The root program command.
732
- */
733
- configureRoot(program) {
734
- buildCommandValidateLocales(program);
735
- }
736
- }
737
-
738
- export { CLI, actionCommandValidateLocales, buildCommandValidateLocales };
851
+ //# sourceMappingURL=validateLocales.js.map