@gtkx/cli 1.4.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 (143) 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/codegen/run-codegen.d.ts +2 -0
  7. package/dist/codegen/run-codegen.d.ts.map +1 -1
  8. package/dist/codegen/run-codegen.js +30 -6
  9. package/dist/codegen/run-codegen.js.map +1 -1
  10. package/dist/commands/codegen.js +1 -1
  11. package/dist/commands/codegen.js.map +1 -1
  12. package/dist/deploy/freedesktop/localize.d.ts +6 -0
  13. package/dist/deploy/freedesktop/localize.d.ts.map +1 -0
  14. package/dist/deploy/freedesktop/localize.js +107 -0
  15. package/dist/deploy/freedesktop/localize.js.map +1 -0
  16. package/dist/deploy/install-hints.d.ts.map +1 -1
  17. package/dist/deploy/install-hints.js +5 -0
  18. package/dist/deploy/install-hints.js.map +1 -1
  19. package/dist/deploy/payload/launcher.d.ts.map +1 -1
  20. package/dist/deploy/payload/launcher.js +2 -0
  21. package/dist/deploy/payload/launcher.js.map +1 -1
  22. package/dist/deploy/payload/stage.d.ts +1 -1
  23. package/dist/deploy/payload/stage.d.ts.map +1 -1
  24. package/dist/deploy/payload/stage.js +20 -11
  25. package/dist/deploy/payload/stage.js.map +1 -1
  26. package/dist/deploy/run-deploy.d.ts.map +1 -1
  27. package/dist/deploy/run-deploy.js +87 -15
  28. package/dist/deploy/run-deploy.js.map +1 -1
  29. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  30. package/dist/deploy/targets/flatpak-source.js +21 -10
  31. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  32. package/dist/deploy/tools.d.ts +6 -1
  33. package/dist/deploy/tools.d.ts.map +1 -1
  34. package/dist/deploy/tools.js +26 -1
  35. package/dist/deploy/tools.js.map +1 -1
  36. package/dist/dev/locale-dir.d.ts +3 -0
  37. package/dist/dev/locale-dir.d.ts.map +1 -0
  38. package/dist/dev/locale-dir.js +17 -0
  39. package/dist/dev/locale-dir.js.map +1 -0
  40. package/dist/dev/runner-main.d.ts.map +1 -1
  41. package/dist/dev/runner-main.js +2 -0
  42. package/dist/dev/runner-main.js.map +1 -1
  43. package/dist/dev/runner.d.ts.map +1 -1
  44. package/dist/dev/runner.js +6 -0
  45. package/dist/dev/runner.js.map +1 -1
  46. package/dist/dev/vite-dev-server.d.ts +1 -0
  47. package/dist/dev/vite-dev-server.d.ts.map +1 -1
  48. package/dist/dev/vite-dev-server.js.map +1 -1
  49. package/dist/i18n/binding-extractor.d.ts +30 -0
  50. package/dist/i18n/binding-extractor.d.ts.map +1 -0
  51. package/dist/i18n/binding-extractor.js +1324 -0
  52. package/dist/i18n/binding-extractor.js.map +1 -0
  53. package/dist/i18n/catalog-template.d.ts +3 -0
  54. package/dist/i18n/catalog-template.d.ts.map +1 -0
  55. package/dist/i18n/catalog-template.js +18 -0
  56. package/dist/i18n/catalog-template.js.map +1 -0
  57. package/dist/i18n/catalogs.d.ts +21 -0
  58. package/dist/i18n/catalogs.d.ts.map +1 -0
  59. package/dist/i18n/catalogs.js +139 -0
  60. package/dist/i18n/catalogs.js.map +1 -0
  61. package/dist/i18n/extractor-plugin.d.ts +8 -0
  62. package/dist/i18n/extractor-plugin.d.ts.map +1 -0
  63. package/dist/i18n/extractor-plugin.js +247 -0
  64. package/dist/i18n/extractor-plugin.js.map +1 -0
  65. package/dist/i18n/metadata-templates.d.ts +11 -0
  66. package/dist/i18n/metadata-templates.d.ts.map +1 -0
  67. package/dist/i18n/metadata-templates.js +11 -0
  68. package/dist/i18n/metadata-templates.js.map +1 -0
  69. package/dist/i18n/source-messages.d.ts +23 -0
  70. package/dist/i18n/source-messages.d.ts.map +1 -0
  71. package/dist/i18n/source-messages.js +403 -0
  72. package/dist/i18n/source-messages.js.map +1 -0
  73. package/dist/i18n/types.d.ts +10 -0
  74. package/dist/i18n/types.d.ts.map +1 -0
  75. package/dist/i18n/types.js +108 -0
  76. package/dist/i18n/types.js.map +1 -0
  77. package/dist/internal/source-imports.d.ts +5 -1
  78. package/dist/internal/source-imports.d.ts.map +1 -1
  79. package/dist/internal/source-imports.js +23 -44
  80. package/dist/internal/source-imports.js.map +1 -1
  81. package/dist/mcp/client.d.ts +0 -1
  82. package/dist/mcp/client.d.ts.map +1 -1
  83. package/dist/mcp/client.js +11 -39
  84. package/dist/mcp/client.js.map +1 -1
  85. package/dist/mcp/handlers.d.ts +2 -1
  86. package/dist/mcp/handlers.d.ts.map +1 -1
  87. package/dist/mcp/handlers.js.map +1 -1
  88. package/dist/settings/render.d.ts +4 -1
  89. package/dist/settings/render.d.ts.map +1 -1
  90. package/dist/settings/render.js +11 -2
  91. package/dist/settings/render.js.map +1 -1
  92. package/dist/settings/schema.d.ts.map +1 -1
  93. package/dist/settings/schema.js +3 -1
  94. package/dist/settings/schema.js.map +1 -1
  95. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  96. package/dist/vite-plugins/asset-imports.js +2 -12
  97. package/dist/vite-plugins/asset-imports.js.map +1 -1
  98. package/dist/vite-plugins/i18n.d.ts +5 -0
  99. package/dist/vite-plugins/i18n.d.ts.map +1 -0
  100. package/dist/vite-plugins/i18n.js +88 -0
  101. package/dist/vite-plugins/i18n.js.map +1 -0
  102. package/dist/vite-plugins/index.d.ts +1 -1
  103. package/dist/vite-plugins/index.d.ts.map +1 -1
  104. package/dist/vite-plugins/index.js +3 -1
  105. package/dist/vite-plugins/index.js.map +1 -1
  106. package/dist/vite-plugins/resources.d.ts.map +1 -1
  107. package/dist/vite-plugins/resources.js +10 -55
  108. package/dist/vite-plugins/resources.js.map +1 -1
  109. package/dist/vite-plugins/self-contained.d.ts.map +1 -1
  110. package/dist/vite-plugins/self-contained.js +128 -135
  111. package/dist/vite-plugins/self-contained.js.map +1 -1
  112. package/package.json +10 -9
  113. package/src/builder.ts +3 -2
  114. package/src/codegen/run-codegen.ts +53 -7
  115. package/src/commands/codegen.ts +1 -1
  116. package/src/deploy/freedesktop/localize.ts +144 -0
  117. package/src/deploy/install-hints.ts +5 -0
  118. package/src/deploy/payload/launcher.ts +2 -0
  119. package/src/deploy/payload/stage.ts +35 -10
  120. package/src/deploy/run-deploy.ts +128 -13
  121. package/src/deploy/targets/flatpak-source.ts +33 -10
  122. package/src/deploy/tools.ts +35 -0
  123. package/src/dev/locale-dir.ts +22 -0
  124. package/src/dev/runner-main.ts +2 -0
  125. package/src/dev/runner.ts +8 -0
  126. package/src/dev/vite-dev-server.ts +1 -0
  127. package/src/i18n/binding-extractor.ts +1969 -0
  128. package/src/i18n/catalog-template.ts +24 -0
  129. package/src/i18n/catalogs.ts +216 -0
  130. package/src/i18n/extractor-plugin.ts +413 -0
  131. package/src/i18n/metadata-templates.ts +29 -0
  132. package/src/i18n/source-messages.ts +608 -0
  133. package/src/i18n/types.ts +154 -0
  134. package/src/internal/source-imports.ts +28 -57
  135. package/src/mcp/client.ts +17 -44
  136. package/src/mcp/handlers.ts +7 -6
  137. package/src/settings/render.ts +16 -2
  138. package/src/settings/schema.ts +3 -1
  139. package/src/vite-plugins/asset-imports.ts +3 -15
  140. package/src/vite-plugins/i18n.ts +146 -0
  141. package/src/vite-plugins/index.ts +3 -0
  142. package/src/vite-plugins/resources.ts +10 -72
  143. package/src/vite-plugins/self-contained.ts +150 -169
