vite-plugin-lingo 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -1
- package/dist/plugin/index.cjs +30 -15
- package/dist/plugin/index.cjs.map +1 -1
- package/dist/plugin/index.d.cts +5 -3
- package/dist/plugin/index.d.ts +5 -3
- package/dist/plugin/index.js +31 -16
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/middleware.d.ts +2 -0
- package/dist/plugin/middleware.js +1 -1
- package/dist/plugin/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,9 +98,30 @@ lingo({
|
|
|
98
98
|
// For other frameworks, './locales' at project root is typical
|
|
99
99
|
localesDir: './locales',
|
|
100
100
|
|
|
101
|
+
// ⚠️ NUCLEAR OPTION - Only use if another plugin conflicts with .po file changes
|
|
102
|
+
// Restart the dev server when a .po file is updated (default: false)
|
|
103
|
+
// Advanced: Use this only when reloadOnPoChange is insufficient and another
|
|
104
|
+
// plugin (like wuchale) stops reacting to changes. Most users won't need this.
|
|
105
|
+
restartOnPoChange: false,
|
|
106
|
+
|
|
107
|
+
// Trigger a full page reload when a .po file is updated (default: true)
|
|
108
|
+
// Ensures UI stays in sync with backend translation files
|
|
109
|
+
reloadOnPoChange: true,
|
|
110
|
+
|
|
101
111
|
// Enable in production (default: false)
|
|
102
112
|
// ⚠️ Only enable with proper authentication!
|
|
103
113
|
production: false,
|
|
114
|
+
|
|
115
|
+
// 🔒 PREMIUM FEATURE (Coming Soon)
|
|
116
|
+
// License key for premium features
|
|
117
|
+
// licenseKey: 'your-license-key',
|
|
118
|
+
|
|
119
|
+
// 🔒 PREMIUM FEATURE (Coming Soon)
|
|
120
|
+
// AI configuration for translation assistance
|
|
121
|
+
// ai: {
|
|
122
|
+
// provider: 'openai' | 'anthropic' | 'google',
|
|
123
|
+
// apiKey: 'your-api-key'
|
|
124
|
+
// },
|
|
104
125
|
})
|
|
105
126
|
```
|
|
106
127
|
|
|
@@ -112,10 +133,104 @@ lingo({
|
|
|
112
133
|
|--------|------|---------|-------------|
|
|
113
134
|
| `route` | `string` | `'/_translations'` | URL path where the editor is served |
|
|
114
135
|
| `localesDir` | `string` | `'./locales'` | Directory containing `.po` files. For SvelteKit projects, commonly `'./src/locales'`. Relative to project root. |
|
|
115
|
-
| `
|
|
136
|
+
| `restartOnPoChange` | `boolean` | `false` | **Nuclear Option** ⚠️ - Only use when another plugin conflicts with `.po` file changes (e.g., wuchale). Restarts the dev server when a `.po` file is updated. Most users won't need this. |
|
|
137
|
+
| `reloadOnPoChange` | `boolean` | `true` | Trigger a full page reload when a `.po` file is updated. Ensures UI stays in sync with backend translation files. |
|
|
138
|
+
| `production` | `boolean` | `false` | Enable editor in production builds. ⚠️ Only enable with proper authentication! |
|
|
139
|
+
| `licenseKey` | `string` | `undefined` | **Coming Soon** 🔒 - License key for premium features (not yet available). |
|
|
140
|
+
| `ai` | `object` | `undefined` | **Coming Soon** 🔒 - AI configuration for translation assistance (not yet available). Will support `'openai'`, `'anthropic'`, or `'google'` as provider with optional `apiKey`. |
|
|
141
|
+
|
|
142
|
+
### Premium Features (Coming Soon)
|
|
143
|
+
|
|
144
|
+
The following premium features are currently in development and will be available in future releases:
|
|
145
|
+
|
|
146
|
+
#### License Key System (`licenseKey`)
|
|
147
|
+
- **Status**: Under development
|
|
148
|
+
- **Purpose**: Enable premium features with license validation
|
|
149
|
+
- **Expected**: Q1 2026
|
|
150
|
+
|
|
151
|
+
#### AI-Powered Translation (`ai`)
|
|
152
|
+
- **Status**: Under development
|
|
153
|
+
- **Purpose**: Assist with translations using your preferred AI provider
|
|
154
|
+
- **Supported Providers**: OpenAI, Anthropic, Google
|
|
155
|
+
- **Expected**: Q1 2026
|
|
156
|
+
|
|
157
|
+
### Advanced Configuration Notes
|
|
158
|
+
|
|
159
|
+
#### `restartOnPoChange` - Nuclear Option
|
|
160
|
+
This is an **advanced fallback option** that should only be used in specific situations:
|
|
161
|
+
|
|
162
|
+
- ✅ **Use when**: Another plugin (like [wuchale](https://wuchale.dev/)) doesn't respond to `.po` file changes
|
|
163
|
+
- ❌ **Don't use**: As your primary reload strategy (use `reloadOnPoChange` instead)
|
|
164
|
+
- ⚠️ **Impact**: Full dev server restart is slower than page reload
|
|
165
|
+
|
|
166
|
+
**Example scenario** where this might be needed:
|
|
167
|
+
```ts
|
|
168
|
+
lingo({
|
|
169
|
+
localesDir: './locales',
|
|
170
|
+
restartOnPoChange: true, // Only if wuchale or other plugins conflict
|
|
171
|
+
reloadOnPoChange: false, // Disable the default fast reload
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Type Definitions
|
|
176
|
+
|
|
177
|
+
#### PluginOptions
|
|
178
|
+
Main configuration interface for the Vite plugin. See **Plugin Options** table above.
|
|
179
|
+
|
|
180
|
+
#### Translation
|
|
181
|
+
Represents a single translation entry in a `.po` file:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
interface Translation {
|
|
185
|
+
msgid: string; // Message ID (original text)
|
|
186
|
+
msgstr: string; // Message string (translated text)
|
|
187
|
+
context?: string; // Optional context for disambiguation
|
|
188
|
+
comments?: { // Optional metadata
|
|
189
|
+
reference?: string; // File reference where string is used
|
|
190
|
+
translator?: string; // Translator notes
|
|
191
|
+
extracted?: string; // Extracted comments
|
|
192
|
+
flag?: string; // Fuzzy or other flags
|
|
193
|
+
};
|
|
194
|
+
fuzzy?: boolean; // Whether translation is marked as fuzzy
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### Language
|
|
199
|
+
Represents a language with all its translations:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
interface Language {
|
|
203
|
+
code: string; // Language code (e.g., 'en', 'es', 'fr')
|
|
204
|
+
name: string; // Language name (e.g., 'English', 'Spanish')
|
|
205
|
+
path: string; // Path to the .po file
|
|
206
|
+
translations: Translation[]; // Array of translation entries
|
|
207
|
+
progress: {
|
|
208
|
+
total: number; // Total number of strings
|
|
209
|
+
translated: number; // Number of translated strings
|
|
210
|
+
fuzzy: number; // Number of fuzzy translations
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### LanguageStats
|
|
216
|
+
Statistics for language translation progress:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
interface LanguageStats {
|
|
220
|
+
code: string; // Language code
|
|
221
|
+
name: string; // Language name
|
|
222
|
+
total: number; // Total number of strings
|
|
223
|
+
translated: number; // Number of translated strings
|
|
224
|
+
fuzzy: number; // Number of fuzzy translations
|
|
225
|
+
untranslated: number; // Number of untranslated strings
|
|
226
|
+
progress: number; // Progress percentage (0-100)
|
|
227
|
+
}
|
|
228
|
+
```
|
|
116
229
|
|
|
117
230
|
### Exported Types
|
|
118
231
|
|
|
232
|
+
All types are exported from the main package:
|
|
233
|
+
|
|
119
234
|
```ts
|
|
120
235
|
import type {
|
|
121
236
|
PluginOptions,
|
package/dist/plugin/index.cjs
CHANGED
|
@@ -201,7 +201,7 @@ function sendError(res, message, statusCode = 500) {
|
|
|
201
201
|
sendJson(res, { success: false, error: message }, statusCode);
|
|
202
202
|
}
|
|
203
203
|
function createApiMiddleware(options) {
|
|
204
|
-
const { localesDir } = options;
|
|
204
|
+
const { localesDir, server } = options;
|
|
205
205
|
return async (req, res, next) => {
|
|
206
206
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
207
207
|
const path = url.pathname;
|
|
@@ -292,7 +292,13 @@ function createApiMiddleware(options) {
|
|
|
292
292
|
var __filename2 = typeof __dirname !== "undefined" ? "" : (0, import_url.fileURLToPath)(importMetaUrl);
|
|
293
293
|
var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : (0, import_path3.dirname)(__filename2);
|
|
294
294
|
function lingoPlugin(options = {}) {
|
|
295
|
-
const {
|
|
295
|
+
const {
|
|
296
|
+
route = "/_translations",
|
|
297
|
+
localesDir = "./locales",
|
|
298
|
+
production = false,
|
|
299
|
+
restartOnPoChange = false,
|
|
300
|
+
reloadOnPoChange = true
|
|
301
|
+
} = options;
|
|
296
302
|
let root;
|
|
297
303
|
let resolvedLocalesDir;
|
|
298
304
|
return {
|
|
@@ -303,6 +309,26 @@ function lingoPlugin(options = {}) {
|
|
|
303
309
|
root = config.root;
|
|
304
310
|
resolvedLocalesDir = (0, import_path3.resolve)(root, localesDir);
|
|
305
311
|
},
|
|
312
|
+
handleHotUpdate({ file, server }) {
|
|
313
|
+
if (file.endsWith(".po")) {
|
|
314
|
+
console.log(`[lingo] .po file updated: ${file}`);
|
|
315
|
+
server.ws.send({
|
|
316
|
+
type: "custom",
|
|
317
|
+
event: "lingo:po-updated",
|
|
318
|
+
data: { path: file }
|
|
319
|
+
});
|
|
320
|
+
if (restartOnPoChange) {
|
|
321
|
+
console.log("[lingo] Restarting dev server...");
|
|
322
|
+
server.restart();
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
if (reloadOnPoChange) {
|
|
326
|
+
console.log("[lingo] Triggering full page reload...");
|
|
327
|
+
server.ws.send({ type: "full-reload" });
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
},
|
|
306
332
|
configureServer(server) {
|
|
307
333
|
const cleanRoute = route.replace(/\/$/, "");
|
|
308
334
|
let uiPath = (0, import_path3.resolve)(__dirnameComputed, "../ui-dist");
|
|
@@ -423,21 +449,10 @@ function lingoPlugin(options = {}) {
|
|
|
423
449
|
`${cleanRoute}/api`,
|
|
424
450
|
createApiMiddleware({
|
|
425
451
|
localesDir: resolvedLocalesDir,
|
|
426
|
-
root
|
|
452
|
+
root,
|
|
453
|
+
server
|
|
427
454
|
})
|
|
428
455
|
);
|
|
429
|
-
const poGlob = (0, import_path3.join)(resolvedLocalesDir, "**/*.po");
|
|
430
|
-
server.watcher.add(poGlob);
|
|
431
|
-
server.watcher.on("change", (path) => {
|
|
432
|
-
if (path.endsWith(".po")) {
|
|
433
|
-
server.ws.send({
|
|
434
|
-
type: "custom",
|
|
435
|
-
event: "lingo:po-updated",
|
|
436
|
-
data: { path }
|
|
437
|
-
});
|
|
438
|
-
console.log(`[lingo] .po file updated: ${path}`);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
456
|
const port = server.config.server.port || 5173;
|
|
442
457
|
const protocol = server.config.server.https ? "https" : "http";
|
|
443
458
|
const host = server.config.server.host || "localhost";
|
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/plugin/index.ts","../../node_modules/tsup/assets/cjs_shims.js","../../src/lib/plugin/middleware.ts","../../src/lib/plugin/po-parser.ts"],"sourcesContent":["import type { Plugin, ViteDevServer } from 'vite';\nimport { resolve, join, dirname } from 'path';\nimport { existsSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport sirv from 'sirv';\nimport type { PluginOptions } from './types.js';\nimport { createApiMiddleware } from './middleware.js';\n\n// Compute __dirname for ESM (CJS already has it)\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - import.meta.url works in ESM, undefined in CJS\nconst __filename = typeof __dirname !== 'undefined' ? '' : fileURLToPath(import.meta.url);\nconst __dirnameComputed = typeof __dirname !== 'undefined' ? __dirname : dirname(__filename);\n\n// Re-export types\nexport type { PluginOptions, Translation, Language, LanguageStats } from './types.js';\n\n/**\n * vite-plugin-lingo - Visual translation editor for .po files\n *\n * @example\n * ```ts\n * // vite.config.ts (default setup)\n * import { defineConfig } from 'vite';\n * import lingo from 'vite-plugin-lingo';\n *\n * export default defineConfig({\n * plugins: [\n * lingo({\n * route: '/_translations',\n * localesDir: './locales'\n * })\n * ]\n * });\n * ```\n *\n * @example\n * ```ts\n * // vite.config.ts (SvelteKit convention)\n * import { defineConfig } from 'vite';\n * import lingo from 'vite-plugin-lingo';\n *\n * export default defineConfig({\n * plugins: [\n * lingo({\n * route: '/_translations',\n * localesDir: './src/locales' // Common in SvelteKit\n * })\n * ]\n * });\n * ```\n */\nexport default function lingoPlugin(options: PluginOptions = {}): any {\n\tconst {\n\t\troute = '/_translations',\n\t\tlocalesDir = './locales',\n\t\tproduction = false,\n\t\trestartOnPoChange = false,\n\t\treloadOnPoChange = true\n\t} = options;\n\n\tlet root: string;\n\tlet resolvedLocalesDir: string;\n\n\treturn {\n\t\tname: 'vite-plugin-lingo',\n\n\t\t// Only apply in serve mode (unless production is enabled)\n\t\tapply: production ? undefined : 'serve',\n\n\t\tconfigResolved(config: any) {\n\t\t\troot = config.root;\n\t\t\tresolvedLocalesDir = resolve(root, localesDir);\n\t\t},\n\n\t\thandleHotUpdate({ file, server }: { file: string; server: ViteDevServer }) {\n\t\t\tif (file.endsWith('.po')) {\n\t\t\t\tconsole.log(`[lingo] .po file updated: ${file}`);\n\n\t\t\t\t// Notify connected clients via WebSocket\n\t\t\t\tserver.ws.send({\n\t\t\t\t\ttype: 'custom',\n\t\t\t\t\tevent: 'lingo:po-updated',\n\t\t\t\t\tdata: { path: file }\n\t\t\t\t});\n\n\t\t\t\tif (restartOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Restarting dev server...');\n\t\t\t\t\tserver.restart();\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\n\t\t\t\tif (reloadOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Triggering full page reload...');\n\t\t\t\t\tserver.ws.send({ type: 'full-reload' });\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tconfigureServer(server: ViteDevServer) {\n\t\t\t// Ensure the route doesn't have trailing slash\n\t\t\tconst cleanRoute = route.replace(/\\/$/, '');\n\n\t\t\t// Find the built UI assets\n\t\t\t// When running from source (dev): __dirnameComputed is src/lib/plugin, UI is at dist/ui-dist\n\t\t\t// When running from dist (published): __dirnameComputed is dist/plugin, UI is at ../ui-dist\n\t\t\tlet uiPath = resolve(__dirnameComputed, '../ui-dist');\n\n\t\t\t// If not found relative to __dirname, try from project root\n\t\t\tif (!existsSync(uiPath)) {\n\t\t\t\tuiPath = resolve(root, 'dist/ui-dist');\n\t\t\t}\n\n\t\t\tconsole.log('[lingo] Looking for UI at:', uiPath);\n\t\t\tconsole.log('[lingo] UI exists:', existsSync(uiPath));\n\n\t\t\t// Serve the editor UI if built assets exist\n\t\t\tif (existsSync(uiPath)) {\n\t\t\t\tconsole.log('[lingo] Serving built UI from:', uiPath);\n\t\t\t\tconst serve = sirv(uiPath, {\n\t\t\t\t\tdev: true,\n\t\t\t\t\tsingle: true // SPA mode\n\t\t\t\t});\n\n\t\t\t\tserver.middlewares.use(cleanRoute, (req, res, next) => {\n\t\t\t\t\t// Redirect to add trailing slash for base route (ensures relative paths work)\n\t\t\t\t\t// When mounted at /path, req.url is already stripped of the prefix\n\t\t\t\t\t// But the browser URL matters for relative path resolution\n\t\t\t\t\tconst reqWithOriginal = req as typeof req & { originalUrl?: string };\n\t\t\t\t\tif (\n\t\t\t\t\t\t(req.url === '/' || req.url === '') &&\n\t\t\t\t\t\treqWithOriginal.originalUrl &&\n\t\t\t\t\t\t!reqWithOriginal.originalUrl.endsWith('/')\n\t\t\t\t\t) {\n\t\t\t\t\t\tres.writeHead(302, { Location: cleanRoute + '/' });\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle favicon.ico requests with an empty response to prevent 404\n\t\t\t\t\tif (req.url === '/favicon.ico') {\n\t\t\t\t\t\tres.statusCode = 204;\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip API routes (handled by separate middleware)\n\t\t\t\t\tif (req.url?.startsWith('/api')) {\n\t\t\t\t\t\treturn next();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Serve static files with sirv\n\t\t\t\t\t// req.url is already stripped of the cleanRoute prefix by Connect\n\t\t\t\t\tserve(req, res, () => next());\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Development mode: serve a simple placeholder\n\t\t\t\tserver.middlewares.use(cleanRoute, (req, res, next) => {\n\t\t\t\t\t// Handle favicon.ico requests with an empty response to prevent 404\n\t\t\t\t\tif (req.url === '/favicon.ico') {\n\t\t\t\t\t\tres.statusCode = 204;\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (req.url?.startsWith('/api')) {\n\t\t\t\t\t\treturn next();\n\t\t\t\t\t}\n\n\t\t\t\t\tres.setHeader('Content-Type', 'text/html');\n\t\t\t\t\tres.end(`\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>🌍 Lingo Translation Editor</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; min-height: 100vh; }\n .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }\n header { background: white; padding: 1rem 2rem; border-bottom: 1px solid #e0e0e0; margin-bottom: 2rem; }\n h1 { font-size: 1.5rem; color: #333; }\n .card { background: white; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n .language { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; border-bottom: 1px solid #eee; }\n .language:last-child { border-bottom: none; }\n .progress { background: #e0e0e0; border-radius: 4px; height: 8px; width: 100px; overflow: hidden; }\n .progress-bar { background: #4caf50; height: 100%; transition: width 0.3s; }\n .loading { text-align: center; padding: 2rem; color: #666; }\n .error { color: #f44336; }\n </style>\n</head>\n<body>\n <header>\n <h1>🌍 Lingo Translation Editor</h1>\n </header>\n <div class=\"container\">\n <div class=\"card\">\n <h2 style=\"margin-bottom: 1rem;\">Languages</h2>\n <div id=\"languages\" class=\"loading\">Loading...</div>\n </div>\n </div>\n <script>\n async function loadLanguages() {\n try {\n const res = await fetch('${cleanRoute}/api/languages');\n const { data, error } = await res.json();\n \n if (error) throw new Error(error);\n \n const container = document.getElementById('languages');\n if (!data || data.length === 0) {\n container.innerHTML = '<p>No .po files found in the locales directory.</p>';\n return;\n }\n \n container.innerHTML = data.map(lang => \\`\n <div class=\"language\">\n <div>\n <strong>\\${lang.name}</strong>\n <span style=\"color: #666; margin-left: 0.5rem;\">(\\${lang.code})</span>\n </div>\n <div style=\"display: flex; align-items: center; gap: 1rem;\">\n <span>\\${lang.translated}/\\${lang.total} translated</span>\n <div class=\"progress\">\n <div class=\"progress-bar\" style=\"width: \\${lang.progress}%\"></div>\n </div>\n </div>\n </div>\n \\`).join('');\n } catch (err) {\n document.getElementById('languages').innerHTML = \n '<p class=\"error\">Error loading languages: ' + err.message + '</p>';\n }\n }\n \n loadLanguages();\n </script>\n</body>\n</html>\n `);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// API endpoints\n\t\t\tserver.middlewares.use(\n\t\t\t\t`${cleanRoute}/api`,\n\t\t\t\tcreateApiMiddleware({\n\t\t\t\t\tlocalesDir: resolvedLocalesDir,\n\t\t\t\t\troot,\n\t\t\t\t\tserver\n\t\t\t\t})\n\t\t\t);\n\n\t\t\t// Log startup message\n\t\t\tconst port = server.config.server.port || 5173;\n\n\t\t\tconst protocol = server.config.server.https ? 'https' : 'http';\n\t\t\tconst host = server.config.server.host || 'localhost';\n\t\t\tconst hostString = typeof host === 'string' ? host : 'localhost';\n\n\t\t\t// Use server.printUrls hook for better timing\n\t\t\tconst originalPrintUrls = server.printUrls;\n\t\t\tserver.printUrls = () => {\n\t\t\t\toriginalPrintUrls?.();\n\t\t\t\tconsole.log(\n\t\t\t\t\t` \\x1b[32m➜\\x1b[0m \\x1b[1mLingo:\\x1b[0m ${protocol}://${hostString}:${port}${cleanRoute}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\t};\n}\n\n// Named export for convenience\nexport { lingoPlugin };\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import type { IncomingMessage, ServerResponse } from 'http';\nimport { join } from 'path';\nimport type { ViteDevServer } from 'vite';\nimport {\n\tfindPoFiles,\n\tparsePoFile,\n\tsavePoFile,\n\tgetLanguageStats,\n\tupdateTranslation\n} from './po-parser.js';\nimport type { Translation } from './types.js';\n\ninterface MiddlewareOptions {\n\tlocalesDir: string;\n\troot: string;\n\tserver?: ViteDevServer;\n}\n\n/**\n * Parse the request body as JSON\n */\nasync function parseBody<T>(req: IncomingMessage): Promise<T> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet body = '';\n\t\treq.on('data', (chunk) => (body += chunk));\n\t\treq.on('end', () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(body));\n\t\t\t} catch {\n\t\t\t\treject(new Error('Invalid JSON'));\n\t\t\t}\n\t\t});\n\t\treq.on('error', reject);\n\t});\n}\n\n/**\n * Send JSON response\n */\nfunction sendJson(res: ServerResponse, data: unknown, statusCode = 200): void {\n\tres.statusCode = statusCode;\n\tres.setHeader('Content-Type', 'application/json');\n\tres.end(JSON.stringify(data));\n}\n\n/**\n * Send error response\n */\nfunction sendError(res: ServerResponse, message: string, statusCode = 500): void {\n\tsendJson(res, { success: false, error: message }, statusCode);\n}\n\n/**\n * Create API middleware for handling translation operations\n */\nexport function createApiMiddleware(options: MiddlewareOptions) {\n\tconst { localesDir, server } = options;\n\n\treturn async (req: IncomingMessage, res: ServerResponse, next: () => void): Promise<void> => {\n\t\tconst url = new URL(req.url || '/', `http://${req.headers.host}`);\n\t\tconst path = url.pathname;\n\t\tconst method = req.method?.toUpperCase();\n\n\t\t// Enable CORS for development\n\t\tres.setHeader('Access-Control-Allow-Origin', '*');\n\t\tres.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n\t\tres.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n\t\t// Handle preflight\n\t\tif (method === 'OPTIONS') {\n\t\t\tres.statusCode = 204;\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// GET /api/languages - List all languages with stats\n\t\t\tif (path === '/languages' && method === 'GET') {\n\t\t\t\tconst stats = await getLanguageStats(localesDir);\n\t\t\t\tsendJson(res, { success: true, data: stats });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// GET /api/translations/:lang - Get all translations for a language\n\t\t\tconst translationsMatch = path.match(/^\\/translations\\/([^/]+)$/);\n\t\t\tif (translationsMatch && method === 'GET') {\n\t\t\t\tconst langCode = translationsMatch[1];\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst translations = await parsePoFile(filePath);\n\t\t\t\t\tsendJson(res, { success: true, data: translations });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, `Language not found: ${langCode}`, 404);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// PUT /api/translations/:lang - Update translations for a language\n\t\t\tif (translationsMatch && method === 'PUT') {\n\t\t\t\tconst langCode = translationsMatch[1];\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await parseBody<Translation | Translation[]>(req);\n\t\t\t\t\tconst updates = Array.isArray(body) ? body : [body];\n\t\t\t\t\tawait savePoFile(filePath, updates);\n\n\t\t\t\t\tsendJson(res, { success: true, message: 'Translations updated' });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, error instanceof Error ? error.message : 'Failed to update', 400);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// PUT /api/translation/:lang/:msgid - Update a single translation\n\t\t\tconst singleMatch = path.match(/^\\/translation\\/([^/]+)\\/(.+)$/);\n\t\t\tif (singleMatch && method === 'PUT') {\n\t\t\t\tconst langCode = singleMatch[1];\n\t\t\t\tconst msgid = decodeURIComponent(singleMatch[2]);\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await parseBody<{ msgstr: string; context?: string }>(req);\n\t\t\t\t\tawait updateTranslation(filePath, msgid, body.msgstr, body.context);\n\n\t\t\t\t\tsendJson(res, { success: true, message: 'Translation updated' });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, error instanceof Error ? error.message : 'Failed to update', 400);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// GET /api/search - Search translations across languages\n\t\t\tif (path === '/search' && method === 'GET') {\n\t\t\t\tconst query = url.searchParams.get('q')?.toLowerCase() || '';\n\t\t\t\tconst lang = url.searchParams.get('lang');\n\n\t\t\t\tconst languages = await findPoFiles(localesDir);\n\t\t\t\tconst results: Array<{\n\t\t\t\t\tlang: string;\n\t\t\t\t\tmsgid: string;\n\t\t\t\t\tmsgstr: string;\n\t\t\t\t\tcontext?: string;\n\t\t\t\t}> = [];\n\n\t\t\t\tfor (const language of languages) {\n\t\t\t\t\tif (lang && language.code !== lang) continue;\n\n\t\t\t\t\tfor (const t of language.translations) {\n\t\t\t\t\t\tif (t.msgid.toLowerCase().includes(query) || t.msgstr.toLowerCase().includes(query)) {\n\t\t\t\t\t\t\tresults.push({\n\t\t\t\t\t\t\t\tlang: language.code,\n\t\t\t\t\t\t\t\tmsgid: t.msgid,\n\t\t\t\t\t\t\t\tmsgstr: t.msgstr,\n\t\t\t\t\t\t\t\tcontext: t.context\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsendJson(res, { success: true, data: results });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Not found\n\t\t\tsendError(res, 'Not found', 404);\n\t\t} catch (error) {\n\t\t\tconsole.error('[lingo] API error:', error);\n\t\t\tsendError(res, error instanceof Error ? error.message : 'Internal server error');\n\t\t}\n\t};\n}\n","import { po } from 'gettext-parser';\nimport { readFile, writeFile, readdir } from 'fs/promises';\nimport { existsSync } from 'fs';\nimport { join, basename } from 'path';\nimport type { Translation, Language, LanguageStats } from './types.js';\n\n/**\n * Parse a .po file and extract translations\n */\nexport async function parsePoFile(filePath: string): Promise<Translation[]> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(filePath);\n\tconst parsed = po.parse(content);\n\n\tconst translations: Translation[] = [];\n\n\tfor (const [context, messages] of Object.entries(parsed.translations)) {\n\t\tfor (const [msgid, data] of Object.entries(messages as Record<string, unknown>)) {\n\t\t\tif (!msgid) continue; // Skip header entry\n\n\t\t\tconst entry = data as {\n\t\t\t\tmsgid: string;\n\t\t\t\tmsgstr?: string[];\n\t\t\t\tcomments?: {\n\t\t\t\t\treference?: string;\n\t\t\t\t\ttranslator?: string;\n\t\t\t\t\textracted?: string;\n\t\t\t\t\tflag?: string;\n\t\t\t\t};\n\t\t\t};\n\n\t\t\ttranslations.push({\n\t\t\t\tmsgid,\n\t\t\t\tmsgstr: entry.msgstr?.[0] || '',\n\t\t\t\tcontext: context || undefined,\n\t\t\t\tcomments: entry.comments,\n\t\t\t\tfuzzy: entry.comments?.flag?.includes('fuzzy') || false\n\t\t\t});\n\t\t}\n\t}\n\n\treturn translations;\n}\n\n/**\n * Save translations back to a .po file\n */\nexport async function savePoFile(filePath: string, updates: Translation[]): Promise<void> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(filePath);\n\tconst parsed = po.parse(content);\n\n\tfor (const update of updates) {\n\t\tconst context = update.context || '';\n\n\t\tif (parsed.translations[context]?.[update.msgid]) {\n\t\t\tparsed.translations[context][update.msgid].msgstr = [update.msgstr];\n\n\t\t\t// Handle fuzzy flag\n\t\t\tif (update.fuzzy !== undefined) {\n\t\t\t\tconst comments = parsed.translations[context][update.msgid].comments || {};\n\t\t\t\tif (update.fuzzy) {\n\t\t\t\t\tcomments.flag = 'fuzzy';\n\t\t\t\t} else {\n\t\t\t\t\tdelete comments.flag;\n\t\t\t\t}\n\t\t\t\tparsed.translations[context][update.msgid].comments = comments;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst compiled = po.compile(parsed);\n\tawait writeFile(filePath, compiled);\n}\n\n/**\n * Update a single translation\n */\nexport async function updateTranslation(\n\tfilePath: string,\n\tmsgid: string,\n\tmsgstr: string,\n\tcontext?: string\n): Promise<void> {\n\tawait savePoFile(filePath, [{ msgid, msgstr, context }]);\n}\n\n/**\n * Find all .po files in a directory\n */\nexport async function findPoFiles(localesDir: string): Promise<Language[]> {\n\tif (!existsSync(localesDir)) {\n\t\treturn [];\n\t}\n\n\tconst files = (await readdir(localesDir)).filter((f) => f.endsWith('.po'));\n\n\treturn Promise.all(\n\t\tfiles.map(async (file) => {\n\t\t\tconst filePath = join(localesDir, file);\n\t\t\tconst code = basename(file, '.po');\n\t\t\tconst translations = await parsePoFile(filePath);\n\n\t\t\tconst translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;\n\t\t\tconst fuzzy = translations.filter((t) => t.fuzzy).length;\n\n\t\t\treturn {\n\t\t\t\tcode,\n\t\t\t\tname: getLanguageName(code),\n\t\t\t\tpath: filePath,\n\t\t\t\ttranslations,\n\t\t\t\tprogress: {\n\t\t\t\t\ttotal: translations.length,\n\t\t\t\t\ttranslated,\n\t\t\t\t\tfuzzy\n\t\t\t\t}\n\t\t\t};\n\t\t})\n\t);\n}\n\n/**\n * Get language statistics for all languages\n */\nexport async function getLanguageStats(localesDir: string): Promise<LanguageStats[]> {\n\tconst languages = await findPoFiles(localesDir);\n\n\treturn languages.map((lang) => ({\n\t\tcode: lang.code,\n\t\tname: lang.name,\n\t\ttotal: lang.progress.total,\n\t\ttranslated: lang.progress.translated,\n\t\tfuzzy: lang.progress.fuzzy,\n\t\tuntranslated: lang.progress.total - lang.progress.translated - lang.progress.fuzzy,\n\t\tprogress:\n\t\t\tlang.progress.total > 0\n\t\t\t\t? Math.round((lang.progress.translated / lang.progress.total) * 100)\n\t\t\t\t: 0\n\t}));\n}\n\n/**\n * Get a human-readable language name from a locale code\n */\nexport function getLanguageName(code: string): string {\n\tconst names: Record<string, string> = {\n\t\ten: 'English',\n\t\tes: 'Spanish',\n\t\tfr: 'French',\n\t\tde: 'German',\n\t\tit: 'Italian',\n\t\tpt: 'Portuguese',\n\t\t'pt-BR': 'Portuguese (Brazil)',\n\t\tja: 'Japanese',\n\t\tko: 'Korean',\n\t\tzh: 'Chinese',\n\t\t'zh-CN': 'Chinese (Simplified)',\n\t\t'zh-TW': 'Chinese (Traditional)',\n\t\tru: 'Russian',\n\t\tar: 'Arabic',\n\t\tnl: 'Dutch',\n\t\tpl: 'Polish',\n\t\tsv: 'Swedish',\n\t\tda: 'Danish',\n\t\tfi: 'Finnish',\n\t\tno: 'Norwegian',\n\t\ttr: 'Turkish',\n\t\tcs: 'Czech',\n\t\thu: 'Hungarian',\n\t\tro: 'Romanian',\n\t\tuk: 'Ukrainian',\n\t\tvi: 'Vietnamese',\n\t\tth: 'Thai',\n\t\tid: 'Indonesian',\n\t\tms: 'Malay',\n\t\the: 'Hebrew',\n\t\thi: 'Hindi'\n\t};\n\treturn names[code] || code.toUpperCase();\n}\n"],"mappings":"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","names":["import_path","import_fs","import_path","resolve","__filename","sirv"]}
|
package/dist/plugin/index.d.cts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Plugin } from 'vite';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Plugin configuration options
|
|
5
3
|
*/
|
|
@@ -17,6 +15,10 @@ interface PluginOptions {
|
|
|
17
15
|
provider: 'openai' | 'anthropic' | 'google';
|
|
18
16
|
apiKey?: string;
|
|
19
17
|
};
|
|
18
|
+
/** Restart the dev server when a .po file is updated (default: false) */
|
|
19
|
+
restartOnPoChange?: boolean;
|
|
20
|
+
/** Trigger a full page reload when a .po file is updated (default: true) */
|
|
21
|
+
reloadOnPoChange?: boolean;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Represents a single translation entry
|
|
@@ -95,6 +97,6 @@ interface LanguageStats {
|
|
|
95
97
|
* });
|
|
96
98
|
* ```
|
|
97
99
|
*/
|
|
98
|
-
declare function lingoPlugin(options?: PluginOptions):
|
|
100
|
+
declare function lingoPlugin(options?: PluginOptions): any;
|
|
99
101
|
|
|
100
102
|
export { type Language, type LanguageStats, type PluginOptions, type Translation, lingoPlugin as default, lingoPlugin };
|
package/dist/plugin/index.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Plugin } from 'vite';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Plugin configuration options
|
|
5
3
|
*/
|
|
@@ -17,6 +15,10 @@ interface PluginOptions {
|
|
|
17
15
|
provider: 'openai' | 'anthropic' | 'google';
|
|
18
16
|
apiKey?: string;
|
|
19
17
|
};
|
|
18
|
+
/** Restart the dev server when a .po file is updated (default: false) */
|
|
19
|
+
restartOnPoChange?: boolean;
|
|
20
|
+
/** Trigger a full page reload when a .po file is updated (default: true) */
|
|
21
|
+
reloadOnPoChange?: boolean;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Represents a single translation entry
|
|
@@ -95,6 +97,6 @@ interface LanguageStats {
|
|
|
95
97
|
* });
|
|
96
98
|
* ```
|
|
97
99
|
*/
|
|
98
|
-
declare function lingoPlugin(options?: PluginOptions):
|
|
100
|
+
declare function lingoPlugin(options?: PluginOptions): any;
|
|
99
101
|
|
|
100
102
|
export { type Language, type LanguageStats, type PluginOptions, type Translation, lingoPlugin as default, lingoPlugin };
|
package/dist/plugin/index.js
CHANGED
|
@@ -6,7 +6,7 @@ var getDirname = () => path.dirname(getFilename());
|
|
|
6
6
|
var __dirname = /* @__PURE__ */ getDirname();
|
|
7
7
|
|
|
8
8
|
// src/lib/plugin/index.ts
|
|
9
|
-
import { resolve,
|
|
9
|
+
import { resolve, dirname } from "path";
|
|
10
10
|
import { existsSync as existsSync2 } from "fs";
|
|
11
11
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
12
12
|
import sirv from "sirv";
|
|
@@ -167,7 +167,7 @@ function sendError(res, message, statusCode = 500) {
|
|
|
167
167
|
sendJson(res, { success: false, error: message }, statusCode);
|
|
168
168
|
}
|
|
169
169
|
function createApiMiddleware(options) {
|
|
170
|
-
const { localesDir } = options;
|
|
170
|
+
const { localesDir, server } = options;
|
|
171
171
|
return async (req, res, next) => {
|
|
172
172
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
173
173
|
const path2 = url.pathname;
|
|
@@ -258,7 +258,13 @@ function createApiMiddleware(options) {
|
|
|
258
258
|
var __filename2 = typeof __dirname !== "undefined" ? "" : fileURLToPath2(import.meta.url);
|
|
259
259
|
var __dirnameComputed = typeof __dirname !== "undefined" ? __dirname : dirname(__filename2);
|
|
260
260
|
function lingoPlugin(options = {}) {
|
|
261
|
-
const {
|
|
261
|
+
const {
|
|
262
|
+
route = "/_translations",
|
|
263
|
+
localesDir = "./locales",
|
|
264
|
+
production = false,
|
|
265
|
+
restartOnPoChange = false,
|
|
266
|
+
reloadOnPoChange = true
|
|
267
|
+
} = options;
|
|
262
268
|
let root;
|
|
263
269
|
let resolvedLocalesDir;
|
|
264
270
|
return {
|
|
@@ -269,6 +275,26 @@ function lingoPlugin(options = {}) {
|
|
|
269
275
|
root = config.root;
|
|
270
276
|
resolvedLocalesDir = resolve(root, localesDir);
|
|
271
277
|
},
|
|
278
|
+
handleHotUpdate({ file, server }) {
|
|
279
|
+
if (file.endsWith(".po")) {
|
|
280
|
+
console.log(`[lingo] .po file updated: ${file}`);
|
|
281
|
+
server.ws.send({
|
|
282
|
+
type: "custom",
|
|
283
|
+
event: "lingo:po-updated",
|
|
284
|
+
data: { path: file }
|
|
285
|
+
});
|
|
286
|
+
if (restartOnPoChange) {
|
|
287
|
+
console.log("[lingo] Restarting dev server...");
|
|
288
|
+
server.restart();
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
if (reloadOnPoChange) {
|
|
292
|
+
console.log("[lingo] Triggering full page reload...");
|
|
293
|
+
server.ws.send({ type: "full-reload" });
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
272
298
|
configureServer(server) {
|
|
273
299
|
const cleanRoute = route.replace(/\/$/, "");
|
|
274
300
|
let uiPath = resolve(__dirnameComputed, "../ui-dist");
|
|
@@ -389,21 +415,10 @@ function lingoPlugin(options = {}) {
|
|
|
389
415
|
`${cleanRoute}/api`,
|
|
390
416
|
createApiMiddleware({
|
|
391
417
|
localesDir: resolvedLocalesDir,
|
|
392
|
-
root
|
|
418
|
+
root,
|
|
419
|
+
server
|
|
393
420
|
})
|
|
394
421
|
);
|
|
395
|
-
const poGlob = join3(resolvedLocalesDir, "**/*.po");
|
|
396
|
-
server.watcher.add(poGlob);
|
|
397
|
-
server.watcher.on("change", (path2) => {
|
|
398
|
-
if (path2.endsWith(".po")) {
|
|
399
|
-
server.ws.send({
|
|
400
|
-
type: "custom",
|
|
401
|
-
event: "lingo:po-updated",
|
|
402
|
-
data: { path: path2 }
|
|
403
|
-
});
|
|
404
|
-
console.log(`[lingo] .po file updated: ${path2}`);
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
422
|
const port = server.config.server.port || 5173;
|
|
408
423
|
const protocol = server.config.server.https ? "https" : "http";
|
|
409
424
|
const host = server.config.server.host || "localhost";
|
package/dist/plugin/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"sources":["../../node_modules/tsup/assets/esm_shims.js","../../src/lib/plugin/index.ts","../../src/lib/plugin/middleware.ts","../../src/lib/plugin/po-parser.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","import type { Plugin, ViteDevServer } from 'vite';\nimport { resolve, join, dirname } from 'path';\nimport { existsSync } from 'fs';\nimport { fileURLToPath } from 'url';\nimport sirv from 'sirv';\nimport type { PluginOptions } from './types.js';\nimport { createApiMiddleware } from './middleware.js';\n\n// Compute __dirname for ESM (CJS already has it)\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-ignore - import.meta.url works in ESM, undefined in CJS\nconst __filename = typeof __dirname !== 'undefined' ? '' : fileURLToPath(import.meta.url);\nconst __dirnameComputed = typeof __dirname !== 'undefined' ? __dirname : dirname(__filename);\n\n// Re-export types\nexport type { PluginOptions, Translation, Language, LanguageStats } from './types.js';\n\n/**\n * vite-plugin-lingo - Visual translation editor for .po files\n *\n * @example\n * ```ts\n * // vite.config.ts (default setup)\n * import { defineConfig } from 'vite';\n * import lingo from 'vite-plugin-lingo';\n *\n * export default defineConfig({\n * plugins: [\n * lingo({\n * route: '/_translations',\n * localesDir: './locales'\n * })\n * ]\n * });\n * ```\n *\n * @example\n * ```ts\n * // vite.config.ts (SvelteKit convention)\n * import { defineConfig } from 'vite';\n * import lingo from 'vite-plugin-lingo';\n *\n * export default defineConfig({\n * plugins: [\n * lingo({\n * route: '/_translations',\n * localesDir: './src/locales' // Common in SvelteKit\n * })\n * ]\n * });\n * ```\n */\nexport default function lingoPlugin(options: PluginOptions = {}): any {\n\tconst {\n\t\troute = '/_translations',\n\t\tlocalesDir = './locales',\n\t\tproduction = false,\n\t\trestartOnPoChange = false,\n\t\treloadOnPoChange = true\n\t} = options;\n\n\tlet root: string;\n\tlet resolvedLocalesDir: string;\n\n\treturn {\n\t\tname: 'vite-plugin-lingo',\n\n\t\t// Only apply in serve mode (unless production is enabled)\n\t\tapply: production ? undefined : 'serve',\n\n\t\tconfigResolved(config: any) {\n\t\t\troot = config.root;\n\t\t\tresolvedLocalesDir = resolve(root, localesDir);\n\t\t},\n\n\t\thandleHotUpdate({ file, server }: { file: string; server: ViteDevServer }) {\n\t\t\tif (file.endsWith('.po')) {\n\t\t\t\tconsole.log(`[lingo] .po file updated: ${file}`);\n\n\t\t\t\t// Notify connected clients via WebSocket\n\t\t\t\tserver.ws.send({\n\t\t\t\t\ttype: 'custom',\n\t\t\t\t\tevent: 'lingo:po-updated',\n\t\t\t\t\tdata: { path: file }\n\t\t\t\t});\n\n\t\t\t\tif (restartOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Restarting dev server...');\n\t\t\t\t\tserver.restart();\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\n\t\t\t\tif (reloadOnPoChange) {\n\t\t\t\t\tconsole.log('[lingo] Triggering full page reload...');\n\t\t\t\t\tserver.ws.send({ type: 'full-reload' });\n\t\t\t\t\treturn [];\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tconfigureServer(server: ViteDevServer) {\n\t\t\t// Ensure the route doesn't have trailing slash\n\t\t\tconst cleanRoute = route.replace(/\\/$/, '');\n\n\t\t\t// Find the built UI assets\n\t\t\t// When running from source (dev): __dirnameComputed is src/lib/plugin, UI is at dist/ui-dist\n\t\t\t// When running from dist (published): __dirnameComputed is dist/plugin, UI is at ../ui-dist\n\t\t\tlet uiPath = resolve(__dirnameComputed, '../ui-dist');\n\n\t\t\t// If not found relative to __dirname, try from project root\n\t\t\tif (!existsSync(uiPath)) {\n\t\t\t\tuiPath = resolve(root, 'dist/ui-dist');\n\t\t\t}\n\n\t\t\tconsole.log('[lingo] Looking for UI at:', uiPath);\n\t\t\tconsole.log('[lingo] UI exists:', existsSync(uiPath));\n\n\t\t\t// Serve the editor UI if built assets exist\n\t\t\tif (existsSync(uiPath)) {\n\t\t\t\tconsole.log('[lingo] Serving built UI from:', uiPath);\n\t\t\t\tconst serve = sirv(uiPath, {\n\t\t\t\t\tdev: true,\n\t\t\t\t\tsingle: true // SPA mode\n\t\t\t\t});\n\n\t\t\t\tserver.middlewares.use(cleanRoute, (req, res, next) => {\n\t\t\t\t\t// Redirect to add trailing slash for base route (ensures relative paths work)\n\t\t\t\t\t// When mounted at /path, req.url is already stripped of the prefix\n\t\t\t\t\t// But the browser URL matters for relative path resolution\n\t\t\t\t\tconst reqWithOriginal = req as typeof req & { originalUrl?: string };\n\t\t\t\t\tif (\n\t\t\t\t\t\t(req.url === '/' || req.url === '') &&\n\t\t\t\t\t\treqWithOriginal.originalUrl &&\n\t\t\t\t\t\t!reqWithOriginal.originalUrl.endsWith('/')\n\t\t\t\t\t) {\n\t\t\t\t\t\tres.writeHead(302, { Location: cleanRoute + '/' });\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Handle favicon.ico requests with an empty response to prevent 404\n\t\t\t\t\tif (req.url === '/favicon.ico') {\n\t\t\t\t\t\tres.statusCode = 204;\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip API routes (handled by separate middleware)\n\t\t\t\t\tif (req.url?.startsWith('/api')) {\n\t\t\t\t\t\treturn next();\n\t\t\t\t\t}\n\n\t\t\t\t\t// Serve static files with sirv\n\t\t\t\t\t// req.url is already stripped of the cleanRoute prefix by Connect\n\t\t\t\t\tserve(req, res, () => next());\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// Development mode: serve a simple placeholder\n\t\t\t\tserver.middlewares.use(cleanRoute, (req, res, next) => {\n\t\t\t\t\t// Handle favicon.ico requests with an empty response to prevent 404\n\t\t\t\t\tif (req.url === '/favicon.ico') {\n\t\t\t\t\t\tres.statusCode = 204;\n\t\t\t\t\t\tres.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (req.url?.startsWith('/api')) {\n\t\t\t\t\t\treturn next();\n\t\t\t\t\t}\n\n\t\t\t\t\tres.setHeader('Content-Type', 'text/html');\n\t\t\t\t\tres.end(`\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>🌍 Lingo Translation Editor</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; min-height: 100vh; }\n .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }\n header { background: white; padding: 1rem 2rem; border-bottom: 1px solid #e0e0e0; margin-bottom: 2rem; }\n h1 { font-size: 1.5rem; color: #333; }\n .card { background: white; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n .language { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; border-bottom: 1px solid #eee; }\n .language:last-child { border-bottom: none; }\n .progress { background: #e0e0e0; border-radius: 4px; height: 8px; width: 100px; overflow: hidden; }\n .progress-bar { background: #4caf50; height: 100%; transition: width 0.3s; }\n .loading { text-align: center; padding: 2rem; color: #666; }\n .error { color: #f44336; }\n </style>\n</head>\n<body>\n <header>\n <h1>🌍 Lingo Translation Editor</h1>\n </header>\n <div class=\"container\">\n <div class=\"card\">\n <h2 style=\"margin-bottom: 1rem;\">Languages</h2>\n <div id=\"languages\" class=\"loading\">Loading...</div>\n </div>\n </div>\n <script>\n async function loadLanguages() {\n try {\n const res = await fetch('${cleanRoute}/api/languages');\n const { data, error } = await res.json();\n \n if (error) throw new Error(error);\n \n const container = document.getElementById('languages');\n if (!data || data.length === 0) {\n container.innerHTML = '<p>No .po files found in the locales directory.</p>';\n return;\n }\n \n container.innerHTML = data.map(lang => \\`\n <div class=\"language\">\n <div>\n <strong>\\${lang.name}</strong>\n <span style=\"color: #666; margin-left: 0.5rem;\">(\\${lang.code})</span>\n </div>\n <div style=\"display: flex; align-items: center; gap: 1rem;\">\n <span>\\${lang.translated}/\\${lang.total} translated</span>\n <div class=\"progress\">\n <div class=\"progress-bar\" style=\"width: \\${lang.progress}%\"></div>\n </div>\n </div>\n </div>\n \\`).join('');\n } catch (err) {\n document.getElementById('languages').innerHTML = \n '<p class=\"error\">Error loading languages: ' + err.message + '</p>';\n }\n }\n \n loadLanguages();\n </script>\n</body>\n</html>\n `);\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// API endpoints\n\t\t\tserver.middlewares.use(\n\t\t\t\t`${cleanRoute}/api`,\n\t\t\t\tcreateApiMiddleware({\n\t\t\t\t\tlocalesDir: resolvedLocalesDir,\n\t\t\t\t\troot,\n\t\t\t\t\tserver\n\t\t\t\t})\n\t\t\t);\n\n\t\t\t// Log startup message\n\t\t\tconst port = server.config.server.port || 5173;\n\n\t\t\tconst protocol = server.config.server.https ? 'https' : 'http';\n\t\t\tconst host = server.config.server.host || 'localhost';\n\t\t\tconst hostString = typeof host === 'string' ? host : 'localhost';\n\n\t\t\t// Use server.printUrls hook for better timing\n\t\t\tconst originalPrintUrls = server.printUrls;\n\t\t\tserver.printUrls = () => {\n\t\t\t\toriginalPrintUrls?.();\n\t\t\t\tconsole.log(\n\t\t\t\t\t` \\x1b[32m➜\\x1b[0m \\x1b[1mLingo:\\x1b[0m ${protocol}://${hostString}:${port}${cleanRoute}`\n\t\t\t\t);\n\t\t\t};\n\t\t}\n\t};\n}\n\n// Named export for convenience\nexport { lingoPlugin };\n","import type { IncomingMessage, ServerResponse } from 'http';\nimport { join } from 'path';\nimport type { ViteDevServer } from 'vite';\nimport {\n\tfindPoFiles,\n\tparsePoFile,\n\tsavePoFile,\n\tgetLanguageStats,\n\tupdateTranslation\n} from './po-parser.js';\nimport type { Translation } from './types.js';\n\ninterface MiddlewareOptions {\n\tlocalesDir: string;\n\troot: string;\n\tserver?: ViteDevServer;\n}\n\n/**\n * Parse the request body as JSON\n */\nasync function parseBody<T>(req: IncomingMessage): Promise<T> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet body = '';\n\t\treq.on('data', (chunk) => (body += chunk));\n\t\treq.on('end', () => {\n\t\t\ttry {\n\t\t\t\tresolve(JSON.parse(body));\n\t\t\t} catch {\n\t\t\t\treject(new Error('Invalid JSON'));\n\t\t\t}\n\t\t});\n\t\treq.on('error', reject);\n\t});\n}\n\n/**\n * Send JSON response\n */\nfunction sendJson(res: ServerResponse, data: unknown, statusCode = 200): void {\n\tres.statusCode = statusCode;\n\tres.setHeader('Content-Type', 'application/json');\n\tres.end(JSON.stringify(data));\n}\n\n/**\n * Send error response\n */\nfunction sendError(res: ServerResponse, message: string, statusCode = 500): void {\n\tsendJson(res, { success: false, error: message }, statusCode);\n}\n\n/**\n * Create API middleware for handling translation operations\n */\nexport function createApiMiddleware(options: MiddlewareOptions) {\n\tconst { localesDir, server } = options;\n\n\treturn async (req: IncomingMessage, res: ServerResponse, next: () => void): Promise<void> => {\n\t\tconst url = new URL(req.url || '/', `http://${req.headers.host}`);\n\t\tconst path = url.pathname;\n\t\tconst method = req.method?.toUpperCase();\n\n\t\t// Enable CORS for development\n\t\tres.setHeader('Access-Control-Allow-Origin', '*');\n\t\tres.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\n\t\tres.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n\t\t// Handle preflight\n\t\tif (method === 'OPTIONS') {\n\t\t\tres.statusCode = 204;\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\t// GET /api/languages - List all languages with stats\n\t\t\tif (path === '/languages' && method === 'GET') {\n\t\t\t\tconst stats = await getLanguageStats(localesDir);\n\t\t\t\tsendJson(res, { success: true, data: stats });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// GET /api/translations/:lang - Get all translations for a language\n\t\t\tconst translationsMatch = path.match(/^\\/translations\\/([^/]+)$/);\n\t\t\tif (translationsMatch && method === 'GET') {\n\t\t\t\tconst langCode = translationsMatch[1];\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst translations = await parsePoFile(filePath);\n\t\t\t\t\tsendJson(res, { success: true, data: translations });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, `Language not found: ${langCode}`, 404);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// PUT /api/translations/:lang - Update translations for a language\n\t\t\tif (translationsMatch && method === 'PUT') {\n\t\t\t\tconst langCode = translationsMatch[1];\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await parseBody<Translation | Translation[]>(req);\n\t\t\t\t\tconst updates = Array.isArray(body) ? body : [body];\n\t\t\t\t\tawait savePoFile(filePath, updates);\n\n\t\t\t\t\tsendJson(res, { success: true, message: 'Translations updated' });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, error instanceof Error ? error.message : 'Failed to update', 400);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// PUT /api/translation/:lang/:msgid - Update a single translation\n\t\t\tconst singleMatch = path.match(/^\\/translation\\/([^/]+)\\/(.+)$/);\n\t\t\tif (singleMatch && method === 'PUT') {\n\t\t\t\tconst langCode = singleMatch[1];\n\t\t\t\tconst msgid = decodeURIComponent(singleMatch[2]);\n\t\t\t\tconst filePath = join(localesDir, `${langCode}.po`);\n\n\t\t\t\ttry {\n\t\t\t\t\tconst body = await parseBody<{ msgstr: string; context?: string }>(req);\n\t\t\t\t\tawait updateTranslation(filePath, msgid, body.msgstr, body.context);\n\n\t\t\t\t\tsendJson(res, { success: true, message: 'Translation updated' });\n\t\t\t\t} catch (error) {\n\t\t\t\t\tsendError(res, error instanceof Error ? error.message : 'Failed to update', 400);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// GET /api/search - Search translations across languages\n\t\t\tif (path === '/search' && method === 'GET') {\n\t\t\t\tconst query = url.searchParams.get('q')?.toLowerCase() || '';\n\t\t\t\tconst lang = url.searchParams.get('lang');\n\n\t\t\t\tconst languages = await findPoFiles(localesDir);\n\t\t\t\tconst results: Array<{\n\t\t\t\t\tlang: string;\n\t\t\t\t\tmsgid: string;\n\t\t\t\t\tmsgstr: string;\n\t\t\t\t\tcontext?: string;\n\t\t\t\t}> = [];\n\n\t\t\t\tfor (const language of languages) {\n\t\t\t\t\tif (lang && language.code !== lang) continue;\n\n\t\t\t\t\tfor (const t of language.translations) {\n\t\t\t\t\t\tif (t.msgid.toLowerCase().includes(query) || t.msgstr.toLowerCase().includes(query)) {\n\t\t\t\t\t\t\tresults.push({\n\t\t\t\t\t\t\t\tlang: language.code,\n\t\t\t\t\t\t\t\tmsgid: t.msgid,\n\t\t\t\t\t\t\t\tmsgstr: t.msgstr,\n\t\t\t\t\t\t\t\tcontext: t.context\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tsendJson(res, { success: true, data: results });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Not found\n\t\t\tsendError(res, 'Not found', 404);\n\t\t} catch (error) {\n\t\t\tconsole.error('[lingo] API error:', error);\n\t\t\tsendError(res, error instanceof Error ? error.message : 'Internal server error');\n\t\t}\n\t};\n}\n","import { po } from 'gettext-parser';\nimport { readFile, writeFile, readdir } from 'fs/promises';\nimport { existsSync } from 'fs';\nimport { join, basename } from 'path';\nimport type { Translation, Language, LanguageStats } from './types.js';\n\n/**\n * Parse a .po file and extract translations\n */\nexport async function parsePoFile(filePath: string): Promise<Translation[]> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(filePath);\n\tconst parsed = po.parse(content);\n\n\tconst translations: Translation[] = [];\n\n\tfor (const [context, messages] of Object.entries(parsed.translations)) {\n\t\tfor (const [msgid, data] of Object.entries(messages as Record<string, unknown>)) {\n\t\t\tif (!msgid) continue; // Skip header entry\n\n\t\t\tconst entry = data as {\n\t\t\t\tmsgid: string;\n\t\t\t\tmsgstr?: string[];\n\t\t\t\tcomments?: {\n\t\t\t\t\treference?: string;\n\t\t\t\t\ttranslator?: string;\n\t\t\t\t\textracted?: string;\n\t\t\t\t\tflag?: string;\n\t\t\t\t};\n\t\t\t};\n\n\t\t\ttranslations.push({\n\t\t\t\tmsgid,\n\t\t\t\tmsgstr: entry.msgstr?.[0] || '',\n\t\t\t\tcontext: context || undefined,\n\t\t\t\tcomments: entry.comments,\n\t\t\t\tfuzzy: entry.comments?.flag?.includes('fuzzy') || false\n\t\t\t});\n\t\t}\n\t}\n\n\treturn translations;\n}\n\n/**\n * Save translations back to a .po file\n */\nexport async function savePoFile(filePath: string, updates: Translation[]): Promise<void> {\n\tif (!existsSync(filePath)) {\n\t\tthrow new Error(`File not found: ${filePath}`);\n\t}\n\n\tconst content = await readFile(filePath);\n\tconst parsed = po.parse(content);\n\n\tfor (const update of updates) {\n\t\tconst context = update.context || '';\n\n\t\tif (parsed.translations[context]?.[update.msgid]) {\n\t\t\tparsed.translations[context][update.msgid].msgstr = [update.msgstr];\n\n\t\t\t// Handle fuzzy flag\n\t\t\tif (update.fuzzy !== undefined) {\n\t\t\t\tconst comments = parsed.translations[context][update.msgid].comments || {};\n\t\t\t\tif (update.fuzzy) {\n\t\t\t\t\tcomments.flag = 'fuzzy';\n\t\t\t\t} else {\n\t\t\t\t\tdelete comments.flag;\n\t\t\t\t}\n\t\t\t\tparsed.translations[context][update.msgid].comments = comments;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst compiled = po.compile(parsed);\n\tawait writeFile(filePath, compiled);\n}\n\n/**\n * Update a single translation\n */\nexport async function updateTranslation(\n\tfilePath: string,\n\tmsgid: string,\n\tmsgstr: string,\n\tcontext?: string\n): Promise<void> {\n\tawait savePoFile(filePath, [{ msgid, msgstr, context }]);\n}\n\n/**\n * Find all .po files in a directory\n */\nexport async function findPoFiles(localesDir: string): Promise<Language[]> {\n\tif (!existsSync(localesDir)) {\n\t\treturn [];\n\t}\n\n\tconst files = (await readdir(localesDir)).filter((f) => f.endsWith('.po'));\n\n\treturn Promise.all(\n\t\tfiles.map(async (file) => {\n\t\t\tconst filePath = join(localesDir, file);\n\t\t\tconst code = basename(file, '.po');\n\t\t\tconst translations = await parsePoFile(filePath);\n\n\t\t\tconst translated = translations.filter((t) => t.msgstr && !t.fuzzy).length;\n\t\t\tconst fuzzy = translations.filter((t) => t.fuzzy).length;\n\n\t\t\treturn {\n\t\t\t\tcode,\n\t\t\t\tname: getLanguageName(code),\n\t\t\t\tpath: filePath,\n\t\t\t\ttranslations,\n\t\t\t\tprogress: {\n\t\t\t\t\ttotal: translations.length,\n\t\t\t\t\ttranslated,\n\t\t\t\t\tfuzzy\n\t\t\t\t}\n\t\t\t};\n\t\t})\n\t);\n}\n\n/**\n * Get language statistics for all languages\n */\nexport async function getLanguageStats(localesDir: string): Promise<LanguageStats[]> {\n\tconst languages = await findPoFiles(localesDir);\n\n\treturn languages.map((lang) => ({\n\t\tcode: lang.code,\n\t\tname: lang.name,\n\t\ttotal: lang.progress.total,\n\t\ttranslated: lang.progress.translated,\n\t\tfuzzy: lang.progress.fuzzy,\n\t\tuntranslated: lang.progress.total - lang.progress.translated - lang.progress.fuzzy,\n\t\tprogress:\n\t\t\tlang.progress.total > 0\n\t\t\t\t? Math.round((lang.progress.translated / lang.progress.total) * 100)\n\t\t\t\t: 0\n\t}));\n}\n\n/**\n * Get a human-readable language name from a locale code\n */\nexport function getLanguageName(code: string): string {\n\tconst names: Record<string, string> = {\n\t\ten: 'English',\n\t\tes: 'Spanish',\n\t\tfr: 'French',\n\t\tde: 'German',\n\t\tit: 'Italian',\n\t\tpt: 'Portuguese',\n\t\t'pt-BR': 'Portuguese (Brazil)',\n\t\tja: 'Japanese',\n\t\tko: 'Korean',\n\t\tzh: 'Chinese',\n\t\t'zh-CN': 'Chinese (Simplified)',\n\t\t'zh-TW': 'Chinese (Traditional)',\n\t\tru: 'Russian',\n\t\tar: 'Arabic',\n\t\tnl: 'Dutch',\n\t\tpl: 'Polish',\n\t\tsv: 'Swedish',\n\t\tda: 'Danish',\n\t\tfi: 'Finnish',\n\t\tno: 'Norwegian',\n\t\ttr: 'Turkish',\n\t\tcs: 'Czech',\n\t\thu: 'Hungarian',\n\t\tro: 'Romanian',\n\t\tuk: 'Ukrainian',\n\t\tvi: 'Vietnamese',\n\t\tth: 'Thai',\n\t\tid: 'Indonesian',\n\t\tms: 'Malay',\n\t\the: 'Hebrew',\n\t\thi: 'Hindi'\n\t};\n\treturn names[code] || code.toUpperCase();\n}\n"],"mappings":"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","names":["existsSync","fileURLToPath","join","resolve","path","join","__filename","fileURLToPath","existsSync"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import type { ViteDevServer } from 'vite';
|
|
2
3
|
interface MiddlewareOptions {
|
|
3
4
|
localesDir: string;
|
|
4
5
|
root: string;
|
|
6
|
+
server?: ViteDevServer;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Create API middleware for handling translation operations
|
|
@@ -36,7 +36,7 @@ function sendError(res, message, statusCode = 500) {
|
|
|
36
36
|
* Create API middleware for handling translation operations
|
|
37
37
|
*/
|
|
38
38
|
export function createApiMiddleware(options) {
|
|
39
|
-
const { localesDir } = options;
|
|
39
|
+
const { localesDir, server } = options;
|
|
40
40
|
return async (req, res, next) => {
|
|
41
41
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
42
42
|
const path = url.pathname;
|
package/dist/plugin/types.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ export interface PluginOptions {
|
|
|
15
15
|
provider: 'openai' | 'anthropic' | 'google';
|
|
16
16
|
apiKey?: string;
|
|
17
17
|
};
|
|
18
|
+
/** Restart the dev server when a .po file is updated (default: false) */
|
|
19
|
+
restartOnPoChange?: boolean;
|
|
20
|
+
/** Trigger a full page reload when a .po file is updated (default: true) */
|
|
21
|
+
reloadOnPoChange?: boolean;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Represents a single translation entry
|