@gtkx/cli 1.6.0 → 2.0.0-beta.2
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 -5
- package/dist/builder.d.ts +1 -0
- package/dist/builder.d.ts.map +1 -1
- package/dist/builder.js +17 -5
- package/dist/builder.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/codegen/freshness.d.ts.map +1 -1
- package/dist/codegen/freshness.js +45 -16
- package/dist/codegen/freshness.js.map +1 -1
- package/dist/codegen/reference.d.ts.map +1 -1
- package/dist/codegen/reference.js +1 -7
- package/dist/codegen/reference.js.map +1 -1
- package/dist/codegen/report.d.ts.map +1 -1
- package/dist/codegen/report.js +5 -16
- package/dist/codegen/report.js.map +1 -1
- package/dist/codegen/run-codegen.d.ts +7 -7
- package/dist/codegen/run-codegen.d.ts.map +1 -1
- package/dist/codegen/run-codegen.js +35 -107
- package/dist/codegen/run-codegen.js.map +1 -1
- package/dist/codegen/store-resolver.d.ts +1 -2
- package/dist/codegen/store-resolver.d.ts.map +1 -1
- package/dist/codegen/store-resolver.js +10 -19
- package/dist/codegen/store-resolver.js.map +1 -1
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +83 -10
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/codegen.d.ts +1 -0
- package/dist/commands/codegen.d.ts.map +1 -1
- package/dist/commands/codegen.js +6 -5
- package/dist/commands/codegen.js.map +1 -1
- package/dist/commands/deploy.d.ts +1 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +4 -2
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +8 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +19 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +5 -15
- package/dist/commands/docs.js.map +1 -1
- package/dist/deploy/freedesktop/copyright.d.ts.map +1 -1
- package/dist/deploy/freedesktop/copyright.js +2 -13
- package/dist/deploy/freedesktop/copyright.js.map +1 -1
- package/dist/deploy/freedesktop/metainfo.d.ts.map +1 -1
- package/dist/deploy/freedesktop/metainfo.js +41 -2
- package/dist/deploy/freedesktop/metainfo.js.map +1 -1
- package/dist/deploy/freedesktop/xml.d.ts.map +1 -1
- package/dist/deploy/freedesktop/xml.js +21 -25
- package/dist/deploy/freedesktop/xml.js.map +1 -1
- package/dist/deploy/nfpm/config.d.ts.map +1 -1
- package/dist/deploy/nfpm/config.js +5 -2
- package/dist/deploy/nfpm/config.js.map +1 -1
- package/dist/deploy/nfpm/contents.d.ts.map +1 -1
- package/dist/deploy/nfpm/contents.js +8 -0
- package/dist/deploy/nfpm/contents.js.map +1 -1
- package/dist/deploy/node-runtime/elf.d.ts +2 -1
- package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
- package/dist/deploy/node-runtime/elf.js +32 -5
- package/dist/deploy/node-runtime/elf.js.map +1 -1
- package/dist/deploy/node-runtime/index.d.ts +2 -1
- package/dist/deploy/node-runtime/index.d.ts.map +1 -1
- package/dist/deploy/node-runtime/index.js +73 -4
- package/dist/deploy/node-runtime/index.js.map +1 -1
- package/dist/deploy/node-runtime/license.d.ts.map +1 -1
- package/dist/deploy/node-runtime/license.js +2 -9
- package/dist/deploy/node-runtime/license.js.map +1 -1
- package/dist/deploy/notices/node-runtime.js +2 -2
- package/dist/deploy/notices/node-runtime.js.map +1 -1
- package/dist/deploy/notices/packages.d.ts.map +1 -1
- package/dist/deploy/notices/packages.js +3 -10
- package/dist/deploy/notices/packages.js.map +1 -1
- package/dist/deploy/payload/copy-tree.d.ts.map +1 -1
- package/dist/deploy/payload/copy-tree.js +3 -3
- package/dist/deploy/payload/copy-tree.js.map +1 -1
- package/dist/deploy/payload/launcher.d.ts.map +1 -1
- package/dist/deploy/payload/launcher.js +14 -1
- package/dist/deploy/payload/launcher.js.map +1 -1
- package/dist/deploy/payload/stage.d.ts.map +1 -1
- package/dist/deploy/payload/stage.js +2 -4
- package/dist/deploy/payload/stage.js.map +1 -1
- package/dist/deploy/run-deploy.d.ts +1 -0
- package/dist/deploy/run-deploy.d.ts.map +1 -1
- package/dist/deploy/run-deploy.js +100 -14
- package/dist/deploy/run-deploy.js.map +1 -1
- package/dist/deploy/settings/build-manifest.d.ts +6 -1
- package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
- package/dist/deploy/settings/build-manifest.js +17 -17
- package/dist/deploy/settings/build-manifest.js.map +1 -1
- package/dist/deploy/settings/extra-files.d.ts +1 -1
- package/dist/deploy/settings/extra-files.d.ts.map +1 -1
- package/dist/deploy/settings/extra-files.js +22 -1
- package/dist/deploy/settings/extra-files.js.map +1 -1
- package/dist/deploy/settings/index.d.ts +1 -0
- package/dist/deploy/settings/index.d.ts.map +1 -1
- package/dist/deploy/settings/index.js +2 -1
- package/dist/deploy/settings/index.js.map +1 -1
- package/dist/deploy/settings/libraries.d.ts.map +1 -1
- package/dist/deploy/settings/libraries.js +2 -19
- package/dist/deploy/settings/libraries.js.map +1 -1
- package/dist/deploy/settings/paths.d.ts +2 -2
- package/dist/deploy/settings/paths.d.ts.map +1 -1
- package/dist/deploy/settings/paths.js +110 -12
- package/dist/deploy/settings/paths.js.map +1 -1
- package/dist/deploy/settings/screenshots.d.ts.map +1 -1
- package/dist/deploy/settings/screenshots.js +6 -31
- package/dist/deploy/settings/screenshots.js.map +1 -1
- package/dist/deploy/targets/flatpak-pnpm.js +4 -4
- package/dist/deploy/targets/flatpak-pnpm.js.map +1 -1
- package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
- package/dist/deploy/targets/flatpak-source.js +15 -6
- package/dist/deploy/targets/flatpak-source.js.map +1 -1
- package/dist/deploy/targets/flatpak-sources.js +1 -1
- package/dist/deploy/targets/flatpak-sources.js.map +1 -1
- package/dist/deploy/types.d.ts +1 -0
- package/dist/deploy/types.d.ts.map +1 -1
- package/dist/deploy/types.js.map +1 -1
- package/dist/dev/headless.d.ts +3 -0
- package/dist/dev/headless.d.ts.map +1 -0
- package/dist/dev/headless.js +35 -0
- package/dist/dev/headless.js.map +1 -0
- package/dist/dev/runner-deps.js +1 -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 +1 -2
- package/dist/dev/runner-main.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 +2 -4
- package/dist/dev/schema-dir.js.map +1 -1
- package/dist/i18n/catalogs.d.ts.map +1 -1
- package/dist/i18n/catalogs.js +2 -1
- package/dist/i18n/catalogs.js.map +1 -1
- package/dist/i18n/metadata-templates.d.ts.map +1 -1
- package/dist/i18n/metadata-templates.js +2 -1
- package/dist/i18n/metadata-templates.js.map +1 -1
- package/dist/i18n/source-messages.d.ts +3 -21
- package/dist/i18n/source-messages.d.ts.map +1 -1
- package/dist/i18n/source-messages.js +900 -85
- package/dist/i18n/source-messages.js.map +1 -1
- package/dist/i18n/types.d.ts +6 -7
- package/dist/i18n/types.d.ts.map +1 -1
- package/dist/i18n/types.js +83 -96
- package/dist/i18n/types.js.map +1 -1
- package/dist/internal/build-manifest.d.ts +5 -2
- package/dist/internal/build-manifest.d.ts.map +1 -1
- package/dist/internal/build-manifest.js +15 -2
- package/dist/internal/build-manifest.js.map +1 -1
- package/dist/internal/build-output.d.ts +8 -0
- package/dist/internal/build-output.d.ts.map +1 -0
- package/dist/internal/build-output.js +69 -0
- package/dist/internal/build-output.js.map +1 -0
- package/dist/internal/docs-elements.js +2 -2
- package/dist/internal/docs-elements.js.map +1 -1
- package/dist/internal/entry-arg.d.ts +4 -1
- package/dist/internal/entry-arg.d.ts.map +1 -1
- package/dist/internal/entry-arg.js +7 -1
- package/dist/internal/entry-arg.js.map +1 -1
- package/dist/internal/icon-path.d.ts.map +1 -1
- package/dist/internal/icon-path.js +5 -16
- package/dist/internal/icon-path.js.map +1 -1
- package/dist/internal/output-directory.d.ts +15 -0
- package/dist/internal/output-directory.d.ts.map +1 -0
- package/dist/internal/output-directory.js +490 -0
- package/dist/internal/output-directory.js.map +1 -0
- package/dist/internal/prepare-project.d.ts +2 -0
- package/dist/internal/prepare-project.d.ts.map +1 -1
- package/dist/internal/prepare-project.js +3 -3
- package/dist/internal/prepare-project.js.map +1 -1
- package/dist/internal/source-imports.d.ts.map +1 -1
- package/dist/internal/source-imports.js +10 -41
- package/dist/internal/source-imports.js.map +1 -1
- package/dist/mcp/client.js +2 -2
- package/dist/mcp/client.js.map +1 -1
- package/dist/settings/render.d.ts +0 -1
- package/dist/settings/render.d.ts.map +1 -1
- package/dist/settings/render.js +1 -11
- package/dist/settings/render.js.map +1 -1
- package/dist/settings/schema.d.ts +2 -2
- package/dist/settings/schema.d.ts.map +1 -1
- package/dist/settings/schema.js +21 -62
- package/dist/settings/schema.js.map +1 -1
- package/dist/vite-plugins/asset-imports.d.ts +1 -2
- package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
- package/dist/vite-plugins/asset-imports.js +19 -32
- package/dist/vite-plugins/asset-imports.js.map +1 -1
- package/dist/vite-plugins/asset-specifier.d.ts +1 -3
- package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
- package/dist/vite-plugins/asset-specifier.js +1 -4
- package/dist/vite-plugins/asset-specifier.js.map +1 -1
- package/dist/vite-plugins/build-manifest.d.ts +5 -1
- package/dist/vite-plugins/build-manifest.d.ts.map +1 -1
- package/dist/vite-plugins/build-manifest.js +2 -1
- package/dist/vite-plugins/build-manifest.js.map +1 -1
- package/dist/vite-plugins/i18n.d.ts.map +1 -1
- package/dist/vite-plugins/i18n.js +59 -6
- package/dist/vite-plugins/i18n.js.map +1 -1
- package/dist/vite-plugins/index.d.ts +8 -1
- package/dist/vite-plugins/index.d.ts.map +1 -1
- package/dist/vite-plugins/index.js +8 -6
- package/dist/vite-plugins/index.js.map +1 -1
- package/dist/vite-plugins/resources.d.ts.map +1 -1
- package/dist/vite-plugins/resources.js +32 -97
- 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 +159 -158
- 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 +2 -4
- package/dist/vite-plugins/settings-worker-env.js.map +1 -1
- package/dist/vite-plugins/settings.d.ts +1 -2
- package/dist/vite-plugins/settings.d.ts.map +1 -1
- package/dist/vite-plugins/settings.js +8 -14
- package/dist/vite-plugins/settings.js.map +1 -1
- package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
- package/dist/vite-plugins/undeclared-library.js +10 -13
- package/dist/vite-plugins/undeclared-library.js.map +1 -1
- package/dist/vite-plugins/worker.d.ts.map +1 -1
- package/dist/vite-plugins/worker.js +0 -1
- package/dist/vite-plugins/worker.js.map +1 -1
- package/package.json +21 -15
- package/src/builder.ts +18 -5
- package/src/cli.ts +3 -0
- package/src/codegen/freshness.ts +66 -17
- package/src/codegen/reference.ts +1 -8
- package/src/codegen/report.ts +5 -21
- package/src/codegen/run-codegen.ts +42 -151
- package/src/codegen/store-resolver.ts +10 -26
- package/src/commands/build.ts +13 -2
- package/src/commands/codegen.ts +6 -5
- package/src/commands/deploy.ts +4 -2
- package/src/commands/dev.ts +20 -1
- package/src/commands/docs.ts +5 -17
- package/src/deploy/freedesktop/copyright.ts +2 -14
- package/src/deploy/freedesktop/metainfo.ts +73 -21
- package/src/deploy/freedesktop/xml.ts +24 -32
- package/src/deploy/nfpm/config.ts +5 -2
- package/src/deploy/nfpm/contents.ts +8 -0
- package/src/deploy/node-runtime/elf.ts +43 -5
- package/src/deploy/node-runtime/index.ts +105 -5
- package/src/deploy/node-runtime/license.ts +2 -8
- package/src/deploy/notices/node-runtime.ts +2 -2
- package/src/deploy/notices/packages.ts +3 -11
- package/src/deploy/payload/copy-tree.ts +3 -4
- package/src/deploy/payload/launcher.ts +20 -1
- package/src/deploy/payload/stage.ts +3 -7
- package/src/deploy/run-deploy.ts +56 -16
- package/src/deploy/settings/build-manifest.ts +30 -17
- package/src/deploy/settings/extra-files.ts +26 -2
- package/src/deploy/settings/index.ts +3 -1
- package/src/deploy/settings/libraries.ts +1 -22
- package/src/deploy/settings/paths.ts +68 -13
- package/src/deploy/settings/screenshots.ts +8 -46
- package/src/deploy/targets/flatpak-pnpm.ts +4 -4
- package/src/deploy/targets/flatpak-source.ts +17 -6
- package/src/deploy/targets/flatpak-sources.ts +1 -1
- package/src/deploy/types.ts +1 -0
- package/src/dev/headless.ts +40 -0
- package/src/dev/runner-deps.ts +1 -1
- package/src/dev/runner-main.ts +1 -2
- package/src/dev/schema-dir.ts +2 -4
- package/src/i18n/catalogs.ts +2 -1
- package/src/i18n/metadata-templates.ts +2 -1
- package/src/i18n/source-messages.ts +1442 -262
- package/src/i18n/types.ts +97 -131
- package/src/internal/build-manifest.ts +25 -1
- package/src/internal/build-output.ts +99 -0
- package/src/internal/docs-elements.ts +2 -2
- package/src/internal/entry-arg.ts +8 -1
- package/src/internal/icon-path.ts +6 -19
- package/src/internal/output-directory.ts +699 -0
- package/src/internal/prepare-project.ts +5 -5
- package/src/internal/source-imports.ts +14 -50
- package/src/mcp/client.ts +2 -2
- package/src/settings/render.ts +1 -13
- package/src/settings/schema.ts +19 -81
- package/src/vite-plugins/asset-imports.ts +20 -44
- package/src/vite-plugins/asset-specifier.ts +0 -5
- package/src/vite-plugins/build-manifest.ts +7 -1
- package/src/vite-plugins/i18n.ts +87 -5
- package/src/vite-plugins/index.ts +16 -11
- package/src/vite-plugins/resources.ts +37 -140
- package/src/vite-plugins/self-contained.ts +184 -185
- package/src/vite-plugins/settings-worker-env.ts +2 -4
- package/src/vite-plugins/settings.ts +11 -20
- package/src/vite-plugins/undeclared-library.ts +14 -20
- package/src/vite-plugins/worker.ts +0 -1
- package/dist/i18n/binding-extractor.d.ts +0 -30
- package/dist/i18n/binding-extractor.d.ts.map +0 -1
- package/dist/i18n/binding-extractor.js +0 -1324
- package/dist/i18n/binding-extractor.js.map +0 -1
- package/dist/i18n/extractor-plugin.d.ts +0 -8
- package/dist/i18n/extractor-plugin.d.ts.map +0 -1
- package/dist/i18n/extractor-plugin.js +0 -247
- package/dist/i18n/extractor-plugin.js.map +0 -1
- package/dist/internal/data-dir.d.ts +0 -4
- package/dist/internal/data-dir.d.ts.map +0 -1
- package/dist/internal/data-dir.js +0 -52
- package/dist/internal/data-dir.js.map +0 -1
- package/dist/vite-plugins/animated.d.ts +0 -5
- package/dist/vite-plugins/animated.d.ts.map +0 -1
- package/dist/vite-plugins/animated.js +0 -325
- package/dist/vite-plugins/animated.js.map +0 -1
- package/src/i18n/binding-extractor.ts +0 -1969
- package/src/i18n/extractor-plugin.ts +0 -413
- package/src/internal/data-dir.ts +0 -70
- package/src/vite-plugins/animated.ts +0 -486
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import type { ExtractedKey,
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtractedKey, ExtractedKeysMap, Logger, Plugin } from "i18next-cli";
|
|
2
|
+
import type { ESTree } from "vite";
|
|
3
|
+
import { type NodePath, parseSync as parseBabelSync, type Scope, traverse, types } from "@babel/core";
|
|
4
|
+
import { isPathInside, isPathWithin, toPosixPath } from "@gtkx/utils";
|
|
5
|
+
import { runExtractor } from "i18next-cli";
|
|
3
6
|
import {
|
|
4
7
|
existsSync,
|
|
5
8
|
mkdirSync,
|
|
@@ -8,32 +11,29 @@ import {
|
|
|
8
11
|
rmSync,
|
|
9
12
|
writeFileSync,
|
|
10
13
|
} from "node:fs";
|
|
11
|
-
import { dirname, isAbsolute, join, relative, resolve
|
|
14
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
15
|
+
import { parseSync, Visitor } from "vite";
|
|
12
16
|
import type { CatalogProject } from "./catalogs.js";
|
|
13
17
|
import { runCliTool } from "../internal/run-cli-tool.js";
|
|
18
|
+
import { sourceLanguage } from "../internal/source-imports.js";
|
|
14
19
|
import { replaceCatalogTemplate } from "./catalog-template.js";
|
|
15
|
-
import { gtkxExtractorPlugin } from "./extractor-plugin.js";
|
|
16
20
|
import { metadataTemplateFiles } from "./metadata-templates.js";
|
|
21
|
+
import { clearI18nResources, i18nToolkitConfig } from "./types.js";
|
|
17
22
|
|
|
18
23
|
type SourceLocation = {
|
|
19
24
|
file: string;
|
|
20
25
|
line?: number | undefined;
|
|
21
26
|
column?: number | undefined;
|
|
22
27
|
};
|
|
28
|
+
type ExtractedLocation = NonNullable<ExtractedKey["locations"]>[number];
|
|
23
29
|
|
|
24
30
|
type SourceMessage = {
|
|
25
31
|
context: string | null;
|
|
26
|
-
kind: "default" | "plural" | "pluralDefaults" | "point";
|
|
27
32
|
locations: SourceLocation[];
|
|
28
|
-
msgid: string;
|
|
29
33
|
namespace: string;
|
|
30
34
|
plural: string | null;
|
|
31
35
|
singular: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
type SourceCatalog = {
|
|
35
|
-
messages: SourceMessage[];
|
|
36
|
-
sourceFiles: string[];
|
|
36
|
+
sourceKey: string;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
type SyntheticEntry = {
|
|
@@ -45,7 +45,6 @@ type SourceExtraction = {
|
|
|
45
45
|
messages: SourceMessage[];
|
|
46
46
|
output: string;
|
|
47
47
|
project: CatalogProject;
|
|
48
|
-
sourceFiles: string[];
|
|
49
48
|
workDir: string;
|
|
50
49
|
};
|
|
51
50
|
|
|
@@ -54,7 +53,6 @@ type CatalogTemplateExtraction = {
|
|
|
54
53
|
output?: string | undefined;
|
|
55
54
|
project: CatalogProject;
|
|
56
55
|
shouldPreserveMetadataMessages: boolean;
|
|
57
|
-
sourceFiles: string[];
|
|
58
56
|
};
|
|
59
57
|
|
|
60
58
|
type PluralVariant = {
|
|
@@ -69,10 +67,50 @@ type PluralGroup = {
|
|
|
69
67
|
variants: PluralVariant[];
|
|
70
68
|
};
|
|
71
69
|
|
|
70
|
+
type TranslationReferences = {
|
|
71
|
+
calls: Set<number>;
|
|
72
|
+
elements: Set<number>;
|
|
73
|
+
entryLocations: Map<string, SourcePoint[]>;
|
|
74
|
+
locations: Set<string>;
|
|
75
|
+
masks: SourceMask[];
|
|
76
|
+
};
|
|
77
|
+
type SourceMask = { end: number; start: number };
|
|
78
|
+
type SourcePoint = { column: number; line: number };
|
|
79
|
+
type TranslationCallKind = "alias" | "canonical";
|
|
80
|
+
type ImportedBinding = {
|
|
81
|
+
imported: string;
|
|
82
|
+
local: string;
|
|
83
|
+
source: string;
|
|
84
|
+
style: "default" | "named" | "namespace";
|
|
85
|
+
};
|
|
86
|
+
type TranslationHookBinding = { call: types.CallExpression; isCanonical: boolean };
|
|
87
|
+
type TranslationMemberExpression = types.MemberExpression | types.OptionalMemberExpression;
|
|
88
|
+
type ResolvedBinding = NonNullable<ReturnType<Scope["getBinding"]>>;
|
|
89
|
+
|
|
72
90
|
const CONTEXT_SEPARATOR = "\u{4}";
|
|
73
91
|
const DEFAULT_NAMESPACE = "translation";
|
|
74
92
|
const POTFILES_FILENAME = "POTFILES.in";
|
|
75
93
|
const SYNTHETIC_FILENAME = "messages.js";
|
|
94
|
+
const UNSUPPORTED_PLURAL_OPTION = /^defaultValue_(few|many|two|zero)$/u;
|
|
95
|
+
const STATIC_SOURCE_OPTION_NAMES: ReadonlySet<string> = new Set([
|
|
96
|
+
"context",
|
|
97
|
+
"defaultValue",
|
|
98
|
+
"defaultValue_one",
|
|
99
|
+
"defaultValue_other",
|
|
100
|
+
"keyPrefix",
|
|
101
|
+
]);
|
|
102
|
+
const NESTED_CONTEXT = /["']?context["']?\s*:\s*(?:"([^"]*)"|'([^']*)')/u;
|
|
103
|
+
const NESTED_COUNT = /["']?count["']?\s*:/u;
|
|
104
|
+
const NESTED_STRUCTURE_TOKEN = /"[^"]*"|'[^']*'|[()]/gu;
|
|
105
|
+
const TRANSLATION_MODULES = new Set(["@gtkx/i18n", "i18next"]);
|
|
106
|
+
const TRANS_ELEMENTS: ReadonlySet<string> = new Set(["Trans", "TransWithoutContext"]);
|
|
107
|
+
const STATIC_KEY_WRAPPER_TYPES: ReadonlySet<string> = new Set([
|
|
108
|
+
"ParenthesizedExpression",
|
|
109
|
+
"TSAsExpression",
|
|
110
|
+
"TSSatisfiesExpression",
|
|
111
|
+
"TSTypeAssertion",
|
|
112
|
+
"TSNonNullExpression",
|
|
113
|
+
]);
|
|
76
114
|
|
|
77
115
|
const quietLogger = (): { logger: Logger; reports: string[] } => {
|
|
78
116
|
const reports: string[] = [];
|
|
@@ -94,31 +132,6 @@ const quietLogger = (): { logger: Logger; reports: string[] } => {
|
|
|
94
132
|
};
|
|
95
133
|
};
|
|
96
134
|
|
|
97
|
-
const extractorConfig = (sourceFiles: string[]): I18nextToolkitConfig => {
|
|
98
|
-
const integration = gtkxExtractorPlugin();
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
locales: ["en"],
|
|
102
|
-
plugins: [integration.plugin],
|
|
103
|
-
extract: {
|
|
104
|
-
input: sourceFiles,
|
|
105
|
-
output: "unused/{{language}}/{{namespace}}.json",
|
|
106
|
-
contextSeparator: CONTEXT_SEPARATOR,
|
|
107
|
-
defaultNS: DEFAULT_NAMESPACE,
|
|
108
|
-
disablePlurals: false,
|
|
109
|
-
extractFromComments: false,
|
|
110
|
-
functions: [],
|
|
111
|
-
generateBasePluralForms: false,
|
|
112
|
-
keySeparator: ".",
|
|
113
|
-
nsSeparator: false,
|
|
114
|
-
pluralSeparator: "_",
|
|
115
|
-
transComponents: integration.transComponents,
|
|
116
|
-
useTranslationNames: [],
|
|
117
|
-
warnOnConflicts: "error",
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
|
|
122
135
|
const splitContext = (key: string): { context: string | null; msgid: string } => {
|
|
123
136
|
const index = key.lastIndexOf(CONTEXT_SEPARATOR);
|
|
124
137
|
|
|
@@ -155,17 +168,22 @@ const normalizedLocations = (locations: SourceLocation[]): SourceLocation[] => {
|
|
|
155
168
|
};
|
|
156
169
|
|
|
157
170
|
const pointSource = (entry: ExtractedKey, msgid: string): string => {
|
|
158
|
-
if (
|
|
171
|
+
if (typeof entry.defaultValue !== "string") {
|
|
159
172
|
return msgid;
|
|
160
173
|
}
|
|
161
174
|
|
|
162
|
-
|
|
175
|
+
if (entry.explicitDefault === true) {
|
|
176
|
+
return entry.defaultValue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return entry.defaultValue === msgid || msgid.endsWith(`.${entry.defaultValue}`)
|
|
180
|
+
? msgid
|
|
181
|
+
: entry.defaultValue;
|
|
163
182
|
};
|
|
164
183
|
|
|
165
184
|
const pointMessage = (entry: ExtractedKey): SourceMessage => {
|
|
166
185
|
const { context, msgid } = splitContext(entry.key);
|
|
167
186
|
validateIdentity(context, msgid);
|
|
168
|
-
const isDefaultExplicit = entry.explicitDefault === true;
|
|
169
187
|
const singular = pointSource(entry, msgid);
|
|
170
188
|
|
|
171
189
|
if (singular.length === 0) {
|
|
@@ -174,12 +192,11 @@ const pointMessage = (entry: ExtractedKey): SourceMessage => {
|
|
|
174
192
|
|
|
175
193
|
return {
|
|
176
194
|
context,
|
|
177
|
-
kind: isDefaultExplicit ? "default" : "point",
|
|
178
195
|
locations: normalizedLocations(entry.locations ?? []),
|
|
179
|
-
msgid,
|
|
180
196
|
namespace: namespaceFor(entry),
|
|
181
197
|
plural: null,
|
|
182
198
|
singular,
|
|
199
|
+
sourceKey: msgid,
|
|
183
200
|
};
|
|
184
201
|
};
|
|
185
202
|
|
|
@@ -242,8 +259,7 @@ const pluralMessage = ({ baseKey, namespace, variants }: PluralGroup): SourceMes
|
|
|
242
259
|
const { one, other } = pairedVariants(variants);
|
|
243
260
|
const { context, msgid } = splitContext(baseKey);
|
|
244
261
|
validateIdentity(context, msgid);
|
|
245
|
-
const
|
|
246
|
-
const singular = hasStableKey ? one.defaultValue : msgid;
|
|
262
|
+
const singular = one.defaultValue;
|
|
247
263
|
const plural = other.defaultValue;
|
|
248
264
|
|
|
249
265
|
if (singular === plural || singular.length === 0 || plural.length === 0) {
|
|
@@ -252,12 +268,11 @@ const pluralMessage = ({ baseKey, namespace, variants }: PluralGroup): SourceMes
|
|
|
252
268
|
|
|
253
269
|
return {
|
|
254
270
|
context,
|
|
255
|
-
kind: hasStableKey ? "pluralDefaults" : "plural",
|
|
256
271
|
locations: normalizedLocations(variants.flatMap((variant) => variant.locations)),
|
|
257
|
-
msgid,
|
|
258
272
|
namespace,
|
|
259
273
|
plural,
|
|
260
274
|
singular,
|
|
275
|
+
sourceKey: msgid,
|
|
261
276
|
};
|
|
262
277
|
};
|
|
263
278
|
|
|
@@ -295,314 +310,1479 @@ const sourceMessages = (entries: ExtractedKey[]): SourceMessage[] => {
|
|
|
295
310
|
const compareMessages = (left: SourceMessage, right: SourceMessage): number =>
|
|
296
311
|
left.namespace.localeCompare(right.namespace) ||
|
|
297
312
|
(left.context ?? "").localeCompare(right.context ?? "") ||
|
|
298
|
-
left.msgid.localeCompare(right.msgid) ||
|
|
299
313
|
left.singular.localeCompare(right.singular) ||
|
|
300
314
|
(left.plural ?? "").localeCompare(right.plural ?? "");
|
|
301
315
|
|
|
316
|
+
const inferLocations = (message: SourceMessage, sources: Map<string, string>): SourceMessage => {
|
|
317
|
+
if (message.locations.length > 0) {
|
|
318
|
+
return message;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const locations: SourceLocation[] = [];
|
|
322
|
+
|
|
323
|
+
for (const [file, source] of sources) {
|
|
324
|
+
const index = source.indexOf(message.sourceKey);
|
|
325
|
+
|
|
326
|
+
if (index !== -1) {
|
|
327
|
+
locations.push({ file, line: source.slice(0, index).split("\n").length });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { ...message, locations };
|
|
332
|
+
};
|
|
333
|
+
|
|
302
334
|
const normalizedSourceFiles = (root: string, paths: string[]): string[] => {
|
|
303
335
|
const files = paths
|
|
304
336
|
.map((path) => resolve(root, path))
|
|
305
|
-
.filter((path) =>
|
|
306
|
-
const projectPath = relative(root, path);
|
|
307
|
-
|
|
308
|
-
return projectPath !== ".." && !projectPath.startsWith(`..${sep}`) && !isAbsolute(projectPath);
|
|
309
|
-
});
|
|
337
|
+
.filter((path) => isPathWithin(root, path));
|
|
310
338
|
|
|
311
339
|
return new Set(files).values().toArray().toSorted((left, right) => left.localeCompare(right));
|
|
312
340
|
};
|
|
313
341
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
342
|
+
type StaticKeyWrapper = ESTree.ParenthesizedExpression |
|
|
343
|
+
ESTree.TSAsExpression |
|
|
344
|
+
ESTree.TSSatisfiesExpression |
|
|
345
|
+
ESTree.TSTypeAssertion |
|
|
346
|
+
ESTree.TSNonNullExpression;
|
|
347
|
+
|
|
348
|
+
const isStaticKeyWrapper = (key: ESTree.Node): key is StaticKeyWrapper =>
|
|
349
|
+
STATIC_KEY_WRAPPER_TYPES.has(key.type);
|
|
350
|
+
|
|
351
|
+
const unwrapStaticWrapper = (node: ESTree.Node): ESTree.Node =>
|
|
352
|
+
isStaticKeyWrapper(node) ? unwrapStaticWrapper(node.expression) : node;
|
|
353
|
+
|
|
354
|
+
const isStaticTranslationKey = (key: ESTree.Node | null | undefined): boolean => {
|
|
355
|
+
if (key === undefined || key === null) {
|
|
356
|
+
return false;
|
|
317
357
|
}
|
|
318
358
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
359
|
+
if (key.type === "Literal") {
|
|
360
|
+
return typeof key.value === "string";
|
|
361
|
+
}
|
|
322
362
|
|
|
323
|
-
if (
|
|
324
|
-
|
|
363
|
+
if (key.type === "TemplateLiteral") {
|
|
364
|
+
return key.expressions.length === 0;
|
|
325
365
|
}
|
|
326
366
|
|
|
327
|
-
|
|
367
|
+
if (key.type === "ParenthesizedExpression") {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (isStaticKeyWrapper(key)) {
|
|
372
|
+
return isStaticTranslationKey(key.expression);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return false;
|
|
328
376
|
};
|
|
329
377
|
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
378
|
+
const assertStaticTranslationKey = (key: ESTree.Node | null | undefined): void => {
|
|
379
|
+
if (!isStaticTranslationKey(key)) {
|
|
380
|
+
throw new Error("Translation keys must be string literals");
|
|
381
|
+
}
|
|
382
|
+
};
|
|
333
383
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
projectRelative.startsWith(`..${sep}`) ||
|
|
338
|
-
isAbsolute(projectRelative)
|
|
339
|
-
) {
|
|
340
|
-
return null;
|
|
384
|
+
const propertyName = (property: ESTree.ObjectProperty): string | null => {
|
|
385
|
+
if (!property.computed && property.key.type === "Identifier") {
|
|
386
|
+
return property.key.name;
|
|
341
387
|
}
|
|
342
388
|
|
|
343
|
-
return
|
|
389
|
+
return property.key.type === "Literal" && typeof property.key.value === "string"
|
|
390
|
+
? property.key.value
|
|
391
|
+
: null;
|
|
344
392
|
};
|
|
345
393
|
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
394
|
+
const assertSupportedOptionProperty = (property: ESTree.ObjectPropertyKind): void => {
|
|
395
|
+
if (property.type === "SpreadElement") {
|
|
396
|
+
throw new Error("Translation extraction requires explicit option properties");
|
|
397
|
+
}
|
|
350
398
|
|
|
351
|
-
const
|
|
352
|
-
const target = resolve(project.poDir, POTFILES_FILENAME);
|
|
353
|
-
writeFileSync(target, sorted.length === 0 ? "" : `${sorted.join("\n")}\n`);
|
|
399
|
+
const name = propertyName(property);
|
|
354
400
|
|
|
355
|
-
|
|
401
|
+
if (name === null) {
|
|
402
|
+
throw new Error("Translation extraction requires static option property names");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (UNSUPPORTED_PLURAL_OPTION.test(name)) {
|
|
406
|
+
throw new Error("GNU gettext accepts one singular and one plural source string");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (STATIC_SOURCE_OPTION_NAMES.has(name)) {
|
|
410
|
+
assertStaticTranslationKey(property.value);
|
|
411
|
+
}
|
|
356
412
|
};
|
|
357
413
|
|
|
358
|
-
const
|
|
359
|
-
|
|
414
|
+
const assertSupportedOptionObject = (options: ESTree.Node | null | undefined): void => {
|
|
415
|
+
if (options === null || options === undefined) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
360
418
|
|
|
361
|
-
if (
|
|
362
|
-
|
|
419
|
+
if (isStaticKeyWrapper(options) && unwrapStaticWrapper(options).type === "ObjectExpression") {
|
|
420
|
+
throw new Error("Translation option objects cannot be wrapped in another expression");
|
|
421
|
+
}
|
|
363
422
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
: `npgettext(${JSON.stringify(message.context)}, ${msgid}, ${plural}, 0);`;
|
|
423
|
+
if (options.type !== "ObjectExpression") {
|
|
424
|
+
throw new Error("Translation option objects must be inline object literals");
|
|
367
425
|
}
|
|
368
426
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
427
|
+
for (const property of options.properties) {
|
|
428
|
+
assertSupportedOptionProperty(property);
|
|
429
|
+
}
|
|
372
430
|
};
|
|
373
431
|
|
|
374
|
-
const
|
|
375
|
-
|
|
432
|
+
const namedImportedBinding = (path: NodePath, source: string): ImportedBinding | null => {
|
|
433
|
+
if (!path.isImportSpecifier() || path.node.importKind === "type") {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
376
436
|
|
|
377
|
-
const
|
|
378
|
-
project: CatalogProject,
|
|
379
|
-
message: SourceMessage,
|
|
380
|
-
): { path: string; line: number | undefined }[] => {
|
|
381
|
-
const owners: { path: string; line: number | undefined }[] = [];
|
|
437
|
+
const imported = path.node.imported;
|
|
382
438
|
|
|
383
|
-
|
|
384
|
-
|
|
439
|
+
return {
|
|
440
|
+
imported: types.isIdentifier(imported) ? imported.name : imported.value,
|
|
441
|
+
local: path.node.local.name,
|
|
442
|
+
source,
|
|
443
|
+
style: "named",
|
|
444
|
+
};
|
|
445
|
+
};
|
|
385
446
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
447
|
+
const defaultImportedBinding = (path: NodePath, source: string): ImportedBinding | null =>
|
|
448
|
+
path.isImportDefaultSpecifier()
|
|
449
|
+
? { imported: "default", local: path.node.local.name, source, style: "default" }
|
|
450
|
+
: null;
|
|
451
|
+
|
|
452
|
+
const namespaceImportedBinding = (path: NodePath, source: string): ImportedBinding | null =>
|
|
453
|
+
path.isImportNamespaceSpecifier()
|
|
454
|
+
? { imported: "*", local: path.node.local.name, source, style: "namespace" }
|
|
455
|
+
: null;
|
|
456
|
+
|
|
457
|
+
const importedBinding = (binding: ResolvedBinding | undefined): ImportedBinding | null => {
|
|
458
|
+
if (binding === undefined) {
|
|
459
|
+
return null;
|
|
389
460
|
}
|
|
390
461
|
|
|
391
|
-
const
|
|
462
|
+
const path = binding.path;
|
|
463
|
+
const declaration = path.parentPath;
|
|
392
464
|
|
|
393
|
-
|
|
465
|
+
if (!declaration.isImportDeclaration() || declaration.node.importKind === "type") {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const source = declaration.node.source.value;
|
|
470
|
+
|
|
471
|
+
return namedImportedBinding(path, source) ??
|
|
472
|
+
defaultImportedBinding(path, source) ??
|
|
473
|
+
namespaceImportedBinding(path, source);
|
|
394
474
|
};
|
|
395
475
|
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
message: SourceMessage,
|
|
399
|
-
sources: Map<string, string>,
|
|
400
|
-
): { path: string; line: number } | null => {
|
|
401
|
-
for (const [path, source] of sources) {
|
|
402
|
-
const index = source.indexOf(message.msgid);
|
|
476
|
+
const babelPropertyName = (property: types.ObjectProperty): string | null => {
|
|
477
|
+
const key = property.key;
|
|
403
478
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
path: projectPath(project.root, path) ?? SYNTHETIC_FILENAME,
|
|
407
|
-
line: source.slice(0, index).split("\n").length,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
479
|
+
if (!property.computed && types.isIdentifier(key)) {
|
|
480
|
+
return key.name;
|
|
410
481
|
}
|
|
411
482
|
|
|
412
|
-
return null;
|
|
483
|
+
return types.isStringLiteral(key) ? key.value : null;
|
|
413
484
|
};
|
|
414
485
|
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
): { path: string; line: number | undefined }[] => {
|
|
420
|
-
const located = locatedOwners(project, message);
|
|
486
|
+
const isBoundIdentifier = (node: types.Node | null, name: string): boolean => {
|
|
487
|
+
if (types.isIdentifier(node)) {
|
|
488
|
+
return node.name === name;
|
|
489
|
+
}
|
|
421
490
|
|
|
422
|
-
if (
|
|
423
|
-
return
|
|
491
|
+
if (types.isAssignmentPattern(node)) {
|
|
492
|
+
return types.isIdentifier(node.left) && node.left.name === name;
|
|
424
493
|
}
|
|
425
494
|
|
|
426
|
-
return
|
|
495
|
+
return false;
|
|
427
496
|
};
|
|
428
497
|
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
|
|
498
|
+
const isHookPatternBinding = (pattern: types.VariableDeclarator["id"], name: string): boolean => {
|
|
499
|
+
if (types.isObjectPattern(pattern)) {
|
|
500
|
+
return pattern.properties.some((property) =>
|
|
501
|
+
types.isObjectProperty(property) &&
|
|
502
|
+
babelPropertyName(property) === "t" &&
|
|
503
|
+
isBoundIdentifier(property.value, name));
|
|
504
|
+
}
|
|
436
505
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const owned = entries.get(owner.path) ?? [];
|
|
440
|
-
owned.push({ call: renderCatalogCall(message), line: owner.line });
|
|
441
|
-
entries.set(owner.path, owned);
|
|
442
|
-
}
|
|
506
|
+
if (types.isArrayPattern(pattern)) {
|
|
507
|
+
return isBoundIdentifier(pattern.elements[0] ?? null, name);
|
|
443
508
|
}
|
|
444
509
|
|
|
445
|
-
|
|
446
|
-
|
|
510
|
+
return false;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
const importedTranslationHook = (scope: Scope, call: types.CallExpression): ImportedBinding | null => {
|
|
514
|
+
if (!types.isIdentifier(call.callee)) {
|
|
515
|
+
return null;
|
|
447
516
|
}
|
|
448
517
|
|
|
449
|
-
|
|
518
|
+
const imported = importedBinding(scope.getBinding(call.callee.name));
|
|
519
|
+
|
|
520
|
+
return imported?.imported === "useTranslation" && TRANSLATION_MODULES.has(imported.source)
|
|
521
|
+
? imported
|
|
522
|
+
: null;
|
|
450
523
|
};
|
|
451
524
|
|
|
452
|
-
const
|
|
453
|
-
|
|
525
|
+
const translationHookBinding = (binding: ResolvedBinding): TranslationHookBinding | null => {
|
|
526
|
+
const path = binding.path;
|
|
454
527
|
|
|
455
|
-
|
|
456
|
-
|
|
528
|
+
if (!path.isVariableDeclarator()) {
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
457
531
|
|
|
458
|
-
|
|
459
|
-
|
|
532
|
+
const pattern = path.node.id;
|
|
533
|
+
const initializer = path.node.init;
|
|
534
|
+
|
|
535
|
+
if (!isHookPatternBinding(pattern, binding.identifier.name) ||
|
|
536
|
+
!types.isCallExpression(initializer)) {
|
|
537
|
+
return null;
|
|
460
538
|
}
|
|
461
539
|
|
|
462
|
-
|
|
463
|
-
};
|
|
540
|
+
const imported = importedTranslationHook(path.scope, initializer);
|
|
464
541
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
542
|
+
if (imported === null) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
468
545
|
|
|
469
|
-
|
|
470
|
-
|
|
546
|
+
return { call: initializer, isCanonical: imported.local === "useTranslation" };
|
|
547
|
+
};
|
|
471
548
|
|
|
472
|
-
|
|
549
|
+
const importedTranslationFunctionKind = (imported: ImportedBinding | null): TranslationCallKind | null => {
|
|
550
|
+
if (imported?.imported !== "t" || !TRANSLATION_MODULES.has(imported.source)) {
|
|
551
|
+
return null;
|
|
473
552
|
}
|
|
474
553
|
|
|
475
|
-
|
|
554
|
+
return imported.local === "t" ? "canonical" : "alias";
|
|
476
555
|
};
|
|
477
556
|
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
557
|
+
const hookTranslationFunctionKind = (binding: ResolvedBinding | undefined): TranslationCallKind | null => {
|
|
558
|
+
if (binding === undefined) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
482
561
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
562
|
+
const hook = translationHookBinding(binding);
|
|
563
|
+
|
|
564
|
+
if (hook === null) {
|
|
565
|
+
return null;
|
|
486
566
|
}
|
|
487
567
|
|
|
488
|
-
return
|
|
568
|
+
return binding.identifier.name === "t" && hook.isCanonical ? "canonical" : "alias";
|
|
489
569
|
};
|
|
490
570
|
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
project: CatalogProject,
|
|
494
|
-
messages: SourceMessage[],
|
|
495
|
-
sourceFiles: string[],
|
|
496
|
-
): string => {
|
|
497
|
-
const entries = syntheticEntries(project, messages, sourceFiles);
|
|
498
|
-
const paths: string[] = [];
|
|
571
|
+
const directTranslationFunctionKind = (scope: Scope, name: string): TranslationCallKind | null => {
|
|
572
|
+
const binding = scope.getBinding(name);
|
|
499
573
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
503
|
-
writeFileSync(target, renderSyntheticSource(owned));
|
|
504
|
-
paths.push(path);
|
|
505
|
-
}
|
|
574
|
+
return importedTranslationFunctionKind(importedBinding(binding)) ?? hookTranslationFunctionKind(binding);
|
|
575
|
+
};
|
|
506
576
|
|
|
507
|
-
|
|
508
|
-
|
|
577
|
+
const babelMemberName = (member: TranslationMemberExpression): string | null => {
|
|
578
|
+
const property = member.property;
|
|
509
579
|
|
|
510
|
-
|
|
580
|
+
if (member.computed) {
|
|
581
|
+
return types.isStringLiteral(property) ? property.value : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return types.isIdentifier(property) ? property.name : null;
|
|
511
585
|
};
|
|
512
586
|
|
|
513
|
-
const
|
|
514
|
-
|
|
587
|
+
const isImportedTranslationObject = (scope: Scope, object: types.Node): boolean => {
|
|
588
|
+
if (!types.isIdentifier(object)) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
515
591
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
"--force-po",
|
|
522
|
-
"--keyword=gettext:1",
|
|
523
|
-
"--keyword=ngettext:1,2",
|
|
524
|
-
"--keyword=pgettext:1c,2",
|
|
525
|
-
"--keyword=npgettext:1c,2,3",
|
|
526
|
-
`--directory=${workDir}`,
|
|
527
|
-
`--files-from=${potfilesPath}`,
|
|
528
|
-
`--output=${output}`,
|
|
529
|
-
],
|
|
530
|
-
target: output,
|
|
531
|
-
});
|
|
592
|
+
const imported = importedBinding(scope.getBinding(object.name));
|
|
593
|
+
|
|
594
|
+
return imported !== null &&
|
|
595
|
+
TRANSLATION_MODULES.has(imported.source) &&
|
|
596
|
+
(imported.style === "namespace" || (imported.source === "i18next" && imported.imported === "default"));
|
|
532
597
|
};
|
|
533
598
|
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
599
|
+
const constantVariableDeclarator = (binding: ResolvedBinding): NodePath<types.VariableDeclarator> | null => {
|
|
600
|
+
const path = binding.path;
|
|
601
|
+
const declaration = path.parentPath;
|
|
602
|
+
|
|
603
|
+
if (!path.isVariableDeclarator() ||
|
|
604
|
+
!declaration.isVariableDeclaration() ||
|
|
605
|
+
declaration.node.kind !== "const") {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return path;
|
|
545
610
|
};
|
|
546
611
|
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
612
|
+
const isTranslationHookResultBinding = (binding: ResolvedBinding | undefined): boolean => {
|
|
613
|
+
if (binding === undefined) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const path = constantVariableDeclarator(binding);
|
|
618
|
+
const initializer = path?.node.init;
|
|
619
|
+
|
|
620
|
+
return path !== null &&
|
|
621
|
+
types.isIdentifier(path.node.id) &&
|
|
622
|
+
types.isCallExpression(initializer) &&
|
|
623
|
+
importedTranslationHook(path.scope, initializer) !== null;
|
|
553
624
|
};
|
|
554
625
|
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
shouldPreserveMetadataMessages,
|
|
560
|
-
output = resolve(project.poDir, `${project.domain}.pot`),
|
|
561
|
-
}: CatalogTemplateExtraction): void => {
|
|
562
|
-
const workDir = mkdtempSync(join(project.poDir, ".gtkx-i18n-"));
|
|
563
|
-
const source = join(workDir, "source.pot");
|
|
564
|
-
const hasPreviousTemplate = existsSync(output);
|
|
626
|
+
const isHookTranslationMember = (scope: Scope, member: TranslationMemberExpression): boolean => {
|
|
627
|
+
if (babelMemberName(member) !== "t") {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
565
630
|
|
|
566
|
-
|
|
567
|
-
if (shouldPreserveMetadataMessages && hasPreviousTemplate) {
|
|
568
|
-
const fragment = join(workDir, "metadata.pot");
|
|
569
|
-
extractMetadataFragment(project, output, fragment);
|
|
570
|
-
extractSourceMessages({ project, messages, sourceFiles, output: source, workDir });
|
|
571
|
-
joinMetadataFragment(source, fragment);
|
|
572
|
-
} else {
|
|
573
|
-
extractSourceMessages({ project, messages, sourceFiles, output: source, workDir });
|
|
574
|
-
}
|
|
631
|
+
const object = member.object;
|
|
575
632
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
rmSync(workDir, { recursive: true, force: true });
|
|
633
|
+
if (types.isCallExpression(object)) {
|
|
634
|
+
return importedTranslationHook(scope, object) !== null;
|
|
579
635
|
}
|
|
636
|
+
|
|
637
|
+
return types.isIdentifier(object) && isTranslationHookResultBinding(scope.getBinding(object.name));
|
|
580
638
|
};
|
|
581
639
|
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
):
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
640
|
+
const isTranslationMember = (scope: Scope, member: TranslationMemberExpression): boolean =>
|
|
641
|
+
babelMemberName(member) === "t" &&
|
|
642
|
+
(isImportedTranslationObject(scope, member.object) || isHookTranslationMember(scope, member));
|
|
643
|
+
|
|
644
|
+
const isDirectTranslationFunction = (scope: Scope, node: types.Node): boolean =>
|
|
645
|
+
types.isIdentifier(node) && directTranslationFunctionKind(scope, node.name) !== null;
|
|
646
|
+
|
|
647
|
+
const isTranslationFunctionValue = (scope: Scope, node: types.Node): boolean => {
|
|
648
|
+
if (isDirectTranslationFunction(scope, node)) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
591
651
|
|
|
592
|
-
return
|
|
652
|
+
return (types.isMemberExpression(node) || types.isOptionalMemberExpression(node)) &&
|
|
653
|
+
isTranslationMember(scope, node);
|
|
593
654
|
};
|
|
594
655
|
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
): Promise<SourceCatalog> => {
|
|
600
|
-
const sourceFiles = normalizedSourceFiles(project.root, paths);
|
|
601
|
-
const messages = await findSourceMessages(sourceFiles);
|
|
602
|
-
writePotfiles(project, sourceFiles);
|
|
603
|
-
extractCatalogTemplate({ project, messages, sourceFiles, shouldPreserveMetadataMessages });
|
|
656
|
+
const isTranslationBindCall = (scope: Scope, node: types.Node): boolean => {
|
|
657
|
+
if (!types.isCallExpression(node) || !types.isMemberExpression(node.callee)) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
604
660
|
|
|
605
|
-
return
|
|
661
|
+
return babelMemberName(node.callee) === "bind" &&
|
|
662
|
+
isTranslationFunctionValue(scope, node.callee.object);
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const isTranslationObjectPatternAlias = (
|
|
666
|
+
path: NodePath<types.VariableDeclarator>,
|
|
667
|
+
binding: ResolvedBinding,
|
|
668
|
+
): boolean => {
|
|
669
|
+
const pattern = path.node.id;
|
|
670
|
+
const initializer = path.node.init;
|
|
671
|
+
|
|
672
|
+
if (!types.isObjectPattern(pattern) ||
|
|
673
|
+
!isHookPatternBinding(pattern, binding.identifier.name) ||
|
|
674
|
+
!types.isIdentifier(initializer)) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return isImportedTranslationObject(path.scope, initializer) ||
|
|
679
|
+
isTranslationHookResultBinding(path.scope.getBinding(initializer.name));
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const isTranslationAliasBinding = (binding: ResolvedBinding | undefined): boolean => {
|
|
683
|
+
if (binding === undefined) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const path = constantVariableDeclarator(binding);
|
|
688
|
+
|
|
689
|
+
if (path === null) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (isTranslationObjectPatternAlias(path, binding)) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const initializer = path.node.init;
|
|
698
|
+
|
|
699
|
+
return types.isIdentifier(path.node.id) &&
|
|
700
|
+
initializer !== null &&
|
|
701
|
+
initializer !== undefined &&
|
|
702
|
+
(isTranslationFunctionValue(path.scope, initializer) || isTranslationBindCall(path.scope, initializer));
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const translationFunctionKind = (scope: Scope, name: string): TranslationCallKind | null => {
|
|
706
|
+
const direct = directTranslationFunctionKind(scope, name);
|
|
707
|
+
|
|
708
|
+
if (direct !== null) {
|
|
709
|
+
return direct;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return isTranslationAliasBinding(scope.getBinding(name)) ? "alias" : null;
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
const translationElementKind = (path: NodePath, name: string): TranslationCallKind | null => {
|
|
716
|
+
const imported = importedBinding(path.scope.getBinding(name));
|
|
717
|
+
|
|
718
|
+
if (imported?.source !== "@gtkx/i18n" || !TRANS_ELEMENTS.has(imported.imported)) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return imported.local === imported.imported ? "canonical" : "alias";
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const nodeStart = (path: NodePath): number => {
|
|
726
|
+
const start = path.node.start;
|
|
727
|
+
|
|
728
|
+
if (start === null || start === undefined) {
|
|
729
|
+
throw new Error("Translation extraction could not locate a source expression");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return start;
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
const registerSourceMask = (node: types.Node, references: TranslationReferences): void => {
|
|
736
|
+
const { start, end } = node;
|
|
737
|
+
|
|
738
|
+
if (start === null || start === undefined || end === null || end === undefined) {
|
|
739
|
+
throw new Error("Translation extraction could not locate a source expression");
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
references.masks.push({ start, end });
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const sourceLocationKey = (line: number, column: number): string => `${String(line)}:${String(column)}`;
|
|
746
|
+
|
|
747
|
+
const sourcePoint = (node: types.Node): SourcePoint | null => {
|
|
748
|
+
const location = node.loc?.start;
|
|
749
|
+
|
|
750
|
+
return location === undefined ? null : { column: location.column, line: location.line };
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const registerSourceLocation = (node: types.Node, references: TranslationReferences): SourcePoint | null => {
|
|
754
|
+
const point = sourcePoint(node);
|
|
755
|
+
|
|
756
|
+
if (point !== null) {
|
|
757
|
+
references.locations.add(sourceLocationKey(point.line, point.column));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
return point;
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
const babelStaticKey = (node: types.Node | null | undefined): string | null => {
|
|
764
|
+
if (node === null || node === undefined) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (types.isStringLiteral(node)) {
|
|
769
|
+
return node.value;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (types.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
773
|
+
return node.quasis[0]?.value.cooked ?? null;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (types.isParenthesizedExpression(node) ||
|
|
777
|
+
types.isTSAsExpression(node) ||
|
|
778
|
+
types.isTSSatisfiesExpression(node) ||
|
|
779
|
+
types.isTSTypeAssertion(node) ||
|
|
780
|
+
types.isTSNonNullExpression(node)) {
|
|
781
|
+
return babelStaticKey(node.expression);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
return null;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
const babelObjectExpression = (node: types.Node | null | undefined): types.ObjectExpression | null => {
|
|
788
|
+
if (node === null || node === undefined) {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (types.isObjectExpression(node)) {
|
|
793
|
+
return node;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (types.isParenthesizedExpression(node) ||
|
|
797
|
+
types.isTSAsExpression(node) ||
|
|
798
|
+
types.isTSSatisfiesExpression(node) ||
|
|
799
|
+
types.isTSTypeAssertion(node) ||
|
|
800
|
+
types.isTSNonNullExpression(node)) {
|
|
801
|
+
return babelObjectExpression(node.expression);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return null;
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const babelNamedProperty = (
|
|
808
|
+
object: types.ObjectExpression | null,
|
|
809
|
+
name: string,
|
|
810
|
+
): types.ObjectProperty | null => {
|
|
811
|
+
if (object === null) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
for (const property of object.properties) {
|
|
816
|
+
if (types.isObjectProperty(property) && babelPropertyName(property) === name) {
|
|
817
|
+
return property;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
return null;
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const babelStringProperty = (object: types.ObjectExpression | null, name: string): string | null => {
|
|
825
|
+
const property = babelNamedProperty(object, name);
|
|
826
|
+
|
|
827
|
+
if (property === null) {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const value = babelStaticKey(property.value);
|
|
832
|
+
|
|
833
|
+
if (value === null) {
|
|
834
|
+
throw new Error(`Translation ${name} values must be string literals`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return value;
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
const callOptions = (call: types.CallExpression): types.ObjectExpression | null =>
|
|
841
|
+
babelObjectExpression(call.arguments[1]) ?? babelObjectExpression(call.arguments[2]);
|
|
842
|
+
|
|
843
|
+
const hookKeyPrefix = (callee: NodePath<types.Identifier>): string => {
|
|
844
|
+
const binding = callee.scope.getBinding(callee.node.name);
|
|
845
|
+
const hook = binding === undefined ? null : translationHookBinding(binding);
|
|
846
|
+
|
|
847
|
+
return babelStringProperty(
|
|
848
|
+
hook === null ? null : babelObjectExpression(hook.call.arguments[1]),
|
|
849
|
+
"keyPrefix",
|
|
850
|
+
) ?? "";
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const prefixedTranslationKey = (prefix: string, key: string): string => {
|
|
854
|
+
if (prefix.length === 0) {
|
|
855
|
+
return key;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return prefix.endsWith(".") ? `${prefix}${key}` : `${prefix}.${key}`;
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
const extractedEntryKey = (call: types.CallExpression, keyPrefix: string): string | null => {
|
|
862
|
+
const key = babelStaticKey(call.arguments[0]);
|
|
863
|
+
|
|
864
|
+
if (key === null) {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const options = callOptions(call);
|
|
869
|
+
const callPrefix = babelStringProperty(options, "keyPrefix");
|
|
870
|
+
|
|
871
|
+
if (callPrefix === "" && keyPrefix.length > 0) {
|
|
872
|
+
throw new Error("An empty call keyPrefix cannot override a hook keyPrefix during extraction");
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const selectedPrefix = callPrefix ?? keyPrefix;
|
|
876
|
+
const prefixed = prefixedTranslationKey(selectedPrefix, key);
|
|
877
|
+
const context = babelStringProperty(options, "context");
|
|
878
|
+
|
|
879
|
+
return context === null ? prefixed : `${prefixed}${CONTEXT_SEPARATOR}${context}`;
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const registerEntryLocation = (
|
|
883
|
+
key: string | null,
|
|
884
|
+
point: SourcePoint | null,
|
|
885
|
+
references: TranslationReferences,
|
|
886
|
+
): void => {
|
|
887
|
+
if (key === null || point === null) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const locations = references.entryLocations.get(key) ?? [];
|
|
892
|
+
locations.push(point);
|
|
893
|
+
references.entryLocations.set(key, locations);
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const registerCallReference = (
|
|
897
|
+
callPath: NodePath<types.CallExpression>,
|
|
898
|
+
references: TranslationReferences,
|
|
899
|
+
keyPrefix: string,
|
|
900
|
+
): void => {
|
|
901
|
+
references.calls.add(nodeStart(callPath));
|
|
902
|
+
const firstArgument = callPath.node.arguments[0];
|
|
903
|
+
const point = registerSourceLocation(firstArgument ?? callPath.node, references);
|
|
904
|
+
registerEntryLocation(extractedEntryKey(callPath.node, keyPrefix), point, references);
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const parserPlugins = (lang: ReturnType<typeof sourceLanguage>): ("decorators" | "jsx" | "typescript")[] => [
|
|
908
|
+
"decorators",
|
|
909
|
+
...(lang === "ts" || lang === "tsx" ? ["typescript" as const] : []),
|
|
910
|
+
...(lang === "jsx" || lang === "tsx" ? ["jsx" as const] : []),
|
|
911
|
+
];
|
|
912
|
+
|
|
913
|
+
const registerIdentifierTranslationCall = (
|
|
914
|
+
callPath: NodePath<types.CallExpression>,
|
|
915
|
+
callee: NodePath,
|
|
916
|
+
references: TranslationReferences,
|
|
917
|
+
): void => {
|
|
918
|
+
if (!callee.isIdentifier()) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const kind = translationFunctionKind(callee.scope, callee.node.name);
|
|
923
|
+
|
|
924
|
+
if (kind === "alias") {
|
|
925
|
+
throw new Error("Translation calls must use the imported name t");
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (kind === "canonical") {
|
|
929
|
+
registerCallReference(callPath, references, hookKeyPrefix(callee));
|
|
930
|
+
} else if (callee.node.name === "t") {
|
|
931
|
+
registerSourceMask(callee.node, references);
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
const registerTranslationCall = (
|
|
936
|
+
callPath: NodePath<types.CallExpression>,
|
|
937
|
+
references: TranslationReferences,
|
|
938
|
+
): void => {
|
|
939
|
+
assertSupportedWrappedCallee(callPath);
|
|
940
|
+
const callee = callPath.get("callee");
|
|
941
|
+
|
|
942
|
+
if (callee.isIdentifier()) {
|
|
943
|
+
registerIdentifierTranslationCall(callPath, callee, references);
|
|
944
|
+
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (callee.isMemberExpression() && isTranslationMember(callee.scope, callee.node)) {
|
|
949
|
+
throw new Error("Translation calls must use the named t export");
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const unwrapTranslationCallee = (node: types.Node): types.Node => {
|
|
954
|
+
if (types.isParenthesizedExpression(node) ||
|
|
955
|
+
types.isTSAsExpression(node) ||
|
|
956
|
+
types.isTSSatisfiesExpression(node) ||
|
|
957
|
+
types.isTSTypeAssertion(node) ||
|
|
958
|
+
types.isTSNonNullExpression(node)) {
|
|
959
|
+
return unwrapTranslationCallee(node.expression);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return node;
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
const isTranslationCallee = (scope: Scope, node: types.Node): boolean => {
|
|
966
|
+
if (types.isIdentifier(node)) {
|
|
967
|
+
return translationFunctionKind(scope, node.name) !== null;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return (types.isMemberExpression(node) || types.isOptionalMemberExpression(node)) &&
|
|
971
|
+
isTranslationMember(scope, node);
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const assertSupportedWrappedCallee = (callPath: NodePath<types.CallExpression>): void => {
|
|
975
|
+
const callee = callPath.node.callee;
|
|
976
|
+
const unwrapped = unwrapTranslationCallee(callee);
|
|
977
|
+
|
|
978
|
+
if (unwrapped !== callee && isTranslationCallee(callPath.scope, unwrapped)) {
|
|
979
|
+
throw new Error("Translation call expressions cannot wrap their callee");
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
const assertSupportedOptionalCall = (callPath: NodePath<types.OptionalCallExpression>): void => {
|
|
984
|
+
const callee = unwrapTranslationCallee(callPath.node.callee);
|
|
985
|
+
|
|
986
|
+
if (isTranslationCallee(callPath.scope, callee)) {
|
|
987
|
+
throw new Error("Translation calls cannot use optional chaining");
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
const assertUnsupportedTranslationTag = (tagPath: NodePath<types.TaggedTemplateExpression>): void => {
|
|
992
|
+
const tag = unwrapTranslationCallee(tagPath.node.tag);
|
|
993
|
+
|
|
994
|
+
if (isTranslationCallee(tagPath.scope, tag)) {
|
|
995
|
+
throw new Error("Translation calls cannot use tagged-template syntax");
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
const registerUnrecognizedElementMasks = (
|
|
1000
|
+
elementPath: NodePath,
|
|
1001
|
+
name: NodePath<types.JSXIdentifier>,
|
|
1002
|
+
references: TranslationReferences,
|
|
1003
|
+
): void => {
|
|
1004
|
+
registerSourceMask(name.node, references);
|
|
1005
|
+
const parent = elementPath.parentPath.node;
|
|
1006
|
+
const closingName = types.isJSXElement(parent) ? parent.closingElement?.name : null;
|
|
1007
|
+
|
|
1008
|
+
if (types.isJSXIdentifier(closingName)) {
|
|
1009
|
+
registerSourceMask(closingName, references);
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
const registerTranslationElement = (elementPath: NodePath, references: TranslationReferences): void => {
|
|
1014
|
+
const name = elementPath.get("name");
|
|
1015
|
+
|
|
1016
|
+
if (!name.isJSXIdentifier()) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const kind = translationElementKind(name, name.node.name);
|
|
1021
|
+
|
|
1022
|
+
if (kind === "alias") {
|
|
1023
|
+
throw new Error("Translation components must use their imported names");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (kind === "canonical") {
|
|
1027
|
+
references.elements.add(nodeStart(elementPath));
|
|
1028
|
+
registerSourceLocation(elementPath.node, references);
|
|
1029
|
+
} else if (TRANS_ELEMENTS.has(name.node.name)) {
|
|
1030
|
+
registerUnrecognizedElementMasks(elementPath, name, references);
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const maskUnrecognizedTranslations = (source: string, references: TranslationReferences): string => {
|
|
1035
|
+
let masked = source;
|
|
1036
|
+
const masks = references.masks.toSorted((left, right) => right.start - left.start);
|
|
1037
|
+
|
|
1038
|
+
for (const { start, end } of masks) {
|
|
1039
|
+
masked = `${masked.slice(0, start)}${"_".repeat(end - start)}${masked.slice(end)}`;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return masked;
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const translationReferences = (
|
|
1046
|
+
path: string,
|
|
1047
|
+
source: string,
|
|
1048
|
+
lang: ReturnType<typeof sourceLanguage>,
|
|
1049
|
+
): TranslationReferences => {
|
|
1050
|
+
const parsed = parseBabelSync(source, {
|
|
1051
|
+
ast: true,
|
|
1052
|
+
babelrc: false,
|
|
1053
|
+
configFile: false,
|
|
1054
|
+
filename: path,
|
|
1055
|
+
parserOpts: { createParenthesizedExpressions: true, plugins: parserPlugins(lang) },
|
|
1056
|
+
sourceType: "module",
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
if (parsed === null) {
|
|
1060
|
+
throw new Error(`Translation extraction could not parse ${path}`);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const references: TranslationReferences = {
|
|
1064
|
+
calls: new Set(),
|
|
1065
|
+
elements: new Set(),
|
|
1066
|
+
entryLocations: new Map(),
|
|
1067
|
+
locations: new Set(),
|
|
1068
|
+
masks: [],
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
traverse(parsed, {
|
|
1072
|
+
CallExpression(callPath) {
|
|
1073
|
+
registerTranslationCall(callPath, references);
|
|
1074
|
+
},
|
|
1075
|
+
JSXOpeningElement(elementPath) {
|
|
1076
|
+
registerTranslationElement(elementPath, references);
|
|
1077
|
+
},
|
|
1078
|
+
OptionalCallExpression(callPath) {
|
|
1079
|
+
assertSupportedOptionalCall(callPath);
|
|
1080
|
+
},
|
|
1081
|
+
TaggedTemplateExpression(tagPath) {
|
|
1082
|
+
assertUnsupportedTranslationTag(tagPath);
|
|
1083
|
+
},
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
return references;
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
const assertSupportedTranslationCall = (node: ESTree.CallExpression, references: TranslationReferences): void => {
|
|
1090
|
+
if (!references.calls.has(node.start)) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
assertStaticTranslationKey(node.arguments[0]);
|
|
1095
|
+
assertSupportedTranslationOptions(node.arguments[1], node.arguments[2]);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const assertSupportedTranslationOptions = (
|
|
1099
|
+
second: ESTree.Node | null | undefined,
|
|
1100
|
+
third: ESTree.Node | null | undefined,
|
|
1101
|
+
): void => {
|
|
1102
|
+
if (third !== undefined && third !== null) {
|
|
1103
|
+
assertStaticTranslationKey(second);
|
|
1104
|
+
assertSupportedOptionObject(third);
|
|
1105
|
+
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (second === undefined || second === null || isStaticTranslationKey(second)) {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
assertSupportedOptionObject(second);
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
const isNamedJsxAttribute = (attribute: ESTree.JSXAttribute, name: string): boolean =>
|
|
1117
|
+
attribute.name.type === "JSXIdentifier" && attribute.name.name === name;
|
|
1118
|
+
|
|
1119
|
+
const assertSupportedTransKey = (attribute: ESTree.JSXAttribute): void => {
|
|
1120
|
+
if (!isNamedJsxAttribute(attribute, "i18nKey")) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const key = attribute.value?.type === "JSXExpressionContainer"
|
|
1125
|
+
? attribute.value.expression
|
|
1126
|
+
: attribute.value;
|
|
1127
|
+
assertStaticTranslationKey(key);
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
const assertSupportedTransStringAttribute = (attribute: ESTree.JSXAttribute, name: string): void => {
|
|
1131
|
+
if (!isNamedJsxAttribute(attribute, name)) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const value = attribute.value?.type === "JSXExpressionContainer"
|
|
1136
|
+
? attribute.value.expression
|
|
1137
|
+
: attribute.value;
|
|
1138
|
+
assertStaticTranslationKey(value);
|
|
1139
|
+
};
|
|
1140
|
+
|
|
1141
|
+
const assertSupportedTransOptions = (attribute: ESTree.JSXAttribute): void => {
|
|
1142
|
+
if (isNamedJsxAttribute(attribute, "tOptions") && attribute.value?.type === "JSXExpressionContainer") {
|
|
1143
|
+
assertSupportedOptionObject(attribute.value.expression);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const assertSupportedTransAttribute = (attribute: ESTree.JSXAttributeItem): void => {
|
|
1148
|
+
if (attribute.type === "JSXSpreadAttribute") {
|
|
1149
|
+
throw new Error("Translation extraction requires explicit component properties");
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
assertSupportedTransKey(attribute);
|
|
1153
|
+
assertSupportedTransStringAttribute(attribute, "context");
|
|
1154
|
+
assertSupportedTransStringAttribute(attribute, "defaults");
|
|
1155
|
+
assertSupportedTransOptions(attribute);
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
const assertSupportedTransElement = (node: ESTree.JSXOpeningElement, references: TranslationReferences): void => {
|
|
1159
|
+
if (!references.elements.has(node.start)) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
for (const attribute of node.attributes) {
|
|
1164
|
+
assertSupportedTransAttribute(attribute);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
const assertSupportedTranslationSyntax = (path: string, source: string): TranslationReferences => {
|
|
1169
|
+
const lang = sourceLanguage(path);
|
|
1170
|
+
|
|
1171
|
+
if (lang === undefined) {
|
|
1172
|
+
return { calls: new Set(), elements: new Set(), entryLocations: new Map(), locations: new Set(), masks: [] };
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const parsed = parseSync(path, source, { lang });
|
|
1176
|
+
const references = translationReferences(path, source, lang);
|
|
1177
|
+
|
|
1178
|
+
new Visitor({
|
|
1179
|
+
CallExpression(node) {
|
|
1180
|
+
assertSupportedTranslationCall(node, references);
|
|
1181
|
+
},
|
|
1182
|
+
JSXOpeningElement(node) {
|
|
1183
|
+
assertSupportedTransElement(node, references);
|
|
1184
|
+
},
|
|
1185
|
+
}).visit(parsed.program);
|
|
1186
|
+
|
|
1187
|
+
return references;
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
const extractedLocationFile = (root: string, file: string): string =>
|
|
1191
|
+
resolve(root, file);
|
|
1192
|
+
|
|
1193
|
+
const isRecognizedLocation = (
|
|
1194
|
+
root: string,
|
|
1195
|
+
references: Map<string, TranslationReferences>,
|
|
1196
|
+
location: SourceLocation,
|
|
1197
|
+
): boolean => {
|
|
1198
|
+
if (location.line === undefined || location.column === undefined) {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const sourceReferences = references.get(extractedLocationFile(root, location.file));
|
|
1203
|
+
|
|
1204
|
+
return sourceReferences?.locations.has(sourceLocationKey(location.line, location.column)) === true;
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const recognizedKeyLocations = (
|
|
1208
|
+
references: Map<string, TranslationReferences>,
|
|
1209
|
+
extractedKey: string,
|
|
1210
|
+
): ExtractedLocation[] => {
|
|
1211
|
+
const locations: ExtractedLocation[] = [];
|
|
1212
|
+
|
|
1213
|
+
for (const [file, sourceReferences] of references) {
|
|
1214
|
+
const points = sourceReferences.entryLocations.get(extractedKey) ?? [];
|
|
1215
|
+
|
|
1216
|
+
for (const point of points) {
|
|
1217
|
+
locations.push({ file, column: point.column, line: point.line });
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return locations;
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
const recognizedEntry = (
|
|
1225
|
+
root: string,
|
|
1226
|
+
references: Map<string, TranslationReferences>,
|
|
1227
|
+
entry: ExtractedKey,
|
|
1228
|
+
): ExtractedKey | null => {
|
|
1229
|
+
const extractedLocations = entry.locations ?? [];
|
|
1230
|
+
const locations = extractedLocations.length === 0
|
|
1231
|
+
? recognizedKeyLocations(references, entry.key)
|
|
1232
|
+
: extractedLocations.filter((location) => isRecognizedLocation(root, references, location));
|
|
1233
|
+
|
|
1234
|
+
return locations.length === 0 ? null : { ...entry, locations };
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
type NestedReferenceParts = { base: string; options: string };
|
|
1238
|
+
|
|
1239
|
+
const quotedNestedReferenceParts = (content: string, quote: string): NestedReferenceParts | null => {
|
|
1240
|
+
const end = content.indexOf(quote, 1);
|
|
1241
|
+
|
|
1242
|
+
return end === -1
|
|
1243
|
+
? null
|
|
1244
|
+
: { base: content.slice(1, end), options: content.slice(end + 1) };
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
const nestedReferenceParts = (value: string): NestedReferenceParts | null => {
|
|
1248
|
+
const content = value.trim();
|
|
1249
|
+
const quote = content[0];
|
|
1250
|
+
|
|
1251
|
+
if (quote === "\"" || quote === "'") {
|
|
1252
|
+
return quotedNestedReferenceParts(content, quote);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const separator = content.indexOf(",");
|
|
1256
|
+
const base = (separator === -1 ? content : content.slice(0, separator)).trim();
|
|
1257
|
+
|
|
1258
|
+
return base.length === 0
|
|
1259
|
+
? null
|
|
1260
|
+
: { base, options: separator === -1 ? "" : content.slice(separator + 1) };
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
const nestedReferenceContext = (options: string): string | null => {
|
|
1264
|
+
const match = NESTED_CONTEXT.exec(options);
|
|
1265
|
+
const context = match?.[1] ?? match?.[2] ?? null;
|
|
1266
|
+
|
|
1267
|
+
return context === null || context.length === 0 ? null : context;
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
const adjustedParenthesisDepth = (character: string | undefined, depth: number): number => {
|
|
1271
|
+
if (character === "(") {
|
|
1272
|
+
return depth + 1;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return character === ")" ? depth - 1 : depth;
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const nestedReferenceEnd = (text: string, start: number): number => {
|
|
1279
|
+
let depth = 0;
|
|
1280
|
+
const tokens = text.slice(start).matchAll(NESTED_STRUCTURE_TOKEN);
|
|
1281
|
+
|
|
1282
|
+
for (const token of tokens) {
|
|
1283
|
+
const character = token[0];
|
|
1284
|
+
|
|
1285
|
+
if (character === ")" && depth === 0) {
|
|
1286
|
+
return start + token.index;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
depth = adjustedParenthesisDepth(character, depth);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
return -1;
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
const nestedReferenceContents = (text: string): string[] => {
|
|
1296
|
+
const contents: string[] = [];
|
|
1297
|
+
let offset = 0;
|
|
1298
|
+
|
|
1299
|
+
while (offset < text.length) {
|
|
1300
|
+
const prefix = text.indexOf("$t(", offset);
|
|
1301
|
+
|
|
1302
|
+
if (prefix === -1) {
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const start = prefix + 3;
|
|
1307
|
+
const end = nestedReferenceEnd(text, start);
|
|
1308
|
+
|
|
1309
|
+
if (end === -1) {
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
contents.push(text.slice(start, end));
|
|
1314
|
+
offset = end + 1;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
return contents;
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
const nestedPluralKeys = (base: string, context: string | null): string[] => {
|
|
1321
|
+
const pluralBase = context === null ? base : `${base}${CONTEXT_SEPARATOR}${context}`;
|
|
1322
|
+
|
|
1323
|
+
return [`${pluralBase}_one`, `${pluralBase}_other`];
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
const nestedReferenceKeys = (content: string): string[] => {
|
|
1327
|
+
const parts = nestedReferenceParts(content);
|
|
1328
|
+
|
|
1329
|
+
if (parts === null) {
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const context = nestedReferenceContext(parts.options);
|
|
1334
|
+
const contextual = context === null
|
|
1335
|
+
? []
|
|
1336
|
+
: [`${parts.base}_${context}`, `${parts.base}${CONTEXT_SEPARATOR}${context}`];
|
|
1337
|
+
|
|
1338
|
+
return NESTED_COUNT.test(parts.options)
|
|
1339
|
+
? [parts.base, ...contextual, ...nestedPluralKeys(parts.base, context)]
|
|
1340
|
+
: [parts.base, ...contextual];
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
const nestedEntryKeys = (text: string): Set<string> => {
|
|
1344
|
+
const keys: Set<string> = new Set();
|
|
1345
|
+
|
|
1346
|
+
for (const content of nestedReferenceContents(text)) {
|
|
1347
|
+
for (const key of nestedReferenceKeys(content)) {
|
|
1348
|
+
keys.add(key);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return keys;
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
const registerNestedKeyLocations = (
|
|
1356
|
+
root: string,
|
|
1357
|
+
references: Map<string, TranslationReferences>,
|
|
1358
|
+
key: string,
|
|
1359
|
+
locations: ExtractedLocation[],
|
|
1360
|
+
): void => {
|
|
1361
|
+
for (const location of locations) {
|
|
1362
|
+
if (location.line === undefined || location.column === undefined) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
const sourceReferences = references.get(resolve(root, location.file));
|
|
1367
|
+
|
|
1368
|
+
if (sourceReferences !== undefined) {
|
|
1369
|
+
registerEntryLocation(key, { column: location.column, line: location.line }, sourceReferences);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
const registerNestedTextLocations = (
|
|
1375
|
+
root: string,
|
|
1376
|
+
references: Map<string, TranslationReferences>,
|
|
1377
|
+
text: string,
|
|
1378
|
+
locations: ExtractedLocation[],
|
|
1379
|
+
): void => {
|
|
1380
|
+
for (const key of nestedEntryKeys(text)) {
|
|
1381
|
+
registerNestedKeyLocations(root, references, key, locations);
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
const registerNestedEntryLocations = (
|
|
1386
|
+
root: string,
|
|
1387
|
+
references: Map<string, TranslationReferences>,
|
|
1388
|
+
entry: ExtractedKey,
|
|
1389
|
+
locations: ExtractedLocation[],
|
|
1390
|
+
): void => {
|
|
1391
|
+
registerNestedTextLocations(root, references, entry.key, locations);
|
|
1392
|
+
|
|
1393
|
+
if (typeof entry.defaultValue === "string") {
|
|
1394
|
+
registerNestedTextLocations(root, references, entry.defaultValue, locations);
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
const didRegisterNestedLocationPass = (
|
|
1399
|
+
root: string,
|
|
1400
|
+
references: Map<string, TranslationReferences>,
|
|
1401
|
+
entries: ExtractedKeysMap,
|
|
1402
|
+
processed: Set<string>,
|
|
1403
|
+
): boolean => {
|
|
1404
|
+
let didRegister = false;
|
|
1405
|
+
|
|
1406
|
+
for (const [mapKey, entry] of entries) {
|
|
1407
|
+
if (processed.has(mapKey)) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const recognized = recognizedEntry(root, references, entry);
|
|
1412
|
+
|
|
1413
|
+
if (recognized !== null) {
|
|
1414
|
+
processed.add(mapKey);
|
|
1415
|
+
registerNestedEntryLocations(root, references, entry, recognized.locations ?? []);
|
|
1416
|
+
didRegister = true;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return didRegister;
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
const registerNestedLocations = (
|
|
1424
|
+
root: string,
|
|
1425
|
+
references: Map<string, TranslationReferences>,
|
|
1426
|
+
entries: ExtractedKeysMap,
|
|
1427
|
+
): void => {
|
|
1428
|
+
const processed: Set<string> = new Set();
|
|
1429
|
+
let hasRegisteredLocations = true;
|
|
1430
|
+
|
|
1431
|
+
while (hasRegisteredLocations) {
|
|
1432
|
+
hasRegisteredLocations = didRegisterNestedLocationPass(root, references, entries, processed);
|
|
1433
|
+
}
|
|
1434
|
+
};
|
|
1435
|
+
|
|
1436
|
+
const retainRecognizedEntries = (
|
|
1437
|
+
root: string,
|
|
1438
|
+
references: Map<string, TranslationReferences>,
|
|
1439
|
+
entries: ExtractedKeysMap,
|
|
1440
|
+
): void => {
|
|
1441
|
+
for (const [mapKey, entry] of entries) {
|
|
1442
|
+
const recognized = recognizedEntry(root, references, entry);
|
|
1443
|
+
|
|
1444
|
+
if (recognized === null) {
|
|
1445
|
+
entries.delete(mapKey);
|
|
1446
|
+
} else {
|
|
1447
|
+
entries.set(mapKey, recognized);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
const fileSourcePointKey = (file: string, line: number, column: number): string =>
|
|
1453
|
+
`${file}\0${sourceLocationKey(line, column)}`;
|
|
1454
|
+
|
|
1455
|
+
const registerExtractedSourcePoint = (
|
|
1456
|
+
root: string,
|
|
1457
|
+
points: Set<string>,
|
|
1458
|
+
location: ExtractedLocation,
|
|
1459
|
+
): void => {
|
|
1460
|
+
if (location.line !== undefined && location.column !== undefined) {
|
|
1461
|
+
points.add(fileSourcePointKey(resolve(root, location.file), location.line, location.column));
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
const extractedSourcePoints = (root: string, entries: ExtractedKeysMap): Set<string> => {
|
|
1466
|
+
const points: Set<string> = new Set();
|
|
1467
|
+
|
|
1468
|
+
for (const entry of entries.values()) {
|
|
1469
|
+
const locations = entry.locations ?? [];
|
|
1470
|
+
|
|
1471
|
+
for (const location of locations) {
|
|
1472
|
+
registerExtractedSourcePoint(root, points, location);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
return points;
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
const hasMissingSourcePoint = (
|
|
1480
|
+
file: string,
|
|
1481
|
+
references: TranslationReferences,
|
|
1482
|
+
extracted: Set<string>,
|
|
1483
|
+
): boolean =>
|
|
1484
|
+
references.locations.values().some((location) => !extracted.has(`${file}\0${location}`));
|
|
1485
|
+
|
|
1486
|
+
const assertAllReferencesExtracted = (
|
|
1487
|
+
root: string,
|
|
1488
|
+
references: Map<string, TranslationReferences>,
|
|
1489
|
+
entries: ExtractedKeysMap,
|
|
1490
|
+
): void => {
|
|
1491
|
+
const extracted = extractedSourcePoints(root, entries);
|
|
1492
|
+
|
|
1493
|
+
for (const [file, sourceReferences] of references) {
|
|
1494
|
+
if (hasMissingSourcePoint(file, sourceReferences, extracted)) {
|
|
1495
|
+
throw new Error("A translation expression could not be extracted safely");
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
|
|
1500
|
+
const findSourceMessages = async (root: string, sourceFiles: string[]): Promise<SourceMessage[]> => {
|
|
1501
|
+
if (sourceFiles.length === 0) {
|
|
1502
|
+
clearI18nResources(root);
|
|
1503
|
+
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
const sources = new Map(sourceFiles.map((file) => [file, readFileSync(file, "utf8")]));
|
|
1508
|
+
const references: Map<string, TranslationReferences> = new Map();
|
|
1509
|
+
|
|
1510
|
+
for (const [file, source] of sources) {
|
|
1511
|
+
references.set(file, assertSupportedTranslationSyntax(file, source));
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
let extracted: ExtractedKeysMap | undefined;
|
|
1515
|
+
const { logger, reports } = quietLogger();
|
|
1516
|
+
|
|
1517
|
+
const capture: Plugin = {
|
|
1518
|
+
name: "gtkx-gettext",
|
|
1519
|
+
onLoad(code, path) {
|
|
1520
|
+
const sourceReferences = references.get(resolve(root, path));
|
|
1521
|
+
|
|
1522
|
+
return sourceReferences === undefined
|
|
1523
|
+
? code
|
|
1524
|
+
: maskUnrecognizedTranslations(code, sourceReferences);
|
|
1525
|
+
},
|
|
1526
|
+
onEnd(keys) {
|
|
1527
|
+
registerNestedLocations(root, references, keys);
|
|
1528
|
+
retainRecognizedEntries(root, references, keys);
|
|
1529
|
+
assertAllReferencesExtracted(root, references, keys);
|
|
1530
|
+
extracted = keys;
|
|
1531
|
+
},
|
|
1532
|
+
};
|
|
1533
|
+
|
|
1534
|
+
const result = await runExtractor(i18nToolkitConfig(root, sourceFiles, [capture]), {
|
|
1535
|
+
logger,
|
|
1536
|
+
quiet: true,
|
|
1537
|
+
syncPrimaryWithDefaults: true,
|
|
1538
|
+
trustDerivedDefaults: true,
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
if (extracted === undefined || reports.length > 0 || result.hasErrors) {
|
|
1542
|
+
throw new Error(reports.join("\n") || "i18next extraction failed");
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
return sourceMessages(extracted.values().toArray())
|
|
1546
|
+
.map((message) => inferLocations(message, sources))
|
|
1547
|
+
.toSorted(compareMessages);
|
|
1548
|
+
};
|
|
1549
|
+
|
|
1550
|
+
const projectPath = (root: string, path: string): string | null => {
|
|
1551
|
+
const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
|
|
1552
|
+
const projectRelative = relative(root, absolute);
|
|
1553
|
+
|
|
1554
|
+
if (!isPathInside(root, absolute)) {
|
|
1555
|
+
return null;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
return toPosixPath(projectRelative);
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
const writePotfiles = (project: CatalogProject, sourceFiles: string[]): string => {
|
|
1562
|
+
const paths = sourceFiles
|
|
1563
|
+
.map((path) => projectPath(project.root, path))
|
|
1564
|
+
.filter((path): path is string => path !== null);
|
|
1565
|
+
|
|
1566
|
+
const sorted = [...new Set(paths)].toSorted((left, right) => left.localeCompare(right));
|
|
1567
|
+
const target = resolve(project.poDir, POTFILES_FILENAME);
|
|
1568
|
+
writeFileSync(target, sorted.length === 0 ? "" : `${sorted.join("\n")}\n`);
|
|
1569
|
+
|
|
1570
|
+
return target;
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
const renderCatalogCall = (message: SourceMessage): string => {
|
|
1574
|
+
const msgid = JSON.stringify(message.singular);
|
|
1575
|
+
|
|
1576
|
+
if (message.plural !== null) {
|
|
1577
|
+
const plural = JSON.stringify(message.plural);
|
|
1578
|
+
|
|
1579
|
+
return message.context === null
|
|
1580
|
+
? `ngettext(${msgid}, ${plural}, 0);`
|
|
1581
|
+
: `npgettext(${JSON.stringify(message.context)}, ${msgid}, ${plural}, 0);`;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
return message.context === null
|
|
1585
|
+
? `gettext(${msgid});`
|
|
1586
|
+
: `pgettext(${JSON.stringify(message.context)}, ${msgid});`;
|
|
1587
|
+
};
|
|
1588
|
+
|
|
1589
|
+
const locatedOwners = (
|
|
1590
|
+
project: CatalogProject,
|
|
1591
|
+
message: SourceMessage,
|
|
1592
|
+
): { path: string; line: number | undefined }[] => {
|
|
1593
|
+
const owners: { path: string; line: number | undefined }[] = [];
|
|
1594
|
+
|
|
1595
|
+
for (const location of message.locations) {
|
|
1596
|
+
const path = projectPath(project.root, location.file);
|
|
1597
|
+
|
|
1598
|
+
if (path !== null) {
|
|
1599
|
+
owners.push({ path, line: location.line });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
const unique = new Map(owners.map((owner) => [`${owner.path}\0${String(owner.line)}`, owner]));
|
|
1604
|
+
|
|
1605
|
+
return unique.values().toArray();
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
const sourceOwners = (
|
|
1609
|
+
project: CatalogProject,
|
|
1610
|
+
message: SourceMessage,
|
|
1611
|
+
): { path: string; line: number | undefined }[] => {
|
|
1612
|
+
const located = locatedOwners(project, message);
|
|
1613
|
+
|
|
1614
|
+
return located.length > 0 ? located : [{ path: SYNTHETIC_FILENAME, line: undefined }];
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
const syntheticEntries = (
|
|
1618
|
+
project: CatalogProject,
|
|
1619
|
+
messages: SourceMessage[],
|
|
1620
|
+
): Map<string, SyntheticEntry[]> => {
|
|
1621
|
+
const entries: Map<string, SyntheticEntry[]> = new Map();
|
|
1622
|
+
|
|
1623
|
+
for (const message of messages) {
|
|
1624
|
+
for (const owner of sourceOwners(project, message)) {
|
|
1625
|
+
const owned = entries.get(owner.path) ?? [];
|
|
1626
|
+
owned.push({ call: renderCatalogCall(message), line: owner.line });
|
|
1627
|
+
entries.set(owner.path, owned);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
if (entries.size === 0) {
|
|
1632
|
+
entries.set(SYNTHETIC_FILENAME, []);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
return entries;
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
const compareSyntheticEntries = (left: SyntheticEntry, right: SyntheticEntry): number =>
|
|
1639
|
+
(left.line ?? Number.MAX_SAFE_INTEGER) - (right.line ?? Number.MAX_SAFE_INTEGER);
|
|
1640
|
+
|
|
1641
|
+
const lastSyntheticLine = (entries: SyntheticEntry[]): number => {
|
|
1642
|
+
let line = 0;
|
|
1643
|
+
|
|
1644
|
+
for (const entry of entries) {
|
|
1645
|
+
line = Math.max(line, entry.line ?? 0);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
return line;
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const appendSyntheticEntry = (lines: string[], entry: SyntheticEntry, requestedLine: number): void => {
|
|
1652
|
+
const index = requestedLine - 1;
|
|
1653
|
+
const existing = lines[index];
|
|
1654
|
+
|
|
1655
|
+
if (existing === undefined || existing.length === 0) {
|
|
1656
|
+
lines[index] = entry.call;
|
|
1657
|
+
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
lines[index] = `${existing} ${entry.call}`;
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
const renderSyntheticSource = (entries: SyntheticEntry[]): string => {
|
|
1665
|
+
const lines: string[] = [];
|
|
1666
|
+
const sorted = entries.toSorted(compareSyntheticEntries);
|
|
1667
|
+
let nextLine = lastSyntheticLine(entries) + 1;
|
|
1668
|
+
|
|
1669
|
+
for (const entry of sorted) {
|
|
1670
|
+
const requestedLine = Math.max(1, entry.line ?? nextLine++);
|
|
1671
|
+
appendSyntheticEntry(lines, entry, requestedLine);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
return `${lines.join("\n")}\n`;
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
const writeSyntheticSources = (
|
|
1678
|
+
workDir: string,
|
|
1679
|
+
project: CatalogProject,
|
|
1680
|
+
messages: SourceMessage[],
|
|
1681
|
+
): string => {
|
|
1682
|
+
const entries = syntheticEntries(project, messages);
|
|
1683
|
+
const paths: string[] = [];
|
|
1684
|
+
|
|
1685
|
+
for (const [path, owned] of entries) {
|
|
1686
|
+
const target = join(workDir, path);
|
|
1687
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
1688
|
+
writeFileSync(target, renderSyntheticSource(owned));
|
|
1689
|
+
paths.push(path);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const potfiles = join(workDir, POTFILES_FILENAME);
|
|
1693
|
+
writeFileSync(potfiles, `${paths.toSorted((left, right) => left.localeCompare(right)).join("\n")}\n`);
|
|
1694
|
+
|
|
1695
|
+
return potfiles;
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
const extractSourceMessages = ({ project, messages, output, workDir }: SourceExtraction): void => {
|
|
1699
|
+
const potfilesPath = writeSyntheticSources(workDir, project, messages);
|
|
1700
|
+
|
|
1701
|
+
runCliTool({
|
|
1702
|
+
tool: "xgettext",
|
|
1703
|
+
args: [
|
|
1704
|
+
"--language=JavaScript",
|
|
1705
|
+
"--from-code=UTF-8",
|
|
1706
|
+
"--force-po",
|
|
1707
|
+
"--keyword=gettext:1",
|
|
1708
|
+
"--keyword=ngettext:1,2",
|
|
1709
|
+
"--keyword=pgettext:1c,2",
|
|
1710
|
+
"--keyword=npgettext:1c,2,3",
|
|
1711
|
+
`--directory=${workDir}`,
|
|
1712
|
+
`--files-from=${potfilesPath}`,
|
|
1713
|
+
`--output=${output}`,
|
|
1714
|
+
],
|
|
1715
|
+
target: output,
|
|
1716
|
+
});
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
const extractMetadataFragment = (project: CatalogProject, input: string, output: string): void => {
|
|
1720
|
+
runCliTool({
|
|
1721
|
+
tool: "msggrep",
|
|
1722
|
+
args: [
|
|
1723
|
+
"--force-po",
|
|
1724
|
+
`--output-file=${output}`,
|
|
1725
|
+
...metadataTemplateFiles(project).map((file) => `--location=${file.relativePath}`),
|
|
1726
|
+
input,
|
|
1727
|
+
],
|
|
1728
|
+
target: input,
|
|
1729
|
+
});
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
const joinMetadataFragment = (output: string, fragment: string): void => {
|
|
1733
|
+
runCliTool({
|
|
1734
|
+
tool: "xgettext",
|
|
1735
|
+
args: ["--language=PO", "--join-existing", "--force-po", `--output=${output}`, fragment],
|
|
1736
|
+
target: output,
|
|
1737
|
+
});
|
|
1738
|
+
};
|
|
1739
|
+
|
|
1740
|
+
const extractCatalogTemplate = ({
|
|
1741
|
+
project,
|
|
1742
|
+
messages,
|
|
1743
|
+
shouldPreserveMetadataMessages,
|
|
1744
|
+
output = resolve(project.poDir, `${project.domain}.pot`),
|
|
1745
|
+
}: CatalogTemplateExtraction): void => {
|
|
1746
|
+
const workDir = mkdtempSync(join(project.poDir, ".gtkx-i18n-"));
|
|
1747
|
+
const source = join(workDir, "source.pot");
|
|
1748
|
+
const hasPreviousTemplate = existsSync(output);
|
|
1749
|
+
|
|
1750
|
+
try {
|
|
1751
|
+
if (shouldPreserveMetadataMessages && hasPreviousTemplate) {
|
|
1752
|
+
const fragment = join(workDir, "metadata.pot");
|
|
1753
|
+
extractMetadataFragment(project, output, fragment);
|
|
1754
|
+
extractSourceMessages({ project, messages, output: source, workDir });
|
|
1755
|
+
joinMetadataFragment(source, fragment);
|
|
1756
|
+
} else {
|
|
1757
|
+
extractSourceMessages({ project, messages, output: source, workDir });
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
replaceCatalogTemplate(source, output);
|
|
1761
|
+
} finally {
|
|
1762
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
const extractSourceCatalogTo = async (
|
|
1767
|
+
project: CatalogProject,
|
|
1768
|
+
paths: string[],
|
|
1769
|
+
output: string,
|
|
1770
|
+
): Promise<void> => {
|
|
1771
|
+
const sourceFiles = normalizedSourceFiles(project.root, paths);
|
|
1772
|
+
const messages = await findSourceMessages(project.root, sourceFiles);
|
|
1773
|
+
writePotfiles(project, sourceFiles);
|
|
1774
|
+
extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages: false, output });
|
|
1775
|
+
};
|
|
1776
|
+
|
|
1777
|
+
const extractSourceCatalog = async (
|
|
1778
|
+
project: CatalogProject,
|
|
1779
|
+
paths: string[],
|
|
1780
|
+
shouldPreserveMetadataMessages = true,
|
|
1781
|
+
): Promise<void> => {
|
|
1782
|
+
const sourceFiles = normalizedSourceFiles(project.root, paths);
|
|
1783
|
+
const messages = await findSourceMessages(project.root, sourceFiles);
|
|
1784
|
+
writePotfiles(project, sourceFiles);
|
|
1785
|
+
extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages });
|
|
606
1786
|
};
|
|
607
1787
|
|
|
608
|
-
export {
|
|
1788
|
+
export { extractSourceCatalog, extractSourceCatalogTo };
|