@gtkx/cli 1.6.0 → 2.0.0-beta.1

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 (186) hide show
  1. package/README.md +5 -5
  2. package/dist/codegen/freshness.d.ts.map +1 -1
  3. package/dist/codegen/freshness.js +45 -16
  4. package/dist/codegen/freshness.js.map +1 -1
  5. package/dist/codegen/reference.d.ts.map +1 -1
  6. package/dist/codegen/reference.js +1 -7
  7. package/dist/codegen/reference.js.map +1 -1
  8. package/dist/codegen/report.d.ts.map +1 -1
  9. package/dist/codegen/report.js +5 -16
  10. package/dist/codegen/report.js.map +1 -1
  11. package/dist/codegen/run-codegen.d.ts +3 -5
  12. package/dist/codegen/run-codegen.d.ts.map +1 -1
  13. package/dist/codegen/run-codegen.js +23 -100
  14. package/dist/codegen/run-codegen.js.map +1 -1
  15. package/dist/codegen/store-resolver.d.ts +0 -1
  16. package/dist/codegen/store-resolver.d.ts.map +1 -1
  17. package/dist/codegen/store-resolver.js +8 -17
  18. package/dist/codegen/store-resolver.js.map +1 -1
  19. package/dist/commands/docs.d.ts.map +1 -1
  20. package/dist/commands/docs.js +5 -15
  21. package/dist/commands/docs.js.map +1 -1
  22. package/dist/deploy/freedesktop/copyright.d.ts.map +1 -1
  23. package/dist/deploy/freedesktop/copyright.js +2 -13
  24. package/dist/deploy/freedesktop/copyright.js.map +1 -1
  25. package/dist/deploy/freedesktop/xml.d.ts.map +1 -1
  26. package/dist/deploy/freedesktop/xml.js +21 -25
  27. package/dist/deploy/freedesktop/xml.js.map +1 -1
  28. package/dist/deploy/node-runtime/license.d.ts.map +1 -1
  29. package/dist/deploy/node-runtime/license.js +2 -9
  30. package/dist/deploy/node-runtime/license.js.map +1 -1
  31. package/dist/deploy/notices/packages.d.ts.map +1 -1
  32. package/dist/deploy/notices/packages.js +3 -10
  33. package/dist/deploy/notices/packages.js.map +1 -1
  34. package/dist/deploy/payload/copy-tree.d.ts.map +1 -1
  35. package/dist/deploy/payload/copy-tree.js +3 -3
  36. package/dist/deploy/payload/copy-tree.js.map +1 -1
  37. package/dist/deploy/payload/stage.d.ts.map +1 -1
  38. package/dist/deploy/payload/stage.js +1 -3
  39. package/dist/deploy/payload/stage.js.map +1 -1
  40. package/dist/deploy/settings/build-manifest.d.ts.map +1 -1
  41. package/dist/deploy/settings/build-manifest.js +4 -15
  42. package/dist/deploy/settings/build-manifest.js.map +1 -1
  43. package/dist/deploy/settings/libraries.d.ts.map +1 -1
  44. package/dist/deploy/settings/libraries.js +2 -19
  45. package/dist/deploy/settings/libraries.js.map +1 -1
  46. package/dist/deploy/settings/paths.d.ts +1 -2
  47. package/dist/deploy/settings/paths.d.ts.map +1 -1
  48. package/dist/deploy/settings/paths.js +5 -15
  49. package/dist/deploy/settings/paths.js.map +1 -1
  50. package/dist/deploy/settings/screenshots.d.ts.map +1 -1
  51. package/dist/deploy/settings/screenshots.js +6 -31
  52. package/dist/deploy/settings/screenshots.js.map +1 -1
  53. package/dist/deploy/targets/flatpak-pnpm.js +4 -4
  54. package/dist/deploy/targets/flatpak-pnpm.js.map +1 -1
  55. package/dist/deploy/targets/flatpak-source.d.ts.map +1 -1
  56. package/dist/deploy/targets/flatpak-source.js +4 -4
  57. package/dist/deploy/targets/flatpak-source.js.map +1 -1
  58. package/dist/deploy/targets/flatpak-sources.js +1 -1
  59. package/dist/deploy/targets/flatpak-sources.js.map +1 -1
  60. package/dist/dev/runner-main.d.ts.map +1 -1
  61. package/dist/dev/runner-main.js +1 -2
  62. package/dist/dev/runner-main.js.map +1 -1
  63. package/dist/dev/schema-dir.d.ts +1 -1
  64. package/dist/dev/schema-dir.d.ts.map +1 -1
  65. package/dist/dev/schema-dir.js +2 -4
  66. package/dist/dev/schema-dir.js.map +1 -1
  67. package/dist/i18n/catalogs.d.ts.map +1 -1
  68. package/dist/i18n/catalogs.js +2 -1
  69. package/dist/i18n/catalogs.js.map +1 -1
  70. package/dist/i18n/metadata-templates.d.ts.map +1 -1
  71. package/dist/i18n/metadata-templates.js +2 -1
  72. package/dist/i18n/metadata-templates.js.map +1 -1
  73. package/dist/i18n/source-messages.d.ts +3 -21
  74. package/dist/i18n/source-messages.d.ts.map +1 -1
  75. package/dist/i18n/source-messages.js +66 -85
  76. package/dist/i18n/source-messages.js.map +1 -1
  77. package/dist/i18n/types.d.ts +6 -7
  78. package/dist/i18n/types.d.ts.map +1 -1
  79. package/dist/i18n/types.js +83 -96
  80. package/dist/i18n/types.js.map +1 -1
  81. package/dist/internal/docs-elements.js +2 -2
  82. package/dist/internal/docs-elements.js.map +1 -1
  83. package/dist/internal/icon-path.d.ts.map +1 -1
  84. package/dist/internal/icon-path.js +5 -16
  85. package/dist/internal/icon-path.js.map +1 -1
  86. package/dist/internal/source-imports.d.ts.map +1 -1
  87. package/dist/internal/source-imports.js +10 -41
  88. package/dist/internal/source-imports.js.map +1 -1
  89. package/dist/settings/render.d.ts +0 -1
  90. package/dist/settings/render.d.ts.map +1 -1
  91. package/dist/settings/render.js +1 -11
  92. package/dist/settings/render.js.map +1 -1
  93. package/dist/settings/schema.d.ts +2 -2
  94. package/dist/settings/schema.d.ts.map +1 -1
  95. package/dist/settings/schema.js +21 -62
  96. package/dist/settings/schema.js.map +1 -1
  97. package/dist/vite-plugins/asset-imports.d.ts +1 -2
  98. package/dist/vite-plugins/asset-imports.d.ts.map +1 -1
  99. package/dist/vite-plugins/asset-imports.js +19 -32
  100. package/dist/vite-plugins/asset-imports.js.map +1 -1
  101. package/dist/vite-plugins/asset-specifier.d.ts +1 -3
  102. package/dist/vite-plugins/asset-specifier.d.ts.map +1 -1
  103. package/dist/vite-plugins/asset-specifier.js +1 -4
  104. package/dist/vite-plugins/asset-specifier.js.map +1 -1
  105. package/dist/vite-plugins/i18n.d.ts.map +1 -1
  106. package/dist/vite-plugins/i18n.js +2 -1
  107. package/dist/vite-plugins/i18n.js.map +1 -1
  108. package/dist/vite-plugins/index.d.ts.map +1 -1
  109. package/dist/vite-plugins/index.js +2 -4
  110. package/dist/vite-plugins/index.js.map +1 -1
  111. package/dist/vite-plugins/resources.d.ts.map +1 -1
  112. package/dist/vite-plugins/resources.js +32 -97
  113. package/dist/vite-plugins/resources.js.map +1 -1
  114. package/dist/vite-plugins/settings-worker-env.d.ts.map +1 -1
  115. package/dist/vite-plugins/settings-worker-env.js +2 -4
  116. package/dist/vite-plugins/settings-worker-env.js.map +1 -1
  117. package/dist/vite-plugins/settings.d.ts +1 -2
  118. package/dist/vite-plugins/settings.d.ts.map +1 -1
  119. package/dist/vite-plugins/settings.js +8 -14
  120. package/dist/vite-plugins/settings.js.map +1 -1
  121. package/dist/vite-plugins/undeclared-library.d.ts.map +1 -1
  122. package/dist/vite-plugins/undeclared-library.js +10 -13
  123. package/dist/vite-plugins/undeclared-library.js.map +1 -1
  124. package/dist/vite-plugins/worker.d.ts.map +1 -1
  125. package/dist/vite-plugins/worker.js +0 -1
  126. package/dist/vite-plugins/worker.js.map +1 -1
  127. package/package.json +21 -15
  128. package/src/codegen/freshness.ts +66 -17
  129. package/src/codegen/reference.ts +1 -8
  130. package/src/codegen/report.ts +5 -21
  131. package/src/codegen/run-codegen.ts +28 -145
  132. package/src/codegen/store-resolver.ts +8 -24
  133. package/src/commands/docs.ts +5 -17
  134. package/src/deploy/freedesktop/copyright.ts +2 -14
  135. package/src/deploy/freedesktop/xml.ts +24 -32
  136. package/src/deploy/node-runtime/license.ts +2 -8
  137. package/src/deploy/notices/packages.ts +3 -11
  138. package/src/deploy/payload/copy-tree.ts +3 -4
  139. package/src/deploy/payload/stage.ts +2 -6
  140. package/src/deploy/settings/build-manifest.ts +4 -16
  141. package/src/deploy/settings/libraries.ts +1 -22
  142. package/src/deploy/settings/paths.ts +6 -16
  143. package/src/deploy/settings/screenshots.ts +8 -46
  144. package/src/deploy/targets/flatpak-pnpm.ts +4 -4
  145. package/src/deploy/targets/flatpak-source.ts +4 -4
  146. package/src/deploy/targets/flatpak-sources.ts +1 -1
  147. package/src/dev/runner-main.ts +1 -2
  148. package/src/dev/schema-dir.ts +2 -4
  149. package/src/i18n/catalogs.ts +2 -1
  150. package/src/i18n/metadata-templates.ts +2 -1
  151. package/src/i18n/source-messages.ts +77 -115
  152. package/src/i18n/types.ts +97 -131
  153. package/src/internal/docs-elements.ts +2 -2
  154. package/src/internal/icon-path.ts +6 -19
  155. package/src/internal/source-imports.ts +14 -50
  156. package/src/settings/render.ts +1 -13
  157. package/src/settings/schema.ts +19 -81
  158. package/src/vite-plugins/asset-imports.ts +20 -44
  159. package/src/vite-plugins/asset-specifier.ts +0 -5
  160. package/src/vite-plugins/i18n.ts +2 -1
  161. package/src/vite-plugins/index.ts +2 -4
  162. package/src/vite-plugins/resources.ts +37 -140
  163. package/src/vite-plugins/settings-worker-env.ts +2 -4
  164. package/src/vite-plugins/settings.ts +11 -20
  165. package/src/vite-plugins/undeclared-library.ts +14 -20
  166. package/src/vite-plugins/worker.ts +0 -1
  167. package/dist/i18n/binding-extractor.d.ts +0 -30
  168. package/dist/i18n/binding-extractor.d.ts.map +0 -1
  169. package/dist/i18n/binding-extractor.js +0 -1324
  170. package/dist/i18n/binding-extractor.js.map +0 -1
  171. package/dist/i18n/extractor-plugin.d.ts +0 -8
  172. package/dist/i18n/extractor-plugin.d.ts.map +0 -1
  173. package/dist/i18n/extractor-plugin.js +0 -247
  174. package/dist/i18n/extractor-plugin.js.map +0 -1
  175. package/dist/internal/data-dir.d.ts +0 -4
  176. package/dist/internal/data-dir.d.ts.map +0 -1
  177. package/dist/internal/data-dir.js +0 -52
  178. package/dist/internal/data-dir.js.map +0 -1
  179. package/dist/vite-plugins/animated.d.ts +0 -5
  180. package/dist/vite-plugins/animated.d.ts.map +0 -1
  181. package/dist/vite-plugins/animated.js +0 -325
  182. package/dist/vite-plugins/animated.js.map +0 -1
  183. package/src/i18n/binding-extractor.ts +0 -1969
  184. package/src/i18n/extractor-plugin.ts +0 -413
  185. package/src/internal/data-dir.ts +0 -70
  186. package/src/vite-plugins/animated.ts +0 -486
