@gtkx/cli 1.4.0 → 1.6.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.
Files changed (206) hide show
  1. package/README.md +1 -1
  2. package/dist/builder.d.ts +1 -0
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +2 -2
  5. package/dist/builder.js.map +1 -1
  6. package/dist/cli.js +4 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/codegen/freshness.d.ts.map +1 -1
  9. package/dist/codegen/freshness.js +2 -1
  10. package/dist/codegen/freshness.js.map +1 -1
  11. package/dist/codegen/reference.d.ts +17 -0
  12. package/dist/codegen/reference.d.ts.map +1 -0
  13. package/dist/codegen/reference.js +37 -0
  14. package/dist/codegen/reference.js.map +1 -0
  15. package/dist/codegen/report.d.ts.map +1 -1
  16. package/dist/codegen/report.js +17 -3
  17. package/dist/codegen/report.js.map +1 -1
  18. package/dist/codegen/run-codegen.d.ts +4 -0
  19. package/dist/codegen/run-codegen.d.ts.map +1 -1
  20. package/dist/codegen/run-codegen.js +93 -27
  21. package/dist/codegen/run-codegen.js.map +1 -1
  22. package/dist/commands/codegen.js +1 -1
  23. package/dist/commands/codegen.js.map +1 -1
  24. package/dist/commands/docs.d.ts.map +1 -1
  25. package/dist/commands/docs.js +10 -16
  26. package/dist/commands/docs.js.map +1 -1
  27. package/dist/commands/mcp.d.ts +13 -0
  28. package/dist/commands/mcp.d.ts.map +1 -0
  29. package/dist/commands/mcp.js +68 -0
  30. package/dist/commands/mcp.js.map +1 -0
  31. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  32. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  33. package/dist/deploy/freedesktop/localize.js +107 -0
  34. package/dist/deploy/freedesktop/localize.js.map +1 -0
  35. package/dist/deploy/install-hints.d.ts.map +1 -1
  36. package/dist/deploy/install-hints.js +5 -0
  37. package/dist/deploy/install-hints.js.map +1 -1
  38. package/dist/deploy/notices/gtkx.js +1 -1
  39. package/dist/deploy/notices/gtkx.js.map +1 -1
  40. package/dist/deploy/notices/libraries.d.ts.map +1 -1
  41. package/dist/deploy/notices/libraries.js +2 -1
  42. package/dist/deploy/notices/libraries.js.map +1 -1
  43. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  44. package/dist/deploy/payload/launcher.js +2 -0
  45. package/dist/deploy/payload/launcher.js.map +1 -1
  46. package/dist/deploy/payload/stage.d.ts +1 -1
  47. package/dist/deploy/payload/stage.d.ts.map +1 -1
  48. package/dist/deploy/payload/stage.js +20 -11
  49. package/dist/deploy/payload/stage.js.map +1 -1
  50. package/dist/deploy/run-deploy.d.ts.map +1 -1
  51. package/dist/deploy/run-deploy.js +87 -15
  52. package/dist/deploy/run-deploy.js.map +1 -1
  53. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  54. package/dist/deploy/settings/libraries.js +7 -9
  55. package/dist/deploy/settings/libraries.js.map +1 -1
  56. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  57. package/dist/deploy/targets/flatpak-source.js +21 -10
  58. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  59. package/dist/deploy/tools.d.ts +6 -1
  60. package/dist/deploy/tools.d.ts.map +1 -1
  61. package/dist/deploy/tools.js +27 -4
  62. package/dist/deploy/tools.js.map +1 -1
  63. package/dist/dev/locale-dir.d.ts +3 -0
  64. package/dist/dev/locale-dir.d.ts.map +1 -0
  65. package/dist/dev/locale-dir.js +17 -0
  66. package/dist/dev/locale-dir.js.map +1 -0
  67. package/dist/dev/runner-main.d.ts.map +1 -1
  68. package/dist/dev/runner-main.js +4 -1
  69. package/dist/dev/runner-main.js.map +1 -1
  70. package/dist/dev/runner.d.ts.map +1 -1
  71. package/dist/dev/runner.js +6 -0
  72. package/dist/dev/runner.js.map +1 -1
  73. package/dist/dev/vite-dev-server.d.ts +1 -0
  74. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  75. package/dist/dev/vite-dev-server.js.map +1 -1
  76. package/dist/i18n/binding-extractor.d.ts +30 -0
  77. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  78. package/dist/i18n/binding-extractor.js +1324 -0
  79. package/dist/i18n/binding-extractor.js.map +1 -0
  80. package/dist/i18n/catalog-template.d.ts +3 -0
  81. package/dist/i18n/catalog-template.d.ts.map +1 -0
  82. package/dist/i18n/catalog-template.js +18 -0
  83. package/dist/i18n/catalog-template.js.map +1 -0
  84. package/dist/i18n/catalogs.d.ts +21 -0
  85. package/dist/i18n/catalogs.d.ts.map +1 -0
  86. package/dist/i18n/catalogs.js +139 -0
  87. package/dist/i18n/catalogs.js.map +1 -0
  88. package/dist/i18n/extractor-plugin.d.ts +8 -0
  89. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  90. package/dist/i18n/extractor-plugin.js +247 -0
  91. package/dist/i18n/extractor-plugin.js.map +1 -0
  92. package/dist/i18n/metadata-templates.d.ts +11 -0
  93. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  94. package/dist/i18n/metadata-templates.js +11 -0
  95. package/dist/i18n/metadata-templates.js.map +1 -0
  96. package/dist/i18n/source-messages.d.ts +23 -0
  97. package/dist/i18n/source-messages.d.ts.map +1 -0
  98. package/dist/i18n/source-messages.js +403 -0
  99. package/dist/i18n/source-messages.js.map +1 -0
  100. package/dist/i18n/types.d.ts +10 -0
  101. package/dist/i18n/types.d.ts.map +1 -0
  102. package/dist/i18n/types.js +108 -0
  103. package/dist/i18n/types.js.map +1 -0
  104. package/dist/internal/agent-rules.d.ts +7 -0
  105. package/dist/internal/agent-rules.d.ts.map +1 -0
  106. package/dist/internal/agent-rules.js +106 -0
  107. package/dist/internal/agent-rules.js.map +1 -0
  108. package/dist/internal/docs-elements.d.ts +8 -0
  109. package/dist/internal/docs-elements.d.ts.map +1 -0
  110. package/dist/internal/docs-elements.js +12 -0
  111. package/dist/internal/docs-elements.js.map +1 -0
  112. package/dist/internal/source-imports.d.ts +5 -1
  113. package/dist/internal/source-imports.d.ts.map +1 -1
  114. package/dist/internal/source-imports.js +25 -47
  115. package/dist/internal/source-imports.js.map +1 -1
  116. package/dist/mcp/client-config.d.ts +22 -0
  117. package/dist/mcp/client-config.d.ts.map +1 -0
  118. package/dist/mcp/client-config.js +94 -0
  119. package/dist/mcp/client-config.js.map +1 -0
  120. package/dist/mcp/client.d.ts +0 -1
  121. package/dist/mcp/client.d.ts.map +1 -1
  122. package/dist/mcp/client.js +11 -39
  123. package/dist/mcp/client.js.map +1 -1
  124. package/dist/mcp/handlers.d.ts +2 -1
  125. package/dist/mcp/handlers.d.ts.map +1 -1
  126. package/dist/mcp/handlers.js.map +1 -1
  127. package/dist/settings/render.d.ts +4 -1
  128. package/dist/settings/render.d.ts.map +1 -1
  129. package/dist/settings/render.js +11 -2
  130. package/dist/settings/render.js.map +1 -1
  131. package/dist/settings/schema.d.ts.map +1 -1
  132. package/dist/settings/schema.js +3 -1
  133. package/dist/settings/schema.js.map +1 -1
  134. package/dist/vite-plugins/animated.d.ts +5 -0
  135. package/dist/vite-plugins/animated.d.ts.map +1 -0
  136. package/dist/vite-plugins/animated.js +325 -0
  137. package/dist/vite-plugins/animated.js.map +1 -0
  138. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  139. package/dist/vite-plugins/asset-imports.js +4 -14
  140. package/dist/vite-plugins/asset-imports.js.map +1 -1
  141. package/dist/vite-plugins/i18n.d.ts +5 -0
  142. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  143. package/dist/vite-plugins/i18n.js +88 -0
  144. package/dist/vite-plugins/i18n.js.map +1 -0
  145. package/dist/vite-plugins/index.d.ts +1 -1
  146. package/dist/vite-plugins/index.d.ts.map +1 -1
  147. package/dist/vite-plugins/index.js +5 -1
  148. package/dist/vite-plugins/index.js.map +1 -1
  149. package/dist/vite-plugins/resources.d.ts.map +1 -1
  150. package/dist/vite-plugins/resources.js +12 -57
  151. package/dist/vite-plugins/resources.js.map +1 -1
  152. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  153. package/dist/vite-plugins/self-contained.js +128 -135
  154. package/dist/vite-plugins/self-contained.js.map +1 -1
  155. package/dist/vite-plugins/settings-worker-env.js +2 -2
  156. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  157. package/dist/vite-plugins/settings.d.ts.map +1 -1
  158. package/dist/vite-plugins/settings.js +2 -1
  159. package/dist/vite-plugins/settings.js.map +1 -1
  160. package/package.json +11 -9
  161. package/src/builder.ts +3 -2
  162. package/src/cli.ts +5 -0
  163. package/src/codegen/freshness.ts +2 -1
  164. package/src/codegen/reference.ts +59 -0
  165. package/src/codegen/report.ts +18 -3
  166. package/src/codegen/run-codegen.ts +134 -31
  167. package/src/commands/codegen.ts +1 -1
  168. package/src/commands/docs.ts +10 -28
  169. package/src/commands/mcp.ts +81 -0
  170. package/src/deploy/freedesktop/localize.ts +144 -0
  171. package/src/deploy/install-hints.ts +5 -0
  172. package/src/deploy/notices/gtkx.ts +1 -1
  173. package/src/deploy/notices/libraries.ts +2 -1
  174. package/src/deploy/payload/launcher.ts +2 -0
  175. package/src/deploy/payload/stage.ts +35 -10
  176. package/src/deploy/run-deploy.ts +128 -13
  177. package/src/deploy/settings/libraries.ts +7 -10
  178. package/src/deploy/targets/flatpak-source.ts +33 -10
  179. package/src/deploy/tools.ts +36 -3
  180. package/src/dev/locale-dir.ts +22 -0
  181. package/src/dev/runner-main.ts +4 -1
  182. package/src/dev/runner.ts +8 -0
  183. package/src/dev/vite-dev-server.ts +1 -0
  184. package/src/i18n/binding-extractor.ts +1969 -0
  185. package/src/i18n/catalog-template.ts +24 -0
  186. package/src/i18n/catalogs.ts +216 -0
  187. package/src/i18n/extractor-plugin.ts +413 -0
  188. package/src/i18n/metadata-templates.ts +29 -0
  189. package/src/i18n/source-messages.ts +608 -0
  190. package/src/i18n/types.ts +154 -0
  191. package/src/internal/agent-rules.ts +135 -0
  192. package/src/internal/docs-elements.ts +21 -0
  193. package/src/internal/source-imports.ts +31 -60
  194. package/src/mcp/client-config.ts +130 -0
  195. package/src/mcp/client.ts +17 -44
  196. package/src/mcp/handlers.ts +7 -6
  197. package/src/settings/render.ts +16 -2
  198. package/src/settings/schema.ts +3 -1
  199. package/src/vite-plugins/animated.ts +486 -0
  200. package/src/vite-plugins/asset-imports.ts +5 -17
  201. package/src/vite-plugins/i18n.ts +146 -0
  202. package/src/vite-plugins/index.ts +5 -0
  203. package/src/vite-plugins/resources.ts +12 -74
  204. package/src/vite-plugins/self-contained.ts +150 -169
  205. package/src/vite-plugins/settings-worker-env.ts +2 -2
  206. package/src/vite-plugins/settings.ts +2 -1
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync, renameSync } from "node:fs";
2
+
3
+ const POT_CREATION_DATE = /^"POT-Creation-Date: .*\\n"$/m;
4
+
5
+ const isUnchangedTemplate = (previous: string, next: string): boolean => {
6
+ const previousDate = POT_CREATION_DATE.exec(previous)?.[0];
7
+
8
+ return previousDate !== undefined && next.replace(POT_CREATION_DATE, () => previousDate) === previous;
9
+ };
10
+
11
+ const replaceCatalogTemplate = (source: string, output: string): void => {
12
+ if (existsSync(output)) {
13
+ const previous = readFileSync(output, "utf8");
14
+ const next = readFileSync(source, "utf8");
15
+
16
+ if (next === previous || isUnchangedTemplate(previous, next)) {
17
+ return;
18
+ }
19
+ }
20
+
21
+ renameSync(source, output);
22
+ };
23
+
24
+ export { replaceCatalogTemplate };
@@ -0,0 +1,216 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ } from "node:fs";
11
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import { runCliTool } from "../internal/run-cli-tool.js";
13
+
14
+ type Catalog = {
15
+ locale: string;
16
+ path: string;
17
+ };
18
+
19
+ type CatalogProject = {
20
+ catalogs: Catalog[];
21
+ domain: string;
22
+ linguasPath: string;
23
+ locales: string[];
24
+ poDir: string;
25
+ poFiles: string[];
26
+ root: string;
27
+ };
28
+
29
+ type PreparedCatalog = {
30
+ isChanged: boolean;
31
+ output: string;
32
+ target: string;
33
+ };
34
+
35
+ const PO_DIRNAME = "po";
36
+ const LOCALE_DIRNAME = "locale";
37
+ const LINGUAS_FILENAME = "LINGUAS";
38
+ const LOCALE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.@+-]*$/;
39
+ const DOMAIN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
40
+ const MODE_MASK = 0o7777;
41
+
42
+ const parseLocales = (linguasPath: string): string[] => {
43
+ const source = readFileSync(linguasPath, "utf8");
44
+
45
+ const tokens = source
46
+ .split(/\r?\n/)
47
+ .flatMap((line) => line.replace(/#.*/, "").trim().split(/\s+/))
48
+ .filter((token) => token.length > 0);
49
+
50
+ const seen: Set<string> = new Set();
51
+
52
+ for (const token of tokens) {
53
+ if (!LOCALE_PATTERN.test(token) || seen.has(token)) {
54
+ throw new Error(`Invalid locale in ${linguasPath}: ${token}`);
55
+ }
56
+
57
+ seen.add(token);
58
+ }
59
+
60
+ return tokens;
61
+ };
62
+
63
+ const requireFile = (path: string): void => {
64
+ if (!existsSync(path) || !statSync(path).isFile()) {
65
+ throw new Error(`Missing translation catalog: ${path}`);
66
+ }
67
+ };
68
+
69
+ const requiresCatalogInitialization = (project: CatalogProject): boolean =>
70
+ project.catalogs.some((catalog) => !existsSync(catalog.path));
71
+
72
+ const resolveCatalogProject = (root: string, domain: string): CatalogProject | null => {
73
+ const projectRoot = resolve(root);
74
+ const poDir = join(projectRoot, PO_DIRNAME);
75
+
76
+ if (!existsSync(poDir)) {
77
+ return null;
78
+ }
79
+
80
+ if (!statSync(poDir).isDirectory()) {
81
+ throw new Error(`Translation source is not a directory: ${poDir}`);
82
+ }
83
+
84
+ if (!DOMAIN_PATTERN.test(domain)) {
85
+ throw new Error(`Invalid translation domain: ${domain}`);
86
+ }
87
+
88
+ const linguasPath = join(poDir, LINGUAS_FILENAME);
89
+ requireFile(linguasPath);
90
+ const locales = parseLocales(linguasPath);
91
+ const catalogs = locales.map((locale) => ({ locale, path: join(poDir, `${locale}.po`) }));
92
+
93
+ return {
94
+ catalogs,
95
+ domain,
96
+ linguasPath,
97
+ locales,
98
+ poDir,
99
+ poFiles: catalogs.map((catalog) => catalog.path),
100
+ root: projectRoot,
101
+ };
102
+ };
103
+
104
+ const compileCatalogs = (project: CatalogProject, outputDir: string): string[] => {
105
+ const localeDir = resolve(outputDir);
106
+
107
+ for (const catalog of project.catalogs) {
108
+ requireFile(catalog.path);
109
+ }
110
+
111
+ return project.catalogs.map((catalog) => {
112
+ const output = join(localeDir, catalog.locale, "LC_MESSAGES", `${project.domain}.mo`);
113
+ mkdirSync(dirname(output), { recursive: true });
114
+
115
+ runCliTool({
116
+ tool: "msgfmt",
117
+ args: ["--check", "--output-file", output, catalog.path],
118
+ target: catalog.path,
119
+ });
120
+
121
+ return output;
122
+ });
123
+ };
124
+
125
+ const initializeCatalog = (catalog: Catalog, template: string, output: string): void => {
126
+ runCliTool({
127
+ tool: "msginit",
128
+ args: [
129
+ "--no-translator",
130
+ "--no-wrap",
131
+ `--locale=${catalog.locale}`,
132
+ `--input=${template}`,
133
+ `--output-file=${output}`,
134
+ ],
135
+ target: catalog.path,
136
+ });
137
+ };
138
+
139
+ const mergeCatalog = (catalog: Catalog, template: string, output: string): void => {
140
+ requireFile(catalog.path);
141
+
142
+ runCliTool({
143
+ tool: "msgmerge",
144
+ args: ["--quiet", "--no-wrap", `--output-file=${output}`, catalog.path, template],
145
+ target: catalog.path,
146
+ });
147
+
148
+ chmodSync(output, statSync(catalog.path).mode & MODE_MASK);
149
+ };
150
+
151
+ const prepareCatalog = (
152
+ catalog: Catalog,
153
+ index: number,
154
+ template: string,
155
+ stagingDir: string,
156
+ ): PreparedCatalog => {
157
+ const output = join(stagingDir, `${String(index)}.po`);
158
+ const isExisting = existsSync(catalog.path);
159
+
160
+ if (isExisting) {
161
+ mergeCatalog(catalog, template, output);
162
+ } else {
163
+ initializeCatalog(catalog, template, output);
164
+ }
165
+
166
+ return {
167
+ isChanged: !isExisting || !readFileSync(output).equals(readFileSync(catalog.path)),
168
+ output,
169
+ target: catalog.path,
170
+ };
171
+ };
172
+
173
+ const synchronizeCatalogs = (project: CatalogProject): void => {
174
+ if (project.catalogs.length === 0) {
175
+ return;
176
+ }
177
+
178
+ const template = join(project.poDir, `${project.domain}.pot`);
179
+ requireFile(template);
180
+ const stagingDir = mkdtempSync(join(project.poDir, ".gtkx-catalog-"));
181
+
182
+ try {
183
+ const merged = project.catalogs.map((catalog, index) =>
184
+ prepareCatalog(catalog, index, template, stagingDir));
185
+
186
+ for (const catalog of merged) {
187
+ if (catalog.isChanged) {
188
+ renameSync(catalog.output, catalog.target);
189
+ }
190
+ }
191
+ } finally {
192
+ rmSync(stagingDir, { recursive: true, force: true });
193
+ }
194
+ };
195
+
196
+ const isCatalogSource = (root: string, path: string): boolean => {
197
+ const projectRoot = resolve(root);
198
+ const sourcePath = isAbsolute(path) ? resolve(path) : resolve(projectRoot, path);
199
+ const poPath = relative(join(projectRoot, PO_DIRNAME), sourcePath);
200
+
201
+ if (poPath === "" || poPath === ".." || poPath.startsWith(`..${sep}`) || isAbsolute(poPath)) {
202
+ return false;
203
+ }
204
+
205
+ return !poPath.includes(sep) && (poPath === LINGUAS_FILENAME || poPath.endsWith(".po"));
206
+ };
207
+
208
+ export {
209
+ type CatalogProject,
210
+ LOCALE_DIRNAME,
211
+ compileCatalogs,
212
+ isCatalogSource,
213
+ requiresCatalogInitialization,
214
+ resolveCatalogProject,
215
+ synchronizeCatalogs,
216
+ };
@@ -0,0 +1,413 @@
1
+ import type { Plugin, PluginContext } from "i18next-cli";
2
+ import {
3
+ type BindingExtraction,
4
+ extractBindings,
5
+ type ScopeInfo,
6
+ } from "./binding-extractor.js";
7
+
8
+ type GtkxExtractorPlugin = {
9
+ plugin: Plugin;
10
+ transComponents: string[];
11
+ };
12
+
13
+ type ExtractorState = {
14
+ extraction: BindingExtraction;
15
+ sentinel: string;
16
+ };
17
+
18
+ type ReconciledNodes = {
19
+ blocked: Set<string>;
20
+ calls: Set<string>;
21
+ trans: Set<string>;
22
+ };
23
+
24
+ const emptyExtraction = (): BindingExtraction => ({
25
+ blockedGetFixedT: new Set(),
26
+ calls: new Map(),
27
+ errors: [],
28
+ messages: [],
29
+ trans: new Map(),
30
+ });
31
+
32
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
33
+ typeof value === "object" && value !== null;
34
+
35
+ const nodeSpanKey = (node: Record<string, unknown>): string | undefined => {
36
+ if (!isRecord(node.span)) {
37
+ return;
38
+ }
39
+
40
+ return typeof node.span.start === "number" && typeof node.span.end === "number"
41
+ ? `${String(node.span.start)}:${String(node.span.end)}`
42
+ : undefined;
43
+ };
44
+
45
+ const identifier = (value: string, source: unknown): Record<string, unknown> => {
46
+ const span = isRecord(source) && isRecord(source.span) ? source.span : { end: 0, start: 0 };
47
+
48
+ return {
49
+ ctxt: 0,
50
+ optional: false,
51
+ span,
52
+ type: "Identifier",
53
+ value,
54
+ };
55
+ };
56
+
57
+ const sentinelFor = (code: string): string => {
58
+ let sentinel = "__gtkxI18nSentinel";
59
+
60
+ while (new RegExp(String.raw`\b${sentinel}\b`, "u").test(code)) {
61
+ sentinel += "_";
62
+ }
63
+
64
+ return sentinel;
65
+ };
66
+
67
+ const canonicalSource = (code: string): string => {
68
+ const hasBom = code.startsWith("\u{FEFF}");
69
+ let hashbangIndex = -1;
70
+
71
+ if (code.startsWith("#!")) {
72
+ hashbangIndex = 0;
73
+ } else if (hasBom && code.startsWith("#!", 1)) {
74
+ hashbangIndex = 1;
75
+ }
76
+
77
+ let canonical = hasBom ? ` ${code.slice(1)}` : code;
78
+
79
+ if (hashbangIndex !== -1) {
80
+ canonical = `${canonical.slice(0, hashbangIndex)}//${canonical.slice(hashbangIndex + 2)}`;
81
+ }
82
+
83
+ return `;${canonical}`;
84
+ };
85
+
86
+ const replaceCallCallee = (node: Record<string, unknown>, sentinel: string): void => {
87
+ node.callee = identifier(sentinel, node.callee);
88
+ };
89
+
90
+ const replaceJsxName = (element: unknown, sentinel: string): void => {
91
+ if (!isRecord(element)) {
92
+ return;
93
+ }
94
+
95
+ if (isRecord(element.opening)) {
96
+ element.opening.name = identifier(sentinel, element.opening.name);
97
+ }
98
+
99
+ if (isRecord(element.closing)) {
100
+ element.closing.name = identifier(sentinel, element.closing.name);
101
+ }
102
+ };
103
+
104
+ const isJsxAttribute = (value: unknown): value is Record<string, unknown> =>
105
+ isRecord(value) && value.type === "JSXAttribute";
106
+
107
+ const isTAttributeName = (value: unknown): boolean =>
108
+ isRecord(value) && value.value === "t";
109
+
110
+ const isExpressionContainer = (value: unknown): value is Record<string, unknown> =>
111
+ isRecord(value) && value.type === "JSXExpressionContainer";
112
+
113
+ const transTExpression = (attribute: unknown): Record<string, unknown> | undefined => {
114
+ if (!isJsxAttribute(attribute)) {
115
+ return;
116
+ }
117
+
118
+ if (!isTAttributeName(attribute.name)) {
119
+ return;
120
+ }
121
+
122
+ if (!isExpressionContainer(attribute.value)) {
123
+ return;
124
+ }
125
+
126
+ return attribute.value;
127
+ };
128
+
129
+ const replaceTransTProp = (
130
+ element: Record<string, unknown>,
131
+ sentinel: string,
132
+ info: ScopeInfo | null,
133
+ context: PluginContext,
134
+ ): void => {
135
+ if (info === null || !isRecord(element.opening) || !Array.isArray(element.opening.attributes)) {
136
+ return;
137
+ }
138
+
139
+ for (const attribute of element.opening.attributes) {
140
+ const expression = transTExpression(attribute);
141
+
142
+ if (expression === undefined) {
143
+ continue;
144
+ }
145
+
146
+ context.setVarInScope(sentinel, info);
147
+ expression.expression = identifier(sentinel, expression.expression);
148
+
149
+ return;
150
+ }
151
+ };
152
+
153
+ const reconcileBlockedNode = (
154
+ value: Record<string, unknown>,
155
+ key: string,
156
+ extraction: BindingExtraction,
157
+ reconciled: ReconciledNodes,
158
+ ): void => {
159
+ if (value.type !== "VariableDeclarator") {
160
+ return;
161
+ }
162
+
163
+ if (!extraction.blockedGetFixedT.has(key)) {
164
+ return;
165
+ }
166
+
167
+ blockUnrelatedGetFixedT(value);
168
+ reconciled.blocked.add(key);
169
+ };
170
+
171
+ const reconcileCallNode = (
172
+ value: Record<string, unknown>,
173
+ key: string,
174
+ extraction: BindingExtraction,
175
+ reconciled: ReconciledNodes,
176
+ ): void => {
177
+ if (value.type === "CallExpression" && extraction.calls.has(key)) {
178
+ reconciled.calls.add(key);
179
+ }
180
+ };
181
+
182
+ const reconcileTransNode = (
183
+ value: Record<string, unknown>,
184
+ key: string,
185
+ extraction: BindingExtraction,
186
+ reconciled: ReconciledNodes,
187
+ ): void => {
188
+ if (value.type === "JSXElement" && extraction.trans.has(key)) {
189
+ reconciled.trans.add(key);
190
+ }
191
+ };
192
+
193
+ const reconcileNode = (
194
+ value: Record<string, unknown>,
195
+ extraction: BindingExtraction,
196
+ reconciled: ReconciledNodes,
197
+ ): void => {
198
+ const key = nodeSpanKey(value);
199
+
200
+ if (key === undefined) {
201
+ return;
202
+ }
203
+
204
+ reconcileBlockedNode(value, key, extraction, reconciled);
205
+ reconcileCallNode(value, key, extraction, reconciled);
206
+ reconcileTransNode(value, key, extraction, reconciled);
207
+ };
208
+
209
+ const visitModuleChild = (
210
+ child: unknown,
211
+ extraction: BindingExtraction,
212
+ reconciled: ReconciledNodes,
213
+ ): void => {
214
+ if (Array.isArray(child)) {
215
+ for (const item of child) {
216
+ visitModuleNode(item, extraction, reconciled);
217
+ }
218
+
219
+ return;
220
+ }
221
+
222
+ visitModuleNode(child, extraction, reconciled);
223
+ };
224
+
225
+ const visitModuleNode = (
226
+ value: unknown,
227
+ extraction: BindingExtraction,
228
+ reconciled: ReconciledNodes,
229
+ ): void => {
230
+ if (!isRecord(value)) {
231
+ return;
232
+ }
233
+
234
+ reconcileNode(value, extraction, reconciled);
235
+
236
+ for (const [name, child] of Object.entries(value)) {
237
+ if (name !== "span") {
238
+ visitModuleChild(child, extraction, reconciled);
239
+ }
240
+ }
241
+ };
242
+
243
+ const wasExtractionReconciled = (
244
+ extraction: BindingExtraction,
245
+ reconciled: ReconciledNodes,
246
+ ): boolean =>
247
+ reconciled.blocked.size === extraction.blockedGetFixedT.size &&
248
+ reconciled.calls.size === extraction.calls.size &&
249
+ reconciled.trans.size === extraction.trans.size;
250
+
251
+ const blockUnrelatedGetFixedT = (declarator: Record<string, unknown>): void => {
252
+ if (!isRecord(declarator.init) || !isRecord(declarator.init.callee)) {
253
+ return;
254
+ }
255
+
256
+ const callee = declarator.init.callee;
257
+
258
+ if (
259
+ callee.type === "MemberExpression" &&
260
+ isRecord(callee.property) &&
261
+ callee.property.value === "getFixedT"
262
+ ) {
263
+ callee.property.value = "__gtkxIgnoredGetFixedT";
264
+ }
265
+ };
266
+
267
+ const prepareModule = (
268
+ module: Record<string, unknown>,
269
+ extraction: BindingExtraction,
270
+ context: PluginContext,
271
+ ): void => {
272
+ const reconciled: ReconciledNodes = {
273
+ blocked: new Set(),
274
+ calls: new Set(),
275
+ trans: new Set(),
276
+ };
277
+
278
+ visitModuleNode(module, extraction, reconciled);
279
+
280
+ if (!wasExtractionReconciled(extraction, reconciled)) {
281
+ context.logger.warn("GTKX could not reconcile the binding-aware and i18next syntax trees");
282
+ }
283
+ };
284
+
285
+ const submitMessages = (
286
+ extraction: BindingExtraction,
287
+ context: PluginContext,
288
+ ): void => {
289
+ for (const error of extraction.errors) {
290
+ context.logger.warn(error);
291
+ }
292
+
293
+ for (const message of extraction.messages) {
294
+ context.addKey({
295
+ key: message.key,
296
+ defaultValue: message.defaultValue,
297
+ explicitDefault: message.explicitDefault,
298
+ locations: [message.location],
299
+ ...(message.ns !== undefined && { ns: message.ns }),
300
+ });
301
+ }
302
+ };
303
+
304
+ const loadSource = async (
305
+ state: ExtractorState,
306
+ transComponents: string[],
307
+ code: string,
308
+ path: string,
309
+ ): Promise<string> => {
310
+ const canonical = canonicalSource(code);
311
+ state.extraction = await extractBindings(canonical, path);
312
+ state.sentinel = sentinelFor(canonical);
313
+ transComponents.splice(0, transComponents.length, state.sentinel);
314
+
315
+ return canonical;
316
+ };
317
+
318
+ const visitCall = (
319
+ state: ExtractorState,
320
+ node: Record<string, unknown>,
321
+ key: string,
322
+ context: PluginContext,
323
+ ): void => {
324
+ const call = state.extraction.calls.get(key);
325
+
326
+ if (call !== undefined) {
327
+ context.setVarInScope(state.sentinel, call.info);
328
+ replaceCallCallee(node, state.sentinel);
329
+ }
330
+ };
331
+
332
+ const visitTrans = (
333
+ state: ExtractorState,
334
+ node: Record<string, unknown>,
335
+ key: string,
336
+ context: PluginContext,
337
+ ): void => {
338
+ const transInfo = state.extraction.trans.get(key);
339
+
340
+ if (transInfo !== undefined) {
341
+ const tSentinel = `${state.sentinel}T`;
342
+ replaceJsxName(node, state.sentinel);
343
+ replaceTransTProp(node, tSentinel, transInfo, context);
344
+ }
345
+ };
346
+
347
+ const visitMatchedNode = (
348
+ state: ExtractorState,
349
+ node: Record<string, unknown>,
350
+ key: string,
351
+ context: PluginContext,
352
+ ): void => {
353
+ if (node.type === "VariableDeclarator" && state.extraction.blockedGetFixedT.has(key)) {
354
+ blockUnrelatedGetFixedT(node);
355
+
356
+ return;
357
+ }
358
+
359
+ if (node.type === "CallExpression") {
360
+ visitCall(state, node, key, context);
361
+
362
+ return;
363
+ }
364
+
365
+ if (node.type === "JSXElement") {
366
+ visitTrans(state, node, key, context);
367
+ }
368
+ };
369
+
370
+ const visitNode = (
371
+ state: ExtractorState,
372
+ node: unknown,
373
+ context: PluginContext,
374
+ ): void => {
375
+ if (!isRecord(node)) {
376
+ return;
377
+ }
378
+
379
+ if (node.type === "Module") {
380
+ prepareModule(node, state.extraction, context);
381
+ submitMessages(state.extraction, context);
382
+
383
+ return;
384
+ }
385
+
386
+ const key = nodeSpanKey(node);
387
+
388
+ if (key !== undefined) {
389
+ visitMatchedNode(state, node, key, context);
390
+ }
391
+ };
392
+
393
+ const gtkxExtractorPlugin = (): GtkxExtractorPlugin => {
394
+ const state: ExtractorState = {
395
+ extraction: emptyExtraction(),
396
+ sentinel: "__gtkxI18nSentinel",
397
+ };
398
+
399
+ const transComponents: string[] = [];
400
+
401
+ const plugin: Plugin = {
402
+ name: "gtkx-react-i18next",
403
+ onLoad: loadSource.bind(undefined, state, transComponents),
404
+ onVisitNode: visitNode.bind(undefined, state),
405
+ };
406
+
407
+ return {
408
+ plugin,
409
+ transComponents,
410
+ };
411
+ };
412
+
413
+ export { gtkxExtractorPlugin };
@@ -0,0 +1,29 @@
1
+ import { join, relative } from "node:path";
2
+ import type { CatalogProject } from "./catalogs.js";
3
+
4
+ type MetadataTemplateKind = "desktop" | "metainfo.xml" | "mime.xml";
5
+
6
+ type MetadataTemplateFile = {
7
+ kind: MetadataTemplateKind;
8
+ path: string;
9
+ relativePath: string;
10
+ };
11
+
12
+ const METADATA_TEMPLATE_KINDS: MetadataTemplateKind[] = ["desktop", "metainfo.xml", "mime.xml"];
13
+ const METADATA_TEMPLATE_DIRNAME = ".gtkx-metadata";
14
+
15
+ const metadataTemplateFile = (project: CatalogProject, kind: MetadataTemplateKind): MetadataTemplateFile => {
16
+ const path = join(project.poDir, METADATA_TEMPLATE_DIRNAME, `${project.domain}.template.${kind}`);
17
+ const relativePath = relative(project.root, path).replaceAll("\\", "/");
18
+
19
+ return { kind, path, relativePath };
20
+ };
21
+
22
+ const metadataTemplateFiles = (project: CatalogProject): MetadataTemplateFile[] =>
23
+ METADATA_TEMPLATE_KINDS.map((kind) => metadataTemplateFile(project, kind));
24
+
25
+ export {
26
+ type MetadataTemplateKind,
27
+ metadataTemplateFile,
28
+ metadataTemplateFiles,
29
+ };