@twin.org/validate-locales 0.0.2-next.21 → 0.0.3-next.1

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/bin/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright 2024 IOTA Stiftung.
3
3
  // SPDX-License-Identifier: Apache-2.0.
4
- import { CLI } from '../dist/esm/index.mjs';
4
+ import { CLI } from '../dist/es/index.js';
5
5
 
6
6
  const cli = new CLI();
7
7
  const result = await cli.run(process.argv);
package/dist/es/cli.js ADDED
@@ -0,0 +1,37 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CLIBase } from "@twin.org/cli-core";
6
+ import { buildCommandValidateLocales } from "./commands/validateLocales.js";
7
+ /**
8
+ * The main entry point for the CLI.
9
+ */
10
+ export class CLI extends CLIBase {
11
+ /**
12
+ * Run the app.
13
+ * @param argv The process arguments.
14
+ * @param localesDirectory The directory for the locales, default to relative to the script.
15
+ * @param options Additional options.
16
+ * @param options.overrideOutputWidth Override the output width.
17
+ * @returns The exit code.
18
+ */
19
+ async run(argv, localesDirectory, options) {
20
+ return this.execute({
21
+ title: "TWIN Validate Locales",
22
+ appName: "validate-locales",
23
+ version: "0.0.3-next.1", // x-release-please-version
24
+ icon: "⚙️ ",
25
+ supportsEnvFiles: false,
26
+ overrideOutputWidth: options?.overrideOutputWidth
27
+ }, localesDirectory ?? path.join(path.dirname(fileURLToPath(import.meta.url)), "../locales"), argv);
28
+ }
29
+ /**
30
+ * Configure any options or actions at the root program level.
31
+ * @param program The root program command.
32
+ */
33
+ configureRoot(program) {
34
+ buildCommandValidateLocales(program);
35
+ }
36
+ }
37
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAE5E;;GAEG;AACH,MAAM,OAAO,GAAI,SAAQ,OAAO;IAC/B;;;;;;;OAOG;IACI,KAAK,CAAC,GAAG,CACf,IAAc,EACd,gBAAyB,EACzB,OAA0C;QAE1C,OAAO,IAAI,CAAC,OAAO,CAClB;YACC,KAAK,EAAE,uBAAuB;YAC9B,OAAO,EAAE,kBAAkB;YAC3B,OAAO,EAAE,cAAc,EAAE,2BAA2B;YACpD,IAAI,EAAE,KAAK;YACX,gBAAgB,EAAE,KAAK;YACvB,mBAAmB,EAAE,OAAO,EAAE,mBAAmB;SACjD,EACD,gBAAgB,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,CAAC,EACzF,IAAI,CACJ,CAAC;IACH,CAAC;IAED;;;OAGG;IACO,aAAa,CAAC,OAAgB;QACvC,2BAA2B,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;CACD","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { CLIBase } from \"@twin.org/cli-core\";\nimport type { Command } from \"commander\";\nimport { buildCommandValidateLocales } from \"./commands/validateLocales.js\";\n\n/**\n * The main entry point for the CLI.\n */\nexport class CLI extends CLIBase {\n\t/**\n\t * Run the app.\n\t * @param argv The process arguments.\n\t * @param localesDirectory The directory for the locales, default to relative to the script.\n\t * @param options Additional options.\n\t * @param options.overrideOutputWidth Override the output width.\n\t * @returns The exit code.\n\t */\n\tpublic async run(\n\t\targv: string[],\n\t\tlocalesDirectory?: string,\n\t\toptions?: { overrideOutputWidth?: number }\n\t): Promise<number> {\n\t\treturn this.execute(\n\t\t\t{\n\t\t\t\ttitle: \"TWIN Validate Locales\",\n\t\t\t\tappName: \"validate-locales\",\n\t\t\t\tversion: \"0.0.3-next.1\", // x-release-please-version\n\t\t\t\ticon: \"⚙️ \",\n\t\t\t\tsupportsEnvFiles: false,\n\t\t\t\toverrideOutputWidth: options?.overrideOutputWidth\n\t\t\t},\n\t\t\tlocalesDirectory ?? path.join(path.dirname(fileURLToPath(import.meta.url)), \"../locales\"),\n\t\t\targv\n\t\t);\n\t}\n\n\t/**\n\t * Configure any options or actions at the root program level.\n\t * @param program The root program command.\n\t */\n\tprotected configureRoot(program: Command): void {\n\t\tbuildCommandValidateLocales(program);\n\t}\n}\n"]}
@@ -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 },
@@ -23,7 +21,7 @@ const ERROR_TYPES = [
23
21
  ];
