intor-cli 0.0.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.
Files changed (92) hide show
  1. package/README.md +46 -0
  2. package/package.json +73 -0
  3. package/src/build/build-schemas/build-schemas.ts +13 -0
  4. package/src/build/build-schemas/index.ts +1 -0
  5. package/src/build/build-types/build-types.ts +46 -0
  6. package/src/build/build-types/index.ts +1 -0
  7. package/src/build/build-types/output/append-config-block.ts +34 -0
  8. package/src/build/build-types/output/append-footer.ts +5 -0
  9. package/src/build/build-types/output/append-header.ts +11 -0
  10. package/src/build/build-types/output/index.ts +3 -0
  11. package/src/build/build-types/utils/indent.ts +3 -0
  12. package/src/build/build-types/utils/render-infer-node.ts +29 -0
  13. package/src/build/index.ts +3 -0
  14. package/src/build/types.ts +19 -0
  15. package/src/cli/commands/check.ts +50 -0
  16. package/src/cli/commands/generate.ts +61 -0
  17. package/src/cli/index.ts +24 -0
  18. package/src/core/collect-messages/collect-runtime-messages.ts +76 -0
  19. package/src/core/collect-messages/index.ts +1 -0
  20. package/src/core/collect-messages/readers.ts +23 -0
  21. package/src/core/collect-messages/resolve-messages-reader.ts +57 -0
  22. package/src/core/constants/extra-exts.ts +2 -0
  23. package/src/core/constants/generated-files.ts +3 -0
  24. package/src/core/constants/index.ts +7 -0
  25. package/src/core/diagnostics/collect.ts +59 -0
  26. package/src/core/diagnostics/group.ts +41 -0
  27. package/src/core/diagnostics/index.ts +2 -0
  28. package/src/core/diagnostics/messages.ts +51 -0
  29. package/src/core/diagnostics/rules/enforce-missing-replacements.ts +55 -0
  30. package/src/core/diagnostics/rules/enforce-missing-rich.ts +55 -0
  31. package/src/core/diagnostics/rules/key/empty.ts +34 -0
  32. package/src/core/diagnostics/rules/key/index.ts +2 -0
  33. package/src/core/diagnostics/rules/key/not-found.ts +43 -0
  34. package/src/core/diagnostics/rules/replacement/index.ts +3 -0
  35. package/src/core/diagnostics/rules/replacement/missing.ts +48 -0
  36. package/src/core/diagnostics/rules/replacement/not-allowed.ts +43 -0
  37. package/src/core/diagnostics/rules/replacement/unused.ts +48 -0
  38. package/src/core/diagnostics/rules/rich/index.ts +3 -0
  39. package/src/core/diagnostics/rules/rich/missing.ts +48 -0
  40. package/src/core/diagnostics/rules/rich/not-allowed.ts +43 -0
  41. package/src/core/diagnostics/rules/rich/unused.ts +48 -0
  42. package/src/core/diagnostics/types.ts +21 -0
  43. package/src/core/diagnostics/utils/get-schema-node-at-path.ts +29 -0
  44. package/src/core/diagnostics/utils/index-usages-by-key.ts +28 -0
  45. package/src/core/diagnostics/utils/resolve-key-path.ts +5 -0
  46. package/src/core/discover-configs/discover-configs.ts +87 -0
  47. package/src/core/discover-configs/index.ts +1 -0
  48. package/src/core/discover-configs/is-intor-resolved-config.ts +15 -0
  49. package/src/core/extract-usages/README.md +84 -0
  50. package/src/core/extract-usages/collectors/collect-key-usages.ts +40 -0
  51. package/src/core/extract-usages/collectors/collect-pre-keys.ts +58 -0
  52. package/src/core/extract-usages/collectors/collect-replacement-usages.ts +68 -0
  53. package/src/core/extract-usages/collectors/collect-rich-usages.ts +57 -0
  54. package/src/core/extract-usages/collectors/collect-translator-bindings.ts +56 -0
  55. package/src/core/extract-usages/collectors/index.ts +5 -0
  56. package/src/core/extract-usages/collectors/utils/extract-static-object-keys.ts +31 -0
  57. package/src/core/extract-usages/collectors/utils/get-config-key.ts +15 -0
  58. package/src/core/extract-usages/collectors/utils/get-object-arg.ts +42 -0
  59. package/src/core/extract-usages/collectors/utils/is-static-string-literal.ts +26 -0
  60. package/src/core/extract-usages/collectors/utils/walk-translator-bindings.ts +47 -0
  61. package/src/core/extract-usages/collectors/utils/walk-translator-method-calls.ts +42 -0
  62. package/src/core/extract-usages/extract-usages.ts +88 -0
  63. package/src/core/extract-usages/index.ts +13 -0
  64. package/src/core/extract-usages/load-source-files-from-tscofnig.ts +76 -0
  65. package/src/core/extract-usages/translator-registry.ts +20 -0
  66. package/src/core/extract-usages/types.ts +53 -0
  67. package/src/core/index.ts +13 -0
  68. package/src/core/infer-schema/index.ts +3 -0
  69. package/src/core/infer-schema/infer-schemas.ts +16 -0
  70. package/src/core/infer-schema/messages/index.ts +1 -0
  71. package/src/core/infer-schema/messages/infer-messages-schema.ts +64 -0
  72. package/src/core/infer-schema/replacements/extract-interpolation-names.ts +70 -0
  73. package/src/core/infer-schema/replacements/index.ts +1 -0
  74. package/src/core/infer-schema/replacements/infer-replacements-schema.ts +63 -0
  75. package/src/core/infer-schema/rich/index.ts +1 -0
  76. package/src/core/infer-schema/rich/infer-rich-schema.ts +66 -0
  77. package/src/core/infer-schema/types.ts +42 -0
  78. package/src/core/infer-schema/utils/infer-object.ts +32 -0
  79. package/src/core/infer-schema/utils/is-message-object.ts +5 -0
  80. package/src/core/read-generated-schema.ts +42 -0
  81. package/src/core/scan-logger.ts +10 -0
  82. package/src/core/write-generated-files.ts +51 -0
  83. package/src/features/check/check.ts +75 -0
  84. package/src/features/check/index.ts +1 -0
  85. package/src/features/check/print-summary.ts +28 -0
  86. package/src/features/generate/generate.ts +76 -0
  87. package/src/features/generate/index.ts +1 -0
  88. package/src/features/generate/print-configs.ts +8 -0
  89. package/src/features/generate/print-summary.ts +19 -0
  90. package/src/features/index.ts +2 -0
  91. package/src/features/print-title.ts +7 -0
  92. package/src/features/spinner.ts +3 -0
