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.
Files changed (24) hide show
  1. package/README.md +0 -6
  2. package/package.json +1 -1
  3. package/src/core/extract-usages/collectors/collect-trans-usages.ts +69 -0
  4. package/src/core/extract-usages/collectors/index.ts +1 -0
  5. package/src/core/extract-usages/extract-usages-from-source-file.ts +51 -31
  6. package/src/core/extract-usages/extract-usages.ts +6 -1
  7. package/src/core/extract-usages/types.ts +6 -0
  8. package/src/features/check/check.ts +3 -0
  9. package/src/features/check/diagnostics/collect.ts +13 -1
  10. package/src/features/check/diagnostics/group.ts +4 -13
  11. package/src/features/check/diagnostics/rules/enforce-missing-replacements.ts +1 -1
  12. package/src/features/check/diagnostics/rules/enforce-missing-rich.ts +1 -1
  13. package/src/features/check/diagnostics/rules/key/empty.ts +4 -4
  14. package/src/features/check/diagnostics/rules/key/not-found.ts +5 -4
  15. package/src/features/check/diagnostics/rules/key/types.ts +8 -0
  16. package/src/features/check/diagnostics/rules/pre-key/not-found.ts +1 -1
  17. package/src/features/check/diagnostics/rules/replacement/missing.ts +1 -1
  18. package/src/features/check/diagnostics/rules/replacement/not-allowed.ts +1 -1
  19. package/src/features/check/diagnostics/rules/replacement/unused.ts +1 -1
  20. package/src/features/check/diagnostics/rules/rich/missing.ts +1 -1
  21. package/src/features/check/diagnostics/rules/rich/not-allowed.ts +1 -1
  22. package/src/features/check/diagnostics/rules/rich/unused.ts +1 -1
  23. package/src/features/check/diagnostics/types.ts +2 -6
  24. 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
  [![NPM version](https://img.shields.io/npm/v/intor-cli?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/intor-cli)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intor-cli",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "The Intor CLI",
5
5
  "author": "Yiming Liao",
6
6
  "homepage": "https://github.com/yiming-liao/intor-cli#readme",
@@ -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 { ExtractedUsages, PreKeyMap } from "./types";
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
- // Translator binding
36
+ // Trans component usages (independent of translator bindings)
29
37
  // -----------------------------------------------------------------------
30
- const translatorBindingMap = collectTranslatorBindings(sourceFile);
31
- if (translatorBindingMap.size === 0) {
32
- return { preKey: [], key: [], replacement: [], rich: [] };
33
- }
38
+ const transUsages = collectTransUsages(sourceFile);
34
39
 
35
40
  // -----------------------------------------------------------------------
36
- // Key usages
41
+ // Translator binding (function-based APIs)
37
42
  // -----------------------------------------------------------------------
38
- const keyUsages = collectKeyUsages(sourceFile, translatorBindingMap);
43
+ const translatorBindingMap = collectTranslatorBindings(sourceFile);
39
44
 
40
- // -----------------------------------------------------------------------
41
- // Replacement usages
42
- // -----------------------------------------------------------------------
43
- const replacementUsages = collectReplacementUsages(
44
- sourceFile,
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
- // Rich usages
50
- // -----------------------------------------------------------------------
51
- const richUsages = collectRichUsages(sourceFile, translatorBindingMap);
51
+ if (translatorBindingMap.size > 0) {
52
+ // ---------------------------------------------------------------------
53
+ // Key usages
54
+ // ---------------------------------------------------------------------
55
+ keyUsages = collectKeyUsages(sourceFile, translatorBindingMap);
52
56
 
53
- // -----------------------------------------------------------------------
54
- // PreKey values
55
- // -----------------------------------------------------------------------
56
- const { preKeyMap, usages } = collectPreKeys(
57
- sourceFile,
58
- translatorBindingMap,
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
- // Attach preKey to all usages that support it
62
- attachPreKey(keyUsages, preKeyMap);
63
- attachPreKey(replacementUsages, preKeyMap);
64
- attachPreKey(richUsages, preKeyMap);
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: usages,
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 && u.replacement.length === 0 && u.rich.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 usages.key) {
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
- severity,
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}::${diagnostic.method}`
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
- factory,
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),
@@ -41,7 +41,7 @@ export function enforceMissingRich(
41
41
 
42
42
  diagnostics.push({
43
43
  severity: "warn",
44
- method,
44
+ origin: method,
45
45
  messageKey: keyPath,
46
46
  code: DIAGNOSTIC_MESSAGES.RICH_MISSING.code,
47
47
  message: DIAGNOSTIC_MESSAGES.RICH_MISSING.message(expected),
@@ -1,4 +1,4 @@
1
- import type { KeyUsage } from "../../../../../core";
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: KeyUsage): Diagnostic[] {
16
- const { method, key, file, line, column } = usage;
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
- method,
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 { KeyUsage, InferNode } from "../../../../../core";
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: KeyUsage,
19
+ usage: KeyUsageLike,
19
20
  messagesSchema: InferNode,
20
21
  ): Diagnostic[] {
21
- const { method, key, preKey, file, line, column } = usage;
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
- method,
31
+ origin,
31
32
  messageKey: key,
32
33
  code: DIAGNOSTIC_MESSAGES.KEY_NOT_FOUND.code,
33
34
  message: DIAGNOSTIC_MESSAGES.KEY_NOT_FOUND.message(),
@@ -0,0 +1,8 @@
1
+ import type { KeyUsage } from "../../../../../core";
2
+
3
+ export interface KeyUsageLike extends Omit<
4
+ KeyUsage,
5
+ "localName" | "factory" | "method"
6
+ > {
7
+ origin: string;
8
+ }
@@ -25,7 +25,7 @@ export function preKeyNotFound(
25
25
  return [
26
26
  {
27
27
  severity: "warn",
28
- factory,
28
+ origin: factory,
29
29
  messageKey: preKey,
30
30
  code: DIAGNOSTIC_MESSAGES.PRE_KEY_NOT_FOUND.code,
31
31
  message: DIAGNOSTIC_MESSAGES.PRE_KEY_NOT_FOUND.message(),
@@ -32,7 +32,7 @@ export function replacementsMissing(
32
32
  return [
33
33
  {
34
34
  severity: "warn",
35
- method,
35
+ origin: method,
36
36
  messageKey: keyPath,
37
37
  code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.code,
38
38
  message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_MISSING.message(missing),
@@ -27,7 +27,7 @@ export function replacementsNotAllowed(
27
27
  return [
28
28
  {
29
29
  severity: "warn",
30
- method,
30
+ origin: method,
31
31
  messageKey: keyPath,
32
32
  code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_NOT_ALLOWED.code,
33
33
  message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_NOT_ALLOWED.message(),
@@ -32,7 +32,7 @@ export function replacementsUnused(
32
32
  return [
33
33
  {
34
34
  severity: "warn",
35
- method,
35
+ origin: method,
36
36
  messageKey: keyPath,
37
37
  code: DIAGNOSTIC_MESSAGES.REPLACEMENTS_UNUSED.code,
38
38
  message: DIAGNOSTIC_MESSAGES.REPLACEMENTS_UNUSED.message(extra),
@@ -32,7 +32,7 @@ export function richMissing(
32
32
  return [
33
33
  {
34
34
  severity: "warn",
35
- method,
35
+ origin: method,
36
36
  messageKey: keyPath,
37
37
  code: DIAGNOSTIC_MESSAGES.RICH_MISSING.code,
38
38
  message: DIAGNOSTIC_MESSAGES.RICH_MISSING.message(missing),
@@ -27,7 +27,7 @@ export function richNotAllowed(
27
27
  return [
28
28
  {
29
29
  severity: "warn",
30
- method,
30
+ origin: method,
31
31
  messageKey: keyPath,
32
32
  code: DIAGNOSTIC_MESSAGES.RICH_NOT_ALLOWED.code,
33
33
  message: DIAGNOSTIC_MESSAGES.RICH_NOT_ALLOWED.message(),
@@ -32,7 +32,7 @@ export function richUnused(
32
32
  return [
33
33
  {
34
34
  severity: "warn",
35
- method,
35
+ origin: method,
36
36
  messageKey: keyPath,
37
37
  code: DIAGNOSTIC_MESSAGES.RICH_UNUSED.code,
38
38
  message: DIAGNOSTIC_MESSAGES.RICH_UNUSED.message(extra),
@@ -1,9 +1,6 @@
1
- import type { TranslatorFactory, TranslatorMethod } from "../../../core";
2
-
3
1
  export interface Diagnostic {
4
2
  severity: "error" | "warn";
5
- factory?: TranslatorFactory;
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
- factory?: TranslatorFactory;
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 { factory, method, messageKey, problems, file, lines } = group;
16
+ const { origin, messageKey, problems, file, lines } = group;
17
17
 
18
- const header = `${messageKey} (${method ?? factory})`;
18
+ const header = `${messageKey} (${origin})`;
19
19
 
20
20
  print(header, 1);
21
21
  printList(