vite-plugin-lingo 0.0.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 (38) hide show
  1. package/README.md +266 -0
  2. package/dist/index.d.ts +8 -0
  3. package/dist/index.js +10 -0
  4. package/dist/plugin/gettext-parser.d.ts +44 -0
  5. package/dist/plugin/index.cjs +449 -0
  6. package/dist/plugin/index.cjs.map +1 -0
  7. package/dist/plugin/index.d.cts +84 -0
  8. package/dist/plugin/index.d.ts +84 -0
  9. package/dist/plugin/index.js +413 -0
  10. package/dist/plugin/index.js.map +1 -0
  11. package/dist/plugin/middleware.d.ts +10 -0
  12. package/dist/plugin/middleware.js +137 -0
  13. package/dist/plugin/po-parser.d.ts +25 -0
  14. package/dist/plugin/po-parser.js +147 -0
  15. package/dist/plugin/types.d.ts +67 -0
  16. package/dist/plugin/types.js +1 -0
  17. package/dist/ui/App.svelte +92 -0
  18. package/dist/ui/App.svelte.d.ts +3 -0
  19. package/dist/ui/app.css +64 -0
  20. package/dist/ui/components/LanguageList.svelte +166 -0
  21. package/dist/ui/components/LanguageList.svelte.d.ts +6 -0
  22. package/dist/ui/components/ProgressBar.svelte +47 -0
  23. package/dist/ui/components/ProgressBar.svelte.d.ts +9 -0
  24. package/dist/ui/components/SearchBar.svelte +54 -0
  25. package/dist/ui/components/SearchBar.svelte.d.ts +6 -0
  26. package/dist/ui/components/ThemeToggle.svelte +17 -0
  27. package/dist/ui/components/ThemeToggle.svelte.d.ts +18 -0
  28. package/dist/ui/components/TranslationEditor.svelte +418 -0
  29. package/dist/ui/components/TranslationEditor.svelte.d.ts +8 -0
  30. package/dist/ui/index.html +24 -0
  31. package/dist/ui/main.d.ts +3 -0
  32. package/dist/ui/main.js +7 -0
  33. package/dist/ui/stores/refresh-signal.svelte.d.ts +6 -0
  34. package/dist/ui/stores/refresh-signal.svelte.js +11 -0
  35. package/dist/ui-dist/assets/index-B5dZv0sy.css +1 -0
  36. package/dist/ui-dist/assets/index-DsX4xzGF.js +10 -0
  37. package/dist/ui-dist/index.html +25 -0
  38. package/package.json +118 -0
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/lib/plugin/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => lingoPlugin,
34
+ lingoPlugin: () => lingoPlugin
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+ var import_path3 = require("path");
38
+ var import_fs2 = require("fs");
39
+ var import_url = require("url");
40
+ var import_sirv = __toESM(require("sirv"), 1);
41
+
42
+ // src/lib/plugin/middleware.ts
43
+ var import_path2 = require("path");
44
+
45
+ // src/lib/plugin/po-parser.ts
46
+ var import_gettext_parser = require("gettext-parser");
47
+ var import_fs = require("fs");
48
+ var import_path = require("path");
49
+ function parsePoFile(filePath) {
50
+ if (!(0, import_fs.existsSync)(filePath)) {
51
+ throw new Error(`File not found: ${filePath}`);
52
+ }
53
+ const content = (0, import_fs.readFileSync)(filePath);
54
+ const parsed = import_gettext_parser.po.parse(content);
55
+ const translations = [];
56
+ for (const [context, messages] of Object.entries(parsed.translations)) {
57
+ for (const [msgid, data] of Object.entries(messages)) {
58
+ if (!msgid) continue;
59
+ const entry = data;
60
+ translations.push({
61
+ msgid,
62
+ msgstr: entry.msgstr?.[0] || "",
63
+ context: context || void 0,
64
+ comments: entry.comments,
65
+ fuzzy: entry.comments?.flag?.includes("fuzzy") || false
66
+ });
67
+ }
68
+ }
69
+ return translations;
70
+ }
71
+ function savePoFile(filePath, updates) {
72
+ if (!(0, import_fs.existsSync)(filePath)) {
73
+ throw new Error(`File not found: ${filePath}`);
74
+ }
75
+ const content = (0, import_fs.readFileSync)(filePath);
76
+ const parsed = import_gettext_parser.po.parse(content);
77
+ for (const update of updates) {
78
+ const context = update.context || "";
79
+ if (parsed.translations[context]?.[update.msgid]) {
80
+ parsed.translations[context][update.msgid].msgstr = [update.msgstr];
81
+ if (update.fuzzy !== void 0) {
82
+ const comments = parsed.translations[context][update.msgid].comments || {};
83
+ if (update.fuzzy) {
84
+ comments.flag = "fuzzy";
85
+ } else {
86
+ delete comments.flag;
87
+ }
88
+ parsed.translations[context][update.msgid].comments = comments;
89
+ }
90
+ }
91
+ }
92
+ const compiled = import_gettext_parser.po.compile(parsed);
93
+ (0, import_fs.writeFileSync)(filePath, compiled);
94
+ }
95
+ function updateTranslation(filePath, msgid, msgstr, context) {
96
+ savePoFile(filePath, [{ msgid, msgstr, context }]);
97
+ }
98
+ function findPoFiles(localesDir) {
99
+ if (!(0, import_fs.existsSync)(localesDir)) {
100
+ return [];
101
+ }
102
+ const files = (0, import_fs.readdirSync)(localesDir).filter((f) => f.endsWith(".po"));
103
+ return files.map((file) => {
104
+ const filePath = (0, import_path.join)(localesDir, file);
105
+ const code = (0, import_path.basename)(file, ".po");
106
+ const translations = parsePoFile(filePath);
107
+ const translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;
108
+ const fuzzy = translations.filter((t) => t.fuzzy).length;
109
+ return {
110
+ code,
111
+ name: getLanguageName(code),
112
+ path: filePath,
113
+ translations,
114
+ progress: {
115
+ total: translations.length,
116
+ translated,
117
+ fuzzy
118
+ }
119
+ };
120
+ });
121
+ }
122
+ function getLanguageStats(localesDir) {
123
+ const languages = findPoFiles(localesDir);
124
+ return languages.map((lang) => ({
125
+ code: lang.code,
126
+ name: lang.name,
127
+ total: lang.progress.total,
128
+ translated: lang.progress.translated,
129
+ fuzzy: lang.progress.fuzzy,
130
+ untranslated: lang.progress.total - lang.progress.translated - lang.progress.fuzzy,
131
+ progress: lang.progress.total > 0 ? Math.round(lang.progress.translated / lang.progress.total * 100) : 0
132
+ }));
133
+ }
134
+ function getLanguageName(code) {
135
+ const names = {
136
+ en: "English",
137
+ es: "Spanish",
138
+ fr: "French",
139
+ de: "German",
140
+ it: "Italian",
141
+ pt: "Portuguese",
142
+ "pt-BR": "Portuguese (Brazil)",
143
+ ja: "Japanese",
144
+ ko: "Korean",
145
+ zh: "Chinese",
146
+ "zh-CN": "Chinese (Simplified)",
147
+ "zh-TW": "Chinese (Traditional)",
148
+ ru: "Russian",
149
+ ar: "Arabic",
150
+ nl: "Dutch",
151
+ pl: "Polish",
152
+ sv: "Swedish",
153
+ da: "Danish",
154
+ fi: "Finnish",
155
+ no: "Norwegian",
156
+ tr: "Turkish",
157
+ cs: "Czech",
158
+ hu: "Hungarian",
159
+ ro: "Romanian",
160
+ uk: "Ukrainian",
161
+ vi: "Vietnamese",
162
+ th: "Thai",
163
+ id: "Indonesian",
164
+ ms: "Malay",
165
+ he: "Hebrew",
166
+ hi: "Hindi"
167
+ };
168
+ return names[code] || code.toUpperCase();
169
+ }
170
+
171
+ // src/lib/plugin/middleware.ts
172
+ async function parseBody(req) {
173
+ return new Promise((resolve2, reject) => {
174
+ let body = "";
175
+ req.on("data", (chunk) => body += chunk);
176
+ req.on("end", () => {
177
+ try {
178
+ resolve2(JSON.parse(body));
179
+ } catch {
180
+ reject(new Error("Invalid JSON"));
181
+ }
182
+ });
183
+ req.on("error", reject);
184
+ });
185
+ }
186
+ function sendJson(res, data, statusCode = 200) {
187
+ res.statusCode = statusCode;
188
+ res.setHeader("Content-Type", "application/json");
189
+ res.end(JSON.stringify(data));
190
+ }
191
+ function sendError(res, message, statusCode = 500) {
192
+ sendJson(res, { success: false, error: message }, statusCode);
193
+ }
194
+ function createApiMiddleware(options) {
195
+ const { localesDir } = options;
196
+ return async (req, res, next) => {
197
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
198
+ const path = url.pathname;
199
+ const method = req.method?.toUpperCase();
200
+ res.setHeader("Access-Control-Allow-Origin", "*");
201
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
202
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
203
+ if (method === "OPTIONS") {
204
+ res.statusCode = 204;
205
+ res.end();
206
+ return;
207
+ }
208
+ try {
209
+ if (path === "/languages" && method === "GET") {
210
+ const stats = getLanguageStats(localesDir);
211
+ sendJson(res, { success: true, data: stats });
212
+ return;
213
+ }
214
+ const translationsMatch = path.match(/^\/translations\/([^/]+)$/);
215
+ if (translationsMatch && method === "GET") {
216
+ const langCode = translationsMatch[1];
217
+ const filePath = (0, import_path2.join)(localesDir, `${langCode}.po`);
218
+ try {
219
+ const translations = parsePoFile(filePath);
220
+ sendJson(res, { success: true, data: translations });
221
+ } catch (error) {
222
+ sendError(res, `Language not found: ${langCode}`, 404);
223
+ }
224
+ return;
225
+ }
226
+ if (translationsMatch && method === "PUT") {
227
+ const langCode = translationsMatch[1];
228
+ const filePath = (0, import_path2.join)(localesDir, `${langCode}.po`);
229
+ try {
230
+ const body = await parseBody(req);
231
+ const updates = Array.isArray(body) ? body : [body];
232
+ savePoFile(filePath, updates);
233
+ sendJson(res, { success: true, message: "Translations updated" });
234
+ } catch (error) {
235
+ sendError(res, error instanceof Error ? error.message : "Failed to update", 400);
236
+ }
237
+ return;
238
+ }
239
+ const singleMatch = path.match(/^\/translation\/([^/]+)\/(.+)$/);
240
+ if (singleMatch && method === "PUT") {
241
+ const langCode = singleMatch[1];
242
+ const msgid = decodeURIComponent(singleMatch[2]);
243
+ const filePath = (0, import_path2.join)(localesDir, `${langCode}.po`);
244
+ try {
245
+ const body = await parseBody(req);
246
+ updateTranslation(filePath, msgid, body.msgstr, body.context);
247
+ sendJson(res, { success: true, message: "Translation updated" });
248
+ } catch (error) {
249
+ sendError(res, error instanceof Error ? error.message : "Failed to update", 400);
250
+ }
251
+ return;
252
+ }
253
+ if (path === "/search" && method === "GET") {
254
+ const query = url.searchParams.get("q")?.toLowerCase() || "";
255
+ const lang = url.searchParams.get("lang");
256
+ const languages = findPoFiles(localesDir);
257
+ const results = [];
258
+ for (const language of languages) {
259
+ if (lang && language.code !== lang) continue;
260
+ for (const t of language.translations) {
261
+ if (t.msgid.toLowerCase().includes(query) || t.msgstr.toLowerCase().includes(query)) {
262
+ results.push({
263
+ lang: language.code,
264
+ msgid: t.msgid,
265
+ msgstr: t.msgstr,
266
+ context: t.context
267
+ });
268
+ }
269
+ }
270
+ }
271
+ sendJson(res, { success: true, data: results });
272
+ return;
273
+ }
274
+ sendError(res, "Not found", 404);
275
+ } catch (error) {
276
+ console.error("[lingo] API error:", error);
277
+ sendError(res, error instanceof Error ? error.message : "Internal server error");
278
+ }
279
+ };
280
+ }
281
+
282
+ // src/lib/plugin/index.ts
283
+ var import_meta = {};
284
+ var __filename = typeof __dirname !== "undefined" ? "" : (0, import_url.fileURLToPath)(import_meta.url);
285
+ var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : (0, import_path3.dirname)(__filename);
286
+ function lingoPlugin(options = {}) {
287
+ const { route = "/_translations", localesDir = "./locales", production = false } = options;
288
+ let root;
289
+ let resolvedLocalesDir;
290
+ return {
291
+ name: "vite-plugin-lingo",
292
+ // Only apply in serve mode (unless production is enabled)
293
+ apply: production ? void 0 : "serve",
294
+ configResolved(config) {
295
+ root = config.root;
296
+ resolvedLocalesDir = (0, import_path3.resolve)(root, localesDir);
297
+ },
298
+ configureServer(server) {
299
+ const cleanRoute = route.replace(/\/$/, "");
300
+ let uiPath = (0, import_path3.resolve)(__dirnameComputed, "../ui-dist");
301
+ if (!(0, import_fs2.existsSync)(uiPath)) {
302
+ uiPath = (0, import_path3.resolve)(root, "dist/ui-dist");
303
+ }
304
+ console.log("[lingo] Looking for UI at:", uiPath);
305
+ console.log("[lingo] UI exists:", (0, import_fs2.existsSync)(uiPath));
306
+ if ((0, import_fs2.existsSync)(uiPath)) {
307
+ console.log("[lingo] Serving built UI from:", uiPath);
308
+ const serve = (0, import_sirv.default)(uiPath, {
309
+ dev: true,
310
+ single: true
311
+ // SPA mode
312
+ });
313
+ server.middlewares.use(cleanRoute, (req, res, next) => {
314
+ const reqWithOriginal = req;
315
+ if ((req.url === "/" || req.url === "") && reqWithOriginal.originalUrl && !reqWithOriginal.originalUrl.endsWith("/")) {
316
+ res.writeHead(302, { Location: cleanRoute + "/" });
317
+ res.end();
318
+ return;
319
+ }
320
+ if (req.url === "/favicon.ico") {
321
+ res.statusCode = 204;
322
+ res.end();
323
+ return;
324
+ }
325
+ if (req.url?.startsWith("/api")) {
326
+ return next();
327
+ }
328
+ serve(req, res, () => next());
329
+ });
330
+ } else {
331
+ server.middlewares.use(cleanRoute, (req, res, next) => {
332
+ if (req.url === "/favicon.ico") {
333
+ res.statusCode = 204;
334
+ res.end();
335
+ return;
336
+ }
337
+ if (req.url?.startsWith("/api")) {
338
+ return next();
339
+ }
340
+ res.setHeader("Content-Type", "text/html");
341
+ res.end(`
342
+ <!DOCTYPE html>
343
+ <html lang="en">
344
+ <head>
345
+ <meta charset="UTF-8">
346
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
347
+ <title>\u{1F30D} Lingo Translation Editor</title>
348
+ <style>
349
+ * { box-sizing: border-box; margin: 0; padding: 0; }
350
+ body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; min-height: 100vh; }
351
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
352
+ header { background: white; padding: 1rem 2rem; border-bottom: 1px solid #e0e0e0; margin-bottom: 2rem; }
353
+ h1 { font-size: 1.5rem; color: #333; }
354
+ .card { background: white; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
355
+ .language { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; border-bottom: 1px solid #eee; }
356
+ .language:last-child { border-bottom: none; }
357
+ .progress { background: #e0e0e0; border-radius: 4px; height: 8px; width: 100px; overflow: hidden; }
358
+ .progress-bar { background: #4caf50; height: 100%; transition: width 0.3s; }
359
+ .loading { text-align: center; padding: 2rem; color: #666; }
360
+ .error { color: #f44336; }
361
+ </style>
362
+ </head>
363
+ <body>
364
+ <header>
365
+ <h1>\u{1F30D} Lingo Translation Editor</h1>
366
+ </header>
367
+ <div class="container">
368
+ <div class="card">
369
+ <h2 style="margin-bottom: 1rem;">Languages</h2>
370
+ <div id="languages" class="loading">Loading...</div>
371
+ </div>
372
+ </div>
373
+ <script>
374
+ async function loadLanguages() {
375
+ try {
376
+ const res = await fetch('${cleanRoute}/api/languages');
377
+ const { data, error } = await res.json();
378
+
379
+ if (error) throw new Error(error);
380
+
381
+ const container = document.getElementById('languages');
382
+ if (!data || data.length === 0) {
383
+ container.innerHTML = '<p>No .po files found in the locales directory.</p>';
384
+ return;
385
+ }
386
+
387
+ container.innerHTML = data.map(lang => \`
388
+ <div class="language">
389
+ <div>
390
+ <strong>\${lang.name}</strong>
391
+ <span style="color: #666; margin-left: 0.5rem;">(\${lang.code})</span>
392
+ </div>
393
+ <div style="display: flex; align-items: center; gap: 1rem;">
394
+ <span>\${lang.translated}/\${lang.total} translated</span>
395
+ <div class="progress">
396
+ <div class="progress-bar" style="width: \${lang.progress}%"></div>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ \`).join('');
401
+ } catch (err) {
402
+ document.getElementById('languages').innerHTML =
403
+ '<p class="error">Error loading languages: ' + err.message + '</p>';
404
+ }
405
+ }
406
+
407
+ loadLanguages();
408
+ </script>
409
+ </body>
410
+ </html>
411
+ `);
412
+ });
413
+ }
414
+ server.middlewares.use(
415
+ `${cleanRoute}/api`,
416
+ createApiMiddleware({
417
+ localesDir: resolvedLocalesDir,
418
+ root
419
+ })
420
+ );
421
+ const poGlob = (0, import_path3.join)(resolvedLocalesDir, "**/*.po");
422
+ server.watcher.add(poGlob);
423
+ server.watcher.on("change", (path) => {
424
+ if (path.endsWith(".po")) {
425
+ server.ws.send({
426
+ type: "custom",
427
+ event: "lingo:po-updated",
428
+ data: { path }
429
+ });
430
+ console.log(`[lingo] .po file updated: ${path}`);
431
+ }
432
+ });
433
+ const port = server.config.server.port || 5173;
434
+ const protocol = server.config.server.https ? "https" : "http";
435
+ const host = server.config.server.host || "localhost";
436
+ const hostString = typeof host === "string" ? host : "localhost";
437
+ const originalPrintUrls = server.printUrls;
438
+ server.printUrls = () => {
439
+ originalPrintUrls?.();
440
+ console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mLingo:\x1B[0m ${protocol}://${hostString}:${port}${cleanRoute}`);
441
+ };
442
+ }
443
+ };
444
+ }
445
+ // Annotate the CommonJS export names for ESM import in node:
446
+ 0 && (module.exports = {
447
+ lingoPlugin
448
+ });
449
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/plugin/index.ts","../../src/lib/plugin/middleware.ts","../../src/lib/plugin/po-parser.ts"],"sourcesContent":["import type { Plugin, ViteDevServer } from 'vite';\nimport { resolve, join, dirname } from 'path';\nimport { existsSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport sirv from 'sirv';\nimport type { PluginOptions } from './types.js';\nimport { createApiMiddleware } from './middleware.js';\nimport { findPoFiles } from './po-parser.js';\n\n// Compute __dirname for ESM (CJS already has it)\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - import.meta.url works in ESM, undefined in CJS\nconst __filename = typeof __dirname !== 'undefined' ? '' : fileURLToPath(import.meta.url);\nconst __dirnameComputed = typeof __dirname !== 'undefined' ? __dirname : dirname(__filename);\n\n// Re-export types\nexport type { PluginOptions, Translation, Language, LanguageStats } from './types.js';\n\n/**\n * vite-plugin-lingo - Visual translation editor for .po files\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from 'vite';\n * import lingo from 'vite-plugin-lingo';\n *\n * export default defineConfig({\n * plugins: [\n * lingo({\n * route: '/_translations',\n * localesDir: './locales'\n * })\n * ]\n * });\n * ```\n */\nexport default function lingoPlugin(options: PluginOptions = {}): Plugin {\n\tconst { route = '/_translations', localesDir = './locales', production = false } = options;\n\n\tlet root: string;\n\tlet resolvedLocalesDir: string;\n\n\treturn {\n\t\tname: 'vite-plugin-lingo',\n\n\t\t// Only apply in serve mode (unless production is enabled)\n\t\tapply: production ? undefined : 'serve',\n\n\t\tconfigResolved(config) {\n\t\t\troot = config.root;\n\t\t\tresolvedLocalesDir = resolve(root, localesDir);\n\t\t},\n\n\t\tconfigureServer(server: ViteDevServer) {\n\t\t\t// Ensure the route doesn't have trailing slash\n\t\t\tconst cleanRoute = route.replace(/\\/$/, '');\n\n\t\t\t// Find the built UI assets\n\t\t\t// When running from source (dev): __dirnameComputed is src/lib/plugin, UI is at dist/ui-dist\n\t\t\t// When running from dist (published): __dirnameComputed is dist/plugin, UI is at ../ui-dist\n\t\t\tlet uiPath = resolve(__dirnameComputed, '../ui-dist');\n\t\t\t\n\t\t\t// If not found relative to __dirname, try from project root\n\t\t\tif (!existsSync(uiPath)) {\n\t\t\t\tuiPath = resolve(root, 'dist/ui-dist');\n\t\t\t}\n\t\t\t\n\t\t\tconsole.log('[lingo] Looking for UI at:', uiPath);\n\t\t\tconsole.log('[lingo] UI exists:', existsSync(uiPath));\n\n\t\t\t// Serve the editor UI if built assets exist\n\t\t\tif (existsSync(uiPath)) {\n\t\t\t\tconsole.log('[lingo] Serving built UI from:', uiPath);\n\t\t\t\tconst serve = sirv(uiPath, {\n\t\t\t\t\tdev: true,\n\t\t\t\t\tsingle: true // SPA mode\n\t\t\t\t});\n\n\t\t\t\tserver.middlewares.use(cleanRoute, (req, res, next) => {\n\t\t\t\t\t// Redirect to add trailing slash for base route (ensures relative paths work)\n\t\t\t\t\t// When mounted at /path, req.url is already stripped of the prefix\n\t\t\t\t\t// But the browser URL matters for relative path resolution\n\t\t\t\t\tconst reqWithOriginal = req as typeof req & { originalUrl?: string };\n\t\t\t\t\tif ((req.url === '/' || req.url === '') && \n\t\t\t\t\t reqWithOriginal.originalUrl && \n\t\t\t\t\t !reqWithOriginal.originalUrl.endsWith('/')) {\n\t\t\t\t\t\tres.writeHead(302, { Location: cleanRoute + '/' });\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle favicon.ico requests with an empty response to prevent 404\n\t\t\t\t\tif (req.url === '/favicon.ico') {\n\t\t\t\t\t\tres.statusCode = 204;\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip API routes (handled by separate middleware)\n\t\t\t\t\tif (req.url?.startsWith('/api')) {\n\t\t\t\t\t\treturn next();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Serve static files with sirv\n\t\t\t\t\t// req.url is already stripped of the cleanRoute prefix by Connect\n\t\t\t\t\tserve(req, res, () => next());\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Development mode: serve a simple placeholder\n\t\t\t\tserver.middlewares.use(cleanRoute, (req, res, next) => {\n\t\t\t\t\t// Handle favicon.ico requests with an empty response to prevent 404\n\t\t\t\t\tif (req.url === '/favicon.ico') {\n\t\t\t\t\t\tres.statusCode = 204;\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (req.url?.startsWith('/api')) {\n\t\t\t\t\t\treturn next();\n\t\t\t\t\t}\n\n\t\t\t\t\tres.setHeader('Content-Type', 'text/html');\n\t\t\t\t\tres.end(`\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>🌍 Lingo Translation Editor</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; min-height: 100vh; }\n .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }\n header { background: white; padding: 1rem 2rem; border-bottom: 1px solid #e0e0e0; margin-bottom: 2rem; }\n h1 { font-size: 1.5rem; color: #333; }\n .card { background: white; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n .language { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; border-bottom: 1px solid #eee; }\n .language:last-child { border-bottom: none; }\n .progress { background: #e0e0e0; border-radius: 4px; height: 8px; width: 100px; overflow: hidden; }\n .progress-bar { background: #4caf50; height: 100%; transition: width 0.3s; }\n .loading { text-align: center; padding: 2rem; color: #666; }\n .error { color: #f44336; }\n </style>\n</head>\n<body>\n <header>\n <h1>🌍 Lingo Translation Editor</h1>\n </header>\n <div class=\"container\">\n <div class=\"card\">\n <h2 style=\"margin-bottom: 1rem;\">Languages</h2>\n <div id=\"languages\" class=\"loading\">Loading...</div>\n </div>\n </div>\n <script>\n async function loadLanguages() {\n try {\n const res = await fetch('${cleanRoute}/api/languages');\n const { data, error } = await res.json();\n \n if (error) throw new Error(error);\n \n const container = document.getElementById('languages');\n if (!data || data.length === 0) {\n container.innerHTML = '<p>No .po files found in the locales directory.</p>';\n return;\n }\n \n container.innerHTML = data.map(lang => \\`\n <div class=\"language\">\n <div>\n <strong>\\${lang.name}</strong>\n <span style=\"color: #666; margin-left: 0.5rem;\">(\\${lang.code})</span>\n </div>\n <div style=\"display: flex; align-items: center; gap: 1rem;\">\n <span>\\${lang.translated}/\\${lang.total} translated</span>\n <div class=\"progress\">\n <div class=\"progress-bar\" style=\"width: \\${lang.progress}%\"></div>\n </div>\n </div>\n </div>\n \\`).join('');\n } catch (err) {\n document.getElementById('languages').innerHTML = \n '<p class=\"error\">Error loading languages: ' + err.message + '</p>';\n }\n }\n \n loadLanguages();\n </script>\n</body>\n</html>\n `);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// API endpoints\n\t\t\tserver.middlewares.use(\n\t\t\t\t`${cleanRoute}/api`,\n\t\t\t\tcreateApiMiddleware({\n\t\t\t\t\tlocalesDir: resolvedLocalesDir,\n\t\t\t\t\troot\n\t\t\t\t})\n\t\t\t);\n\n\t\t\t// Watch .po files for changes\n\t\t\tconst poGlob = join(resolvedLocalesDir, '**/*.po');\n\t\t\tserver.watcher.add(poGlob);\n\n\t\t\tserver.watcher.on('change', (path) => {\n\t\t\t\tif (path.endsWith('.po')) {\n\t\t\t\t\t// Notify connected clients via WebSocket\n\t\t\t\t\tserver.ws.send({\n\t\t\t\t\t\ttype: 'custom',\n\t\t\t\t\t\tevent: 'lingo:po-updated',\n\t\t\t\t\t\tdata: { path }\n\t\t\t\t\t});\n\n\t\t\t\t\tconsole.log(`[lingo] .po file updated: ${path}`);\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Log startup message\n\t\t\tconst port = server.config.server.port || 5173;\n\t\t\tconst protocol = server.config.server.https ? 'https' : 'http';\n\t\t\tconst host = server.config.server.host || 'localhost';\n\t\t\tconst hostString = typeof host === 'string' ? host : 'localhost';\n\n\t\t\t// Use server.printUrls hook for better timing\n\t\t\tconst originalPrintUrls = server.printUrls;\n\t\t\tserver.printUrls = () => {\n\t\t\t\toriginalPrintUrls?.();\n\t\t\t\tconsole.log(` \\x1b[32m➜\\x1b[0m \\x1b[1mLingo:\\x1b[0m ${protocol}://${hostString}:${port}${cleanRoute}`);\n\t\t\t};\n\t\t}\n\t};\n}\n\n// Named export for convenience\nexport { lingoPlugin };\n","import type { IncomingMessage, ServerResponse } from 'http';\nimport { join } from 'path';\nimport {\n\tfindPoFiles,\n\tparsePoFile,\n\tsavePoFile,\n\tgetLanguageStats,\n\tupdateTranslation\n} from './po-parser.js';\nimport type { Translation } from './types.js';\n\ninterface MiddlewareOptions {\n\tlocalesDir: string;\n\troot: string;\n}\n\n/**\n * Parse the request body as JSON\n */\nasync function parseBody<T>(req: IncomingMessage): Promise<T> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet body = '';\n\t\treq.on('data', (chunk) => (body += chunk));\n\t\treq.on('end', () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(body));\n\t\t\t} catch {\n\t\t\t\treject(new Error('Invalid JSON'));\n\t\t\t}\n\t\t});\n\t\treq.on('error', reject);\n\t});\n}\n\n/**\n * Send JSON response\n */\nfunction sendJson(res: ServerResponse, data: unknown, statusCode = 200): void {\n\tres.statusCode = statusCode;\n\tres.setHeader('Content-Type', 'application/json');\n\tres.end(JSON.stringify(data));\n}\n\n/**\n * Send error response\n */\nfunction sendError(res: ServerResponse, message: string, statusCode = 500): void {\n\tsendJson(res, { success: false, error: message }, statusCode);\n}\n\n/**\n * Create API middleware for handling translation operations\n */\nexport function createApiMiddleware(options: MiddlewareOptions) {\n\tconst { localesDir } = options;\n\n\treturn async (\n\t\treq: IncomingMessage,\n\t\tres: ServerResponse,\n\t\tnext: () => void\n\t): Promise<void> => {\n\t\tconst url = new URL(req.url || '/', `http://${req.headers.host}`);\n\t\tconst path = url.pathname;\n\t\tconst method = req.method?.toUpperCase();\n\n\t\t// Enable CORS for development\n\t\tres.setHeader('Access-Control-Allow-Origin', '*');\n\t\tres.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n\t\tres.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n\t\t// Handle preflight\n\t\tif (method === 'OPTIONS') {\n\t\t\tres.statusCode = 204;\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// GET /api/languages - List all languages with stats\n\t\t\tif (path === '/languages' && method === 'GET') {\n\t\t\t\tconst stats = getLanguageStats(localesDir);\n\t\t\t\tsendJson(res, { success: true, data: stats });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// GET /api/translations/:lang - Get all translations for a language\n\t\t\tconst translationsMatch = path.match(/^\\/translations\\/([^/]+)$/);\n\t\t\tif (translationsMatch && method === 'GET') {\n\t\t\t\tconst langCode = translationsMatch[1];\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst translations = parsePoFile(filePath);\n\t\t\t\t\tsendJson(res, { success: true, data: translations });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, `Language not found: ${langCode}`, 404);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// PUT /api/translations/:lang - Update translations for a language\n\t\t\tif (translationsMatch && method === 'PUT') {\n\t\t\t\tconst langCode = translationsMatch[1];\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await parseBody<Translation | Translation[]>(req);\n\t\t\t\t\tconst updates = Array.isArray(body) ? body : [body];\n\t\t\t\t\tsavePoFile(filePath, updates);\n\t\t\t\t\tsendJson(res, { success: true, message: 'Translations updated' });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, error instanceof Error ? error.message : 'Failed to update', 400);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// PUT /api/translation/:lang/:msgid - Update a single translation\n\t\t\tconst singleMatch = path.match(/^\\/translation\\/([^/]+)\\/(.+)$/);\n\t\t\tif (singleMatch && method === 'PUT') {\n\t\t\t\tconst langCode = singleMatch[1];\n\t\t\t\tconst msgid = decodeURIComponent(singleMatch[2]);\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await parseBody<{ msgstr: string; context?: string }>(req);\n\t\t\t\t\tupdateTranslation(filePath, msgid, body.msgstr, body.context);\n\t\t\t\t\tsendJson(res, { success: true, message: 'Translation updated' });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, error instanceof Error ? error.message : 'Failed to update', 400);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// GET /api/search - Search translations across languages\n\t\t\tif (path === '/search' && method === 'GET') {\n\t\t\t\tconst query = url.searchParams.get('q')?.toLowerCase() || '';\n\t\t\t\tconst lang = url.searchParams.get('lang');\n\n\t\t\t\tconst languages = findPoFiles(localesDir);\n\t\t\t\tconst results: Array<{\n\t\t\t\t\tlang: string;\n\t\t\t\t\tmsgid: string;\n\t\t\t\t\tmsgstr: string;\n\t\t\t\t\tcontext?: string;\n\t\t\t\t}> = [];\n\n\t\t\t\tfor (const language of languages) {\n\t\t\t\t\tif (lang && language.code !== lang) continue;\n\n\t\t\t\t\tfor (const t of language.translations) {\n\t\t\t\t\t\tif (t.msgid.toLowerCase().includes(query) || t.msgstr.toLowerCase().includes(query)) {\n\t\t\t\t\t\t\tresults.push({\n\t\t\t\t\t\t\t\tlang: language.code,\n\t\t\t\t\t\t\t\tmsgid: t.msgid,\n\t\t\t\t\t\t\t\tmsgstr: t.msgstr,\n\t\t\t\t\t\t\t\tcontext: t.context\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsendJson(res, { success: true, data: results });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Not found\n\t\t\tsendError(res, 'Not found', 404);\n\t\t} catch (error) {\n\t\t\tconsole.error('[lingo] API error:', error);\n\t\t\tsendError(res, error instanceof Error ? error.message : 'Internal server error');\n\t\t}\n\t};\n}\n","import { po } from 'gettext-parser';\nimport { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs';\nimport { join, basename } from 'path';\nimport type { Translation, Language, LanguageStats } from './types.js';\n\n/**\n * Parse a .po file and extract translations\n */\nexport function parsePoFile(filePath: string): Translation[] {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = readFileSync(filePath);\n\tconst parsed = po.parse(content);\n\n\tconst translations: Translation[] = [];\n\n\tfor (const [context, messages] of Object.entries(parsed.translations)) {\n\t\tfor (const [msgid, data] of Object.entries(messages as Record<string, unknown>)) {\n\t\t\tif (!msgid) continue; // Skip header entry\n\n\t\t\tconst entry = data as {\n\t\t\t\tmsgid: string;\n\t\t\t\tmsgstr?: string[];\n\t\t\t\tcomments?: {\n\t\t\t\t\treference?: string;\n\t\t\t\t\ttranslator?: string;\n\t\t\t\t\textracted?: string;\n\t\t\t\t\tflag?: string;\n\t\t\t\t};\n\t\t\t};\n\n\t\t\ttranslations.push({\n\t\t\t\tmsgid,\n\t\t\t\tmsgstr: entry.msgstr?.[0] || '',\n\t\t\t\tcontext: context || undefined,\n\t\t\t\tcomments: entry.comments,\n\t\t\t\tfuzzy: entry.comments?.flag?.includes('fuzzy') || false\n\t\t\t});\n\t\t}\n\t}\n\n\treturn translations;\n}\n\n/**\n * Save translations back to a .po file\n */\nexport function savePoFile(filePath: string, updates: Translation[]): void {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = readFileSync(filePath);\n\tconst parsed = po.parse(content);\n\n\tfor (const update of updates) {\n\t\tconst context = update.context || '';\n\n\t\tif (parsed.translations[context]?.[update.msgid]) {\n\t\t\tparsed.translations[context][update.msgid].msgstr = [update.msgstr];\n\n\t\t\t// Handle fuzzy flag\n\t\t\tif (update.fuzzy !== undefined) {\n\t\t\t\tconst comments = parsed.translations[context][update.msgid].comments || {};\n\t\t\t\tif (update.fuzzy) {\n\t\t\t\t\tcomments.flag = 'fuzzy';\n\t\t\t\t} else {\n\t\t\t\t\tdelete comments.flag;\n\t\t\t\t}\n\t\t\t\tparsed.translations[context][update.msgid].comments = comments;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst compiled = po.compile(parsed);\n\twriteFileSync(filePath, compiled);\n}\n\n/**\n * Update a single translation\n */\nexport function updateTranslation(\n\tfilePath: string,\n\tmsgid: string,\n\tmsgstr: string,\n\tcontext?: string\n): void {\n\tsavePoFile(filePath, [{ msgid, msgstr, context }]);\n}\n\n/**\n * Find all .po files in a directory\n */\nexport function findPoFiles(localesDir: string): Language[] {\n\tif (!existsSync(localesDir)) {\n\t\treturn [];\n\t}\n\n\tconst files = readdirSync(localesDir).filter((f) => f.endsWith('.po'));\n\n\treturn files.map((file) => {\n\t\tconst filePath = join(localesDir, file);\n\t\tconst code = basename(file, '.po');\n\t\tconst translations = parsePoFile(filePath);\n\n\t\tconst translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;\n\t\tconst fuzzy = translations.filter((t) => t.fuzzy).length;\n\n\t\treturn {\n\t\t\tcode,\n\t\t\tname: getLanguageName(code),\n\t\t\tpath: filePath,\n\t\t\ttranslations,\n\t\t\tprogress: {\n\t\t\t\ttotal: translations.length,\n\t\t\t\ttranslated,\n\t\t\t\tfuzzy\n\t\t\t}\n\t\t};\n\t});\n}\n\n/**\n * Get language statistics for all languages\n */\nexport function getLanguageStats(localesDir: string): LanguageStats[] {\n\tconst languages = findPoFiles(localesDir);\n\n\treturn languages.map((lang) => ({\n\t\tcode: lang.code,\n\t\tname: lang.name,\n\t\ttotal: lang.progress.total,\n\t\ttranslated: lang.progress.translated,\n\t\tfuzzy: lang.progress.fuzzy,\n\t\tuntranslated: lang.progress.total - lang.progress.translated - lang.progress.fuzzy,\n\t\tprogress:\n\t\t\tlang.progress.total > 0\n\t\t\t\t? Math.round((lang.progress.translated / lang.progress.total) * 100)\n\t\t\t\t: 0\n\t}));\n}\n\n/**\n * Get a human-readable language name from a locale code\n */\nexport function getLanguageName(code: string): string {\n\tconst names: Record<string, string> = {\n\t\ten: 'English',\n\t\tes: 'Spanish',\n\t\tfr: 'French',\n\t\tde: 'German',\n\t\tit: 'Italian',\n\t\tpt: 'Portuguese',\n\t\t'pt-BR': 'Portuguese (Brazil)',\n\t\tja: 'Japanese',\n\t\tko: 'Korean',\n\t\tzh: 'Chinese',\n\t\t'zh-CN': 'Chinese (Simplified)',\n\t\t'zh-TW': 'Chinese (Traditional)',\n\t\tru: 'Russian',\n\t\tar: 'Arabic',\n\t\tnl: 'Dutch',\n\t\tpl: 'Polish',\n\t\tsv: 'Swedish',\n\t\tda: 'Danish',\n\t\tfi: 'Finnish',\n\t\tno: 'Norwegian',\n\t\ttr: 'Turkish',\n\t\tcs: 'Czech',\n\t\thu: 'Hungarian',\n\t\tro: 'Romanian',\n\t\tuk: 'Ukrainian',\n\t\tvi: 'Vietnamese',\n\t\tth: 'Thai',\n\t\tid: 'Indonesian',\n\t\tms: 'Malay',\n\t\the: 'Hebrew',\n\t\thi: 'Hindi'\n\t};\n\treturn names[code] || code.toUpperCase();\n}\n"],"mappings":"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","names":["import_path","import_fs","import_path","resolve","sirv"]}
@@ -0,0 +1,84 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /**
4
+ * Plugin configuration options
5
+ */
6
+ interface PluginOptions {
7
+ /** Route where editor is served (default: '/_translations') */
8
+ route?: string;
9
+ /** Path to .po files directory */
10
+ localesDir?: string;
11
+ /** Enable in production mode (premium) */
12
+ production?: boolean;
13
+ /** License key for premium features */
14
+ licenseKey?: string;
15
+ /** AI configuration (premium) */
16
+ ai?: {
17
+ provider: 'openai' | 'anthropic' | 'google';
18
+ apiKey?: string;
19
+ };
20
+ }
21
+ /**
22
+ * Represents a single translation entry
23
+ */
24
+ interface Translation {
25
+ msgid: string;
26
+ msgstr: string;
27
+ context?: string;
28
+ comments?: {
29
+ reference?: string;
30
+ translator?: string;
31
+ extracted?: string;
32
+ flag?: string;
33
+ };
34
+ fuzzy?: boolean;
35
+ }
36
+ /**
37
+ * Represents a language with its translations
38
+ */
39
+ interface Language {
40
+ code: string;
41
+ name: string;
42
+ path: string;
43
+ translations: Translation[];
44
+ progress: {
45
+ total: number;
46
+ translated: number;
47
+ fuzzy: number;
48
+ };
49
+ }
50
+ /**
51
+ * Language stats for the overview
52
+ */
53
+ interface LanguageStats {
54
+ code: string;
55
+ name: string;
56
+ total: number;
57
+ translated: number;
58
+ fuzzy: number;
59
+ untranslated: number;
60
+ progress: number;
61
+ }
62
+
63
+ /**
64
+ * vite-plugin-lingo - Visual translation editor for .po files
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // vite.config.ts
69
+ * import { defineConfig } from 'vite';
70
+ * import lingo from 'vite-plugin-lingo';
71
+ *
72
+ * export default defineConfig({
73
+ * plugins: [
74
+ * lingo({
75
+ * route: '/_translations',
76
+ * localesDir: './locales'
77
+ * })
78
+ * ]
79
+ * });
80
+ * ```
81
+ */
82
+ declare function lingoPlugin(options?: PluginOptions): Plugin;
83
+
84
+ export { type Language, type LanguageStats, type PluginOptions, type Translation, lingoPlugin as default, lingoPlugin };
@@ -0,0 +1,84 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ /**
4
+ * Plugin configuration options
5
+ */
6
+ interface PluginOptions {
7
+ /** Route where editor is served (default: '/_translations') */
8
+ route?: string;
9
+ /** Path to .po files directory */
10
+ localesDir?: string;
11
+ /** Enable in production mode (premium) */
12
+ production?: boolean;
13
+ /** License key for premium features */
14
+ licenseKey?: string;
15
+ /** AI configuration (premium) */
16
+ ai?: {
17
+ provider: 'openai' | 'anthropic' | 'google';
18
+ apiKey?: string;
19
+ };
20
+ }
21
+ /**
22
+ * Represents a single translation entry
23
+ */
24
+ interface Translation {
25
+ msgid: string;
26
+ msgstr: string;
27
+ context?: string;
28
+ comments?: {
29
+ reference?: string;
30
+ translator?: string;
31
+ extracted?: string;
32
+ flag?: string;
33
+ };
34
+ fuzzy?: boolean;
35
+ }
36
+ /**
37
+ * Represents a language with its translations
38
+ */
39
+ interface Language {
40
+ code: string;
41
+ name: string;
42
+ path: string;
43
+ translations: Translation[];
44
+ progress: {
45
+ total: number;
46
+ translated: number;
47
+ fuzzy: number;
48
+ };
49
+ }
50
+ /**
51
+ * Language stats for the overview
52
+ */
53
+ interface LanguageStats {
54
+ code: string;
55
+ name: string;
56
+ total: number;
57
+ translated: number;
58
+ fuzzy: number;
59
+ untranslated: number;
60
+ progress: number;
61
+ }
62
+
63
+ /**
64
+ * vite-plugin-lingo - Visual translation editor for .po files
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // vite.config.ts
69
+ * import { defineConfig } from 'vite';
70
+ * import lingo from 'vite-plugin-lingo';
71
+ *
72
+ * export default defineConfig({
73
+ * plugins: [
74
+ * lingo({
75
+ * route: '/_translations',
76
+ * localesDir: './locales'
77
+ * })
78
+ * ]
79
+ * });
80
+ * ```
81
+ */
82
+ declare function lingoPlugin(options?: PluginOptions): Plugin;
83
+
84
+ export { type Language, type LanguageStats, type PluginOptions, type Translation, lingoPlugin as default, lingoPlugin };