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

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.
@@ -46,7 +46,7 @@ const ERROR_TYPES = [
46
46
  ];
47
47
  const SKIP_FILES = ["**/models/**/*.ts"];
48
48
  const SKIP_LITERALS = [
49
- /\d+\.\d+\.\d+(-\w+(\.\w+)*)?$/, // Version string
49
+ /^\d+\.\d+\.\d+(-\w+(\.\w+)*)?(-\d)?$/, // Version string
50
50
  /^[^@]+@[^@]+\.[^@]+$/, // Email string
51
51
  /\.json$/i, // ending in .json
52
52
  /\.js$/i, // ending in .js
@@ -116,7 +116,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
116
116
  const localeEntries = [];
117
117
  let hasQuoteError = false;
118
118
  let hasUnused = false;
119
- let hasMissing = false;
119
+ let hasFailures = false;
120
120
  for (const localeFile of localeFiles) {
121
121
  const dictionary = await cliCore.CLIUtils.readJsonFile(localeFile);
122
122
  const locale = path.basename(localeFile, path.extname(localeFile));
@@ -141,27 +141,27 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
141
141
  });
142
142
  }
143
143
  }
144
- let missing = [];
144
+ let failures = [];
145
145
  const captureVariables = {};
146
146
  for (const sourceFile of sourceFiles) {
147
147
  const source = await promises.readFile(sourceFile, "utf8");
148
148
  const sourceTs = ts__namespace.createSourceFile(sourceFile, source, ts__namespace.ScriptTarget.ESNext, true, ts__namespace.ScriptKind.TS);
149
- visit(sourceTs, sourceTs, localeEntries, missing, captureVariables);
149
+ visit(sourceTs, sourceTs, localeEntries, failures, captureVariables);
150
150
  }
151
- missing = missing.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
152
- if (missing.length === 0) {
151
+ failures = failures.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
152
+ if (failures.length === 0) {
153
153
  cliCore.CLIDisplay.write(core.I18n.formatMessage("commands.validate-locales.labels.noMissingLocaleEntries"));
154
154
  cliCore.CLIDisplay.break();
155
155
  }
156
156
  else {
157
- hasMissing = true;
158
- for (const missingRef of missing) {
159
- if (missingRef.type === "key") {
157
+ hasFailures = true;
158
+ for (const failureRef of failures) {
159
+ if (failureRef.type === "key") {
160
160
  cliCore.CLIDisplay.errorMessage(core.I18n.formatMessage("error.validateLocales.missingLocaleEntry", {
161
- key: missingRef.key,
162
- source: missingRef.source,
163
- line: missingRef.line,
164
- column: missingRef.column
161
+ key: failureRef.key,
162
+ source: failureRef.source,
163
+ line: failureRef.line,
164
+ column: failureRef.column
165
165
  }));
166
166
  }
167
167
  }
@@ -187,7 +187,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
187
187
  }
188
188
  }
189
189
  }
190
- if (hasMissing || hasUnused || hasQuoteError) {
190
+ if (hasFailures || hasUnused || hasQuoteError) {
191
191
  throw new core.GeneralError("validateLocales", "validationFailed");
192
192
  }
193
193
  }
@@ -196,43 +196,43 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
196
196
  * @param sourceFile The TypeScript source file for position calculations.
197
197
  * @param node The node to visit.
198
198
  * @param localeEntries The locale entries.
199
- * @param missing The missing entries.
199
+ * @param failures The failure entries.
200
200
  * @param captureVariables The capture variables.
201
201
  */
202
- function visit(sourceFile, node, localeEntries, missing, captureVariables) {
202
+ function visit(sourceFile, node, localeEntries, failures, captureVariables) {
203
203
  let handled = false;
204
204
  if (ts__namespace.isNewExpression(node) &&
205
205
  ts__namespace.isIdentifier(node.expression) &&
206
206
  ERROR_TYPES.some(errorType => errorType.name === node.expression.getText())) {
207
- processErrorType(sourceFile, node, node.expression.text, localeEntries, missing);
207
+ processErrorType(sourceFile, node, node.expression.text, localeEntries, failures);
208
208
  handled = true;
209
209
  }
210
210
  else if (ts__namespace.isStringLiteral(node)) {
211
- processStringLiteral(sourceFile, node, localeEntries, missing);
211
+ processStringLiteral(sourceFile, node, localeEntries, failures);
212
212
  handled = true;
213
213
  }
214
214
  else if (ts__namespace.isTemplateExpression(node)) {
215
- processTemplateExpression(sourceFile, node, localeEntries, missing);
215
+ processTemplateExpression(sourceFile, node, localeEntries, failures);
216
216
  handled = true;
217
217
  }
218
218
  else if (ts__namespace.isCallExpression(node)) {
219
- handled = processCallExpression(sourceFile, node, localeEntries, missing, captureVariables);
219
+ handled = processCallExpression(sourceFile, node, localeEntries, failures, captureVariables);
220
220
  }
221
221
  else if (ts__namespace.isFunctionDeclaration(node)) {
222
222
  handled = processFunctionDeclaration(sourceFile, node);
223
223
  }
224
224
  else if (ts__namespace.isVariableDeclaration(node)) {
225
- handled = processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables);
225
+ handled = processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables);
226
226
  }
227
227
  else if (ts__namespace.isImportDeclaration(node) || ts__namespace.isExportDeclaration(node)) {
228
228
  // Don't care about string in imports/exports
229
229
  handled = true;
230
230
  }
231
231
  else if (ts__namespace.isPropertyAssignment(node)) {
232
- handled = processPropertyAssignment(sourceFile, node, localeEntries, missing);
232
+ handled = processPropertyAssignment(sourceFile, node, localeEntries, failures);
233
233
  }
234
234
  if (!handled) {
235
- ts__namespace.forEachChild(node, child => visit(sourceFile, child, localeEntries, missing, captureVariables));
235
+ ts__namespace.forEachChild(node, child => visit(sourceFile, child, localeEntries, failures, captureVariables));
236
236
  }
237
237
  }