@@ -0,0 +1,144 @@
1
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import type { CatalogProject } from "../../i18n/catalogs.js";
5
+ import type { StagedMetadata } from "../payload/stage.js";
6
+ import { metadataTemplateFile, type MetadataTemplateKind } from "../../i18n/metadata-templates.js";
7
+ import { runCliTool } from "../../internal/run-cli-tool.js";
8
+
9
+ type MergeMode = "--desktop" | "--xml";
10
+
11
+ type MergeRequest = {
12
+ mode: MergeMode;
13
+ stem: string;
14
+ extension: string;
15
+ template: string;
16
+ };
17
+
18
+ type MetadataTemplate = {
19
+ contents: string;
20
+ path: string;
21
+ relativePath: string;
22
+ };
23
+
24
+ const metadataTemplates = (project: CatalogProject, metadata: StagedMetadata): MetadataTemplate[] => {
25
+ const template = (kind: MetadataTemplateKind, contents: string): MetadataTemplate => {
26
+ const { path, relativePath } = metadataTemplateFile(project, kind);
27
+
28
+ return { contents, path, relativePath };
29
+ };
30
+
31
+ return [
32
+ template("desktop", metadata.desktopEntry),
33
+ template("metainfo.xml", metadata.metainfo),
34
+ ...(metadata.mimePackage === null ? [] : [template("mime.xml", metadata.mimePackage)]),
35
+ ];
36
+ };
37
+
38
+ const mergeCatalogs = (project: CatalogProject, workDir: string, request: MergeRequest): string => {
39
+ const { extension, mode, stem, template } = request;
40
+ let current = join(workDir, `${stem}-source${extension}`);
41
+ writeFileSync(current, template);
42
+
43
+ for (const [index, catalog] of project.catalogs.entries()) {
44
+ const output = join(workDir, `${stem}-${String(index)}${extension}`);
45
+
46
+ runCliTool({
47
+ tool: "msgfmt",
48
+ args: [
49
+ mode,
50
+ `--template=${current}`,
51
+ `--locale=${catalog.locale}`,
52
+ `--output-file=${output}`,
53
+ catalog.path,
54
+ ],
55
+ target: catalog.path,
56
+ });
57
+
58
+ current = output;
59
+ }
60
+
61
+ return readFileSync(current, "utf8");
62
+ };
63
+
64
+ const localizeMetadata = (metadata: StagedMetadata, project: CatalogProject | null): StagedMetadata => {
65
+ if (project === null || project.catalogs.length === 0) {
66
+ return metadata;
67
+ }
68
+
69
+ const workDir = mkdtempSync(join(tmpdir(), "gtkx-deploy-metadata-"));
70
+
71
+ try {
72
+ return {
73
+ desktopEntry: mergeCatalogs(project, workDir, {
74
+ mode: "--desktop",
75
+ stem: "application",
76
+ extension: ".desktop",
77
+ template: metadata.desktopEntry,
78
+ }),
79
+ metainfo: mergeCatalogs(project, workDir, {
80
+ mode: "--xml",
81
+ stem: "metainfo",
82
+ extension: ".metainfo.xml",
83
+ template: metadata.metainfo,
84
+ }),
85
+ mimePackage: metadata.mimePackage === null
86
+ ? null
87
+ : mergeCatalogs(project, workDir, {
88
+ mode: "--xml",
89
+ stem: "mime",
90
+ extension: ".xml",
91
+ template: metadata.mimePackage,
92
+ }),
93
+ };
94
+ } finally {
95
+ rmSync(workDir, { recursive: true, force: true });
96
+ }
97
+ };
98
+
99
+ const extractMetadataMessages = (
100
+ metadata: StagedMetadata,
101
+ project: CatalogProject,
102
+ catalogTemplate?: string,
103
+ ): void => {
104
+ const potPath = catalogTemplate ?? join(project.poDir, `${project.domain}.pot`);
105
+
106
+ if (!existsSync(potPath)) {
107
+ throw new Error(`Cannot add the generated metadata to the missing catalog template: ${potPath}`);
108
+ }
109
+
110
+ const templates = metadataTemplates(project, metadata);
111
+ const firstTemplate = templates[0];
112
+
113
+ if (firstTemplate === undefined) {
114
+ throw new Error("Cannot extract metadata messages without a generated template");
115
+ }
116
+
117
+ const metadataDir = dirname(firstTemplate.path);
118
+ mkdirSync(metadataDir, { recursive: true });
119
+
120
+ try {
121
+ for (const template of templates) {
122
+ writeFileSync(template.path, template.contents);
123
+ }
124
+
125
+ runCliTool({
126
+ tool: "xgettext",
127
+ args: [
128
+ "--join-existing",
129
+ "--force-po",
130
+ "--from-code=UTF-8",
131
+ `--output=${potPath}`,
132
+ ...templates.map((template) => template.relativePath),
133
+ ],
134
+ target: potPath,
135
+ options: { cwd: project.root },
136
+ });
137
+ } finally {
138
+ for (const template of templates) {
139
+ rmSync(template.path, { force: true });
140
+ }
141
+ }
142
+ };
143
+
144
+ export { extractMetadataMessages, localizeMetadata };
@@ -43,8 +43,13 @@ const PACKAGE_FOR_TOOL: Record<string, FamilyPackages> = {
43
43
  fedora: "flatpak-builder",
44
44
  suse: "flatpak-builder",
45
45
  },
