vite-plugin-lingo 0.0.1

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