@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 +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} +125 -115
- 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 +3 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/models/ILocaleFailure.d.ts +25 -0
- package/docs/changelog.md +42 -0
- package/locales/en.json +3 -1
- package/package.json +11 -13
- package/dist/cjs/index.cjs +0 -763
- package/dist/types/models/ILocaleMissingReference.d.ts +0 -25
package/bin/index.js
CHANGED
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
125
|
+
visit(sourceTs, sourceTs, localeEntries, failures, captureVariables);
|
|
127
126
|
}
|
|
128
|
-
|
|
129
|
-
if (
|
|
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
|
-
|
|
135
|
-
for (const
|
|
136
|
-
if (
|
|
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:
|
|
139
|
-
source:
|
|
140
|
-
line:
|
|
141
|
-
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 (
|
|
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
|
|
175
|
+
* @param failures The failure entries.
|
|
177
176
|
* @param captureVariables The capture variables.
|
|
178
177
|
*/
|
|
179
|
-
function visit(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
187
|
+
processStringLiteral(sourceFile, node, localeEntries, failures);
|
|
189
188
|
handled = true;
|
|
190
189
|
}
|
|
191
190
|
else if (ts.isTemplateExpression(node)) {
|
|
192
|
-
processTemplateExpression(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
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,
|
|
208
|
+
handled = processPropertyAssignment(sourceFile, node, localeEntries, failures);
|
|
210
209
|
}
|
|
211
210
|
if (!handled) {
|
|
212
|
-
ts.forEachChild(node, child => visit(sourceFile, child, localeEntries,
|
|
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
|
|
220
|
+
* @param failures The failure entries.
|
|
222
221
|
*/
|
|
223
|
-
function processErrorType(sourceFile, node, errorType, localeEntries,
|
|
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,
|
|
234
|
+
checkPropertyUsage(sourceFile, node, localeEntry, localeKey, usedProperties, failures);
|
|
236
235
|
}
|
|
237
236
|
}
|
|
238
237
|
else {
|
|
239
|
-
|
|
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
|
|
270
|
+
* @param failures The failure entries.
|
|
272
271
|
*/
|
|
273
|
-
function processStringLiteral(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
292
|
+
checkPropertyUsage(sourceFile, node, localeEntry, `error.${node.text}`, usedProperties, failures);
|
|
294
293
|
}
|
|
295
294
|
}
|
|
296
295
|
if (!localeEntry) {
|
|
297
|
-
|
|
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
|
|
311
|
+
* @param failures The failure entries.
|
|
313
312
|
*/
|
|
314
|
-
function processTemplateExpression(sourceFile, node, localeEntries,
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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 (
|
|
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 ?? [],
|
|
384
|
+
checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
|
|
383
385
|
}
|
|
384
386
|
else {
|
|
385
|
-
|
|
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 ?? [],
|
|
406
|
+
checkPropertyUsage(sourceFile, node, localeEntry, localeKey, dataNames ?? [], failures);
|
|
405
407
|
}
|
|
406
408
|
else {
|
|
407
|
-
|
|
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
|
|
427
|
+
* @param failures The failure entries.
|
|
426
428
|
* @returns True if processed, false otherwise.
|
|
427
429
|
*/
|
|
428
|
-
function processFunctionDeclaration(sourceFile, node, localeEntries,
|
|
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
|
|
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,
|
|
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
|
|
463
|
+
* @param failures The failure entries.
|
|
462
464
|
* @returns True if processed, false otherwise.
|
|
463
465
|
*/
|
|
464
|
-
function processPropertyAssignment(sourceFile, node, localeEntries,
|
|
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
|
-
|
|
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 => /[
|
|
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
|
|
578
|
+
* @param failures The failure entries.
|
|
577
579
|
*/
|
|
578
|
-
function checkPropertyUsage(sourceFile, node, localeEntry, key, usedProperties,
|
|
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
|
-
|
|
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
|
|
689
|
-
if (Is.stringValue(
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|