@@ -1,5 +1,6 @@
1
- import type { ExtractedKey, I18nextToolkitConfig, Logger } from "i18next-cli";
2
- import { findKeys } from "i18next-cli";
1
+ import type { ExtractedKey, ExtractedKeysMap, Logger, Plugin } from "i18next-cli";
2
+ import { isPathInside, isPathWithin, toPosixPath } from "@gtkx/utils";
3
+ import { runExtractor } from "i18next-cli";
3
4
  import {
4
5
  existsSync,
5
6
  mkdirSync,
@@ -8,12 +9,12 @@ import {
8
9
  rmSync,
9
10
  writeFileSync,
10
11
  } from "node:fs";
11
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
12
13
  import type { CatalogProject } from "./catalogs.js";
13
14
  import { runCliTool } from "../internal/run-cli-tool.js";
14
15
  import { replaceCatalogTemplate } from "./catalog-template.js";
15
- import { gtkxExtractorPlugin } from "./extractor-plugin.js";
16
16
  import { metadataTemplateFiles } from "./metadata-templates.js";
17
+ import { clearI18nResources, i18nToolkitConfig } from "./types.js";
17
18
 
18
19
  type SourceLocation = {
19
20
  file: string;
@@ -23,17 +24,11 @@ type SourceLocation = {
23
24
 
24
25
  type SourceMessage = {
25
26
  context: string | null;
26
- kind: "default" | "plural" | "pluralDefaults" | "point";
27
27
  locations: SourceLocation[];
28
- msgid: string;
29
28
  namespace: string;
30
29
  plural: string | null;
31
30
  singular: string;
32
- };
33
-
34
- type SourceCatalog = {
35
- messages: SourceMessage[];
36
- sourceFiles: string[];
31
+ sourceKey: string;
37
32
  };
38
33
 
39
34
  type SyntheticEntry = {
@@ -45,7 +40,6 @@ type SourceExtraction = {
45
40
  messages: SourceMessage[];
46
41
  output: string;
47
42
  project: CatalogProject;
48
- sourceFiles: string[];
49
43
  workDir: string;
50
44
  };
51
45
 
@@ -54,7 +48,6 @@ type CatalogTemplateExtraction = {
54
48
  output?: string | undefined;
55
49
  project: CatalogProject;
56
50
  shouldPreserveMetadataMessages: boolean;
57
- sourceFiles: string[];
58
51
  };
59
52
 
60
53
  type PluralVariant = {
@@ -94,31 +87,6 @@ const quietLogger = (): { logger: Logger; reports: string[] } => {
94
87
  };
95
88
  };
96
89
 
97
- const extractorConfig = (sourceFiles: string[]): I18nextToolkitConfig => {
98
- const integration = gtkxExtractorPlugin();
99
-
100
- return {
101
- locales: ["en"],
102
- plugins: [integration.plugin],
103
- extract: {
104
- input: sourceFiles,
105
- output: "unused/{{language}}/{{namespace}}.json",
106
- contextSeparator: CONTEXT_SEPARATOR,
107
- defaultNS: DEFAULT_NAMESPACE,
108
- disablePlurals: false,
109
- extractFromComments: false,
110
- functions: [],
111
- generateBasePluralForms: false,
112
- keySeparator: ".",
113
- nsSeparator: false,
114
- pluralSeparator: "_",
115
- transComponents: integration.transComponents,
116
- useTranslationNames: [],
117
- warnOnConflicts: "error",
118
- },
119
- };
120
- };
121
-
122
90
  const splitContext = (key: string): { context: string | null; msgid: string } => {
123
91
  const index = key.lastIndexOf(CONTEXT_SEPARATOR);
124
92
 
@@ -155,17 +123,22 @@ const normalizedLocations = (locations: SourceLocation[]): SourceLocation[] => {
155
123
  };
156
124
 
157
125
  const pointSource = (entry: ExtractedKey, msgid: string): string => {
158
- if (entry.explicitDefault !== true || typeof entry.defaultValue !== "string") {
126
+ if (typeof entry.defaultValue !== "string") {
159
127
  return msgid;
160
128
  }
161
129
 
162
- return entry.defaultValue;
130
+ if (entry.explicitDefault === true) {
131
+ return entry.defaultValue;
132
+ }
133
+
134
+ return entry.defaultValue === msgid || msgid.endsWith(`.${entry.defaultValue}`)
135
+ ? msgid
136
+ : entry.defaultValue;
163
137
  };
164
138
 
165
139
  const pointMessage = (entry: ExtractedKey): SourceMessage => {
166
140
  const { context, msgid } = splitContext(entry.key);
167
141
  validateIdentity(context, msgid);
168
- const isDefaultExplicit = entry.explicitDefault === true;
169
142
  const singular = pointSource(entry, msgid);
170
143
 
171
144
  if (singular.length === 0) {
@@ -174,12 +147,11 @@ const pointMessage = (entry: ExtractedKey): SourceMessage => {
174
147
 
175
148
  return {
176
149
  context,
177
- kind: isDefaultExplicit ? "default" : "point",
178
150
  locations: normalizedLocations(entry.locations ?? []),
179
- msgid,
180
151
  namespace: namespaceFor(entry),
181
152
  plural: null,
182
153
  singular,
154
+ sourceKey: msgid,
183
155
  };
184
156
  };
185
157
 
@@ -242,8 +214,7 @@ const pluralMessage = ({ baseKey, namespace, variants }: PluralGroup): SourceMes
242
214
  const { one, other } = pairedVariants(variants);
243
215
  const { context, msgid } = splitContext(baseKey);
244
216
  validateIdentity(context, msgid);
245
- const hasStableKey = one.defaultValue !== other.defaultValue;
246
- const singular = hasStableKey ? one.defaultValue : msgid;
217
+ const singular = one.defaultValue;
247
218
  const plural = other.defaultValue;
248
219
 
249
220
  if (singular === plural || singular.length === 0 || plural.length === 0) {
@@ -252,12 +223,11 @@ const pluralMessage = ({ baseKey, namespace, variants }: PluralGroup): SourceMes
252
223
 
253
224
  return {
254
225
  context,
255
- kind: hasStableKey ? "pluralDefaults" : "plural",
256
226
  locations: normalizedLocations(variants.flatMap((variant) => variant.locations)),
257
- msgid,
258
227
  namespace,
259
228
  plural,
260
229
  singular,
230
+ sourceKey: msgid,
261
231
  };
262
232
  };
263
233
 
@@ -295,52 +265,79 @@ const sourceMessages = (entries: ExtractedKey[]): SourceMessage[] => {
295
265
  const compareMessages = (left: SourceMessage, right: SourceMessage): number =>
296
266
  left.namespace.localeCompare(right.namespace) ||
297
267
  (left.context ?? "").localeCompare(right.context ?? "") ||
298
- left.msgid.localeCompare(right.msgid) ||
299
268
  left.singular.localeCompare(right.singular) ||
300
269
  (left.plural ?? "").localeCompare(right.plural ?? "");
301
270
 
271
+ const inferLocations = (message: SourceMessage, sources: Map<string, string>): SourceMessage => {
272
+ if (message.locations.length > 0) {
273
+ return message;
274
+ }
275
+
276
+ const locations: SourceLocation[] = [];
277
+
278
+ for (const [file, source] of sources) {
279
+ const index = source.indexOf(message.sourceKey);
280
+
281
+ if (index !== -1) {
282
+ locations.push({ file, line: source.slice(0, index).split("\n").length });
283
+ }
284
+ }
285
+
286
+ return { ...message, locations };
287
+ };
288
+
302
289
  const normalizedSourceFiles = (root: string, paths: string[]): string[] => {
303
290
  const files = paths
304
291
  .map((path) => resolve(root, path))
305
- .filter((path) => {
306
- const projectPath = relative(root, path);
307
-
308
- return projectPath !== ".." && !projectPath.startsWith(`..${sep}`) && !isAbsolute(projectPath);
309
- });
292
+ .filter((path) => isPathWithin(root, path));
310
293
 
311
294
  return new Set(files).values().toArray().toSorted((left, right) => left.localeCompare(right));
312
295
  };
313
296
 
314
- const findSourceMessages = async (sourceFiles: string[]): Promise<SourceMessage[]> => {
297
+ const findSourceMessages = async (root: string, sourceFiles: string[]): Promise<SourceMessage[]> => {
315
298
  if (sourceFiles.length === 0) {
299
+ clearI18nResources(root);
300
+
316
301
  return [];
317
302
  }
318
303
 
319
- const fileErrors: string[] = [];
304
+ let extracted: ExtractedKeysMap | undefined;
320
305
  const { logger, reports } = quietLogger();
321
- const { allKeys } = await findKeys(extractorConfig(sourceFiles), logger, fileErrors);
322
306
 
323
- if (fileErrors.length > 0 || reports.length > 0) {
324
- throw new Error([...fileErrors, ...reports].join("\n"));
307
+ const capture: Plugin = {
308
+ name: "gtkx-gettext",
309
+ onEnd(keys) {
310
+ extracted = keys;
311
+ },
312
+ };
313
+
314
+ const result = await runExtractor(i18nToolkitConfig(root, sourceFiles, [capture]), {
315
+ logger,
316
+ quiet: true,
317
+ syncPrimaryWithDefaults: true,
318
+ trustDerivedDefaults: true,
319
+ });
320
+
321
+ if (extracted === undefined || reports.length > 0 || result.hasErrors) {
322
+ throw new Error(reports.join("\n") || "i18next extraction failed");
325
323
  }
326
324
 
327
- return sourceMessages(allKeys.values().toArray()).toSorted(compareMessages);
325
+ const sources = new Map(sourceFiles.map((file) => [file, readFileSync(file, "utf8")]));
326
+
327
+ return sourceMessages(extracted.values().toArray())
328
+ .map((message) => inferLocations(message, sources))
329
+ .toSorted(compareMessages);
328
330
  };
329
331
 
330
332
  const projectPath = (root: string, path: string): string | null => {
331
333
  const absolute = isAbsolute(path) ? resolve(path) : resolve(root, path);
332
334
  const projectRelative = relative(root, absolute);
333
335
 
334
- if (
335
- projectRelative === "" ||
336
- projectRelative === ".." ||
337
- projectRelative.startsWith(`..${sep}`) ||
338
- isAbsolute(projectRelative)
339
- ) {
336
+ if (!isPathInside(root, absolute)) {
340
337
  return null;
341
338
  }
342
339
 
343
- return projectRelative.replaceAll("\\", "/");
340
+ return toPosixPath(projectRelative);
344
341
  };
345
342
 
346
343
  const writePotfiles = (project: CatalogProject, sourceFiles: string[]): string => {
@@ -371,9 +368,6 @@ const renderCatalogCall = (message: SourceMessage): string => {
371
368
  : `pgettext(${JSON.stringify(message.context)}, ${msgid});`;
372
369
  };
373
370
 
374
- const readSources = (sourceFiles: string[]): Map<string, string> =>
375
- new Map(sourceFiles.map((path) => [path, readFileSync(path, "utf8")]));
376
-
377
371
  const locatedOwners = (
378
372
  project: CatalogProject,
379
373
  message: SourceMessage,
@@ -393,49 +387,23 @@ const locatedOwners = (
393
387
  return unique.values().toArray();
394
388
  };
395
389
 
396
- const inferredOwner = (
397
- project: CatalogProject,
398
- message: SourceMessage,
399
- sources: Map<string, string>,
400
- ): { path: string; line: number } | null => {
401
- for (const [path, source] of sources) {
402
- const index = source.indexOf(message.msgid);
403
-
404
- if (index !== -1) {
405
- return {
406
- path: projectPath(project.root, path) ?? SYNTHETIC_FILENAME,
407
- line: source.slice(0, index).split("\n").length,
408
- };
409
- }
410
- }
411
-
412
- return null;
413
- };
414
-
415
390
  const sourceOwners = (
416
391
  project: CatalogProject,
417
392
  message: SourceMessage,
418
- sources: Map<string, string>,
419
393
  ): { path: string; line: number | undefined }[] => {
420
394
  const located = locatedOwners(project, message);
421
395
 
422
- if (located.length > 0) {
423
- return located;
424
- }
425
-
426
- return [inferredOwner(project, message, sources) ?? { path: SYNTHETIC_FILENAME, line: undefined }];
396
+ return located.length > 0 ? located : [{ path: SYNTHETIC_FILENAME, line: undefined }];
427
397
  };
428
398
 
429
399
  const syntheticEntries = (
430
400
  project: CatalogProject,
431
401
  messages: SourceMessage[],
432
- sourceFiles: string[],
433
402
  ): Map<string, SyntheticEntry[]> => {
434
- const sources = readSources(sourceFiles);
435
403
  const entries: Map<string, SyntheticEntry[]> = new Map();
436
404
 
437
405
  for (const message of messages) {
438
- for (const owner of sourceOwners(project, message, sources)) {
406
+ for (const owner of sourceOwners(project, message)) {
439
407
  const owned = entries.get(owner.path) ?? [];
440
408
  owned.push({ call: renderCatalogCall(message), line: owner.line });
441
409
  entries.set(owner.path, owned);
@@ -492,9 +460,8 @@ const writeSyntheticSources = (
492
460
  workDir: string,
493
461
  project: CatalogProject,
494
462
  messages: SourceMessage[],
495
- sourceFiles: string[],
496
463
  ): string => {
497
- const entries = syntheticEntries(project, messages, sourceFiles);
464
+ const entries = syntheticEntries(project, messages);
498
465
  const paths: string[] = [];
499
466
 
500
467
  for (const [path, owned] of entries) {
@@ -510,8 +477,8 @@ const writeSyntheticSources = (
510
477
  return potfiles;
511
478
  };
512
479
 
513
- const extractSourceMessages = ({ project, messages, sourceFiles, output, workDir }: SourceExtraction): void => {
514
- const potfilesPath = writeSyntheticSources(workDir, project, messages, sourceFiles);
480
+ const extractSourceMessages = ({ project, messages, output, workDir }: SourceExtraction): void => {
481
+ const potfilesPath = writeSyntheticSources(workDir, project, messages);
515
482
 
516
483
  runCliTool({
517
484
  tool: "xgettext",
@@ -555,7 +522,6 @@ const joinMetadataFragment = (output: string, fragment: string): void => {
555
522
  const extractCatalogTemplate = ({
556
523
  project,
557
524
  messages,
558
- sourceFiles,
559
525
  shouldPreserveMetadataMessages,
560
526
  output = resolve(project.poDir, `${project.domain}.pot`),
561
527
  }: CatalogTemplateExtraction): void => {
@@ -567,10 +533,10 @@ const extractCatalogTemplate = ({
567
533
  if (shouldPreserveMetadataMessages && hasPreviousTemplate) {
568
534
  const fragment = join(workDir, "metadata.pot");
569
535
  extractMetadataFragment(project, output, fragment);
570
- extractSourceMessages({ project, messages, sourceFiles, output: source, workDir });
536
+ extractSourceMessages({ project, messages, output: source, workDir });
571
537
  joinMetadataFragment(source, fragment);
572
538
  } else {
573
- extractSourceMessages({ project, messages, sourceFiles, output: source, workDir });
539
+ extractSourceMessages({ project, messages, output: source, workDir });
574
540
  }
575
541
 
576
542
  replaceCatalogTemplate(source, output);
@@ -583,26 +549,22 @@ const extractSourceCatalogTo = async (
583
549
  project: CatalogProject,
584
550
  paths: string[],
585
551
  output: string,
586
- ): Promise<SourceCatalog> => {
552
+ ): Promise<void> => {
587
553
  const sourceFiles = normalizedSourceFiles(project.root, paths);
588
- const messages = await findSourceMessages(sourceFiles);
554
+ const messages = await findSourceMessages(project.root, sourceFiles);
589
555
  writePotfiles(project, sourceFiles);
590
- extractCatalogTemplate({ project, messages, sourceFiles, shouldPreserveMetadataMessages: false, output });
591
-
592
- return { messages, sourceFiles };
556
+ extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages: false, output });
593
557
  };
594
558
 
595
559
  const extractSourceCatalog = async (
596
560
  project: CatalogProject,
597
561
  paths: string[],
598
562
  shouldPreserveMetadataMessages = true,
599
- ): Promise<SourceCatalog> => {
563
+ ): Promise<void> => {
600
564
  const sourceFiles = normalizedSourceFiles(project.root, paths);
601
- const messages = await findSourceMessages(sourceFiles);
565
+ const messages = await findSourceMessages(project.root, sourceFiles);
602
566
  writePotfiles(project, sourceFiles);
603
- extractCatalogTemplate({ project, messages, sourceFiles, shouldPreserveMetadataMessages });
604
-
605
- return { messages, sourceFiles };
567
+ extractCatalogTemplate({ project, messages, shouldPreserveMetadataMessages });
606
568
  };
607
569
 
608
- export { type SourceMessage, extractSourceCatalog, extractSourceCatalogTo };
570
+ export { extractSourceCatalog, extractSourceCatalogTo };
package/src/i18n/types.ts CHANGED
@@ -1,154 +1,120 @@
1
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import type { I18nextToolkitConfig, Logger, Plugin } from "i18next-cli";
2
+ import { runTypesGenerator } from "i18next-cli";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
4
  import { dirname, join } from "node:path";
3
- import type { SourceMessage } from "./source-messages.js";
4
-
5
- type I18nTypesResult = {
6
- isWritten: boolean;
7
- path: string;
8
- };
9
5
 
6
+ const CONTEXT_SEPARATOR = "\u{4}";
7
+ const DEFAULT_NAMESPACE = "translation";
10
8
  const I18N_TYPES_FILENAME = "i18n.d.ts";
11
- const INTERPOLATION_PATTERN = /\{\{[^{}]+\}\}/u;
12
-
13
- const stringType = (values: string[]): string =>
14
- [...new Set(values)].map((value) => JSON.stringify(value)).join(" | ");
15
-
16
- const contextField = (context: string | null): string =>
17
- context === null ? "" : ` context: ${JSON.stringify(context)};`;
18
-
19
- const hasInterpolation = (values: string[]): boolean => values.some((value) => INTERPOLATION_PATTERN.test(value));
9
+ const I18N_RESOURCES_FILENAME = "i18n-resources.d.ts";
20
10
 
21
- const pointVariant = (message: SourceMessage): string => {
22
- const isRequired = message.context !== null || hasInterpolation([message.msgid]);
23
- const isInterpolated = hasInterpolation([message.msgid]);
11
+ const generatedDir = (root: string): string => join(root, "node_modules", ".gtkx");
12
+ const i18nResourceDir = (root: string): string => join(generatedDir(root), "i18n");
13
+ const i18nTypesPath = (root: string): string => join(generatedDir(root), I18N_TYPES_FILENAME);
14
+ const i18nResourcesPath = (root: string): string => join(generatedDir(root), I18N_RESOURCES_FILENAME);
24
15
 
25
- return (
26
- `{${contextField(message.context)} interpolated: ${String(isInterpolated)}; kind: "point"; ` +
27
- `required: ${String(isRequired)}; value: ${JSON.stringify(message.msgid)} }`
28
- );
16
+ const clearI18nResources = (root: string): void => {
17
+ rmSync(i18nResourceDir(root), { force: true, recursive: true });
29
18
  };
30
19
 
31
- const defaultVariant = (message: SourceMessage): string => {
32
- const values = [message.singular];
33
- const isInterpolated = hasInterpolation(values);
34
- const isRequired = message.context !== null || isInterpolated;
20
+ const i18nToolkitConfig = (
21
+ root: string,
22
+ sourceFiles: string[],
23
+ plugins: Plugin[] = [],
24
+ ): I18nextToolkitConfig => ({
25
+ locales: ["en"],
26
+ plugins,
27
+ extract: {
28
+ contextSeparator: CONTEXT_SEPARATOR,
29
+ defaultNS: DEFAULT_NAMESPACE,
30
+ disablePlurals: false,
31
+ extractFromComments: false,
32
+ functions: ["t"],
33
+ generateBasePluralForms: false,
34
+ input: sourceFiles,
35
+ keySeparator: ".",
36
+ nsSeparator: false,
37
+ output: join(i18nResourceDir(root), "{{language}}", "{{namespace}}.json"),
38
+ pluralSeparator: "_",
39
+ primaryLanguage: "en",
40
+ removeUnusedKeys: true,
41
+ transComponents: ["Trans", "TransWithoutContext"],
42
+ useTranslationNames: ["useTranslation"],
43
+ warnOnConflicts: "error",
44
+ },
45
+ types: {
46
+ input: join(i18nResourceDir(root), "en", "*.json"),
47
+ output: i18nTypesPath(root),
48
+ resourcesFile: i18nResourcesPath(root),
49
+ },
50
+ });
51
+
52
+ const declarations = (): string => `import type Resources from "./i18n-resources.js";
53
+
54
+ declare module "@gtkx/i18n" {
55
+ interface I18nResources extends Resources {}
56
+ }
57
+ `;
58
+
59
+ const writeChanged = (path: string, content: string): void => {
60
+ let current: string | null;
35
61
 
36
- return (
37
- `{${contextField(message.context)} defaultValue: ${JSON.stringify(message.singular)}; ` +
38
- `interpolated: ${String(isInterpolated)}; kind: "default"; required: ${String(isRequired)}; ` +
39
- `value: ${stringType(values)} }`
40
- );
41
- };
42
-
43
- const requirePlural = (message: SourceMessage): string => {
44
- if (message.plural === null) {
45
- throw new Error("Plural translation metadata requires a plural source string");
46
- }
47
-
48
- return message.plural;
49
- };
50
-
51
- const pluralVariant = (message: SourceMessage): string => {
52
- const plural = requirePlural(message);
53
- const values = [message.singular, plural];
54
- const isInterpolated = hasInterpolation(values);
55
-
56
- return (
57
- `{${contextField(message.context)} defaultValue: ${JSON.stringify(plural)}; ` +
58
- `interpolated: ${String(isInterpolated)}; kind: "plural"; required: true; ` +
59
- `value: ${stringType(values)} }`
60
- );
61
- };
62
-
63
- const pluralDefaultsVariant = (message: SourceMessage): string => {
64
- const plural = requirePlural(message);
65
- const values = [message.singular, plural];
66
- const isInterpolated = hasInterpolation(values);
67
-
68
- return (
69
- `{${contextField(message.context)} defaultValueOne: ${JSON.stringify(message.singular)}; ` +
70
- `defaultValueOther: ${JSON.stringify(plural)}; interpolated: ${String(isInterpolated)}; ` +
71
- `kind: "pluralDefaults"; required: true; value: ${stringType(values)} }`
72
- );
73
- };
74
-
75
- const variantsFor = (message: SourceMessage): string[] => {
76
- if (message.kind === "plural") {
77
- return [pluralVariant(message)];
62
+ try {
63
+ current = readFileSync(path, "utf8");
64
+ } catch {
65
+ current = null;
78
66
  }
79
67
 
80
- if (message.kind === "pluralDefaults") {
81
- return [pluralDefaultsVariant(message)];
68
+ if (current === content) {
69
+ return;
82
70
  }
83
71
 
84
- return message.kind === "default" ? [defaultVariant(message)] : [pointVariant(message)];
72
+ mkdirSync(dirname(path), { recursive: true });
73
+ writeFileSync(path, content);
85
74
  };
86
75
 
87
- const renderRegistry = (messages: SourceMessage[]): string[] => {
88
- const entries: Map<string, Set<string>> = new Map();
89
-
90
- for (const message of messages) {
91
- const variants = entries.get(message.msgid) ?? new Set<string>();
92
-
93
- for (const variant of variantsFor(message)) {
94
- variants.add(variant);
95
- }
96
-
97
- entries.set(message.msgid, variants);
98
- }
99
-
100
- return [...entries]
101
- .toSorted(([left], [right]) => left.localeCompare(right))
102
- .map(([msgid, variants]) => ` ${JSON.stringify(msgid)}: ${[...variants].join(" | ")};`);
76
+ const typegenLogger = (): { errors: string[]; logger: Logger } => {
77
+ const errors: string[] = [];
78
+
79
+ return {
80
+ errors,
81
+ logger: {
82
+ error(value) {
83
+ errors.push(String(value));
84
+ },
85
+ info() {
86
+ return;
87
+ },
88
+ warn() {
89
+ return;
90
+ },
91
+ },
92
+ };
103
93
  };
104
94
 
105
- const generatedHeader = (): string[] => [
106
- "/**",
107
- " * Translation declarations generated by GTKX. Regenerated by",
108
- " * `gtkx codegen`, `gtkx dev`, and `gtkx build`; do not edit.",
109
- " */",
110
- ];
95
+ const emitI18nTypes = async (root: string): Promise<void> => {
96
+ const typesPath = i18nTypesPath(root);
97
+ const resourcesPath = i18nResourcesPath(root);
98
+ writeChanged(typesPath, declarations());
99
+ const { errors, logger } = typegenLogger();
100
+ await runTypesGenerator(i18nToolkitConfig(root, []), { logger, quiet: true });
111
101
 
112
- const renderI18nTypes = (messages: SourceMessage[], isEnabled = true): string => {
113
- if (!isEnabled) {
114
- return [...generatedHeader(), "export {};", ""].join("\n");
102
+ if (errors.length > 0 || !existsSync(typesPath) || !existsSync(resourcesPath)) {
103
+ throw new Error(errors.join("\n") || "i18next type generation did not produce its outputs");
115
104
  }
116
-
117
- return [
118
- ...generatedHeader(),
119
- 'import "@gtkx/i18n";',
120
- "",
121
- 'declare module "@gtkx/i18n" {',
122
- " interface TranslationRegistry {",
123
- ...renderRegistry(messages),
124
- " }",
125
- "}",
126
- "",
127
- ].join("\n");
128
105
  };
129
106
 
130
- const i18nTypesPath = (root: string): string => join(root, "node_modules", ".gtkx", I18N_TYPES_FILENAME);
131
-
132
- const readFileOrNull = (path: string): string | null => {
133
- try {
134
- return readFileSync(path, "utf8");
135
- } catch {
136
- return null;
137
- }
107
+ const clearI18nTypes = (root: string): void => {
108
+ rmSync(i18nTypesPath(root), { force: true });
109
+ rmSync(i18nResourcesPath(root), { force: true });
110
+ clearI18nResources(root);
138
111
  };
139
112
 
140
- const emitI18nTypes = (root: string, messages: SourceMessage[], isEnabled = true): I18nTypesResult => {
141
- const path = i18nTypesPath(root);
142
- const content = renderI18nTypes(messages, isEnabled);
143
-
144
- if (readFileOrNull(path) === content) {
145
- return { isWritten: false, path };
146
- }
147
-
148
- mkdirSync(dirname(path), { recursive: true });
149
- writeFileSync(path, content);
150
-
151
- return { isWritten: true, path };
113
+ export {
114
+ I18N_TYPES_FILENAME,
115
+ clearI18nResources,
116
+ clearI18nTypes,
117
+ emitI18nTypes,
118
+ i18nToolkitConfig,
119
+ i18nTypesPath,
152
120
  };
153
-
154
- export { I18N_TYPES_FILENAME, emitI18nTypes, i18nTypesPath };
@@ -7,13 +7,13 @@ type DocsElements = {
7
7
  };
8
8
 
9
9
  const resolveDocsElements = async (cwd: string): Promise<DocsElements> => {
10
- const { gi, reactSubexports } = resolveStore(cwd);
10
+ const { gi } = resolveStore(cwd);
11
11
 
12
12
  if (!existsSync(gi.storeDir)) {
13
13
  return { props: {}, omittedProps: {} };
14
14
  }
15
15
 
16
- const { props, omittedProps } = await readBuiltinElements(reactSubexports, gi.storeDir);
16
+ const { props, omittedProps } = await readBuiltinElements();
17
17
 
18
18
  return { props, omittedProps };
19
19
  };