@gjsify/vite-plugin-gettext 0.4.28 → 0.4.30
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/lib/gettext.d.ts +2 -2
- package/lib/gettext.js +11 -11
- package/lib/index.d.ts +6 -6
- package/lib/index.js +5 -5
- package/lib/msgfmt.d.ts +2 -2
- package/lib/msgfmt.js +41 -45
- package/lib/po2json.d.ts +2 -2
- package/lib/po2json.js +10 -10
- package/lib/types.d.ts +2 -2
- package/lib/utils.js +17 -19
- package/lib/xgettext.d.ts +2 -2
- package/lib/xgettext.js +103 -111
- package/package.json +1 -1
package/lib/gettext.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Plugin } from
|
|
2
|
-
import type { GettextPluginOptions } from
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
import type { GettextPluginOptions } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a Vite plugin that compiles PO translation files to binary MO format
|
|
5
5
|
* The MO files are placed in the standard gettext directory structure:
|
package/lib/gettext.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { execa } from
|
|
2
|
-
import path from
|
|
3
|
-
import { checkDependencies, findAvailableLanguages, generateLinguasFile, ensureDirectory
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { checkDependencies, findAvailableLanguages, generateLinguasFile, ensureDirectory } from './utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Creates a Vite plugin that compiles PO translation files to binary MO format
|
|
6
6
|
* The MO files are placed in the standard gettext directory structure:
|
|
@@ -9,8 +9,8 @@ import { checkDependencies, findAvailableLanguages, generateLinguasFile, ensureD
|
|
|
9
9
|
* @returns A Vite plugin that handles PO compilation
|
|
10
10
|
*/
|
|
11
11
|
export function gettextPlugin(options) {
|
|
12
|
-
const { poDirectory, moDirectory, filename =
|
|
13
|
-
const pluginName =
|
|
12
|
+
const { poDirectory, moDirectory, filename = 'messages.mo', verbose = false } = options;
|
|
13
|
+
const pluginName = 'vite-plugin-gettext';
|
|
14
14
|
async function compileMoFiles() {
|
|
15
15
|
try {
|
|
16
16
|
// Check if PO directory exists
|
|
@@ -34,16 +34,16 @@ export function gettextPlugin(options) {
|
|
|
34
34
|
// Generate LINGUAS file
|
|
35
35
|
await generateLinguasFile(languages, poDirectory, verbose);
|
|
36
36
|
// Create MO directory
|
|
37
|
-
await ensureDirectory(path.join(moDirectory,
|
|
37
|
+
await ensureDirectory(path.join(moDirectory, 'locale'));
|
|
38
38
|
for (const lang of languages) {
|
|
39
39
|
const poFile = path.join(poDirectory, `${lang}.po`);
|
|
40
|
-
const moPath = path.join(moDirectory,
|
|
40
|
+
const moPath = path.join(moDirectory, 'locale', lang, 'LC_MESSAGES');
|
|
41
41
|
const moFile = path.join(moPath, filename);
|
|
42
42
|
await ensureDirectory(moPath);
|
|
43
43
|
if (verbose) {
|
|
44
44
|
console.log(`[${pluginName}] Compiling ${poFile} to ${moFile}`);
|
|
45
45
|
}
|
|
46
|
-
await execa(
|
|
46
|
+
await execa('msgfmt', ['--output-file=' + moFile, poFile]);
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
catch (error) {
|
|
@@ -53,13 +53,13 @@ export function gettextPlugin(options) {
|
|
|
53
53
|
return {
|
|
54
54
|
name: pluginName,
|
|
55
55
|
async buildStart() {
|
|
56
|
-
await checkDependencies(
|
|
56
|
+
await checkDependencies('msgfmt', pluginName, verbose);
|
|
57
57
|
await compileMoFiles();
|
|
58
58
|
},
|
|
59
59
|
configureServer(server) {
|
|
60
60
|
server.watcher.add(poDirectory);
|
|
61
|
-
server.watcher.on(
|
|
62
|
-
if (file.endsWith(
|
|
61
|
+
server.watcher.on('change', async (file) => {
|
|
62
|
+
if (file.endsWith('.po')) {
|
|
63
63
|
if (verbose) {
|
|
64
64
|
console.log(`[${pluginName}] PO file changed: ${file}, recompiling`);
|
|
65
65
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { gettextPlugin } from
|
|
2
|
-
export { msgfmtPlugin } from
|
|
3
|
-
export { xgettextPlugin } from
|
|
4
|
-
export { po2jsonPlugin } from
|
|
5
|
-
export type { GettextPluginOptions, MsgfmtPluginOptions, MsgfmtFormat, XGettextPluginOptions
|
|
6
|
-
export * from
|
|
1
|
+
export { gettextPlugin } from './gettext.js';
|
|
2
|
+
export { msgfmtPlugin } from './msgfmt.js';
|
|
3
|
+
export { xgettextPlugin } from './xgettext.js';
|
|
4
|
+
export { po2jsonPlugin } from './po2json.js';
|
|
5
|
+
export type { GettextPluginOptions, MsgfmtPluginOptions, MsgfmtFormat, XGettextPluginOptions } from './types.js';
|
|
6
|
+
export * from './utils.js';
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { gettextPlugin } from
|
|
2
|
-
export { msgfmtPlugin } from
|
|
3
|
-
export { xgettextPlugin } from
|
|
4
|
-
export { po2jsonPlugin } from
|
|
5
|
-
export * from
|
|
1
|
+
export { gettextPlugin } from './gettext.js';
|
|
2
|
+
export { msgfmtPlugin } from './msgfmt.js';
|
|
3
|
+
export { xgettextPlugin } from './xgettext.js';
|
|
4
|
+
export { po2jsonPlugin } from './po2json.js';
|
|
5
|
+
export * from './utils.js';
|
package/lib/msgfmt.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Plugin } from
|
|
2
|
-
import type { MsgfmtPluginOptions } from
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
import type { MsgfmtPluginOptions } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a Vite plugin that compiles PO translation files to various formats
|
|
5
5
|
* Supports metainfo files with special processing
|
package/lib/msgfmt.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { execa } from
|
|
2
|
-
import path from
|
|
3
|
-
import fs from
|
|
4
|
-
import { checkDependencies, findAvailableLanguages, ensureDirectory
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { checkDependencies, findAvailableLanguages, ensureDirectory } from './utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Remove XML comments from a file content
|
|
7
7
|
* @param content The XML content as string
|
|
@@ -31,27 +31,27 @@ function buildMsgfmtArgs(baseArgs, options) {
|
|
|
31
31
|
*/
|
|
32
32
|
function getOutputExtension(format) {
|
|
33
33
|
switch (format) {
|
|
34
|
-
case
|
|
35
|
-
return
|
|
36
|
-
case
|
|
37
|
-
case
|
|
38
|
-
return
|
|
39
|
-
case
|
|
40
|
-
return
|
|
41
|
-
case
|
|
42
|
-
return
|
|
43
|
-
case
|
|
44
|
-
return
|
|
45
|
-
case
|
|
46
|
-
return
|
|
47
|
-
case
|
|
48
|
-
return
|
|
49
|
-
case
|
|
50
|
-
return
|
|
51
|
-
case
|
|
52
|
-
return
|
|
34
|
+
case 'mo':
|
|
35
|
+
return '.mo';
|
|
36
|
+
case 'java':
|
|
37
|
+
case 'java2':
|
|
38
|
+
return '.class';
|
|
39
|
+
case 'csharp':
|
|
40
|
+
return '.dll';
|
|
41
|
+
case 'csharp-resources':
|
|
42
|
+
return '.resources.dll';
|
|
43
|
+
case 'tcl':
|
|
44
|
+
return '.msg';
|
|
45
|
+
case 'desktop':
|
|
46
|
+
return '.desktop';
|
|
47
|
+
case 'xml':
|
|
48
|
+
return '.xml';
|
|
49
|
+
case 'json':
|
|
50
|
+
return '.json';
|
|
51
|
+
case 'qt':
|
|
52
|
+
return '.qm';
|
|
53
53
|
default:
|
|
54
|
-
return
|
|
54
|
+
return '.mo';
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
@@ -61,8 +61,8 @@ function getOutputExtension(format) {
|
|
|
61
61
|
* @returns A Vite plugin that handles PO compilation
|
|
62
62
|
*/
|
|
63
63
|
export function msgfmtPlugin(options) {
|
|
64
|
-
const { poDirectory, outputDirectory, domain =
|
|
65
|
-
const pluginName =
|
|
64
|
+
const { poDirectory, outputDirectory, domain = 'messages', format = 'mo', templateFile, verbose = false, msgfmtOptions = [], useLocaleStructure = true, removeComments = true, } = options;
|
|
65
|
+
const pluginName = 'vite-plugin-msgfmt';
|
|
66
66
|
async function compilePoFiles() {
|
|
67
67
|
try {
|
|
68
68
|
// Check if PO directory exists
|
|
@@ -78,7 +78,7 @@ export function msgfmtPlugin(options) {
|
|
|
78
78
|
// Create output directory
|
|
79
79
|
await ensureDirectory(outputDirectory);
|
|
80
80
|
// For XML format, we can use the bulk mode if a template is provided
|
|
81
|
-
if (format ===
|
|
81
|
+
if (format === 'xml' && templateFile) {
|
|
82
82
|
// Use bulk mode for XML format
|
|
83
83
|
const outputFile = path.join(outputDirectory, options.filename || `${domain}${getOutputExtension(format)}`);
|
|
84
84
|
if (verbose) {
|
|
@@ -86,17 +86,17 @@ export function msgfmtPlugin(options) {
|
|
|
86
86
|
}
|
|
87
87
|
// Build arguments for bulk mode
|
|
88
88
|
const baseArgs = [
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
'--output-file=' + outputFile,
|
|
90
|
+
'--xml',
|
|
91
|
+
'--template=' + templateFile,
|
|
92
|
+
'-d',
|
|
93
93
|
poDirectory,
|
|
94
94
|
];
|
|
95
95
|
const args = buildMsgfmtArgs(baseArgs, { msgfmtOptions });
|
|
96
96
|
if (verbose) {
|
|
97
|
-
console.log(`[${pluginName}] Running msgfmt with: ${args.join(
|
|
97
|
+
console.log(`[${pluginName}] Running msgfmt with: ${args.join(' ')}`);
|
|
98
98
|
}
|
|
99
|
-
await execa(
|
|
99
|
+
await execa('msgfmt', args);
|
|
100
100
|
// Remove comments from XML output if requested
|
|
101
101
|
if (removeComments !== false) {
|
|
102
102
|
try {
|
|
@@ -128,9 +128,9 @@ export function msgfmtPlugin(options) {
|
|
|
128
128
|
const poFile = path.join(poDirectory, `${lang}.po`);
|
|
129
129
|
let outputPath;
|
|
130
130
|
let outputFile;
|
|
131
|
-
if (useLocaleStructure && format ===
|
|
131
|
+
if (useLocaleStructure && format === 'mo') {
|
|
132
132
|
// Use standard gettext locale structure
|
|
133
|
-
outputPath = path.join(outputDirectory,
|
|
133
|
+
outputPath = path.join(outputDirectory, 'locale', lang, 'LC_MESSAGES');
|
|
134
134
|
outputFile = path.join(outputPath, options.filename || `${domain}${getOutputExtension(format)}`);
|
|
135
135
|
}
|
|
136
136
|
else {
|
|
@@ -144,16 +144,12 @@ export function msgfmtPlugin(options) {
|
|
|
144
144
|
console.log(`[${pluginName}] Compiling ${poFile} to ${outputFile}`);
|
|
145
145
|
}
|
|
146
146
|
// Build arguments for individual processing
|
|
147
|
-
const baseArgs = [
|
|
148
|
-
"--output-file=" + outputFile,
|
|
149
|
-
`--${format}`,
|
|
150
|
-
poFile
|
|
151
|
-
];
|
|
147
|
+
const baseArgs = ['--output-file=' + outputFile, `--${format}`, poFile];
|
|
152
148
|
const args = buildMsgfmtArgs(baseArgs, { msgfmtOptions });
|
|
153
149
|
if (verbose) {
|
|
154
|
-
console.log(`[${pluginName}] Running msgfmt with: ${args.join(
|
|
150
|
+
console.log(`[${pluginName}] Running msgfmt with: ${args.join(' ')}`);
|
|
155
151
|
}
|
|
156
|
-
await execa(
|
|
152
|
+
await execa('msgfmt', args);
|
|
157
153
|
}
|
|
158
154
|
}
|
|
159
155
|
}
|
|
@@ -164,13 +160,13 @@ export function msgfmtPlugin(options) {
|
|
|
164
160
|
return {
|
|
165
161
|
name: pluginName,
|
|
166
162
|
async buildStart() {
|
|
167
|
-
await checkDependencies(
|
|
163
|
+
await checkDependencies('msgfmt', pluginName, verbose);
|
|
168
164
|
await compilePoFiles();
|
|
169
165
|
},
|
|
170
166
|
configureServer(server) {
|
|
171
167
|
server.watcher.add(poDirectory);
|
|
172
|
-
server.watcher.on(
|
|
173
|
-
if (file.endsWith(
|
|
168
|
+
server.watcher.on('change', async (file) => {
|
|
169
|
+
if (file.endsWith('.po')) {
|
|
174
170
|
if (verbose) {
|
|
175
171
|
console.log(`[${pluginName}] PO file changed: ${file}, recompiling`);
|
|
176
172
|
}
|
package/lib/po2json.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Plugin } from
|
|
2
|
-
import type { GettextPo2JsonPluginOptions } from
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
import type { GettextPo2JsonPluginOptions } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a Vite plugin that converts PO translation files to JSON format
|
|
5
5
|
* The JSON files are placed in the specified output directory
|
package/lib/po2json.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from
|
|
2
|
-
import fs from
|
|
3
|
-
import * as gettextParser from
|
|
4
|
-
import { findAvailableLanguages, ensureDirectory
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import * as gettextParser from 'gettext-parser';
|
|
4
|
+
import { findAvailableLanguages, ensureDirectory } from './utils.js';
|
|
5
5
|
/**
|
|
6
6
|
* Simplifies the gettext-parser output to a clean key-value object
|
|
7
7
|
* where the key is the original text and the value is the translation
|
|
@@ -15,7 +15,7 @@ function simplifyTranslations(translations) {
|
|
|
15
15
|
const contextTranslations = translations.translations[context];
|
|
16
16
|
// Skip the header (empty msgid)
|
|
17
17
|
Object.keys(contextTranslations).forEach((key) => {
|
|
18
|
-
if (key ===
|
|
18
|
+
if (key === '')
|
|
19
19
|
return;
|
|
20
20
|
const translation = contextTranslations[key];
|
|
21
21
|
// Get the original text (msgid)
|
|
@@ -23,7 +23,7 @@ function simplifyTranslations(translations) {
|
|
|
23
23
|
// Get the translated text (first item in msgstr array)
|
|
24
24
|
const translated = translation.msgstr[0];
|
|
25
25
|
// Only add the translation if it exists and is not empty
|
|
26
|
-
if (translated && translated.trim() !==
|
|
26
|
+
if (translated && translated.trim() !== '') {
|
|
27
27
|
result[original] = translated;
|
|
28
28
|
}
|
|
29
29
|
});
|
|
@@ -82,8 +82,8 @@ async function createDefaultLanguageJson(jsonDirectory, allTranslations, default
|
|
|
82
82
|
* @returns A Vite plugin that handles PO to JSON conversion
|
|
83
83
|
*/
|
|
84
84
|
export function po2jsonPlugin(options) {
|
|
85
|
-
const { poDirectory, jsonDirectory, defaultLanguage =
|
|
86
|
-
const pluginName =
|
|
85
|
+
const { poDirectory, jsonDirectory, defaultLanguage = 'en', verbose = false, additionalTranslations = {}, } = options;
|
|
86
|
+
const pluginName = 'vite-plugin-gettext-po2json';
|
|
87
87
|
async function convertPoToJson() {
|
|
88
88
|
try {
|
|
89
89
|
// Check if PO directory exists
|
|
@@ -175,8 +175,8 @@ export function po2jsonPlugin(options) {
|
|
|
175
175
|
},
|
|
176
176
|
configureServer(server) {
|
|
177
177
|
server.watcher.add(poDirectory);
|
|
178
|
-
server.watcher.on(
|
|
179
|
-
if (file.endsWith(
|
|
178
|
+
server.watcher.on('change', async (file) => {
|
|
179
|
+
if (file.endsWith('.po')) {
|
|
180
180
|
if (verbose) {
|
|
181
181
|
console.log(`[${pluginName}] PO file changed: ${file}, reconverting`);
|
|
182
182
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface XGettextPluginOptions {
|
|
|
22
22
|
/** Version of the POT file, defaults to '1.0' */
|
|
23
23
|
version?: string;
|
|
24
24
|
/** Preset to use for extracting strings, defaults to 'glib' */
|
|
25
|
-
preset?:
|
|
25
|
+
preset?: 'glib';
|
|
26
26
|
/** URL for reporting bugs in the POT file */
|
|
27
27
|
msgidBugsAddress?: string;
|
|
28
28
|
/** Copyright holder to set in the POT file */
|
|
@@ -59,7 +59,7 @@ export interface GettextPluginOptions {
|
|
|
59
59
|
/**
|
|
60
60
|
* Output format types for msgfmt
|
|
61
61
|
*/
|
|
62
|
-
export type MsgfmtFormat =
|
|
62
|
+
export type MsgfmtFormat = 'mo' | 'java' | 'java2' | 'csharp' | 'csharp-resources' | 'tcl' | 'desktop' | 'xml' | 'json' | 'qt';
|
|
63
63
|
/**
|
|
64
64
|
* Configuration options for the msgfmt plugin
|
|
65
65
|
* Used to compile PO files to various formats including binary MO
|
package/lib/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { execa } from
|
|
2
|
-
import path from
|
|
3
|
-
import fs from
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
4
|
/**
|
|
5
5
|
* Checks if a gettext utility is installed and available
|
|
6
6
|
* @param command The command to check (msgfmt, xgettext, etc.)
|
|
@@ -10,17 +10,17 @@ import fs from "node:fs/promises";
|
|
|
10
10
|
*/
|
|
11
11
|
export async function checkDependencies(command, pluginName, verbose) {
|
|
12
12
|
try {
|
|
13
|
-
await execa(command, [
|
|
13
|
+
await execa(command, ['--version']);
|
|
14
14
|
if (verbose) {
|
|
15
15
|
console.log(`[${pluginName}] Found ${command}`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
catch (
|
|
18
|
+
catch (_error) {
|
|
19
19
|
throw new Error(`${command} not found. Please install gettext:\n` +
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
' Ubuntu/Debian: sudo apt-get install gettext\n' +
|
|
21
|
+
' Fedora: sudo dnf install gettext\n' +
|
|
22
|
+
' Arch: sudo pacman -S gettext\n' +
|
|
23
|
+
' macOS: brew install gettext');
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
@@ -33,15 +33,13 @@ export async function checkDependencies(command, pluginName, verbose) {
|
|
|
33
33
|
export async function findAvailableLanguages(poDirectory, pluginName, verbose) {
|
|
34
34
|
try {
|
|
35
35
|
const files = await fs.readdir(poDirectory);
|
|
36
|
-
const languages = files
|
|
37
|
-
.filter((file) => file.endsWith(".po"))
|
|
38
|
-
.map((file) => path.basename(file, ".po"));
|
|
36
|
+
const languages = files.filter((file) => file.endsWith('.po')).map((file) => path.basename(file, '.po'));
|
|
39
37
|
if (verbose) {
|
|
40
|
-
console.log(`[${pluginName}] Found languages: ${languages.join(
|
|
38
|
+
console.log(`[${pluginName}] Found languages: ${languages.join(', ')}`);
|
|
41
39
|
}
|
|
42
40
|
return languages;
|
|
43
41
|
}
|
|
44
|
-
catch (
|
|
42
|
+
catch (_error) {
|
|
45
43
|
if (verbose) {
|
|
46
44
|
console.log(`[${pluginName}] No PO directory found at ${poDirectory}`);
|
|
47
45
|
}
|
|
@@ -55,16 +53,16 @@ export async function findAvailableLanguages(poDirectory, pluginName, verbose) {
|
|
|
55
53
|
* @param verbose Enable verbose logging
|
|
56
54
|
*/
|
|
57
55
|
export async function generateLinguasFile(languages, poDirectory, verbose = false) {
|
|
58
|
-
const linguasPath = path.join(poDirectory,
|
|
59
|
-
const content = languages.join(
|
|
56
|
+
const linguasPath = path.join(poDirectory, 'LINGUAS');
|
|
57
|
+
const content = languages.join('\n');
|
|
60
58
|
try {
|
|
61
59
|
await fs.writeFile(linguasPath, content);
|
|
62
60
|
if (verbose) {
|
|
63
|
-
console.log(`Generated LINGUAS file with languages: ${languages.join(
|
|
61
|
+
console.log(`Generated LINGUAS file with languages: ${languages.join(', ')}`);
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
catch (error) {
|
|
67
|
-
console.error(
|
|
65
|
+
console.error('Error writing LINGUAS file:', error);
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
/**
|
|
@@ -85,7 +83,7 @@ export function processFilename(filePath) {
|
|
|
85
83
|
let extension = path.extname(filename).toLowerCase();
|
|
86
84
|
let processedFilename = filename;
|
|
87
85
|
// Handle .in extension
|
|
88
|
-
if (filename.endsWith(
|
|
86
|
+
if (filename.endsWith('.in')) {
|
|
89
87
|
processedFilename = filename.substring(0, filename.length - 3);
|
|
90
88
|
extension = path.extname(processedFilename).toLowerCase();
|
|
91
89
|
}
|
package/lib/xgettext.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Plugin } from
|
|
2
|
-
import type { XGettextPluginOptions } from
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
import type { XGettextPluginOptions } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Creates a Vite plugin that extracts translatable strings from source files
|
|
5
5
|
* Uses GNU xgettext to generate a POT template file that can be used as basis for translations
|
package/lib/xgettext.js
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
import { execa } from
|
|
2
|
-
import path from
|
|
3
|
-
import fs from
|
|
4
|
-
import { existsSync } from
|
|
5
|
-
import glob from
|
|
6
|
-
import { checkDependencies, ensureDirectory, processFilename
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import glob from 'fast-glob';
|
|
6
|
+
import { checkDependencies, ensureDirectory, processFilename } from './utils.js';
|
|
7
7
|
// Add GLib preset constants
|
|
8
8
|
// From https://github.com/mesonbuild/meson/blob/467da051c859ba3112803b035e317bddadd756ef/mesonbuild/modules/i18n.py
|
|
9
9
|
const GLIB_PRESET_ARGS = [
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
'--from-code=UTF-8',
|
|
11
|
+
'--add-comments',
|
|
12
12
|
// https://developer.gnome.org/glib/stable/glib-I18N.html
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
13
|
+
'--keyword=_',
|
|
14
|
+
'--keyword=N_',
|
|
15
|
+
'--keyword=C_:1c,2',
|
|
16
|
+
'--keyword=NC_:1c,2',
|
|
17
|
+
'--keyword=g_dcgettext:2',
|
|
18
|
+
'--keyword=g_dngettext:2,3',
|
|
19
|
+
'--keyword=g_dpgettext2:2c,3',
|
|
20
|
+
'--flag=N_:1:pass-c-format',
|
|
21
|
+
'--flag=C_:2:pass-c-format',
|
|
22
|
+
'--flag=NC_:2:pass-c-format',
|
|
23
|
+
'--flag=g_dngettext:2:pass-c-format',
|
|
24
|
+
'--flag=g_strdup_printf:1:c-format',
|
|
25
|
+
'--flag=g_string_printf:2:c-format',
|
|
26
|
+
'--flag=g_string_append_printf:2:c-format',
|
|
27
|
+
'--flag=g_error_new:3:c-format',
|
|
28
|
+
'--flag=g_set_error:4:c-format',
|
|
29
|
+
'--flag=g_markup_printf_escaped:1:c-format',
|
|
30
|
+
'--flag=g_log:3:c-format',
|
|
31
|
+
'--flag=g_print:1:c-format',
|
|
32
|
+
'--flag=g_printerr:1:c-format',
|
|
33
|
+
'--flag=g_printf:1:c-format',
|
|
34
|
+
'--flag=g_fprintf:2:c-format',
|
|
35
|
+
'--flag=g_sprintf:2:c-format',
|
|
36
|
+
'--flag=g_snprintf:3:c-format',
|
|
37
37
|
];
|
|
38
38
|
/**
|
|
39
39
|
* Build command arguments with common options
|
|
@@ -45,17 +45,17 @@ function buildCommandArgs(baseArgs, options) {
|
|
|
45
45
|
const args = [...baseArgs];
|
|
46
46
|
// Check if additional options already contain the flags to avoid duplicates
|
|
47
47
|
const additionalOptions = options.additionalOptions || [];
|
|
48
|
-
const hasNoLocation = additionalOptions.includes(
|
|
49
|
-
const hasNoWrap = additionalOptions.includes(
|
|
50
|
-
const hasSortOutput = additionalOptions.includes(
|
|
48
|
+
const hasNoLocation = additionalOptions.includes('--no-location');
|
|
49
|
+
const hasNoWrap = additionalOptions.includes('--no-wrap');
|
|
50
|
+
const hasSortOutput = additionalOptions.includes('--sort-output');
|
|
51
51
|
if (options.noLocation && !hasNoLocation) {
|
|
52
|
-
args.push(
|
|
52
|
+
args.push('--no-location');
|
|
53
53
|
}
|
|
54
54
|
if (options.noWrap && !hasNoWrap) {
|
|
55
|
-
args.push(
|
|
55
|
+
args.push('--no-wrap');
|
|
56
56
|
}
|
|
57
57
|
if (options.sortOutput && !hasSortOutput) {
|
|
58
|
-
args.push(
|
|
58
|
+
args.push('--sort-output');
|
|
59
59
|
}
|
|
60
60
|
if (additionalOptions.length > 0) {
|
|
61
61
|
args.push(...additionalOptions);
|
|
@@ -69,17 +69,17 @@ function buildCommandArgs(baseArgs, options) {
|
|
|
69
69
|
* @returns A Vite plugin that handles string extraction
|
|
70
70
|
*/
|
|
71
71
|
export function xgettextPlugin(options) {
|
|
72
|
-
const pluginName =
|
|
72
|
+
const pluginName = 'vite-plugin-xgettext';
|
|
73
73
|
return {
|
|
74
74
|
name: pluginName,
|
|
75
75
|
async buildStart() {
|
|
76
|
-
await checkDependencies(
|
|
76
|
+
await checkDependencies('xgettext', pluginName, options.verbose ?? false);
|
|
77
77
|
const files = await glob(options.sources);
|
|
78
78
|
await extractStrings(files, options, pluginName);
|
|
79
79
|
},
|
|
80
80
|
configureServer(server) {
|
|
81
81
|
server.watcher.add(options.sources);
|
|
82
|
-
server.watcher.on(
|
|
82
|
+
server.watcher.on('change', async (file) => {
|
|
83
83
|
if (options.sources.some((pattern) => file.match(pattern))) {
|
|
84
84
|
if (options.verbose) {
|
|
85
85
|
console.log(`[${pluginName}] Source file changed: ${file}, re-running extraction`);
|
|
@@ -106,7 +106,7 @@ async function generatePotfiles(files, outputDir, pluginName, verbose = false) {
|
|
|
106
106
|
const potFiles = [];
|
|
107
107
|
for (const [group, groupFiles] of fileGroups) {
|
|
108
108
|
const potfilePath = path.join(outputDir, `${group}.POTFILES`);
|
|
109
|
-
const content = groupFiles.join(
|
|
109
|
+
const content = groupFiles.join('\n');
|
|
110
110
|
try {
|
|
111
111
|
await fs.writeFile(potfilePath, content);
|
|
112
112
|
potFiles.push(potfilePath);
|
|
@@ -124,27 +124,27 @@ function getFileGroup(fullFilename) {
|
|
|
124
124
|
// Process filename to handle .in extension
|
|
125
125
|
const { filename, extension } = processFilename(fullFilename);
|
|
126
126
|
// Special handling for metainfo.xml files
|
|
127
|
-
if (filename.endsWith(
|
|
128
|
-
return
|
|
127
|
+
if (filename.endsWith('.metainfo.xml') || filename.endsWith('.appdata.xml')) {
|
|
128
|
+
return 'metainfo';
|
|
129
129
|
}
|
|
130
130
|
switch (extension) {
|
|
131
|
-
case
|
|
132
|
-
case
|
|
133
|
-
case
|
|
134
|
-
return
|
|
135
|
-
case
|
|
136
|
-
case
|
|
137
|
-
return
|
|
138
|
-
case
|
|
139
|
-
return
|
|
140
|
-
case
|
|
141
|
-
return
|
|
131
|
+
case '.ts':
|
|
132
|
+
case '.js':
|
|
133
|
+
case '.tsx':
|
|
134
|
+
return 'js';
|
|
135
|
+
case '.ui':
|
|
136
|
+
case '.xml':
|
|
137
|
+
return 'ui';
|
|
138
|
+
case '.blp':
|
|
139
|
+
return 'blp';
|
|
140
|
+
case '.desktop':
|
|
141
|
+
return 'desktop';
|
|
142
142
|
default:
|
|
143
|
-
return
|
|
143
|
+
return 'other';
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
async function extractStrings(files, options, pluginName) {
|
|
147
|
-
const { output, domain =
|
|
147
|
+
const { output, domain = 'messages', keywords = [], preset, verbose = false } = options;
|
|
148
148
|
const noWrap = options.noWrap || false;
|
|
149
149
|
try {
|
|
150
150
|
const outputDir = path.dirname(output);
|
|
@@ -152,7 +152,7 @@ async function extractStrings(files, options, pluginName) {
|
|
|
152
152
|
// Read existing POT-Creation-Date from previous POT if present (for preservation)
|
|
153
153
|
let prevPotCreationDate;
|
|
154
154
|
try {
|
|
155
|
-
const existingPot = await fs.readFile(output,
|
|
155
|
+
const existingPot = await fs.readFile(output, 'utf-8');
|
|
156
156
|
const m = existingPot.match(/"POT-Creation-Date:\s*([^\n]+)\\n"/);
|
|
157
157
|
if (m && m[1]) {
|
|
158
158
|
prevPotCreationDate = m[1];
|
|
@@ -169,71 +169,69 @@ async function extractStrings(files, options, pluginName) {
|
|
|
169
169
|
// Create temporary POT files for each group
|
|
170
170
|
const tempPotFiles = [];
|
|
171
171
|
for (const potFile of potFiles) {
|
|
172
|
-
const group = path.basename(potFile).split(
|
|
172
|
+
const group = path.basename(potFile).split('.')[0];
|
|
173
173
|
const tempOutput = path.join(outputDir, `temp_${group}.pot`);
|
|
174
174
|
// Build base arguments
|
|
175
175
|
const baseArgs = [
|
|
176
|
-
|
|
177
|
-
options.version ?
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
'--package-name=' + domain,
|
|
177
|
+
options.version ? '--package-version=' + options.version : '',
|
|
178
|
+
'--output=' + tempOutput,
|
|
179
|
+
'--files-from=' + potFile,
|
|
180
|
+
'--from-code=UTF-8',
|
|
181
|
+
'--add-comments',
|
|
182
182
|
].filter(Boolean);
|
|
183
183
|
// Add bug report address if specified
|
|
184
184
|
if (options.msgidBugsAddress) {
|
|
185
|
-
baseArgs.push(
|
|
185
|
+
baseArgs.push('--msgid-bugs-address=' + options.msgidBugsAddress);
|
|
186
186
|
}
|
|
187
187
|
// Add copyright holder if specified
|
|
188
188
|
if (options.copyrightHolder) {
|
|
189
|
-
baseArgs.push(
|
|
189
|
+
baseArgs.push('--copyright-holder=' + options.copyrightHolder);
|
|
190
190
|
}
|
|
191
191
|
// Add language-specific settings
|
|
192
192
|
switch (group) {
|
|
193
|
-
case
|
|
194
|
-
case
|
|
195
|
-
baseArgs.push(
|
|
193
|
+
case 'js':
|
|
194
|
+
case 'blp':
|
|
195
|
+
baseArgs.push('--language=JavaScript');
|
|
196
196
|
baseArgs.push(...keywords.map((k) => `--keyword=${k}`));
|
|
197
|
-
if (preset ===
|
|
197
|
+
if (preset === 'glib') {
|
|
198
198
|
baseArgs.push(...GLIB_PRESET_ARGS);
|
|
199
199
|
}
|
|
200
200
|
break;
|
|
201
|
-
case
|
|
202
|
-
baseArgs.push(
|
|
201
|
+
case 'ui':
|
|
202
|
+
baseArgs.push('--language=Glade');
|
|
203
203
|
break;
|
|
204
|
-
case
|
|
204
|
+
case 'metainfo':
|
|
205
205
|
// Find the first existing metainfo.its file
|
|
206
206
|
const metainfoItsPath = await findMetainfoItsPath();
|
|
207
207
|
if (!metainfoItsPath) {
|
|
208
|
-
console.warn(
|
|
208
|
+
console.warn('Warning: Could not find metainfo.its in any of the expected locations');
|
|
209
209
|
// Continue without the ITS file
|
|
210
210
|
}
|
|
211
211
|
else {
|
|
212
212
|
baseArgs.push(`--its=${metainfoItsPath}`);
|
|
213
213
|
}
|
|
214
214
|
break;
|
|
215
|
-
case
|
|
216
|
-
baseArgs.push(
|
|
215
|
+
case 'desktop':
|
|
216
|
+
baseArgs.push('--language=Desktop');
|
|
217
217
|
break;
|
|
218
218
|
}
|
|
219
219
|
// Build final arguments with options handling
|
|
220
220
|
const args = buildCommandArgs(baseArgs, {
|
|
221
221
|
noLocation: options.noLocation,
|
|
222
222
|
noWrap,
|
|
223
|
-
additionalOptions: options.xgettextOptions
|
|
223
|
+
additionalOptions: options.xgettextOptions,
|
|
224
224
|
});
|
|
225
225
|
if (verbose) {
|
|
226
|
-
console.log(`[${pluginName}] Running xgettext for ${group}:`, args.join(
|
|
226
|
+
console.log(`[${pluginName}] Running xgettext for ${group}:`, args.join(' '));
|
|
227
227
|
}
|
|
228
228
|
// Enforce deterministic timestamps if requested
|
|
229
229
|
const env = { ...process.env };
|
|
230
230
|
if (options.deterministic) {
|
|
231
|
-
const epoch = typeof options.sourceDateEpoch ===
|
|
232
|
-
? options.sourceDateEpoch
|
|
233
|
-
: 0;
|
|
231
|
+
const epoch = typeof options.sourceDateEpoch === 'number' ? options.sourceDateEpoch : 0;
|
|
234
232
|
env.SOURCE_DATE_EPOCH = String(epoch);
|
|
235
233
|
}
|
|
236
|
-
await execa(
|
|
234
|
+
await execa('xgettext', args, { env });
|
|
237
235
|
// Check if file exists before adding to tempPotFiles
|
|
238
236
|
try {
|
|
239
237
|
await fs.access(tempOutput);
|
|
@@ -242,27 +240,25 @@ async function extractStrings(files, options, pluginName) {
|
|
|
242
240
|
console.log(`[${pluginName}] Successfully created temporary POT file: ${tempOutput}`);
|
|
243
241
|
}
|
|
244
242
|
}
|
|
245
|
-
catch (
|
|
243
|
+
catch (_error) {
|
|
246
244
|
console.warn(`[${pluginName}] Failed to create temporary POT file: ${tempOutput}`);
|
|
247
245
|
}
|
|
248
246
|
}
|
|
249
247
|
// Combine all temporary POT files using msgcat
|
|
250
248
|
if (tempPotFiles.length > 0) {
|
|
251
|
-
const baseMsgcatArgs = [
|
|
249
|
+
const baseMsgcatArgs = ['--use-first', '-o', output, ...tempPotFiles];
|
|
252
250
|
const msgcatArgs = buildCommandArgs(baseMsgcatArgs, {
|
|
253
251
|
noLocation: options.noLocation,
|
|
254
252
|
sortOutput: options.sortOutput,
|
|
255
253
|
noWrap,
|
|
256
|
-
additionalOptions: options.msgcatOptions
|
|
254
|
+
additionalOptions: options.msgcatOptions,
|
|
257
255
|
});
|
|
258
256
|
const env = { ...process.env };
|
|
259
257
|
if (options.deterministic) {
|
|
260
|
-
const epoch = typeof options.sourceDateEpoch ===
|
|
261
|
-
? options.sourceDateEpoch
|
|
262
|
-
: 0;
|
|
258
|
+
const epoch = typeof options.sourceDateEpoch === 'number' ? options.sourceDateEpoch : 0;
|
|
263
259
|
env.SOURCE_DATE_EPOCH = String(epoch);
|
|
264
260
|
}
|
|
265
|
-
await execa(
|
|
261
|
+
await execa('msgcat', msgcatArgs, { env });
|
|
266
262
|
// Clean up temporary files
|
|
267
263
|
for (const tempFile of tempPotFiles) {
|
|
268
264
|
await fs.unlink(tempFile);
|
|
@@ -287,10 +283,10 @@ async function extractStrings(files, options, pluginName) {
|
|
|
287
283
|
}
|
|
288
284
|
}
|
|
289
285
|
if (!normalizedDate && options.deterministic) {
|
|
290
|
-
normalizedDate = formatSourceDateEpoch(typeof options.sourceDateEpoch ===
|
|
286
|
+
normalizedDate = formatSourceDateEpoch(typeof options.sourceDateEpoch === 'number' ? options.sourceDateEpoch : 0);
|
|
291
287
|
}
|
|
292
288
|
if (normalizedDate) {
|
|
293
|
-
const content = await fs.readFile(output,
|
|
289
|
+
const content = await fs.readFile(output, 'utf-8');
|
|
294
290
|
const replaced = content.replace(/^"POT-Creation-Date: .*\\n"$/m, `"POT-Creation-Date: ${normalizedDate}\\n"`);
|
|
295
291
|
if (replaced !== content) {
|
|
296
292
|
await fs.writeFile(output, replaced);
|
|
@@ -314,33 +310,29 @@ async function extractStrings(files, options, pluginName) {
|
|
|
314
310
|
}
|
|
315
311
|
async function updatePoFiles(potFile, pluginName, verbose, options) {
|
|
316
312
|
try {
|
|
317
|
-
const linguasPath = path.join(path.dirname(potFile),
|
|
318
|
-
const languages = (await fs.readFile(linguasPath,
|
|
319
|
-
.split("\n")
|
|
320
|
-
.filter(Boolean);
|
|
313
|
+
const linguasPath = path.join(path.dirname(potFile), 'LINGUAS');
|
|
314
|
+
const languages = (await fs.readFile(linguasPath, 'utf-8')).split('\n').filter(Boolean);
|
|
321
315
|
for (const lang of languages) {
|
|
322
316
|
const poFile = path.join(path.dirname(potFile), `${lang}.po`);
|
|
323
317
|
if (verbose) {
|
|
324
318
|
console.log(`[${pluginName}] Updating ${poFile}`);
|
|
325
319
|
}
|
|
326
|
-
const baseMsgmergeArgs = [
|
|
320
|
+
const baseMsgmergeArgs = ['--update', '--backup=none', poFile, potFile];
|
|
327
321
|
const args = buildCommandArgs(baseMsgmergeArgs, {
|
|
328
322
|
noLocation: options.noLocation,
|
|
329
|
-
noWrap: options.noWrap
|
|
323
|
+
noWrap: options.noWrap,
|
|
330
324
|
});
|
|
331
325
|
const env = { ...process.env };
|
|
332
326
|
if (options.deterministic) {
|
|
333
|
-
const epoch = typeof options.sourceDateEpoch ===
|
|
334
|
-
? options.sourceDateEpoch
|
|
335
|
-
: 0;
|
|
327
|
+
const epoch = typeof options.sourceDateEpoch === 'number' ? options.sourceDateEpoch : 0;
|
|
336
328
|
env.SOURCE_DATE_EPOCH = String(epoch);
|
|
337
329
|
}
|
|
338
|
-
await execa(
|
|
330
|
+
await execa('msgmerge', args, { env });
|
|
339
331
|
// Post-process with msgcat to unwrap existing wrapped lines
|
|
340
332
|
if (options.noWrap) {
|
|
341
|
-
const tempFile = poFile +
|
|
342
|
-
const msgcatArgs = [
|
|
343
|
-
await execa(
|
|
333
|
+
const tempFile = poFile + '.tmp';
|
|
334
|
+
const msgcatArgs = ['--width=0', '--no-wrap', '-o', tempFile, poFile];
|
|
335
|
+
await execa('msgcat', msgcatArgs, { env });
|
|
344
336
|
await fs.rename(tempFile, poFile);
|
|
345
337
|
if (verbose) {
|
|
346
338
|
console.log(`[${pluginName}] Unwrapped lines in ${poFile}`);
|
|
@@ -358,7 +350,7 @@ async function updatePoFiles(potFile, pluginName, verbose, options) {
|
|
|
358
350
|
*/
|
|
359
351
|
function formatSourceDateEpoch(epochSeconds) {
|
|
360
352
|
const date = new Date(epochSeconds * 1000);
|
|
361
|
-
const pad = (n) => String(n).padStart(2,
|
|
353
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
362
354
|
const year = date.getUTCFullYear();
|
|
363
355
|
const month = pad(date.getUTCMonth() + 1);
|
|
364
356
|
const day = pad(date.getUTCDate());
|
|
@@ -372,18 +364,18 @@ function formatSourceDateEpoch(epochSeconds) {
|
|
|
372
364
|
*/
|
|
373
365
|
async function findMetainfoItsPath() {
|
|
374
366
|
// Default path
|
|
375
|
-
const defaultPath =
|
|
367
|
+
const defaultPath = '/usr/share/gettext/its/metainfo.its';
|
|
376
368
|
// Check default path first
|
|
377
369
|
if (existsSync(defaultPath)) {
|
|
378
370
|
return defaultPath;
|
|
379
371
|
}
|
|
380
372
|
try {
|
|
381
373
|
// Use glob to find all potential gettext version directories
|
|
382
|
-
const getTextDirs = await glob(
|
|
374
|
+
const getTextDirs = await glob('/usr/share/gettext-*');
|
|
383
375
|
// Sort by version (newest first) if possible
|
|
384
376
|
getTextDirs.sort((a, b) => {
|
|
385
|
-
const versionA = a.replace(
|
|
386
|
-
const versionB = b.replace(
|
|
377
|
+
const versionA = a.replace('/usr/share/gettext-', '');
|
|
378
|
+
const versionB = b.replace('/usr/share/gettext-', '');
|
|
387
379
|
return versionB.localeCompare(versionA);
|
|
388
380
|
});
|
|
389
381
|
// Add specific version paths we know about
|
|
@@ -392,7 +384,7 @@ async function findMetainfoItsPath() {
|
|
|
392
384
|
return metainfoItsPaths.find((path) => existsSync(path));
|
|
393
385
|
}
|
|
394
386
|
catch (error) {
|
|
395
|
-
console.warn(
|
|
387
|
+
console.warn('Error searching for metainfo.its:', error);
|
|
396
388
|
return undefined;
|
|
397
389
|
}
|
|
398
390
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/vite-plugin-gettext",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"description": "Gettext PO/MO/JSON pipeline for Vite / Rollup / Rolldown — extract via xgettext, compile via msgfmt, JSON for browser targets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|