@gtkx/cli 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +5 -4
  2. package/dist/builder.d.ts +1 -1
  3. package/dist/builder.d.ts.map +1 -1
  4. package/dist/builder.js +6 -6
  5. package/dist/builder.js.map +1 -1
  6. package/dist/codegen/run-codegen.d.ts +3 -2
  7. package/dist/codegen/run-codegen.d.ts.map +1 -1
  8. package/dist/codegen/run-codegen.js +41 -12
  9. package/dist/codegen/run-codegen.js.map +1 -1
  10. package/dist/commands/build.d.ts +0 -4
  11. package/dist/commands/build.d.ts.map +1 -1
  12. package/dist/commands/build.js +0 -5
  13. package/dist/commands/build.js.map +1 -1
  14. package/dist/commands/codegen.d.ts.map +1 -1
  15. package/dist/commands/codegen.js +2 -4
  16. package/dist/commands/codegen.js.map +1 -1
  17. package/dist/deploy/depends.d.ts +2 -2
  18. package/dist/deploy/depends.d.ts.map +1 -1
  19. package/dist/deploy/depends.js +30 -15
  20. package/dist/deploy/depends.js.map +1 -1
  21. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  22. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  23. package/dist/deploy/freedesktop/localize.js +107 -0
  24. package/dist/deploy/freedesktop/localize.js.map +1 -0
  25. package/dist/deploy/install-hints.d.ts.map +1 -1
  26. package/dist/deploy/install-hints.js +5 -0
  27. package/dist/deploy/install-hints.js.map +1 -1
  28. package/dist/deploy/nfpm/config.js +1 -1
  29. package/dist/deploy/nfpm/config.js.map +1 -1
  30. package/dist/deploy/node-runtime/elf.d.ts +1 -1
  31. package/dist/deploy/node-runtime/elf.d.ts.map +1 -1
  32. package/dist/deploy/node-runtime/elf.js +2 -2
  33. package/dist/deploy/node-runtime/elf.js.map +1 -1
  34. package/dist/deploy/node-runtime/index.js +2 -2
  35. package/dist/deploy/node-runtime/index.js.map +1 -1
  36. package/dist/deploy/notices/collect.d.ts +3 -1
  37. package/dist/deploy/notices/collect.d.ts.map +1 -1
  38. package/dist/deploy/notices/collect.js +2 -2
  39. package/dist/deploy/notices/collect.js.map +1 -1
  40. package/dist/deploy/notices/packages.d.ts +2 -1
  41. package/dist/deploy/notices/packages.d.ts.map +1 -1
  42. package/dist/deploy/notices/packages.js +4 -27
  43. package/dist/deploy/notices/packages.js.map +1 -1
  44. package/dist/deploy/payload/icons.d.ts +7 -3
  45. package/dist/deploy/payload/icons.d.ts.map +1 -1
  46. package/dist/deploy/payload/icons.js +46 -28
  47. package/dist/deploy/payload/icons.js.map +1 -1
  48. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  49. package/dist/deploy/payload/launcher.js +2 -0
  50. package/dist/deploy/payload/launcher.js.map +1 -1
  51. package/dist/deploy/payload/stage.d.ts +1 -1
  52. package/dist/deploy/payload/stage.d.ts.map +1 -1
  53. package/dist/deploy/payload/stage.js +23 -13
  54. package/dist/deploy/payload/stage.js.map +1 -1
  55. package/dist/deploy/run-deploy.d.ts.map +1 -1
  56. package/dist/deploy/run-deploy.js +149 -25
  57. package/dist/deploy/run-deploy.js.map +1 -1
  58. package/dist/deploy/settings/build-manifest.d.ts +9 -0
  59. package/dist/deploy/settings/build-manifest.d.ts.map +1 -0
  60. package/dist/deploy/settings/build-manifest.js +80 -0
  61. package/dist/deploy/settings/build-manifest.js.map +1 -0
  62. package/dist/deploy/settings/index.d.ts.map +1 -1
  63. package/dist/deploy/settings/index.js +9 -3
  64. package/dist/deploy/settings/index.js.map +1 -1
  65. package/dist/deploy/settings/libraries.d.ts +8 -0
  66. package/dist/deploy/settings/libraries.d.ts.map +1 -0
  67. package/dist/deploy/settings/libraries.js +39 -0
  68. package/dist/deploy/settings/libraries.js.map +1 -0
  69. package/dist/deploy/settings/paths.d.ts +3 -0
  70. package/dist/deploy/settings/paths.d.ts.map +1 -1
  71. package/dist/deploy/settings/paths.js +6 -32
  72. package/dist/deploy/settings/paths.js.map +1 -1
  73. package/dist/deploy/targets/appimage.d.ts.map +1 -1
  74. package/dist/deploy/targets/appimage.js +19 -13
  75. package/dist/deploy/targets/appimage.js.map +1 -1
  76. package/dist/deploy/targets/flatpak-manifest.d.ts +2 -1
  77. package/dist/deploy/targets/flatpak-manifest.d.ts.map +1 -1
  78. package/dist/deploy/targets/flatpak-manifest.js +5 -1
  79. package/dist/deploy/targets/flatpak-manifest.js.map +1 -1
  80. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  81. package/dist/deploy/targets/flatpak-source.js +30 -16
  82. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  83. package/dist/deploy/tools.d.ts +6 -1
  84. package/dist/deploy/tools.d.ts.map +1 -1
  85. package/dist/deploy/tools.js +26 -1
  86. package/dist/deploy/tools.js.map +1 -1
  87. package/dist/deploy/types.d.ts +4 -4
  88. package/dist/deploy/types.d.ts.map +1 -1
  89. package/dist/deploy/types.js.map +1 -1
  90. package/dist/dev/icon-dir.d.ts +1 -1
  91. package/dist/dev/icon-dir.d.ts.map +1 -1
  92. package/dist/dev/icon-dir.js +20 -7
  93. package/dist/dev/icon-dir.js.map +1 -1
  94. package/dist/dev/locale-dir.d.ts +3 -0
  95. package/dist/dev/locale-dir.d.ts.map +1 -0
  96. package/dist/dev/locale-dir.js +17 -0
  97. package/dist/dev/locale-dir.js.map +1 -0
  98. package/dist/dev/runner-deps.d.ts.map +1 -1
  99. package/dist/dev/runner-deps.js +5 -1
  100. package/dist/dev/runner-deps.js.map +1 -1
  101. package/dist/dev/runner-main.d.ts.map +1 -1
  102. package/dist/dev/runner-main.js +6 -4
  103. package/dist/dev/runner-main.js.map +1 -1
  104. package/dist/dev/runner.d.ts +1 -1
  105. package/dist/dev/runner.d.ts.map +1 -1
  106. package/dist/dev/runner.js +7 -1
  107. package/dist/dev/runner.js.map +1 -1
  108. package/dist/dev/schema-dir.d.ts +1 -1
  109. package/dist/dev/schema-dir.d.ts.map +1 -1
  110. package/dist/dev/schema-dir.js +3 -1
  111. package/dist/dev/schema-dir.js.map +1 -1
  112. package/dist/dev/vite-dev-server.d.ts +1 -0
  113. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  114. package/dist/dev/vite-dev-server.js.map +1 -1
  115. package/dist/i18n/binding-extractor.d.ts +30 -0
  116. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  117. package/dist/i18n/binding-extractor.js +1324 -0
  118. package/dist/i18n/binding-extractor.js.map +1 -0
  119. package/dist/i18n/catalog-template.d.ts +3 -0
  120. package/dist/i18n/catalog-template.d.ts.map +1 -0
  121. package/dist/i18n/catalog-template.js +18 -0
  122. package/dist/i18n/catalog-template.js.map +1 -0
  123. package/dist/i18n/catalogs.d.ts +21 -0
  124. package/dist/i18n/catalogs.d.ts.map +1 -0
  125. package/dist/i18n/catalogs.js +139 -0
  126. package/dist/i18n/catalogs.js.map +1 -0
  127. package/dist/i18n/extractor-plugin.d.ts +8 -0
  128. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  129. package/dist/i18n/extractor-plugin.js +247 -0
  130. package/dist/i18n/extractor-plugin.js.map +1 -0
  131. package/dist/i18n/metadata-templates.d.ts +11 -0
  132. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  133. package/dist/i18n/metadata-templates.js +11 -0
  134. package/dist/i18n/metadata-templates.js.map +1 -0
  135. package/dist/i18n/source-messages.d.ts +23 -0
  136. package/dist/i18n/source-messages.d.ts.map +1 -0
  137. package/dist/i18n/source-messages.js +403 -0
  138. package/dist/i18n/source-messages.js.map +1 -0
  139. package/dist/i18n/types.d.ts +10 -0
  140. package/dist/i18n/types.d.ts.map +1 -0
  141. package/dist/i18n/types.js +108 -0
  142. package/dist/i18n/types.js.map +1 -0
  143. package/dist/internal/build-manifest.d.ts +20 -0
  144. package/dist/internal/build-manifest.d.ts.map +1 -0
  145. package/dist/internal/build-manifest.js +6 -0
  146. package/dist/internal/build-manifest.js.map +1 -0
  147. package/dist/internal/icon-path.d.ts +13 -0
  148. package/dist/internal/icon-path.d.ts.map +1 -0
  149. package/dist/internal/icon-path.js +86 -0
  150. package/dist/internal/icon-path.js.map +1 -0
  151. package/dist/internal/source-imports.d.ts +12 -0
  152. package/dist/internal/source-imports.d.ts.map +1 -0
  153. package/dist/internal/source-imports.js +123 -0
  154. package/dist/internal/source-imports.js.map +1 -0
  155. package/dist/mcp/client.d.ts +0 -1
  156. package/dist/mcp/client.d.ts.map +1 -1
  157. package/dist/mcp/client.js +11 -39
  158. package/dist/mcp/client.js.map +1 -1
  159. package/dist/mcp/handlers.d.ts +2 -1
  160. package/dist/mcp/handlers.d.ts.map +1 -1
  161. package/dist/mcp/handlers.js.map +1 -1
  162. package/dist/mcp/widget-properties.d.ts.map +1 -1
  163. package/dist/mcp/widget-properties.js +3 -12
  164. package/dist/mcp/widget-properties.js.map +1 -1
  165. package/dist/settings/render.d.ts +10 -1
  166. package/dist/settings/render.d.ts.map +1 -1
  167. package/dist/settings/render.js +78 -20
  168. package/dist/settings/render.js.map +1 -1
  169. package/dist/settings/schema.d.ts +4 -3
  170. package/dist/settings/schema.d.ts.map +1 -1
  171. package/dist/settings/schema.js +124 -15
  172. package/dist/settings/schema.js.map +1 -1
  173. package/dist/vite-plugins/asset-emitter.d.ts +1 -1
  174. package/dist/vite-plugins/asset-emitter.d.ts.map +1 -1
  175. package/dist/vite-plugins/asset-emitter.js.map +1 -1
  176. package/dist/vite-plugins/asset-extensions.d.ts.map +1 -1
  177. package/dist/vite-plugins/asset-extensions.js +1 -0
  178. package/dist/vite-plugins/asset-extensions.js.map +1 -1
  179. package/dist/vite-plugins/asset-imports.d.ts +2 -1
  180. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  181. package/dist/vite-plugins/asset-imports.js +39 -28
  182. package/dist/vite-plugins/asset-imports.js.map +1 -1
  183. package/dist/vite-plugins/asset-specifier.d.ts +13 -1
  184. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  185. package/dist/vite-plugins/asset-specifier.js +39 -1
  186. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  187. package/dist/vite-plugins/build-manifest.d.ts +5 -0
  188. package/dist/vite-plugins/build-manifest.d.ts.map +1 -0
  189. package/dist/vite-plugins/{bundled-packages.js → build-manifest.js} +20 -14
  190. package/dist/vite-plugins/build-manifest.js.map +1 -0
  191. package/dist/vite-plugins/built-url.d.ts +1 -1
  192. package/dist/vite-plugins/built-url.d.ts.map +1 -1
  193. package/dist/vite-plugins/built-url.js +37 -24
  194. package/dist/vite-plugins/built-url.js.map +1 -1
  195. package/dist/vite-plugins/i18n.d.ts +5 -0
  196. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  197. package/dist/vite-plugins/i18n.js +88 -0
  198. package/dist/vite-plugins/i18n.js.map +1 -0
  199. package/dist/vite-plugins/icons.d.ts +2 -1
  200. package/dist/vite-plugins/icons.d.ts.map +1 -1
  201. package/dist/vite-plugins/icons.js +25 -12
  202. package/dist/vite-plugins/icons.js.map +1 -1
  203. package/dist/vite-plugins/index.d.ts +2 -1
  204. package/dist/vite-plugins/index.d.ts.map +1 -1
  205. package/dist/vite-plugins/index.js +9 -5
  206. package/dist/vite-plugins/index.js.map +1 -1
  207. package/dist/vite-plugins/native.d.ts.map +1 -1
  208. package/dist/vite-plugins/native.js +23 -4
  209. package/dist/vite-plugins/native.js.map +1 -1
  210. package/dist/vite-plugins/resource-init-module.d.ts +7 -3
  211. package/dist/vite-plugins/resource-init-module.d.ts.map +1 -1
  212. package/dist/vite-plugins/resource-init-module.js +39 -14
  213. package/dist/vite-plugins/resource-init-module.js.map +1 -1
  214. package/dist/vite-plugins/resource-shared.d.ts +3 -1
  215. package/dist/vite-plugins/resource-shared.d.ts.map +1 -1
  216. package/dist/vite-plugins/resource-shared.js +3 -1
  217. package/dist/vite-plugins/resource-shared.js.map +1 -1
  218. package/dist/vite-plugins/resources.d.ts +1 -1
  219. package/dist/vite-plugins/resources.d.ts.map +1 -1
  220. package/dist/vite-plugins/resources.js +877 -86
  221. package/dist/vite-plugins/resources.js.map +1 -1
  222. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  223. package/dist/vite-plugins/self-contained.js +128 -135
  224. package/dist/vite-plugins/self-contained.js.map +1 -1
  225. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  226. package/dist/vite-plugins/settings-worker-env.js +6 -3
  227. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  228. package/dist/vite-plugins/settings.d.ts +3 -1
  229. package/dist/vite-plugins/settings.d.ts.map +1 -1
  230. package/dist/vite-plugins/settings.js +63 -23
  231. package/dist/vite-plugins/settings.js.map +1 -1
  232. package/env.d.ts +11 -192
  233. package/package.json +11 -10
  234. package/src/builder.ts +7 -7
  235. package/src/codegen/run-codegen.ts +64 -15
  236. package/src/commands/build.ts +0 -5
  237. package/src/commands/codegen.ts +2 -4
  238. package/src/deploy/depends.ts +47 -18
  239. package/src/deploy/freedesktop/localize.ts +144 -0
  240. package/src/deploy/install-hints.ts +5 -0
  241. package/src/deploy/nfpm/config.ts +1 -1
  242. package/src/deploy/node-runtime/elf.ts +3 -3
  243. package/src/deploy/node-runtime/index.ts +2 -2
  244. package/src/deploy/notices/collect.ts +4 -2
  245. package/src/deploy/notices/packages.ts +5 -43
  246. package/src/deploy/payload/icons.ts +64 -34
  247. package/src/deploy/payload/launcher.ts +2 -0
  248. package/src/deploy/payload/stage.ts +40 -12
  249. package/src/deploy/run-deploy.ts +214 -24
  250. package/src/deploy/settings/build-manifest.ts +122 -0
  251. package/src/deploy/settings/index.ts +9 -4
  252. package/src/deploy/settings/libraries.ts +57 -0
  253. package/src/deploy/settings/paths.ts +10 -47
  254. package/src/deploy/targets/appimage.ts +32 -15
  255. package/src/deploy/targets/flatpak-manifest.ts +7 -1
  256. package/src/deploy/targets/flatpak-source.ts +42 -20
  257. package/src/deploy/tools.ts +35 -0
  258. package/src/deploy/types.ts +4 -4
  259. package/src/dev/icon-dir.ts +31 -7
  260. package/src/dev/locale-dir.ts +22 -0
  261. package/src/dev/runner-deps.ts +5 -1
  262. package/src/dev/runner-main.ts +6 -4
  263. package/src/dev/runner.ts +10 -2
  264. package/src/dev/schema-dir.ts +3 -1
  265. package/src/dev/vite-dev-server.ts +1 -0
  266. package/src/i18n/binding-extractor.ts +1969 -0
  267. package/src/i18n/catalog-template.ts +24 -0
  268. package/src/i18n/catalogs.ts +216 -0
  269. package/src/i18n/extractor-plugin.ts +413 -0
  270. package/src/i18n/metadata-templates.ts +29 -0
  271. package/src/i18n/source-messages.ts +608 -0
  272. package/src/i18n/types.ts +154 -0
  273. package/src/internal/build-manifest.ts +32 -0
  274. package/src/internal/icon-path.ts +127 -0
  275. package/src/internal/source-imports.ts +171 -0
  276. package/src/mcp/client.ts +17 -44
  277. package/src/mcp/handlers.ts +7 -6
  278. package/src/mcp/widget-properties.ts +3 -14
  279. package/src/settings/render.ts +113 -21
  280. package/src/settings/schema.ts +192 -17
  281. package/src/vite-plugins/asset-emitter.ts +1 -1
  282. package/src/vite-plugins/asset-extensions.ts +1 -0
  283. package/src/vite-plugins/asset-imports.ts +62 -33
  284. package/src/vite-plugins/asset-specifier.ts +73 -1
  285. package/src/vite-plugins/{bundled-packages.ts → build-manifest.ts} +30 -19
  286. package/src/vite-plugins/built-url.ts +49 -28
  287. package/src/vite-plugins/i18n.ts +146 -0
  288. package/src/vite-plugins/icons.ts +33 -14
  289. package/src/vite-plugins/index.ts +15 -5
  290. package/src/vite-plugins/native.ts +31 -4
  291. package/src/vite-plugins/resource-init-module.ts +44 -15
  292. package/src/vite-plugins/resource-shared.ts +4 -0
  293. package/src/vite-plugins/resources.ts +1397 -100
  294. package/src/vite-plugins/self-contained.ts +150 -169
  295. package/src/vite-plugins/settings-worker-env.ts +7 -3
  296. package/src/vite-plugins/settings.ts +89 -21
  297. package/dist/vite-plugins/bundled-packages.d.ts +0 -5
  298. package/dist/vite-plugins/bundled-packages.d.ts.map +0 -1
  299. package/dist/vite-plugins/bundled-packages.js.map +0 -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
+ };