24
22
  const SKIP_FILES = ["**/models/**/*.ts"];
25
23
  const SKIP_LITERALS = [
26
- /\d+\.\d+\.\d+(-\w+(\.\w+)*)?$/, // Version string
24
+ /^\d+\.\d+\.\d+(-\w+(\.\w+)*)?(-\d)?$/, // Version string
27
25
  /^[^@]+@[^@]+\.[^@]+$/, // Email string
28
26
  /\.json$/i, // ending in .json
29
27
  /\.js$/i, // ending in .js
@@ -33,7 +31,8 @@ const SKIP_LITERALS = [
33
31
  /\.lock$/i, // ending in .lock
34
32
  /\.toml$/i, // ending in .toml
35
33
  /\.{3}/i, // ...
36
- /@twin\.org/ // starting with @twin.org
34
+ /@twin\.org/, // starting with @twin.org
35
+ /^console.log/i // console.log
37
36
  ];
38
37
  const SKIP_METHODS = [/^generateRest/, /^generateSocket/];
39
38
  const CAPTURE_VARIABLES = [/ROUTES_SOURCE/];
@@ -41,7 +40,7 @@ const CAPTURE_VARIABLES = [/ROUTES_SOURCE/];
41
40
  * Build the root command to be consumed by the CLI.
42
41
  * @param program The command to build on.
43
42
  */
44
- function buildCommandValidateLocales(program) {
43
+ export function buildCommandValidateLocales(program) {
45
44
  program
46
45
  .option(I18n.formatMessage("commands.validate-locales.options.source.param"), I18n.formatMessage("commands.validate-locales.options.source.description"), "src/**/*.ts")
47
46
  .option(I18n.formatMessage("commands.validate-locales.options.locales.param"), I18n.formatMessage("commands.validate-locales.options.locales.description"), "locales/**/*.json")
@@ -57,7 +56,7 @@ function buildCommandValidateLocales(program) {
57
56
  * @param opts.locales The locales glob.
58
57
  * @param opts.ignoreFile The ignore file path.
59
58
  */
60
- async function actionCommandValidateLocales(opts) {
59
+ export async function actionCommandValidateLocales(opts) {
61
60
  opts.source = path.resolve(opts.source);
62
61
  opts.locales = path.resolve(opts.locales);
63
62
  opts.ignoreFile = path.resolve(opts.ignoreFile);
@@ -93,7 +92,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
93
92
  const localeEntries = [];
94
93
  let hasQuoteError = false;
95
94
  let hasUnused = false;
96
- let hasMissing = false;
95
+ let hasFailures = false;
97
96
  for (const localeFile of localeFiles) {
98
97
  const dictionary = await CLIUtils.readJsonFile(localeFile);
99
98
  const locale = path.basename(localeFile, path.extname(localeFile));
@@ -118,27 +117,27 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
118
117
  });
119
118
  }
120
119
  }
121
- let missing = [];
120
+ let failures = [];
122
121
  const captureVariables = {};
123
122
  for (const sourceFile of sourceFiles) {
124
123
  const source = await readFile(sourceFile, "utf8");
125
124
  const sourceTs = ts.createSourceFile(sourceFile, source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
126
- visit(sourceTs, sourceTs, localeEntries, missing, captureVariables);
125
+ visit(sourceTs, sourceTs, localeEntries, failures, captureVariables);
127
126
  }
128
- missing = missing.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
129
- if (missing.length === 0) {
127
+ failures = failures.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
128
+ if (failures.length === 0) {
130
129
  CLIDisplay.write(I18n.formatMessage("commands.validate-locales.labels.noMissingLocaleEntries"));
131
130
  CLIDisplay.break();
132
131
  }
133
132
  else {
134
- hasMissing = true;
135
- for (const missingRef of missing) {
136
- if (missingRef.type === "key") {
133
+ hasFailures = true;
134
+ for (const failureRef of failures) {
135
+ if (failureRef.type === "key") {
137
136
  CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.missingLocaleEntry", {
138
- key: missingRef.key,
139
- source: missingRef.source,
140
- line: missingRef.line,
141
- column: missingRef.column
137
+ key: failureRef.key,
138
+ source: failureRef.source,
139
+ line: failureRef.line,
140
+ column: failureRef.column
142
141
  }));
143
142
  }
144
143
  }
@@ -164,7 +163,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
164
163
  }
165
164
  }
166
165
  }
167
- if (hasMissing || hasUnused || hasQuoteError) {
166
+ if (hasFailures || hasUnused || hasQuoteError) {
168
167
  throw new GeneralError("validateLocales", "validationFailed");
169
168
  }
170
169
  }
@@ -173,43 +172,43 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
173
172
  * @param sourceFile The TypeScript source file for position calculations.
174
173
  * @param node The node to visit.
175
174
  * @param localeEntries The locale entries.
176
- * @param missing The missing entries.
175
+ * @param failures The failure entries.
177
176
  * @param captureVariables The capture variables.
178
177
  */
179
- function visit(sourceFile, node, localeEntries, missing, captureVariables) {
178
+ function visit(sourceFile, node, localeEntries, failures, captureVariables) {
180
179
  let handled = false;
181
180
  if (ts.isNewExpression(node) &&
182
181
  ts.isIdentifier(node.expression) &&
183
182
  ERROR_TYPES.some(errorType => errorType.name === node.expression.getText())) {
184
- processErrorType(sourceFile, node, node.expression.text, localeEntries, missing);
183
+ processErrorType(sourceFile, node, node.expression.text, localeEntries, failures);
185
184
  handled = true;
186
185
  }
187
186
  else if (ts.isStringLiteral(node)) {
188
- processStringLiteral(sourceFile, node, localeEntries, missing);
187
+ processStringLiteral(sourceFile, node, localeEntries, failures);
189
188
  handled = true;
190
189
  }
191
190
  else if (ts.isTemplateExpression(node)) {
192
- processTemplateExpression(sourceFile, node, localeEntries, missing);
191
+ processTemplateExpression(sourceFile, node, localeEntries, failures);
193
192
  handled = true;
194
193
  }
195
194
  else if (ts.isCallExpression(node)) {
196
- handled = processCallExpression(sourceFile, node, localeEntries, missing, captureVariables);
195
+ handled = processCallExpression(sourceFile, node, localeEntries, failures, captureVariables);
197
196
  }
198
197
  else if (ts.isFunctionDeclaration(node)) {
199
- handled = processFunctionDeclaration(sourceFile, node);
198
+ handled = processFunctionDeclaration(sourceFile, node, localeEntries, failures);
200
199
  }
201
200
  else if (ts.isVariableDeclaration(node)) {
202
- handled = processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables);
201
+ handled = processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables);
203
202
  }
204
203
  else if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
205
204
  // Don't care about string in imports/exports
206
205
  handled = true;
207
206
  }
208
207
  else if (ts.isPropertyAssignment(node)) {
209
- handled = processPropertyAssignment(sourceFile, node, localeEntries, missing);
208
+ handled = processPropertyAssignment(sourceFile, node, localeEntries, failures);
210
209
  }
211
210
  if (!handled) {
212
- ts.forEachChild(node, child => visit(sourceFile, child, localeEntries, missing, captureVariables));
211
+ ts.forEachChild(node, child => visit(sourceFile, child, localeEntries, failures, captureVariables));
213
212
  }
214
213
  }