46
+ msgfmt: { arch: "gettext", debian: "gettext", fedora: "gettext", suse: "gettext-tools" },
47
+ msginit: { arch: "gettext", debian: "gettext", fedora: "gettext", suse: "gettext-tools" },
48
+ msggrep: { arch: "gettext", debian: "gettext", fedora: "gettext", suse: "gettext-tools" },
49
+ msgmerge: { arch: "gettext", debian: "gettext", fedora: "gettext", suse: "gettext-tools" },
46
50
  strip: { arch: "binutils", debian: "binutils", fedora: "binutils", suse: "binutils" },
47
51
  tar: { arch: "tar", debian: "tar", fedora: "tar", suse: "tar" },
52
+ xgettext: { arch: "gettext", debian: "gettext", fedora: "gettext", suse: "gettext-tools" },
48
53
  };
49
54
 
50
55
  const GENERATOR_SOURCE = "git+https://github.com/flatpak/flatpak-builder-tools.git#subdirectory=node";
@@ -18,6 +18,8 @@ const renderLauncher = (settings: DeploySettings): string => {
18
18
  "set -e",
19
19
  'self=$(readlink -f "$0")',
20
20
  'prefix=$(dirname "$(dirname "$self")")',
21
+ 'GTKX_LOCALE_DIR="$prefix/share/locale"',
22
+ "export GTKX_LOCALE_DIR",
21
23
  ...(shouldUseCompileCache ? compileCacheLines(settings.binaryName) : []),
22
24
  `exec "${libDir}/${NODE_FILENAME}" "${libDir}/${BUNDLE_FILENAME}" "$@"`,
23
25
  "",
@@ -2,12 +2,12 @@ import { sortStringsBy } from "@gtkx/utils";
2
2
  import { existsSync, rmSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import type { DeploySettings, DeployTargetName, NodeRuntime, NoticeSection, StagedFile } from "../types.js";
5
+ import { LOCALE_DIRNAME } from "../../i18n/catalogs.js";
5
6
  import { BUILD_MANIFEST_FILENAME } from "../../internal/build-manifest.js";
6
7
  import { listFilesRecursive } from "../../internal/list-files.js";
7
8
  import { BUNDLE_FILENAME } from "../../vite-plugins/esm-extension.js";
8
9
  import { renderCopyright } from "../freedesktop/copyright.js";
9
10
  import { renderDbusService } from "../freedesktop/dbus-service.js";
10
- import { renderDesktopEntry } from "../freedesktop/desktop-entry.js";
11
11
  import { renderNotices } from "../notices/render.js";
12
12
  import { copyInto, EXECUTABLE_MODE, executableModeFor, writeInto } from "./copy-tree.js";
13
13
  import { stageIcons } from "./icons.js";
@@ -36,6 +36,7 @@ const SHARE_MIME_PACKAGES = "share/mime/packages";
36
36
  const SHARE_DOC = "share/doc";
37
37
  const SHARE_LICENSES = "share/licenses";
38
38
  const SHARE_DBUS_SERVICES = "share/dbus-1/services";
39
+ const SHARE_LOCALE = `share/${LOCALE_DIRNAME}`;
39
40
 
40
41
  const PREFIX_FOR: Record<DeployTargetName, string> = {
41
42
  appimage: "/usr",
@@ -53,6 +54,8 @@ const isIconAsset = (rel: string): boolean => rel === DIST_ICONS_DIR || rel.star
53
54
  const isBuildMetadata = (rel: string): boolean =>
54
55
  rel === BUILD_MANIFEST_FILENAME || rel === LEGACY_PACKAGES_MANIFEST_FILENAME;
55
56
 
57
+ const isLocaleAsset = (rel: string): boolean => rel === LOCALE_DIRNAME || rel.startsWith(`${LOCALE_DIRNAME}/`);
58
+
56
59
  const nodeLicenseDestination = (settings: DeploySettings): string =>
57
60
  `${licenseDirFor(settings)}/${NODE_FILENAME}/${NODE_LICENSE_FILENAME}`;
58
61
 
@@ -64,10 +67,14 @@ const stageRuntimeFiles = (settings: DeploySettings, root: string): StagedFile[]
64
67
  }
65
68
 
66
69
  return listFilesRecursive(dist)
67
- .filter((file) => !isIconAsset(file.rel) && !isBuildMetadata(file.rel))
70
+ .filter((file) => !isIconAsset(file.rel) && !isBuildMetadata(file.rel) && !isLocaleAsset(file.rel))
68
71
  .map((file) => copyInto(root, join(libDirFor(settings), file.rel), file.absPath));
69
72
  };
70
73
 
74
+ const stageCatalogs = (settings: DeploySettings, root: string): StagedFile[] =>
75
+ listFilesRecursive(join(settings.paths.dist, LOCALE_DIRNAME))
76
+ .map((file) => copyInto(root, join(SHARE_LOCALE, file.rel), file.absPath));
77
+
71
78
  const stageNodeBinary = (settings: DeploySettings, root: string, node: NodeRuntime | null): StagedFile[] =>
72
79
  node === null ? [] : [copyInto(root, join(libDirFor(settings), NODE_FILENAME), node.path, EXECUTABLE_MODE)];
73
80
 
@@ -104,6 +111,7 @@ const stagePayload = ({ settings, node, metadata }: StageRequest): StagedFile[]
104
111
  writeInto(root, join("bin", settings.binaryName), renderLauncher(settings), EXECUTABLE_MODE),
105
112
  ...stageNodeBinary(settings, root, node),
106
113
  ...stageRuntimeFiles(settings, root),
114
+ ...stageCatalogs(settings, root),
107
115
  ...stageMetadata(settings, root, metadata),
108
116
  ...stageIcons(settings, root),
109
117
  ...stageSchemas(settings, root),
@@ -111,7 +119,18 @@ const stagePayload = ({ settings, node, metadata }: StageRequest): StagedFile[]
111
119
  ]);