238
238
  /**
@@ -241,12 +241,12 @@ function visit(sourceFile, node, localeEntries, missing, captureVariables) {
241
241
  * @param node The node to process.
242
242
  * @param errorType The error type.
243
243
  * @param localeEntries The locale entries.
244
- * @param missing The missing entries.
244
+ * @param failures The failure entries.
245
245
  */
246
- function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
246
+ function processErrorType(sourceFile, node, errorType, localeEntries, failures) {
247
247
  const errType = ERROR_TYPES.find(e => e.name === errorType);
248
248
  if (core.Is.object(errType)) {
249
- const localeKey = localeFromClassAndMessage(node.arguments?.[0], node.arguments?.[1], "error");
249
+ const localeKey = localeFromClassAndMessage(sourceFile, node.arguments?.[0], node.arguments?.[1], "error", failures);
250
250
  if (core.Is.stringValue(localeKey)) {
251
251
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
252
252
  if (core.Is.object(localeEntry)) {
@@ -255,11 +255,11 @@ function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
255
255
  if (errType.dynamicPropertyIndex !== -1) {
256
256
  usedProperties.push(...getPropertiesFromNode(node.arguments?.[errType.dynamicPropertyIndex]));
257
257
  }
258
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, missing);
258
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, failures);
259
259
  }
260
260
  }
