intor-cli 0.0.13 → 0.0.14
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 +0 -6
- package/package.json +1 -1
- package/src/core/extract-usages/collectors/collect-trans-usages.ts +69 -0
- package/src/core/extract-usages/collectors/index.ts +1 -0
- package/src/core/extract-usages/extract-usages-from-source-file.ts +51 -31
- package/src/core/extract-usages/extract-usages.ts +6 -1
- package/src/core/extract-usages/types.ts +6 -0
- package/src/features/check/check.ts +3 -0
- package/src/features/check/diagnostics/collect.ts +13 -1
- package/src/features/check/diagnostics/group.ts +4 -13
- package/src/features/check/diagnostics/rules/enforce-missing-replacements.ts +1 -1
- package/src/features/check/diagnostics/rules/enforce-missing-rich.ts +1 -1
- package/src/features/check/diagnostics/rules/key/empty.ts +4 -4
- package/src/features/check/diagnostics/rules/key/not-found.ts +5 -4
- package/src/features/check/diagnostics/rules/key/types.ts +8 -0
- package/src/features/check/diagnostics/rules/pre-key/not-found.ts +1 -1
- package/src/features/check/diagnostics/rules/replacement/missing.ts +1 -1
- package/src/features/check/diagnostics/rules/replacement/not-allowed.ts +1 -1
- package/src/features/check/diagnostics/rules/replacement/unused.ts +1 -1
- package/src/features/check/diagnostics/rules/rich/missing.ts +1 -1
- package/src/features/check/diagnostics/rules/rich/not-allowed.ts +1 -1
- package/src/features/check/diagnostics/rules/rich/unused.ts +1 -1
- package/src/features/check/diagnostics/types.ts +2 -6
- package/src/features/check/print-summary.ts +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
<h1 align="center">intor-cli</h1>
|
|
2
2
|
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
CLI tool for intor.
|
|
6
|
-
|
|
7
|
-
</div>
|
|
8
|
-
|
|
9
3
|
<div align="center">
|
|
10
4
|
|
|
11
5
|
[](https://www.npmjs.com/package/intor-cli)
|
package/package.json
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { TransUsage } from "../types";
|
|
2
|
+
import type { SourceFile } from "ts-morph";
|
|
3
|
+
import { Node, SyntaxKind } from "ts-morph";
|
|
4
|
+
import { isStaticStringLiteral } from "./utils/is-static-string-literal";
|
|
5
|
+
|
|
6
|
+
const COMPONENT_NAME = "Trans";
|
|
7
|
+
const KEY_PROPERTY_NAME = "i18nKey";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collect static translation key usages from <Trans /> components
|
|
11
|
+
* within a single source file.
|
|
12
|
+
*/
|
|
13
|
+
export function collectTransUsages(sourceFile: SourceFile): TransUsage[] {
|
|
14
|
+
const usages: TransUsage[] = [];
|
|
15
|
+
|
|
16
|
+
sourceFile.forEachDescendant((node) => {
|
|
17
|
+
// ------------------------------------------------------------
|
|
18
|
+
// Match <Trans ... /> or <Trans ...></Trans>
|
|
19
|
+
// ------------------------------------------------------------
|
|
20
|
+
if (
|
|
21
|
+
!node.isKind(SyntaxKind.JsxSelfClosingElement) &&
|
|
22
|
+
!node.isKind(SyntaxKind.JsxOpeningElement)
|
|
23
|
+
) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tagName = node.getTagNameNode()?.getText();
|
|
28
|
+
if (tagName !== COMPONENT_NAME) return;
|
|
29
|
+
|
|
30
|
+
// ------------------------------------------------------------
|
|
31
|
+
// Resolve i18nKey attribute
|
|
32
|
+
// ------------------------------------------------------------
|
|
33
|
+
const i18nKeyAttr = node
|
|
34
|
+
.getAttributes()
|
|
35
|
+
.find(
|
|
36
|
+
(attr) =>
|
|
37
|
+
attr.isKind(SyntaxKind.JsxAttribute) &&
|
|
38
|
+
attr.getNameNode().getText() === KEY_PROPERTY_NAME,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!i18nKeyAttr || !Node.isJsxAttribute(i18nKeyAttr)) return;
|
|
42
|
+
|
|
43
|
+
const initializer = i18nKeyAttr.getInitializer();
|
|
44
|
+
if (!initializer) return;
|
|
45
|
+
|
|
46
|
+
// Support:
|
|
47
|
+
// <Trans i18nKey="home.title" />
|
|
48
|
+
// <Trans i18nKey={"home.title"} />
|
|
49
|
+
const expr = initializer.isKind(SyntaxKind.JsxExpression)
|
|
50
|
+
? initializer.getExpression()
|
|
51
|
+
: initializer;
|
|
52
|
+
if (!isStaticStringLiteral(expr)) return;
|
|
53
|
+
const literal = expr;
|
|
54
|
+
|
|
55
|
+
// ------------------------------------------------------------
|
|
56
|
+
// Source location
|
|
57
|
+
// ------------------------------------------------------------
|
|
58
|
+
const pos = sourceFile.getLineAndColumnAtPos(literal.getStart());
|
|
59
|
+
|
|
60
|
+
usages.push({
|
|
61
|
+
key: literal.getLiteralText(),
|
|
62
|
+
file: sourceFile.getFilePath(),
|
|
63
|
+
line: pos.line,
|
|
64
|
+
column: pos.column,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return usages;
|
|
69
|
+
}
|
|
@@ -3,3 +3,4 @@ export { collectKeyUsages } from "./collect-key-usages";
|
|
|
3
3
|
export { collectReplacementUsages } from "./collect-replacement-usages";
|
|
4
4
|
export { collectRichUsages } from "./collect-rich-usages";
|
|
5
5
|
export { collectPreKeys } from "./collect-pre-keys";
|
|
6
|
+
export { collectTransUsages } from "./collect-trans-usages";
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtractedUsages,
|
|
3
|
+
KeyUsage,
|
|
4
|
+
PreKeyMap,
|
|
5
|
+
PreKeyUsage,
|
|
6
|
+
ReplacementUsage,
|
|
7
|
+
RichUsage,
|
|
8
|
+
} from "./types";
|
|
2
9
|
import type { SourceFile } from "ts-morph";
|
|
3
10
|
import {
|
|
4
11
|
collectTranslatorBindings,
|
|
@@ -6,6 +13,7 @@ import {
|
|
|
6
13
|
collectReplacementUsages,
|
|
7
14
|
collectRichUsages,
|
|
8
15
|
collectPreKeys,
|
|
16
|
+
collectTransUsages,
|
|
9
17
|
} from "./collectors";
|
|
10
18
|
|
|
11
19
|
function attachPreKey<T extends { localName: string; preKey?: string }>(
|
|
@@ -25,48 +33,60 @@ export function extractUsagesFromSourceFile(
|
|
|
25
33
|
sourceFile: SourceFile,
|
|
26
34
|
): ExtractedUsages {
|
|
27
35
|
// -----------------------------------------------------------------------
|
|
28
|
-
//
|
|
36
|
+
// Trans component usages (independent of translator bindings)
|
|
29
37
|
// -----------------------------------------------------------------------
|
|
30
|
-
const
|
|
31
|
-
if (translatorBindingMap.size === 0) {
|
|
32
|
-
return { preKey: [], key: [], replacement: [], rich: [] };
|
|
33
|
-
}
|
|
38
|
+
const transUsages = collectTransUsages(sourceFile);
|
|
34
39
|
|
|
35
40
|
// -----------------------------------------------------------------------
|
|
36
|
-
//
|
|
41
|
+
// Translator binding (function-based APIs)
|
|
37
42
|
// -----------------------------------------------------------------------
|
|
38
|
-
const
|
|
43
|
+
const translatorBindingMap = collectTranslatorBindings(sourceFile);
|
|
39
44
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
translatorBindingMap,
|
|
46
|
-
);
|
|
45
|
+
// Prepare defaults
|
|
46
|
+
let keyUsages: KeyUsage[] = [];
|
|
47
|
+
let replacementUsages: ReplacementUsage[] = [];
|
|
48
|
+
let richUsages: RichUsage[] = [];
|
|
49
|
+
let preKeyUsages: PreKeyUsage[] = [];
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
if (translatorBindingMap.size > 0) {
|
|
52
|
+
// ---------------------------------------------------------------------
|
|
53
|
+
// Key usages
|
|
54
|
+
// ---------------------------------------------------------------------
|
|
55
|
+
keyUsages = collectKeyUsages(sourceFile, translatorBindingMap);
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
// ---------------------------------------------------------------------
|
|
58
|
+
// Replacement usages
|
|
59
|
+
// ---------------------------------------------------------------------
|
|
60
|
+
replacementUsages = collectReplacementUsages(
|
|
61
|
+
sourceFile,
|
|
62
|
+
translatorBindingMap,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------
|
|
66
|
+
// Rich usages
|
|
67
|
+
// ---------------------------------------------------------------------
|
|
68
|
+
richUsages = collectRichUsages(sourceFile, translatorBindingMap);
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
// ---------------------------------------------------------------------
|
|
71
|
+
// PreKey values
|
|
72
|
+
// ---------------------------------------------------------------------
|
|
73
|
+
const { preKeyMap, usages } = collectPreKeys(
|
|
74
|
+
sourceFile,
|
|
75
|
+
translatorBindingMap,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
attachPreKey(keyUsages, preKeyMap);
|
|
79
|
+
attachPreKey(replacementUsages, preKeyMap);
|
|
80
|
+
attachPreKey(richUsages, preKeyMap);
|
|
81
|
+
|
|
82
|
+
preKeyUsages = usages;
|
|
83
|
+
}
|
|
65
84
|
|
|
66
85
|
return {
|
|
67
|
-
preKey:
|
|
86
|
+
preKey: preKeyUsages,
|
|
68
87
|
key: keyUsages,
|
|
69
88
|
replacement: replacementUsages,
|
|
70
89
|
rich: richUsages,
|
|
90
|
+
trans: transUsages,
|
|
71
91
|
};
|
|
72
92
|
}
|
|
@@ -6,7 +6,10 @@ import { loadSourceFilesFromTsconfig } from "./load-source-files-from-tscofnig";
|
|
|
6
6
|
|
|
7
7
|
/** Check whether a file-level extraction produced any meaningful usage */
|
|
8
8
|
const isEmpty = (u: ExtractedUsages) =>
|
|
9
|
-
u.key.length === 0 &&
|
|
9
|
+
u.key.length === 0 &&
|
|
10
|
+
u.replacement.length === 0 &&
|
|
11
|
+
u.rich.length === 0 &&
|
|
12
|
+
u.trans.length === 0;
|
|
10
13
|
|
|
11
14
|
export interface ExtractUsagesOptions {
|
|
12
15
|
tsconfigPath?: string;
|
|
@@ -25,6 +28,7 @@ export function extractUsages(options?: ExtractUsagesOptions): ExtractedUsages {
|
|
|
25
28
|
key: [],
|
|
26
29
|
replacement: [],
|
|
27
30
|
rich: [],
|
|
31
|
+
trans: [],
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
// Debug counters
|
|
@@ -52,6 +56,7 @@ export function extractUsages(options?: ExtractUsagesOptions): ExtractedUsages {
|
|
|
52
56
|
result.key.push(...partial.key);
|
|
53
57
|
result.replacement.push(...partial.replacement);
|
|
54
58
|
result.rich.push(...partial.rich);
|
|
59
|
+
result.trans.push(...partial.trans);
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
// Debug summary
|
|
@@ -52,10 +52,16 @@ export interface RichUsage extends TranslatorBinding, SourceLocation {
|
|
|
52
52
|
preKey?: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
export interface TransUsage extends SourceLocation {
|
|
56
|
+
key: string;
|
|
57
|
+
configKey?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
/** Aggregated static translator usages extracted from a project. */
|
|
56
61
|
export interface ExtractedUsages {
|
|
57
62
|
preKey: PreKeyUsage[];
|
|
58
63
|
key: KeyUsage[];
|
|
59
64
|
replacement: ReplacementUsage[];
|
|
60
65
|
rich: RichUsage[];
|
|
66
|
+
trans: TransUsage[];
|
|
61
67
|
}
|
|
@@ -58,6 +58,9 @@ export async function check(extractOptions?: ExtractUsagesOptions) {
|
|
|
58
58
|
rich: usages.rich.filter(
|
|
59
59
|
(u) => resolveConfigKey(u.configKey, defaultConfigKey) === configKey,
|
|
60
60
|
),
|
|
61
|
+
trans: usages.trans.filter(
|
|
62
|
+
(u) => resolveConfigKey(u.configKey, defaultConfigKey) === configKey,
|
|
63
|
+
),
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
// Diagnostic
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Diagnostic } from "./types";
|
|
2
2
|
import type { ExtractedUsages, InferredSchemas } from "../../../core";
|
|
3
|
+
import type { KeyUsageLike } from "./rules/key/types";
|
|
3
4
|
import { enforceMissingReplacements } from "./rules/enforce-missing-replacements";
|
|
4
5
|
import { enforceMissingRich } from "./rules/enforce-missing-rich";
|
|
5
6
|
import { keyEmpty, keyNotFound } from "./rules/key";
|
|
@@ -18,13 +19,24 @@ export function collectDiagnostics(
|
|
|
18
19
|
) {
|
|
19
20
|
const diagnostics: Diagnostic[] = [];
|
|
20
21
|
|
|
22
|
+
const allKeyUsages: KeyUsageLike[] = [
|
|
23
|
+
...usages.key.map((u) => ({ ...u, origin: u.method })),
|
|
24
|
+
...usages.trans.map((u) => ({
|
|
25
|
+
origin: "<Trans />",
|
|
26
|
+
key: u.key,
|
|
27
|
+
file: u.file,
|
|
28
|
+
line: u.line,
|
|
29
|
+
column: u.column,
|
|
30
|
+
})),
|
|
31
|
+
];
|
|
32
|
+
|
|
21
33
|
// PreKey
|
|
22
34
|
for (const usage of usages.preKey) {
|
|
23
35
|
diagnostics.push(...preKeyNotFound(usage, messagesSchema));
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
// Key
|
|
27
|
-
for (const usage of
|
|
39
|
+
for (const usage of allKeyUsages) {
|
|
28
40
|
diagnostics.push(...keyNotFound(usage, messagesSchema), ...keyEmpty(usage));
|
|
29
41
|
}
|
|
30
42
|
|
|
@@ -8,16 +8,8 @@ export function groupDiagnostics(diagnostics: Diagnostic[]): DiagnosticGroup[] {
|
|
|
8
8
|
const map = new Map<string, DiagnosticGroup>();
|
|
9
9
|
|
|
10
10
|
for (const diagnostic of diagnostics) {
|
|
11
|
-
const {
|
|
12
|
-
|
|
13
|
-
factory,
|
|
14
|
-
method,
|
|
15
|
-
message,
|
|
16
|
-
messageKey,
|
|
17
|
-
file,
|
|
18
|
-
line,
|
|
19
|
-
column,
|
|
20
|
-
} = diagnostic;
|
|
11
|
+
const { severity, origin, message, messageKey, file, line, column } =
|
|
12
|
+
diagnostic;
|
|
21
13
|
|
|
22
14
|
// --------------------------------------------------
|
|
23
15
|
// Grouping key
|
|
@@ -25,15 +17,14 @@ export function groupDiagnostics(diagnostics: Diagnostic[]): DiagnosticGroup[] {
|
|
|
25
17
|
// - Fallback to exact source location
|
|
26
18
|
// --------------------------------------------------
|
|
27
19
|
const groupId = messageKey
|
|
28
|
-
? `${file}::${messageKey}::${
|
|
20
|
+
? `${file}::${messageKey}::${origin}`
|
|
29
21
|
: `${file}::${line}:${column}`;
|
|
30
22
|
|
|
31
23
|
// Initialize group if not exists
|
|
32
24
|
if (!map.has(groupId)) {
|
|
33
25
|
map.set(groupId, {
|
|
34
26
|
severity,
|
|
35
|
-
|
|
36
|
-
method,
|
|
27
|
+
origin,
|
|
37
28
|
messageKey,
|
|
38
29
|
problems: [],
|
|
39
30
|
file,
|
|
@@ -41,7 +41,7 @@ export function enforceMissingReplacements(
|
|
|
41
41
|
|
|
42
42
|
diagnostics.push({
|
|
43
43
|
severity: "warn",
|
|
44
|
-
method,
|
|
44
|
+
origin: method,
|
|
45
45
|
messageKey: keyPath,
|
|
46
46
|
code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.code,
|
|
47
47
|
message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.message(expected),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KeyUsageLike } from "./types";
|
|
2
2
|
import type { Diagnostic } from "../../types";
|
|
3
3
|
import { DIAGNOSTIC_MESSAGES } from "../../messages";
|
|
4
4
|
|
|
@@ -12,14 +12,14 @@ import { DIAGNOSTIC_MESSAGES } from "../../messages";
|
|
|
12
12
|
* t("")
|
|
13
13
|
* ```
|
|
14
14
|
*/
|
|
15
|
-
export function keyEmpty(usage:
|
|
16
|
-
const {
|
|
15
|
+
export function keyEmpty(usage: KeyUsageLike): Diagnostic[] {
|
|
16
|
+
const { origin, key, file, line, column } = usage;
|
|
17
17
|
|
|
18
18
|
if (!key) {
|
|
19
19
|
return [
|
|
20
20
|
{
|
|
21
21
|
severity: "warn",
|
|
22
|
-
|
|
22
|
+
origin,
|
|
23
23
|
messageKey: key,
|
|
24
24
|
code: DIAGNOSTIC_MESSAGES.KEY_EMPTY.code,
|
|
25
25
|
message: DIAGNOSTIC_MESSAGES.KEY_EMPTY.message(),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { KeyUsageLike } from "./types";
|
|
2
|
+
import type { InferNode } from "../../../../../core";
|
|
2
3
|
import type { Diagnostic } from "../../types";
|
|
3
4
|
import { DIAGNOSTIC_MESSAGES } from "../../messages";
|
|
4
5
|
import { getSchemaNodeAtPath } from "../../utils/get-schema-node-at-path";
|
|
@@ -15,10 +16,10 @@ import { resolveKeyPath } from "../../utils/resolve-key-path";
|
|
|
15
16
|
* ```
|
|
16
17
|
*/
|
|
17
18
|
export function keyNotFound(
|
|
18
|
-
usage:
|
|
19
|
+
usage: KeyUsageLike,
|
|
19
20
|
messagesSchema: InferNode,
|
|
20
21
|
): Diagnostic[] {
|
|
21
|
-
const {
|
|
22
|
+
const { origin, key, preKey, file, line, column } = usage;
|
|
22
23
|
|
|
23
24
|
if (!key) return [];
|
|
24
25
|
|
|
@@ -27,7 +28,7 @@ export function keyNotFound(
|
|
|
27
28
|
return [
|
|
28
29
|
{
|
|
29
30
|
severity: "warn",
|
|
30
|
-
|
|
31
|
+
origin,
|
|
31
32
|
messageKey: key,
|
|
32
33
|
code: DIAGNOSTIC_MESSAGES.KEY_NOT_FOUND.code,
|
|
33
34
|
message: DIAGNOSTIC_MESSAGES.KEY_NOT_FOUND.message(),
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import type { TranslatorFactory, TranslatorMethod } from "../../../core";
|
|
2
|
-
|
|
3
1
|
export interface Diagnostic {
|
|
4
2
|
severity: "error" | "warn";
|
|
5
|
-
|
|
6
|
-
method?: TranslatorMethod;
|
|
3
|
+
origin: string;
|
|
7
4
|
messageKey: string;
|
|
8
5
|
code: string;
|
|
9
6
|
message: string;
|
|
@@ -14,8 +11,7 @@ export interface Diagnostic {
|
|
|
14
11
|
|
|
15
12
|
export interface DiagnosticGroup {
|
|
16
13
|
severity: "error" | "warn";
|
|
17
|
-
|
|
18
|
-
method?: TranslatorMethod;
|
|
14
|
+
origin: string;
|
|
19
15
|
messageKey: string;
|
|
20
16
|
problems: string[]; // list of bullet messages
|
|
21
17
|
file: string;
|
|
@@ -13,9 +13,9 @@ export function printSummary(configId: string, grouped: DiagnosticGroup[]) {
|
|
|
13
13
|
|
|
14
14
|
// Problems
|
|
15
15
|
for (const group of grouped) {
|
|
16
|
-
const {
|
|
16
|
+
const { origin, messageKey, problems, file, lines } = group;
|
|
17
17
|
|
|
18
|
-
const header = `${messageKey} (${
|
|
18
|
+
const header = `${messageKey} (${origin})`;
|
|
19
19
|
|
|
20
20
|
print(header, 1);
|
|
21
21
|
printList(
|