@gtkx/cli 1.3.0 → 1.5.0
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 +5 -4
- package/dist/builder.d.ts +1 -1
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +6 -6
- package/dist/builder.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +3 -2
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +41 -12
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/commands/build.d.ts +0 -4
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +0 -5
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/codegen.d.ts.map +1 -1
- package/dist/commands/codegen.js +2 -4
- package/dist/commands/codegen.js.map +1 -1
- package/dist/deploy/depends.d.ts +2 -2
- package/dist/deploy/depends.d.ts.map +1 -1
- package/dist/deploy/depends.js +30 -15
- package/dist/deploy/depends.js.map +1 -1
- package/dist/deploy/freedesktop/localize.d.ts +6 -0
- package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
- package/dist/deploy/freedesktop/localize.js +107 -0
- package/dist/deploy/freedesktop/localize.js.map +1 -0
- package/dist/deploy/install-hints.d.ts.map +1 -1
- package/dist/deploy/install-hints.js +5 -0
- package/dist/deploy/install-hints.js.map +1 -1
- package/dist/deploy/nfpm/config.js +1 -1
- package/dist/deploy/nfpm/config.js.map +1 -1
- package/dist/deploy/node-runtime/elf.d.ts +1 -1
- package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
- package/dist/deploy/node-runtime/elf.js +2 -2
- package/dist/deploy/node-runtime/elf.js.map +1 -1
- package/dist/deploy/node-runtime/index.js +2 -2
- package/dist/deploy/node-runtime/index.js.map +1 -1
- package/dist/deploy/notices/collect.d.ts +3 -1
- package/dist/deploy/notices/collect.d.ts.map +1 -1
- package/dist/deploy/notices/collect.js +2 -2
- package/dist/deploy/notices/collect.js.map +1 -1
- package/dist/deploy/notices/packages.d.ts +2 -1
- package/dist/deploy/notices/packages.d.ts.map +1 -1
- package/dist/deploy/notices/packages.js +4 -27
- package/dist/deploy/notices/packages.js.map +1 -1
- package/dist/deploy/payload/icons.d.ts +7 -3
- package/dist/deploy/payload/icons.d.ts.map +1 -1
- package/dist/deploy/payload/icons.js +46 -28
- package/dist/deploy/payload/icons.js.map +1 -1
- package/dist/deploy/payload/launcher.d.ts.map +1 -1
- package/dist/deploy/payload/launcher.js +2 -0
- package/dist/deploy/payload/launcher.js.map +1 -1
- package/dist/deploy/payload/stage.d.ts +1 -1
- package/dist/deploy/payload/stage.d.ts.map +1 -1
- package/dist/deploy/payload/stage.js +23 -13
- package/dist/deploy/payload/stage.js.map +1 -1
- package/dist/deploy/run-deploy.d.ts.map +1 -1
- package/dist/deploy/run-deploy.js +149 -25
- package/dist/deploy/run-deploy.js.map +1 -1
- package/dist/deploy/settings/build-manifest.d.ts +9 -0
- package/dist/deploy/settings/build-manifest.d.ts.map +1 -0
- package/dist/deploy/settings/build-manifest.js +80 -0
- package/dist/deploy/settings/build-manifest.js.map +1 -0
- package/dist/deploy/settings/index.d.ts.map +1 -1
- package/dist/deploy/settings/index.js +9 -3
- package/dist/deploy/settings/index.js.map +1 -1
- package/dist/deploy/settings/libraries.d.ts +8 -0
- package/dist/deploy/settings/libraries.d.ts.map +1 -0
- package/dist/deploy/settings/libraries.js +39 -0
- package/dist/deploy/settings/libraries.js.map +1 -0
- package/dist/deploy/settings/paths.d.ts +3 -0
- package/dist/deploy/settings/paths.d.ts.map +1 -1
- package/dist/deploy/settings/paths.js +6 -32
- package/dist/deploy/settings/paths.js.map +1 -1
- package/dist/deploy/targets/appimage.d.ts.map +1 -1
- package/dist/deploy/targets/appimage.js +19 -13
- package/dist/deploy/targets/appimage.js.map +1 -1
- package/dist/deploy/targets/flatpak-manifest.d.ts +2 -1
- package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
- package/dist/deploy/targets/flatpak-manifest.js +5 -1
- package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
- package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
- package/dist/deploy/targets/flatpak-source.js +30 -16
- package/dist/deploy/targets/flatpak-source.js.map +1 -1
- package/dist/deploy/tools.d.ts +6 -1
- package/dist/deploy/tools.d.ts.map +1 -1
- package/dist/deploy/tools.js +26 -1
- package/dist/deploy/tools.js.map +1 -1
- package/dist/deploy/types.d.ts +4 -4
- package/dist/deploy/types.d.ts.map +1 -1
- package/dist/deploy/types.js.map +1 -1
- package/dist/dev/icon-dir.d.ts +1 -1
- package/dist/dev/icon-dir.d.ts.map +1 -1
- package/dist/dev/icon-dir.js +20 -7
- package/dist/dev/icon-dir.js.map +1 -1
- package/dist/dev/locale-dir.d.ts +3 -0
- package/dist/dev/locale-dir.d.ts.map +1 -0
- package/dist/dev/locale-dir.js +17 -0
- package/dist/dev/locale-dir.js.map +1 -0
- package/dist/dev/runner-deps.d.ts.map +1 -1
- package/dist/dev/runner-deps.js +5 -1
- package/dist/dev/runner-deps.js.map +1 -1
- package/dist/dev/runner-main.d.ts.map +1 -1
- package/dist/dev/runner-main.js +6 -4
- package/dist/dev/runner-main.js.map +1 -1
- package/dist/dev/runner.d.ts +1 -1
- package/dist/dev/runner.d.ts.map +1 -1
- package/dist/dev/runner.js +7 -1
- package/dist/dev/runner.js.map +1 -1
- package/dist/dev/schema-dir.d.ts +1 -1
- package/dist/dev/schema-dir.d.ts.map +1 -1
- package/dist/dev/schema-dir.js +3 -1
- package/dist/dev/schema-dir.js.map +1 -1
- package/dist/dev/vite-dev-server.d.ts +1 -0
- package/dist/dev/vite-dev-server.d.ts.map +1 -1
- package/dist/dev/vite-dev-server.js.map +1 -1
- package/dist/i18n/binding-extractor.d.ts +30 -0
- package/dist/i18n/binding-extractor.d.ts.map +1 -0
- package/dist/i18n/binding-extractor.js +1324 -0
- package/dist/i18n/binding-extractor.js.map +1 -0
- package/dist/i18n/catalog-template.d.ts +3 -0
- package/dist/i18n/catalog-template.d.ts.map +1 -0
- package/dist/i18n/catalog-template.js +18 -0
- package/dist/i18n/catalog-template.js.map +1 -0
- package/dist/i18n/catalogs.d.ts +21 -0
- package/dist/i18n/catalogs.d.ts.map +1 -0
- package/dist/i18n/catalogs.js +139 -0
- package/dist/i18n/catalogs.js.map +1 -0
- package/dist/i18n/extractor-plugin.d.ts +8 -0
- package/dist/i18n/extractor-plugin.d.ts.map +1 -0
- package/dist/i18n/extractor-plugin.js +247 -0
- package/dist/i18n/extractor-plugin.js.map +1 -0
- package/dist/i18n/metadata-templates.d.ts +11 -0
- package/dist/i18n/metadata-templates.d.ts.map +1 -0
- package/dist/i18n/metadata-templates.js +11 -0
- package/dist/i18n/metadata-templates.js.map +1 -0
- package/dist/i18n/source-messages.d.ts +23 -0
- package/dist/i18n/source-messages.d.ts.map +1 -0
- package/dist/i18n/source-messages.js +403 -0
- package/dist/i18n/source-messages.js.map +1 -0
- package/dist/i18n/types.d.ts +10 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +108 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/internal/build-manifest.d.ts +20 -0
- package/dist/internal/build-manifest.d.ts.map +1 -0
- package/dist/internal/build-manifest.js +6 -0
- package/dist/internal/build-manifest.js.map +1 -0
- package/dist/internal/icon-path.d.ts +13 -0
- package/dist/internal/icon-path.d.ts.map +1 -0
- package/dist/internal/icon-path.js +86 -0
- package/dist/internal/icon-path.js.map +1 -0
- package/dist/internal/source-imports.d.ts +12 -0
- package/dist/internal/source-imports.d.ts.map +1 -0
- package/dist/internal/source-imports.js +123 -0
- package/dist/internal/source-imports.js.map +1 -0
- package/dist/mcp/client.d.ts +0 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +11 -39
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/handlers.d.ts +2 -1
- package/dist/mcp/handlers.d.ts.map +1 -1
- package/dist/mcp/handlers.js.map +1 -1
- package/dist/mcp/widget-properties.d.ts.map +1 -1
- package/dist/mcp/widget-properties.js +3 -12
- package/dist/mcp/widget-properties.js.map +1 -1
- package/dist/settings/render.d.ts +10 -1
- package/dist/settings/render.d.ts.map +1 -1
- package/dist/settings/render.js +78 -20
- package/dist/settings/render.js.map +1 -1
- package/dist/settings/schema.d.ts +4 -3
- package/dist/settings/schema.d.ts.map +1 -1
- package/dist/settings/schema.js +124 -15
- package/dist/settings/schema.js.map +1 -1
- package/dist/vite-plugins/asset-emitter.d.ts +1 -1
- package/dist/vite-plugins/asset-emitter.d.ts.map +1 -1
- package/dist/vite-plugins/asset-emitter.js.map +1 -1
- package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
- package/dist/vite-plugins/asset-extensions.js +1 -0
- package/dist/vite-plugins/asset-extensions.js.map +1 -1
- package/dist/vite-plugins/asset-imports.d.ts +2 -1
- package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
- package/dist/vite-plugins/asset-imports.js +39 -28
- package/dist/vite-plugins/asset-imports.js.map +1 -1
- package/dist/vite-plugins/asset-specifier.d.ts +13 -1
- package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
- package/dist/vite-plugins/asset-specifier.js +39 -1
- package/dist/vite-plugins/asset-specifier.js.map +1 -1
- package/dist/vite-plugins/build-manifest.d.ts +5 -0
- package/dist/vite-plugins/build-manifest.d.ts.map +1 -0
- package/dist/vite-plugins/{bundled-packages.js → build-manifest.js} +20 -14
- package/dist/vite-plugins/build-manifest.js.map +1 -0
- package/dist/vite-plugins/built-url.d.ts +1 -1
- package/dist/vite-plugins/built-url.d.ts.map +1 -1
- package/dist/vite-plugins/built-url.js +37 -24
- package/dist/vite-plugins/built-url.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts +5 -0
- package/dist/vite-plugins/i18n.d.ts.map +1 -0
- package/dist/vite-plugins/i18n.js +88 -0
- package/dist/vite-plugins/i18n.js.map +1 -0
- package/dist/vite-plugins/icons.d.ts +2 -1
- package/dist/vite-plugins/icons.d.ts.map +1 -1
- package/dist/vite-plugins/icons.js +25 -12
- package/dist/vite-plugins/icons.js.map +1 -1
- package/dist/vite-plugins/index.d.ts +2 -1
- package/dist/vite-plugins/index.d.ts.map +1 -1
- package/dist/vite-plugins/index.js +9 -5
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/native.d.ts.map +1 -1
- package/dist/vite-plugins/native.js +23 -4
- package/dist/vite-plugins/native.js.map +1 -1
- package/dist/vite-plugins/resource-init-module.d.ts +7 -3
- package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
- package/dist/vite-plugins/resource-init-module.js +39 -14
- package/dist/vite-plugins/resource-init-module.js.map +1 -1
- package/dist/vite-plugins/resource-shared.d.ts +3 -1
- package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
- package/dist/vite-plugins/resource-shared.js +3 -1
- package/dist/vite-plugins/resource-shared.js.map +1 -1
- package/dist/vite-plugins/resources.d.ts +1 -1
- package/dist/vite-plugins/resources.d.ts.map +1 -1
- package/dist/vite-plugins/resources.js +877 -86
- package/dist/vite-plugins/resources.js.map +1 -1
- package/dist/vite-plugins/self-contained.d.ts.map +1 -1
- package/dist/vite-plugins/self-contained.js +128 -135
- package/dist/vite-plugins/self-contained.js.map +1 -1
- package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
- package/dist/vite-plugins/settings-worker-env.js +6 -3
- package/dist/vite-plugins/settings-worker-env.js.map +1 -1
- package/dist/vite-plugins/settings.d.ts +3 -1
- package/dist/vite-plugins/settings.d.ts.map +1 -1
- package/dist/vite-plugins/settings.js +63 -23
- package/dist/vite-plugins/settings.js.map +1 -1
- package/env.d.ts +11 -192
- package/package.json +11 -10
- package/src/builder.ts +7 -7
- package/src/codegen/run-codegen.ts +64 -15
- package/src/commands/build.ts +0 -5
- package/src/commands/codegen.ts +2 -4
- package/src/deploy/depends.ts +47 -18
- package/src/deploy/freedesktop/localize.ts +144 -0
- package/src/deploy/install-hints.ts +5 -0
- package/src/deploy/nfpm/config.ts +1 -1
- package/src/deploy/node-runtime/elf.ts +3 -3
- package/src/deploy/node-runtime/index.ts +2 -2
- package/src/deploy/notices/collect.ts +4 -2
- package/src/deploy/notices/packages.ts +5 -43
- package/src/deploy/payload/icons.ts +64 -34
- package/src/deploy/payload/launcher.ts +2 -0
- package/src/deploy/payload/stage.ts +40 -12
- package/src/deploy/run-deploy.ts +214 -24
- package/src/deploy/settings/build-manifest.ts +122 -0
- package/src/deploy/settings/index.ts +9 -4
- package/src/deploy/settings/libraries.ts +57 -0
- package/src/deploy/settings/paths.ts +10 -47
- package/src/deploy/targets/appimage.ts +32 -15
- package/src/deploy/targets/flatpak-manifest.ts +7 -1
- package/src/deploy/targets/flatpak-source.ts +42 -20
- package/src/deploy/tools.ts +35 -0
- package/src/deploy/types.ts +4 -4
- package/src/dev/icon-dir.ts +31 -7
- package/src/dev/locale-dir.ts +22 -0
- package/src/dev/runner-deps.ts +5 -1
- package/src/dev/runner-main.ts +6 -4
- package/src/dev/runner.ts +10 -2
- package/src/dev/schema-dir.ts +3 -1
- package/src/dev/vite-dev-server.ts +1 -0
- package/src/i18n/binding-extractor.ts +1969 -0
- package/src/i18n/catalog-template.ts +24 -0
- package/src/i18n/catalogs.ts +216 -0
- package/src/i18n/extractor-plugin.ts +413 -0
- package/src/i18n/metadata-templates.ts +29 -0
- package/src/i18n/source-messages.ts +608 -0
- package/src/i18n/types.ts +154 -0
- package/src/internal/build-manifest.ts +32 -0
- package/src/internal/icon-path.ts +127 -0
- package/src/internal/source-imports.ts +171 -0
- package/src/mcp/client.ts +17 -44
- package/src/mcp/handlers.ts +7 -6
- package/src/mcp/widget-properties.ts +3 -14
- package/src/settings/render.ts +113 -21
- package/src/settings/schema.ts +192 -17
- package/src/vite-plugins/asset-emitter.ts +1 -1
- package/src/vite-plugins/asset-extensions.ts +1 -0
- package/src/vite-plugins/asset-imports.ts +62 -33
- package/src/vite-plugins/asset-specifier.ts +73 -1
- package/src/vite-plugins/{bundled-packages.ts → build-manifest.ts} +30 -19
- package/src/vite-plugins/built-url.ts +49 -28
- package/src/vite-plugins/i18n.ts +146 -0
- package/src/vite-plugins/icons.ts +33 -14
- package/src/vite-plugins/index.ts +15 -5
- package/src/vite-plugins/native.ts +31 -4
- package/src/vite-plugins/resource-init-module.ts +44 -15
- package/src/vite-plugins/resource-shared.ts +4 -0
- package/src/vite-plugins/resources.ts +1397 -100
- package/src/vite-plugins/self-contained.ts +150 -169
- package/src/vite-plugins/settings-worker-env.ts +7 -3
- package/src/vite-plugins/settings.ts +89 -21
- package/dist/vite-plugins/bundled-packages.d.ts +0 -5
- package/dist/vite-plugins/bundled-packages.d.ts.map +0 -1
- package/dist/vite-plugins/bundled-packages.js.map +0 -1
|
@@ -0,0 +1,1969 @@
|
|
|
1
|
+
import { types as babel, type NodePath as BabelNodePath, parseAsync, traverse } from "@babel/core";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
|
|
4
|
+
type ApiName =
|
|
5
|
+
| "IcuTrans" |
|
|
6
|
+
"IcuTransWithoutContext" |
|
|
7
|
+
"Trans" |
|
|
8
|
+
"TransWithoutContext" |
|
|
9
|
+
"Translation" |
|
|
10
|
+
"getFixedT" |
|
|
11
|
+
"getI18n" |
|
|
12
|
+
"init" |
|
|
13
|
+
"t" |
|
|
14
|
+
"useTranslation" |
|
|
15
|
+
"withTranslation";
|
|
16
|
+
|
|
17
|
+
type ScopeInfo = {
|
|
18
|
+
defaultNs?: string;
|
|
19
|
+
keyPrefix?: string;
|
|
20
|
+
namespaces?: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type TSemantic = {
|
|
24
|
+
fixed: boolean;
|
|
25
|
+
info: ScopeInfo;
|
|
26
|
+
kind: "t";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Semantic =
|
|
30
|
+
| TSemantic |
|
|
31
|
+
{ api: ApiName; kind: "api" } |
|
|
32
|
+
{ info: ScopeInfo; kind: "enhancer" } |
|
|
33
|
+
{ kind: "initPromise" } |
|
|
34
|
+
{ kind: "namespace"; module: string } |
|
|
35
|
+
{ kind: "translationContext" } |
|
|
36
|
+
{ instance: boolean; kind: "translationObject"; t: TSemantic };
|
|
37
|
+
|
|
38
|
+
type ExtractedCall = {
|
|
39
|
+
info: ScopeInfo;
|
|
40
|
+
key: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type ExtractedMessage = {
|
|
44
|
+
defaultValue: string;
|
|
45
|
+
explicitDefault: boolean;
|
|
46
|
+
key: string;
|
|
47
|
+
location: {
|
|
48
|
+
column?: number;
|
|
49
|
+
file: string;
|
|
50
|
+
line?: number;
|
|
51
|
+
};
|
|
52
|
+
ns?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type BindingExtraction = {
|
|
56
|
+
blockedGetFixedT: Set<string>;
|
|
57
|
+
calls: Map<string, ExtractedCall>;
|
|
58
|
+
errors: string[];
|
|
59
|
+
messages: ExtractedMessage[];
|
|
60
|
+
trans: Map<string, ScopeInfo | null>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type NodePath<T extends babel.Node = babel.Node> = BabelNodePath<T>;
|
|
64
|
+
type Binding = NonNullable<ReturnType<NodePath["scope"]["getBinding"]>>;
|
|
65
|
+
type ParserPlugin = "decorators-legacy" | "jsx" | "typescript";
|
|
66
|
+
|
|
67
|
+
type ScopeCallInput = {
|
|
68
|
+
file: string;
|
|
69
|
+
namespaceIndex: number;
|
|
70
|
+
optionsIndex?: number;
|
|
71
|
+
path: NodePath;
|
|
72
|
+
prefixIndex?: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type ComponentInput = {
|
|
76
|
+
contextPath?: NodePath | undefined;
|
|
77
|
+
countPath?: NodePath | undefined;
|
|
78
|
+
defaultPath?: NodePath | undefined;
|
|
79
|
+
isIcu: boolean;
|
|
80
|
+
keyPath?: NodePath | undefined;
|
|
81
|
+
namespacePath?: NodePath | undefined;
|
|
82
|
+
optionsPath?: NodePath | undefined;
|
|
83
|
+
path: NodePath;
|
|
84
|
+
tPath?: NodePath | undefined;
|
|
85
|
+
valuesPath?: NodePath | undefined;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type ComponentKeys = {
|
|
89
|
+
fallback?: string | undefined;
|
|
90
|
+
rawKeys: string[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type ComponentScope = {
|
|
94
|
+
keys: string[];
|
|
95
|
+
ns?: string | undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type Resolution<T> = {
|
|
99
|
+
isValid: boolean;
|
|
100
|
+
value?: T | undefined;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type MessageInput = {
|
|
104
|
+
defaultValue: string;
|
|
105
|
+
hasExplicitDefault: boolean;
|
|
106
|
+
key: string;
|
|
107
|
+
ns?: string | undefined;
|
|
108
|
+
path: NodePath;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type PluralPairInput = {
|
|
112
|
+
key: string;
|
|
113
|
+
ns?: string | undefined;
|
|
114
|
+
path: NodePath;
|
|
115
|
+
plural: string;
|
|
116
|
+
singular: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const API_NAMES: readonly ApiName[] = [
|
|
120
|
+
"IcuTrans",
|
|
121
|
+
"IcuTransWithoutContext",
|
|
122
|
+
"Trans",
|
|
123
|
+
"TransWithoutContext",
|
|
124
|
+
"Translation",
|
|
125
|
+
"getFixedT",
|
|
126
|
+
"getI18n",
|
|
127
|
+
"init",
|
|
128
|
+
"t",
|
|
129
|
+
"useTranslation",
|
|
130
|
+
"withTranslation",
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const API_NAME_SET: Set<string> = new Set(API_NAMES);
|
|
134
|
+
const GTKX_MODULE = "@gtkx/i18n";
|
|
135
|
+
const I18NEXT_MODULE = "i18next";
|
|
136
|
+
const REACT_I18NEXT_MODULE = "react-i18next";
|
|
137
|
+
const SUPPORTED_MODULES = new Set([GTKX_MODULE, I18NEXT_MODULE, REACT_I18NEXT_MODULE]);
|
|
138
|
+
const I18NEXT_APIS: Set<ApiName> = new Set(["getFixedT", "init", "t"]);
|
|
139
|
+
const CONTEXT_SEPARATOR = "\u{4}";
|
|
140
|
+
const EMPTY_INFO: ScopeInfo = {};
|
|
141
|
+
const GLOBAL_T: TSemantic = { fixed: false, info: EMPTY_INFO, kind: "t" };
|
|
142
|
+
|
|
143
|
+
const oneChild = (path: NodePath, key: string): NodePath | undefined => {
|
|
144
|
+
const value = path.get(key);
|
|
145
|
+
|
|
146
|
+
return Array.isArray(value) || value.node === null ? undefined : value;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const childPaths = (path: NodePath, key: string): NodePath[] => {
|
|
150
|
+
const value = path.get(key);
|
|
151
|
+
|
|
152
|
+
return Array.isArray(value)
|
|
153
|
+
? value.filter((candidate) => candidate.node !== null)
|
|
154
|
+
: [];
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const isTransparentExpression = (path: NodePath): boolean => [
|
|
158
|
+
path.isParenthesizedExpression(),
|
|
159
|
+
path.isTSAsExpression(),
|
|
160
|
+
path.isTSInstantiationExpression(),
|
|
161
|
+
path.isTSNonNullExpression(),
|
|
162
|
+
path.isTSSatisfiesExpression(),
|
|
163
|
+
path.isTSTypeAssertion(),
|
|
164
|
+
].includes(true);
|
|
165
|
+
|
|
166
|
+
const unwrapPath = (path: NodePath): NodePath => {
|
|
167
|
+
let current = path;
|
|
168
|
+
|
|
169
|
+
while (isTransparentExpression(current)) {
|
|
170
|
+
const expression = oneChild(current, "expression");
|
|
171
|
+
|
|
172
|
+
if (expression === undefined) {
|
|
173
|
+
return current;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
current = expression;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return current;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const staticPropertyName = (path: NodePath): string | undefined => {
|
|
183
|
+
if (path.isIdentifier()) {
|
|
184
|
+
return path.node.name;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (path.isStringLiteral()) {
|
|
188
|
+
return path.node.value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return path.isNumericLiteral() ? String(path.node.value) : undefined;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const memberProperty = (path: NodePath): string | undefined => {
|
|
195
|
+
const member = unwrapPath(path);
|
|
196
|
+
|
|
197
|
+
if (!member.isMemberExpression() && !member.isOptionalMemberExpression()) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const property = oneChild(member, "property");
|
|
202
|
+
|
|
203
|
+
if (property === undefined) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return !member.node.computed && property.isIdentifier()
|
|
208
|
+
? property.node.name
|
|
209
|
+
: staticPropertyName(property);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const constantDeclaratorBinding = (
|
|
213
|
+
path: NodePath,
|
|
214
|
+
resolving: Set<Binding>,
|
|
215
|
+
): Binding | undefined => {
|
|
216
|
+
if (!path.isIdentifier()) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const binding = path.scope.getBinding(path.node.name);
|
|
221
|
+
|
|
222
|
+
if (!binding?.constant) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return resolving.has(binding) || !binding.path.isVariableDeclarator()
|
|
227
|
+
? undefined
|
|
228
|
+
: binding;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const literalIdentifierString = (
|
|
232
|
+
path: NodePath,
|
|
233
|
+
resolving: Set<Binding>,
|
|
234
|
+
): string | undefined => {
|
|
235
|
+
const binding = constantDeclaratorBinding(path, resolving);
|
|
236
|
+
|
|
237
|
+
if (binding === undefined) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const init = oneChild(binding.path, "init");
|
|
242
|
+
|
|
243
|
+
if (init === undefined) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
resolving.add(binding);
|
|
248
|
+
const resolved = literalString(init, resolving);
|
|
249
|
+
resolving.delete(binding);
|
|
250
|
+
|
|
251
|
+
return resolved;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const literalMemberString = (
|
|
255
|
+
path: NodePath,
|
|
256
|
+
resolving: Set<Binding>,
|
|
257
|
+
): string | undefined => {
|
|
258
|
+
const object = oneChild(path, "object");
|
|
259
|
+
const property = memberProperty(path);
|
|
260
|
+
|
|
261
|
+
if (property === undefined || object?.isIdentifier() !== true) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const binding = constantDeclaratorBinding(object, resolving);
|
|
266
|
+
|
|
267
|
+
if (binding === undefined) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const member = objectProperty(oneChild(binding.path, "init"), property);
|
|
272
|
+
|
|
273
|
+
if (member === undefined) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
resolving.add(binding);
|
|
278
|
+
const resolved = literalString(member, resolving);
|
|
279
|
+
resolving.delete(binding);
|
|
280
|
+
|
|
281
|
+
return resolved;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const directLiteralString = (value: NodePath): string | undefined => {
|
|
285
|
+
if (value.isStringLiteral()) {
|
|
286
|
+
return value.node.value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!value.isTemplateLiteral() || value.node.expressions.length > 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const quasi = value.node.quasis[0];
|
|
294
|
+
|
|
295
|
+
return quasi?.value.cooked ?? quasi?.value.raw;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const literalString = (
|
|
299
|
+
path: NodePath | undefined,
|
|
300
|
+
resolving: Set<Binding> = new Set(),
|
|
301
|
+
): string | undefined => {
|
|
302
|
+
if (path === undefined) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const value = unwrapPath(path);
|
|
307
|
+
const direct = directLiteralString(value);
|
|
308
|
+
|
|
309
|
+
if (direct !== undefined) {
|
|
310
|
+
return direct;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return value.isIdentifier()
|
|
314
|
+
? literalIdentifierString(value, resolving)
|
|
315
|
+
: literalMemberString(value, resolving);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const isStaticObjectProperty = (path: NodePath, resolving: Set<Binding>): boolean => {
|
|
319
|
+
if (!path.isSpreadElement()) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return isStaticObject(oneChild(path, "argument"), resolving);
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const isStaticObject = (
|
|
327
|
+
path: NodePath | undefined,
|
|
328
|
+
resolving: Set<Binding> = new Set(),
|
|
329
|
+
): boolean => {
|
|
330
|
+
if (path === undefined) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const value = unwrapPath(path);
|
|
335
|
+
const binding = constantDeclaratorBinding(value, resolving);
|
|
336
|
+
|
|
337
|
+
if (binding !== undefined) {
|
|
338
|
+
resolving.add(binding);
|
|
339
|
+
const isResult = isStaticObject(oneChild(binding.path, "init"), resolving);
|
|
340
|
+
resolving.delete(binding);
|
|
341
|
+
|
|
342
|
+
return isResult;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return value.isObjectExpression() &&
|
|
346
|
+
childPaths(value, "properties").every((property) => isStaticObjectProperty(property, resolving));
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const propertyKeyName = (
|
|
350
|
+
property: NodePath<babel.ObjectMethod | babel.ObjectProperty>,
|
|
351
|
+
): string | undefined => {
|
|
352
|
+
const key = oneChild(property, "key");
|
|
353
|
+
|
|
354
|
+
if (key === undefined) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const propertyName = property.node.computed
|
|
359
|
+
? literalString(key)
|
|
360
|
+
: staticPropertyName(key);
|
|
361
|
+
|
|
362
|
+
if (propertyName === undefined && property.node.computed) {
|
|
363
|
+
throw new Error("Translation options cannot contain dynamic computed properties");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return propertyName;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const spreadPropertyValue = (
|
|
370
|
+
property: NodePath,
|
|
371
|
+
name: string,
|
|
372
|
+
resolving: Set<Binding>,
|
|
373
|
+
): NodePath | undefined => {
|
|
374
|
+
const argument = oneChild(property, "argument");
|
|
375
|
+
const spread = objectProperty(argument, name, resolving);
|
|
376
|
+
|
|
377
|
+
if (spread !== undefined) {
|
|
378
|
+
return spread;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!isStaticObject(argument)) {
|
|
382
|
+
throw new Error("Translation options cannot contain dynamic object spreads");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return undefined;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const directPropertyValue = (property: NodePath, name: string): NodePath | undefined => {
|
|
389
|
+
if (!property.isObjectMethod() && !property.isObjectProperty()) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (propertyKeyName(property) !== name) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (property.isObjectMethod()) {
|
|
398
|
+
return property;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const key = oneChild(property, "key");
|
|
402
|
+
|
|
403
|
+
return property.node.shorthand ? key : oneChild(property, "value");
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const objectPropertyEntry = (
|
|
407
|
+
property: NodePath,
|
|
408
|
+
name: string,
|
|
409
|
+
resolving: Set<Binding>,
|
|
410
|
+
): NodePath | undefined => property.isSpreadElement()
|
|
411
|
+
? spreadPropertyValue(property, name, resolving)
|
|
412
|
+
: directPropertyValue(property, name);
|
|
413
|
+
|
|
414
|
+
const objectExpressionProperty = (
|
|
415
|
+
value: NodePath,
|
|
416
|
+
name: string,
|
|
417
|
+
resolving: Set<Binding>,
|
|
418
|
+
): NodePath | undefined => {
|
|
419
|
+
for (const property of childPaths(value, "properties").toReversed()) {
|
|
420
|
+
const result = objectPropertyEntry(property, name, resolving);
|
|
421
|
+
|
|
422
|
+
if (result !== undefined) {
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return undefined;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const objectProperty = (
|
|
431
|
+
path: NodePath | undefined,
|
|
432
|
+
name: string,
|
|
433
|
+
resolving: Set<Binding> = new Set(),
|
|
434
|
+
): NodePath | undefined => {
|
|
435
|
+
if (path === undefined) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const value = unwrapPath(path);
|
|
440
|
+
const binding = constantDeclaratorBinding(value, resolving);
|
|
441
|
+
|
|
442
|
+
if (binding !== undefined) {
|
|
443
|
+
resolving.add(binding);
|
|
444
|
+
const resolved = objectProperty(oneChild(binding.path, "init"), name, resolving);
|
|
445
|
+
resolving.delete(binding);
|
|
446
|
+
|
|
447
|
+
return resolved;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (!value.isObjectExpression()) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return objectExpressionProperty(value, name, resolving);
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const resolvedNamespaceIdentifier = (
|
|
458
|
+
value: NodePath,
|
|
459
|
+
resolving: Set<Binding>,
|
|
460
|
+
): string[] | undefined => {
|
|
461
|
+
const binding = constantDeclaratorBinding(value, resolving);
|
|
462
|
+
|
|
463
|
+
if (binding === undefined) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
resolving.add(binding);
|
|
468
|
+
const resolved = namespaceValues(oneChild(binding.path, "init"), resolving);
|
|
469
|
+
resolving.delete(binding);
|
|
470
|
+
|
|
471
|
+
return resolved;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const namespaceValues = (
|
|
475
|
+
path: NodePath | undefined,
|
|
476
|
+
resolving: Set<Binding> = new Set(),
|
|
477
|
+
): string[] => {
|
|
478
|
+
const direct = literalString(path);
|
|
479
|
+
|
|
480
|
+
if (direct !== undefined) {
|
|
481
|
+
return [direct];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (path === undefined) {
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const value = unwrapPath(path);
|
|
489
|
+
const resolved = resolvedNamespaceIdentifier(value, resolving);
|
|
490
|
+
|
|
491
|
+
if (resolved !== undefined) {
|
|
492
|
+
return resolved;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!value.isArrayExpression()) {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const values = childPaths(value, "elements").map((element) => literalString(element, resolving));
|
|
500
|
+
|
|
501
|
+
return values.every((candidate): candidate is string => candidate !== undefined) ? values : [];
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const normalizedInfo = (namespaces: string[], keyPrefix: string | undefined): ScopeInfo => {
|
|
505
|
+
const info: ScopeInfo = {};
|
|
506
|
+
const defaultNs = namespaces[0];
|
|
507
|
+
|
|
508
|
+
if (defaultNs !== undefined) {
|
|
509
|
+
info.defaultNs = defaultNs;
|
|
510
|
+
|
|
511
|
+
if (namespaces.length > 1) {
|
|
512
|
+
info.namespaces = namespaces;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (keyPrefix !== undefined && keyPrefix.length > 0) {
|
|
517
|
+
info.keyPrefix = keyPrefix;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return info;
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const isAbsentArgument = (path: NodePath): boolean => {
|
|
524
|
+
if (path.isNullLiteral()) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return path.isIdentifier() &&
|
|
529
|
+
path.node.name === "undefined" &&
|
|
530
|
+
path.scope.getBinding("undefined") === undefined;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const staticNamespaces = (path: NodePath | undefined, file: string): string[] => {
|
|
534
|
+
if (path === undefined || isAbsentArgument(path)) {
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const namespaces = namespaceValues(path);
|
|
539
|
+
|
|
540
|
+
if (namespaces.length === 0) {
|
|
541
|
+
throw new Error(`Translation namespaces must be static strings in ${file}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return namespaces;
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const staticKeyPrefix = (path: NodePath | undefined, file: string): string | undefined => {
|
|
548
|
+
if (path === undefined || isAbsentArgument(path)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const keyPrefix = literalString(path);
|
|
553
|
+
|
|
554
|
+
if (keyPrefix === undefined) {
|
|
555
|
+
throw new Error(`Translation key prefixes must be static strings in ${file}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return keyPrefix;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const callArguments = (path: NodePath): NodePath[] => childPaths(path, "arguments");
|
|
562
|
+
|
|
563
|
+
const scopeFromCall = (input: ScopeCallInput): ScopeInfo => {
|
|
564
|
+
const args = callArguments(input.path);
|
|
565
|
+
const options = input.optionsIndex === undefined ? undefined : args[input.optionsIndex];
|
|
566
|
+
const optionPrefix = staticKeyPrefix(objectProperty(options, "keyPrefix"), input.file);
|
|
567
|
+
const positional = input.prefixIndex === undefined ? undefined : args[input.prefixIndex];
|
|
568
|
+
const positionalPrefix = staticKeyPrefix(positional, input.file);
|
|
569
|
+
|
|
570
|
+
return normalizedInfo(
|
|
571
|
+
staticNamespaces(args[input.namespaceIndex], input.file),
|
|
572
|
+
optionPrefix ?? positionalPrefix,
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
const isApiName = (name: string): name is ApiName => API_NAME_SET.has(name);
|
|
577
|
+
|
|
578
|
+
const isApiAvailable = (moduleName: string, api: ApiName): boolean => {
|
|
579
|
+
if (moduleName === GTKX_MODULE) {
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (moduleName === I18NEXT_MODULE) {
|
|
584
|
+
return I18NEXT_APIS.has(api);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return moduleName === REACT_I18NEXT_MODULE && !I18NEXT_APIS.has(api);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const apiSemantic = (api: ApiName): Semantic =>
|
|
591
|
+
api === "t" ? GLOBAL_T : { api, kind: "api" };
|
|
592
|
+
|
|
593
|
+
const spanKey = (node: { end?: number | null; start?: number | null }): string | undefined =>
|
|
594
|
+
typeof node.start === "number" && typeof node.end === "number"
|
|
595
|
+
? `${String(node.start)}:${String(node.end)}`
|
|
596
|
+
: undefined;
|
|
597
|
+
|
|
598
|
+
const pathLocation = (path: NodePath, file: string): ExtractedMessage["location"] => {
|
|
599
|
+
const start = path.node.loc?.start;
|
|
600
|
+
|
|
601
|
+
return start === undefined
|
|
602
|
+
? { file }
|
|
603
|
+
: { column: start.column, file, line: start.line };
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const patternLocalName = (path: NodePath): string | undefined => {
|
|
607
|
+
const value = unwrapPath(path);
|
|
608
|
+
|
|
609
|
+
if (value.isIdentifier()) {
|
|
610
|
+
return value.node.name;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const left = value.isAssignmentPattern() ? oneChild(value, "left") : undefined;
|
|
614
|
+
|
|
615
|
+
return left?.isIdentifier() === true ? left.node.name : undefined;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const scopeWithPrefix = (info: ScopeInfo, keyPrefix: string): ScopeInfo => {
|
|
619
|
+
const scoped = { ...info };
|
|
620
|
+
|
|
621
|
+
if (keyPrefix.length === 0) {
|
|
622
|
+
delete scoped.keyPrefix;
|
|
623
|
+
} else {
|
|
624
|
+
scoped.keyPrefix = keyPrefix;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return scoped;
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const isClassPath = (path: NodePath): boolean => path.isClass();
|
|
631
|
+
|
|
632
|
+
const isCallPath = (path: NodePath): boolean =>
|
|
633
|
+
path.isCallExpression() || path.isOptionalCallExpression();
|
|
634
|
+
|
|
635
|
+
const isMemberPath = (path: NodePath): boolean =>
|
|
636
|
+
path.isMemberExpression() || path.isOptionalMemberExpression();
|
|
637
|
+
|
|
638
|
+
const isStaticStringArray = (path: NodePath | undefined): boolean =>
|
|
639
|
+
path !== undefined && literalString(path) === undefined && namespaceValues(path).length > 0;
|
|
640
|
+
|
|
641
|
+
const analyzeProgram = (program: NodePath, file: string): BindingExtraction =>
|
|
642
|
+
new BindingAnalyzer(file).analyze(program);
|
|
643
|
+
|
|
644
|
+
const parserPlugins = (path: string): ParserPlugin[] => {
|
|
645
|
+
const extension = extname(path).toLowerCase();
|
|
646
|
+
const plugins: ParserPlugin[] = ["decorators-legacy"];
|
|
647
|
+
|
|
648
|
+
if ([".cts", ".mts", ".ts", ".tsx"].includes(extension)) {
|
|
649
|
+
plugins.push("typescript");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if ([".cjs", ".js", ".jsx", ".mjs", ".tsx"].includes(extension)) {
|
|
653
|
+
plugins.push("jsx");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return plugins;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const extractBindings = async (code: string, path: string): Promise<BindingExtraction> => {
|
|
660
|
+
const ast = await parseAsync(code, {
|
|
661
|
+
babelrc: false,
|
|
662
|
+
configFile: false,
|
|
663
|
+
filename: path,
|
|
664
|
+
parserOpts: {
|
|
665
|
+
plugins: parserPlugins(path),
|
|
666
|
+
sourceType: "unambiguous",
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (ast === null) {
|
|
671
|
+
throw new Error(`Unable to parse ${path}`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
let extraction: BindingExtraction | undefined;
|
|
675
|
+
|
|
676
|
+
traverse(ast, {
|
|
677
|
+
Program(programPath) {
|
|
678
|
+
extraction = analyzeProgram(programPath, path);
|
|
679
|
+
programPath.stop();
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (extraction === undefined) {
|
|
684
|
+
throw new Error(`Unable to analyze ${path}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return extraction;
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
class BindingAnalyzer {
|
|
691
|
+
private readonly bindingCache: Map<Binding, Semantic | null> = new Map();
|
|
692
|
+
private readonly blockedGetFixedT: Set<string> = new Set();
|
|
693
|
+
private readonly calls: Map<string, ExtractedCall> = new Map();
|
|
694
|
+
private readonly errors: Set<string> = new Set();
|
|
695
|
+
private readonly file: string;
|
|
696
|
+
private readonly hocClasses: Map<babel.Node, TSemantic> = new Map();
|
|
697
|
+
private readonly messages: ExtractedMessage[] = [];
|
|
698
|
+
private readonly resolving: Set<Binding> = new Set();
|
|
699
|
+
private readonly specialBindings: Map<Binding, Semantic> = new Map();
|
|
700
|
+
private readonly trans: Map<string, ScopeInfo | null> = new Map();
|
|
701
|
+
|
|
702
|
+
constructor(file: string) {
|
|
703
|
+
this.file = file;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private runSemanticPass(program: NodePath): void {
|
|
707
|
+
program.traverse({
|
|
708
|
+
CallExpression: this.registerCallSemantics.bind(this),
|
|
709
|
+
JSXElement: this.registerJsxElement.bind(this),
|
|
710
|
+
OptionalCallExpression: this.registerCallSemantics.bind(this),
|
|
711
|
+
VariableDeclarator: this.registerBlockedGetFixedT.bind(this),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private runTranslationPass(program: NodePath): void {
|
|
716
|
+
program.traverse({
|
|
717
|
+
CallExpression: this.registerTranslationCall.bind(this),
|
|
718
|
+
OptionalCallExpression: this.registerTranslationCall.bind(this),
|
|
719
|
+
TaggedTemplateExpression: this.registerTaggedTranslation.bind(this),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private importSemantic(binding: Binding): Semantic | undefined {
|
|
724
|
+
const path = binding.path;
|
|
725
|
+
const declaration = path.parentPath;
|
|
726
|
+
|
|
727
|
+
if (!declaration.isImportDeclaration()) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (this.isTypeImport(path, declaration)) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const moduleName = declaration.node.source.value;
|
|
736
|
+
|
|
737
|
+
if (path.isImportNamespaceSpecifier()) {
|
|
738
|
+
return SUPPORTED_MODULES.has(moduleName) ? { kind: "namespace", module: moduleName } : undefined;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
return this.importSpecifierSemantic(path, moduleName);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private isTypeImport(path: NodePath, declaration: NodePath<babel.ImportDeclaration>): boolean {
|
|
745
|
+
return declaration.node.importKind === "type" ||
|
|
746
|
+
(path.isImportSpecifier() && path.node.importKind === "type");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private importSpecifierSemantic(path: NodePath, moduleName: string): Semantic | undefined {
|
|
750
|
+
if (path.isImportDefaultSpecifier()) {
|
|
751
|
+
return moduleName === I18NEXT_MODULE
|
|
752
|
+
? { kind: "namespace", module: moduleName }
|
|
753
|
+
: undefined;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return path.isImportSpecifier()
|
|
757
|
+
? this.namedImportSemantic(path, moduleName)
|
|
758
|
+
: undefined;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private namedImportSemantic(path: NodePath<babel.ImportSpecifier>, moduleName: string): Semantic | undefined {
|
|
762
|
+
const imported = path.node.imported;
|
|
763
|
+
const name = babel.isIdentifier(imported) ? imported.name : imported.value;
|
|
764
|
+
|
|
765
|
+
return isApiName(name) && isApiAvailable(moduleName, name)
|
|
766
|
+
? apiSemantic(name)
|
|
767
|
+
: undefined;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private semanticMember(object: Semantic, property: string): Semantic | undefined {
|
|
771
|
+
if (object.kind === "namespace") {
|
|
772
|
+
return this.namespaceMember(object.module, property);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return this.localSemanticMember(object, property);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private namespaceMember(moduleName: string, property: string): Semantic | undefined {
|
|
779
|
+
return isApiName(property) && isApiAvailable(moduleName, property)
|
|
780
|
+
? apiSemantic(property)
|
|
781
|
+
: undefined;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private localSemanticMember(object: Semantic, property: string): Semantic | undefined {
|
|
785
|
+
if (object.kind === "translationContext") {
|
|
786
|
+
return property === "i18n"
|
|
787
|
+
? { instance: true, kind: "translationObject", t: GLOBAL_T }
|
|
788
|
+
: undefined;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return object.kind === "translationObject"
|
|
792
|
+
? this.translationObjectMember(object, property)
|
|
793
|
+
: undefined;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private translationObjectMember(
|
|
797
|
+
object: Extract<Semantic, { kind: "translationObject" }>,
|
|
798
|
+
property: string,
|
|
799
|
+
): Semantic | undefined {
|
|
800
|
+
if (["0", "t"].includes(property)) {
|
|
801
|
+
return object.t;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (!object.instance && ["1", "i18n"].includes(property)) {
|
|
805
|
+
return { instance: true, kind: "translationObject", t: GLOBAL_T };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
return object.instance && ["getFixedT", "init"].includes(property)
|
|
809
|
+
? { api: property as "getFixedT" | "init", kind: "api" }
|
|
810
|
+
: undefined;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private resolvePath(input: NodePath): Semantic | undefined {
|
|
814
|
+
const path = unwrapPath(input);
|
|
815
|
+
|
|
816
|
+
if (path.isIdentifier()) {
|
|
817
|
+
return this.resolveIdentifierPath(path);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (isMemberPath(path)) {
|
|
821
|
+
return this.resolveMemberPath(path);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (isCallPath(path)) {
|
|
825
|
+
return this.resolveCallPath(path);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return path.isAwaitExpression()
|
|
829
|
+
? this.resolveAwaitPath(path)
|
|
830
|
+
: undefined;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private resolveIdentifierPath(path: NodePath<babel.Identifier>): Semantic | undefined {
|
|
834
|
+
const binding = path.scope.getBinding(path.node.name);
|
|
835
|
+
|
|
836
|
+
return binding === undefined ? undefined : this.resolveBinding(binding);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private resolveMemberPath(path: NodePath): Semantic | undefined {
|
|
840
|
+
const objectPath = oneChild(path, "object");
|
|
841
|
+
const property = memberProperty(path);
|
|
842
|
+
|
|
843
|
+
if (objectPath === undefined || property === undefined) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const object = this.resolvePath(objectPath);
|
|
848
|
+
|
|
849
|
+
return object === undefined ? undefined : this.semanticMember(object, property);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
private resolveCallPath(path: NodePath): Semantic | undefined {
|
|
853
|
+
const callee = oneChild(path, "callee");
|
|
854
|
+
|
|
855
|
+
if (callee === undefined) {
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const required = this.requireSemantic(path, callee);
|
|
860
|
+
|
|
861
|
+
if (required !== undefined) {
|
|
862
|
+
return required;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const called = this.resolvePath(callee);
|
|
866
|
+
|
|
867
|
+
return called?.kind === "api" ? this.apiCallSemantic(called.api, path) : undefined;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
private requireSemantic(path: NodePath, callee: NodePath): Semantic | undefined {
|
|
871
|
+
if (!callee.isIdentifier() || callee.node.name !== "require") {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (callee.scope.getBinding(callee.node.name) !== undefined) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const moduleName = literalString(callArguments(path)[0]);
|
|
880
|
+
|
|
881
|
+
return moduleName !== undefined && SUPPORTED_MODULES.has(moduleName)
|
|
882
|
+
? { kind: "namespace", module: moduleName }
|
|
883
|
+
: undefined;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
private apiCallSemantic(api: ApiName, path: NodePath): Semantic | undefined {
|
|
887
|
+
switch (api) {
|
|
888
|
+
case "useTranslation": {
|
|
889
|
+
return this.useTranslationSemantic(path);
|
|
890
|
+
}
|
|
891
|
+
case "getI18n": {
|
|
892
|
+
return { instance: true, kind: "translationObject", t: GLOBAL_T };
|
|
893
|
+
}
|
|
894
|
+
case "getFixedT": {
|
|
895
|
+
return this.fixedTranslationSemantic(path);
|
|
896
|
+
}
|
|
897
|
+
case "withTranslation": {
|
|
898
|
+
return this.translationEnhancerSemantic(path);
|
|
899
|
+
}
|
|
900
|
+
case "init": {
|
|
901
|
+
return { kind: "initPromise" };
|
|
902
|
+
}
|
|
903
|
+
case "IcuTrans":
|
|
904
|
+
case "IcuTransWithoutContext":
|
|
905
|
+
case "Trans":
|
|
906
|
+
case "TransWithoutContext":
|
|
907
|
+
case "Translation":
|
|
908
|
+
case "t": {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private useTranslationSemantic(path: NodePath): Semantic {
|
|
915
|
+
const info = scopeFromCall({ file: this.file, namespaceIndex: 0, optionsIndex: 1, path });
|
|
916
|
+
|
|
917
|
+
return {
|
|
918
|
+
instance: false,
|
|
919
|
+
kind: "translationObject",
|
|
920
|
+
t: { fixed: true, info, kind: "t" },
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private fixedTranslationSemantic(path: NodePath): TSemantic {
|
|
925
|
+
const info = scopeFromCall({
|
|
926
|
+
file: this.file,
|
|
927
|
+
namespaceIndex: 1,
|
|
928
|
+
path,
|
|
929
|
+
prefixIndex: 2,
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
return { fixed: true, info, kind: "t" };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private translationEnhancerSemantic(path: NodePath): Semantic {
|
|
936
|
+
const info = scopeFromCall({ file: this.file, namespaceIndex: 0, optionsIndex: 1, path });
|
|
937
|
+
|
|
938
|
+
return { info, kind: "enhancer" };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private resolveAwaitPath(path: NodePath): Semantic | undefined {
|
|
942
|
+
const argument = oneChild(path, "argument");
|
|
943
|
+
const awaited = argument === undefined ? undefined : this.resolvePath(argument);
|
|
944
|
+
|
|
945
|
+
return awaited?.kind === "initPromise" ? GLOBAL_T : undefined;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
private resolveBinding(binding: Binding): Semantic | undefined {
|
|
949
|
+
const special = this.specialBindings.get(binding);
|
|
950
|
+
|
|
951
|
+
if (special !== undefined) {
|
|
952
|
+
return special;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return binding.constant ? this.resolveConstantBinding(binding) : undefined;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
private resolveConstantBinding(binding: Binding): Semantic | undefined {
|
|
959
|
+
const cached = this.bindingCache.get(binding);
|
|
960
|
+
|
|
961
|
+
if (cached !== undefined) {
|
|
962
|
+
return cached ?? undefined;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (this.resolving.has(binding)) {
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
this.resolving.add(binding);
|
|
970
|
+
const resolved = this.importSemantic(binding) ?? this.declaratorSemantic(binding);
|
|
971
|
+
this.resolving.delete(binding);
|
|
972
|
+
this.bindingCache.set(binding, resolved ?? null);
|
|
973
|
+
|
|
974
|
+
return resolved;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private declaratorSemantic(binding: Binding): Semantic | undefined {
|
|
978
|
+
const path = binding.path;
|
|
979
|
+
|
|
980
|
+
if (!path.isVariableDeclarator()) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const id = oneChild(path, "id");
|
|
985
|
+
const init = oneChild(path, "init");
|
|
986
|
+
|
|
987
|
+
if (id === undefined || init === undefined) {
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (id.isIdentifier() && id.node.name === binding.identifier.name) {
|
|
992
|
+
return this.resolvePath(init);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
return this.patternDeclaratorSemantic(id, init, binding.identifier.name);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private patternDeclaratorSemantic(id: NodePath, init: NodePath, name: string): Semantic | undefined {
|
|
999
|
+
if (id.isObjectPattern()) {
|
|
1000
|
+
return this.objectPatternSemantic(id, init, name);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return id.isArrayPattern()
|
|
1004
|
+
? this.arrayPatternSemantic(id, init, name)
|
|
1005
|
+
: undefined;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
private objectPatternSemantic(id: NodePath, init: NodePath, name: string): Semantic | undefined {
|
|
1009
|
+
const object = this.resolvePath(init);
|
|
1010
|
+
|
|
1011
|
+
if (object === undefined) {
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
for (const property of childPaths(id, "properties")) {
|
|
1016
|
+
const resolved = this.patternPropertySemantic(property, object, name);
|
|
1017
|
+
|
|
1018
|
+
if (resolved !== undefined) {
|
|
1019
|
+
return resolved;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return undefined;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private patternPropertySemantic(
|
|
1027
|
+
property: NodePath,
|
|
1028
|
+
object: Semantic,
|
|
1029
|
+
name: string,
|
|
1030
|
+
): Semantic | undefined {
|
|
1031
|
+
if (!property.isObjectProperty()) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const key = oneChild(property, "key");
|
|
1036
|
+
const value = oneChild(property, "value");
|
|
1037
|
+
|
|
1038
|
+
if (key === undefined || value === undefined || patternLocalName(value) !== name) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const propertyName = staticPropertyName(key);
|
|
1043
|
+
|
|
1044
|
+
return propertyName === undefined ? undefined : this.semanticMember(object, propertyName);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
private arrayPatternSemantic(id: NodePath, init: NodePath, name: string): Semantic | undefined {
|
|
1048
|
+
const object = this.resolvePath(init);
|
|
1049
|
+
|
|
1050
|
+
if (object?.kind !== "translationObject") {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const elements = childPaths(id, "elements");
|
|
1055
|
+
|
|
1056
|
+
if (elements[0] !== undefined && patternLocalName(elements[0]) === name) {
|
|
1057
|
+
return object.t;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return !object.instance && elements[1] !== undefined && patternLocalName(elements[1]) === name
|
|
1061
|
+
? { instance: true, kind: "translationObject", t: GLOBAL_T }
|
|
1062
|
+
: undefined;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
private jsxSemantic(path: NodePath): Semantic | undefined {
|
|
1066
|
+
if (path.isJSXIdentifier()) {
|
|
1067
|
+
const binding = path.scope.getBinding(path.node.name);
|
|
1068
|
+
|
|
1069
|
+
return binding === undefined ? undefined : this.resolveBinding(binding);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
return path.isJSXMemberExpression()
|
|
1073
|
+
? this.jsxMemberSemantic(path)
|
|
1074
|
+
: undefined;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
private jsxMemberSemantic(path: NodePath<babel.JSXMemberExpression>): Semantic | undefined {
|
|
1078
|
+
const object = oneChild(path, "object");
|
|
1079
|
+
const property = oneChild(path, "property");
|
|
1080
|
+
|
|
1081
|
+
if (object === undefined || property?.isJSXIdentifier() !== true) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const base = this.jsxSemantic(object);
|
|
1086
|
+
|
|
1087
|
+
return base === undefined
|
|
1088
|
+
? undefined
|
|
1089
|
+
: this.semanticMember(base, property.node.name);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private resolveFunction(input: NodePath | undefined): NodePath | undefined {
|
|
1093
|
+
if (input === undefined) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const path = unwrapPath(input);
|
|
1098
|
+
|
|
1099
|
+
if (path.isFunction() || path.isClass()) {
|
|
1100
|
+
return path;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
if (!path.isIdentifier()) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return this.resolveFunctionBinding(path.scope.getBinding(path.node.name));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
private resolveFunctionBinding(binding: Binding | undefined): NodePath | undefined {
|
|
1111
|
+
if (binding?.path.isFunctionDeclaration() === true || binding?.path.isClassDeclaration() === true) {
|
|
1112
|
+
return binding.path;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return binding?.path.isVariableDeclarator() === true
|
|
1116
|
+
? this.resolveFunction(oneChild(binding.path, "init"))
|
|
1117
|
+
: undefined;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private registerPatternBinding(path: NodePath, semantic: Semantic): void {
|
|
1121
|
+
const name = patternLocalName(path);
|
|
1122
|
+
const binding = name === undefined ? undefined : path.scope.getBinding(name);
|
|
1123
|
+
|
|
1124
|
+
if (binding === undefined) {
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const existing = this.specialBindings.get(binding);
|
|
1129
|
+
|
|
1130
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(semantic)) {
|
|
1131
|
+
this.errors.add(`A translation binding cannot be reused with conflicting scopes in ${this.file}`);
|
|
1132
|
+
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
this.specialBindings.set(binding, semantic);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private registerSemanticPattern(path: NodePath, semantic: Semantic): void {
|
|
1140
|
+
const target = this.semanticPatternTarget(path);
|
|
1141
|
+
|
|
1142
|
+
if (target.isIdentifier()) {
|
|
1143
|
+
this.registerPatternBinding(target, semantic);
|
|
1144
|
+
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (target.isObjectPattern()) {
|
|
1149
|
+
this.registerObjectSemanticPattern(target, semantic);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private semanticPatternTarget(path: NodePath): NodePath {
|
|
1154
|
+
const target = unwrapPath(path);
|
|
1155
|
+
|
|
1156
|
+
return target.isAssignmentPattern()
|
|
1157
|
+
? oneChild(target, "left") ?? target
|
|
1158
|
+
: target;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
private registerObjectSemanticPattern(pattern: NodePath, semantic: Semantic): void {
|
|
1162
|
+
for (const property of childPaths(pattern, "properties")) {
|
|
1163
|
+
this.registerSemanticProperty(property, semantic);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
private registerSemanticProperty(property: NodePath, semantic: Semantic): void {
|
|
1168
|
+
if (!property.isObjectProperty()) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const key = oneChild(property, "key");
|
|
1173
|
+
const value = oneChild(property, "value");
|
|
1174
|
+
const name = key === undefined ? undefined : staticPropertyName(key);
|
|
1175
|
+
const member = name === undefined ? undefined : this.semanticMember(semantic, name);
|
|
1176
|
+
|
|
1177
|
+
if (value !== undefined && member !== undefined) {
|
|
1178
|
+
this.registerSemanticPattern(value, member);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private registerTParameter(fn: NodePath, index: number, t: TSemantic): void {
|
|
1183
|
+
const parameter = childPaths(fn, "params")[index];
|
|
1184
|
+
|
|
1185
|
+
if (parameter !== undefined) {
|
|
1186
|
+
this.registerPatternBinding(parameter, t);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private registerHocComponent(component: NodePath | undefined, info: ScopeInfo): void {
|
|
1191
|
+
const target = this.resolveFunction(component);
|
|
1192
|
+
|
|
1193
|
+
if (target === undefined) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const t: TSemantic = { fixed: true, info, kind: "t" };
|
|
1198
|
+
|
|
1199
|
+
if (target.isClass()) {
|
|
1200
|
+
this.hocClasses.set(target.node, t);
|
|
1201
|
+
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
this.registerHocParameter(childPaths(target, "params")[0], t);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
private registerHocParameter(parameter: NodePath | undefined, t: TSemantic): void {
|
|
1209
|
+
if (parameter === undefined) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
const unwrapped = unwrapPath(parameter);
|
|
1214
|
+
|
|
1215
|
+
if (unwrapped.isIdentifier() || unwrapped.isAssignmentPattern()) {
|
|
1216
|
+
this.registerPatternBinding(unwrapped, { instance: false, kind: "translationObject", t });
|
|
1217
|
+
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (unwrapped.isObjectPattern()) {
|
|
1222
|
+
this.registerHocObjectPattern(unwrapped, t);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
private registerHocObjectPattern(pattern: NodePath, t: TSemantic): void {
|
|
1227
|
+
for (const property of childPaths(pattern, "properties")) {
|
|
1228
|
+
this.registerHocProperty(property, t);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
private registerHocProperty(property: NodePath, t: TSemantic): void {
|
|
1233
|
+
if (!property.isObjectProperty()) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const key = oneChild(property, "key");
|
|
1238
|
+
const value = oneChild(property, "value");
|
|
1239
|
+
|
|
1240
|
+
if (key !== undefined && value !== undefined && staticPropertyName(key) === "t") {
|
|
1241
|
+
this.registerPatternBinding(value, t);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
private jsxAttribute(element: NodePath, name: string): NodePath | undefined {
|
|
1246
|
+
const opening = oneChild(element, "openingElement");
|
|
1247
|
+
|
|
1248
|
+
if (opening === undefined) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
for (const attribute of childPaths(opening, "attributes").toReversed()) {
|
|
1253
|
+
const value = this.jsxAttributeValue(attribute, name);
|
|
1254
|
+
|
|
1255
|
+
if (value !== undefined) {
|
|
1256
|
+
return value;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
return undefined;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
private jsxAttributeValue(attribute: NodePath, name: string): NodePath | undefined {
|
|
1264
|
+
if (attribute.isJSXSpreadAttribute()) {
|
|
1265
|
+
return objectProperty(oneChild(attribute, "argument"), name);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (!attribute.isJSXAttribute()) {
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const attributeName = oneChild(attribute, "name");
|
|
1273
|
+
|
|
1274
|
+
if (attributeName?.isJSXIdentifier() !== true || attributeName.node.name !== name) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const value = oneChild(attribute, "value");
|
|
1279
|
+
|
|
1280
|
+
return value?.isJSXExpressionContainer() === true ? oneChild(value, "expression") : value;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
private scopeFromJsx(element: NodePath): ScopeInfo {
|
|
1284
|
+
return normalizedInfo(
|
|
1285
|
+
staticNamespaces(this.jsxAttribute(element, "ns"), this.file),
|
|
1286
|
+
staticKeyPrefix(this.jsxAttribute(element, "keyPrefix"), this.file),
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
private addComponentMessage(input: ComponentInput): void {
|
|
1291
|
+
const componentKeys = this.componentKeys(input);
|
|
1292
|
+
|
|
1293
|
+
if (componentKeys === undefined) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (!this.validateIcu(input, componentKeys)) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const t = this.componentT(input.tPath);
|
|
1302
|
+
|
|
1303
|
+
if (!t.isValid) {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const context = this.componentContext(input);
|
|
1308
|
+
|
|
1309
|
+
if (!context.isValid) {
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const scope = this.componentScope(input, componentKeys.rawKeys, t.value, context.value);
|
|
1314
|
+
this.addScopedComponentMessages(input, componentKeys, scope);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
private addScopedComponentMessages(
|
|
1318
|
+
input: ComponentInput,
|
|
1319
|
+
componentKeys: ComponentKeys,
|
|
1320
|
+
scope: ComponentScope,
|
|
1321
|
+
): void {
|
|
1322
|
+
if (this.componentHasCount(input)) {
|
|
1323
|
+
this.addPluralComponentMessages(input, componentKeys.fallback, scope);
|
|
1324
|
+
} else {
|
|
1325
|
+
this.addPointComponentMessages(input, componentKeys, scope);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
private componentKeys(input: ComponentInput): ComponentKeys | undefined {
|
|
1330
|
+
const optionDefault = objectProperty(input.optionsPath, "defaultValue");
|
|
1331
|
+
const fallback = literalString(input.defaultPath) ?? literalString(optionDefault);
|
|
1332
|
+
const explicitKeys = this.explicitComponentKeys(input.keyPath);
|
|
1333
|
+
|
|
1334
|
+
if (explicitKeys === undefined) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const rawKeys = explicitKeys.length > 0 ? explicitKeys : this.fallbackKeys(fallback);
|
|
1339
|
+
|
|
1340
|
+
if (rawKeys.length === 0) {
|
|
1341
|
+
this.errors.add(`Translation components require a static key or default in ${this.file}`);
|
|
1342
|
+
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return { fallback, rawKeys };
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
private explicitComponentKeys(keyPath: NodePath | undefined): string[] | undefined {
|
|
1350
|
+
if (keyPath === undefined) {
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const keys = namespaceValues(keyPath);
|
|
1355
|
+
|
|
1356
|
+
if (keys.length === 0) {
|
|
1357
|
+
this.errors.add(`Translation component keys must be static strings in ${this.file}`);
|
|
1358
|
+
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return keys;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
private fallbackKeys(fallback: string | undefined): string[] {
|
|
1366
|
+
return fallback === undefined ? [] : [fallback];
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
private validateIcu(input: ComponentInput, keys: ComponentKeys): boolean {
|
|
1370
|
+
if (!input.isIcu) {
|
|
1371
|
+
return true;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (keys.rawKeys.length > 1) {
|
|
1375
|
+
this.errors.add(`ICU translations require one static key in ${this.file}`);
|
|
1376
|
+
|
|
1377
|
+
return false;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
return this.validateIcuFallback(input, keys.fallback);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private validateIcuFallback(input: ComponentInput, fallback: string | undefined): boolean {
|
|
1384
|
+
if (fallback === undefined) {
|
|
1385
|
+
this.errors.add(`ICU translations require a static defaultTranslation in ${this.file}`);
|
|
1386
|
+
|
|
1387
|
+
return false;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const withoutInterpolation = fallback.replaceAll(/\{\{[^{}]+\}\}/gu, "");
|
|
1391
|
+
|
|
1392
|
+
if (/\{[^{}]+\}/u.test(withoutInterpolation)) {
|
|
1393
|
+
this.errors.add(`GTKX gettext catalogs do not support ICU MessageFormat syntax in ${this.file}`);
|
|
1394
|
+
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return this.validateIcuCount(input, fallback);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
private validateIcuCount(input: ComponentInput, fallback: string): boolean {
|
|
1402
|
+
const hasValueCount = objectProperty(input.valuesPath, "count") !== undefined;
|
|
1403
|
+
|
|
1404
|
+
if (hasValueCount || /\{\{\s*count\s*\}\}/u.test(fallback)) {
|
|
1405
|
+
this.errors.add(`ICU translations cannot infer a GNU gettext plural pair in ${this.file}`);
|
|
1406
|
+
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
private componentT(path: NodePath | undefined): Resolution<TSemantic> {
|
|
1414
|
+
if (path === undefined) {
|
|
1415
|
+
return { isValid: true };
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const semantic = this.resolvePath(path);
|
|
1419
|
+
|
|
1420
|
+
if (semantic?.kind !== "t") {
|
|
1421
|
+
this.errors.add(`Translation component t props must have a statically known scope in ${this.file}`);
|
|
1422
|
+
|
|
1423
|
+
return { isValid: false };
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return { isValid: true, value: semantic };
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
private componentContext(input: ComponentInput): Resolution<string> {
|
|
1430
|
+
const optionContext = objectProperty(input.optionsPath, "context");
|
|
1431
|
+
const contextPath = input.contextPath ?? optionContext;
|
|
1432
|
+
|
|
1433
|
+
if (contextPath === undefined) {
|
|
1434
|
+
return { isValid: true };
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const context = literalString(contextPath);
|
|
1438
|
+
|
|
1439
|
+
if (context === undefined) {
|
|
1440
|
+
this.errors.add(`Translation contexts must be static strings in ${this.file}`);
|
|
1441
|
+
|
|
1442
|
+
return { isValid: false };
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
return { isValid: true, value: context };
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
private componentScope(
|
|
1449
|
+
input: ComponentInput,
|
|
1450
|
+
rawKeys: string[],
|
|
1451
|
+
t: TSemantic | undefined,
|
|
1452
|
+
context: string | undefined,
|
|
1453
|
+
): ComponentScope {
|
|
1454
|
+
const prefix = t?.info.keyPrefix;
|
|
1455
|
+
const baseKeys = rawKeys.map((key) => prefix === undefined ? key : `${prefix}.${key}`);
|
|
1456
|
+
|
|
1457
|
+
const keys = baseKeys.map((key) => context === undefined
|
|
1458
|
+
? key
|
|
1459
|
+
: `${key}${CONTEXT_SEPARATOR}${context}`);
|
|
1460
|
+
|
|
1461
|
+
const explicitNs = staticNamespaces(input.namespacePath, this.file)[0];
|
|
1462
|
+
|
|
1463
|
+
return { keys, ns: explicitNs ?? t?.info.defaultNs };
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private componentHasCount(input: ComponentInput): boolean {
|
|
1467
|
+
return input.countPath !== undefined ||
|
|
1468
|
+
objectProperty(input.optionsPath, "count") !== undefined ||
|
|
1469
|
+
objectProperty(input.valuesPath, "count") !== undefined;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private addPluralComponentMessages(
|
|
1473
|
+
input: ComponentInput,
|
|
1474
|
+
fallback: string | undefined,
|
|
1475
|
+
scope: ComponentScope,
|
|
1476
|
+
): void {
|
|
1477
|
+
if (input.isIcu) {
|
|
1478
|
+
this.errors.add(`ICU translations cannot infer a GNU gettext plural pair in ${this.file}`);
|
|
1479
|
+
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (this.hasUnsupportedPluralOptions(input.optionsPath)) {
|
|
1484
|
+
this.errors.add(`GNU gettext catalogs only support cardinal one/other source pairs in ${this.file}`);
|
|
1485
|
+
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const defaults = this.pluralDefaults(input.optionsPath, fallback);
|
|
1490
|
+
|
|
1491
|
+
if (defaults === undefined) {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
for (const key of scope.keys) {
|
|
1496
|
+
this.addPluralPair({ key, ns: scope.ns, path: input.path, ...defaults });
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
private pluralDefaults(
|
|
1501
|
+
options: NodePath | undefined,
|
|
1502
|
+
fallback: string | undefined,
|
|
1503
|
+
): Pick<PluralPairInput, "plural" | "singular"> | undefined {
|
|
1504
|
+
const singular = literalString(objectProperty(options, "defaultValue_one")) ?? fallback;
|
|
1505
|
+
const plural = literalString(objectProperty(options, "defaultValue_other"));
|
|
1506
|
+
|
|
1507
|
+
if (singular === undefined || plural === undefined || singular === plural) {
|
|
1508
|
+
this.errors.add(
|
|
1509
|
+
`Count-based Trans calls require distinct static singular and plural defaults in ${this.file}`,
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
return { plural, singular };
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
private hasUnsupportedPluralOptions(options: NodePath | undefined): boolean {
|
|
1519
|
+
return objectProperty(options, "ordinal") !== undefined ||
|
|
1520
|
+
objectProperty(options, "defaultValue_zero") !== undefined;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private addPluralPair(input: PluralPairInput): void {
|
|
1524
|
+
this.addMessage({
|
|
1525
|
+
defaultValue: input.singular,
|
|
1526
|
+
hasExplicitDefault: true,
|
|
1527
|
+
key: `${input.key}_one`,
|
|
1528
|
+
ns: input.ns,
|
|
1529
|
+
path: input.path,
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
this.addMessage({
|
|
1533
|
+
defaultValue: input.plural,
|
|
1534
|
+
hasExplicitDefault: true,
|
|
1535
|
+
key: `${input.key}_other`,
|
|
1536
|
+
ns: input.ns,
|
|
1537
|
+
path: input.path,
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
private addPointComponentMessages(
|
|
1542
|
+
input: ComponentInput,
|
|
1543
|
+
componentKeys: ComponentKeys,
|
|
1544
|
+
scope: ComponentScope,
|
|
1545
|
+
): void {
|
|
1546
|
+
for (const [index, key] of scope.keys.entries()) {
|
|
1547
|
+
const defaultValue = componentKeys.fallback ?? componentKeys.rawKeys[index] ?? key;
|
|
1548
|
+
|
|
1549
|
+
this.addMessage({
|
|
1550
|
+
defaultValue,
|
|
1551
|
+
hasExplicitDefault: componentKeys.fallback !== undefined,
|
|
1552
|
+
key,
|
|
1553
|
+
ns: scope.ns,
|
|
1554
|
+
path: input.path,
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
private addMessage(input: MessageInput): void {
|
|
1560
|
+
this.messages.push({
|
|
1561
|
+
defaultValue: input.defaultValue,
|
|
1562
|
+
explicitDefault: input.hasExplicitDefault,
|
|
1563
|
+
key: input.key,
|
|
1564
|
+
location: pathLocation(input.path, this.file),
|
|
1565
|
+
...(input.ns !== undefined && { ns: input.ns }),
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
private markTranslationCallback(callback: NodePath | undefined, info: ScopeInfo): void {
|
|
1570
|
+
const fn = this.resolveFunction(callback);
|
|
1571
|
+
|
|
1572
|
+
if (fn !== undefined && !fn.isClass()) {
|
|
1573
|
+
this.registerTParameter(fn, 0, { fixed: true, info, kind: "t" });
|
|
1574
|
+
this.registerTranslationContextParameter(fn);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
private registerTranslationContextParameter(fn: NodePath): void {
|
|
1579
|
+
const parameter = childPaths(fn, "params")[1];
|
|
1580
|
+
|
|
1581
|
+
if (parameter !== undefined) {
|
|
1582
|
+
this.registerSemanticPattern(parameter, { kind: "translationContext" });
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
private registerCallSemantics(path: NodePath): void {
|
|
1587
|
+
const callee = oneChild(path, "callee");
|
|
1588
|
+
const called = callee === undefined ? undefined : this.resolvePath(callee);
|
|
1589
|
+
this.registerHocCall(path, called);
|
|
1590
|
+
this.registerTranslationCallbackCall(path, called);
|
|
1591
|
+
this.registerInitCallback(path, called);
|
|
1592
|
+
this.registerInitPromiseCallback(path, callee);
|
|
1593
|
+
this.registerCallableComponent(path, called);
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
private registerHocCall(path: NodePath, called: Semantic | undefined): void {
|
|
1597
|
+
if (called?.kind === "enhancer") {
|
|
1598
|
+
this.registerHocComponent(callArguments(path)[0], called.info);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
private registerTranslationCallbackCall(path: NodePath, called: Semantic | undefined): void {
|
|
1603
|
+
if (called?.kind !== "api" || called.api !== "Translation") {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const props = callArguments(path)[0];
|
|
1608
|
+
|
|
1609
|
+
const info = normalizedInfo(
|
|
1610
|
+
staticNamespaces(objectProperty(props, "ns"), this.file),
|
|
1611
|
+
staticKeyPrefix(objectProperty(props, "keyPrefix"), this.file),
|
|
1612
|
+
);
|
|
1613
|
+
|
|
1614
|
+
this.markTranslationCallback(objectProperty(props, "children"), info);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
private registerInitCallback(path: NodePath, called: Semantic | undefined): void {
|
|
1618
|
+
if (called?.kind !== "api" || called.api !== "init") {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const callback = callArguments(path).findLast((argument) => this.resolveFunction(argument) !== undefined);
|
|
1623
|
+
const fn = this.resolveFunction(callback);
|
|
1624
|
+
|
|
1625
|
+
if (fn !== undefined && !fn.isClass()) {
|
|
1626
|
+
this.registerTParameter(fn, 1, GLOBAL_T);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
private registerInitPromiseCallback(path: NodePath, callee: NodePath | undefined): void {
|
|
1631
|
+
if (callee === undefined) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (memberProperty(callee) !== "then") {
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const receiver = oneChild(unwrapPath(callee), "object");
|
|
1640
|
+
const received = receiver === undefined ? undefined : this.resolvePath(receiver);
|
|
1641
|
+
|
|
1642
|
+
if (received?.kind !== "initPromise") {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
this.registerInitPromiseFunction(this.resolveFunction(callArguments(path)[0]));
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
private registerInitPromiseFunction(fn: NodePath | undefined): void {
|
|
1650
|
+
if (fn === undefined || fn.isClass()) {
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
this.registerTParameter(fn, 0, GLOBAL_T);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
private registerCallableComponent(path: NodePath, called: Semantic | undefined): void {
|
|
1658
|
+
if (called?.kind !== "api") {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
if (["IcuTrans", "IcuTransWithoutContext"].includes(called.api)) {
|
|
1663
|
+
this.addComponentMessage(this.callableIcuInput(path));
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if (["Trans", "TransWithoutContext"].includes(called.api)) {
|
|
1667
|
+
this.addComponentMessage(this.callableTransInput(path));
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
private callableIcuInput(path: NodePath): ComponentInput {
|
|
1672
|
+
const props = callArguments(path)[0];
|
|
1673
|
+
|
|
1674
|
+
return {
|
|
1675
|
+
defaultPath: objectProperty(props, "defaultTranslation"),
|
|
1676
|
+
isIcu: true,
|
|
1677
|
+
keyPath: objectProperty(props, "i18nKey"),
|
|
1678
|
+
namespacePath: objectProperty(props, "ns"),
|
|
1679
|
+
path,
|
|
1680
|
+
tPath: objectProperty(props, "t"),
|
|
1681
|
+
valuesPath: objectProperty(props, "values"),
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
private callableTransInput(path: NodePath): ComponentInput {
|
|
1686
|
+
const props = callArguments(path)[0];
|
|
1687
|
+
|
|
1688
|
+
return {
|
|
1689
|
+
contextPath: objectProperty(props, "context"),
|
|
1690
|
+
countPath: objectProperty(props, "count"),
|
|
1691
|
+
defaultPath: objectProperty(props, "defaults") ?? objectProperty(props, "children"),
|
|
1692
|
+
isIcu: false,
|
|
1693
|
+
keyPath: objectProperty(props, "i18nKey"),
|
|
1694
|
+
namespacePath: objectProperty(props, "ns"),
|
|
1695
|
+
optionsPath: objectProperty(props, "tOptions"),
|
|
1696
|
+
path,
|
|
1697
|
+
tPath: objectProperty(props, "t"),
|
|
1698
|
+
valuesPath: objectProperty(props, "values"),
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
private registerBlockedGetFixedT(path: NodePath): void {
|
|
1703
|
+
const callee = this.possibleGetFixedCallee(path);
|
|
1704
|
+
|
|
1705
|
+
if (callee === undefined) {
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
const semantic = this.resolvePath(callee);
|
|
1710
|
+
|
|
1711
|
+
if (this.isGetFixedApi(semantic)) {
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const key = spanKey(path.node);
|
|
1716
|
+
|
|
1717
|
+
if (key !== undefined) {
|
|
1718
|
+
this.blockedGetFixedT.add(key);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
private possibleGetFixedCallee(path: NodePath): NodePath | undefined {
|
|
1723
|
+
const init = oneChild(path, "init");
|
|
1724
|
+
|
|
1725
|
+
if (!init?.isCallExpression()) {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const callee = oneChild(init, "callee");
|
|
1730
|
+
|
|
1731
|
+
return callee !== undefined && memberProperty(callee) === "getFixedT"
|
|
1732
|
+
? callee
|
|
1733
|
+
: undefined;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
private isGetFixedApi(semantic: Semantic | undefined): boolean {
|
|
1737
|
+
return semantic?.kind === "api" && semantic.api === "getFixedT";
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private registerJsxElement(path: NodePath): void {
|
|
1741
|
+
const opening = oneChild(path, "openingElement");
|
|
1742
|
+
const name = opening === undefined ? undefined : oneChild(opening, "name");
|
|
1743
|
+
const component = name === undefined ? undefined : this.jsxSemantic(name);
|
|
1744
|
+
|
|
1745
|
+
if (opening === undefined || component?.kind !== "api") {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
this.registerApiJsx(path, opening, component.api);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
private registerApiJsx(path: NodePath, opening: NodePath, api: ApiName): void {
|
|
1753
|
+
if (["Trans", "TransWithoutContext"].includes(api)) {
|
|
1754
|
+
this.registerTransJsx(path, opening);
|
|
1755
|
+
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
if (api === "Translation") {
|
|
1760
|
+
this.registerTranslationJsx(path);
|
|
1761
|
+
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
if (["IcuTrans", "IcuTransWithoutContext"].includes(api)) {
|
|
1766
|
+
this.addComponentMessage(this.jsxIcuInput(path));
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
private registerTransJsx(path: NodePath, opening: NodePath): void {
|
|
1771
|
+
if (!this.validateTransJsxProps(path, opening)) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
const key = spanKey(path.node);
|
|
1776
|
+
|
|
1777
|
+
if (key === undefined) {
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const scope = this.transJsxScope(path);
|
|
1782
|
+
|
|
1783
|
+
if (!scope.isValid) {
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
this.trans.set(key, scope.value ?? null);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
private validateTransJsxProps(path: NodePath, opening: NodePath): boolean {
|
|
1791
|
+
const hasSpread = childPaths(opening, "attributes").some((attribute) => attribute.isJSXSpreadAttribute());
|
|
1792
|
+
|
|
1793
|
+
if (hasSpread) {
|
|
1794
|
+
this.errors.add(`Trans JSX props must be written explicitly for static extraction in ${this.file}`);
|
|
1795
|
+
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
if (isStaticStringArray(this.jsxAttribute(path, "i18nKey"))) {
|
|
1800
|
+
this.errors.add(`Trans JSX i18nKey arrays are not supported by static extraction in ${this.file}`);
|
|
1801
|
+
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
return true;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
private transJsxScope(path: NodePath): Resolution<ScopeInfo> {
|
|
1809
|
+
const tProp = this.jsxAttribute(path, "t");
|
|
1810
|
+
|
|
1811
|
+
if (tProp === undefined) {
|
|
1812
|
+
return { isValid: true };
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const semantic = this.resolvePath(tProp);
|
|
1816
|
+
|
|
1817
|
+
if (semantic?.kind !== "t") {
|
|
1818
|
+
this.errors.add(`Trans t props must have a statically known scope in ${this.file}`);
|
|
1819
|
+
|
|
1820
|
+
return { isValid: false };
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
if (!this.validatePrefixedTransNamespace(path, semantic)) {
|
|
1824
|
+
return { isValid: false };
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
return { isValid: true, value: semantic.info };
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
private validatePrefixedTransNamespace(path: NodePath, t: TSemantic): boolean {
|
|
1831
|
+
if (t.info.keyPrefix === undefined || this.jsxAttribute(path, "ns") === undefined) {
|
|
1832
|
+
return true;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
this.errors.add(`Prefixed Trans components cannot also override ns during static extraction in ${this.file}`);
|
|
1836
|
+
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
private registerTranslationJsx(path: NodePath): void {
|
|
1841
|
+
const info = this.scopeFromJsx(path);
|
|
1842
|
+
this.markTranslationCallback(this.jsxAttribute(path, "children"), info);
|
|
1843
|
+
|
|
1844
|
+
for (const child of childPaths(path, "children")) {
|
|
1845
|
+
if (child.isJSXExpressionContainer()) {
|
|
1846
|
+
this.markTranslationCallback(oneChild(child, "expression"), info);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
private jsxIcuInput(path: NodePath): ComponentInput {
|
|
1852
|
+
return {
|
|
1853
|
+
defaultPath: this.jsxAttribute(path, "defaultTranslation"),
|
|
1854
|
+
isIcu: true,
|
|
1855
|
+
keyPath: this.jsxAttribute(path, "i18nKey"),
|
|
1856
|
+
namespacePath: this.jsxAttribute(path, "ns"),
|
|
1857
|
+
path,
|
|
1858
|
+
tPath: this.jsxAttribute(path, "t"),
|
|
1859
|
+
valuesPath: this.jsxAttribute(path, "values"),
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
private classT(callee: NodePath): TSemantic | undefined {
|
|
1864
|
+
if (memberProperty(callee) !== "t") {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const object = oneChild(unwrapPath(callee), "object");
|
|
1869
|
+
|
|
1870
|
+
if (object === undefined || memberProperty(object) !== "props") {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
const receiver = oneChild(unwrapPath(object), "object");
|
|
1875
|
+
|
|
1876
|
+
if (receiver?.isThisExpression() !== true) {
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
const owner = callee.findParent(isClassPath);
|
|
1881
|
+
|
|
1882
|
+
return owner === null ? undefined : this.hocClasses.get(owner.node);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
private optionsKeyPrefix(call: NodePath): NodePath | undefined {
|
|
1886
|
+
for (const argument of callArguments(call).slice(1)) {
|
|
1887
|
+
const keyPrefix = objectProperty(argument, "keyPrefix");
|
|
1888
|
+
|
|
1889
|
+
if (keyPrefix !== undefined) {
|
|
1890
|
+
return keyPrefix;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
return undefined;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
private registerTranslationCall(path: NodePath): void {
|
|
1898
|
+
const callee = oneChild(path, "callee");
|
|
1899
|
+
|
|
1900
|
+
if (callee === undefined) {
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
const resolved = this.resolvePath(callee);
|
|
1905
|
+
const t = resolved?.kind === "t" ? resolved : this.classT(callee);
|
|
1906
|
+
const key = spanKey(path.node);
|
|
1907
|
+
|
|
1908
|
+
if (t === undefined || key === undefined) {
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const info = this.translationCallScope(path, t);
|
|
1913
|
+
|
|
1914
|
+
if (info !== undefined) {
|
|
1915
|
+
this.calls.set(key, { info, key });
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
private translationCallScope(path: NodePath, t: TSemantic): ScopeInfo | undefined {
|
|
1920
|
+
const keyPrefix = this.optionsKeyPrefix(path);
|
|
1921
|
+
|
|
1922
|
+
if (keyPrefix === undefined) {
|
|
1923
|
+
return t.info;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
const literalPrefix = literalString(keyPrefix);
|
|
1927
|
+
|
|
1928
|
+
if (literalPrefix === undefined) {
|
|
1929
|
+
this.errors.add(`Translation keyPrefix options must be static strings in ${this.file}`);
|
|
1930
|
+
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
if (!t.fixed) {
|
|
1935
|
+
this.errors.add(`keyPrefix options require a fixed translation function in ${this.file}`);
|
|
1936
|
+
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
return scopeWithPrefix(t.info, literalPrefix);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
private registerTaggedTranslation(path: NodePath): void {
|
|
1944
|
+
const tag = oneChild(path, "tag");
|
|
1945
|
+
const resolved = tag === undefined ? undefined : this.resolvePath(tag);
|
|
1946
|
+
|
|
1947
|
+
if (resolved?.kind === "t") {
|
|
1948
|
+
this.errors.add(
|
|
1949
|
+
`Tagged-template translations are not compatible with strict GTKX codegen in ${this.file}`,
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
analyze(program: NodePath): BindingExtraction {
|
|
1955
|
+
this.runSemanticPass(program);
|
|
1956
|
+
this.bindingCache.clear();
|
|
1957
|
+
this.runTranslationPass(program);
|
|
1958
|
+
|
|
1959
|
+
return {
|
|
1960
|
+
blockedGetFixedT: this.blockedGetFixedT,
|
|
1961
|
+
calls: this.calls,
|
|
1962
|
+
errors: [...this.errors],
|
|
1963
|
+
messages: this.messages,
|
|
1964
|
+
trans: this.trans,
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
export { type BindingExtraction, type ScopeInfo, extractBindings };
|