vite-plugin-lingo 0.1.0 → 0.1.2
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/README.md +8 -0
- package/dist/plugin/index.cjs +78 -53
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +5 -3
- package/dist/plugin/index.d.ts +5 -3
- package/dist/plugin/index.js +87 -60
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/middleware.d.ts +2 -0
- package/dist/plugin/middleware.js +6 -6
- package/dist/plugin/po-parser.d.ts +5 -5
- package/dist/plugin/po-parser.js +16 -15
- package/dist/plugin/types.d.ts +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -98,6 +98,14 @@ lingo({
|
|
|
98
98
|
// For other frameworks, './locales' at project root is typical
|
|
99
99
|
localesDir: './locales',
|
|
100
100
|
|
|
101
|
+
// Restart the dev server when a .po file is updated (default: false)
|
|
102
|
+
// Useful if another plugin (like wuchale) stops reacting to changes
|
|
103
|
+
restartOnPoChange: false,
|
|
104
|
+
|
|
105
|
+
// Trigger a full page reload when a .po file is updated (default: true)
|
|
106
|
+
// Ensured UI stays in sync with backend translation files
|
|
107
|
+
reloadOnPoChange: true,
|
|
108
|
+
|
|
101
109
|
// Enable in production (default: false)
|
|
102
110
|
// ⚠️ Only enable with proper authentication!
|
|
103
111
|
production: false,
|
package/dist/plugin/index.cjs
CHANGED
|
@@ -34,6 +34,12 @@ __export(index_exports, {
|
|
|
34
34
|
lingoPlugin: () => lingoPlugin
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
39
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
40
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
41
|
+
|
|
42
|
+
// src/lib/plugin/index.ts
|
|
37
43
|
var import_path3 = require("path");
|
|
38
44
|
var import_fs2 = require("fs");
|
|
39
45
|
var import_url = require("url");
|
|
@@ -44,13 +50,14 @@ var import_path2 = require("path");
|
|
|
44
50
|
|
|
45
51
|
// src/lib/plugin/po-parser.ts
|
|
46
52
|
var import_gettext_parser = require("gettext-parser");
|
|
53
|
+
var import_promises = require("fs/promises");
|
|
47
54
|
var import_fs = require("fs");
|
|
48
55
|
var import_path = require("path");
|
|
49
|
-
function parsePoFile(filePath) {
|
|
56
|
+
async function parsePoFile(filePath) {
|
|
50
57
|
if (!(0, import_fs.existsSync)(filePath)) {
|
|
51
58
|
throw new Error(`File not found: ${filePath}`);
|
|
52
59
|
}
|
|
53
|
-
const content = (0,
|
|
60
|
+
const content = await (0, import_promises.readFile)(filePath);
|
|
54
61
|
const parsed = import_gettext_parser.po.parse(content);
|
|
55
62
|
const translations = [];
|
|
56
63
|
for (const [context, messages] of Object.entries(parsed.translations)) {
|
|
@@ -68,11 +75,11 @@ function parsePoFile(filePath) {
|
|
|
68
75
|
}
|
|
69
76
|
return translations;
|
|
70
77
|
}
|
|
71
|
-
function savePoFile(filePath, updates) {
|
|
78
|
+
async function savePoFile(filePath, updates) {
|
|
72
79
|
if (!(0, import_fs.existsSync)(filePath)) {
|
|
73
80
|
throw new Error(`File not found: ${filePath}`);
|
|
74
81
|
}
|
|
75
|
-
const content = (0,
|
|
82
|
+
const content = await (0, import_promises.readFile)(filePath);
|
|
76
83
|
const parsed = import_gettext_parser.po.parse(content);
|
|
77
84
|
for (const update of updates) {
|
|
78
85
|
const context = update.context || "";
|
|
@@ -90,37 +97,39 @@ function savePoFile(filePath, updates) {
|
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
const compiled = import_gettext_parser.po.compile(parsed);
|
|
93
|
-
(0,
|
|
100
|
+
await (0, import_promises.writeFile)(filePath, compiled);
|
|
94
101
|
}
|
|
95
|
-
function updateTranslation(filePath, msgid, msgstr, context) {
|
|
96
|
-
savePoFile(filePath, [{ msgid, msgstr, context }]);
|
|
102
|
+
async function updateTranslation(filePath, msgid, msgstr, context) {
|
|
103
|
+
await savePoFile(filePath, [{ msgid, msgstr, context }]);
|
|
97
104
|
}
|
|
98
|
-
function findPoFiles(localesDir) {
|
|
105
|
+
async function findPoFiles(localesDir) {
|
|
99
106
|
if (!(0, import_fs.existsSync)(localesDir)) {
|
|
100
107
|
return [];
|
|
101
108
|
}
|
|
102
|
-
const files = (0,
|
|
103
|
-
return
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
109
|
+
const files = (await (0, import_promises.readdir)(localesDir)).filter((f) => f.endsWith(".po"));
|
|
110
|
+
return Promise.all(
|
|
111
|
+
files.map(async (file) => {
|
|
112
|
+
const filePath = (0, import_path.join)(localesDir, file);
|
|
113
|
+
const code = (0, import_path.basename)(file, ".po");
|
|
114
|
+
const translations = await parsePoFile(filePath);
|
|
115
|
+
const translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;
|
|
116
|
+
const fuzzy = translations.filter((t) => t.fuzzy).length;
|
|
117
|
+
return {
|
|
118
|
+
code,
|
|
119
|
+
name: getLanguageName(code),
|
|
120
|
+
path: filePath,
|
|
121
|
+
translations,
|
|
122
|
+
progress: {
|
|
123
|
+
total: translations.length,
|
|
124
|
+
translated,
|
|
125
|
+
fuzzy
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
})
|
|
129
|
+
);
|
|
121
130
|
}
|
|
122
|
-
function getLanguageStats(localesDir) {
|
|
123
|
-
const languages = findPoFiles(localesDir);
|
|
131
|
+
async function getLanguageStats(localesDir) {
|
|
132
|
+
const languages = await findPoFiles(localesDir);
|
|
124
133
|
return languages.map((lang) => ({
|
|
125
134
|
code: lang.code,
|
|
126
135
|
name: lang.name,
|
|
@@ -192,7 +201,7 @@ function sendError(res, message, statusCode = 500) {
|
|
|
192
201
|
sendJson(res, { success: false, error: message }, statusCode);
|
|
193
202
|
}
|
|
194
203
|
function createApiMiddleware(options) {
|
|
195
|
-
const { localesDir } = options;
|
|
204
|
+
const { localesDir, server } = options;
|
|
196
205
|
return async (req, res, next) => {
|
|
197
206
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
198
207
|
const path = url.pathname;
|
|
@@ -207,7 +216,7 @@ function createApiMiddleware(options) {
|
|
|
207
216
|
}
|
|
208
217
|
try {
|
|
209
218
|
if (path === "/languages" && method === "GET") {
|
|
210
|
-
const stats = getLanguageStats(localesDir);
|
|
219
|
+
const stats = await getLanguageStats(localesDir);
|
|
211
220
|
sendJson(res, { success: true, data: stats });
|
|
212
221
|
return;
|
|
213
222
|
}
|
|
@@ -216,7 +225,7 @@ function createApiMiddleware(options) {
|
|
|
216
225
|
const langCode = translationsMatch[1];
|
|
217
226
|
const filePath = (0, import_path2.join)(localesDir, `${langCode}.po`);
|
|
218
227
|
try {
|
|
219
|
-
const translations = parsePoFile(filePath);
|
|
228
|
+
const translations = await parsePoFile(filePath);
|
|
220
229
|
sendJson(res, { success: true, data: translations });
|
|
221
230
|
} catch (error) {
|
|
222
231
|
sendError(res, `Language not found: ${langCode}`, 404);
|
|
@@ -229,7 +238,7 @@ function createApiMiddleware(options) {
|
|
|
229
238
|
try {
|
|
230
239
|
const body = await parseBody(req);
|
|
231
240
|
const updates = Array.isArray(body) ? body : [body];
|
|
232
|
-
savePoFile(filePath, updates);
|
|
241
|
+
await savePoFile(filePath, updates);
|
|
233
242
|
sendJson(res, { success: true, message: "Translations updated" });
|
|
234
243
|
} catch (error) {
|
|
235
244
|
sendError(res, error instanceof Error ? error.message : "Failed to update", 400);
|
|
@@ -243,7 +252,7 @@ function createApiMiddleware(options) {
|
|
|
243
252
|
const filePath = (0, import_path2.join)(localesDir, `${langCode}.po`);
|
|
244
253
|
try {
|
|
245
254
|
const body = await parseBody(req);
|
|
246
|
-
updateTranslation(filePath, msgid, body.msgstr, body.context);
|
|
255
|
+
await updateTranslation(filePath, msgid, body.msgstr, body.context);
|
|
247
256
|
sendJson(res, { success: true, message: "Translation updated" });
|
|
248
257
|
} catch (error) {
|
|
249
258
|
sendError(res, error instanceof Error ? error.message : "Failed to update", 400);
|
|
@@ -253,7 +262,7 @@ function createApiMiddleware(options) {
|
|
|
253
262
|
if (path === "/search" && method === "GET") {
|
|
254
263
|
const query = url.searchParams.get("q")?.toLowerCase() || "";
|
|
255
264
|
const lang = url.searchParams.get("lang");
|
|
256
|
-
const languages = findPoFiles(localesDir);
|
|
265
|
+
const languages = await findPoFiles(localesDir);
|
|
257
266
|
const results = [];
|
|
258
267
|
for (const language of languages) {
|
|
259
268
|
if (lang && language.code !== lang) continue;
|
|
@@ -280,11 +289,16 @@ function createApiMiddleware(options) {
|
|
|
280
289
|
}
|
|
281
290
|
|
|
282
291
|
// src/lib/plugin/index.ts
|
|
283
|
-
var
|
|
284
|
-
var
|
|
285
|
-
var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : (0, import_path3.dirname)(__filename);
|
|
292
|
+
var __filename2 = typeof __dirname !== "undefined" ? "" : (0, import_url.fileURLToPath)(importMetaUrl);
|
|
293
|
+
var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : (0, import_path3.dirname)(__filename2);
|
|
286
294
|
function lingoPlugin(options = {}) {
|
|
287
|
-
const {
|
|
295
|
+
const {
|
|
296
|
+
route = "/_translations",
|
|
297
|
+
localesDir = "./locales",
|
|
298
|
+
production = false,
|
|
299
|
+
restartOnPoChange = false,
|
|
300
|
+
reloadOnPoChange = true
|
|
301
|
+
} = options;
|
|
288
302
|
let root;
|
|
289
303
|
let resolvedLocalesDir;
|
|
290
304
|
return {
|
|
@@ -295,6 +309,26 @@ function lingoPlugin(options = {}) {
|
|
|
295
309
|
root = config.root;
|
|
296
310
|
resolvedLocalesDir = (0, import_path3.resolve)(root, localesDir);
|
|
297
311
|
},
|
|
312
|
+
handleHotUpdate({ file, server }) {
|
|
313
|
+
if (file.endsWith(".po")) {
|
|
314
|
+
console.log(`[lingo] .po file updated: ${file}`);
|
|
315
|
+
server.ws.send({
|
|
316
|
+
type: "custom",
|
|
317
|
+
event: "lingo:po-updated",
|
|
318
|
+
data: { path: file }
|
|
319
|
+
});
|
|
320
|
+
if (restartOnPoChange) {
|
|
321
|
+
console.log("[lingo] Restarting dev server...");
|
|
322
|
+
server.restart();
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
if (reloadOnPoChange) {
|
|
326
|
+
console.log("[lingo] Triggering full page reload...");
|
|
327
|
+
server.ws.send({ type: "full-reload" });
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
298
332
|
configureServer(server) {
|
|
299
333
|
const cleanRoute = route.replace(/\/$/, "");
|
|
300
334
|
let uiPath = (0, import_path3.resolve)(__dirnameComputed, "../ui-dist");
|
|
@@ -415,21 +449,10 @@ function lingoPlugin(options = {}) {
|
|
|
415
449
|
`${cleanRoute}/api`,
|
|
416
450
|
createApiMiddleware({
|
|
417
451
|
localesDir: resolvedLocalesDir,
|
|
418
|
-
root
|
|
452
|
+
root,
|
|
453
|
+
server
|
|
419
454
|
})
|
|
420
455
|
);
|
|
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
456
|
const port = server.config.server.port || 5173;
|
|
434
457
|
const protocol = server.config.server.https ? "https" : "http";
|
|
435
458
|
const host = server.config.server.host || "localhost";
|
|
@@ -437,7 +460,9 @@ function lingoPlugin(options = {}) {
|
|
|
437
460
|
const originalPrintUrls = server.printUrls;
|
|
438
461
|
server.printUrls = () => {
|
|
439
462
|
originalPrintUrls?.();
|
|
440
|
-
console.log(
|
|
463
|
+
console.log(
|
|
464
|
+
` \x1B[32m\u279C\x1B[0m \x1B[1mLingo:\x1B[0m ${protocol}://${hostString}:${port}${cleanRoute}`
|
|
465
|
+
);
|
|
441
466
|
};
|
|
442
467
|
}
|
|
443
468
|
};
|
|
@@ -1 +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 (default setup)\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 *\n * @example\n * ```ts\n * // vite.config.ts (SvelteKit convention)\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: './src/locales' // Common in SvelteKit\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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/plugin/index.ts","../../node_modules/tsup/assets/cjs_shims.js","../../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';\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 (default setup)\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 *\n * @example\n * ```ts\n * // vite.config.ts (SvelteKit convention)\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: './src/locales' // Common in SvelteKit\n * })\n * ]\n * });\n * ```\n */\nexport default function lingoPlugin(options: PluginOptions = {}): any {\n\tconst {\n\t\troute = '/_translations',\n\t\tlocalesDir = './locales',\n\t\tproduction = false,\n\t\trestartOnPoChange = false,\n\t\treloadOnPoChange = true\n\t} = 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: any) {\n\t\t\troot = config.root;\n\t\t\tresolvedLocalesDir = resolve(root, localesDir);\n\t\t},\n\n\t\thandleHotUpdate({ file, server }: { file: string; server: ViteDevServer }) {\n\t\t\tif (file.endsWith('.po')) {\n\t\t\t\tconsole.log(`[lingo] .po file updated: ${file}`);\n\n\t\t\t\t// Notify connected clients via WebSocket\n\t\t\t\tserver.ws.send({\n\t\t\t\t\ttype: 'custom',\n\t\t\t\t\tevent: 'lingo:po-updated',\n\t\t\t\t\tdata: { path: file }\n\t\t\t\t});\n\n\t\t\t\tif (restartOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Restarting dev server...');\n\t\t\t\t\tserver.restart();\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\n\t\t\t\tif (reloadOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Triggering full page reload...');\n\t\t\t\t\tserver.ws.send({ type: 'full-reload' });\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t}\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\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\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 (\n\t\t\t\t\t\t(req.url === '/' || req.url === '') &&\n\t\t\t\t\t\treqWithOriginal.originalUrl &&\n\t\t\t\t\t\t!reqWithOriginal.originalUrl.endsWith('/')\n\t\t\t\t\t) {\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\tserver\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\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(\n\t\t\t\t\t` \\x1b[32m➜\\x1b[0m \\x1b[1mLingo:\\x1b[0m ${protocol}://${hostString}:${port}${cleanRoute}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\t};\n}\n\n// Named export for convenience\nexport { lingoPlugin };\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import type { IncomingMessage, ServerResponse } from 'http';\nimport { join } from 'path';\nimport type { ViteDevServer } from 'vite';\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\tserver?: ViteDevServer;\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, server } = options;\n\n\treturn async (req: IncomingMessage, res: ServerResponse, next: () => void): 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 = await 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 = await 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\tawait savePoFile(filePath, updates);\n\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\tawait updateTranslation(filePath, msgid, body.msgstr, body.context);\n\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 = await 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 { readFile, writeFile, readdir } from 'fs/promises';\nimport { 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 async function parsePoFile(filePath: string): Promise<Translation[]> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(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 async function savePoFile(filePath: string, updates: Translation[]): Promise<void> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(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\tawait writeFile(filePath, compiled);\n}\n\n/**\n * Update a single translation\n */\nexport async function updateTranslation(\n\tfilePath: string,\n\tmsgid: string,\n\tmsgstr: string,\n\tcontext?: string\n): Promise<void> {\n\tawait savePoFile(filePath, [{ msgid, msgstr, context }]);\n}\n\n/**\n * Find all .po files in a directory\n */\nexport async function findPoFiles(localesDir: string): Promise<Language[]> {\n\tif (!existsSync(localesDir)) {\n\t\treturn [];\n\t}\n\n\tconst files = (await readdir(localesDir)).filter((f) => f.endsWith('.po'));\n\n\treturn Promise.all(\n\t\tfiles.map(async (file) => {\n\t\t\tconst filePath = join(localesDir, file);\n\t\t\tconst code = basename(file, '.po');\n\t\t\tconst translations = await parsePoFile(filePath);\n\n\t\t\tconst translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;\n\t\t\tconst fuzzy = translations.filter((t) => t.fuzzy).length;\n\n\t\t\treturn {\n\t\t\t\tcode,\n\t\t\t\tname: getLanguageName(code),\n\t\t\t\tpath: filePath,\n\t\t\t\ttranslations,\n\t\t\t\tprogress: {\n\t\t\t\t\ttotal: translations.length,\n\t\t\t\t\ttranslated,\n\t\t\t\t\tfuzzy\n\t\t\t\t}\n\t\t\t};\n\t\t})\n\t);\n}\n\n/**\n * Get language statistics for all languages\n */\nexport async function getLanguageStats(localesDir: string): Promise<LanguageStats[]> {\n\tconst languages = await 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","__filename","sirv"]}
|
package/dist/plugin/index.d.cts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Plugin } from 'vite';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Plugin configuration options
|
|
5
3
|
*/
|
|
@@ -17,6 +15,10 @@ interface PluginOptions {
|
|
|
17
15
|
provider: 'openai' | 'anthropic' | 'google';
|
|
18
16
|
apiKey?: string;
|
|
19
17
|
};
|
|
18
|
+
/** Restart the dev server when a .po file is updated (default: false) */
|
|
19
|
+
restartOnPoChange?: boolean;
|
|
20
|
+
/** Trigger a full page reload when a .po file is updated (default: true) */
|
|
21
|
+
reloadOnPoChange?: boolean;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Represents a single translation entry
|
|
@@ -95,6 +97,6 @@ interface LanguageStats {
|
|
|
95
97
|
* });
|
|
96
98
|
* ```
|
|
97
99
|
*/
|
|
98
|
-
declare function lingoPlugin(options?: PluginOptions):
|
|
100
|
+
declare function lingoPlugin(options?: PluginOptions): any;
|
|
99
101
|
|
|
100
102
|
export { type Language, type LanguageStats, type PluginOptions, type Translation, lingoPlugin as default, lingoPlugin };
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Plugin } from 'vite';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Plugin configuration options
|
|
5
3
|
*/
|
|
@@ -17,6 +15,10 @@ interface PluginOptions {
|
|
|
17
15
|
provider: 'openai' | 'anthropic' | 'google';
|
|
18
16
|
apiKey?: string;
|
|
19
17
|
};
|
|
18
|
+
/** Restart the dev server when a .po file is updated (default: false) */
|
|
19
|
+
restartOnPoChange?: boolean;
|
|
20
|
+
/** Trigger a full page reload when a .po file is updated (default: true) */
|
|
21
|
+
reloadOnPoChange?: boolean;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Represents a single translation entry
|
|
@@ -95,6 +97,6 @@ interface LanguageStats {
|
|
|
95
97
|
* });
|
|
96
98
|
* ```
|
|
97
99
|
*/
|
|
98
|
-
declare function lingoPlugin(options?: PluginOptions):
|
|
100
|
+
declare function lingoPlugin(options?: PluginOptions): any;
|
|
99
101
|
|
|
100
102
|
export { type Language, type LanguageStats, type PluginOptions, type Translation, lingoPlugin as default, lingoPlugin };
|
package/dist/plugin/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
5
|
+
var getDirname = () => path.dirname(getFilename());
|
|
6
|
+
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
|
+
|
|
1
8
|
// src/lib/plugin/index.ts
|
|
2
|
-
import { resolve,
|
|
9
|
+
import { resolve, dirname } from "path";
|
|
3
10
|
import { existsSync as existsSync2 } from "fs";
|
|
4
|
-
import { fileURLToPath } from "url";
|
|
11
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5
12
|
import sirv from "sirv";
|
|
6
13
|
|
|
7
14
|
// src/lib/plugin/middleware.ts
|
|
@@ -9,13 +16,14 @@ import { join as join2 } from "path";
|
|
|
9
16
|
|
|
10
17
|
// src/lib/plugin/po-parser.ts
|
|
11
18
|
import { po } from "gettext-parser";
|
|
12
|
-
import {
|
|
19
|
+
import { readFile, writeFile, readdir } from "fs/promises";
|
|
20
|
+
import { existsSync } from "fs";
|
|
13
21
|
import { join, basename } from "path";
|
|
14
|
-
function parsePoFile(filePath) {
|
|
22
|
+
async function parsePoFile(filePath) {
|
|
15
23
|
if (!existsSync(filePath)) {
|
|
16
24
|
throw new Error(`File not found: ${filePath}`);
|
|
17
25
|
}
|
|
18
|
-
const content =
|
|
26
|
+
const content = await readFile(filePath);
|
|
19
27
|
const parsed = po.parse(content);
|
|
20
28
|
const translations = [];
|
|
21
29
|
for (const [context, messages] of Object.entries(parsed.translations)) {
|
|
@@ -33,11 +41,11 @@ function parsePoFile(filePath) {
|
|
|
33
41
|
}
|
|
34
42
|
return translations;
|
|
35
43
|
}
|
|
36
|
-
function savePoFile(filePath, updates) {
|
|
44
|
+
async function savePoFile(filePath, updates) {
|
|
37
45
|
if (!existsSync(filePath)) {
|
|
38
46
|
throw new Error(`File not found: ${filePath}`);
|
|
39
47
|
}
|
|
40
|
-
const content =
|
|
48
|
+
const content = await readFile(filePath);
|
|
41
49
|
const parsed = po.parse(content);
|
|
42
50
|
for (const update of updates) {
|
|
43
51
|
const context = update.context || "";
|
|
@@ -55,37 +63,39 @@ function savePoFile(filePath, updates) {
|
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
const compiled = po.compile(parsed);
|
|
58
|
-
|
|
66
|
+
await writeFile(filePath, compiled);
|
|
59
67
|
}
|
|
60
|
-
function updateTranslation(filePath, msgid, msgstr, context) {
|
|
61
|
-
savePoFile(filePath, [{ msgid, msgstr, context }]);
|
|
68
|
+
async function updateTranslation(filePath, msgid, msgstr, context) {
|
|
69
|
+
await savePoFile(filePath, [{ msgid, msgstr, context }]);
|
|
62
70
|
}
|
|
63
|
-
function findPoFiles(localesDir) {
|
|
71
|
+
async function findPoFiles(localesDir) {
|
|
64
72
|
if (!existsSync(localesDir)) {
|
|
65
73
|
return [];
|
|
66
74
|
}
|
|
67
|
-
const files =
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
75
|
+
const files = (await readdir(localesDir)).filter((f) => f.endsWith(".po"));
|
|
76
|
+
return Promise.all(
|
|
77
|
+
files.map(async (file) => {
|
|
78
|
+
const filePath = join(localesDir, file);
|
|
79
|
+
const code = basename(file, ".po");
|
|
80
|
+
const translations = await parsePoFile(filePath);
|
|
81
|
+
const translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;
|
|
82
|
+
const fuzzy = translations.filter((t) => t.fuzzy).length;
|
|
83
|
+
return {
|
|
84
|
+
code,
|
|
85
|
+
name: getLanguageName(code),
|
|
86
|
+
path: filePath,
|
|
87
|
+
translations,
|
|
88
|
+
progress: {
|
|
89
|
+
total: translations.length,
|
|
90
|
+
translated,
|
|
91
|
+
fuzzy
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
})
|
|
95
|
+
);
|
|
86
96
|
}
|
|
87
|
-
function getLanguageStats(localesDir) {
|
|
88
|
-
const languages = findPoFiles(localesDir);
|
|
97
|
+
async function getLanguageStats(localesDir) {
|
|
98
|
+
const languages = await findPoFiles(localesDir);
|
|
89
99
|
return languages.map((lang) => ({
|
|
90
100
|
code: lang.code,
|
|
91
101
|
name: lang.name,
|
|
@@ -157,10 +167,10 @@ function sendError(res, message, statusCode = 500) {
|
|
|
157
167
|
sendJson(res, { success: false, error: message }, statusCode);
|
|
158
168
|
}
|
|
159
169
|
function createApiMiddleware(options) {
|
|
160
|
-
const { localesDir } = options;
|
|
170
|
+
const { localesDir, server } = options;
|
|
161
171
|
return async (req, res, next) => {
|
|
162
172
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
163
|
-
const
|
|
173
|
+
const path2 = url.pathname;
|
|
164
174
|
const method = req.method?.toUpperCase();
|
|
165
175
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
166
176
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
@@ -171,17 +181,17 @@ function createApiMiddleware(options) {
|
|
|
171
181
|
return;
|
|
172
182
|
}
|
|
173
183
|
try {
|
|
174
|
-
if (
|
|
175
|
-
const stats = getLanguageStats(localesDir);
|
|
184
|
+
if (path2 === "/languages" && method === "GET") {
|
|
185
|
+
const stats = await getLanguageStats(localesDir);
|
|
176
186
|
sendJson(res, { success: true, data: stats });
|
|
177
187
|
return;
|
|
178
188
|
}
|
|
179
|
-
const translationsMatch =
|
|
189
|
+
const translationsMatch = path2.match(/^\/translations\/([^/]+)$/);
|
|
180
190
|
if (translationsMatch && method === "GET") {
|
|
181
191
|
const langCode = translationsMatch[1];
|
|
182
192
|
const filePath = join2(localesDir, `${langCode}.po`);
|
|
183
193
|
try {
|
|
184
|
-
const translations = parsePoFile(filePath);
|
|
194
|
+
const translations = await parsePoFile(filePath);
|
|
185
195
|
sendJson(res, { success: true, data: translations });
|
|
186
196
|
} catch (error) {
|
|
187
197
|
sendError(res, `Language not found: ${langCode}`, 404);
|
|
@@ -194,31 +204,31 @@ function createApiMiddleware(options) {
|
|
|
194
204
|
try {
|
|
195
205
|
const body = await parseBody(req);
|
|
196
206
|
const updates = Array.isArray(body) ? body : [body];
|
|
197
|
-
savePoFile(filePath, updates);
|
|
207
|
+
await savePoFile(filePath, updates);
|
|
198
208
|
sendJson(res, { success: true, message: "Translations updated" });
|
|
199
209
|
} catch (error) {
|
|
200
210
|
sendError(res, error instanceof Error ? error.message : "Failed to update", 400);
|
|
201
211
|
}
|
|
202
212
|
return;
|
|
203
213
|
}
|
|
204
|
-
const singleMatch =
|
|
214
|
+
const singleMatch = path2.match(/^\/translation\/([^/]+)\/(.+)$/);
|
|
205
215
|
if (singleMatch && method === "PUT") {
|
|
206
216
|
const langCode = singleMatch[1];
|
|
207
217
|
const msgid = decodeURIComponent(singleMatch[2]);
|
|
208
218
|
const filePath = join2(localesDir, `${langCode}.po`);
|
|
209
219
|
try {
|
|
210
220
|
const body = await parseBody(req);
|
|
211
|
-
updateTranslation(filePath, msgid, body.msgstr, body.context);
|
|
221
|
+
await updateTranslation(filePath, msgid, body.msgstr, body.context);
|
|
212
222
|
sendJson(res, { success: true, message: "Translation updated" });
|
|
213
223
|
} catch (error) {
|
|
214
224
|
sendError(res, error instanceof Error ? error.message : "Failed to update", 400);
|
|
215
225
|
}
|
|
216
226
|
return;
|
|
217
227
|
}
|
|
218
|
-
if (
|
|
228
|
+
if (path2 === "/search" && method === "GET") {
|
|
219
229
|
const query = url.searchParams.get("q")?.toLowerCase() || "";
|
|
220
230
|
const lang = url.searchParams.get("lang");
|
|
221
|
-
const languages = findPoFiles(localesDir);
|
|
231
|
+
const languages = await findPoFiles(localesDir);
|
|
222
232
|
const results = [];
|
|
223
233
|
for (const language of languages) {
|
|
224
234
|
if (lang && language.code !== lang) continue;
|
|
@@ -245,10 +255,16 @@ function createApiMiddleware(options) {
|
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
// src/lib/plugin/index.ts
|
|
248
|
-
var
|
|
249
|
-
var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : dirname(
|
|
258
|
+
var __filename2 = typeof __dirname !== "undefined" ? "" : fileURLToPath2(import.meta.url);
|
|
259
|
+
var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : dirname(__filename2);
|
|
250
260
|
function lingoPlugin(options = {}) {
|
|
251
|
-
const {
|
|
261
|
+
const {
|
|
262
|
+
route = "/_translations",
|
|
263
|
+
localesDir = "./locales",
|
|
264
|
+
production = false,
|
|
265
|
+
restartOnPoChange = false,
|
|
266
|
+
reloadOnPoChange = true
|
|
267
|
+
} = options;
|
|
252
268
|
let root;
|
|
253
269
|
let resolvedLocalesDir;
|
|
254
270
|
return {
|
|
@@ -259,6 +275,26 @@ function lingoPlugin(options = {}) {
|
|
|
259
275
|
root = config.root;
|
|
260
276
|
resolvedLocalesDir = resolve(root, localesDir);
|
|
261
277
|
},
|
|
278
|
+
handleHotUpdate({ file, server }) {
|
|
279
|
+
if (file.endsWith(".po")) {
|
|
280
|
+
console.log(`[lingo] .po file updated: ${file}`);
|
|
281
|
+
server.ws.send({
|
|
282
|
+
type: "custom",
|
|
283
|
+
event: "lingo:po-updated",
|
|
284
|
+
data: { path: file }
|
|
285
|
+
});
|
|
286
|
+
if (restartOnPoChange) {
|
|
287
|
+
console.log("[lingo] Restarting dev server...");
|
|
288
|
+
server.restart();
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
if (reloadOnPoChange) {
|
|
292
|
+
console.log("[lingo] Triggering full page reload...");
|
|
293
|
+
server.ws.send({ type: "full-reload" });
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
262
298
|
configureServer(server) {
|
|
263
299
|
const cleanRoute = route.replace(/\/$/, "");
|
|
264
300
|
let uiPath = resolve(__dirnameComputed, "../ui-dist");
|
|
@@ -379,21 +415,10 @@ function lingoPlugin(options = {}) {
|
|
|
379
415
|
`${cleanRoute}/api`,
|
|
380
416
|
createApiMiddleware({
|
|
381
417
|
localesDir: resolvedLocalesDir,
|
|
382
|
-
root
|
|
418
|
+
root,
|
|
419
|
+
server
|
|
383
420
|
})
|
|
384
421
|
);
|
|
385
|
-
const poGlob = join3(resolvedLocalesDir, "**/*.po");
|
|
386
|
-
server.watcher.add(poGlob);
|
|
387
|
-
server.watcher.on("change", (path) => {
|
|
388
|
-
if (path.endsWith(".po")) {
|
|
389
|
-
server.ws.send({
|
|
390
|
-
type: "custom",
|
|
391
|
-
event: "lingo:po-updated",
|
|
392
|
-
data: { path }
|
|
393
|
-
});
|
|
394
|
-
console.log(`[lingo] .po file updated: ${path}`);
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
422
|
const port = server.config.server.port || 5173;
|
|
398
423
|
const protocol = server.config.server.https ? "https" : "http";
|
|
399
424
|
const host = server.config.server.host || "localhost";
|
|
@@ -401,7 +426,9 @@ function lingoPlugin(options = {}) {
|
|
|
401
426
|
const originalPrintUrls = server.printUrls;
|
|
402
427
|
server.printUrls = () => {
|
|
403
428
|
originalPrintUrls?.();
|
|
404
|
-
console.log(
|
|
429
|
+
console.log(
|
|
430
|
+
` \x1B[32m\u279C\x1B[0m \x1B[1mLingo:\x1B[0m ${protocol}://${hostString}:${port}${cleanRoute}`
|
|
431
|
+
);
|
|
405
432
|
};
|
|
406
433
|
}
|
|
407
434
|
};
|
package/dist/plugin/index.js.map
CHANGED
|
@@ -1 +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 (default setup)\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 *\n * @example\n * ```ts\n * // vite.config.ts (SvelteKit convention)\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: './src/locales' // Common in SvelteKit\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":["join","existsSync","join","resolve","join","existsSync","join"]}
|
|
1
|
+
{"version":3,"sources":["../../node_modules/tsup/assets/esm_shims.js","../../src/lib/plugin/index.ts","../../src/lib/plugin/middleware.ts","../../src/lib/plugin/po-parser.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","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';\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 (default setup)\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 *\n * @example\n * ```ts\n * // vite.config.ts (SvelteKit convention)\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: './src/locales' // Common in SvelteKit\n * })\n * ]\n * });\n * ```\n */\nexport default function lingoPlugin(options: PluginOptions = {}): any {\n\tconst {\n\t\troute = '/_translations',\n\t\tlocalesDir = './locales',\n\t\tproduction = false,\n\t\trestartOnPoChange = false,\n\t\treloadOnPoChange = true\n\t} = 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: any) {\n\t\t\troot = config.root;\n\t\t\tresolvedLocalesDir = resolve(root, localesDir);\n\t\t},\n\n\t\thandleHotUpdate({ file, server }: { file: string; server: ViteDevServer }) {\n\t\t\tif (file.endsWith('.po')) {\n\t\t\t\tconsole.log(`[lingo] .po file updated: ${file}`);\n\n\t\t\t\t// Notify connected clients via WebSocket\n\t\t\t\tserver.ws.send({\n\t\t\t\t\ttype: 'custom',\n\t\t\t\t\tevent: 'lingo:po-updated',\n\t\t\t\t\tdata: { path: file }\n\t\t\t\t});\n\n\t\t\t\tif (restartOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Restarting dev server...');\n\t\t\t\t\tserver.restart();\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\n\t\t\t\tif (reloadOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Triggering full page reload...');\n\t\t\t\t\tserver.ws.send({ type: 'full-reload' });\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t}\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\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\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 (\n\t\t\t\t\t\t(req.url === '/' || req.url === '') &&\n\t\t\t\t\t\treqWithOriginal.originalUrl &&\n\t\t\t\t\t\t!reqWithOriginal.originalUrl.endsWith('/')\n\t\t\t\t\t) {\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\tserver\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\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(\n\t\t\t\t\t` \\x1b[32m➜\\x1b[0m \\x1b[1mLingo:\\x1b[0m ${protocol}://${hostString}:${port}${cleanRoute}`\n\t\t\t\t);\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 type { ViteDevServer } from 'vite';\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\tserver?: ViteDevServer;\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, server } = options;\n\n\treturn async (req: IncomingMessage, res: ServerResponse, next: () => void): 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 = await 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 = await 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\tawait savePoFile(filePath, updates);\n\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\tawait updateTranslation(filePath, msgid, body.msgstr, body.context);\n\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 = await 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 { readFile, writeFile, readdir } from 'fs/promises';\nimport { 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 async function parsePoFile(filePath: string): Promise<Translation[]> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(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 async function savePoFile(filePath: string, updates: Translation[]): Promise<void> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(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\tawait writeFile(filePath, compiled);\n}\n\n/**\n * Update a single translation\n */\nexport async function updateTranslation(\n\tfilePath: string,\n\tmsgid: string,\n\tmsgstr: string,\n\tcontext?: string\n): Promise<void> {\n\tawait savePoFile(filePath, [{ msgid, msgstr, context }]);\n}\n\n/**\n * Find all .po files in a directory\n */\nexport async function findPoFiles(localesDir: string): Promise<Language[]> {\n\tif (!existsSync(localesDir)) {\n\t\treturn [];\n\t}\n\n\tconst files = (await readdir(localesDir)).filter((f) => f.endsWith('.po'));\n\n\treturn Promise.all(\n\t\tfiles.map(async (file) => {\n\t\t\tconst filePath = join(localesDir, file);\n\t\t\tconst code = basename(file, '.po');\n\t\t\tconst translations = await parsePoFile(filePath);\n\n\t\t\tconst translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;\n\t\t\tconst fuzzy = translations.filter((t) => t.fuzzy).length;\n\n\t\t\treturn {\n\t\t\t\tcode,\n\t\t\t\tname: getLanguageName(code),\n\t\t\t\tpath: filePath,\n\t\t\t\ttranslations,\n\t\t\t\tprogress: {\n\t\t\t\t\ttotal: translations.length,\n\t\t\t\t\ttranslated,\n\t\t\t\t\tfuzzy\n\t\t\t\t}\n\t\t\t};\n\t\t})\n\t);\n}\n\n/**\n * Get language statistics for all languages\n */\nexport async function getLanguageStats(localesDir: string): Promise<LanguageStats[]> {\n\tconst languages = await 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":";AACA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,IAAM,cAAc,MAAM,cAAc,YAAY,GAAG;AACvD,IAAM,aAAa,MAAM,KAAK,QAAQ,YAAY,CAAC;AAE5C,IAAM,YAA4B,2BAAW;;;ACNpD,SAAS,SAAe,eAAe;AACvC,SAAS,cAAAA,mBAAkB;AAC3B,SAAS,iBAAAC,sBAAqB;AAC9B,OAAO,UAAU;;;ACHjB,SAAS,QAAAC,aAAY;;;ACDrB,SAAS,UAAU;AACnB,SAAS,UAAU,WAAW,eAAe;AAC7C,SAAS,kBAAkB;AAC3B,SAAS,MAAM,gBAAgB;AAM/B,eAAsB,YAAY,UAA0C;AAC3E,MAAI,CAAC,WAAW,QAAQ,GAAG;AAC1B,UAAM,IAAI,MAAM,mBAAmB,QAAQ,EAAE;AAAA,EAC9C;AAEA,QAAM,UAAU,MAAM,SAAS,QAAQ;AACvC,QAAM,SAAS,GAAG,MAAM,OAAO;AAE/B,QAAM,eAA8B,CAAC;AAErC,aAAW,CAAC,SAAS,QAAQ,KAAK,OAAO,QAAQ,OAAO,YAAY,GAAG;AACtE,eAAW,CAAC,OAAO,IAAI,KAAK,OAAO,QAAQ,QAAmC,GAAG;AAChF,UAAI,CAAC,MAAO;AAEZ,YAAM,QAAQ;AAWd,mBAAa,KAAK;AAAA,QACjB;AAAA,QACA,QAAQ,MAAM,SAAS,CAAC,KAAK;AAAA,QAC7B,SAAS,WAAW;AAAA,QACpB,UAAU,MAAM;AAAA,QAChB,OAAO,MAAM,UAAU,MAAM,SAAS,OAAO,KAAK;AAAA,MACnD,CAAC;AAAA,IACF;AAAA,EACD;AAEA,SAAO;AACR;AAKA,eAAsB,WAAW,UAAkB,SAAuC;AACzF,MAAI,CAAC,WAAW,QAAQ,GAAG;AAC1B,UAAM,IAAI,MAAM,mBAAmB,QAAQ,EAAE;AAAA,EAC9C;AAEA,QAAM,UAAU,MAAM,SAAS,QAAQ;AACvC,QAAM,SAAS,GAAG,MAAM,OAAO;AAE/B,aAAW,UAAU,SAAS;AAC7B,UAAM,UAAU,OAAO,WAAW;AAElC,QAAI,OAAO,aAAa,OAAO,IAAI,OAAO,KAAK,GAAG;AACjD,aAAO,aAAa,OAAO,EAAE,OAAO,KAAK,EAAE,SAAS,CAAC,OAAO,MAAM;AAGlE,UAAI,OAAO,UAAU,QAAW;AAC/B,cAAM,WAAW,OAAO,aAAa,OAAO,EAAE,OAAO,KAAK,EAAE,YAAY,CAAC;AACzE,YAAI,OAAO,OAAO;AACjB,mBAAS,OAAO;AAAA,QACjB,OAAO;AACN,iBAAO,SAAS;AAAA,QACjB;AACA,eAAO,aAAa,OAAO,EAAE,OAAO,KAAK,EAAE,WAAW;AAAA,MACvD;AAAA,IACD;AAAA,EACD;AAEA,QAAM,WAAW,GAAG,QAAQ,MAAM;AAClC,QAAM,UAAU,UAAU,QAAQ;AACnC;AAKA,eAAsB,kBACrB,UACA,OACA,QACA,SACgB;AAChB,QAAM,WAAW,UAAU,CAAC,EAAE,OAAO,QAAQ,QAAQ,CAAC,CAAC;AACxD;AAKA,eAAsB,YAAY,YAAyC;AAC1E,MAAI,CAAC,WAAW,UAAU,GAAG;AAC5B,WAAO,CAAC;AAAA,EACT;AAEA,QAAM,SAAS,MAAM,QAAQ,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,SAAS,KAAK,CAAC;AAEzE,SAAO,QAAQ;AAAA,IACd,MAAM,IAAI,OAAO,SAAS;AACzB,YAAM,WAAW,KAAK,YAAY,IAAI;AACtC,YAAM,OAAO,SAAS,MAAM,KAAK;AACjC,YAAM,eAAe,MAAM,YAAY,QAAQ;AAE/C,YAAM,aAAa,aAAa,OAAO,CAAC,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,EAAE;AACpE,YAAM,QAAQ,aAAa,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE;AAElD,aAAO;AAAA,QACN;AAAA,QACA,MAAM,gBAAgB,IAAI;AAAA,QAC1B,MAAM;AAAA,QACN;AAAA,QACA,UAAU;AAAA,UACT,OAAO,aAAa;AAAA,UACpB;AAAA,UACA;AAAA,QACD;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AACD;AAKA,eAAsB,iBAAiB,YAA8C;AACpF,QAAM,YAAY,MAAM,YAAY,UAAU;AAE9C,SAAO,UAAU,IAAI,CAAC,UAAU;AAAA,IAC/B,MAAM,KAAK;AAAA,IACX,MAAM,KAAK;AAAA,IACX,OAAO,KAAK,SAAS;AAAA,IACrB,YAAY,KAAK,SAAS;AAAA,IAC1B,OAAO,KAAK,SAAS;AAAA,IACrB,cAAc,KAAK,SAAS,QAAQ,KAAK,SAAS,aAAa,KAAK,SAAS;AAAA,IAC7E,UACC,KAAK,SAAS,QAAQ,IACnB,KAAK,MAAO,KAAK,SAAS,aAAa,KAAK,SAAS,QAAS,GAAG,IACjE;AAAA,EACL,EAAE;AACH;AAKO,SAAS,gBAAgB,MAAsB;AACrD,QAAM,QAAgC;AAAA,IACrC,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,SAAS;AAAA,IACT,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACL;AACA,SAAO,MAAM,IAAI,KAAK,KAAK,YAAY;AACxC;;;ADpKA,eAAe,UAAa,KAAkC;AAC7D,SAAO,IAAI,QAAQ,CAACC,UAAS,WAAW;AACvC,QAAI,OAAO;AACX,QAAI,GAAG,QAAQ,CAAC,UAAW,QAAQ,KAAM;AACzC,QAAI,GAAG,OAAO,MAAM;AACnB,UAAI;AACH,QAAAA,SAAQ,KAAK,MAAM,IAAI,CAAC;AAAA,MACzB,QAAQ;AACP,eAAO,IAAI,MAAM,cAAc,CAAC;AAAA,MACjC;AAAA,IACD,CAAC;AACD,QAAI,GAAG,SAAS,MAAM;AAAA,EACvB,CAAC;AACF;AAKA,SAAS,SAAS,KAAqB,MAAe,aAAa,KAAW;AAC7E,MAAI,aAAa;AACjB,MAAI,UAAU,gBAAgB,kBAAkB;AAChD,MAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC7B;AAKA,SAAS,UAAU,KAAqB,SAAiB,aAAa,KAAW;AAChF,WAAS,KAAK,EAAE,SAAS,OAAO,OAAO,QAAQ,GAAG,UAAU;AAC7D;AAKO,SAAS,oBAAoB,SAA4B;AAC/D,QAAM,EAAE,YAAY,OAAO,IAAI;AAE/B,SAAO,OAAO,KAAsB,KAAqB,SAAoC;AAC5F,UAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,IAAI,EAAE;AAChE,UAAMC,QAAO,IAAI;AACjB,UAAM,SAAS,IAAI,QAAQ,YAAY;AAGvC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,UAAU,gCAAgC,iCAAiC;AAC/E,QAAI,UAAU,gCAAgC,cAAc;AAG5D,QAAI,WAAW,WAAW;AACzB,UAAI,aAAa;AACjB,UAAI,IAAI;AACR;AAAA,IACD;AAEA,QAAI;AAEH,UAAIA,UAAS,gBAAgB,WAAW,OAAO;AAC9C,cAAM,QAAQ,MAAM,iBAAiB,UAAU;AAC/C,iBAAS,KAAK,EAAE,SAAS,MAAM,MAAM,MAAM,CAAC;AAC5C;AAAA,MACD;AAGA,YAAM,oBAAoBA,MAAK,MAAM,2BAA2B;AAChE,UAAI,qBAAqB,WAAW,OAAO;AAC1C,cAAM,WAAW,kBAAkB,CAAC;AACpC,cAAM,WAAWC,MAAK,YAAY,GAAG,QAAQ,KAAK;AAElD,YAAI;AACH,gBAAM,eAAe,MAAM,YAAY,QAAQ;AAC/C,mBAAS,KAAK,EAAE,SAAS,MAAM,MAAM,aAAa,CAAC;AAAA,QACpD,SAAS,OAAO;AACf,oBAAU,KAAK,uBAAuB,QAAQ,IAAI,GAAG;AAAA,QACtD;AACA;AAAA,MACD;AAGA,UAAI,qBAAqB,WAAW,OAAO;AAC1C,cAAM,WAAW,kBAAkB,CAAC;AACpC,cAAM,WAAWA,MAAK,YAAY,GAAG,QAAQ,KAAK;AAElD,YAAI;AACH,gBAAM,OAAO,MAAM,UAAuC,GAAG;AAC7D,gBAAM,UAAU,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC,IAAI;AAClD,gBAAM,WAAW,UAAU,OAAO;AAElC,mBAAS,KAAK,EAAE,SAAS,MAAM,SAAS,uBAAuB,CAAC;AAAA,QACjE,SAAS,OAAO;AACf,oBAAU,KAAK,iBAAiB,QAAQ,MAAM,UAAU,oBAAoB,GAAG;AAAA,QAChF;AACA;AAAA,MACD;AAGA,YAAM,cAAcD,MAAK,MAAM,gCAAgC;AAC/D,UAAI,eAAe,WAAW,OAAO;AACpC,cAAM,WAAW,YAAY,CAAC;AAC9B,cAAM,QAAQ,mBAAmB,YAAY,CAAC,CAAC;AAC/C,cAAM,WAAWC,MAAK,YAAY,GAAG,QAAQ,KAAK;AAElD,YAAI;AACH,gBAAM,OAAO,MAAM,UAAgD,GAAG;AACtE,gBAAM,kBAAkB,UAAU,OAAO,KAAK,QAAQ,KAAK,OAAO;AAElE,mBAAS,KAAK,EAAE,SAAS,MAAM,SAAS,sBAAsB,CAAC;AAAA,QAChE,SAAS,OAAO;AACf,oBAAU,KAAK,iBAAiB,QAAQ,MAAM,UAAU,oBAAoB,GAAG;AAAA,QAChF;AACA;AAAA,MACD;AAGA,UAAID,UAAS,aAAa,WAAW,OAAO;AAC3C,cAAM,QAAQ,IAAI,aAAa,IAAI,GAAG,GAAG,YAAY,KAAK;AAC1D,cAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AAExC,cAAM,YAAY,MAAM,YAAY,UAAU;AAC9C,cAAM,UAKD,CAAC;AAEN,mBAAW,YAAY,WAAW;AACjC,cAAI,QAAQ,SAAS,SAAS,KAAM;AAEpC,qBAAW,KAAK,SAAS,cAAc;AACtC,gBAAI,EAAE,MAAM,YAAY,EAAE,SAAS,KAAK,KAAK,EAAE,OAAO,YAAY,EAAE,SAAS,KAAK,GAAG;AACpF,sBAAQ,KAAK;AAAA,gBACZ,MAAM,SAAS;AAAA,gBACf,OAAO,EAAE;AAAA,gBACT,QAAQ,EAAE;AAAA,gBACV,SAAS,EAAE;AAAA,cACZ,CAAC;AAAA,YACF;AAAA,UACD;AAAA,QACD;AAEA,iBAAS,KAAK,EAAE,SAAS,MAAM,MAAM,QAAQ,CAAC;AAC9C;AAAA,MACD;AAGA,gBAAU,KAAK,aAAa,GAAG;AAAA,IAChC,SAAS,OAAO;AACf,cAAQ,MAAM,sBAAsB,KAAK;AACzC,gBAAU,KAAK,iBAAiB,QAAQ,MAAM,UAAU,uBAAuB;AAAA,IAChF;AAAA,EACD;AACD;;;ADjKA,IAAME,cAAa,OAAO,cAAc,cAAc,KAAKC,eAAc,YAAY,GAAG;AACxF,IAAM,oBAAoB,OAAO,cAAc,cAAc,YAAY,QAAQD,WAAU;AAwC5E,SAAR,YAA6B,UAAyB,CAAC,GAAQ;AACrE,QAAM;AAAA,IACL,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,aAAa;AAAA,IACb,oBAAoB;AAAA,IACpB,mBAAmB;AAAA,EACpB,IAAI;AAEJ,MAAI;AACJ,MAAI;AAEJ,SAAO;AAAA,IACN,MAAM;AAAA;AAAA,IAGN,OAAO,aAAa,SAAY;AAAA,IAEhC,eAAe,QAAa;AAC3B,aAAO,OAAO;AACd,2BAAqB,QAAQ,MAAM,UAAU;AAAA,IAC9C;AAAA,IAEA,gBAAgB,EAAE,MAAM,OAAO,GAA4C;AAC1E,UAAI,KAAK,SAAS,KAAK,GAAG;AACzB,gBAAQ,IAAI,6BAA6B,IAAI,EAAE;AAG/C,eAAO,GAAG,KAAK;AAAA,UACd,MAAM;AAAA,UACN,OAAO;AAAA,UACP,MAAM,EAAE,MAAM,KAAK;AAAA,QACpB,CAAC;AAED,YAAI,mBAAmB;AACtB,kBAAQ,IAAI,kCAAkC;AAC9C,iBAAO,QAAQ;AACf,iBAAO,CAAC;AAAA,QACT;AAEA,YAAI,kBAAkB;AACrB,kBAAQ,IAAI,wCAAwC;AACpD,iBAAO,GAAG,KAAK,EAAE,MAAM,cAAc,CAAC;AACtC,iBAAO,CAAC;AAAA,QACT;AAAA,MACD;AAAA,IACD;AAAA,IAEA,gBAAgB,QAAuB;AAEtC,YAAM,aAAa,MAAM,QAAQ,OAAO,EAAE;AAK1C,UAAI,SAAS,QAAQ,mBAAmB,YAAY;AAGpD,UAAI,CAACE,YAAW,MAAM,GAAG;AACxB,iBAAS,QAAQ,MAAM,cAAc;AAAA,MACtC;AAEA,cAAQ,IAAI,8BAA8B,MAAM;AAChD,cAAQ,IAAI,sBAAsBA,YAAW,MAAM,CAAC;AAGpD,UAAIA,YAAW,MAAM,GAAG;AACvB,gBAAQ,IAAI,kCAAkC,MAAM;AACpD,cAAM,QAAQ,KAAK,QAAQ;AAAA,UAC1B,KAAK;AAAA,UACL,QAAQ;AAAA;AAAA,QACT,CAAC;AAED,eAAO,YAAY,IAAI,YAAY,CAAC,KAAK,KAAK,SAAS;AAItD,gBAAM,kBAAkB;AACxB,eACE,IAAI,QAAQ,OAAO,IAAI,QAAQ,OAChC,gBAAgB,eAChB,CAAC,gBAAgB,YAAY,SAAS,GAAG,GACxC;AACD,gBAAI,UAAU,KAAK,EAAE,UAAU,aAAa,IAAI,CAAC;AACjD,gBAAI,IAAI;AACR;AAAA,UACD;AAGA,cAAI,IAAI,QAAQ,gBAAgB;AAC/B,gBAAI,aAAa;AACjB,gBAAI,IAAI;AACR;AAAA,UACD;AAGA,cAAI,IAAI,KAAK,WAAW,MAAM,GAAG;AAChC,mBAAO,KAAK;AAAA,UACb;AAIA,gBAAM,KAAK,KAAK,MAAM,KAAK,CAAC;AAAA,QAC7B,CAAC;AAAA,MACF,OAAO;AAEN,eAAO,YAAY,IAAI,YAAY,CAAC,KAAK,KAAK,SAAS;AAEtD,cAAI,IAAI,QAAQ,gBAAgB;AAC/B,gBAAI,aAAa;AACjB,gBAAI,IAAI;AACR;AAAA,UACD;AAEA,cAAI,IAAI,KAAK,WAAW,MAAM,GAAG;AAChC,mBAAO,KAAK;AAAA,UACb;AAEA,cAAI,UAAU,gBAAgB,WAAW;AACzC,cAAI,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAmCsB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAmClC;AAAA,QACP,CAAC;AAAA,MACF;AAGA,aAAO,YAAY;AAAA,QAClB,GAAG,UAAU;AAAA,QACb,oBAAoB;AAAA,UACnB,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,QACD,CAAC;AAAA,MACF;AAGA,YAAM,OAAO,OAAO,OAAO,OAAO,QAAQ;AAE1C,YAAM,WAAW,OAAO,OAAO,OAAO,QAAQ,UAAU;AACxD,YAAM,OAAO,OAAO,OAAO,OAAO,QAAQ;AAC1C,YAAM,aAAa,OAAO,SAAS,WAAW,OAAO;AAGrD,YAAM,oBAAoB,OAAO;AACjC,aAAO,YAAY,MAAM;AACxB,4BAAoB;AACpB,gBAAQ;AAAA,UACP,iDAA4C,QAAQ,MAAM,UAAU,IAAI,IAAI,GAAG,UAAU;AAAA,QAC1F;AAAA,MACD;AAAA,IACD;AAAA,EACD;AACD;","names":["existsSync","fileURLToPath","join","resolve","path","join","__filename","fileURLToPath","existsSync"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
2
3
|
interface MiddlewareOptions {
|
|
3
4
|
localesDir: string;
|
|
4
5
|
root: string;
|
|
6
|
+
server?: ViteDevServer;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Create API middleware for handling translation operations
|
|
@@ -36,7 +36,7 @@ function sendError(res, message, statusCode = 500) {
|
|
|
36
36
|
* Create API middleware for handling translation operations
|
|
37
37
|
*/
|
|
38
38
|
export function createApiMiddleware(options) {
|
|
39
|
-
const { localesDir } = options;
|
|
39
|
+
const { localesDir, server } = options;
|
|
40
40
|
return async (req, res, next) => {
|
|
41
41
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
42
42
|
const path = url.pathname;
|
|
@@ -54,7 +54,7 @@ export function createApiMiddleware(options) {
|
|
|
54
54
|
try {
|
|
55
55
|
// GET /api/languages - List all languages with stats
|
|
56
56
|
if (path === '/languages' && method === 'GET') {
|
|
57
|
-
const stats = getLanguageStats(localesDir);
|
|
57
|
+
const stats = await getLanguageStats(localesDir);
|
|
58
58
|
sendJson(res, { success: true, data: stats });
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
@@ -64,7 +64,7 @@ export function createApiMiddleware(options) {
|
|
|
64
64
|
const langCode = translationsMatch[1];
|
|
65
65
|
const filePath = join(localesDir, `${langCode}.po`);
|
|
66
66
|
try {
|
|
67
|
-
const translations = parsePoFile(filePath);
|
|
67
|
+
const translations = await parsePoFile(filePath);
|
|
68
68
|
sendJson(res, { success: true, data: translations });
|
|
69
69
|
}
|
|
70
70
|
catch (error) {
|
|
@@ -79,7 +79,7 @@ export function createApiMiddleware(options) {
|
|
|
79
79
|
try {
|
|
80
80
|
const body = await parseBody(req);
|
|
81
81
|
const updates = Array.isArray(body) ? body : [body];
|
|
82
|
-
savePoFile(filePath, updates);
|
|
82
|
+
await savePoFile(filePath, updates);
|
|
83
83
|
sendJson(res, { success: true, message: 'Translations updated' });
|
|
84
84
|
}
|
|
85
85
|
catch (error) {
|
|
@@ -95,7 +95,7 @@ export function createApiMiddleware(options) {
|
|
|
95
95
|
const filePath = join(localesDir, `${langCode}.po`);
|
|
96
96
|
try {
|
|
97
97
|
const body = await parseBody(req);
|
|
98
|
-
updateTranslation(filePath, msgid, body.msgstr, body.context);
|
|
98
|
+
await updateTranslation(filePath, msgid, body.msgstr, body.context);
|
|
99
99
|
sendJson(res, { success: true, message: 'Translation updated' });
|
|
100
100
|
}
|
|
101
101
|
catch (error) {
|
|
@@ -107,7 +107,7 @@ export function createApiMiddleware(options) {
|
|
|
107
107
|
if (path === '/search' && method === 'GET') {
|
|
108
108
|
const query = url.searchParams.get('q')?.toLowerCase() || '';
|
|
109
109
|
const lang = url.searchParams.get('lang');
|
|
110
|
-
const languages = findPoFiles(localesDir);
|
|
110
|
+
const languages = await findPoFiles(localesDir);
|
|
111
111
|
const results = [];
|
|
112
112
|
for (const language of languages) {
|
|
113
113
|
if (lang && language.code !== lang)
|
|
@@ -2,23 +2,23 @@ import type { Translation, Language, LanguageStats } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Parse a .po file and extract translations
|
|
4
4
|
*/
|
|
5
|
-
export declare function parsePoFile(filePath: string): Translation[]
|
|
5
|
+
export declare function parsePoFile(filePath: string): Promise<Translation[]>;
|
|
6
6
|
/**
|
|
7
7
|
* Save translations back to a .po file
|
|
8
8
|
*/
|
|
9
|
-
export declare function savePoFile(filePath: string, updates: Translation[]): void
|
|
9
|
+
export declare function savePoFile(filePath: string, updates: Translation[]): Promise<void>;
|
|
10
10
|
/**
|
|
11
11
|
* Update a single translation
|
|
12
12
|
*/
|
|
13
|
-
export declare function updateTranslation(filePath: string, msgid: string, msgstr: string, context?: string): void
|
|
13
|
+
export declare function updateTranslation(filePath: string, msgid: string, msgstr: string, context?: string): Promise<void>;
|
|
14
14
|
/**
|
|
15
15
|
* Find all .po files in a directory
|
|
16
16
|
*/
|
|
17
|
-
export declare function findPoFiles(localesDir: string): Language[]
|
|
17
|
+
export declare function findPoFiles(localesDir: string): Promise<Language[]>;
|
|
18
18
|
/**
|
|
19
19
|
* Get language statistics for all languages
|
|
20
20
|
*/
|
|
21
|
-
export declare function getLanguageStats(localesDir: string): LanguageStats[]
|
|
21
|
+
export declare function getLanguageStats(localesDir: string): Promise<LanguageStats[]>;
|
|
22
22
|
/**
|
|
23
23
|
* Get a human-readable language name from a locale code
|
|
24
24
|
*/
|
package/dist/plugin/po-parser.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { po } from 'gettext-parser';
|
|
2
|
-
import {
|
|
2
|
+
import { readFile, writeFile, readdir } from 'fs/promises';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
3
4
|
import { join, basename } from 'path';
|
|
4
5
|
/**
|
|
5
6
|
* Parse a .po file and extract translations
|
|
6
7
|
*/
|
|
7
|
-
export function parsePoFile(filePath) {
|
|
8
|
+
export async function parsePoFile(filePath) {
|
|
8
9
|
if (!existsSync(filePath)) {
|
|
9
10
|
throw new Error(`File not found: ${filePath}`);
|
|
10
11
|
}
|
|
11
|
-
const content =
|
|
12
|
+
const content = await readFile(filePath);
|
|
12
13
|
const parsed = po.parse(content);
|
|
13
14
|
const translations = [];
|
|
14
15
|
for (const [context, messages] of Object.entries(parsed.translations)) {
|
|
@@ -30,11 +31,11 @@ export function parsePoFile(filePath) {
|
|
|
30
31
|
/**
|
|
31
32
|
* Save translations back to a .po file
|
|
32
33
|
*/
|
|
33
|
-
export function savePoFile(filePath, updates) {
|
|
34
|
+
export async function savePoFile(filePath, updates) {
|
|
34
35
|
if (!existsSync(filePath)) {
|
|
35
36
|
throw new Error(`File not found: ${filePath}`);
|
|
36
37
|
}
|
|
37
|
-
const content =
|
|
38
|
+
const content = await readFile(filePath);
|
|
38
39
|
const parsed = po.parse(content);
|
|
39
40
|
for (const update of updates) {
|
|
40
41
|
const context = update.context || '';
|
|
@@ -54,26 +55,26 @@ export function savePoFile(filePath, updates) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
const compiled = po.compile(parsed);
|
|
57
|
-
|
|
58
|
+
await writeFile(filePath, compiled);
|
|
58
59
|
}
|
|
59
60
|
/**
|
|
60
61
|
* Update a single translation
|
|
61
62
|
*/
|
|
62
|
-
export function updateTranslation(filePath, msgid, msgstr, context) {
|
|
63
|
-
savePoFile(filePath, [{ msgid, msgstr, context }]);
|
|
63
|
+
export async function updateTranslation(filePath, msgid, msgstr, context) {
|
|
64
|
+
await savePoFile(filePath, [{ msgid, msgstr, context }]);
|
|
64
65
|
}
|
|
65
66
|
/**
|
|
66
67
|
* Find all .po files in a directory
|
|
67
68
|
*/
|
|
68
|
-
export function findPoFiles(localesDir) {
|
|
69
|
+
export async function findPoFiles(localesDir) {
|
|
69
70
|
if (!existsSync(localesDir)) {
|
|
70
71
|
return [];
|
|
71
72
|
}
|
|
72
|
-
const files =
|
|
73
|
-
return files.map((file) => {
|
|
73
|
+
const files = (await readdir(localesDir)).filter((f) => f.endsWith('.po'));
|
|
74
|
+
return Promise.all(files.map(async (file) => {
|
|
74
75
|
const filePath = join(localesDir, file);
|
|
75
76
|
const code = basename(file, '.po');
|
|
76
|
-
const translations = parsePoFile(filePath);
|
|
77
|
+
const translations = await parsePoFile(filePath);
|
|
77
78
|
const translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;
|
|
78
79
|
const fuzzy = translations.filter((t) => t.fuzzy).length;
|
|
79
80
|
return {
|
|
@@ -87,13 +88,13 @@ export function findPoFiles(localesDir) {
|
|
|
87
88
|
fuzzy
|
|
88
89
|
}
|
|
89
90
|
};
|
|
90
|
-
});
|
|
91
|
+
}));
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
93
94
|
* Get language statistics for all languages
|
|
94
95
|
*/
|
|
95
|
-
export function getLanguageStats(localesDir) {
|
|
96
|
-
const languages = findPoFiles(localesDir);
|
|
96
|
+
export async function getLanguageStats(localesDir) {
|
|
97
|
+
const languages = await findPoFiles(localesDir);
|
|
97
98
|
return languages.map((lang) => ({
|
|
98
99
|
code: lang.code,
|
|
99
100
|
name: lang.name,
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface PluginOptions {
|
|
|
15
15
|
provider: 'openai' | 'anthropic' | 'google';
|
|
16
16
|
apiKey?: string;
|
|
17
17
|
};
|
|
18
|
+
/** Restart the dev server when a .po file is updated (default: false) */
|
|
19
|
+
restartOnPoChange?: boolean;
|
|
20
|
+
/** Trigger a full page reload when a .po file is updated (default: true) */
|
|
21
|
+
reloadOnPoChange?: boolean;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Represents a single translation entry
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-lingo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Visual translation editor for .po files in Vite projects",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Michael-Obele",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"build": "bun run build:svelte && bun run build:plugin && bun run build:ui && publint",
|
|
20
20
|
"build:netlify": "vite build",
|
|
21
21
|
"build:ui": "vite build --config vite.ui.config.ts",
|
|
22
|
+
"sync-version": "npm version $(npm view vite-plugin-lingo version --registry=https://registry.npmjs.org/) --no-git-tag-version",
|
|
22
23
|
"build:ui:watch": "vite build --config vite.ui.config.ts --watch",
|
|
23
24
|
"build:svelte": "svelte-kit sync && svelte-package",
|
|
24
25
|
"build:plugin": "tsup",
|