vite-plugin-lingo 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin/index.cjs +48 -38
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.js +60 -48
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/middleware.js +5 -5
- package/dist/plugin/po-parser.d.ts +5 -5
- package/dist/plugin/po-parser.js +16 -15
- package/package.json +2 -1
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,
|
|
@@ -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,9 +289,8 @@ 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
295
|
const { route = "/_translations", localesDir = "./locales", production = false } = options;
|
|
288
296
|
let root;
|
|
@@ -437,7 +445,9 @@ function lingoPlugin(options = {}) {
|
|
|
437
445
|
const originalPrintUrls = server.printUrls;
|
|
438
446
|
server.printUrls = () => {
|
|
439
447
|
originalPrintUrls?.();
|
|
440
|
-
console.log(
|
|
448
|
+
console.log(
|
|
449
|
+
` \x1B[32m\u279C\x1B[0m \x1B[1mLingo:\x1B[0m ${protocol}://${hostString}:${port}${cleanRoute}`
|
|
450
|
+
);
|
|
441
451
|
};
|
|
442
452
|
}
|
|
443
453
|
};
|
|
@@ -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 = {}): 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\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})\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(\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 {\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 (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\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\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.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
9
|
import { resolve, join as join3, 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,
|
|
@@ -160,7 +170,7 @@ function createApiMiddleware(options) {
|
|
|
160
170
|
const { localesDir } = 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,8 +255,8 @@ 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
261
|
const { route = "/_translations", localesDir = "./locales", production = false } = options;
|
|
252
262
|
let root;
|
|
@@ -384,14 +394,14 @@ function lingoPlugin(options = {}) {
|
|
|
384
394
|
);
|
|
385
395
|
const poGlob = join3(resolvedLocalesDir, "**/*.po");
|
|
386
396
|
server.watcher.add(poGlob);
|
|
387
|
-
server.watcher.on("change", (
|
|
388
|
-
if (
|
|
397
|
+
server.watcher.on("change", (path2) => {
|
|
398
|
+
if (path2.endsWith(".po")) {
|
|
389
399
|
server.ws.send({
|
|
390
400
|
type: "custom",
|
|
391
401
|
event: "lingo:po-updated",
|
|
392
|
-
data: { path }
|
|
402
|
+
data: { path: path2 }
|
|
393
403
|
});
|
|
394
|
-
console.log(`[lingo] .po file updated: ${
|
|
404
|
+
console.log(`[lingo] .po file updated: ${path2}`);
|
|
395
405
|
}
|
|
396
406
|
});
|
|
397
407
|
const port = server.config.server.port || 5173;
|
|
@@ -401,7 +411,9 @@ function lingoPlugin(options = {}) {
|
|
|
401
411
|
const originalPrintUrls = server.printUrls;
|
|
402
412
|
server.printUrls = () => {
|
|
403
413
|
originalPrintUrls?.();
|
|
404
|
-
console.log(
|
|
414
|
+
console.log(
|
|
415
|
+
` \x1B[32m\u279C\x1B[0m \x1B[1mLingo:\x1B[0m ${protocol}://${hostString}:${port}${cleanRoute}`
|
|
416
|
+
);
|
|
405
417
|
};
|
|
406
418
|
}
|
|
407
419
|
};
|
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 = {}): 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\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})\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(\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 {\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 (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\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\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":["join","existsSync","fileURLToPath","join","resolve","path","join","__filename","fileURLToPath","existsSync","join","path"]}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-lingo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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",
|