215
214
  /**
@@ -218,12 +217,12 @@ function visit(sourceFile, node, localeEntries, missing, captureVariables) {
218
217
  * @param node The node to process.
219
218
  * @param errorType The error type.
220
219
  * @param localeEntries The locale entries.
221
- * @param missing The missing entries.
220
+ * @param failures The failure entries.
222
221
  */
223
- function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
222
+ function processErrorType(sourceFile, node, errorType, localeEntries, failures) {
224
223
  const errType = ERROR_TYPES.find(e => e.name === errorType);
225
224
  if (Is.object(errType)) {
226
- const localeKey = localeFromClassAndMessage(node.arguments?.[0], node.arguments?.[1], "error");
225
+ const localeKey = localeFromClassAndMessage(sourceFile, node.arguments?.[0], node.arguments?.[1], "error", failures);
227
226
  if (Is.stringValue(localeKey)) {
228
227
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
229
228
  if (Is.object(localeEntry)) {
@@ -232,11 +231,11 @@ function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
232
231
  if (errType.dynamicPropertyIndex !== -1) {
233
232
  usedProperties.push(...getPropertiesFromNode(node.arguments?.[errType.dynamicPropertyIndex]));
234
233
  }
235
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, missing);
234
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, failures);
236
235
  }
237
236
  }