@@ -0,0 +1,29 @@
1
+ import type { InferNode } from "../../infer-schema";
2
+
3
+ /**
4
+ * Resolve a dot-separated key path to a schema node.
5
+ *
6
+ * Returns:
7
+ * - InferNode if the path exists
8
+ * - null if any segment is missing or non-object
9
+ */
10
+ export function getSchemaNodeAtPath(
11
+ schema: InferNode,
12
+ path: string,
13
+ ): InferNode | null {
14
+ if (schema.kind !== "object") return null;
15
+
16
+ const segments = path.split(".");
17
+ let node: InferNode = schema;
18
+
19
+ for (const segment of segments) {
20
+ if (node.kind !== "object") return null;
21
+
22
+ const next: InferNode | undefined = node.properties[segment];
23
+ if (!next) return null;
24
+
25
+ node = next;
26
+ }
27
+
28
+ return node;
29
+ }
@@ -0,0 +1,28 @@
1
+ import type { TranslatorMethod } from "../../extract-usages";
2
+ import { resolveKeyPath } from "./resolve-key-path";
3
+
4
+ /**
5
+ * Index extracted usages by their resolved message key.
6
+ */
7
+ export function indexUsagesByKey<
8
+ T extends { key: string; preKey?: string; method: TranslatorMethod },
9
+ >(usages: T[]): Map<string, T[]> {
10
+ const map = new Map<string, T[]>();
11
+
12
+ for (const usage of usages) {
13
+ const keyPath = resolveKeyPath(usage.key, usage.preKey);
14
+ const indexKey = `${usage.method}::${keyPath}`;
15
+
16
+ // Append this usage to the existing group for the same key
17
+ const list = map.get(indexKey);
18
+
19
+ if (list) {
20
+ list.push(usage);
21
+ } else {
22
+ // First usage encountered for this message key
23
+ map.set(indexKey, [usage]);
24
+ }
25
+ }
26
+
27
+ return map;
28
+ }
@@ -0,0 +1,5 @@
1
+ export function resolveKeyPath(key: string, preKey?: string): string {
2
+ if (!preKey) return key;
3
+ if (!key) return preKey;
4
+ return `${preKey}.${key}`;
5
+ }
@@ -0,0 +1,87 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ import { type IntorResolvedConfig } from "intor";
5
+ import { createLogger } from "../scan-logger";
6
+ import { isIntorResolvedConfig } from "./is-intor-resolved-config";
7
+
8
+ const DEFAULT_PATTERNS = ["**/*.{ts,js}"];
9
+ const DEFAULT_IGNORE = [
10
+ "**/node_modules/**",
11
+ "**/dist/**",
12
+ "**/*.d.ts",
13
+ "**/*.test.*",
14
+ "**/*.test-d.ts",
15
+ ];
16
+
17
+ export interface ConfigEntry {
18
+ filePath: string;
19
+ config: IntorResolvedConfig;
20
+ }
21
+
22
+ /**
23
+ * Discover and resolve Intor configs from the current workspace.
24
+ */
25
+ export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
26
+ const files = await fg(DEFAULT_PATTERNS, { ignore: DEFAULT_IGNORE });
27
+ const log = createLogger(debug);
28
+ log("scan", `found ${files.length} candidate files`);
29
+
30
+ const configEntries: ConfigEntry[] = [];
31
+
32
+ // Iterate through candidate files
33
+ for (const file of files) {
34
+ const absPath = path.resolve(process.cwd(), file);
35
+ const relPath = path.relative(process.cwd(), absPath);
36
+
37
+ // ----------------------------------------------------------------------
38
+ // Read file content
39
+ // ----------------------------------------------------------------------
40
+ let content: string;
41
+ try {
42
+ content = await fs.promises.readFile(absPath, "utf8");
43
+ } catch {
44
+ log("warn", `failed to read ${relPath}`);
45
+ continue;
46
+ }
47
+
48
+ // ----------------------------------------------------------------------
49
+ // Skip files that clearly do not define an Intor config
50
+ // ----------------------------------------------------------------------
51
+ if (!content.includes("defineIntorConfig(")) {
52
+ log("skip", `${relPath} (no defineIntorConfig)`);
53
+ continue;
54
+ }
55
+ log("load", relPath);
56
+
57
+ // ----------------------------------------------------------------------
58
+ // Dynamic import & export inspection
59
+ // ----------------------------------------------------------------------
60
+ try {
61
+ const moduleExports = await import(absPath);
62
+ let matched = false;
63
+
64
+ for (const module of Object.values(moduleExports)) {
65
+ const config = module as IntorResolvedConfig;
66
+ if (!isIntorResolvedConfig(config)) continue;
67
+
68
+ matched = true;
69
+
70
+ configEntries.push({ filePath: absPath, config });
71
+ log(" ok ", `resolved config "${config.id}"`);
72
+ }
73
+
74
+ if (!matched) {
75
+ log("warn", `no valid Intor config export found in ${relPath}`);
76
+ }
77
+ } catch {
78
+ log("warn", `failed to import ${relPath}`);
79
+ }
80
+ }
81
+
82
+ if (configEntries.length === 0) {
83
+ log("info", "no Intor config discovered");
84
+ }
85
+
86
+ return configEntries;
87
+ }
@@ -0,0 +1 @@
1
+ export { discoverConfigs, type ConfigEntry } from "./discover-configs";
@@ -0,0 +1,15 @@
1
+ import type { IntorResolvedConfig } from "intor";
2
+
3
+ export function isIntorResolvedConfig(
4
+ value: unknown,
5
+ ): value is IntorResolvedConfig {
6
+ if (!value || typeof value !== "object") return false;
7
+
8
+ const config = value as IntorResolvedConfig;
9
+
10
+ return (
11
+ typeof config.id === "string" &&
12
+ typeof config.defaultLocale === "string" &&
13
+ Array.isArray(config.supportedLocales)
14
+ );
15
+ }
@@ -0,0 +1,84 @@
1
+ ## Extract Targets
2
+
3
+ #### • Translator Usages (collectTranslatorUsages)
4
+
5
+ Extracts translator method bindings only from destructured calls of registered translator factories.
6
+
7
+ ```ts
8
+ // ✅ Supported
9
+ const { t, hasKey } = useTranslator();
10
+ t("home.title");
11
+ hasKey("home.title");
12
+
13
+ // ❌ Not supported
14
+ const translator = useTranslator();
15
+ translator.t("home.title");
16
+ ```
17
+
18
+ #### • Key Usages (collectKeyUsages)
19
+
20
+ Extracts static string literal keys from the first argument of registered translator method calls.
21
+
22
+ ```ts
23
+ // ✅ Supported
24
+ t("title");
25
+ t(`title`);
26
+
27
+ // ❌ Not supported
28
+ t(key);
29
+ t(getTitle());
30
+ t(`title.${key}`);
31
+ ```
32
+
33
+ #### • Replacement Usages (collectReplacementUsages)
34
+
35
+ Extracts static replacement keys based on method semantics.
36
+
37
+ - For `t`: from the last object-literal argument.
38
+ - For `tRich`: from the object-literal argument following rich tag definitions.
39
+
40
+ ```ts
41
+ // ✅ Supported
42
+ t("title", { name: "John", count: 3 });
43
+ t("title", options, { name: "John" });
44
+
45
+ // ❌ Not supported
46
+ t("title");
47
+ t("title", replacements);
48
+ t("title", { name: getName() });
49
+ tRich("title", { link: () => null });
50
+ ```
51
+
52
+ #### • Rich Usages (collectRichUsages)
53
+
54
+ Extracts static rich tag names from `tRich` calls only.
55
+
56
+ ```ts
57
+ // ✅ Supported
58
+ tRich("title", {
59
+ link: () => <a>Link</a>,
60
+ strong: () => <strong>Text</strong>,
61
+ });
62
+
63
+ // ❌ Not supported
64
+ tRich("title", richTags);
65
+ ```
66
+
67
+ #### • PreKeys (collectPreKeys)
68
+
69
+ 1. If the first argument is a static string literal, use it as preKey.
70
+ 2. Otherwise, if the last argument is an object literal containing a static preKey, use it.
71
+ 3. Ignore all other cases.
72
+
73
+ ```ts
74
+ // ✅ Supported (positional)
75
+ const { t, hasKey } = useTranslator("home");
76
+
77
+ // ✅ Supported (options object)
78
+ const { tRich } = getTranslator(_, _, { preKey: "dashboard" });
79
+
80
+ // ❌ Not supported
81
+ const { t } = useTranslator(prefix);
82
+ const { t } = getTranslator({ preKey: dynamic });
83
+ const translator = useTranslator("home"); // non-destructured binding
84
+ ```
@@ -0,0 +1,40 @@
1
+ import type { KeyUsage, TranslatorBindingMap } from "../types";
2
+ import type { SourceFile } from "ts-morph";
3
+ import { isStaticStringLiteral } from "./utils/is-static-string-literal";
4
+ import { walkTranslatorMethodCalls } from "./utils/walk-translator-method-calls";
5
+
6
+ /**
7
+ * Collect static translation key usages from translator method calls
8
+ * within a single source file.
9
+ */
10
+ export function collectKeyUsages(
11
+ sourceFile: SourceFile,
12
+ translatorBindingMap: TranslatorBindingMap,
13
+ ): KeyUsage[] {
14
+ const keyUsages: KeyUsage[] = [];
15
+
16
+ walkTranslatorMethodCalls(
17
+ sourceFile,
18
+ translatorBindingMap,
19
+ ({ sourceFile, translatorUsage, call, localName }) => {
20
+ const firstArg = call.getArguments()[0];
21
+ if (!isStaticStringLiteral(firstArg)) return;
22
+
23
+ // Resolve source location for diagnostics
24
+ const pos = sourceFile.getLineAndColumnAtPos(firstArg.getStart());
25
+
26
+ keyUsages.push({
27
+ configKey: translatorUsage.configKey,
28
+ factory: translatorUsage.factory,
29
+ method: translatorUsage.method,
30
+ localName,
31
+ key: firstArg.getLiteralText(),
32
+ file: sourceFile.getFilePath(),
33
+ line: pos.line,
34
+ column: pos.column,
35
+ });
36
+ },
37
+ );
38
+
39
+ return keyUsages;
40
+ }
@@ -0,0 +1,58 @@
1
+ import type { PreKeyMap, TranslatorBindingMap } from "../types";
2
+ import { SyntaxKind, type SourceFile } from "ts-morph";
3
+ import { getObjectArg } from "./utils/get-object-arg";
4
+ import { isStaticStringLiteral } from "./utils/is-static-string-literal";
5
+ import { walkTranslatorBindings } from "./utils/walk-translator-bindings";
6
+
7
+ const PREKEY_PROPERTY_NAME = "preKey";
8
+
9
+ /**
10
+ * Collect static preKey values associated with translator bindings
11
+ * within a single source file.
12
+ */
13
+ export function collectPreKeys(
14
+ sourceFile: SourceFile,
15
+ translatorBindingMap: TranslatorBindingMap,
16
+ ): PreKeyMap {
17
+ const preKeyMap = new Map<string, string>();
18
+
19
+ walkTranslatorBindings(sourceFile, ({ call, binding }) => {
20
+ // Iterate over destructured translator binding elements (e.g. { t, hasKey, ... })
21
+ for (const el of binding.getElements()) {
22
+ const localName = el.getNameNode().getText(); // `t` from `const { t } = useTranslator`
23
+ if (!translatorBindingMap.has(localName)) continue;
24
+
25
+ // -----------------------------------------------------------------------
26
+ // Resolve static preKey from translator factory arguments
27
+ // -----------------------------------------------------------------------
28
+ // 1. From the first positional string argument (e.g. `useTranslator("preKey")`)
29
+ const firstArg = call.getArguments()[0];
30
+ if (isStaticStringLiteral(firstArg)) {
31
+ preKeyMap.set(localName, firstArg.getLiteralText());
32
+ continue;
33
+ }
34
+
35
+ // 2. From the last options object (e.g. `getTranslator(_, { preKey })`)
36
+ const lastArg = getObjectArg(call, "last");
37
+ if (!lastArg) continue;
38
+
39
+ // Extract the `preKey` property from the options object
40
+ const prop = lastArg.getProperty(PREKEY_PROPERTY_NAME);
41
+ if (!prop || !prop.isKind(SyntaxKind.PropertyAssignment)) continue;
42
+
43
+ // Only accept static string initializers as preKey values
44
+ const value = prop.getInitializer();
45
+ if (!value) continue;
46
+ if (
47
+ !value.isKind(SyntaxKind.StringLiteral) &&
48
+ !value.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
49
+ ) {
50
+ continue;
51
+ }
52
+
53
+ preKeyMap.set(localName, value.getLiteralText());
54
+ }
55
+ });
56
+
57
+ return preKeyMap;
58
+ }
@@ -0,0 +1,68 @@
1
+ import type { ReplacementUsage, TranslatorBindingMap } from "../types";
2
+ import type { ObjectLiteralExpression, SourceFile } from "ts-morph";
3
+ import { TRANSLATOR_METHOD } from "../translator-registry";
4
+ import { extractStaticObjectKeys } from "./utils/extract-static-object-keys";
5
+ import { getObjectArg } from "./utils/get-object-arg";
6
+ import { isStaticStringLiteral } from "./utils/is-static-string-literal";
7
+ import { walkTranslatorMethodCalls } from "./utils/walk-translator-method-calls";
8
+
9
+ /**
10
+ * Collect static replacement usages from translator method calls
11
+ * within a single source file.
12
+ */
13
+ export function collectReplacementUsages(
14
+ sourceFile: SourceFile,
15
+ translatorBindingMap: TranslatorBindingMap,
16
+ ): ReplacementUsage[] {
17
+ const replacementUsages: ReplacementUsage[] = [];
18
+
19
+ walkTranslatorMethodCalls(
20
+ sourceFile,
21
+ translatorBindingMap,
22
+ ({ sourceFile, translatorUsage, call, localName }) => {
23
+ if (translatorUsage.method !== "t" && translatorUsage.method !== "tRich")
24
+ return;
25
+
26
+ const firstArg = call.getArguments()[0];
27
+ if (!isStaticStringLiteral(firstArg)) return;
28
+
29
+ // ----------------------------------------------------------------------
30
+ // Resolve replacements based on method semantics
31
+ // ----------------------------------------------------------------------
32
+ let replacementArg: ObjectLiteralExpression | null = null;
33
+
34
+ if (translatorUsage.method === TRANSLATOR_METHOD.t) {
35
+ replacementArg = getObjectArg(call, 2);
36
+ }
37
+
38
+ if (translatorUsage.method === TRANSLATOR_METHOD.tRich) {
39
+ replacementArg = getObjectArg(call, 3);
40
+ }
41
+
42
+ if (!replacementArg) return;
43
+
44
+ // ----------------------------------------------------------------------
45
+ // Extract static replacement keys from the replacements object
46
+ // ----------------------------------------------------------------------
47
+ const keys: string[] = extractStaticObjectKeys(replacementArg);
48
+ if (keys.length === 0) return;
49
+
50
+ // Resolve source location for diagnostics
51
+ const pos = sourceFile.getLineAndColumnAtPos(replacementArg.getStart());
52
+
53
+ replacementUsages.push({
54
+ configKey: translatorUsage.configKey,
55
+ factory: translatorUsage.factory,
56
+ method: translatorUsage.method,
57
+ localName,
58
+ key: firstArg.getLiteralText(),
59
+ replacements: keys,
60
+ file: sourceFile.getFilePath(),
61
+ line: pos.line,
62
+ column: pos.column,
63
+ });
64
+ },
65
+ );
66
+
67
+ return replacementUsages;
68
+ }
@@ -0,0 +1,57 @@
1
+ import type { RichUsage, TranslatorBindingMap } from "../types";
2
+ import type { SourceFile } from "ts-morph";
3
+ import { extractStaticObjectKeys } from "./utils/extract-static-object-keys";
4
+ import { getObjectArg } from "./utils/get-object-arg";
5
+ import { isStaticStringLiteral } from "./utils/is-static-string-literal";
6
+ import { walkTranslatorMethodCalls } from "./utils/walk-translator-method-calls";
7
+
8
+ /**
9
+ * Collect static rich tag usages from translator method calls
10
+ * within a single source file.
11
+ */
12
+ export function collectRichUsages(
13
+ sourceFile: SourceFile,
14
+ translatorBindingMap: TranslatorBindingMap,
15
+ ): RichUsage[] {
16
+ const richUsages: RichUsage[] = [];
17
+
18
+ walkTranslatorMethodCalls(
19
+ sourceFile,
20
+ translatorBindingMap,
21
+ ({ sourceFile, translatorUsage, call, localName }) => {
22
+ if (translatorUsage.method !== "tRich") return;
23
+
24
+ const firstArg = call.getArguments()[0];
25
+ if (!isStaticStringLiteral(firstArg)) return;
26
+
27
+ // ----------------------------------------------------------------------
28
+ // Resolve rich tags from the first object-literal argument after the key
29
+ // ----------------------------------------------------------------------
30
+ const richArg = getObjectArg(call, 2);
31
+ if (!richArg) return;
32
+
33
+ // ----------------------------------------------------------------------
34
+ // Extract static rich tag definitions from the rich object argument
35
+ // ----------------------------------------------------------------------
36
+ const keys: string[] = extractStaticObjectKeys(richArg);
37
+ if (keys.length === 0) return;
38
+
39
+ // Resolve source location for diagnostics
40
+ const pos = sourceFile.getLineAndColumnAtPos(richArg.getStart());
41
+
42
+ richUsages.push({
43
+ configKey: translatorUsage.configKey,
44
+ factory: translatorUsage.factory,
45
+ method: translatorUsage.method,
46
+ localName,
47
+ key: firstArg.getLiteralText(),
48
+ rich: keys,
49
+ file: sourceFile.getFilePath(),
50
+ line: pos.line,
51
+ column: pos.column,
52
+ });
53
+ },
54
+ );
55
+
56
+ return richUsages;
57
+ }
@@ -0,0 +1,56 @@
1
+ import type { TranslatorBinding, TranslatorBindingMap } from "../types";
2
+ import { SyntaxKind, type SourceFile } from "ts-morph";
3
+ import { getConfigKey } from "../../../core/extract-usages/collectors/utils/get-config-key";
4
+ import {
5
+ TRANSLATOR_FACTORIES,
6
+ TRANSLATOR_METHODS,
7
+ type TranslatorFactory,
8
+ type TranslatorMethod,
9
+ } from "../translator-registry";
10
+ import { walkTranslatorBindings } from "./utils/walk-translator-bindings";
11
+
12
+ export const TRANSLATOR_FACTORIES_SET = new Set<TranslatorFactory>(
13
+ TRANSLATOR_FACTORIES,
14
+ );
15
+ export const TRANSLATOR_METHODS_SET = new Set<TranslatorMethod>(
16
+ TRANSLATOR_METHODS,
17
+ );
18
+
19
+ /**
20
+ * Collect static translator bindings from supported translator factories
21
+ * within a single source file.
22
+ */
23
+ export function collectTranslatorBindings(
24
+ sourceFile: SourceFile,
25
+ ): TranslatorBindingMap {
26
+ const translatorBindingMap = new Map<string, TranslatorBinding>();
27
+
28
+ walkTranslatorBindings(sourceFile, ({ call, binding }) => {
29
+ // ----------------------------------------------------------------------
30
+ // Resolve the translator factory name and ensure it is supported
31
+ // ----------------------------------------------------------------------
32
+ const expr = call.getExpression();
33
+ if (!expr.isKind(SyntaxKind.Identifier)) return;
34
+ const factoryName = expr.getText() as TranslatorFactory;
35
+ if (!TRANSLATOR_FACTORIES_SET.has(factoryName)) return;
36
+
37
+ // Iterate over destructured translator binding elements (e.g. { t, hasKey, ... })
38
+ for (const el of binding.getElements()) {
39
+ // ----------------------------------------------------------------------
40
+ // Resolve the translator method name from the destructured binding
41
+ // ----------------------------------------------------------------------
42
+ const localName = el.getNameNode().getText(); // `t` from `const { t } = useTranslator`
43
+ const aliasName = el.getPropertyNameNode()?.getText(); // `translate` from `const { t: translate } = useTranslator`
44
+ const methodName = (aliasName ?? localName) as TranslatorMethod; // aliasName > originalName
45
+ if (!TRANSLATOR_METHODS_SET.has(methodName)) continue;
46
+
47
+ translatorBindingMap.set(localName, {
48
+ factory: factoryName,
49
+ method: methodName,
50
+ configKey: getConfigKey(call),
51
+ });
52
+ }
53
+ });
54
+
55
+ return translatorBindingMap;
56
+ }
@@ -0,0 +1,5 @@
1
+ export { collectTranslatorBindings } from "./collect-translator-bindings";
2
+ export { collectKeyUsages } from "./collect-key-usages";
3
+ export { collectReplacementUsages } from "./collect-replacement-usages";
4
+ export { collectRichUsages } from "./collect-rich-usages";
5
+ export { collectPreKeys } from "./collect-pre-keys";
@@ -0,0 +1,31 @@
1
+ import { SyntaxKind, type ObjectLiteralExpression } from "ts-morph";
2
+
3
+ /**
4
+ * Extract static property keys from an object literal expression.
5
+ */
6
+ export function extractStaticObjectKeys(
7
+ obj: ObjectLiteralExpression,
8
+ ): string[] {
9
+ const keys: string[] = [];
10
+
11
+ // Walk through object literal properties
12
+ for (const prop of obj.getProperties()) {
13
+ // Only support simple property assignments (skip spreads, methods, etc.)
14
+ if (!prop.isKind(SyntaxKind.PropertyAssignment)) continue;
15
+
16
+ const nameNode = prop.getNameNode();
17
+
18
+ // { foo: ... }
19
+ if (nameNode.isKind(SyntaxKind.Identifier)) {
20
+ keys.push(nameNode.getText());
21
+ continue;
22
+ }
23
+
24
+ // { "foo": ... }
25
+ if (nameNode.isKind(SyntaxKind.StringLiteral)) {
26
+ keys.push(nameNode.getLiteralText());
27
+ }
28
+ }
29
+
30
+ return keys;
31
+ }
@@ -0,0 +1,15 @@
1
+ import { SyntaxKind, type CallExpression } from "ts-morph";
2
+
3
+ export function getConfigKey(call: CallExpression): string | undefined {
4
+ const typeArgs = call.getTypeArguments();
5
+ if (typeArgs.length === 0) return undefined;
6
+
7
+ const firstArg = typeArgs[0];
8
+
9
+ if (!firstArg.isKind(SyntaxKind.LiteralType)) return undefined;
10
+
11
+ const literal = firstArg.getLiteral();
12
+ if (!literal || !literal.isKind(SyntaxKind.StringLiteral)) return undefined;
13
+
14
+ return literal.getLiteralText();
15
+ }
@@ -0,0 +1,42 @@
1
+ import type {
2
+ CallExpression,
3
+ Node,
4
+ ObjectLiteralExpression,
5
+ ts,
6
+ } from "ts-morph";
7
+ import { SyntaxKind } from "ts-morph";
8
+
9
+ /**
10
+ * position:
11
+ * - "first" | "last"
12
+ * - number: 1-based argument position (2 = second argument)
13
+ */
14
+ type ObjectArgPosition = "first" | "last" | number;
15
+
16
+ export function getObjectArg(
17
+ node: CallExpression,
18
+ position: ObjectArgPosition,
19
+ ): ObjectLiteralExpression | null {
20
+ const args = node.getArguments();
21
+
22
+ let target: Node<ts.Node> | undefined | null = null;
23
+
24
+ switch (position) {
25
+ case "first": {
26
+ target = args[0];
27
+ break;
28
+ }
29
+
30
+ case "last": {
31
+ target = args.at(-1);
32
+ break;
33
+ }
34
+
35
+ default: {
36
+ target = args[position - 1];
37
+ }
38
+ }
39
+
40
+ if (!target?.isKind(SyntaxKind.ObjectLiteralExpression)) return null;
41
+ return target;
42
+ }
@@ -0,0 +1,26 @@
1
+ import type {
2
+ Node,
3
+ NoSubstitutionTemplateLiteral,
4
+ StringLiteral,
5
+ ts,
6
+ } from "ts-morph";
7
+ import { SyntaxKind } from "ts-morph";
8
+
9
+ /**
10
+ * Check whether a node is a static string literal.
11
+ *
12
+ * Supports:
13
+ * - "text"
14
+ * - 'text'
15
+ * - \`text\` (no substitutions)
16
+ */
17
+ export function isStaticStringLiteral(
18
+ node: Node<ts.Node> | undefined,
19
+ ): node is StringLiteral | NoSubstitutionTemplateLiteral {
20
+ if (!node) return false;
21
+
22
+ return (
23
+ node.isKind(SyntaxKind.StringLiteral) ||
24
+ node.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
25
+ );
26
+ }