112
120
  };
113
121
 
114
- const stageActivation = (settings: DeploySettings, root: string, target: DeployTargetName): StagedFile[] => {
122
+ const withoutDbusActivation = (desktopEntry: string): string =>
123
+ desktopEntry
124
+ .split("\n")
125
+ .filter((line) => line !== "DBusActivatable=true")
126
+ .join("\n");
127
+
128
+ const stageActivation = (
129
+ settings: DeploySettings,
130
+ root: string,
131
+ target: DeployTargetName,
132
+ metadata: StagedMetadata,
133
+ ): StagedFile[] => {
115
134
  if (!settings.isDbusActivatable) {
116
135
  return [];
117
136
  }
@@ -120,7 +139,7 @@ const stageActivation = (settings: DeploySettings, root: string, target: DeployT
120
139
  return [writeInto(
121
140
  root,
122
141
  join(SHARE_APPLICATIONS, `${settings.applicationId}.desktop`),
123
- renderDesktopEntry({ ...settings, isDbusActivatable: false }),
142
+ withoutDbusActivation(metadata.desktopEntry),
124
143
  )];
125
144
  }
126
145
 
@@ -131,11 +150,16 @@ const stageActivation = (settings: DeploySettings, root: string, target: DeployT
131
150
  )];
132
151
  };
