@gjsify/vite-plugin-gettext 0.3.21 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/xgettext.ts DELETED
@@ -1,525 +0,0 @@
1
- import { type Plugin } from "vite";
2
- import { execa } from "execa";
3
- import path from "node:path";
4
- import fs from "node:fs/promises";
5
- import { existsSync } from "node:fs";
6
- import glob from "fast-glob";
7
- import type { XGettextPluginOptions } from "./types.js";
8
- import {
9
- checkDependencies,
10
- ensureDirectory,
11
- processFilename,
12
- } from "./utils.js";
13
-
14
- // Add GLib preset constants
15
- // From https://github.com/mesonbuild/meson/blob/467da051c859ba3112803b035e317bddadd756ef/mesonbuild/modules/i18n.py
16
- const GLIB_PRESET_ARGS = [
17
- "--from-code=UTF-8",
18
- "--add-comments",
19
- // https://developer.gnome.org/glib/stable/glib-I18N.html
20
- "--keyword=_",
21
- "--keyword=N_",
22
- "--keyword=C_:1c,2",
23
- "--keyword=NC_:1c,2",
24
- "--keyword=g_dcgettext:2",
25
- "--keyword=g_dngettext:2,3",
26
- "--keyword=g_dpgettext2:2c,3",
27
- "--flag=N_:1:pass-c-format",
28
- "--flag=C_:2:pass-c-format",
29
- "--flag=NC_:2:pass-c-format",
30
- "--flag=g_dngettext:2:pass-c-format",
31
- "--flag=g_strdup_printf:1:c-format",
32
- "--flag=g_string_printf:2:c-format",
33
- "--flag=g_string_append_printf:2:c-format",
34
- "--flag=g_error_new:3:c-format",
35
- "--flag=g_set_error:4:c-format",
36
- "--flag=g_markup_printf_escaped:1:c-format",
37
- "--flag=g_log:3:c-format",
38
- "--flag=g_print:1:c-format",
39
- "--flag=g_printerr:1:c-format",
40
- "--flag=g_printf:1:c-format",
41
- "--flag=g_fprintf:2:c-format",
42
- "--flag=g_sprintf:2:c-format",
43
- "--flag=g_snprintf:3:c-format",
44
- ];
45
-
46
- /**
47
- * Build command arguments with common options
48
- * @param baseArgs Base arguments for the command
49
- * @param options Options to add to arguments
50
- * @returns Complete argument array
51
- */
52
- function buildCommandArgs(
53
- baseArgs: string[],
54
- options: {
55
- noLocation?: boolean;
56
- noWrap?: boolean;
57
- sortOutput?: boolean;
58
- additionalOptions?: string[];
59
- }
60
- ): string[] {
61
- const args = [...baseArgs];
62
-
63
- // Check if additional options already contain the flags to avoid duplicates
64
- const additionalOptions = options.additionalOptions || [];
65
- const hasNoLocation = additionalOptions.includes("--no-location");
66
- const hasNoWrap = additionalOptions.includes("--no-wrap");
67
- const hasSortOutput = additionalOptions.includes("--sort-output");
68
-
69
- if (options.noLocation && !hasNoLocation) {
70
- args.push("--no-location");
71
- }
72
-
73
- if (options.noWrap && !hasNoWrap) {
74
- args.push("--no-wrap");
75
- }
76
-
77
- if (options.sortOutput && !hasSortOutput) {
78
- args.push("--sort-output");
79
- }
80
-
81
- if (additionalOptions.length > 0) {
82
- args.push(...additionalOptions);
83
- }
84
-
85
- return args;
86
- }
87
-
88
- /**
89
- * Creates a Vite plugin that extracts translatable strings from source files
90
- * Uses GNU xgettext to generate a POT template file that can be used as basis for translations
91
- * @param options Configuration options for the plugin
92
- * @returns A Vite plugin that handles string extraction
93
- */
94
- export function xgettextPlugin(options: XGettextPluginOptions): Plugin {
95
- const pluginName = "vite-plugin-xgettext";
96
-
97
- return {
98
- name: pluginName,
99
-
100
- async buildStart() {
101
- await checkDependencies("xgettext", pluginName, options.verbose ?? false);
102
- const files = await glob(options.sources);
103
- await extractStrings(files, options, pluginName);
104
- },
105
-
106
- configureServer(server) {
107
- server.watcher.add(options.sources);
108
-
109
- server.watcher.on("change", async (file) => {
110
- if (options.sources.some((pattern) => file.match(pattern))) {
111
- if (options.verbose) {
112
- console.log(
113
- `[${pluginName}] Source file changed: ${file}, re-running extraction`
114
- );
115
- }
116
- const files = await glob(options.sources);
117
- await extractStrings(files, options, pluginName);
118
- }
119
- });
120
- },
121
- };
122
- }
123
-
124
- async function generatePotfiles(
125
- files: string[],
126
- outputDir: string,
127
- pluginName: string,
128
- verbose = false
129
- ) {
130
- // Group files by extension
131
- const fileGroups = new Map<string, string[]>();
132
-
133
- files.forEach((file) => {
134
- const filename = path.basename(file);
135
- const group = getFileGroup(filename);
136
- if (!fileGroups.has(group)) {
137
- fileGroups.set(group, []);
138
- }
139
- fileGroups.get(group)?.push(file);
140
- });
141
-
142
- // Generate POTFILES for each group
143
- const potFiles: string[] = [];
144
-
145
- for (const [group, groupFiles] of fileGroups) {
146
- const potfilePath = path.join(outputDir, `${group}.POTFILES`);
147
- const content = groupFiles.join("\n");
148
-
149
- try {
150
- await fs.writeFile(potfilePath, content);
151
- potFiles.push(potfilePath);
152
- if (verbose) {
153
- console.log(
154
- `[${pluginName}] Generated ${group}.POTFILES with ${groupFiles.length} source files`
155
- );
156
- }
157
- } catch (error) {
158
- console.error(`[${pluginName}] Error writing ${group}.POTFILES:`, error);
159
- }
160
- }
161
-
162
- return potFiles;
163
- }
164
-
165
- function getFileGroup(fullFilename: string): string {
166
- // Process filename to handle .in extension
167
- const { filename, extension } = processFilename(fullFilename);
168
-
169
- // Special handling for metainfo.xml files
170
- if (filename.endsWith(".metainfo.xml") || filename.endsWith(".appdata.xml")) {
171
- return "metainfo";
172
- }
173
-
174
- switch (extension) {
175
- case ".ts":
176
- case ".js":
177
- case ".tsx":
178
- return "js";
179
- case ".ui":
180
- case ".xml":
181
- return "ui";
182
- case ".blp":
183
- return "blp";
184
- case ".desktop":
185
- return "desktop";
186
- default:
187
- return "other";
188
- }
189
- }
190
-
191
- async function extractStrings(
192
- files: string[],
193
- options: XGettextPluginOptions,
194
- pluginName: string
195
- ) {
196
- const {
197
- output,
198
- domain = "messages",
199
- keywords = [],
200
- preset,
201
- verbose = false,
202
- } = options;
203
-
204
- const noWrap = options.noWrap || false;
205
-
206
- try {
207
- const outputDir = path.dirname(output);
208
- await ensureDirectory(outputDir);
209
-
210
- // Read existing POT-Creation-Date from previous POT if present (for preservation)
211
- let prevPotCreationDate: string | undefined;
212
- try {
213
- const existingPot = await fs.readFile(output, "utf-8");
214
- const m = existingPot.match(/"POT-Creation-Date:\s*([^\n]+)\\n"/);
215
- if (m && m[1]) {
216
- prevPotCreationDate = m[1];
217
- if (verbose) {
218
- console.log(
219
- `[${pluginName}] Found previous POT-Creation-Date '${prevPotCreationDate}'`
220
- );
221
- }
222
- }
223
- } catch {
224
- // No previous POT available
225
- }
226
-
227
- // Generate grouped POTFILES
228
- const potFiles = await generatePotfiles(
229
- files,
230
- outputDir,
231
- pluginName,
232
- verbose
233
- );
234
-
235
- // Create temporary POT files for each group
236
- const tempPotFiles: string[] = [];
237
-
238
- for (const potFile of potFiles) {
239
- const group = path.basename(potFile).split(".")[0];
240
- const tempOutput = path.join(outputDir, `temp_${group}.pot`);
241
-
242
- // Build base arguments
243
- const baseArgs = [
244
- "--package-name=" + domain,
245
- options.version ? "--package-version=" + options.version : "",
246
- "--output=" + tempOutput,
247
- "--files-from=" + potFile,
248
- "--from-code=UTF-8",
249
- "--add-comments",
250
- ].filter(Boolean);
251
-
252
- // Add bug report address if specified
253
- if (options.msgidBugsAddress) {
254
- baseArgs.push("--msgid-bugs-address=" + options.msgidBugsAddress);
255
- }
256
-
257
- // Add copyright holder if specified
258
- if (options.copyrightHolder) {
259
- baseArgs.push("--copyright-holder=" + options.copyrightHolder);
260
- }
261
-
262
- // Add language-specific settings
263
- switch (group) {
264
- case "js":
265
- case "blp":
266
- baseArgs.push("--language=JavaScript");
267
- baseArgs.push(...keywords.map((k) => `--keyword=${k}`));
268
- if (preset === "glib") {
269
- baseArgs.push(...GLIB_PRESET_ARGS);
270
- }
271
- break;
272
- case "ui":
273
- baseArgs.push("--language=Glade");
274
- break;
275
- case "metainfo":
276
- // Find the first existing metainfo.its file
277
- const metainfoItsPath = await findMetainfoItsPath();
278
-
279
- if (!metainfoItsPath) {
280
- console.warn(
281
- "Warning: Could not find metainfo.its in any of the expected locations"
282
- );
283
- // Continue without the ITS file
284
- } else {
285
- baseArgs.push(`--its=${metainfoItsPath}`);
286
- }
287
- break;
288
- case "desktop":
289
- baseArgs.push("--language=Desktop");
290
- break;
291
- }
292
-
293
- // Build final arguments with options handling
294
- const args = buildCommandArgs(baseArgs, {
295
- noLocation: options.noLocation,
296
- noWrap,
297
- additionalOptions: options.xgettextOptions
298
- });
299
-
300
- if (verbose) {
301
- console.log(
302
- `[${pluginName}] Running xgettext for ${group}:`,
303
- args.join(" ")
304
- );
305
- }
306
-
307
- // Enforce deterministic timestamps if requested
308
- const env = { ...process.env };
309
- if (options.deterministic) {
310
- const epoch =
311
- typeof options.sourceDateEpoch === "number"
312
- ? options.sourceDateEpoch
313
- : 0;
314
- env.SOURCE_DATE_EPOCH = String(epoch);
315
- }
316
-
317
- await execa("xgettext", args, { env });
318
-
319
- // Check if file exists before adding to tempPotFiles
320
- try {
321
- await fs.access(tempOutput);
322
- tempPotFiles.push(tempOutput);
323
- if (verbose) {
324
- console.log(
325
- `[${pluginName}] Successfully created temporary POT file: ${tempOutput}`
326
- );
327
- }
328
- } catch (error) {
329
- console.warn(
330
- `[${pluginName}] Failed to create temporary POT file: ${tempOutput}`
331
- );
332
- }
333
- }
334
-
335
- // Combine all temporary POT files using msgcat
336
- if (tempPotFiles.length > 0) {
337
- const baseMsgcatArgs = ["--use-first", "-o", output, ...tempPotFiles];
338
- const msgcatArgs = buildCommandArgs(baseMsgcatArgs, {
339
- noLocation: options.noLocation,
340
- sortOutput: options.sortOutput,
341
- noWrap,
342
- additionalOptions: options.msgcatOptions
343
- });
344
-
345
- const env = { ...process.env };
346
- if (options.deterministic) {
347
- const epoch =
348
- typeof options.sourceDateEpoch === "number"
349
- ? options.sourceDateEpoch
350
- : 0;
351
- env.SOURCE_DATE_EPOCH = String(epoch);
352
- }
353
-
354
- await execa("msgcat", msgcatArgs, { env });
355
-
356
- // Clean up temporary files
357
- for (const tempFile of tempPotFiles) {
358
- await fs.unlink(tempFile);
359
- }
360
- for (const potFile of potFiles) {
361
- await fs.unlink(potFile);
362
- }
363
- }
364
-
365
- // Optionally normalize POT-Creation-Date header to a fixed or preserved value
366
- if (options.fixedCreationDate || options.preserveCreationDate || options.deterministic) {
367
- try {
368
- let normalizedDate: string | undefined = undefined;
369
-
370
- if (options.fixedCreationDate) {
371
- normalizedDate = options.fixedCreationDate;
372
- } else if (options.preserveCreationDate) {
373
- if (prevPotCreationDate) {
374
- normalizedDate = prevPotCreationDate;
375
- if (verbose) {
376
- console.log(
377
- `[${pluginName}] Preserving existing POT-Creation-Date '${normalizedDate}'`
378
- );
379
- }
380
- }
381
- }
382
-
383
- if (!normalizedDate && options.deterministic) {
384
- normalizedDate = formatSourceDateEpoch(
385
- typeof options.sourceDateEpoch === "number" ? options.sourceDateEpoch : 0
386
- );
387
- }
388
-
389
- if (normalizedDate) {
390
- const content = await fs.readFile(output, "utf-8");
391
- const replaced = content.replace(
392
- /^"POT-Creation-Date: .*\\n"$/m,
393
- `"POT-Creation-Date: ${normalizedDate}\\n"`
394
- );
395
- if (replaced !== content) {
396
- await fs.writeFile(output, replaced);
397
- if (verbose) {
398
- console.log(
399
- `[${pluginName}] Normalized POT-Creation-Date to '${normalizedDate}'`
400
- );
401
- }
402
- }
403
- }
404
- } catch (e) {
405
- console.warn(
406
- `[${pluginName}] Failed to normalize POT-Creation-Date header:`,
407
- e
408
- );
409
- }
410
- }
411
-
412
- if (options.autoUpdatePo) {
413
- await updatePoFiles(
414
- options.output,
415
- pluginName,
416
- options.verbose || false,
417
- options
418
- );
419
- }
420
- } catch (error) {
421
- throw new Error(`Failed to extract translations: ${error}`);
422
- }
423
- }
424
-
425
- async function updatePoFiles(
426
- potFile: string,
427
- pluginName: string,
428
- verbose: boolean,
429
- options: XGettextPluginOptions
430
- ) {
431
- try {
432
- const linguasPath = path.join(path.dirname(potFile), "LINGUAS");
433
- const languages = (await fs.readFile(linguasPath, "utf-8"))
434
- .split("\n")
435
- .filter(Boolean);
436
-
437
- for (const lang of languages) {
438
- const poFile = path.join(path.dirname(potFile), `${lang}.po`);
439
- if (verbose) {
440
- console.log(`[${pluginName}] Updating ${poFile}`);
441
- }
442
- const baseMsgmergeArgs = ["--update", "--backup=none", poFile, potFile];
443
- const args = buildCommandArgs(baseMsgmergeArgs, {
444
- noLocation: options.noLocation,
445
- noWrap: options.noWrap
446
- });
447
-
448
- const env = { ...process.env };
449
- if (options.deterministic) {
450
- const epoch =
451
- typeof options.sourceDateEpoch === "number"
452
- ? options.sourceDateEpoch
453
- : 0;
454
- env.SOURCE_DATE_EPOCH = String(epoch);
455
- }
456
-
457
- await execa("msgmerge", args, { env });
458
-
459
- // Post-process with msgcat to unwrap existing wrapped lines
460
- if (options.noWrap) {
461
- const tempFile = poFile + ".tmp";
462
- const msgcatArgs = ["--width=0", "--no-wrap", "-o", tempFile, poFile];
463
- await execa("msgcat", msgcatArgs, { env });
464
- await fs.rename(tempFile, poFile);
465
- if (verbose) {
466
- console.log(`[${pluginName}] Unwrapped lines in ${poFile}`);
467
- }
468
- }
469
- }
470
- } catch (error) {
471
- console.error(`[${pluginName}] Error updating PO files:`, error);
472
- }
473
- }
474
-
475
- /**
476
- * Formats a date in gettext header format using an epoch (seconds) in UTC timezone
477
- * Example output: 1970-01-01 00:00+0000
478
- */
479
- function formatSourceDateEpoch(epochSeconds: number): string {
480
- const date = new Date(epochSeconds * 1000);
481
- const pad = (n: number) => String(n).padStart(2, "0");
482
- const year = date.getUTCFullYear();
483
- const month = pad(date.getUTCMonth() + 1);
484
- const day = pad(date.getUTCDate());
485
- const hours = pad(date.getUTCHours());
486
- const minutes = pad(date.getUTCMinutes());
487
- return `${year}-${month}-${day} ${hours}:${minutes}+0000`;
488
- }
489
-
490
- /**
491
- * Finds the first existing metainfo.its file from installed gettext versions
492
- * @returns The path to the metainfo.its file if found, otherwise undefined
493
- */
494
- async function findMetainfoItsPath(): Promise<string | undefined> {
495
- // Default path
496
- const defaultPath = "/usr/share/gettext/its/metainfo.its";
497
-
498
- // Check default path first
499
- if (existsSync(defaultPath)) {
500
- return defaultPath;
501
- }
502
-
503
- try {
504
- // Use glob to find all potential gettext version directories
505
- const getTextDirs = await glob("/usr/share/gettext-*");
506
-
507
- // Sort by version (newest first) if possible
508
- getTextDirs.sort((a, b) => {
509
- const versionA = a.replace("/usr/share/gettext-", "");
510
- const versionB = b.replace("/usr/share/gettext-", "");
511
- return versionB.localeCompare(versionA);
512
- });
513
-
514
- // Add specific version paths we know about
515
- const metainfoItsPaths = getTextDirs.map(
516
- (dir) => `${dir}/its/metainfo.its`
517
- );
518
-
519
- // Find first existing path
520
- return metainfoItsPaths.find((path) => existsSync(path));
521
- } catch (error) {
522
- console.warn("Error searching for metainfo.its:", error);
523
- return undefined;
524
- }
525
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "rootDir": "src",
4
- "outDir": "lib",
5
- "declaration": true,
6
- "target": "ESNext",
7
- "module": "ESNext",
8
- "moduleResolution": "bundler",
9
- "strict": true,
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "types": ["node"]
13
- },
14
- "include": ["src/**/*"]
15
- }