261
261
  else {
262
- missing.push({
262
+ failures.push({
263
263
  type: "key",
264
264
  key: localeKey,
265
265
  source: path.resolve(sourceFile.fileName),
@@ -291,9 +291,9 @@ function findAndReferenceLocale(localeEntries, entryToMatch) {
291
291
  * @param sourceFile The TypeScript source file for position calculations.
292
292
  * @param node The node to process.
293
293
  * @param localeEntries The locale entries.
294
- * @param missing The missing entries.
294
+ * @param failures The failure entries.
295
295
  */
296
- function processStringLiteral(sourceFile, node, localeEntries, missing) {
296
+ function processStringLiteral(sourceFile, node, localeEntries, failures) {
297
297
  if (node.text.length > 3 &&
298
298
  node.text.includes(".") &&
299
299
  !/[ ()/]/.test(node.text) &&
@@ -306,18 +306,18 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
306
306
  if (localeEntry) {
307
307
  localeEntry.referenced = true;
308
308
  const usedProperties = getPropertiesFromNode(node);
309
- checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, missing);
309
+ checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, failures);
310
310
  }
311
311
  if (!localeEntry && ["validation.", "common."].some(t => node.text.startsWith(t))) {
312
312
  localeEntry = findAndReferenceLocale(localeEntries, `error.${node.text}`);
313
313
  if (localeEntry) {
314
314
  localeEntry.referenced = true;
315
315
  const usedProperties = getPropertiesFromNode(node);
316
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, missing);
316
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, failures);
317
317
  }
318
318
  }
319
319
  if (!localeEntry) {
320
- missing.push({
320
+ failures.push({
321
321
  type: "key",
322
322
  key: node.text,
323
323
  source: path.resolve(sourceFile.fileName),
@@ -332,9 +332,9 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
332
332
  * @param sourceFile The TypeScript source file for position calculations.
333
333
  * @param node The node to process.
334
334
  * @param localeEntries The locale entries.
335
- * @param missing The missing entries.
335
+ * @param failures The failure entries.
336
336
  */
337
- function processTemplateExpression(sourceFile, node, localeEntries, missing) {
337
+ function processTemplateExpression(sourceFile, node, localeEntries, failures) {
338
338
  // This case handles templates like `error.${nameof(Class)}.message`
339
339
  const templateParts = extractTemplatePartsWithExpressions(node);
340
340
  // Join all literal text parts to form a potential locale key
@@ -343,14 +343,14 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
343
343
  let localeEntry = findAndReferenceLocale(localeEntries, key);
344
344
  if (localeEntry) {
345
345
  const usedProperties = getPropertiesFromNode(node);
346
- checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, missing);
346
+ checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, failures);
347
347
  }
348
348
  else if (["validation.", "common."].some(t => key.startsWith(t))) {
349
349
  localeEntry = findAndReferenceLocale(localeEntries, `error.${key}`);
350
350
  if (localeEntry) {
351
351
  localeEntry.referenced = true;
352
352
  const usedProperties = getPropertiesFromNode(node.parent.parent);
353
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, missing);
353
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, failures);
354
354
  }
355
355
  }
356
356
  }
@@ -360,11 +360,11 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
360
360
  * @param sourceFile The TypeScript source file for position calculations.
361
361
  * @param node The node to process.
362
362
  * @param localeEntries The locale entries.
363
- * @param missing The missing entries.
363
+ * @param failures The failure entries.
364
364
  * @param captureVariables The capture variables.
365
365
  * @returns True if processed, false otherwise.
366
366
  */
367
- function processCallExpression(sourceFile, node, localeEntries, missing, captureVariables) {
367
+ function processCallExpression(sourceFile, node, localeEntries, failures, captureVariables) {
368
368
  if (ts__namespace.isPropertyAccessExpression(node.expression)) {
369
369
  const functionName = node.expression.name.getText();
370
370
  if (functionName === "log" &&
@@ -398,14 +398,14 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
398
398
  }
399
399
  }
400
400
  }
401
- const localeKey = localeFromClassAndMessage(source, message, level);
401
+ const localeKey = localeFromClassAndMessage(sourceFile, source, message, level, failures);
402
402
  if (core.Is.stringValue(localeKey)) {
403
403
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
404
404
  if (core.Is.object(localeEntry)) {
405
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
405
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
406
406
  }
407
407
  else {
408
- missing.push({
408
+ failures.push({
409
409
  type: "key",
410
410
  key: localeKey,
411
411
  source: path.resolve(sourceFile.fileName),
@@ -424,10 +424,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
424
424
  const dataNames = getPropertiesFromNode(node.arguments[1]);
425
425
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
426
426
  if (core.Is.object(localeEntry)) {
427
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
427
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
428
428
  }
429
429
  else {
430
- missing.push({
430
+ failures.push({
431
431
  type: "key",
432
432
  key: localeKey,
433
433
  source: path.resolve(sourceFile.fileName),
@@ -445,10 +445,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
445
445
  * @param sourceFile The TypeScript source file for position calculations.
446
446
  * @param node The node to process.
447
447
  * @param localeEntries The locale entries.
448
- * @param missing The missing entries.
448
+ * @param failures The failure entries.
449
449
  * @returns True if processed, false otherwise.
450
450
  */
451
- function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
451
+ function processFunctionDeclaration(sourceFile, node, localeEntries, failures) {
452
452
  if (core.Is.object(node.name) &&
453
453
  ts__namespace.isIdentifier(node.name) &&
454
454
  SKIP_METHODS.some(re => re.test(node.name?.text ?? ""))) {
@@ -461,11 +461,11 @@ function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
461
461
  * @param sourceFile The TypeScript source file for position calculations.
462
462
  * @param node The node to process.
463
463
  * @param localeEntries The locale entries.
464
- * @param missing The missing entries.
464
+ * @param failures The failure entries.
465
465
  * @param captureVariables The capture variables.
466
466
  * @returns True if processed, false otherwise.
467
467
  */
468
- function processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables) {
468
+ function processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables) {
469
469
  if (core.Is.object(node.name) &&
470
470
  core.Is.object(node.initializer) &&
471
471
  ts__namespace.isIdentifier(node.name) &&
@@ -481,10 +481,10 @@ function processVariableDeclaration(sourceFile, node, localeEntries, missing, ca
481
481
  * @param sourceFile The TypeScript source file for position calculations.
482
482
  * @param node The node to process.
483
483
  * @param localeEntries The locale entries.
484
- * @param missing The missing entries.
484
+ * @param failures The failure entries.
485
485
  * @returns True if processed, false otherwise.
486
486
  */
487
- function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
487
+ function processPropertyAssignment(sourceFile, node, localeEntries, failures) {
488
488
  if (core.Is.object(node.name) && ts__namespace.isIdentifier(node.name) && node.name.getText() === "message") {
489
489
  const localeKey = getExpandedText(node.initializer);
490
490
  if (core.Is.stringValue(localeKey)) {
@@ -493,7 +493,7 @@ function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
493
493
  localeEntry = findAndReferenceLocale(localeEntries, `error.${localeKey}`);
494
494
  }
495
495
  if (!core.Is.object(localeEntry)) {
496
- missing.push({
496
+ failures.push({
497
497
  type: "key",
498
498
  key: localeKey,
499
499
  source: path.resolve(sourceFile.fileName),
@@ -552,7 +552,7 @@ function extractTemplatePartsWithExpressions(node) {
552
552
  * @returns True if the template parts are valid, false otherwise.
553
553
  */
554
554
  function hasValidTemplateContent(templateParts) {
555
- return !templateParts.some(part => /[ #,/:=?|]/.test(part));
555
+ return !templateParts.some(part => /[#,/:=?|]/.test(part));
556
556
  }
557
557
  /**
558
558
  * Expand template parts into a single string, processing nameof expressions.
@@ -596,9 +596,9 @@ function expandTemplatePart(templatePart) {
596
596
  * @param localeEntry The locale entry to check against.
597
597
  * @param key The key in the locale entry.
598
598
  * @param usedProperties The properties used in the code.
599
- * @param missing The missing entries.
599
+ * @param failures The failure entries.
600
600
  */
601
- function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, missing) {
601
+ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, failures) {
602
602
  for (const propName of localeEntry.propertyNames) {
603
603
  const propIndex = usedProperties.indexOf(propName);
604
604
  if (propIndex === -1) {
@@ -610,7 +610,7 @@ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties,
610
610
  line: position.line,
611
611
  column: position.column
612
612
  }));
613
- missing.push({
613
+ failures.push({
614
614
  type: "property",
615
615
  key: localeEntry.key,
616
616
  source: path.resolve(sourceFile.fileName),
@@ -696,30 +696,72 @@ function isSkipLiteral(value) {
696
696
  }
697
697
  /**
698
698
  * Get the locale from the class and message nodes.
699
+ * @param sourceFile The TypeScript source file for position calculations.
699
700
  * @param classNode The class node.
700
701
  * @param messageNode The message node.
701
702
  * @param prefix The prefix for the locale key.
703
+ * @param failures The failure entries.
702
704
  * @returns The locale entry or undefined.
703
705
  */
704
- function localeFromClassAndMessage(classNode, messageNode, prefix) {
706
+ function localeFromClassAndMessage(sourceFile, classNode, messageNode, prefix, failures) {
705
707
  if (!classNode || !messageNode) {
706
708
  return undefined;
707
709
  }
708
710
  const classNameParam = classNode.getText();
709
711
  const classNameParamParts = classNameParam?.split(".");
710
712
  if (core.Is.array(classNameParamParts)) {
711
- const localeKey = getExpandedText(messageNode);
712
- if (core.Is.stringValue(localeKey)) {
713
- const localeKeyParts = [];
714
- if (core.Is.stringValue(prefix)) {
715
- localeKeyParts.push(prefix);
713
+ const messageKey = getExpandedText(messageNode);
714
+ if (core.Is.stringValue(messageKey)) {
715
+ if (messageKey.includes(" ")) {
716
+ // If the message contains spaces then it is not a key
717
+ // but should be replaced by one
718
+ const position = getSourcePosition(sourceFile, messageNode);
719
+ cliCore.CLIDisplay.errorMessage(core.I18n.formatMessage("error.validateLocales.shouldBeKey", {
720
+ value: messageKey,
721
+ source: path.resolve(sourceFile.fileName),
722
+ line: position.line,
723
+ column: position.column
724
+ }));
725
+ failures.push({
726
+ type: "noKey",
727
+ key: messageKey,
728
+ source: path.resolve(sourceFile.fileName),
729
+ ...position
730
+ });
716
731
  }
717
- // If the key is not fully qualified then add the class name
718
- if (!localeKey.includes(".")) {
719
- localeKeyParts.push(expandTemplatePart(classNameParam));
732
+ else {
733
+ const finalKeyParts = [];
734
+ const classNameExpanded = expandTemplatePart(classNameParam);
735
+ const messageKeyParts = messageKey.split(".");
736
+ if (messageKeyParts.length === 2 && classNameExpanded === messageKeyParts[0]) {
737
+ // But if it is fully qualified with exactly two segments and starts with the class name
738
+ // then the class name is redundant and should be removed in the source
739
+ const position = getSourcePosition(sourceFile, messageNode);
740
+ cliCore.CLIDisplay.errorMessage(core.I18n.formatMessage("error.validateLocales.noNeedToQualify", {
741
+ key: messageKey,
742
+ property: classNameParam,
743
+ source: path.resolve(sourceFile.fileName),
744
+ line: position.line,
745
+ column: position.column
746
+ }));
747
+ failures.push({
748
+ type: "qualify",
749
+ key: messageKey,
750
+ source: path.resolve(sourceFile.fileName),
751
+ ...position
752
+ });
753
+ }
754
+ else if (!messageKey.includes(".")) {
755
+ // If the key is not fully qualified then add the class name
756
+ // to the final key
757
+ finalKeyParts.push(classNameExpanded);
758
+ }
759
+ finalKeyParts.push(messageKey);
760
+ if (core.Is.stringValue(prefix)) {
761
+ finalKeyParts.unshift(prefix);
762
+ }
763
+ return finalKeyParts.join(".");
720
764
  }
721
- localeKeyParts.push(localeKey);
722
- return localeKeyParts.join(".");
723
765
  }
724
766
  }
725
767
  return undefined;
@@ -743,7 +785,7 @@ class CLI extends cliCore.CLIBase {
743
785
  return this.execute({
744
786
  title: "TWIN Validate Locales",
745
787
  appName: "validate-locales",
746
- version: "0.0.2-next.21", // x-release-please-version
788
+ version: "0.0.2-next.22", // x-release-please-version
747
789
  icon: "⚙️ ",
748
790
  supportsEnvFiles: false,
749
791
  overrideOutputWidth: options?.overrideOutputWidth
@@ -23,7 +23,7 @@ const ERROR_TYPES = [
23
23
  ];
24
24
  const SKIP_FILES = ["**/models/**/*.ts"];
25
25
  const SKIP_LITERALS = [
26
- /\d+\.\d+\.\d+(-\w+(\.\w+)*)?$/, // Version string
26
+ /^\d+\.\d+\.\d+(-\w+(\.\w+)*)?(-\d)?$/, // Version string
27
27
  /^[^@]+@[^@]+\.[^@]+$/, // Email string
28
28
  /\.json$/i, // ending in .json
29
29
  /\.js$/i, // ending in .js
@@ -93,7 +93,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
93
93
  const localeEntries = [];
94
94
  let hasQuoteError = false;
95
95
  let hasUnused = false;
96
- let hasMissing = false;
96
+ let hasFailures = false;
97
97
  for (const localeFile of localeFiles) {
98
98
  const dictionary = await CLIUtils.readJsonFile(localeFile);
99
99
  const locale = path.basename(localeFile, path.extname(localeFile));
@@ -118,27 +118,27 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
118
118
  });
119
119
  }
120
120
  }
121
- let missing = [];
121
+ let failures = [];
122
122
  const captureVariables = {};
123
123
  for (const sourceFile of sourceFiles) {
124
124
  const source = await readFile(sourceFile, "utf8");
125
125
  const sourceTs = ts.createSourceFile(sourceFile, source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
126
- visit(sourceTs, sourceTs, localeEntries, missing, captureVariables);
126
+ visit(sourceTs, sourceTs, localeEntries, failures, captureVariables);
127
127
  }
128
- missing = missing.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
129
- if (missing.length === 0) {
128
+ failures = failures.filter(mr => !ignore.some(pattern => pattern.test(mr.key)));
129
+ if (failures.length === 0) {
130
130
  CLIDisplay.write(I18n.formatMessage("commands.validate-locales.labels.noMissingLocaleEntries"));
131
131
  CLIDisplay.break();
132
132
  }
133
133
  else {
134
- hasMissing = true;
135
- for (const missingRef of missing) {
136
- if (missingRef.type === "key") {
134
+ hasFailures = true;
135
+ for (const failureRef of failures) {
136
+ if (failureRef.type === "key") {
137
137
  CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.missingLocaleEntry", {
138
- key: missingRef.key,
139
- source: missingRef.source,
140
- line: missingRef.line,
141
- column: missingRef.column
138
+ key: failureRef.key,
139
+ source: failureRef.source,
140
+ line: failureRef.line,
141
+ column: failureRef.column
142
142
  }));
143
143
  }
144
144
  }
@@ -164,7 +164,7 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
164
164
  }
165
165
  }
166
166
  }
167
- if (hasMissing || hasUnused || hasQuoteError) {
167
+ if (hasFailures || hasUnused || hasQuoteError) {
168
168
  throw new GeneralError("validateLocales", "validationFailed");
169
169
  }
170
170
  }
@@ -173,43 +173,43 @@ async function validateLocales(sourceFiles, localeFiles, ignore) {
173
173
  * @param sourceFile The TypeScript source file for position calculations.
174
174
  * @param node The node to visit.
175
175
  * @param localeEntries The locale entries.
176
- * @param missing The missing entries.
176
+ * @param failures The failure entries.
177
177
  * @param captureVariables The capture variables.
178
178
  */
179
- function visit(sourceFile, node, localeEntries, missing, captureVariables) {
179
+ function visit(sourceFile, node, localeEntries, failures, captureVariables) {
180
180
  let handled = false;
181
181
  if (ts.isNewExpression(node) &&
182
182
  ts.isIdentifier(node.expression) &&
183
183
  ERROR_TYPES.some(errorType => errorType.name === node.expression.getText())) {
184
- processErrorType(sourceFile, node, node.expression.text, localeEntries, missing);
184
+ processErrorType(sourceFile, node, node.expression.text, localeEntries, failures);
185
185
  handled = true;
186
186
  }
187
187
  else if (ts.isStringLiteral(node)) {
188
- processStringLiteral(sourceFile, node, localeEntries, missing);
188
+ processStringLiteral(sourceFile, node, localeEntries, failures);
189
189
  handled = true;
190
190
  }
191
191
  else if (ts.isTemplateExpression(node)) {
192
- processTemplateExpression(sourceFile, node, localeEntries, missing);
192
+ processTemplateExpression(sourceFile, node, localeEntries, failures);
193
193
  handled = true;
194
194
  }
195
195
  else if (ts.isCallExpression(node)) {
196
- handled = processCallExpression(sourceFile, node, localeEntries, missing, captureVariables);
196
+ handled = processCallExpression(sourceFile, node, localeEntries, failures, captureVariables);
197
197
  }
198
198
  else if (ts.isFunctionDeclaration(node)) {
199
199
  handled = processFunctionDeclaration(sourceFile, node);
200
200
  }
201
201
  else if (ts.isVariableDeclaration(node)) {
202
- handled = processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables);
202
+ handled = processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables);
203
203
  }
204
204
  else if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
205
205
  // Don't care about string in imports/exports
206
206
  handled = true;
207
207
  }
208
208
  else if (ts.isPropertyAssignment(node)) {
209
- handled = processPropertyAssignment(sourceFile, node, localeEntries, missing);
209
+ handled = processPropertyAssignment(sourceFile, node, localeEntries, failures);
210
210
  }
211
211
  if (!handled) {
212
- ts.forEachChild(node, child => visit(sourceFile, child, localeEntries, missing, captureVariables));
212
+ ts.forEachChild(node, child => visit(sourceFile, child, localeEntries, failures, captureVariables));
213
213
  }
214
214
  }
215
215
  /**
@@ -218,12 +218,12 @@ function visit(sourceFile, node, localeEntries, missing, captureVariables) {
218
218
  * @param node The node to process.
219
219
  * @param errorType The error type.
220
220
  * @param localeEntries The locale entries.
221
- * @param missing The missing entries.
221
+ * @param failures The failure entries.
222
222
  */
223
- function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
223
+ function processErrorType(sourceFile, node, errorType, localeEntries, failures) {
224
224
  const errType = ERROR_TYPES.find(e => e.name === errorType);
225
225
  if (Is.object(errType)) {
226
- const localeKey = localeFromClassAndMessage(node.arguments?.[0], node.arguments?.[1], "error");
226
+ const localeKey = localeFromClassAndMessage(sourceFile, node.arguments?.[0], node.arguments?.[1], "error", failures);
227
227
  if (Is.stringValue(localeKey)) {
228
228
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
229
229
  if (Is.object(localeEntry)) {
@@ -232,11 +232,11 @@ function processErrorType(sourceFile, node, errorType, localeEntries, missing) {
232
232
  if (errType.dynamicPropertyIndex !== -1) {
233
233
  usedProperties.push(...getPropertiesFromNode(node.arguments?.[errType.dynamicPropertyIndex]));
234
234
  }
235
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, missing);
235
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, failures);
236
236
  }
237
237
  }
238
238
  else {
239
- missing.push({
239
+ failures.push({
240
240
  type: "key",
241
241
  key: localeKey,
242
242
  source: path.resolve(sourceFile.fileName),
@@ -268,9 +268,9 @@ function findAndReferenceLocale(localeEntries, entryToMatch) {
268
268
  * @param sourceFile The TypeScript source file for position calculations.
269
269
  * @param node The node to process.
270
270
  * @param localeEntries The locale entries.
271
- * @param missing The missing entries.
271
+ * @param failures The failure entries.
272
272
  */
273
- function processStringLiteral(sourceFile, node, localeEntries, missing) {
273
+ function processStringLiteral(sourceFile, node, localeEntries, failures) {
274
274
  if (node.text.length > 3 &&
275
275
  node.text.includes(".") &&
276
276
  !/[ ()/]/.test(node.text) &&
@@ -283,18 +283,18 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
283
283
  if (localeEntry) {
284
284
  localeEntry.referenced = true;
285
285
  const usedProperties = getPropertiesFromNode(node);
286
- checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, missing);
286
+ checkPropertyUsage(sourceFile, node, localeEntry, node.text, usedProperties, failures);
287
287
  }
288
288
  if (!localeEntry && ["validation.", "common."].some(t => node.text.startsWith(t))) {
289
289
  localeEntry = findAndReferenceLocale(localeEntries, `error.${node.text}`);
290
290
  if (localeEntry) {
291
291
  localeEntry.referenced = true;
292
292
  const usedProperties = getPropertiesFromNode(node);
293
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, missing);
293
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, failures);
294
294
  }
295
295
  }
296
296
  if (!localeEntry) {
297
- missing.push({
297
+ failures.push({
298
298
  type: "key",
299
299
  key: node.text,
300
300
  source: path.resolve(sourceFile.fileName),
@@ -309,9 +309,9 @@ function processStringLiteral(sourceFile, node, localeEntries, missing) {
309
309
  * @param sourceFile The TypeScript source file for position calculations.
310
310
  * @param node The node to process.
311
311
  * @param localeEntries The locale entries.
312
- * @param missing The missing entries.
312
+ * @param failures The failure entries.
313
313
  */
314
- function processTemplateExpression(sourceFile, node, localeEntries, missing) {
314
+ function processTemplateExpression(sourceFile, node, localeEntries, failures) {
315
315
  // This case handles templates like `error.${nameof(Class)}.message`
316
316
  const templateParts = extractTemplatePartsWithExpressions(node);
317
317
  // Join all literal text parts to form a potential locale key
@@ -320,14 +320,14 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
320
320
  let localeEntry = findAndReferenceLocale(localeEntries, key);
321
321
  if (localeEntry) {
322
322
  const usedProperties = getPropertiesFromNode(node);
323
- checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, missing);
323
+ checkPropertyUsage(sourceFile, node, localeEntry, localeEntry.key, usedProperties, failures);
324
324
  }
325
325
  else if (["validation.", "common."].some(t => key.startsWith(t))) {
326
326
  localeEntry = findAndReferenceLocale(localeEntries, `error.${key}`);
327
327
  if (localeEntry) {
328
328
  localeEntry.referenced = true;
329
329
  const usedProperties = getPropertiesFromNode(node.parent.parent);
330
- checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, missing);
330
+ checkPropertyUsage(sourceFile, node, localeEntry, `error.${key}`, usedProperties, failures);
331
331
  }
332
332
  }
333
333
  }
@@ -337,11 +337,11 @@ function processTemplateExpression(sourceFile, node, localeEntries, missing) {
337
337
  * @param sourceFile The TypeScript source file for position calculations.
338
338
  * @param node The node to process.
339
339
  * @param localeEntries The locale entries.
340
- * @param missing The missing entries.
340
+ * @param failures The failure entries.
341
341
  * @param captureVariables The capture variables.
342
342
  * @returns True if processed, false otherwise.
343
343
  */
344
- function processCallExpression(sourceFile, node, localeEntries, missing, captureVariables) {
344
+ function processCallExpression(sourceFile, node, localeEntries, failures, captureVariables) {
345
345
  if (ts.isPropertyAccessExpression(node.expression)) {
346
346
  const functionName = node.expression.name.getText();
347
347
  if (functionName === "log" &&
@@ -375,14 +375,14 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
375
375
  }
376
376
  }
377
377
  }
378
- const localeKey = localeFromClassAndMessage(source, message, level);
378
+ const localeKey = localeFromClassAndMessage(sourceFile, source, message, level, failures);
379
379
  if (Is.stringValue(localeKey)) {
380
380
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
381
381
  if (Is.object(localeEntry)) {
382
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
382
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
383
383
  }
384
384
  else {
385
- missing.push({
385
+ failures.push({
386
386
  type: "key",
387
387
  key: localeKey,
388
388
  source: path.resolve(sourceFile.fileName),
@@ -401,10 +401,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
401
401
  const dataNames = getPropertiesFromNode(node.arguments[1]);
402
402
  const localeEntry = findAndReferenceLocale(localeEntries, localeKey);
403
403
  if (Is.object(localeEntry)) {
404
- checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], missing);
404
+ checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
405
405
  }
406
406
  else {
407
- missing.push({
407
+ failures.push({
408
408
  type: "key",
409
409
  key: localeKey,
410
410
  source: path.resolve(sourceFile.fileName),
@@ -422,10 +422,10 @@ function processCallExpression(sourceFile, node, localeEntries, missing, capture
422
422
  * @param sourceFile The TypeScript source file for position calculations.
423
423
  * @param node The node to process.
424
424
  * @param localeEntries The locale entries.
425
- * @param missing The missing entries.
425
+ * @param failures The failure entries.
426
426
  * @returns True if processed, false otherwise.
427
427
  */
428
- function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
428
+ function processFunctionDeclaration(sourceFile, node, localeEntries, failures) {
429
429
  if (Is.object(node.name) &&
430
430
  ts.isIdentifier(node.name) &&
431
431
  SKIP_METHODS.some(re => re.test(node.name?.text ?? ""))) {
@@ -438,11 +438,11 @@ function processFunctionDeclaration(sourceFile, node, localeEntries, missing) {
438
438
  * @param sourceFile The TypeScript source file for position calculations.
439
439
  * @param node The node to process.
440
440
  * @param localeEntries The locale entries.
441
- * @param missing The missing entries.
441
+ * @param failures The failure entries.
442
442
  * @param captureVariables The capture variables.
443
443
  * @returns True if processed, false otherwise.
444
444
  */
445
- function processVariableDeclaration(sourceFile, node, localeEntries, missing, captureVariables) {
445
+ function processVariableDeclaration(sourceFile, node, localeEntries, failures, captureVariables) {
446
446
  if (Is.object(node.name) &&
447
447
  Is.object(node.initializer) &&
448
448
  ts.isIdentifier(node.name) &&
@@ -458,10 +458,10 @@ function processVariableDeclaration(sourceFile, node, localeEntries, missing, ca
458
458
  * @param sourceFile The TypeScript source file for position calculations.
459
459
  * @param node The node to process.
460
460
  * @param localeEntries The locale entries.
461
- * @param missing The missing entries.
461
+ * @param failures The failure entries.
462
462
  * @returns True if processed, false otherwise.
463
463
  */
464
- function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
464
+ function processPropertyAssignment(sourceFile, node, localeEntries, failures) {
465
465
  if (Is.object(node.name) && ts.isIdentifier(node.name) && node.name.getText() === "message") {
466
466
  const localeKey = getExpandedText(node.initializer);
467
467
  if (Is.stringValue(localeKey)) {
@@ -470,7 +470,7 @@ function processPropertyAssignment(sourceFile, node, localeEntries, missing) {
470
470
  localeEntry = findAndReferenceLocale(localeEntries, `error.${localeKey}`);
471
471
  }
472
472
  if (!Is.object(localeEntry)) {
473
- missing.push({
473
+ failures.push({
474
474
  type: "key",
475
475
  key: localeKey,
476
476
  source: path.resolve(sourceFile.fileName),
@@ -529,7 +529,7 @@ function extractTemplatePartsWithExpressions(node) {
529
529
  * @returns True if the template parts are valid, false otherwise.
530
530
  */
531
531
  function hasValidTemplateContent(templateParts) {
532
- return !templateParts.some(part => /[ #,/:=?|]/.test(part));
532
+ return !templateParts.some(part => /[#,/:=?|]/.test(part));
533
533
  }
534
534
  /**
535
535
  * Expand template parts into a single string, processing nameof expressions.
@@ -573,9 +573,9 @@ function expandTemplatePart(templatePart) {
573
573
  * @param localeEntry The locale entry to check against.
574
574
  * @param key The key in the locale entry.
575
575
  * @param usedProperties The properties used in the code.
576
- * @param missing The missing entries.
576
+ * @param failures The failure entries.
577
577
  */
578
- function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, missing) {
578
+ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties, failures) {
579
579
  for (const propName of localeEntry.propertyNames) {
580
580
  const propIndex = usedProperties.indexOf(propName);
581
581
  if (propIndex === -1) {
@@ -587,7 +587,7 @@ function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties,
587
587
  line: position.line,
588
588
  column: position.column
589
589
  }));
590
- missing.push({
590
+ failures.push({
591
591
  type: "property",
592
592
  key: localeEntry.key,
593
593
  source: path.resolve(sourceFile.fileName),
@@ -673,30 +673,72 @@ function isSkipLiteral(value) {
673
673
  }
674
674
  /**
675
675
  * Get the locale from the class and message nodes.
676
+ * @param sourceFile The TypeScript source file for position calculations.
676
677
  * @param classNode The class node.
677
678
  * @param messageNode The message node.
678
679
  * @param prefix The prefix for the locale key.
680
+ * @param failures The failure entries.
679
681
  * @returns The locale entry or undefined.
680
682
  */
681
- function localeFromClassAndMessage(classNode, messageNode, prefix) {
683
+ function localeFromClassAndMessage(sourceFile, classNode, messageNode, prefix, failures) {
682
684
  if (!classNode || !messageNode) {
683
685
  return undefined;
684
686
  }
685
687
  const classNameParam = classNode.getText();
686
688
  const classNameParamParts = classNameParam?.split(".");
687
689
  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);
690
+ const messageKey = getExpandedText(messageNode);
691
+ if (Is.stringValue(messageKey)) {
692
+ if (messageKey.includes(" ")) {
693
+ // If the message contains spaces then it is not a key
694
+ // but should be replaced by one
695
+ const position = getSourcePosition(sourceFile, messageNode);
696
+ CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.shouldBeKey", {
697
+ value: messageKey,
698
+ source: path.resolve(sourceFile.fileName),
699
+ line: position.line,
700
+ column: position.column
701
+ }));
702
+ failures.push({
703
+ type: "noKey",
704
+ key: messageKey,
705
+ source: path.resolve(sourceFile.fileName),
706
+ ...position
707
+ });
693
708
  }
694
- // If the key is not fully qualified then add the class name
695
- if (!localeKey.includes(".")) {
696
- localeKeyParts.push(expandTemplatePart(classNameParam));
709
+ else {
710
+ const finalKeyParts = [];
711
+ const classNameExpanded = expandTemplatePart(classNameParam);
712
+ const messageKeyParts = messageKey.split(".");
713
+ if (messageKeyParts.length === 2 && classNameExpanded === messageKeyParts[0]) {
714
+ // But if it is fully qualified with exactly two segments and starts with the class name
715
+ // then the class name is redundant and should be removed in the source
716
+ const position = getSourcePosition(sourceFile, messageNode);
717
+ CLIDisplay.errorMessage(I18n.formatMessage("error.validateLocales.noNeedToQualify", {
718
+ key: messageKey,
719
+ property: classNameParam,
720
+ source: path.resolve(sourceFile.fileName),
721
+ line: position.line,
722
+ column: position.column
723
+ }));
724
+ failures.push({
725
+ type: "qualify",
726
+ key: messageKey,
727
+ source: path.resolve(sourceFile.fileName),
728
+ ...position
729
+ });
730
+ }
731
+ else if (!messageKey.includes(".")) {
732
+ // If the key is not fully qualified then add the class name
733
+ // to the final key
734
+ finalKeyParts.push(classNameExpanded);
735
+ }
736
+ finalKeyParts.push(messageKey);
737
+ if (Is.stringValue(prefix)) {
738
+ finalKeyParts.unshift(prefix);
739
+ }
740
+ return finalKeyParts.join(".");
697
741
  }
698
- localeKeyParts.push(localeKey);
699
- return localeKeyParts.join(".");
700
742
  }
701
743
  }
702
744
  return undefined;
@@ -720,7 +762,7 @@ class CLI extends CLIBase {
720
762
  return this.execute({
721
763
  title: "TWIN Validate Locales",
722
764
  appName: "validate-locales",
723
- version: "0.0.2-next.21", // x-release-please-version
765
+ version: "0.0.2-next.22", // x-release-please-version
724
766
  icon: "⚙️ ",
725
767
  supportsEnvFiles: false,
726
768
  overrideOutputWidth: options?.overrideOutputWidth
@@ -115,7 +115,9 @@
115
115
  "usesSingleQuotes": "Locale entry \"{key}\" uses single quotes around a parameter. Use double quotes otherwise the value will not be substituted correctly.",
116
116
  "missingLocaleEntry": "Locale entry \"{key}\" is missing from the locale file it is referenced in source file \"{source}:{line}:{column}\".",
117
117
  "unableToProcessContent": "Unable to process content \"{content}\".",
118
- "missingPropertyInLocale": "Missing property \"{property}\" when referencing key \"{key}\" in source file \"{source}:{line}:{column}\"."
118
+ "missingPropertyInLocale": "Missing property \"{property}\" when referencing key \"{key}\" in source file \"{source}:{line}:{column}\".",
119
+ "noNeedToQualify": "No need to qualify key \"{key}\" with property \"{property}\" in source file \"{source}:{line}:{column}\", as source is the same.",
120
+ "shouldBeKey": "Value \"{value}\" should be an i18n key in source file \"{source}:{line}:{column}\"."
119
121
  }
120
122
  },
121
123
  "warn": {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Model for a locale failure reference.
3
+ */
4
+ export interface ILocaleFailure {
5
+ /**
6
+ * The type of the failure reference.
7
+ */
8
+ type: string;
9
+ /**
10
+ * The key of the failure reference.
11
+ */
12
+ key: string;
13
+ /**
14
+ * The source file for the failure reference.
15
+ */
16
+ source: string;
17
+ /**
18
+ * The line number for the failure reference.
19
+ */
20
+ line: number;
21
+ /**
22
+ * The column number for the failure reference.
23
+ */
24
+ column: number;
25
+ }
package/docs/changelog.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.2-next.22](https://github.com/twinfoundation/framework/compare/validate-locales-v0.0.2-next.21...validate-locales-v0.0.2-next.22) (2025-10-10)
4
+
5
+
6
+ ### Features
7
+
8
+ * detect unused prefixes and non i18 key messages ([a357810](https://github.com/twinfoundation/framework/commit/a357810754e25478496c2e6fd71ddc49dee9f747))
9
+
10
+
11
+ ### Dependencies
12
+
13
+ * The following workspace dependencies were updated
14
+ * dependencies
15
+ * @twin.org/cli-core bumped from 0.0.2-next.21 to 0.0.2-next.22
16
+ * @twin.org/core bumped from 0.0.2-next.21 to 0.0.2-next.22
17
+ * @twin.org/nameof bumped from 0.0.2-next.21 to 0.0.2-next.22
18
+ * @twin.org/nameof-transformer bumped from 0.0.2-next.21 to 0.0.2-next.22
19
+ * devDependencies
20
+ * @twin.org/merge-locales bumped from 0.0.2-next.21 to 0.0.2-next.22
21
+ * @twin.org/nameof-vitest-plugin bumped from 0.0.2-next.21 to 0.0.2-next.22
22
+
3
23
  ## [0.0.2-next.21](https://github.com/twinfoundation/framework/compare/validate-locales-v0.0.2-next.20...validate-locales-v0.0.2-next.21) (2025-10-09)
4
24
 
5
25
 
package/locales/en.json CHANGED
@@ -6,7 +6,9 @@
6
6
  "usesSingleQuotes": "Locale entry \"{key}\" uses single quotes around a parameter. Use double quotes otherwise the value will not be substituted correctly.",
7
7
  "missingLocaleEntry": "Locale entry \"{key}\" is missing from the locale file it is referenced in source file \"{source}:{line}:{column}\".",
8
8
  "unableToProcessContent": "Unable to process content \"{content}\".",
9
- "missingPropertyInLocale": "Missing property \"{property}\" when referencing key \"{key}\" in source file \"{source}:{line}:{column}\"."
9
+ "missingPropertyInLocale": "Missing property \"{property}\" when referencing key \"{key}\" in source file \"{source}:{line}:{column}\".",
10
+ "noNeedToQualify": "No need to qualify key \"{key}\" with property \"{property}\" in source file \"{source}:{line}:{column}\", as source is the same.",
11
+ "shouldBeKey": "Value \"{value}\" should be an i18n key in source file \"{source}:{line}:{column}\"."
10
12
  }
11
13
  },
12
14
  "commands": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twin.org/validate-locales",
3
- "version": "0.0.2-next.21",
3
+ "version": "0.0.2-next.22",
4
4
  "description": "Tool to validate source files against the locales",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,13 +14,13 @@
14
14
  "node": ">=20.0.0"
15
15
  },
16
16
  "dependencies": {
17
- "@twin.org/cli-core": "0.0.2-next.21",
18
- "@twin.org/core": "0.0.2-next.21",
19
- "@twin.org/nameof": "0.0.2-next.21",
20
- "@twin.org/nameof-transformer": "0.0.2-next.21",
17
+ "@twin.org/cli-core": "0.0.2-next.22",
18
+ "@twin.org/core": "0.0.2-next.22",
19
+ "@twin.org/nameof": "0.0.2-next.22",
20
+ "@twin.org/nameof-transformer": "0.0.2-next.22",
21
21
  "commander": "14.0.1",
22
22
  "glob": "11.0.3",
23
- "typescript": "5.9.2"
23
+ "typescript": "5.9.3"
24
24
  },
25
25
  "main": "./dist/cjs/index.cjs",
26
26
  "module": "./dist/esm/index.mjs",
@@ -1,25 +0,0 @@
1
- /**
2
- * Model for a locale dictionary missing reference.
3
- */
4
- export interface ILocaleMissingReference {
5
- /**
6
- * The type of the missing reference.
7
- */
8
- type: string;
9
- /**
10
- * The key of the missing reference.
11
- */
12
- key: string;
13
- /**
14
- * The source file for the missing reference.
15
- */
16
- source: string;
17
- /**
18
- * The line number for the missing reference.
19
- */
20
- line: number;
21
- /**
22
- * The column number for the missing reference.
23
- */
24
- column: number;
25
- }