133
152
 
134
- const stageOverlay = (settings: DeploySettings, target: DeployTargetName, notices: NoticeSection[]): StagedFile[] => {
153
+ const stageOverlay = (
154
+ settings: DeploySettings,
155
+ target: DeployTargetName,
156
+ notices: NoticeSection[],
157
+ metadata: StagedMetadata,
158
+ ): StagedFile[] => {
135
159
  const root = join(settings.paths.overlay, target);
136
160
  rmSync(root, { recursive: true, force: true, maxRetries: 5 });
137
161
  const licenseFile = settings.paths.licenseFile;
138
- const activation = stageActivation(settings, root, target);
162
+ const activation = stageActivation(settings, root, target, metadata);
139
163
 
140
164
  if (target === "deb") {
141
165
  return [
@@ -154,11 +178,12 @@ const stageOverlay = (settings: DeploySettings, target: DeployTargetName, notice
154
178
  const stageOverlays = (
155
179
  settings: DeploySettings,
156
180
  notices: NoticeSection[],
181
+ metadata: StagedMetadata,
157
182
  ): Record<DeployTargetName, StagedFile[]> => ({
158
- appimage: stageOverlay(settings, "appimage", notices),
159
- deb: stageOverlay(settings, "deb", notices),
160
- flatpak: stageOverlay(settings, "flatpak", notices),
161
- rpm: stageOverlay(settings, "rpm", notices),
183
+ appimage: stageOverlay(settings, "appimage", notices, metadata),
184
+ deb: stageOverlay(settings, "deb", notices, metadata),
185
+ flatpak: stageOverlay(settings, "flatpak", notices, metadata),
186
+ rpm: stageOverlay(settings, "rpm", notices, metadata),
162
187
  });
163
188
 
164
189
  export {
@@ -1,6 +1,6 @@
1
1
  import { loadConfig } from "@gtkx/config";
2
2
  import { info, warn } from "@gtkx/utils";
3
- import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join, relative } from "node:path";
5
5
  import type {
6
6
  DeployArtifact,
@@ -12,7 +12,19 @@ import type {
12
12
  } from "./types.js";
13
13
  import { build as buildApp } from "../builder.js";
14
14
  import { ensureGenerated } from "../codegen/run-codegen.js";
15
+ import { replaceCatalogTemplate } from "../i18n/catalog-template.js";
16
+ import {
17
+ type CatalogProject,
18
+ compileCatalogs,
19
+ LOCALE_DIRNAME,
20
+ requiresCatalogInitialization,
21
+ resolveCatalogProject,
22
+ synchronizeCatalogs,
23
+ } from "../i18n/catalogs.js";
24
+ import { extractSourceCatalogTo } from "../i18n/source-messages.js";
25
+ import { discoverSourceFiles } from "../internal/source-imports.js";
15
26
  import { renderDesktopEntry } from "./freedesktop/desktop-entry.js";
27
+ import { extractMetadataMessages, localizeMetadata } from "./freedesktop/localize.js";
16
28
  import { renderMetainfo } from "./freedesktop/metainfo.js";
17
29
  import { renderMimePackage } from "./freedesktop/mime-package.js";
18
30
  import { validateDesktopEntry, validateMetainfo } from "./freedesktop/validate.js";
@@ -32,10 +44,15 @@ import {
32
44
  DESKTOP_FILE_VALIDATE,
33
45
  FLATPAK_NODE_GENERATOR,
34
46
  FLATPAK_NODE_GENERATOR_PNPM,
47
+ MSGFMT,
48
+ MSGGREP,
49
+ MSGINIT,
50
+ MSGMERGE,
35
51
  probeTools,
36
52
  STRIP,
37
53
  TAR,
38
54
  warnMissingOptional,
55
+ XGETTEXT,
39
56
  } from "./tools.js";
40
57
 
41
58
  type DeployOptions = {
@@ -47,6 +64,14 @@ type DeployOptions = {
47
64
  shouldSkipBuild: boolean;
48
65
  };
49
66
 
67
+ type PreflightRequest = {
68
+ targets: DeployTarget[];
69
+ settings: DeploySettings;
70
+ project: CatalogProject | null;
71
+ shouldPrintManifests: boolean;
72
+ shouldSkipBuild: boolean;
73
+ };
74
+
50
75
  const BUILD_MODE = "production";
51
76
  const BYTES_PER_MIB = 1024 * 1024;
52
77
  const WEBKIT_LIBRARY = "WebKit-6.0";
@@ -198,12 +223,50 @@ const runtimeToolsFor = (targets: DeployTarget[], settings: DeploySettings): Dep
198
223
  return node.shouldStrip === false ? archiveTools : [...archiveTools, STRIP];
199
224
  };
200
225
 
201
- const preflight = (targets: DeployTarget[], settings: DeploySettings, shouldPrintManifests: boolean): void => {
226
+ const mutableCatalogTools = (project: CatalogProject): DeployTool[] => {
227
+ if (project.catalogs.length === 0) {
228
+ return [MSGGREP, XGETTEXT];
229
+ }
230
+
231
+ return [
232
+ MSGFMT,
233
+ MSGGREP,
234
+ ...(requiresCatalogInitialization(project) ? [MSGINIT] : []),
235
+ MSGMERGE,
236
+ XGETTEXT,
237
+ ];
238
+ };
239
+
240
+ const catalogTools = (project: CatalogProject | null, shouldSkipBuild: boolean): DeployTool[] => {
241
+ if (project === null) {
242
+ return [];
243
+ }
244
+
245
+ if (!shouldSkipBuild) {
246
+ return mutableCatalogTools(project);
247
+ }
248
+
249
+ return project.catalogs.length === 0 ? [] : [MSGFMT];
250
+ };
251
+
252
+ const preflight = ({
253
+ targets,
254
+ settings,
255
+ project,
256
+ shouldPrintManifests,
257
+ shouldSkipBuild,
258
+ }: PreflightRequest): void => {
202
259
  const packagerTools = shouldPrintManifests
203
260
  ? []
204
261
  : [...runtimeToolsFor(targets, settings), ...targets.flatMap((target) => target.tools)];
205
262
 
206
- const required = [DESKTOP_FILE_VALIDATE, APPSTREAMCLI, ...sourceModeTools(targets, settings)];
263
+ const required = [
264
+ DESKTOP_FILE_VALIDATE,
265
+ APPSTREAMCLI,
266
+ ...catalogTools(project, shouldSkipBuild),
267
+ ...sourceModeTools(targets, settings),
268
+ ];
269
+
207
270
  const report = probeTools([...required, ...packagerTools]);
208
271
  assertTools(report);
209
272
  warnMissingOptional(report);
@@ -229,19 +292,55 @@ const loadSettings = async (options: DeployOptions): Promise<DeploySettings> =>
229
292
  return resolveDeploySettings({ root, config, outDirOverride: options.outDir });
230
293
  };
231
294
 
295
+ const synchronizeMetadataCatalogs = async (
296
+ settings: DeploySettings,
297
+ templates: StagedMetadata,
298
+ project: CatalogProject | null,
299
+ ): Promise<void> => {
300
+ if (project === null) {
301
+ return;
302
+ }
303
+
304
+ const srcDir = join(project.root, "src");
305
+ const sourceFiles = discoverSourceFiles(existsSync(srcDir) ? srcDir : project.root);
306
+ const stagingDir = mkdtempSync(join(project.poDir, ".gtkx-deploy-i18n-"));
307
+ const stagedTemplate = join(stagingDir, `${project.domain}.pot`);
308
+ const template = join(project.poDir, `${project.domain}.pot`);
309
+
310
+ try {
311
+ await extractSourceCatalogTo(project, sourceFiles, stagedTemplate);
312
+ extractMetadataMessages(templates, project, stagedTemplate);
313
+ replaceCatalogTemplate(stagedTemplate, template);
314
+ synchronizeCatalogs(project);
315
+ compileCatalogs(project, join(settings.paths.dist, LOCALE_DIRNAME));
316
+ } finally {
317
+ rmSync(stagingDir, { recursive: true, force: true });
318
+ }
319
+ };
320
+
232
321
  const buildPayload = async (
233
322
  options: DeployOptions,
234
323
  settings: DeploySettings,
235
324
  targets: DeployTarget[],
325
+ project: CatalogProject | null,
236
326
  ): Promise<DeployPayload> => {
237
- const metadata = renderMetadata(settings);
238
- validateMetadata(settings, metadata, isFlathubSubmission(settings, targets));
327
+ const templates = renderMetadata(settings);
239
328
 
240
329
  if (!options.shouldSkipBuild) {
241
330
  info(`Building ${options.entry}`);
242
- await buildApp({ entry: options.entry, vite: { root: options.cwd } });
331
+
332
+ await buildApp({
333
+ entry: options.entry,
334
+ vite: { root: options.cwd },
335
+ });
336
+
337
+ await synchronizeMetadataCatalogs(settings, templates, project);
338
+ } else if (project !== null) {
339
+ compileCatalogs(project, join(settings.paths.dist, LOCALE_DIRNAME));
243
340
  }
244
341
 
342
+ const metadata = localizeMetadata(templates, project);
343
+ validateMetadata(settings, metadata, isFlathubSubmission(settings, targets));
245
344
  const buildManifest = readBuildManifest(settings);
246
345
 
247
346
  const builtSettings: DeploySettings = {
@@ -262,7 +361,7 @@ const buildPayload = async (
262
361
  node,
263
362
  stage,
264
363
  notices,
265
- overlays: stageOverlays(builtSettings, notices),
364
+ overlays: stageOverlays(builtSettings, notices, metadata),
266
365
  };
267
366
  };
268
367
 
@@ -312,15 +411,31 @@ const announce = (settings: DeploySettings, targets: DeployTarget[]): void => {
312
411
  };
313
412
 
314
413
  const runDeploy = async (options: DeployOptions): Promise<void> => {
414
+ let settings = await loadSettings(options);
415
+ let project = resolveCatalogProject(settings.paths.root, settings.applicationId);
416
+ let targets = targetsFor(resolveTargetNames(options, settings));
417
+ announce(settings, targets);
418
+
419
+ preflight({
420
+ targets,
421
+ settings,
422
+ project,
423
+ shouldPrintManifests: options.shouldPrintManifests,
424
+ shouldSkipBuild: options.shouldSkipBuild,
425
+ });
426
+
315
427
  if (!options.shouldSkipBuild) {
316
- await ensureGenerated(options.cwd, { shouldAnnounce: true, mode: BUILD_MODE });
428
+ await ensureGenerated(options.cwd, {
429
+ shouldAnnounce: true,
430
+ mode: BUILD_MODE,
431
+ });
432
+
433
+ settings = await loadSettings(options);
434
+ project = resolveCatalogProject(settings.paths.root, settings.applicationId);
435
+ targets = targetsFor(resolveTargetNames(options, settings));
317
436
  }
318
437
 
319
- const settings = await loadSettings(options);
320
- const targets = targetsFor(resolveTargetNames(options, settings));
321
- announce(settings, targets);
322
- preflight(targets, settings, options.shouldPrintManifests);
323
- const payload = await buildPayload(options, settings, targets);
438
+ const payload = await buildPayload(options, settings, targets, project);
324
439
  const rendered = renderTargetManifests(targets, payload);
325
440
 
326
441
  if (options.shouldPrintManifests) {
@@ -1,12 +1,10 @@
1
- import { existsSync, realpathSync } from "node:fs";
1
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
2
2
  import { posix, relative, resolve } from "node:path";
3
3
  import type { DeployPayload, DeploySettings } from "../types.js";
4
+ import { LOCALE_DIRNAME } from "../../i18n/catalogs.js";
4
5
  import { listFilesRecursive } from "../../internal/list-files.js";
5
6
  import { BUNDLE_FILENAME } from "../../vite-plugins/esm-extension.js";
6
7
  import { renderDbusService } from "../freedesktop/dbus-service.js";
7
- import { renderDesktopEntry } from "../freedesktop/desktop-entry.js";
8
- import { renderMetainfo } from "../freedesktop/metainfo.js";
9
- import { renderMimePackage } from "../freedesktop/mime-package.js";
10
8
  import { optional } from "../nfpm/optional.js";
11
9
  import { renderNotices } from "../notices/render.js";
12
10
  import { executableModeFor } from "../payload/copy-tree.js";
@@ -60,6 +58,7 @@ const installCommand = (source: string, destination: string, mode: string): stri
60
58
 
61
59
  const runtimeInstallCommands = (settings: DeploySettings, nodeExtensionPath: string): string[] => {
62
60
  const lib = `${DESTINATION}/lib/${settings.binaryName}`;
61
+ const locale = `${DESTINATION}/share/${LOCALE_DIRNAME}`;
63
62
 
64
63
  return [
65
64
  installCommand(`${nodeExtensionPath}/bin/node`, `${lib}/node`, "m755"),
@@ -69,6 +68,7 @@ const runtimeInstallCommands = (settings: DeploySettings, nodeExtensionPath: str
69
68
  "test ! -f dist/gschemas.compiled || " +
70
69
  installCommand("dist/gschemas.compiled", `${lib}/gschemas.compiled`, "m644"),
71
70
  `test ! -d dist/assets || cp -a dist/assets ${lib}/assets`,
71
+ `test ! -d dist/${LOCALE_DIRNAME} || { mkdir -p ${locale} && cp -a dist/${LOCALE_DIRNAME}/. ${locale}/; }`,
72
72
  installCommand(LAUNCHER_FILENAME, `${DESTINATION}/bin/${settings.binaryName}`, "m755"),
73
73
  ];
74
74
  };
@@ -87,10 +87,35 @@ const activationInstallCommands = (settings: DeploySettings): string[] =>
87
87
  )]
88
88
  : [];
89
89
 
90
- const mimeSources = (settings: DeploySettings): FlatpakModule[] => {
91
- const contents = renderMimePackage(settings);
90
+ const stagedContents = (payload: DeployPayload, destination: string): string => {
91
+ const file = payload.stage.find((candidate) => candidate.rel === destination);
92
92
 
93
- return contents === null ? [] : [inlineSource(`${settings.applicationId}.xml`, contents)];
93
+ if (file === undefined) {
94
+ throw new Error(`Cannot render the source Flatpak: ${destination} was not staged`);
95
+ }
96
+
97
+ return readFileSync(file.abs, "utf8");
98
+ };
99
+
100
+ const stagedMetadataSources = (payload: DeployPayload): FlatpakModule[] => {
101
+ const settings = payload.settings;
102
+
103
+ return [
104
+ inlineSource(
105
+ `${settings.applicationId}.desktop`,
106
+ stagedContents(payload, posix.join("share", "applications", `${settings.applicationId}.desktop`)),
107
+ ),
108
+ inlineSource(
109
+ `${settings.applicationId}.metainfo.xml`,
110
+ stagedContents(payload, posix.join("share", "metainfo", `${settings.applicationId}.metainfo.xml`)),
111
+ ),
112
+ ...(settings.fileAssociations.length === 0
113
+ ? []
114
+ : [inlineSource(
115
+ `${settings.applicationId}.xml`,
116
+ stagedContents(payload, posix.join("share", "mime", "packages", `${settings.applicationId}.xml`)),
117
+ )]),
118
+ ];
94
119
  };
95
120
 
96
121
  const mimeInstallCommands = (settings: DeploySettings): string[] =>
@@ -239,9 +264,7 @@ const flatpakSourceModule = (payload: DeployPayload): FlatpakModule => {
239
264
  resolveGitSource(settings),
240
265
  ...(pin === null ? [] : pnpmSources(pin, moduleDirFor(settings))),
241
266
  GENERATED_SOURCES,
242
- inlineSource(`${settings.applicationId}.desktop`, renderDesktopEntry(settings)),
243
- inlineSource(`${settings.applicationId}.metainfo.xml`, renderMetainfo(settings)),
244
- ...mimeSources(settings),
267
+ ...stagedMetadataSources(payload),
245
268
  inlineSource(LAUNCHER_FILENAME, renderLauncher(settings)),
246
269
  inlineSource(NOTICES_FILENAME, renderNotices(settings, payload.notices)),
247
270
  ...activationSource(settings),
@@ -56,6 +56,36 @@ const FLATPAK_NODE_GENERATOR_PNPM: DeployTool = {
56
56
  isPresent: () => hasGeneratorPnpmSupport(),
57
57
  };
58
58
 
59
+ const MSGFMT: DeployTool = {
60
+ command: "msgfmt",
61
+ purpose: "compiles the gettext catalogs and localized metadata",
62
+ isOptional: false,
63
+ };
64
+
65
+ const MSGGREP: DeployTool = {
66
+ command: "msggrep",
67
+ purpose: "retains generated metadata while refreshing the catalog template",
68
+ isOptional: false,
69
+ };
70
+
71
+ const MSGINIT: DeployTool = {
72
+ command: "msginit",
73
+ purpose: "initializes newly listed gettext catalogs",
74
+ isOptional: false,
75
+ };
76
+
77
+ const MSGMERGE: DeployTool = {
78
+ command: "msgmerge",
79
+ purpose: "synchronizes translations with the catalog template",
80
+ isOptional: false,
81
+ };
82
+
83
+ const XGETTEXT: DeployTool = {
84
+ command: "xgettext",
85
+ purpose: "extracts the gettext catalog template",
86
+ isOptional: false,
87
+ };
88
+
59
89
  const STRIP: DeployTool = {
60
90
  command: "strip",
61
91
  purpose: "would shrink the bundled Node.js by removing its symbols",
@@ -149,8 +179,13 @@ export {
149
179
  FILE_TOOL,
150
180
  FLATPAK,
151
181
  FLATPAK_BUILDER,
182
+ MSGFMT,
183
+ MSGGREP,
184
+ MSGINIT,
185
+ MSGMERGE,
152
186
  probeTools,
153
187
  STRIP,
154
188
  TAR,
155
189
  warnMissingOptional,
190
+ XGETTEXT,
156
191
  };
@@ -0,0 +1,22 @@
1
+ import { compileCatalogs, resolveCatalogProject, synchronizeCatalogs } from "../i18n/catalogs.js";
2
+ import { createRetainedStagingDir } from "../internal/staging-dir.js";
3
+
4
+ const LOCALE_DIR_ENV = "GTKX_LOCALE_DIR";
5
+ const localeDir = createRetainedStagingDir("locale");
6
+
7
+ const prepareDevLocaleDir = (root: string, domain: string): string | null => {
8
+ const project = resolveCatalogProject(root, domain);
9
+
10
+ if (project === null) {
11
+ return null;
12
+ }
13
+
14
+ const outputDir = localeDir.retain();
15
+ synchronizeCatalogs(project);
16
+ compileCatalogs(project, outputDir);
17
+ process.env[LOCALE_DIR_ENV] = outputDir;
18
+
19
+ return outputDir;
20
+ };
21
+
22
+ export { prepareDevLocaleDir };
@@ -3,6 +3,7 @@ import { error } from "@gtkx/utils";
3
3
  import { resolve } from "node:path";
4
4
  import { DEV_ENTRY_ENV } from "./entry-env.js";
5
5
  import { prepareDevIconDir } from "./icon-dir.js";
6
+ import { prepareDevLocaleDir } from "./locale-dir.js";
6
7
  import { createDevRunner } from "./runner.js";
7
8
  import { prepareDevSchemaDir } from "./schema-dir.js";
8
9
 
@@ -22,6 +23,7 @@ const main = async (): Promise<void> => {
22
23
  }
23
24
 
24
25
  const { config, root } = await loadConfig(cwd, { mode: "development" });
26
+ prepareDevLocaleDir(root, config.applicationId);
25
27
  prepareDevSchemaDir(root, config.future?.v2ResourceImports === true);
26
28
  prepareDevIconDir(root, config.applicationId, config.applicationIcon);
27
29
  const entryPath = resolve(cwd, entryArg);