@gjsify/vite-plugin-gettext 0.4.0 → 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/po2json.ts DELETED
@@ -1,281 +0,0 @@
1
- import { type Plugin } from "vite";
2
- import path from "node:path";
3
- import fs from "node:fs/promises";
4
- import * as gettextParser from "gettext-parser";
5
- import type { GettextPo2JsonPluginOptions } from "./types.js";
6
- import {
7
- checkDependencies,
8
- findAvailableLanguages,
9
- ensureDirectory,
10
- } from "./utils.js";
11
-
12
- /**
13
- * Simplifies the gettext-parser output to a clean key-value object
14
- * where the key is the original text and the value is the translation
15
- * @param translations The parsed PO file from gettext-parser
16
- * @returns A simplified object with just the translations
17
- */
18
- function simplifyTranslations(translations: any): Record<string, string> {
19
- const result: Record<string, string> = {};
20
-
21
- // Go through all translation contexts
22
- Object.keys(translations.translations).forEach((context) => {
23
- const contextTranslations = translations.translations[context];
24
-
25
- // Skip the header (empty msgid)
26
- Object.keys(contextTranslations).forEach((key) => {
27
- if (key === "") return;
28
-
29
- const translation = contextTranslations[key];
30
- // Get the original text (msgid)
31
- const original = translation.msgid;
32
- // Get the translated text (first item in msgstr array)
33
- const translated = translation.msgstr[0];
34
-
35
- // Only add the translation if it exists and is not empty
36
- if (translated && translated.trim() !== "") {
37
- result[original] = translated;
38
- }
39
- });
40
- });
41
-
42
- return result;
43
- }
44
-
45
- /**
46
- * Creates a dictionary of all original strings from all translations
47
- * For the default language, we need to gather all possible keys
48
- * @param jsonDirectory Directory with JSON files
49
- * @param allTranslations Collection of all translations
50
- * @param defaultLanguage The default language code
51
- * @param verbose Whether to log verbose messages
52
- * @param pluginName The name of the plugin
53
- * @param additionalTranslations Additional translations to include
54
- * @returns Object with original strings as both keys and values
55
- */
56
- async function createDefaultLanguageJson(
57
- jsonDirectory: string,
58
- allTranslations: Record<string, Record<string, string>>,
59
- defaultLanguage: string,
60
- verbose: boolean,
61
- pluginName: string,
62
- additionalTranslations: Record<string, string> = {}
63
- ): Promise<void> {
64
- // Create a set of all original strings from all translations
65
- const allOriginalStrings = new Set<string>();
66
-
67
- // Collect all original strings from all translations
68
- Object.values(allTranslations).forEach((translations) => {
69
- Object.keys(translations).forEach((key) => {
70
- allOriginalStrings.add(key);
71
- });
72
- });
73
-
74
- // Create the default language JSON with keys matching values
75
- const defaultLanguageJson: Record<string, string> = {};
76
- allOriginalStrings.forEach((str) => {
77
- defaultLanguageJson[str] = str;
78
- });
79
-
80
- // Process additional translations
81
- const finalTranslations = { ...defaultLanguageJson };
82
-
83
- // For each additional translation, try to find a translation or use the original
84
- Object.entries(additionalTranslations).forEach(([key, originalText]) => {
85
- // If there's a translation for the original text, use it
86
- if (defaultLanguageJson[originalText]) {
87
- finalTranslations[key] = defaultLanguageJson[originalText];
88
- } else {
89
- // Otherwise use the original text
90
- finalTranslations[key] = originalText;
91
- }
92
- });
93
-
94
- // Write the default language file with .default.json extension
95
- const defaultLangDefaultFile = path.join(
96
- jsonDirectory,
97
- `${defaultLanguage}.default.json`
98
- );
99
-
100
- if (verbose) {
101
- console.log(
102
- `[${pluginName}] Creating default language file: ${defaultLangDefaultFile}`
103
- );
104
- }
105
-
106
- await fs.writeFile(
107
- defaultLangDefaultFile,
108
- JSON.stringify(finalTranslations, null, 2)
109
- );
110
- }
111
-
112
- /**
113
- * Creates a Vite plugin that converts PO translation files to JSON format
114
- * The JSON files are placed in the specified output directory
115
- * @param options Configuration options for the plugin
116
- * @returns A Vite plugin that handles PO to JSON conversion
117
- */
118
- export function po2jsonPlugin(options: GettextPo2JsonPluginOptions): Plugin {
119
- const {
120
- poDirectory,
121
- jsonDirectory,
122
- defaultLanguage = "en",
123
- verbose = false,
124
- additionalTranslations = {},
125
- } = options;
126
-
127
- const pluginName = "vite-plugin-gettext-po2json";
128
-
129
- async function convertPoToJson() {
130
- try {
131
- // Check if PO directory exists
132
- try {
133
- await ensureDirectory(poDirectory);
134
- } catch {
135
- if (verbose) {
136
- console.log(
137
- `[${pluginName}] PO directory ${poDirectory} does not exist yet, skipping conversion`
138
- );
139
- }
140
- return;
141
- }
142
-
143
- // Find available languages
144
- const languages = await findAvailableLanguages(
145
- poDirectory,
146
- pluginName,
147
- verbose
148
- );
149
-
150
- if (languages.length === 0) {
151
- if (verbose) {
152
- console.log(`[${pluginName}] No translation files found`);
153
- }
154
- return;
155
- }
156
-
157
- // Create JSON directory
158
- await ensureDirectory(jsonDirectory);
159
-
160
- // Collection of all translations to create the default language file
161
- const allTranslations: Record<string, Record<string, string>> = {};
162
-
163
- // Skip the default language if it exists in the list
164
- const nonDefaultLanguages = languages.filter(
165
- (lang) => lang !== defaultLanguage
166
- );
167
-
168
- // Handle default language if it exists in the list
169
- if (languages.includes(defaultLanguage)) {
170
- const poFile = path.join(poDirectory, `${defaultLanguage}.po`);
171
- const jsonFile = path.join(jsonDirectory, `${defaultLanguage}.json`);
172
-
173
- if (verbose) {
174
- console.log(
175
- `[${pluginName}] Converting default language ${poFile} to ${jsonFile}`
176
- );
177
- }
178
-
179
- // Read and parse PO file
180
- const poContent = await fs.readFile(poFile);
181
- const translations = gettextParser.po.parse(poContent);
182
-
183
- // Convert the translations to a simple JSON object
184
- const simplifiedTranslations = simplifyTranslations(translations);
185
-
186
- // Process additional translations for default language
187
- const finalTranslations = { ...simplifiedTranslations };
188
-
189
- // For each additional translation, add it to the default language file
190
- Object.entries(additionalTranslations).forEach(
191
- ([key, originalText]) => {
192
- finalTranslations[key] = originalText;
193
- }
194
- );
195
-
196
- // Write JSON file for default language
197
- await fs.writeFile(
198
- jsonFile,
199
- JSON.stringify(finalTranslations, null, 2)
200
- );
201
- }
202
-
203
- // Add additional translations for all languages
204
- for (const lang of nonDefaultLanguages) {
205
- const poFile = path.join(poDirectory, `${lang}.po`);
206
- const jsonFile = path.join(jsonDirectory, `${lang}.json`);
207
-
208
- if (verbose) {
209
- console.log(`[${pluginName}] Converting ${poFile} to ${jsonFile}`);
210
- }
211
-
212
- // Read and parse PO file
213
- const poContent = await fs.readFile(poFile);
214
- const translations = gettextParser.po.parse(poContent);
215
-
216
- // Convert the translations to a simple JSON object
217
- const simplifiedTranslations = simplifyTranslations(translations);
218
-
219
- // Store translations for creating the default language file
220
- allTranslations[lang] = simplifiedTranslations;
221
-
222
- // Process additional translations
223
- const finalTranslations = { ...simplifiedTranslations };
224
-
225
- // For each additional translation, try to find a translation or use the original
226
- Object.entries(additionalTranslations).forEach(
227
- ([key, originalText]) => {
228
- // If there's a translation for the original text, use it
229
- if (simplifiedTranslations[originalText]) {
230
- finalTranslations[key] = simplifiedTranslations[originalText];
231
- } else {
232
- // Otherwise use the original text
233
- finalTranslations[key] = originalText;
234
- }
235
- }
236
- );
237
-
238
- // Write JSON file
239
- await fs.writeFile(
240
- jsonFile,
241
- JSON.stringify(finalTranslations, null, 2)
242
- );
243
- }
244
-
245
- // Create the default language file (with all original strings as both keys and values)
246
- await createDefaultLanguageJson(
247
- jsonDirectory,
248
- allTranslations,
249
- defaultLanguage,
250
- verbose,
251
- pluginName,
252
- additionalTranslations
253
- );
254
- } catch (error) {
255
- throw new Error(`Failed to convert PO files to JSON: ${error}`);
256
- }
257
- }
258
-
259
- return {
260
- name: pluginName,
261
-
262
- async buildStart() {
263
- await convertPoToJson();
264
- },
265
-
266
- configureServer(server) {
267
- server.watcher.add(poDirectory);
268
-
269
- server.watcher.on("change", async (file) => {
270
- if (file.endsWith(".po")) {
271
- if (verbose) {
272
- console.log(
273
- `[${pluginName}] PO file changed: ${file}, reconverting`
274
- );
275
- }
276
- await convertPoToJson();
277
- }
278
- });
279
- },
280
- };
281
- }
package/src/types.ts DELETED
@@ -1,138 +0,0 @@
1
- /**
2
- * Configuration options for the xgettext plugin
3
- * Used to extract translatable strings from source files
4
- */
5
- export interface XGettextPluginOptions {
6
- /** Glob patterns for source files to extract strings from */
7
- sources: string[];
8
- /** Output path for the POT template file */
9
- output: string;
10
- /** The gettext domain name, defaults to 'messages' */
11
- domain?: string;
12
- /** Keywords to look for when extracting strings, defaults to ['_', 'gettext', 'ngettext'] */
13
- keywords?: string[];
14
- /** Additional options to pass to xgettext command */
15
- xgettextOptions?: string[];
16
- /** Additional options to pass to msgcat when combining POT files */
17
- msgcatOptions?: string[];
18
- /** Enable verbose logging */
19
- verbose?: boolean;
20
- /** Automatically update PO files after POT changes */
21
- autoUpdatePo?: boolean;
22
- /** Version of the POT file, defaults to '1.0' */
23
- version?: string;
24
- /** Preset to use for extracting strings, defaults to 'glib' */
25
- preset?: "glib";
26
- /** URL for reporting bugs in the POT file */
27
- msgidBugsAddress?: string;
28
- /** Copyright holder to set in the POT file */
29
- copyrightHolder?: string;
30
- /** If true, do not include source reference locations in POT/PO files */
31
- noLocation?: boolean;
32
- /** If true, attempt to make output reproducible (stable timestamps/order) */
33
- deterministic?: boolean;
34
- /** When deterministic is true, use this SOURCE_DATE_EPOCH (seconds since epoch). Defaults to 0. */
35
- sourceDateEpoch?: number;
36
- /** Optionally force a fixed POT-Creation-Date header value (e.g. '1970-01-01 00:00+0000') */
37
- fixedCreationDate?: string;
38
- /** If true, preserve the existing POT-Creation-Date from the current POT file if present */
39
- preserveCreationDate?: boolean;
40
- /** If true, sort the output messages for stable diffs (passed to msgcat) */
41
- sortOutput?: boolean;
42
- /** Whether to disable text wrapping in PO/POT files, defaults to false */
43
- noWrap?: boolean;
44
- }
45
-
46
- /**
47
- * Configuration options for the gettext plugin
48
- * Used to compile PO files to binary MO format
49
- */
50
- export interface GettextPluginOptions {
51
- /** Directory containing PO translation files */
52
- poDirectory: string;
53
- /** Output directory for compiled MO files */
54
- moDirectory: string;
55
- /** Filename of the MO file, defaults to 'messages.mo' */
56
- filename?: string;
57
- /** Enable verbose logging */
58
- verbose?: boolean;
59
- }
60
-
61
- /**
62
- * Output format types for msgfmt
63
- */
64
- export type MsgfmtFormat =
65
- | "mo"
66
- | "java"
67
- | "java2"
68
- | "csharp"
69
- | "csharp-resources"
70
- | "tcl"
71
- | "desktop"
72
- | "xml"
73
- | "json"
74
- | "qt";
75
-
76
- /**
77
- * Configuration options for the msgfmt plugin
78
- * Used to compile PO files to various formats including binary MO
79
- */
80
- export interface MsgfmtPluginOptions {
81
- /** Directory containing PO translation files */
82
- poDirectory: string;
83
- /** Output directory for compiled files */
84
- outputDirectory: string;
85
- /** The gettext domain name, defaults to 'messages' */
86
- domain?: string;
87
- /** Output filename, defaults to 'messages.mo' */
88
- filename?: string;
89
- /** Output format, defaults to 'mo' */
90
- format?: MsgfmtFormat;
91
- /** Path to template file, required for XML format */
92
- templateFile?: string;
93
- /** Enable verbose logging */
94
- verbose?: boolean;
95
- /** Additional options to pass to msgfmt command */
96
- msgfmtOptions?: string[];
97
- /** Whether to use the standard locale structure (locale/LANG/LC_MESSAGES/domain.mo) */
98
- useLocaleStructure?: boolean;
99
- /** Whether to remove XML comments from output files, defaults to true */
100
- removeComments?: boolean;
101
- }
102
-
103
- export interface PluginOptions {
104
- pluginName: string;
105
- verbose?: boolean;
106
- }
107
-
108
- /**
109
- * Options for the PO to JSON conversion plugin
110
- */
111
- export interface GettextPo2JsonPluginOptions {
112
- /**
113
- * Directory containing PO files
114
- */
115
- poDirectory: string;
116
-
117
- /**
118
- * Directory where JSON files will be saved
119
- */
120
- jsonDirectory: string;
121
-
122
- /**
123
- * Default language code (default: 'en')
124
- */
125
- defaultLanguage?: string;
126
-
127
- /**
128
- * Enable verbose logging
129
- */
130
- verbose?: boolean;
131
-
132
- /**
133
- * Additional translations to include in all language files
134
- * Keys are identifiers and values are the English text
135
- * The English text will be translated for non-default languages if translations exist
136
- */
137
- additionalTranslations?: Record<string, string>;
138
- }
package/src/utils.ts DELETED
@@ -1,119 +0,0 @@
1
- import { execa } from "execa";
2
- import path from "node:path";
3
- import fs from "node:fs/promises";
4
-
5
- /**
6
- * Checks if a gettext utility is installed and available
7
- * @param command The command to check (msgfmt, xgettext, etc.)
8
- * @param pluginName Name of the plugin for logging
9
- * @param verbose Enable verbose logging
10
- * @throws Error if the command is not found
11
- */
12
- export async function checkDependencies(
13
- command: string,
14
- pluginName: string,
15
- verbose: boolean
16
- ) {
17
- try {
18
- await execa(command, ["--version"]);
19
- if (verbose) {
20
- console.log(`[${pluginName}] Found ${command}`);
21
- }
22
- } catch (error) {
23
- throw new Error(
24
- `${command} not found. Please install gettext:\n` +
25
- " Ubuntu/Debian: sudo apt-get install gettext\n" +
26
- " Fedora: sudo dnf install gettext\n" +
27
- " Arch: sudo pacman -S gettext\n" +
28
- " macOS: brew install gettext"
29
- );
30
- }
31
- }
32
-
33
- /**
34
- * Scans the PO directory to find available language translations
35
- * @param poDirectory Directory containing PO files
36
- * @param pluginName Name of the plugin for logging
37
- * @param verbose Enable verbose logging
38
- * @returns Array of language codes found (e.g. ['de', 'fr', 'es'])
39
- */
40
- export async function findAvailableLanguages(
41
- poDirectory: string,
42
- pluginName: string,
43
- verbose: boolean
44
- ): Promise<string[]> {
45
- try {
46
- const files = await fs.readdir(poDirectory);
47
- const languages = files
48
- .filter((file) => file.endsWith(".po"))
49
- .map((file) => path.basename(file, ".po"));
50
-
51
- if (verbose) {
52
- console.log(`[${pluginName}] Found languages: ${languages.join(", ")}`);
53
- }
54
-
55
- return languages;
56
- } catch (error) {
57
- if (verbose) {
58
- console.log(`[${pluginName}] No PO directory found at ${poDirectory}`);
59
- }
60
- return [];
61
- }
62
- }
63
-
64
- /**
65
- * Generates a LINGUAS file containing the list of available languages
66
- * @param languages List of language codes
67
- * @param poDirectory Directory where the LINGUAS file should be created
68
- * @param verbose Enable verbose logging
69
- */
70
- export async function generateLinguasFile(
71
- languages: string[],
72
- poDirectory: string,
73
- verbose = false
74
- ) {
75
- const linguasPath = path.join(poDirectory, "LINGUAS");
76
- const content = languages.join("\n");
77
-
78
- try {
79
- await fs.writeFile(linguasPath, content);
80
- if (verbose) {
81
- console.log(
82
- `Generated LINGUAS file with languages: ${languages.join(", ")}`
83
- );
84
- }
85
- } catch (error) {
86
- console.error("Error writing LINGUAS file:", error);
87
- }
88
- }
89
-
90
- /**
91
- * Creates directory structure recursively
92
- * @param directory Directory path to create
93
- */
94
- export async function ensureDirectory(directory: string): Promise<void> {
95
- await fs.mkdir(directory, { recursive: true });
96
- }
97
-
98
- /**
99
- * Processes a filename with potential .in suffix
100
- * @param filePath Original file path or filename
101
- * @returns Object with processed filename and extension
102
- */
103
- export function processFilename(filePath: string): {
104
- filename: string;
105
- extension: string;
106
- } {
107
- // Extract just the filename if a path is provided
108
- const filename = path.basename(filePath);
109
- let extension = path.extname(filename).toLowerCase();
110
- let processedFilename = filename;
111
-
112
- // Handle .in extension
113
- if (filename.endsWith(".in")) {
114
- processedFilename = filename.substring(0, filename.length - 3);
115
- extension = path.extname(processedFilename).toLowerCase();
116
- }
117
-
118
- return { filename: processedFilename, extension };
119
- }