238
237
  else {
239
- missing.push({
238
+ failures.push({
240
239
  type: "key",
241
240
  key: localeKey,
242
241
  source: path.resolve(sourceFile.fileName),
@@ -268,9 +267,9 @@ function findAndReferenceLocale(localeEntries, entryToMatch) {
268
267
  * @param sourceFile The TypeScript source file for position calculations.
269
268
  * @param node The node to process.
270
269
  * @param localeEntries The locale entries.
271
- * @param missing The missing entries.
270
+ * @param failures The failure entries.
272
271
  */
273
- function processStringLiteral(sourceFile, node, localeEntries, missing) {
272
+ function processStringLiteral(sourceFile, node, localeEntries, failures) {
274
273
  if (node.text.length > 3 &&
275
274
  node.text.includes(".") &&
276
275
  !/[ ()/]/.test(node.text) &&
@@ -283,18 +282,18 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
283
282
  if (localeEntry) {
284
283
  localeEntry.referenced = true;
285
284
  const usedProperties = getPropertiesFromNode(node);
286
- checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, missing);
285
+ checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, failures);
287
286
  }
288
287
  if (!localeEntry && ["validation.", "common."].some(t => node.text.startsWith(t))) {
289
288
  localeEntry = findAndReferenceLocale(localeEntries, `error.${node.text}`);
290
289
  if (localeEntry) {
291
290
  localeEntry.referenced = true;
292
291
  const usedProperties = getPropertiesFromNode(node);
293
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, missing);
292
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, failures);
294
293
  }
295
294
  }
296
295
  if (!localeEntry) {
297
- missing.push({
296
+ failures.push({
298
297
  type: "key",
299
298
  key: node.text,
300
299
  source: path.resolve(sourceFile.fileName),
@@ -309,9 +308,9 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
309
308
  * @param sourceFile The TypeScript source file for position calculations.
310
309
  * @param node The node to process.
311
310
  * @param localeEntries The locale entries.
312
- * @param missing The missing entries.
311
+ * @param failures The failure entries.
313
312
  */
314
- function processTemplateExpression(sourceFile, node, localeEntries, missing) {
313
+ function processTemplateExpression(sourceFile, node, localeEntries, failures) {
315
314
  // This case handles templates like `error.${nameof(Class)}.message`
316
315
  const templateParts = extractTemplatePartsWithExpressions(node);
317
316
  // Join all literal text parts to form a potential locale key
@@ -320,14 +319,14 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
320
319
  let localeEntry = findAndReferenceLocale(localeEntries, key);
321
320
  if (localeEntry) {
322
321
  const usedProperties = getPropertiesFromNode(node);
323
- checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, missing);
322
+ checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, failures);
324
323
  }
325
324
  else if (["validation.", "common."].some(t => key.startsWith(t))) {
326
325
  localeEntry = findAndReferenceLocale(localeEntries, `error.${key}`);
327
326
  if (localeEntry) {
328
327
  localeEntry.referenced = true;
329
328
  const usedProperties = getPropertiesFromNode(node.parent.parent);
330
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, missing);
329
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, failures);
331
330
  }
332
331
  }
333
332
  }
@@ -337,14 +336,17 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
337
336
  * @param sourceFile The TypeScript source file for position calculations.
338
337
  * @param node The node to process.
339
338
  * @param localeEntries The locale entries.
340
- * @param missing The missing entries.
339
+ * @param failures The failure entries.
341
340
  * @param captureVariables The capture variables.
342
341
  * @returns True if processed, false otherwise.
343
342
  */
344
- function processCallExpression(sourceFile, node, localeEntries, missing, captureVariables) {
343
+ function processCallExpression(sourceFile, node, localeEntries, failures, captureVariables) {
345
344
  if (ts.isPropertyAccessExpression(node.expression)) {
346
345
  const functionName = node.expression.name.getText();
347
- if (functionName === "log" &&
346
+ if (node.expression.getText() === "console.log") {
347
+ return true;
348
+ }
349
+ else if (functionName === "log" &&
348
350
  node.arguments.length === 1 &&
349
351
  ts.isObjectLiteralExpression(node.arguments[0])) {
350
352
  let level;
@@ -375,14 +377,14 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
375
377
  }
376
378
  }
377
379
  }
378
- const localeKey = localeFromClassAndMessage(source, message, level);
380
+ const localeKey = localeFromClassAndMessage(sourceFile, source, message, level, failures);
379
381
  if (Is.stringValue(localeKey)) {
380
382
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
381
383
  if (Is.object(localeEntry)) {
382
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
384
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
383
385
  }
384
386
  else {
385
- missing.push({
387
+ failures.push({
386
388
  type: "key",
387
389
  key: localeKey,
388
390
  source: path.resolve(sourceFile.fileName),
@@ -401,10 +403,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
401
403
  const dataNames = getPropertiesFromNode(node.arguments[1]);
402
404
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
403
405
  if (Is.object(localeEntry)) {
404
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
406
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
405
407
  }
406
408
  else {
407
- missing.push({
409
+ failures.push({
408
410
  type: "key",
409
411
  key: localeKey,
410
412
  source: path.resolve(sourceFile.fileName),
@@ -422,10 +424,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
422
424
  * @param sourceFile The TypeScript source file for position calculations.
423
425
  * @param node The node to process.
424
426
  * @param localeEntries The locale entries.
425
- * @param missing The missing entries.
427
+ * @param failures The failure entries.
426
428
  * @returns True if processed, false otherwise.
427
429
  */
428
- function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
430
+ function processFunctionDeclaration(sourceFile, node, localeEntries, failures) {
429
431
  if (Is.object(node.name) &&
430
432
  ts.isIdentifier(node.name) &&
431
433
  SKIP_METHODS.some(re => re.test(node.name?.text ?? ""))) {
@@ -438,11 +440,11 @@ function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
438
440
  * @param sourceFile The TypeScript source file for position calculations.
439
441
  * @param node The node to process.
440
442
  * @param localeEntries The locale entries.
441
- * @param missing The missing entries.
443
+ * @param failures The failure entries.
442
444
  * @param captureVariables The capture variables.
443
445
  * @returns True if processed, false otherwise.
444
446
  */
445
- function processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables) {
447
+ function processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables) {
446
448
  if (Is.object(node.name) &&
447
449
  Is.object(node.initializer) &&
448
450
  ts.isIdentifier(node.name) &&
@@ -458,10 +460,10 @@ function processVariableDeclaration(sourceFile, node, localeEntries, missing, ca
458
460
  * @param sourceFile The TypeScript source file for position calculations.
459
461
  * @param node The node to process.
460
462
  * @param localeEntries The locale entries.
461
- * @param missing The missing entries.
463
+ * @param failures The failure entries.
462
464
  * @returns True if processed, false otherwise.
463
465
  */
464
- function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
466
+ function processPropertyAssignment(sourceFile, node, localeEntries, failures) {
465
467
  if (Is.object(node.name) && ts.isIdentifier(node.name) && node.name.getText() === "message") {
466
468
  const localeKey = getExpandedText(node.initializer);
467
469
  if (Is.stringValue(localeKey)) {
@@ -470,7 +472,7 @@ function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
470
472
  localeEntry = findAndReferenceLocale(localeEntries, `error.${localeKey}`);
471
473
  }
472
474
  if (!Is.object(localeEntry)) {
473
- missing.push({
475
+ failures.push({
474
476
  type: "key",
475
477
  key: localeKey,
476
478
  source: path.resolve(sourceFile.fileName),
@@ -529,7 +531,7 @@ function extractTemplatePartsWithExpressions(node) {
529
531
  * @returns True if the template parts are valid, false otherwise.
530
532
  */
531
533
  function hasValidTemplateContent(templateParts) {
532
- return !templateParts.some(part => /[ #,/:=?|]/.test(part));
534
+ return !templateParts.some(part => /[#,/:=?|]/.test(part));
533
535
  }
534
536
  /**
535
537
  * Expand template parts into a single string, processing nameof expressions.
@@ -573,9 +575,9 @@ function expandTemplatePart(templatePart) {
573
575
  * @param localeEntry The locale entry to check against.
574
576
  * @param key The key in the locale entry.
575
577
  * @param usedProperties The properties used in the code.
576
- * @param missing The missing entries.
578
+ * @param failures The failure entries.
577
579
  */
578
- function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, missing) {
580
+ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, failures) {
579
581
  for (const propName of localeEntry.propertyNames) {
580
582
  const propIndex = usedProperties.indexOf(propName);
581
583
  if (propIndex === -1) {
@@ -587,7 +589,7 @@ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties,
587
589
  line: position.line,
588
590
  column: position.column
589
591
  }));
590
- missing.push({
592
+ failures.push({
591
593
  type: "property",
592
594
  key: localeEntry.key,
593
595
  source: path.resolve(sourceFile.fileName),
@@ -673,66 +675,74 @@ function isSkipLiteral(value) {
673
675
  }
674
676
  /**
675
677
  * Get the locale from the class and message nodes.
678
+ * @param sourceFile The TypeScript source file for position calculations.
676
679
  * @param classNode The class node.
677
680
  * @param messageNode The message node.
678
681
  * @param prefix The prefix for the locale key.
682
+ * @param failures The failure entries.
679
683
  * @returns The locale entry or undefined.
680
684
  */
681
- function localeFromClassAndMessage(classNode, messageNode, prefix) {
685
+ function localeFromClassAndMessage(sourceFile, classNode, messageNode, prefix, failures) {
682
686
  if (!classNode || !messageNode) {
683
687
  return undefined;
684
688
  }
685
689
  const classNameParam = classNode.getText();
686
690
  const classNameParamParts = classNameParam?.split(".");
687
691
  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);
692
+ const messageKey = getExpandedText(messageNode);
693
+ if (Is.stringValue(messageKey)) {
694
+ if (messageKey.includes(" ")) {
695
+ // If the message contains spaces then it is not a key
696
+ // but should be replaced by one
697
+ const position = getSourcePosition(sourceFile, messageNode);
698
+ CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.shouldBeKey", {
699
+ value: messageKey,
700
+ source: path.resolve(sourceFile.fileName),
701
+ line: position.line,
702
+ column: position.column
703
+ }));
704
+ failures.push({
705
+ type: "noKey",
706
+ key: messageKey,
707
+ source: path.resolve(sourceFile.fileName),
708
+ ...position
709
+ });
693
710
  }
694
- // If the key is not fully qualified then add the class name
695
- if (!localeKey.includes(".")) {
696
- localeKeyParts.push(expandTemplatePart(classNameParam));
711
+ else {
712
+ const finalKeyParts = [];
713
+ const classNameExpanded = expandTemplatePart(classNameParam);
714
+ const messageKeyParts = messageKey.split(".");
715
+ if (messageKeyParts.length === 2 && classNameExpanded === messageKeyParts[0]) {
716
+ // But if it is fully qualified with exactly two segments and starts with the class name
717
+ // then the class name is redundant and should be removed in the source
718
+ const position = getSourcePosition(sourceFile, messageNode);
719
+ CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.noNeedToQualify", {
720
+ key: messageKey,
721
+ property: classNameParam,
722
+ source: path.resolve(sourceFile.fileName),
723
+ line: position.line,
724
+ column: position.column
725
+ }));
726
+ failures.push({
727
+ type: "qualify",
728
+ key: messageKey,
729
+ source: path.resolve(sourceFile.fileName),
730
+ ...position
731
+ });
732
+ }
733
+ else if (!messageKey.includes(".")) {
734
+ // If the key is not fully qualified then add the class name
735
+ // to the final key
736
+ finalKeyParts.push(classNameExpanded);
737
+ }
738
+ finalKeyParts.push(messageKey);
739
+ if (Is.stringValue(prefix)) {
740
+ finalKeyParts.unshift(prefix);
741
+ }
742
+ return finalKeyParts.join(".");
697
743
  }
698
- localeKeyParts.push(localeKey);
699
- return localeKeyParts.join(".");
700
744
  }
701
745
  }
702
746
  return undefined;
703
747
  }
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 };
748
+ //# sourceMappingURL=validateLocales.js.map