@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.
- package/README.md +4 -4
- package/bin/index.js +1 -1
- package/dist/es/cli.js +37 -0
- package/dist/es/cli.js.map +1 -0
- package/dist/{esm/index.mjs → es/commands/validateLocales.js} +242 -129
- package/dist/es/commands/validateLocales.js.map +1 -0
- package/dist/es/index.js +5 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/ILocaleDictionaryEntry.js +2 -0
- package/dist/es/models/ILocaleDictionaryEntry.js.map +1 -0
- package/dist/es/models/ILocaleFailure.js +2 -0
- package/dist/es/models/ILocaleFailure.js.map +1 -0
- package/dist/locales/en.json +8 -3
- package/dist/types/index.d.ts +2 -2
- package/dist/types/models/ILocaleFailure.d.ts +25 -0
- package/docs/changelog.md +949 -3
- package/docs/reference/classes/CLI.md +66 -2
- package/docs/usage.md +35 -0
- package/locales/en.json +3 -1
- package/package.json +15 -17
- package/dist/cjs/index.cjs +0 -763
- package/dist/types/models/ILocaleMissingReference.d.ts +0 -25
- package/docs/examples.md +0 -15
|
@@ -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
|
-
{
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
135
|
+
visit(sourceTs, sourceTs, localeEntries, failures, captureVariables);
|
|
127
136
|
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
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
|
-
|
|
135
|
-
for (const
|
|
136
|
-
if (
|
|
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:
|
|
139
|
-
source:
|
|
140
|
-
line:
|
|
141
|
-
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 (
|
|
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
|
|
185
|
+
* @param failures The failure entries.
|
|
177
186
|
* @param captureVariables The capture variables.
|
|
178
187
|
*/
|
|
179
|
-
function visit(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
197
|
+
processStringLiteral(sourceFile, node, localeEntries, failures);
|
|
189
198
|
handled = true;
|
|
190
199
|
}
|
|
191
200
|
else if (ts.isTemplateExpression(node)) {
|
|
192
|
-
processTemplateExpression(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
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,
|
|
221
|
+
handled = processPropertyAssignment(sourceFile, node, localeEntries, failures);
|
|
210
222
|
}
|
|
211
223
|
if (!handled) {
|
|
212
|
-
ts.forEachChild(node, child => visit(sourceFile, child, localeEntries,
|
|
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
|
|
233
|
+
* @param failures The failure entries.
|
|
222
234
|
*/
|
|
223
|
-
function processErrorType(sourceFile, node, errorType, localeEntries,
|
|
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,
|
|
247
|
+
checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, failures);
|
|
236
248
|
}
|
|
237
249
|
}
|
|
238
250
|
else {
|
|
239
|
-
|
|
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
|
|
283
|
+
* @param failures The failure entries.
|
|
272
284
|
*/
|
|
273
|
-
function processStringLiteral(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
305
|
+
checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, failures);
|
|
294
306
|
}
|
|
295
307
|
}
|
|
296
308
|
if (!localeEntry) {
|
|
297
|
-
|
|
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
|
|
324
|
+
* @param failures The failure entries.
|
|
313
325
|
*/
|
|
314
|
-
function processTemplateExpression(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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 (
|
|
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 ?? [],
|
|
397
|
+
checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
|
|
383
398
|
}
|
|
384
399
|
else {
|
|
385
|
-
|
|
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 ?? [],
|
|
430
|
+
checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
|
|
405
431
|
}
|
|
406
432
|
else {
|
|
407
|
-
|
|
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
|
|
451
|
+
* @param failures The failure entries.
|
|
426
452
|
* @returns True if processed, false otherwise.
|
|
427
453
|
*/
|
|
428
|
-
function processFunctionDeclaration(sourceFile, node, localeEntries,
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
487
|
+
* @param failures The failure entries.
|
|
462
488
|
* @returns True if processed, false otherwise.
|
|
463
489
|
*/
|
|
464
|
-
function
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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 => /[
|
|
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
|
|
679
|
+
* @param failures The failure entries.
|
|
577
680
|
*/
|
|
578
|
-
function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties,
|
|
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
|
-
|
|
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
|
|
689
|
-
if (Is.stringValue(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|