@lingo.dev/compiler 0.3.5 ā 0.3.7
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 +55 -2
- package/build/metadata/manager.cjs +76 -107
- package/build/metadata/manager.mjs +75 -103
- package/build/metadata/manager.mjs.map +1 -1
- package/build/plugin/build-translator.cjs +6 -6
- package/build/plugin/build-translator.mjs +9 -9
- package/build/plugin/build-translator.mjs.map +1 -1
- package/build/plugin/next-compiler-loader.cjs +1 -2
- package/build/plugin/next-compiler-loader.mjs +2 -3
- package/build/plugin/next-compiler-loader.mjs.map +1 -1
- package/build/plugin/next.cjs +1 -3
- package/build/plugin/next.mjs +1 -3
- package/build/plugin/next.mjs.map +1 -1
- package/build/plugin/unplugin.cjs +1 -2
- package/build/plugin/unplugin.mjs +2 -3
- package/build/plugin/unplugin.mjs.map +1 -1
- package/build/react/server/ServerLingoProvider.d.cts +2 -2
- package/build/react/server/ServerLingoProvider.d.mts +2 -2
- package/build/react/shared/LingoProvider.d.cts +2 -2
- package/build/react/shared/LingoProvider.d.mts +2 -2
- package/build/translation-server/translation-server.cjs +4 -4
- package/build/translation-server/translation-server.mjs +5 -5
- package/build/translation-server/translation-server.mjs.map +1 -1
- package/build/translators/local-cache.mjs +8 -8
- package/build/translators/local-cache.mjs.map +1 -1
- package/build/translators/pluralization/service.cjs +3 -3
- package/build/translators/pluralization/service.mjs +3 -3
- package/build/translators/pluralization/service.mjs.map +1 -1
- package/build/translators/translation-service.cjs +6 -10
- package/build/translators/translation-service.mjs +6 -10
- package/build/translators/translation-service.mjs.map +1 -1
- package/package.json +3 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-server.mjs","names":["out: Record<string, any>"],"sources":["../../src/translation-server/translation-server.ts"],"sourcesContent":["/**\n * Simple HTTP server for serving translations during development\n *\n * This server:\n * - Finds a free port automatically\n * - Serves translations via:\n * - GET /translations/:locale - Full dictionary (cached)\n * - POST /translations/:locale (body: { hashes: string[] }) - Batch translation\n * - Uses the same translation logic as middleware\n * - Can be started/stopped programmatically\n */\n\nimport http from \"http\";\nimport crypto from \"crypto\";\nimport type { Socket } from \"net\";\nimport { URL } from \"url\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type { MetadataSchema, TranslationMiddlewareConfig } from \"../types\";\nimport { getLogger } from \"./logger\";\nimport { TranslationService } from \"../translators\";\nimport {\n createEmptyMetadata,\n getMetadataPath,\n loadMetadata,\n} from \"../metadata/manager\";\nimport type { TranslationServerEvent } from \"./ws-events\";\nimport { createEvent } from \"./ws-events\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\nimport { parseLocaleOrThrow } from \"../utils/is-valid-locale\";\n\nexport interface TranslationServerOptions {\n config: TranslationMiddlewareConfig;\n translationService?: TranslationService;\n onReady?: (port: number) => void;\n onError?: (error: Error) => void;\n}\n\nexport class TranslationServer {\n private server: http.Server | null = null;\n private url: string | undefined = undefined;\n private logger;\n private readonly config: TranslationMiddlewareConfig;\n private readonly configHash: string;\n private readonly startPort: number;\n private readonly onReadyCallback?: (port: number) => void;\n private readonly onErrorCallback?: (error: Error) => void;\n private metadata: MetadataSchema | null = null;\n private connections: Set<Socket> = new Set();\n private wss: WebSocketServer | null = null;\n private wsClients: Set<WebSocket> = new Set();\n\n // Translation activity tracking for \"busy\" notifications\n private activeTranslations = 0;\n private isBusy = false;\n private busyTimeout: NodeJS.Timeout | null = null;\n private readonly BUSY_DEBOUNCE_MS = 500; // Time after last translation to send \"idle\" event\n private readonly translationService: TranslationService;\n\n constructor(options: TranslationServerOptions) {\n this.config = options.config;\n this.configHash = hashConfig(options.config);\n this.translationService =\n options.translationService ??\n // Fallback is for CLI start only.\n new TranslationService(options.config, getLogger(options.config));\n this.startPort = options.config.dev.translationServerStartPort;\n this.onReadyCallback = options.onReady;\n this.onErrorCallback = options.onError;\n this.logger = getLogger(this.config);\n }\n\n /**\n * Start the server and find an available port\n */\n async start(): Promise<number> {\n if (this.server) {\n throw new Error(\"Server is already running\");\n }\n\n this.logger.info(`š§ Initializing translator...`);\n\n const port = await this.findAvailablePort(this.startPort);\n\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n // Log that we received a request (before async handling)\n this.logger.info(`š„ Received: ${req.method} ${req.url}`);\n\n // Wrap async handler and catch errors explicitly\n this.handleRequest(req, res).catch((error) => {\n this.logger.error(`Request handler error:`, error);\n this.logger.error(error.stack);\n\n // Send error response if headers not sent\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n });\n });\n\n this.logger.debug(`Starting translation server on port ${port}`);\n\n // Track connections for graceful shutdown\n this.server.on(\"connection\", (socket) => {\n this.connections.add(socket);\n socket.once(\"close\", () => {\n this.connections.delete(socket);\n });\n });\n\n this.server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n // Port is in use, try next one\n reject(new Error(`Port ${port} is already in use`));\n } else {\n this.logger.error(`Translation server error: ${error.message}\\n`);\n this.onErrorCallback?.(error);\n reject(error);\n }\n });\n\n this.server.listen(port, \"127.0.0.1\", () => {\n this.url = `http://127.0.0.1:${port}`;\n this.logger.info(`Translation server listening on ${this.url}`);\n\n // Initialize WebSocket server on the same port\n this.initializeWebSocket();\n\n this.onReadyCallback?.(port);\n resolve(port);\n });\n });\n }\n\n /**\n * Initialize WebSocket server for real-time dev widget updates\n */\n private initializeWebSocket(): void {\n if (!this.server) {\n throw new Error(\"HTTP server must be started before WebSocket\");\n }\n\n this.wss = new WebSocketServer({ server: this.server });\n\n this.wss.on(\"connection\", (ws: WebSocket) => {\n this.wsClients.add(ws);\n this.logger.debug(\n `WebSocket client connected. Total clients: ${this.wsClients.size}`,\n );\n\n // Send initial connected event\n this.sendToClient(ws, createEvent(\"connected\", { serverUrl: this.url! }));\n\n ws.on(\"close\", () => {\n this.wsClients.delete(ws);\n this.logger.debug(\n `WebSocket client disconnected. Total clients: ${this.wsClients.size}`,\n );\n });\n\n ws.on(\"error\", (error) => {\n this.logger.error(`WebSocket client error:`, error);\n this.wsClients.delete(ws);\n });\n });\n\n this.wss.on(\"error\", (error) => {\n this.logger.error(`WebSocket server error:`, error);\n this.onErrorCallback?.(error);\n });\n\n this.logger.info(`WebSocket server initialized`);\n }\n\n /**\n * Send event to a specific WebSocket client\n */\n private sendToClient(ws: WebSocket, event: TranslationServerEvent): void {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify(event));\n }\n }\n\n /**\n * Broadcast event to all connected WebSocket clients\n */\n private broadcast(event: TranslationServerEvent): void {\n const message = JSON.stringify(event);\n for (const client of this.wsClients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n }\n }\n\n /**\n * Stop the server\n */\n async stop(): Promise<void> {\n if (!this.server) {\n this.logger.debug(\"Translation server is not running. Nothing to stop.\");\n return;\n }\n\n // Clear any pending busy timeout\n if (this.busyTimeout) {\n clearTimeout(this.busyTimeout);\n this.busyTimeout = null;\n }\n\n // Close all WebSocket connections\n for (const client of this.wsClients) {\n client.close();\n }\n this.wsClients.clear();\n\n // Close WebSocket server\n if (this.wss) {\n this.wss.close();\n this.wss = null;\n }\n\n // Destroy all active HTTP connections to prevent hanging\n for (const socket of this.connections) {\n socket.destroy();\n }\n this.connections.clear();\n\n return new Promise((resolve, reject) => {\n this.server!.close((error) => {\n if (error) {\n reject(error);\n } else {\n this.logger.info(`Translation server stopped`);\n this.server = null;\n this.url = undefined;\n resolve();\n }\n });\n });\n }\n\n /**\n * Get the current port (null if not running)\n */\n getUrl(): string | undefined {\n return this.url;\n }\n\n /**\n * Start a new server or get the URL of an existing one on the preferred port.\n *\n * This method optimizes for the common case where a translation server is already\n * running on a preferred port. If that port is taken, it checks if it's our service\n * by calling the health check endpoint. If it is, we reuse it instead of starting\n * a new server on a different port.\n *\n * @returns URL of the running server (new or existing)\n */\n async startOrGetUrl(): Promise<string> {\n if (this.server && this.url) {\n this.logger.info(`Using existing server instance at ${this.url}`);\n return this.url;\n }\n\n const preferredPort = this.startPort;\n const preferredUrl = `http://127.0.0.1:${preferredPort}`;\n\n // Check if port is available\n const portAvailable = await this.isPortAvailable(preferredPort);\n\n if (portAvailable) {\n // Port is free, start a new server\n this.logger.info(\n `Port ${preferredPort} is available, starting new server...`,\n );\n await this.start();\n return this.url!;\n }\n\n // Port is taken, check if it's our translation server\n this.logger.info(\n `Port ${preferredPort} is in use, checking if it's a translation server...`,\n );\n const isOurServer = await this.checkIfTranslationServer(preferredUrl);\n\n if (isOurServer) {\n // It's our server, reuse it\n this.logger.info(\n `ā
Found existing translation server at ${preferredUrl}, reusing it`,\n );\n this.url = preferredUrl;\n return preferredUrl;\n }\n\n // Port is taken by something else, start a new server on a different port\n this.logger.info(\n `Port ${preferredPort} is in use by another service, finding alternative...`,\n );\n await this.start();\n return this.url!;\n }\n\n /**\n * Check if server is running\n */\n isRunning(): boolean {\n return this.server !== null && this.url !== null;\n }\n\n /**\n * Reload metadata from disk\n * Useful when metadata has been updated during runtime (e.g., new transformations)\n */\n async reloadMetadata(): Promise<void> {\n try {\n this.metadata = await loadMetadata(getMetadataPath(this.config));\n this.logger.debug(\n `Reloaded metadata: ${Object.keys(this.metadata.entries).length} entries`,\n );\n } catch (error) {\n this.logger.warn(\"Failed to reload metadata:\", error);\n this.metadata = createEmptyMetadata();\n }\n }\n\n /**\n * Translate the entire dictionary for a given locale\n *\n * This method always reloads metadata from disk before translating to ensure\n * all entries added during build-time transformations are included.\n *\n * This is the recommended method for build-time translation generation.\n */\n async translateAll(locale: LocaleCode): Promise<{\n translations: Record<string, string>;\n errors: Array<{ hash: string; error: string }>;\n }> {\n if (!this.translationService) {\n throw new Error(\"Translation server not initialized\");\n }\n\n // Always reload metadata to get the latest entries\n // This is critical for build-time translation where metadata is updated\n // continuously as files are transformed\n await this.reloadMetadata();\n\n if (!this.metadata) {\n throw new Error(\"Failed to load metadata\");\n }\n\n const allHashes = Object.keys(this.metadata.entries);\n\n this.logger.info(\n `Translating all ${allHashes.length} entries to ${locale}`,\n );\n\n // Broadcast batch start event\n const startTime = Date.now();\n this.broadcast(\n createEvent(\"batch:start\", {\n locale,\n total: allHashes.length,\n hashes: allHashes,\n }),\n );\n\n const result = await this.translationService.translate(\n locale,\n this.metadata,\n allHashes,\n );\n\n // Broadcast batch complete event\n const duration = Date.now() - startTime;\n this.broadcast(\n createEvent(\"batch:complete\", {\n locale,\n total: allHashes.length,\n successful: Object.keys(result.translations).length,\n failed: result.errors.length,\n duration,\n }),\n );\n\n return result;\n }\n\n /**\n * Find an available port starting from the given port\n */\n private async findAvailablePort(\n startPort: number,\n maxAttempts = 100,\n ): Promise<number> {\n for (let port = startPort; port < startPort + maxAttempts; port++) {\n if (await this.isPortAvailable(port)) {\n return port;\n }\n }\n throw new Error(\n `Could not find available port in range ${startPort}-${startPort + maxAttempts}`,\n );\n }\n\n /**\n * Check if a port is available\n */\n private async isPortAvailable(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const testServer = http.createServer();\n\n testServer.once(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n resolve(false);\n } else {\n resolve(false);\n }\n });\n\n testServer.once(\"listening\", () => {\n testServer.close(() => {\n resolve(true);\n });\n });\n\n testServer.listen(port, \"127.0.0.1\");\n });\n }\n\n /**\n * Mark translation activity start - emits busy event if not already busy\n */\n private startTranslationActivity(): void {\n this.activeTranslations++;\n\n // Clear any pending idle timeout\n if (this.busyTimeout) {\n clearTimeout(this.busyTimeout);\n this.busyTimeout = null;\n }\n\n // Emit busy event if this is the first active translation\n if (!this.isBusy && this.activeTranslations > 0) {\n this.isBusy = true;\n this.broadcast(\n createEvent(\"server:busy\", {\n activeTranslations: this.activeTranslations,\n }),\n );\n this.logger.debug(\n `[BUSY] Server is now busy (${this.activeTranslations} active)`,\n );\n }\n }\n\n /**\n * Mark translation activity end - emits idle event after debounce period\n */\n private endTranslationActivity(): void {\n this.activeTranslations = Math.max(0, this.activeTranslations - 1);\n\n // If no more active translations, schedule idle notification\n if (this.activeTranslations === 0 && this.isBusy) {\n // Clear any existing timeout\n if (this.busyTimeout) {\n clearTimeout(this.busyTimeout);\n }\n\n // Wait for debounce period before sending idle event\n // This prevents rapid busy->idle->busy cycles when translations come in quick succession\n this.busyTimeout = setTimeout(() => {\n if (this.activeTranslations === 0) {\n this.isBusy = false;\n this.broadcast(createEvent(\"server:idle\", {}));\n this.logger.debug(\"[IDLE] Server is now idle\");\n }\n }, this.BUSY_DEBOUNCE_MS);\n }\n }\n\n /**\n * Check if a given URL is running our translation server by calling the health endpoint\n * Also verifies that the config hash matches to ensure compatible configuration\n */\n private async checkIfTranslationServer(url: string): Promise<boolean> {\n return new Promise((resolve) => {\n const healthUrl = `${url}/health`;\n\n const req = http.get(healthUrl, { timeout: 2000 }, (res) => {\n let data = \"\";\n\n res.on(\"data\", (chunk) => {\n data += chunk.toString();\n });\n\n res.on(\"end\", () => {\n try {\n if (res.statusCode === 200) {\n const json = JSON.parse(data);\n // Our translation server returns { status: \"ok\", port: ..., configHash: ... }\n // Check if config hash matches (if present)\n // If configHash is missing (old server), accept it for backward compatibility\n if (json.configHash && json.configHash !== this.configHash) {\n this.logger.warn(\n `Existing server has different config (hash: ${json.configHash} vs ${this.configHash}), will start new server`,\n );\n resolve(false);\n return;\n }\n resolve(true);\n return;\n }\n resolve(false);\n } catch (error) {\n // Failed to parse JSON or invalid response\n resolve(false);\n }\n });\n });\n\n req.on(\"error\", () => {\n // Connection failed, not our server\n resolve(false);\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n resolve(false);\n });\n });\n }\n\n /**\n * Handle incoming HTTP request\n */\n private async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n this.logger.info(`š Processing: ${req.method} ${req.url}`);\n\n try {\n const url = new URL(req.url || \"\", `http://${req.headers.host}`);\n\n // Log request\n this.logger.debug(`${req.method} ${url.pathname}`);\n\n // Handle CORS for browser requests\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n res.setHeader(\n \"Access-Control-Expose-Headers\",\n \"Content-Type, Cache-Control\",\n );\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (url.pathname === \"/health\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n port: this.url,\n configHash: this.configHash,\n }),\n );\n return;\n }\n\n // Batch translation endpoint: POST /translations/:locale\n const postMatch = url.pathname.match(/^\\/translations\\/([^/]+)$/);\n if (postMatch && req.method === \"POST\") {\n const [, locale] = postMatch;\n\n await this.handleBatchTranslationRequest(locale, req, res);\n return;\n }\n\n // Translation dictionary endpoint: GET /translations/:locale\n const dictMatch = url.pathname.match(/^\\/translations\\/([^/]+)$/);\n if (dictMatch && req.method === \"GET\") {\n const [, locale] = dictMatch;\n await this.handleDictionaryRequest(locale, res);\n return;\n }\n\n // 404 for unknown routes\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Not Found\",\n message: \"Unknown endpoint\",\n availableEndpoints: [\n \"GET /health\",\n \"GET /translations/:locale\",\n \"POST /translations/:locale (with body: { hashes: string[] })\",\n ],\n }),\n );\n } catch (error) {\n this.logger.error(\"Error handling request:\", error);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Internal Server Error\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n }\n\n /**\n * Handle batch translation request\n */\n private async handleBatchTranslationRequest(\n locale: string,\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n try {\n const parsedLocale = parseLocaleOrThrow(locale);\n\n // Read request body\n let body = \"\";\n this.logger.debug(\"Reading request body...\");\n for await (const chunk of req) {\n body += chunk.toString();\n this.logger.debug(`Chunk read, body: ${body}`);\n }\n\n // Parse body\n const { hashes } = JSON.parse(body);\n\n this.logger.debug(`Parsed hashes: ${hashes.join(\",\")}`);\n\n if (!Array.isArray(hashes)) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Bad Request\",\n message: \"Body must contain 'hashes' array\",\n }),\n );\n return;\n }\n // Reload metadata to ensure we have the latest entries\n // (new entries may have been added since server started)\n await this.reloadMetadata();\n\n if (!this.metadata) {\n throw new Error(\"Failed to load metadata\");\n }\n\n this.logger.info(`š Translating ${hashes.length} hashes to ${locale}`);\n this.logger.debug(`š Hashes: ${hashes.join(\", \")}`);\n\n // Mark translation activity start\n this.startTranslationActivity();\n\n try {\n const result = await this.translationService.translate(\n parsedLocale,\n this.metadata,\n hashes,\n );\n\n // Return successful response\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"no-cache\",\n });\n res.end(\n JSON.stringify({\n locale,\n translations: result.translations,\n errors: result.errors,\n }),\n );\n } finally {\n // Mark translation activity end\n this.endTranslationActivity();\n }\n } catch (error) {\n this.logger.error(\n `Error getting batch translations for ${locale}:`,\n error,\n );\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Translation generation failed\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n }\n\n /**\n * Handle request for full translation dictionary\n */\n private async handleDictionaryRequest(\n locale: string,\n res: http.ServerResponse,\n ): Promise<void> {\n try {\n const parsedLocale = parseLocaleOrThrow(locale);\n\n // Reload metadata to ensure we have the latest entries\n // (new entries may have been added since server started)\n await this.reloadMetadata();\n\n if (!this.metadata) {\n throw new Error(\"Failed to load metadata\");\n }\n\n this.logger.info(`š Requesting full dictionary for ${locale}`);\n\n const allHashes = Object.keys(this.metadata.entries);\n\n // Translate all hashes\n const result = await this.translationService.translate(\n parsedLocale,\n this.metadata,\n allHashes,\n );\n\n // Return successful response\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"public, max-age=3600\",\n });\n res.end(\n JSON.stringify({\n locale,\n translations: result.translations,\n errors: result.errors,\n }),\n );\n } catch (error) {\n this.logger.error(`Error getting dictionary for ${locale}:`, error);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Translation generation failed\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n }\n}\n\ntype SerializablePrimitive = string | number | boolean | null | undefined;\n\ntype SerializableObject = {\n [key: string]: SerializableValue;\n};\nexport type SerializableValue =\n | SerializablePrimitive\n | SerializableValue[]\n | SerializableObject;\n\nexport function stableStringify(\n value: Record<string, SerializableValue>,\n): string {\n const normalize = (v: any): any => {\n if (v === undefined) return undefined;\n if (typeof v === \"function\") return undefined;\n if (v === null) return null;\n\n if (Array.isArray(v)) {\n return v.map(normalize).filter((x) => x !== undefined);\n }\n\n if (typeof v === \"object\") {\n const out: Record<string, any> = {};\n for (const key of Object.keys(v).sort()) {\n const next = normalize(v[key]);\n if (next !== undefined) out[key] = next;\n }\n return out;\n }\n\n return v;\n };\n\n return JSON.stringify(normalize(value));\n}\n\n/**\n * Generate a stable hash of a config object\n * Filters out functions and non-serializable values\n * Sorts keys for stability\n */\nexport function hashConfig(config: Record<string, SerializableValue>): string {\n const serialized = stableStringify(config);\n return crypto.createHash(\"md5\").update(serialized).digest(\"hex\").slice(0, 12);\n}\n\nexport async function startTranslationServer(\n options: TranslationServerOptions,\n): Promise<TranslationServer> {\n const server = new TranslationServer(options);\n await server.start();\n return server;\n}\n\n/**\n * Create a translation server and start it or reuse an existing one on the preferred port\n *\n * Since we have little control over the dev server start in next, we can start the translation server only in the async config or in the loader,\n * they both could be run in different processes, and we need a way to avoid starting multiple servers.\n * This one will try to start a server on the preferred port (which seems to be an atomic operation), and if it fails,\n * it checks if the server that is already started is ours and returns its url.\n *\n * @returns Object containing the server instance and its URL\n */\nexport async function startOrGetTranslationServer(\n options: TranslationServerOptions,\n): Promise<{ server: TranslationServer; url: string }> {\n const server = new TranslationServer(options);\n const url = await server.startOrGetUrl();\n return { server, url };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"translation-server.mjs","names":["out: Record<string, any>"],"sources":["../../src/translation-server/translation-server.ts"],"sourcesContent":["/**\n * Simple HTTP server for serving translations during development\n *\n * This server:\n * - Finds a free port automatically\n * - Serves translations via:\n * - GET /translations/:locale - Full dictionary (cached)\n * - POST /translations/:locale (body: { hashes: string[] }) - Batch translation\n * - Uses the same translation logic as middleware\n * - Can be started/stopped programmatically\n */\n\nimport http from \"http\";\nimport crypto from \"crypto\";\nimport type { Socket } from \"net\";\nimport { URL } from \"url\";\nimport { WebSocket, WebSocketServer } from \"ws\";\nimport type { MetadataSchema, TranslationMiddlewareConfig } from \"../types\";\nimport { getLogger } from \"./logger\";\nimport { TranslationService } from \"../translators\";\nimport { getMetadataPath, loadMetadata } from \"../metadata/manager\";\nimport type { TranslationServerEvent } from \"./ws-events\";\nimport { createEvent } from \"./ws-events\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\nimport { parseLocaleOrThrow } from \"../utils/is-valid-locale\";\n\nexport interface TranslationServerOptions {\n config: TranslationMiddlewareConfig;\n translationService?: TranslationService;\n onReady?: (port: number) => void;\n onError?: (error: Error) => void;\n}\n\nexport class TranslationServer {\n private server: http.Server | null = null;\n private url: string | undefined = undefined;\n private logger;\n private readonly config: TranslationMiddlewareConfig;\n private readonly configHash: string;\n private readonly startPort: number;\n private readonly onReadyCallback?: (port: number) => void;\n private readonly onErrorCallback?: (error: Error) => void;\n private metadata: MetadataSchema | null = null;\n private connections: Set<Socket> = new Set();\n private wss: WebSocketServer | null = null;\n private wsClients: Set<WebSocket> = new Set();\n\n // Translation activity tracking for \"busy\" notifications\n private activeTranslations = 0;\n private isBusy = false;\n private busyTimeout: NodeJS.Timeout | null = null;\n private readonly BUSY_DEBOUNCE_MS = 500; // Time after last translation to send \"idle\" event\n private readonly translationService: TranslationService;\n\n constructor(options: TranslationServerOptions) {\n this.config = options.config;\n this.configHash = hashConfig(options.config);\n this.translationService =\n options.translationService ??\n // Fallback is for CLI start only.\n new TranslationService(options.config, getLogger(options.config));\n this.startPort = options.config.dev.translationServerStartPort;\n this.onReadyCallback = options.onReady;\n this.onErrorCallback = options.onError;\n this.logger = getLogger(this.config);\n }\n\n /**\n * Start the server and find an available port\n */\n async start(): Promise<number> {\n if (this.server) {\n throw new Error(\"Server is already running\");\n }\n\n this.logger.info(`š§ Initializing translator...`);\n\n const port = await this.findAvailablePort(this.startPort);\n\n return new Promise((resolve, reject) => {\n this.server = http.createServer((req, res) => {\n // Log that we received a request (before async handling)\n this.logger.info(`š„ Received: ${req.method} ${req.url}`);\n\n // Wrap async handler and catch errors explicitly\n this.handleRequest(req, res).catch((error) => {\n this.logger.error(`Request handler error:`, error);\n this.logger.error(error.stack);\n\n // Send error response if headers not sent\n if (!res.headersSent) {\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Internal Server Error\" }));\n }\n });\n });\n\n this.logger.debug(`Starting translation server on port ${port}`);\n\n // Track connections for graceful shutdown\n this.server.on(\"connection\", (socket) => {\n this.connections.add(socket);\n socket.once(\"close\", () => {\n this.connections.delete(socket);\n });\n });\n\n this.server.on(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n // Port is in use, try next one\n reject(new Error(`Port ${port} is already in use`));\n } else {\n this.logger.error(`Translation server error: ${error.message}\\n`);\n this.onErrorCallback?.(error);\n reject(error);\n }\n });\n\n this.server.listen(port, \"127.0.0.1\", () => {\n this.url = `http://127.0.0.1:${port}`;\n this.logger.info(`Translation server listening on ${this.url}`);\n\n // Initialize WebSocket server on the same port\n this.initializeWebSocket();\n\n this.onReadyCallback?.(port);\n resolve(port);\n });\n });\n }\n\n /**\n * Initialize WebSocket server for real-time dev widget updates\n */\n private initializeWebSocket(): void {\n if (!this.server) {\n throw new Error(\"HTTP server must be started before WebSocket\");\n }\n\n this.wss = new WebSocketServer({ server: this.server });\n\n this.wss.on(\"connection\", (ws: WebSocket) => {\n this.wsClients.add(ws);\n this.logger.debug(\n `WebSocket client connected. Total clients: ${this.wsClients.size}`,\n );\n\n // Send initial connected event\n this.sendToClient(ws, createEvent(\"connected\", { serverUrl: this.url! }));\n\n ws.on(\"close\", () => {\n this.wsClients.delete(ws);\n this.logger.debug(\n `WebSocket client disconnected. Total clients: ${this.wsClients.size}`,\n );\n });\n\n ws.on(\"error\", (error) => {\n this.logger.error(`WebSocket client error:`, error);\n this.wsClients.delete(ws);\n });\n });\n\n this.wss.on(\"error\", (error) => {\n this.logger.error(`WebSocket server error:`, error);\n this.onErrorCallback?.(error);\n });\n\n this.logger.info(`WebSocket server initialized`);\n }\n\n /**\n * Send event to a specific WebSocket client\n */\n private sendToClient(ws: WebSocket, event: TranslationServerEvent): void {\n if (ws.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify(event));\n }\n }\n\n /**\n * Broadcast event to all connected WebSocket clients\n */\n private broadcast(event: TranslationServerEvent): void {\n const message = JSON.stringify(event);\n for (const client of this.wsClients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(message);\n }\n }\n }\n\n /**\n * Stop the server\n */\n async stop(): Promise<void> {\n if (!this.server) {\n this.logger.debug(\"Translation server is not running. Nothing to stop.\");\n return;\n }\n\n // Clear any pending busy timeout\n if (this.busyTimeout) {\n clearTimeout(this.busyTimeout);\n this.busyTimeout = null;\n }\n\n // Close all WebSocket connections\n for (const client of this.wsClients) {\n client.close();\n }\n this.wsClients.clear();\n\n // Close WebSocket server\n if (this.wss) {\n this.wss.close();\n this.wss = null;\n }\n\n // Destroy all active HTTP connections to prevent hanging\n for (const socket of this.connections) {\n socket.destroy();\n }\n this.connections.clear();\n\n return new Promise((resolve, reject) => {\n this.server!.close((error) => {\n if (error) {\n reject(error);\n } else {\n this.logger.info(`Translation server stopped`);\n this.server = null;\n this.url = undefined;\n resolve();\n }\n });\n });\n }\n\n /**\n * Get the current port (null if not running)\n */\n getUrl(): string | undefined {\n return this.url;\n }\n\n /**\n * Start a new server or get the URL of an existing one on the preferred port.\n *\n * This method optimizes for the common case where a translation server is already\n * running on a preferred port. If that port is taken, it checks if it's our service\n * by calling the health check endpoint. If it is, we reuse it instead of starting\n * a new server on a different port.\n *\n * @returns URL of the running server (new or existing)\n */\n async startOrGetUrl(): Promise<string> {\n if (this.server && this.url) {\n this.logger.info(`Using existing server instance at ${this.url}`);\n return this.url;\n }\n\n const preferredPort = this.startPort;\n const preferredUrl = `http://127.0.0.1:${preferredPort}`;\n\n // Check if port is available\n const portAvailable = await this.isPortAvailable(preferredPort);\n\n if (portAvailable) {\n // Port is free, start a new server\n this.logger.info(\n `Port ${preferredPort} is available, starting new server...`,\n );\n await this.start();\n return this.url!;\n }\n\n // Port is taken, check if it's our translation server\n this.logger.info(\n `Port ${preferredPort} is in use, checking if it's a translation server...`,\n );\n const isOurServer = await this.checkIfTranslationServer(preferredUrl);\n\n if (isOurServer) {\n // It's our server, reuse it\n this.logger.info(\n `ā
Found existing translation server at ${preferredUrl}, reusing it`,\n );\n this.url = preferredUrl;\n return preferredUrl;\n }\n\n // Port is taken by something else, start a new server on a different port\n this.logger.info(\n `Port ${preferredPort} is in use by another service, finding alternative...`,\n );\n await this.start();\n return this.url!;\n }\n\n /**\n * Check if server is running\n */\n isRunning(): boolean {\n return this.server !== null && this.url !== null;\n }\n\n /**\n * Reload metadata from disk\n * Useful when metadata has been updated during runtime (e.g., new transformations)\n */\n async reloadMetadata(): Promise<void> {\n try {\n this.metadata = await loadMetadata(getMetadataPath(this.config));\n this.logger.debug(\n `Reloaded metadata: ${Object.keys(this.metadata).length} entries`,\n );\n } catch (error) {\n this.logger.warn(\"Failed to reload metadata:\", error);\n this.metadata = {};\n }\n }\n\n /**\n * Translate the entire dictionary for a given locale\n *\n * This method always reloads metadata from disk before translating to ensure\n * all entries added during build-time transformations are included.\n *\n * This is the recommended method for build-time translation generation.\n */\n async translateAll(locale: LocaleCode): Promise<{\n translations: Record<string, string>;\n errors: Array<{ hash: string; error: string }>;\n }> {\n if (!this.translationService) {\n throw new Error(\"Translation server not initialized\");\n }\n\n // Always reload metadata to get the latest entries\n // This is critical for build-time translation where metadata is updated\n // continuously as files are transformed\n await this.reloadMetadata();\n\n if (!this.metadata) {\n throw new Error(\"Failed to load metadata\");\n }\n\n const allHashes = Object.keys(this.metadata);\n\n this.logger.info(\n `Translating all ${allHashes.length} entries to ${locale}`,\n );\n\n // Broadcast batch start event\n const startTime = Date.now();\n this.broadcast(\n createEvent(\"batch:start\", {\n locale,\n total: allHashes.length,\n hashes: allHashes,\n }),\n );\n\n const result = await this.translationService.translate(\n locale,\n this.metadata,\n allHashes,\n );\n\n // Broadcast batch complete event\n const duration = Date.now() - startTime;\n this.broadcast(\n createEvent(\"batch:complete\", {\n locale,\n total: allHashes.length,\n successful: Object.keys(result.translations).length,\n failed: result.errors.length,\n duration,\n }),\n );\n\n return result;\n }\n\n /**\n * Find an available port starting from the given port\n */\n private async findAvailablePort(\n startPort: number,\n maxAttempts = 100,\n ): Promise<number> {\n for (let port = startPort; port < startPort + maxAttempts; port++) {\n if (await this.isPortAvailable(port)) {\n return port;\n }\n }\n throw new Error(\n `Could not find available port in range ${startPort}-${startPort + maxAttempts}`,\n );\n }\n\n /**\n * Check if a port is available\n */\n private async isPortAvailable(port: number): Promise<boolean> {\n return new Promise((resolve) => {\n const testServer = http.createServer();\n\n testServer.once(\"error\", (error: NodeJS.ErrnoException) => {\n if (error.code === \"EADDRINUSE\") {\n resolve(false);\n } else {\n resolve(false);\n }\n });\n\n testServer.once(\"listening\", () => {\n testServer.close(() => {\n resolve(true);\n });\n });\n\n testServer.listen(port, \"127.0.0.1\");\n });\n }\n\n /**\n * Mark translation activity start - emits busy event if not already busy\n */\n private startTranslationActivity(): void {\n this.activeTranslations++;\n\n // Clear any pending idle timeout\n if (this.busyTimeout) {\n clearTimeout(this.busyTimeout);\n this.busyTimeout = null;\n }\n\n // Emit busy event if this is the first active translation\n if (!this.isBusy && this.activeTranslations > 0) {\n this.isBusy = true;\n this.broadcast(\n createEvent(\"server:busy\", {\n activeTranslations: this.activeTranslations,\n }),\n );\n this.logger.debug(\n `[BUSY] Server is now busy (${this.activeTranslations} active)`,\n );\n }\n }\n\n /**\n * Mark translation activity end - emits idle event after debounce period\n */\n private endTranslationActivity(): void {\n this.activeTranslations = Math.max(0, this.activeTranslations - 1);\n\n // If no more active translations, schedule idle notification\n if (this.activeTranslations === 0 && this.isBusy) {\n // Clear any existing timeout\n if (this.busyTimeout) {\n clearTimeout(this.busyTimeout);\n }\n\n // Wait for debounce period before sending idle event\n // This prevents rapid busy->idle->busy cycles when translations come in quick succession\n this.busyTimeout = setTimeout(() => {\n if (this.activeTranslations === 0) {\n this.isBusy = false;\n this.broadcast(createEvent(\"server:idle\", {}));\n this.logger.debug(\"[IDLE] Server is now idle\");\n }\n }, this.BUSY_DEBOUNCE_MS);\n }\n }\n\n /**\n * Check if a given URL is running our translation server by calling the health endpoint\n * Also verifies that the config hash matches to ensure compatible configuration\n */\n private async checkIfTranslationServer(url: string): Promise<boolean> {\n return new Promise((resolve) => {\n const healthUrl = `${url}/health`;\n\n const req = http.get(healthUrl, { timeout: 2000 }, (res) => {\n let data = \"\";\n\n res.on(\"data\", (chunk) => {\n data += chunk.toString();\n });\n\n res.on(\"end\", () => {\n try {\n if (res.statusCode === 200) {\n const json = JSON.parse(data);\n // Our translation server returns { status: \"ok\", port: ..., configHash: ... }\n // Check if config hash matches (if present)\n // If configHash is missing (old server), accept it for backward compatibility\n if (json.configHash && json.configHash !== this.configHash) {\n this.logger.warn(\n `Existing server has different config (hash: ${json.configHash} vs ${this.configHash}), will start new server`,\n );\n resolve(false);\n return;\n }\n resolve(true);\n return;\n }\n resolve(false);\n } catch (error) {\n // Failed to parse JSON or invalid response\n resolve(false);\n }\n });\n });\n\n req.on(\"error\", () => {\n // Connection failed, not our server\n resolve(false);\n });\n\n req.on(\"timeout\", () => {\n req.destroy();\n resolve(false);\n });\n });\n }\n\n /**\n * Handle incoming HTTP request\n */\n private async handleRequest(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n this.logger.info(`š Processing: ${req.method} ${req.url}`);\n\n try {\n const url = new URL(req.url || \"\", `http://${req.headers.host}`);\n\n // Log request\n this.logger.debug(`${req.method} ${url.pathname}`);\n\n // Handle CORS for browser requests\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n res.setHeader(\n \"Access-Control-Expose-Headers\",\n \"Content-Type, Cache-Control\",\n );\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (url.pathname === \"/health\") {\n res.writeHead(200, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n port: this.url,\n configHash: this.configHash,\n }),\n );\n return;\n }\n\n // Batch translation endpoint: POST /translations/:locale\n const postMatch = url.pathname.match(/^\\/translations\\/([^/]+)$/);\n if (postMatch && req.method === \"POST\") {\n const [, locale] = postMatch;\n\n await this.handleBatchTranslationRequest(locale, req, res);\n return;\n }\n\n // Translation dictionary endpoint: GET /translations/:locale\n const dictMatch = url.pathname.match(/^\\/translations\\/([^/]+)$/);\n if (dictMatch && req.method === \"GET\") {\n const [, locale] = dictMatch;\n await this.handleDictionaryRequest(locale, res);\n return;\n }\n\n // 404 for unknown routes\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Not Found\",\n message: \"Unknown endpoint\",\n availableEndpoints: [\n \"GET /health\",\n \"GET /translations/:locale\",\n \"POST /translations/:locale (with body: { hashes: string[] })\",\n ],\n }),\n );\n } catch (error) {\n this.logger.error(\"Error handling request:\", error);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Internal Server Error\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n }\n\n /**\n * Handle batch translation request\n */\n private async handleBatchTranslationRequest(\n locale: string,\n req: http.IncomingMessage,\n res: http.ServerResponse,\n ): Promise<void> {\n try {\n const parsedLocale = parseLocaleOrThrow(locale);\n\n // Read request body\n let body = \"\";\n this.logger.debug(\"Reading request body...\");\n for await (const chunk of req) {\n body += chunk.toString();\n this.logger.debug(`Chunk read, body: ${body}`);\n }\n\n // Parse body\n const { hashes } = JSON.parse(body);\n\n this.logger.debug(`Parsed hashes: ${hashes.join(\",\")}`);\n\n if (!Array.isArray(hashes)) {\n res.writeHead(400, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Bad Request\",\n message: \"Body must contain 'hashes' array\",\n }),\n );\n return;\n }\n // Reload metadata to ensure we have the latest entries\n // (new entries may have been added since server started)\n await this.reloadMetadata();\n\n if (!this.metadata) {\n throw new Error(\"Failed to load metadata\");\n }\n\n this.logger.info(`š Translating ${hashes.length} hashes to ${locale}`);\n this.logger.debug(`š Hashes: ${hashes.join(\", \")}`);\n\n // Mark translation activity start\n this.startTranslationActivity();\n\n try {\n const result = await this.translationService.translate(\n parsedLocale,\n this.metadata,\n hashes,\n );\n\n // Return successful response\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"no-cache\",\n });\n res.end(\n JSON.stringify({\n locale,\n translations: result.translations,\n errors: result.errors,\n }),\n );\n } finally {\n // Mark translation activity end\n this.endTranslationActivity();\n }\n } catch (error) {\n this.logger.error(\n `Error getting batch translations for ${locale}:`,\n error,\n );\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Translation generation failed\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n }\n\n /**\n * Handle request for full translation dictionary\n */\n private async handleDictionaryRequest(\n locale: string,\n res: http.ServerResponse,\n ): Promise<void> {\n try {\n const parsedLocale = parseLocaleOrThrow(locale);\n\n // Reload metadata to ensure we have the latest entries\n // (new entries may have been added since server started)\n await this.reloadMetadata();\n\n if (!this.metadata) {\n throw new Error(\"Failed to load metadata\");\n }\n\n this.logger.info(`š Requesting full dictionary for ${locale}`);\n\n const allHashes = Object.keys(this.metadata);\n\n // Translate all hashes\n const result = await this.translationService.translate(\n parsedLocale,\n this.metadata,\n allHashes,\n );\n\n // Return successful response\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"public, max-age=3600\",\n });\n res.end(\n JSON.stringify({\n locale,\n translations: result.translations,\n errors: result.errors,\n }),\n );\n } catch (error) {\n this.logger.error(`Error getting dictionary for ${locale}:`, error);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n error: \"Translation generation failed\",\n message: error instanceof Error ? error.message : \"Unknown error\",\n }),\n );\n }\n }\n}\n\ntype SerializablePrimitive = string | number | boolean | null | undefined;\n\ntype SerializableObject = {\n [key: string]: SerializableValue;\n};\nexport type SerializableValue =\n | SerializablePrimitive\n | SerializableValue[]\n | SerializableObject;\n\nexport function stableStringify(\n value: Record<string, SerializableValue>,\n): string {\n const normalize = (v: any): any => {\n if (v === undefined) return undefined;\n if (typeof v === \"function\") return undefined;\n if (v === null) return null;\n\n if (Array.isArray(v)) {\n return v.map(normalize).filter((x) => x !== undefined);\n }\n\n if (typeof v === \"object\") {\n const out: Record<string, any> = {};\n for (const key of Object.keys(v).sort()) {\n const next = normalize(v[key]);\n if (next !== undefined) out[key] = next;\n }\n return out;\n }\n\n return v;\n };\n\n return JSON.stringify(normalize(value));\n}\n\n/**\n * Generate a stable hash of a config object\n * Filters out functions and non-serializable values\n * Sorts keys for stability\n */\nexport function hashConfig(config: Record<string, SerializableValue>): string {\n const serialized = stableStringify(config);\n return crypto.createHash(\"md5\").update(serialized).digest(\"hex\").slice(0, 12);\n}\n\nexport async function startTranslationServer(\n options: TranslationServerOptions,\n): Promise<TranslationServer> {\n const server = new TranslationServer(options);\n await server.start();\n return server;\n}\n\n/**\n * Create a translation server and start it or reuse an existing one on the preferred port\n *\n * Since we have little control over the dev server start in next, we can start the translation server only in the async config or in the loader,\n * they both could be run in different processes, and we need a way to avoid starting multiple servers.\n * This one will try to start a server on the preferred port (which seems to be an atomic operation), and if it fails,\n * it checks if the server that is already started is ours and returns its url.\n *\n * @returns Object containing the server instance and its URL\n */\nexport async function startOrGetTranslationServer(\n options: TranslationServerOptions,\n): Promise<{ server: TranslationServer; url: string }> {\n const server = new TranslationServer(options);\n const url = await server.startOrGetUrl();\n return { server, url };\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { dictionaryFrom } from "./api.mjs";
|
|
2
2
|
import { DEFAULT_TIMEOUTS, withTimeout } from "../utils/timeout.mjs";
|
|
3
|
-
import * as fs from "fs/promises";
|
|
3
|
+
import * as fs$1 from "fs/promises";
|
|
4
4
|
import * as path$1 from "path";
|
|
5
5
|
|
|
6
6
|
//#region src/translators/local-cache.ts
|
|
@@ -27,7 +27,7 @@ var LocalTranslationCache = class {
|
|
|
27
27
|
async getDictionary(locale) {
|
|
28
28
|
try {
|
|
29
29
|
const cachePath = this.getCachePath(locale);
|
|
30
|
-
const content = await withTimeout(fs.readFile(cachePath, "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Read cache for ${locale}`);
|
|
30
|
+
const content = await withTimeout(fs$1.readFile(cachePath, "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Read cache for ${locale}`);
|
|
31
31
|
return JSON.parse(content);
|
|
32
32
|
} catch {
|
|
33
33
|
return null;
|
|
@@ -41,8 +41,8 @@ var LocalTranslationCache = class {
|
|
|
41
41
|
try {
|
|
42
42
|
const cachePath = this.getCachePath(locale);
|
|
43
43
|
const cacheDir = path$1.dirname(cachePath);
|
|
44
|
-
await withTimeout(fs.mkdir(cacheDir, { recursive: true }), DEFAULT_TIMEOUTS.FILE_IO, `Create cache directory for ${locale}`);
|
|
45
|
-
await withTimeout(fs.writeFile(cachePath, JSON.stringify(dictionary, null, 2), "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Write cache for ${locale}`);
|
|
44
|
+
await withTimeout(fs$1.mkdir(cacheDir, { recursive: true }), DEFAULT_TIMEOUTS.FILE_IO, `Create cache directory for ${locale}`);
|
|
45
|
+
await withTimeout(fs$1.writeFile(cachePath, JSON.stringify(dictionary, null, 2), "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Write cache for ${locale}`);
|
|
46
46
|
} catch (error) {
|
|
47
47
|
this.logger.error(`Failed to write cache for locale ${locale}:`, error);
|
|
48
48
|
throw error;
|
|
@@ -82,7 +82,7 @@ var LocalTranslationCache = class {
|
|
|
82
82
|
async has(locale) {
|
|
83
83
|
try {
|
|
84
84
|
const cachePath = this.getCachePath(locale);
|
|
85
|
-
await fs.access(cachePath);
|
|
85
|
+
await fs$1.access(cachePath);
|
|
86
86
|
return true;
|
|
87
87
|
} catch {
|
|
88
88
|
return false;
|
|
@@ -94,7 +94,7 @@ var LocalTranslationCache = class {
|
|
|
94
94
|
async clear(locale) {
|
|
95
95
|
try {
|
|
96
96
|
const cachePath = this.getCachePath(locale);
|
|
97
|
-
await fs.unlink(cachePath);
|
|
97
|
+
await fs$1.unlink(cachePath);
|
|
98
98
|
} catch {}
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
@@ -102,8 +102,8 @@ var LocalTranslationCache = class {
|
|
|
102
102
|
*/
|
|
103
103
|
async clearAll() {
|
|
104
104
|
try {
|
|
105
|
-
const files = await fs.readdir(this.config.cacheDir);
|
|
106
|
-
await Promise.all(files.filter((file) => file.endsWith(".json")).map((file) => fs.unlink(path$1.join(this.config.cacheDir, file))));
|
|
105
|
+
const files = await fs$1.readdir(this.config.cacheDir);
|
|
106
|
+
await Promise.all(files.filter((file) => file.endsWith(".json")).map((file) => fs$1.unlink(path$1.join(this.config.cacheDir, file))));
|
|
107
107
|
} catch {}
|
|
108
108
|
}
|
|
109
109
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-cache.mjs","names":["logger: Logger","path"],"sources":["../../src/translators/local-cache.ts"],"sourcesContent":["/**\n * Local disk-based translation cache implementation\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport type { LocalCacheConfig, TranslationCache } from \"./cache\";\nimport { dictionaryFrom, type DictionarySchema } from \"./api\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../utils/timeout\";\nimport type { Logger } from \"../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\n/**\n * Local file system cache for translations\n * Stores translations as JSON files in .lingo/cache/\n */\nexport class LocalTranslationCache implements TranslationCache {\n private config: LocalCacheConfig;\n\n constructor(\n config: LocalCacheConfig,\n private logger: Logger,\n ) {\n this.config = config;\n }\n\n private getCachePath(locale: LocaleCode): string {\n return path.join(this.config.cacheDir, `${locale}.json`);\n }\n\n /**\n * Read dictionary file from disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n async getDictionary(locale: LocaleCode): Promise<DictionarySchema | null> {\n try {\n const cachePath = this.getCachePath(locale);\n const content = await withTimeout(\n fs.readFile(cachePath, \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Read cache for ${locale}`,\n );\n return JSON.parse(content);\n } catch {\n return null;\n }\n }\n\n /**\n * Write dictionary file to disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n private async setDictionary(\n locale: LocaleCode,\n dictionary: DictionarySchema,\n ): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n const cacheDir = path.dirname(cachePath);\n\n // Ensure cache directory exists\n await withTimeout(\n fs.mkdir(cacheDir, { recursive: true }),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Create cache directory for ${locale}`,\n );\n\n // Write cache file\n await withTimeout(\n fs.writeFile(cachePath, JSON.stringify(dictionary, null, 2), \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Write cache for ${locale}`,\n );\n } catch (error) {\n this.logger.error(`Failed to write cache for locale ${locale}:`, error);\n throw error;\n }\n }\n\n /**\n * Get cached translations for a locale\n */\n async get(\n locale: LocaleCode,\n hashes?: string[],\n ): Promise<Record<string, string>> {\n const dictionary = await this.getDictionary(locale);\n if (!dictionary) {\n return {};\n }\n if (hashes) {\n return hashes.reduce(\n (acc, hash) => ({ ...acc, [hash]: dictionary.entries[hash] }),\n {},\n );\n }\n return dictionary.entries || {};\n }\n\n /**\n * Update cache with new translations (merge)\n */\n async update(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n const existing = await this.get(locale);\n\n const merged = { ...existing, ...translations };\n\n await this.set(locale, merged);\n }\n\n /**\n * Replace entire cache for a locale\n */\n async set(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n await this.setDictionary(locale, dictionaryFrom(locale, translations));\n }\n\n /**\n * Check if cache exists for a locale\n */\n async has(locale: LocaleCode): Promise<boolean> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.access(cachePath);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Clear cache for a specific locale\n */\n async clear(locale: LocaleCode): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.unlink(cachePath);\n } catch {\n // Ignore errors if file doesn't exist\n }\n }\n\n /**\n * Clear all cached translations\n */\n async clearAll(): Promise<void> {\n try {\n const files = await fs.readdir(this.config.cacheDir);\n\n await Promise.all(\n files\n .filter((file) => file.endsWith(\".json\"))\n .map((file) => fs.unlink(path.join(this.config.cacheDir, file))),\n );\n } catch {\n // Ignore errors if directory doesn't exist\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,wBAAb,MAA+D;CAC7D,AAAQ;CAER,YACE,QACA,AAAQA,QACR;EADQ;AAER,OAAK,SAAS;;CAGhB,AAAQ,aAAa,QAA4B;AAC/C,SAAOC,OAAK,KAAK,KAAK,OAAO,UAAU,GAAG,OAAO,OAAO;;;;;;CAO1D,MAAM,cAAc,QAAsD;AACxE,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,UAAU,MAAM,
|
|
1
|
+
{"version":3,"file":"local-cache.mjs","names":["logger: Logger","path","fs"],"sources":["../../src/translators/local-cache.ts"],"sourcesContent":["/**\n * Local disk-based translation cache implementation\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport type { LocalCacheConfig, TranslationCache } from \"./cache\";\nimport { dictionaryFrom, type DictionarySchema } from \"./api\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../utils/timeout\";\nimport type { Logger } from \"../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\n/**\n * Local file system cache for translations\n * Stores translations as JSON files in .lingo/cache/\n */\nexport class LocalTranslationCache implements TranslationCache {\n private config: LocalCacheConfig;\n\n constructor(\n config: LocalCacheConfig,\n private logger: Logger,\n ) {\n this.config = config;\n }\n\n private getCachePath(locale: LocaleCode): string {\n return path.join(this.config.cacheDir, `${locale}.json`);\n }\n\n /**\n * Read dictionary file from disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n async getDictionary(locale: LocaleCode): Promise<DictionarySchema | null> {\n try {\n const cachePath = this.getCachePath(locale);\n const content = await withTimeout(\n fs.readFile(cachePath, \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Read cache for ${locale}`,\n );\n return JSON.parse(content);\n } catch {\n return null;\n }\n }\n\n /**\n * Write dictionary file to disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n private async setDictionary(\n locale: LocaleCode,\n dictionary: DictionarySchema,\n ): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n const cacheDir = path.dirname(cachePath);\n\n // Ensure cache directory exists\n await withTimeout(\n fs.mkdir(cacheDir, { recursive: true }),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Create cache directory for ${locale}`,\n );\n\n // Write cache file\n await withTimeout(\n fs.writeFile(cachePath, JSON.stringify(dictionary, null, 2), \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Write cache for ${locale}`,\n );\n } catch (error) {\n this.logger.error(`Failed to write cache for locale ${locale}:`, error);\n throw error;\n }\n }\n\n /**\n * Get cached translations for a locale\n */\n async get(\n locale: LocaleCode,\n hashes?: string[],\n ): Promise<Record<string, string>> {\n const dictionary = await this.getDictionary(locale);\n if (!dictionary) {\n return {};\n }\n if (hashes) {\n return hashes.reduce(\n (acc, hash) => ({ ...acc, [hash]: dictionary.entries[hash] }),\n {},\n );\n }\n return dictionary.entries || {};\n }\n\n /**\n * Update cache with new translations (merge)\n */\n async update(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n const existing = await this.get(locale);\n\n const merged = { ...existing, ...translations };\n\n await this.set(locale, merged);\n }\n\n /**\n * Replace entire cache for a locale\n */\n async set(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n await this.setDictionary(locale, dictionaryFrom(locale, translations));\n }\n\n /**\n * Check if cache exists for a locale\n */\n async has(locale: LocaleCode): Promise<boolean> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.access(cachePath);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Clear cache for a specific locale\n */\n async clear(locale: LocaleCode): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.unlink(cachePath);\n } catch {\n // Ignore errors if file doesn't exist\n }\n }\n\n /**\n * Clear all cached translations\n */\n async clearAll(): Promise<void> {\n try {\n const files = await fs.readdir(this.config.cacheDir);\n\n await Promise.all(\n files\n .filter((file) => file.endsWith(\".json\"))\n .map((file) => fs.unlink(path.join(this.config.cacheDir, file))),\n );\n } catch {\n // Ignore errors if directory doesn't exist\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,wBAAb,MAA+D;CAC7D,AAAQ;CAER,YACE,QACA,AAAQA,QACR;EADQ;AAER,OAAK,SAAS;;CAGhB,AAAQ,aAAa,QAA4B;AAC/C,SAAOC,OAAK,KAAK,KAAK,OAAO,UAAU,GAAG,OAAO,OAAO;;;;;;CAO1D,MAAM,cAAc,QAAsD;AACxE,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,UAAU,MAAM,YACpBC,KAAG,SAAS,WAAW,QAAQ,EAC/B,iBAAiB,SACjB,kBAAkB,SACnB;AACD,UAAO,KAAK,MAAM,QAAQ;UACpB;AACN,UAAO;;;;;;;CAQX,MAAc,cACZ,QACA,YACe;AACf,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,WAAWD,OAAK,QAAQ,UAAU;AAGxC,SAAM,YACJC,KAAG,MAAM,UAAU,EAAE,WAAW,MAAM,CAAC,EACvC,iBAAiB,SACjB,8BAA8B,SAC/B;AAGD,SAAM,YACJA,KAAG,UAAU,WAAW,KAAK,UAAU,YAAY,MAAM,EAAE,EAAE,QAAQ,EACrE,iBAAiB,SACjB,mBAAmB,SACpB;WACM,OAAO;AACd,QAAK,OAAO,MAAM,oCAAoC,OAAO,IAAI,MAAM;AACvE,SAAM;;;;;;CAOV,MAAM,IACJ,QACA,QACiC;EACjC,MAAM,aAAa,MAAM,KAAK,cAAc,OAAO;AACnD,MAAI,CAAC,WACH,QAAO,EAAE;AAEX,MAAI,OACF,QAAO,OAAO,QACX,KAAK,UAAU;GAAE,GAAG;IAAM,OAAO,WAAW,QAAQ;GAAO,GAC5D,EAAE,CACH;AAEH,SAAO,WAAW,WAAW,EAAE;;;;;CAMjC,MAAM,OACJ,QACA,cACe;EAGf,MAAM,SAAS;GAAE,GAFA,MAAM,KAAK,IAAI,OAAO;GAET,GAAG;GAAc;AAE/C,QAAM,KAAK,IAAI,QAAQ,OAAO;;;;;CAMhC,MAAM,IACJ,QACA,cACe;AACf,QAAM,KAAK,cAAc,QAAQ,eAAe,QAAQ,aAAa,CAAC;;;;;CAMxE,MAAM,IAAI,QAAsC;AAC9C,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,SAAMA,KAAG,OAAO,UAAU;AAC1B,UAAO;UACD;AACN,UAAO;;;;;;CAOX,MAAM,MAAM,QAAmC;AAC7C,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,SAAMA,KAAG,OAAO,UAAU;UACpB;;;;;CAQV,MAAM,WAA0B;AAC9B,MAAI;GACF,MAAM,QAAQ,MAAMA,KAAG,QAAQ,KAAK,OAAO,SAAS;AAEpD,SAAM,QAAQ,IACZ,MACG,QAAQ,SAAS,KAAK,SAAS,QAAQ,CAAC,CACxC,KAAK,SAASA,KAAG,OAAOD,OAAK,KAAK,KAAK,OAAO,UAAU,KAAK,CAAC,CAAC,CACnE;UACK"}
|
|
@@ -146,7 +146,7 @@ var PluralizationService = class {
|
|
|
146
146
|
*/
|
|
147
147
|
async process(metadata) {
|
|
148
148
|
const startTime = performance.now();
|
|
149
|
-
const totalEntries = Object.keys(metadata
|
|
149
|
+
const totalEntries = Object.keys(metadata).length;
|
|
150
150
|
if (totalEntries === 0) return {
|
|
151
151
|
total: 0,
|
|
152
152
|
candidates: 0,
|
|
@@ -156,7 +156,7 @@ var PluralizationService = class {
|
|
|
156
156
|
durationMs: 0
|
|
157
157
|
};
|
|
158
158
|
this.logger.debug(`Starting pluralization processing for ${totalEntries} entries`);
|
|
159
|
-
const candidates = require_pattern_detector.detectPluralCandidates(Object.fromEntries(Object.entries(metadata
|
|
159
|
+
const candidates = require_pattern_detector.detectPluralCandidates(Object.fromEntries(Object.entries(metadata).map(([hash, entry]) => [hash, entry.sourceText])), this.logger);
|
|
160
160
|
this.logger.debug(`Found ${candidates.length} plural candidates (${(candidates.length / totalEntries * 100).toFixed(1)}%)`);
|
|
161
161
|
if (candidates.length === 0) return {
|
|
162
162
|
total: totalEntries,
|
|
@@ -174,7 +174,7 @@ var PluralizationService = class {
|
|
|
174
174
|
let failed = 0;
|
|
175
175
|
for (const candidate of candidates) {
|
|
176
176
|
const result = icuResults.get(candidate.hash);
|
|
177
|
-
const entry = metadata
|
|
177
|
+
const entry = metadata[candidate.hash];
|
|
178
178
|
this.logger.debug(`Processing candidate: ${candidate.sourceText}`);
|
|
179
179
|
if (!entry) {
|
|
180
180
|
this.logger.warn(`Entry not found for hash: ${candidate.hash}`);
|
|
@@ -145,7 +145,7 @@ var PluralizationService = class {
|
|
|
145
145
|
*/
|
|
146
146
|
async process(metadata) {
|
|
147
147
|
const startTime = performance.now();
|
|
148
|
-
const totalEntries = Object.keys(metadata
|
|
148
|
+
const totalEntries = Object.keys(metadata).length;
|
|
149
149
|
if (totalEntries === 0) return {
|
|
150
150
|
total: 0,
|
|
151
151
|
candidates: 0,
|
|
@@ -155,7 +155,7 @@ var PluralizationService = class {
|
|
|
155
155
|
durationMs: 0
|
|
156
156
|
};
|
|
157
157
|
this.logger.debug(`Starting pluralization processing for ${totalEntries} entries`);
|
|
158
|
-
const candidates = detectPluralCandidates(Object.fromEntries(Object.entries(metadata
|
|
158
|
+
const candidates = detectPluralCandidates(Object.fromEntries(Object.entries(metadata).map(([hash, entry]) => [hash, entry.sourceText])), this.logger);
|
|
159
159
|
this.logger.debug(`Found ${candidates.length} plural candidates (${(candidates.length / totalEntries * 100).toFixed(1)}%)`);
|
|
160
160
|
if (candidates.length === 0) return {
|
|
161
161
|
total: totalEntries,
|
|
@@ -173,7 +173,7 @@ var PluralizationService = class {
|
|
|
173
173
|
let failed = 0;
|
|
174
174
|
for (const candidate of candidates) {
|
|
175
175
|
const result = icuResults.get(candidate.hash);
|
|
176
|
-
const entry = metadata
|
|
176
|
+
const entry = metadata[candidate.hash];
|
|
177
177
|
this.logger.debug(`Processing candidate: ${candidate.sourceText}`);
|
|
178
178
|
if (!entry) {
|
|
179
179
|
this.logger.warn(`Entry not found for hash: ${candidate.hash}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":["logger: Logger","batchRequest: PluralizationBatch"],"sources":["../../../src/translators/pluralization/service.ts"],"sourcesContent":["/**\n * Pluralization service with batching and caching\n */\n\nimport type { LanguageModel } from \"ai\";\nimport { generateText } from \"ai\";\nimport type {\n ICUGenerationResult,\n PluralCandidate,\n PluralizationBatch,\n PluralizationConfig,\n PluralizationResponse,\n PluralizationStats,\n} from \"./types\";\nimport {\n createAiModel,\n parseModelString,\n validateAndGetApiKeys,\n} from \"../lingo/model-factory\";\nimport { Logger } from \"../../utils/logger\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../../utils/timeout\";\nimport { getSystemPrompt } from \"./prompt\";\nimport { obj2xml, parseXmlFromResponseText } from \"../parse-xml\";\nimport { shots } from \"./shots\";\nimport type { MetadataSchema } from \"../../types\";\nimport { detectPluralCandidates } from \"./pattern-detector\";\nimport { validateICU } from \"./icu-validator\";\n\n/**\n * Pluralization service with batching and model reuse\n */\nexport class PluralizationService {\n private readonly languageModel: LanguageModel;\n private cache = new Map<string, ICUGenerationResult>();\n private readonly prompt: string;\n private readonly sourceLocale: string;\n\n constructor(\n config: PluralizationConfig,\n private logger: Logger,\n ) {\n const localeModel = parseModelString(config.model);\n if (!localeModel) {\n throw new Error(`Invalid model format in pluralization service: \"${config.model}\"`);\n }\n\n // Validate and fetch API keys for the pluralization provider\n // We need to create a models config that validateAndFetchApiKeys can use\n const modelsConfig: Record<string, string> = {\n \"*:*\": config.model,\n };\n\n const validatedKeys = validateAndGetApiKeys(modelsConfig);\n\n this.languageModel = createAiModel(localeModel, validatedKeys);\n this.sourceLocale = config.sourceLocale;\n this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale });\n\n this.logger.debug(\n `Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`,\n );\n }\n\n /**\n * Generate ICU formats for multiple candidates in a single batch\n *\n * @param candidates Array of plural candidates\n * @param batchSize Maximum candidates per batch (default: 10)\n * @returns Map of hash -> ICU generation result\n */\n async generateBatch(\n candidates: PluralCandidate[],\n batchSize: number = 10,\n ): Promise<Map<string, ICUGenerationResult>> {\n const { uncachedCandidates, results } = candidates.reduce(\n (acc, c) => {\n const cached = this.cache.get(c.hash);\n if (cached) {\n acc.results.set(c.hash, cached);\n } else {\n acc.uncachedCandidates.push(c);\n }\n return acc;\n },\n {\n uncachedCandidates: [] as PluralCandidate[],\n results: new Map<string, ICUGenerationResult>(),\n },\n );\n\n if (uncachedCandidates.length === 0) {\n return results;\n }\n\n this.logger.debug(\n `Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`,\n );\n\n // Process in batches\n for (let i = 0; i < uncachedCandidates.length; i += batchSize) {\n const batch = uncachedCandidates.slice(i, i + batchSize);\n\n this.logger.debug(\n `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`,\n );\n\n const batchResults = await this.processBatch(batch);\n\n // Store results and cache them\n for (const [hash, result] of batchResults) {\n results.set(hash, result);\n this.cache.set(hash, result);\n }\n }\n\n return results;\n }\n\n /**\n * Process a single batch of candidates\n */\n private async processBatch(\n candidates: PluralCandidate[],\n ): Promise<Map<string, ICUGenerationResult>> {\n const results = new Map<string, ICUGenerationResult>();\n\n try {\n // Prepare batch request in XML format\n const batchRequest: PluralizationBatch = {\n version: 0.1,\n sourceLocale: this.sourceLocale,\n candidates: {\n candidate: candidates.map((c) => ({\n hash: c.hash,\n text: c.sourceText,\n })),\n },\n };\n\n // Call LLM with XML format and few-shot examples\n const response = await withTimeout(\n generateText({\n model: this.languageModel,\n messages: [\n {\n role: \"system\",\n content: this.prompt,\n },\n // Add few-shot examples\n ...shots.flatMap((shotsTuple) => [\n {\n role: \"user\" as const,\n content: obj2xml(shotsTuple[0]),\n },\n {\n role: \"assistant\" as const,\n content: obj2xml(shotsTuple[1]),\n },\n ]),\n {\n role: \"user\",\n content: obj2xml(batchRequest),\n },\n ],\n }),\n DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch\n `Pluralization with ${this.languageModel}`,\n );\n\n const responseText = response.text.trim();\n this.logger.debug(\n `LLM XML response: ${responseText.substring(0, 200)}...`,\n );\n // Parse XML response\n const parsed =\n parseXmlFromResponseText<PluralizationResponse>(responseText);\n\n // Process results\n const resultArray = Array.isArray(parsed.results.result)\n ? parsed.results.result\n : [parsed.results.result];\n\n for (const result of resultArray) {\n const candidate = candidates.find((c) => c.hash === result.hash);\n if (!candidate) {\n this.logger.warn(`No candidate found for hash: ${result.hash}`);\n continue;\n }\n\n if (result.shouldPluralize && result.icuText) {\n this.logger.debug(\n `ā ICU format generated for \"${candidate.sourceText}\": \"${result.icuText}\"`,\n );\n results.set(result.hash, {\n success: true,\n icuText: result.icuText,\n reasoning: result.reasoning,\n });\n } else {\n this.logger.debug(\n `ā Pluralization not appropriate for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n results.set(result.hash, {\n success: false,\n reasoning: result.reasoning,\n });\n }\n }\n\n // Handle missing results (LLM didn't return result for some candidates)\n for (const candidate of candidates) {\n if (!results.has(candidate.hash)) {\n this.logger.warn(\n `No result returned for a candidate: ${candidate.sourceText}`,\n );\n results.set(candidate.hash, {\n success: false,\n error: \"No result returned by LLM\",\n });\n }\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"Unknown error\";\n this.logger.error(`Failed to process batch: ${errorMsg}`);\n\n // Mark all candidates as failed\n for (const candidate of candidates) {\n results.set(candidate.hash, {\n success: false,\n error: errorMsg,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Process metadata entries for pluralization\n *\n * This is the main entry point that:\n * 1. Detects plural candidates using pattern matching\n * 2. Generates ICU format using LLM (batched)\n * 3. Validates the ICU format\n * 4. Updates metadata entries in-place (modifies sourceText)\n * 5. Returns statistics\n * @param metadata Metadata schema with translation entries\n\n * @returns Statistics about the pluralization process\n */\n async process(metadata: MetadataSchema): Promise<PluralizationStats> {\n const startTime = performance.now();\n const totalEntries = Object.keys(metadata.entries).length;\n\n if (totalEntries === 0) {\n return {\n total: 0,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: 0,\n };\n }\n\n this.logger.debug(\n `Starting pluralization processing for ${totalEntries} entries`,\n );\n\n // Step 1: Detect plural candidates using pattern matching\n const entriesMap: Record<string, string> = Object.fromEntries(\n Object.entries(metadata.entries).map(([hash, entry]) => [\n hash,\n entry.sourceText,\n ]),\n );\n\n const candidates = detectPluralCandidates(entriesMap, this.logger);\n\n this.logger.debug(\n `Found ${candidates.length} plural candidates (${((candidates.length / totalEntries) * 100).toFixed(1)}%)`,\n );\n\n if (candidates.length === 0) {\n const endTime = performance.now();\n return {\n total: totalEntries,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: endTime - startTime,\n };\n }\n\n // Step 2: Generate ICU formats with batching\n this.logger.debug(\"Generating ICU formats with batching...\");\n const icuResults = await this.generateBatch(candidates, 10);\n\n // Step 3: Validate and update metadata entries\n this.logger.debug(\"Validating and updating entries...\");\n let pluralized = 0;\n let rejected = 0;\n let failed = 0;\n\n for (const candidate of candidates) {\n const result = icuResults.get(candidate.hash);\n const entry = metadata.entries[candidate.hash];\n this.logger.debug(`Processing candidate: ${candidate.sourceText}`);\n if (!entry) {\n this.logger.warn(`Entry not found for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (!result) {\n this.logger.warn(`No result for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (result.error) {\n this.logger.warn(\n `Error generating ICU for \"${candidate.sourceText}\": ${result.error}`,\n );\n failed++;\n continue;\n }\n\n if (!result.success || !result.icuText) {\n this.logger.debug(\n `Rejected pluralization for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n rejected++;\n continue;\n }\n\n const isValid = validateICU(\n result.icuText,\n candidate.sourceText,\n this.logger,\n );\n\n if (!isValid) {\n this.logger.warn(\n `Invalid ICU format generated for \"${candidate.sourceText}\", falling back to original`,\n );\n failed++;\n continue;\n }\n\n this.logger.debug(\n `Pluralizing: \"${entry.sourceText}\" -> \"${result.icuText}\"`,\n );\n entry.sourceText = result.icuText;\n pluralized++;\n }\n\n const endTime = performance.now();\n const duration = endTime - startTime;\n\n this.logger.debug(\n `Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`,\n );\n\n return {\n total: totalEntries,\n candidates: candidates.length,\n pluralized,\n rejected,\n failed,\n durationMs: duration,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":["logger: Logger","batchRequest: PluralizationBatch"],"sources":["../../../src/translators/pluralization/service.ts"],"sourcesContent":["/**\n * Pluralization service with batching and caching\n */\n\nimport type { LanguageModel } from \"ai\";\nimport { generateText } from \"ai\";\nimport type {\n ICUGenerationResult,\n PluralCandidate,\n PluralizationBatch,\n PluralizationConfig,\n PluralizationResponse,\n PluralizationStats,\n} from \"./types\";\nimport {\n createAiModel,\n parseModelString,\n validateAndGetApiKeys,\n} from \"../lingo/model-factory\";\nimport { Logger } from \"../../utils/logger\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../../utils/timeout\";\nimport { getSystemPrompt } from \"./prompt\";\nimport { obj2xml, parseXmlFromResponseText } from \"../parse-xml\";\nimport { shots } from \"./shots\";\nimport type { MetadataSchema } from \"../../types\";\nimport { detectPluralCandidates } from \"./pattern-detector\";\nimport { validateICU } from \"./icu-validator\";\n\n/**\n * Pluralization service with batching and model reuse\n */\nexport class PluralizationService {\n private readonly languageModel: LanguageModel;\n private cache = new Map<string, ICUGenerationResult>();\n private readonly prompt: string;\n private readonly sourceLocale: string;\n\n constructor(\n config: PluralizationConfig,\n private logger: Logger,\n ) {\n const localeModel = parseModelString(config.model);\n if (!localeModel) {\n throw new Error(\n `Invalid model format in pluralization service: \"${config.model}\"`,\n );\n }\n\n // Validate and fetch API keys for the pluralization provider\n // We need to create a models config that validateAndFetchApiKeys can use\n const modelsConfig: Record<string, string> = {\n \"*:*\": config.model,\n };\n\n const validatedKeys = validateAndGetApiKeys(modelsConfig);\n\n this.languageModel = createAiModel(localeModel, validatedKeys);\n this.sourceLocale = config.sourceLocale;\n this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale });\n\n this.logger.debug(\n `Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`,\n );\n }\n\n /**\n * Generate ICU formats for multiple candidates in a single batch\n *\n * @param candidates Array of plural candidates\n * @param batchSize Maximum candidates per batch (default: 10)\n * @returns Map of hash -> ICU generation result\n */\n async generateBatch(\n candidates: PluralCandidate[],\n batchSize: number = 10,\n ): Promise<Map<string, ICUGenerationResult>> {\n const { uncachedCandidates, results } = candidates.reduce(\n (acc, c) => {\n const cached = this.cache.get(c.hash);\n if (cached) {\n acc.results.set(c.hash, cached);\n } else {\n acc.uncachedCandidates.push(c);\n }\n return acc;\n },\n {\n uncachedCandidates: [] as PluralCandidate[],\n results: new Map<string, ICUGenerationResult>(),\n },\n );\n\n if (uncachedCandidates.length === 0) {\n return results;\n }\n\n this.logger.debug(\n `Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`,\n );\n\n // Process in batches\n for (let i = 0; i < uncachedCandidates.length; i += batchSize) {\n const batch = uncachedCandidates.slice(i, i + batchSize);\n\n this.logger.debug(\n `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`,\n );\n\n const batchResults = await this.processBatch(batch);\n\n // Store results and cache them\n for (const [hash, result] of batchResults) {\n results.set(hash, result);\n this.cache.set(hash, result);\n }\n }\n\n return results;\n }\n\n /**\n * Process a single batch of candidates\n */\n private async processBatch(\n candidates: PluralCandidate[],\n ): Promise<Map<string, ICUGenerationResult>> {\n const results = new Map<string, ICUGenerationResult>();\n\n try {\n // Prepare batch request in XML format\n const batchRequest: PluralizationBatch = {\n version: 0.1,\n sourceLocale: this.sourceLocale,\n candidates: {\n candidate: candidates.map((c) => ({\n hash: c.hash,\n text: c.sourceText,\n })),\n },\n };\n\n // Call LLM with XML format and few-shot examples\n const response = await withTimeout(\n generateText({\n model: this.languageModel,\n messages: [\n {\n role: \"system\",\n content: this.prompt,\n },\n // Add few-shot examples\n ...shots.flatMap((shotsTuple) => [\n {\n role: \"user\" as const,\n content: obj2xml(shotsTuple[0]),\n },\n {\n role: \"assistant\" as const,\n content: obj2xml(shotsTuple[1]),\n },\n ]),\n {\n role: \"user\",\n content: obj2xml(batchRequest),\n },\n ],\n }),\n DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch\n `Pluralization with ${this.languageModel}`,\n );\n\n const responseText = response.text.trim();\n this.logger.debug(\n `LLM XML response: ${responseText.substring(0, 200)}...`,\n );\n // Parse XML response\n const parsed =\n parseXmlFromResponseText<PluralizationResponse>(responseText);\n\n // Process results\n const resultArray = Array.isArray(parsed.results.result)\n ? parsed.results.result\n : [parsed.results.result];\n\n for (const result of resultArray) {\n const candidate = candidates.find((c) => c.hash === result.hash);\n if (!candidate) {\n this.logger.warn(`No candidate found for hash: ${result.hash}`);\n continue;\n }\n\n if (result.shouldPluralize && result.icuText) {\n this.logger.debug(\n `ā ICU format generated for \"${candidate.sourceText}\": \"${result.icuText}\"`,\n );\n results.set(result.hash, {\n success: true,\n icuText: result.icuText,\n reasoning: result.reasoning,\n });\n } else {\n this.logger.debug(\n `ā Pluralization not appropriate for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n results.set(result.hash, {\n success: false,\n reasoning: result.reasoning,\n });\n }\n }\n\n // Handle missing results (LLM didn't return result for some candidates)\n for (const candidate of candidates) {\n if (!results.has(candidate.hash)) {\n this.logger.warn(\n `No result returned for a candidate: ${candidate.sourceText}`,\n );\n results.set(candidate.hash, {\n success: false,\n error: \"No result returned by LLM\",\n });\n }\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : \"Unknown error\";\n this.logger.error(`Failed to process batch: ${errorMsg}`);\n\n // Mark all candidates as failed\n for (const candidate of candidates) {\n results.set(candidate.hash, {\n success: false,\n error: errorMsg,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Process metadata entries for pluralization\n *\n * This is the main entry point that:\n * 1. Detects plural candidates using pattern matching\n * 2. Generates ICU format using LLM (batched)\n * 3. Validates the ICU format\n * 4. Updates metadata entries in-place (modifies sourceText)\n * 5. Returns statistics\n * @param metadata Metadata schema with translation entries\n\n * @returns Statistics about the pluralization process\n */\n async process(metadata: MetadataSchema): Promise<PluralizationStats> {\n const startTime = performance.now();\n const totalEntries = Object.keys(metadata).length;\n\n if (totalEntries === 0) {\n return {\n total: 0,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: 0,\n };\n }\n\n this.logger.debug(\n `Starting pluralization processing for ${totalEntries} entries`,\n );\n\n // Step 1: Detect plural candidates using pattern matching\n const entriesMap: Record<string, string> = Object.fromEntries(\n Object.entries(metadata).map(([hash, entry]) => [hash, entry.sourceText]),\n );\n\n const candidates = detectPluralCandidates(entriesMap, this.logger);\n\n this.logger.debug(\n `Found ${candidates.length} plural candidates (${((candidates.length / totalEntries) * 100).toFixed(1)}%)`,\n );\n\n if (candidates.length === 0) {\n const endTime = performance.now();\n return {\n total: totalEntries,\n candidates: 0,\n pluralized: 0,\n rejected: 0,\n failed: 0,\n durationMs: endTime - startTime,\n };\n }\n\n // Step 2: Generate ICU formats with batching\n this.logger.debug(\"Generating ICU formats with batching...\");\n const icuResults = await this.generateBatch(candidates, 10);\n\n // Step 3: Validate and update metadata entries\n this.logger.debug(\"Validating and updating entries...\");\n let pluralized = 0;\n let rejected = 0;\n let failed = 0;\n\n for (const candidate of candidates) {\n const result = icuResults.get(candidate.hash);\n const entry = metadata[candidate.hash];\n this.logger.debug(`Processing candidate: ${candidate.sourceText}`);\n if (!entry) {\n this.logger.warn(`Entry not found for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (!result) {\n this.logger.warn(`No result for hash: ${candidate.hash}`);\n failed++;\n continue;\n }\n\n if (result.error) {\n this.logger.warn(\n `Error generating ICU for \"${candidate.sourceText}\": ${result.error}`,\n );\n failed++;\n continue;\n }\n\n if (!result.success || !result.icuText) {\n this.logger.debug(\n `Rejected pluralization for \"${candidate.sourceText}\": ${result.reasoning}`,\n );\n rejected++;\n continue;\n }\n\n const isValid = validateICU(\n result.icuText,\n candidate.sourceText,\n this.logger,\n );\n\n if (!isValid) {\n this.logger.warn(\n `Invalid ICU format generated for \"${candidate.sourceText}\", falling back to original`,\n );\n failed++;\n continue;\n }\n\n this.logger.debug(\n `Pluralizing: \"${entry.sourceText}\" -> \"${result.icuText}\"`,\n );\n entry.sourceText = result.icuText;\n pluralized++;\n }\n\n const endTime = performance.now();\n const duration = endTime - startTime;\n\n this.logger.debug(\n `Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`,\n );\n\n return {\n total: totalEntries,\n candidates: candidates.length,\n pluralized,\n rejected,\n failed,\n durationMs: duration,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -51,7 +51,7 @@ Set the required API keys for real translations.`);
|
|
|
51
51
|
*/
|
|
52
52
|
async translate(locale, metadata, requestedHashes) {
|
|
53
53
|
const startTime = performance.now();
|
|
54
|
-
const workingHashes = requestedHashes || Object.keys(metadata
|
|
54
|
+
const workingHashes = requestedHashes || Object.keys(metadata);
|
|
55
55
|
this.logger.debug(`Translation requested for ${workingHashes.length} hashes in locale: ${locale}`);
|
|
56
56
|
const cachedTranslations = await this.cache.get(locale);
|
|
57
57
|
const uncachedHashes = workingHashes.filter((hash) => !cachedTranslations[hash]);
|
|
@@ -68,12 +68,9 @@ Set the required API keys for real translations.`);
|
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
70
|
this.logger.debug(`Generating translations for ${uncachedHashes.length} uncached hashes in ${locale}...`);
|
|
71
|
-
const filteredMetadata =
|
|
72
|
-
...metadata,
|
|
73
|
-
entries: Object.fromEntries(uncachedHashes.map((hash) => [hash, metadata.entries[hash]]).filter(([_, entry]) => entry !== void 0))
|
|
74
|
-
};
|
|
71
|
+
const filteredMetadata = Object.fromEntries(uncachedHashes.map((hash) => [hash, metadata[hash]]).filter(([_, entry]) => entry !== void 0));
|
|
75
72
|
if (this.pluralizationService) {
|
|
76
|
-
this.logger.debug(`Processing pluralization for ${Object.keys(filteredMetadata
|
|
73
|
+
this.logger.debug(`Processing pluralization for ${Object.keys(filteredMetadata).length} entries...`);
|
|
77
74
|
const pluralStats = await this.pluralizationService.process(filteredMetadata);
|
|
78
75
|
this.logger.debug(`Pluralization stats: ${pluralStats.pluralized} pluralized, ${pluralStats.rejected} rejected, ${pluralStats.failed} failed`);
|
|
79
76
|
}
|
|
@@ -81,14 +78,13 @@ Set the required API keys for real translations.`);
|
|
|
81
78
|
const hashesNeedingTranslation = [];
|
|
82
79
|
this.logger.debug(`Checking for overrides in ${uncachedHashes.length} entries`);
|
|
83
80
|
for (const hash of uncachedHashes) {
|
|
84
|
-
const entry = filteredMetadata
|
|
81
|
+
const entry = filteredMetadata[hash];
|
|
85
82
|
if (!entry) continue;
|
|
86
83
|
if (entry.overrides && entry.overrides[locale]) {
|
|
87
84
|
overriddenTranslations[hash] = entry.overrides[locale];
|
|
88
85
|
this.logger.debug(`Using override for ${hash} in locale ${locale}: "${entry.overrides[locale]}"`);
|
|
89
86
|
} else hashesNeedingTranslation.push(hash);
|
|
90
87
|
}
|
|
91
|
-
Object.keys(overriddenTranslations).length;
|
|
92
88
|
const entriesToTranslate = this.prepareEntries(filteredMetadata, hashesNeedingTranslation);
|
|
93
89
|
let newTranslations = { ...overriddenTranslations };
|
|
94
90
|
const errors = [];
|
|
@@ -121,7 +117,7 @@ Set the required API keys for real translations.`);
|
|
|
121
117
|
};
|
|
122
118
|
}
|
|
123
119
|
for (const hash of uncachedHashes) if (!newTranslations[hash]) {
|
|
124
|
-
const entry = filteredMetadata
|
|
120
|
+
const entry = filteredMetadata[hash];
|
|
125
121
|
errors.push({
|
|
126
122
|
hash,
|
|
127
123
|
sourceText: entry?.sourceText || "",
|
|
@@ -158,7 +154,7 @@ Set the required API keys for real translations.`);
|
|
|
158
154
|
prepareEntries(metadata, hashes) {
|
|
159
155
|
const entries = {};
|
|
160
156
|
for (const hash of hashes) {
|
|
161
|
-
const entry = metadata
|
|
157
|
+
const entry = metadata[hash];
|
|
162
158
|
if (entry) entries[hash] = {
|
|
163
159
|
text: entry.sourceText,
|
|
164
160
|
context: entry.context || {}
|
|
@@ -51,7 +51,7 @@ Set the required API keys for real translations.`);
|
|
|
51
51
|
*/
|
|
52
52
|
async translate(locale, metadata, requestedHashes) {
|
|
53
53
|
const startTime = performance.now();
|
|
54
|
-
const workingHashes = requestedHashes || Object.keys(metadata
|
|
54
|
+
const workingHashes = requestedHashes || Object.keys(metadata);
|
|
55
55
|
this.logger.debug(`Translation requested for ${workingHashes.length} hashes in locale: ${locale}`);
|
|
56
56
|
const cachedTranslations = await this.cache.get(locale);
|
|
57
57
|
const uncachedHashes = workingHashes.filter((hash) => !cachedTranslations[hash]);
|
|
@@ -68,12 +68,9 @@ Set the required API keys for real translations.`);
|
|
|
68
68
|
}
|
|
69
69
|
};
|
|
70
70
|
this.logger.debug(`Generating translations for ${uncachedHashes.length} uncached hashes in ${locale}...`);
|
|
71
|
-
const filteredMetadata =
|
|
72
|
-
...metadata,
|
|
73
|
-
entries: Object.fromEntries(uncachedHashes.map((hash) => [hash, metadata.entries[hash]]).filter(([_, entry]) => entry !== void 0))
|
|
74
|
-
};
|
|
71
|
+
const filteredMetadata = Object.fromEntries(uncachedHashes.map((hash) => [hash, metadata[hash]]).filter(([_, entry]) => entry !== void 0));
|
|
75
72
|
if (this.pluralizationService) {
|
|
76
|
-
this.logger.debug(`Processing pluralization for ${Object.keys(filteredMetadata
|
|
73
|
+
this.logger.debug(`Processing pluralization for ${Object.keys(filteredMetadata).length} entries...`);
|
|
77
74
|
const pluralStats = await this.pluralizationService.process(filteredMetadata);
|
|
78
75
|
this.logger.debug(`Pluralization stats: ${pluralStats.pluralized} pluralized, ${pluralStats.rejected} rejected, ${pluralStats.failed} failed`);
|
|
79
76
|
}
|
|
@@ -81,14 +78,13 @@ Set the required API keys for real translations.`);
|
|
|
81
78
|
const hashesNeedingTranslation = [];
|
|
82
79
|
this.logger.debug(`Checking for overrides in ${uncachedHashes.length} entries`);
|
|
83
80
|
for (const hash of uncachedHashes) {
|
|
84
|
-
const entry = filteredMetadata
|
|
81
|
+
const entry = filteredMetadata[hash];
|
|
85
82
|
if (!entry) continue;
|
|
86
83
|
if (entry.overrides && entry.overrides[locale]) {
|
|
87
84
|
overriddenTranslations[hash] = entry.overrides[locale];
|
|
88
85
|
this.logger.debug(`Using override for ${hash} in locale ${locale}: "${entry.overrides[locale]}"`);
|
|
89
86
|
} else hashesNeedingTranslation.push(hash);
|
|
90
87
|
}
|
|
91
|
-
Object.keys(overriddenTranslations).length;
|
|
92
88
|
const entriesToTranslate = this.prepareEntries(filteredMetadata, hashesNeedingTranslation);
|
|
93
89
|
let newTranslations = { ...overriddenTranslations };
|
|
94
90
|
const errors = [];
|
|
@@ -121,7 +117,7 @@ Set the required API keys for real translations.`);
|
|
|
121
117
|
};
|
|
122
118
|
}
|
|
123
119
|
for (const hash of uncachedHashes) if (!newTranslations[hash]) {
|
|
124
|
-
const entry = filteredMetadata
|
|
120
|
+
const entry = filteredMetadata[hash];
|
|
125
121
|
errors.push({
|
|
126
122
|
hash,
|
|
127
123
|
sourceText: entry?.sourceText || "",
|
|
@@ -158,7 +154,7 @@ Set the required API keys for real translations.`);
|
|
|
158
154
|
prepareEntries(metadata, hashes) {
|
|
159
155
|
const entries = {};
|
|
160
156
|
for (const hash of hashes) {
|
|
161
|
-
const entry = metadata
|
|
157
|
+
const entry = metadata[hash];
|
|
162
158
|
if (entry) entries[hash] = {
|
|
163
159
|
text: entry.sourceText,
|
|
164
160
|
context: entry.context || {}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"translation-service.mjs","names":["config: TranslationServiceConfig","logger: Logger","filteredMetadata: MetadataSchema","overriddenTranslations: Record<string, string>","hashesNeedingTranslation: string[]","newTranslations: Record<string, string>","errors: TranslationError[]","entries: Record<string, TranslatableEntry>","result: Record<string, string>"],"sources":["../../src/translators/translation-service.ts"],"sourcesContent":["/**\n * TranslationService - Main orchestrator for translation workflow\n *\n * Responsibilities:\n * - Coordinates between metadata, cache, and translator\n * - Determines what needs translation\n * - Handles caching strategy\n * - Manages partial failures\n */\n\nimport type { TranslationCache } from \"./cache\";\nimport type { TranslatableEntry, Translator } from \"./api\";\nimport type { LingoEnvironment, MetadataSchema } from \"../types\";\nimport {\n type PluralizationConfig,\n PluralizationService,\n} from \"./pluralization\";\nimport type { Logger } from \"../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\nimport { PseudoTranslator } from \"./pseudotranslator\";\nimport { LingoTranslator } from \"./lingo\";\nimport { type CacheConfig, createCache } from \"./cache-factory\";\nimport { MemoryTranslationCache } from \"./memory-cache\";\n\nexport type TranslationServiceConfig = {\n /**\n * Source locale (e.g., \"en\")\n */\n sourceLocale: LocaleCode;\n\n /**\n * Pluralization configuration\n * If provided, enables automatic pluralization of source messages\n */\n pluralization: Omit<PluralizationConfig, \"sourceLocale\">;\n models: \"lingo.dev\" | Record<string, string>;\n prompt?: string;\n environment: LingoEnvironment;\n dev?: {\n usePseudotranslator?: boolean;\n };\n} & CacheConfig;\n\nexport interface TranslationResult {\n /**\n * Successfully translated entries (hash -> translated text)\n */\n translations: Record<string, string>;\n\n errors: TranslationError[];\n\n stats: {\n total: number;\n cached: number;\n translated: number;\n failed: number;\n };\n}\n\nexport interface TranslationError {\n hash: string;\n sourceText: string;\n error: string;\n}\n\nexport class TranslationService {\n private pluralizationService?: PluralizationService;\n private translator: Translator<any>;\n private cache: TranslationCache;\n\n constructor(\n private config: TranslationServiceConfig,\n private logger: Logger,\n ) {\n const isDev = config.environment === \"development\";\n\n // 1. Explicit dev override takes precedence\n if (isDev && config.dev?.usePseudotranslator) {\n this.logger.info(\n \"š Using pseudotranslator (dev.usePseudotranslator enabled)\",\n );\n this.translator = new PseudoTranslator({ delayMedian: 100 }, logger);\n this.cache = new MemoryTranslationCache();\n } else {\n // 2. Try to create real translator\n // LingoTranslator constructor will validate and fetch API keys\n // If validation fails, it will throw an error with helpful message\n try {\n const models = config.models;\n\n this.logger.debug(\n `Creating Lingo translator with models: ${JSON.stringify(models)}`,\n );\n\n this.cache = createCache(config);\n this.translator = new LingoTranslator(\n {\n models,\n sourceLocale: config.sourceLocale,\n prompt: config.prompt,\n },\n this.logger,\n );\n\n if (this.config.pluralization?.enabled) {\n this.pluralizationService = new PluralizationService(\n {\n ...this.config.pluralization,\n sourceLocale: this.config.sourceLocale,\n },\n this.logger,\n );\n }\n } catch (error) {\n // 3. Auto-fallback in dev mode if creation fails\n if (isDev) {\n // Use console.error to ensure visibility in all contexts (loader, server, etc.)\n const errorMsg =\n error instanceof Error ? error.message : String(error);\n this.logger.warn(`ā ļø Translation setup error: \\n${errorMsg}\\n\nā ļø Auto-fallback to pseudotranslator in development mode.\nSet the required API keys for real translations.`);\n\n this.translator = new PseudoTranslator(\n { delayMedian: 100 },\n this.logger,\n );\n this.cache = new MemoryTranslationCache();\n } else {\n // 4. Fail in production\n throw error;\n }\n }\n }\n }\n\n /**\n * Translate entries to target locale\n *\n * @param locale Target locale (including source locale)\n * @param metadata Metadata schema with all entries\n * @param requestedHashes Optional: only translate specific hashes\n * @returns Translation result with translations and errors\n */\n async translate(\n locale: LocaleCode,\n metadata: MetadataSchema,\n requestedHashes?: string[],\n ): Promise<TranslationResult> {\n const startTime = performance.now();\n\n // Step 1: Determine which hashes we need to work with\n const workingHashes = requestedHashes || Object.keys(metadata.entries);\n\n this.logger.debug(\n `Translation requested for ${workingHashes.length} hashes in locale: ${locale}`,\n );\n\n // Step 2: Check cache first (same for all locales, including source)\n const cachedTranslations = await this.cache.get(locale);\n\n // Step 3: Determine what needs translation/pluralization\n const uncachedHashes = workingHashes.filter(\n (hash) => !cachedTranslations[hash],\n );\n this.logger.debug(\n `${uncachedHashes.length} hashes need processing, ${workingHashes.length - uncachedHashes.length} are cached`,\n );\n\n const cachedCount = workingHashes.length - uncachedHashes.length;\n\n if (uncachedHashes.length === 0) {\n return {\n translations: this.pickTranslations(cachedTranslations, workingHashes),\n errors: [],\n stats: {\n total: workingHashes.length,\n cached: cachedCount,\n translated: 0,\n failed: 0,\n },\n };\n }\n\n this.logger.debug(\n `Generating translations for ${uncachedHashes.length} uncached hashes in ${locale}...`,\n );\n\n // Step 4: Filter metadata to only uncached entries\n const filteredMetadata: MetadataSchema = {\n ...metadata,\n entries: Object.fromEntries(\n uncachedHashes\n .map((hash) => [hash, metadata.entries[hash]])\n .filter(([_, entry]) => entry !== undefined),\n ),\n };\n\n // Step 5: Process pluralization for filtered entries\n if (this.pluralizationService) {\n this.logger.debug(\n `Processing pluralization for ${Object.keys(filteredMetadata.entries).length} entries...`,\n );\n const pluralStats =\n await this.pluralizationService.process(filteredMetadata);\n this.logger.debug(\n `Pluralization stats: ${pluralStats.pluralized} pluralized, ${pluralStats.rejected} rejected, ${pluralStats.failed} failed`,\n );\n }\n\n // Step 6: Separate overridden entries from entries that need translation\n const overriddenTranslations: Record<string, string> = {};\n const hashesNeedingTranslation: string[] = [];\n\n this.logger.debug(\n `Checking for overrides in ${uncachedHashes.length} entries`,\n );\n\n for (const hash of uncachedHashes) {\n const entry = filteredMetadata.entries[hash];\n if (!entry) continue;\n\n // Check if this entry has an override for the current locale\n if (entry.overrides && entry.overrides[locale]) {\n overriddenTranslations[hash] = entry.overrides[locale];\n this.logger.debug(\n `Using override for ${hash} in locale ${locale}: \"${entry.overrides[locale]}\"`,\n );\n } else {\n hashesNeedingTranslation.push(hash);\n }\n }\n\n const overrideCount = Object.keys(overriddenTranslations).length;\n\n // Step 7: Prepare entries for translation (excluding overridden ones)\n const entriesToTranslate = this.prepareEntries(\n filteredMetadata,\n hashesNeedingTranslation,\n );\n\n // Step 8: Translate or return source text\n let newTranslations: Record<string, string> = { ...overriddenTranslations };\n const errors: TranslationError[] = [];\n\n if (locale === this.config.sourceLocale) {\n // For source locale, just return the (possibly pluralized) sourceText\n this.logger.debug(\n `Source locale detected, returning sourceText for ${hashesNeedingTranslation.length} entries`,\n );\n for (const [hash, entry] of Object.entries(entriesToTranslate)) {\n newTranslations[hash] = entry.text;\n }\n } else if (Object.keys(entriesToTranslate).length > 0) {\n // For other locales, translate only entries without overrides\n try {\n this.logger.debug(\n `Translating ${locale} with ${Object.keys(entriesToTranslate).length} entries`,\n );\n const translatedTexts = await this.translator.translate(\n locale,\n entriesToTranslate,\n );\n // Merge translated texts with overridden translations\n newTranslations = { ...overriddenTranslations, ...translatedTexts };\n } catch (error) {\n this.logger.error(`Translation failed:`, error);\n\n return {\n translations: this.pickTranslations(\n cachedTranslations,\n workingHashes,\n ),\n errors: [\n {\n hash: \"all\",\n sourceText: \"all\",\n error:\n error instanceof Error\n ? error.message\n : \"Unknown translation error\",\n },\n ],\n stats: {\n total: workingHashes.length,\n cached: cachedCount,\n translated: 0,\n failed: uncachedHashes.length,\n },\n };\n }\n\n // Check for partial failures (some hashes didn't get translated)\n for (const hash of uncachedHashes) {\n if (!newTranslations[hash]) {\n const entry = filteredMetadata.entries[hash];\n errors.push({\n hash,\n sourceText: entry?.sourceText || \"\",\n error: \"Translator doesn't return translation\",\n });\n }\n }\n }\n\n // Step 5: Update cache with successful translations (skip for pseudo)\n if (Object.keys(newTranslations).length > 0) {\n try {\n await this.cache.update(locale, newTranslations);\n } catch (error) {\n this.logger.error(`Failed to update cache:`, error);\n // Don't fail the request if cache update fails\n }\n }\n\n // Step 6: Merge and return\n const allTranslations = { ...cachedTranslations, ...newTranslations };\n const result = this.pickTranslations(allTranslations, workingHashes);\n\n const endTime = performance.now();\n this.logger.debug(\n `Translation completed for ${locale}: ${Object.keys(newTranslations).length} new, ${cachedCount} cached, ${errors.length} errors in ${(endTime - startTime).toFixed(2)}ms`,\n );\n\n return {\n translations: result,\n errors,\n stats: {\n total: workingHashes.length,\n cached: cachedCount,\n translated: Object.keys(newTranslations).length,\n failed: errors.length,\n },\n };\n }\n\n /**\n * Prepare metadata entries for translation\n */\n private prepareEntries(\n metadata: MetadataSchema,\n hashes: string[],\n ): Record<string, TranslatableEntry> {\n const entries: Record<string, TranslatableEntry> = {};\n\n for (const hash of hashes) {\n const entry = metadata.entries[hash];\n if (entry) {\n entries[hash] = {\n text: entry.sourceText,\n context: entry.context || {},\n };\n }\n }\n\n return entries;\n }\n\n /**\n * Pick only requested translations from the full set\n */\n // TODO (AleksandrSl 14/12/2025): SHould I use this in the build somehow?\n private pickTranslations(\n allTranslations: Record<string, string>,\n requestedHashes: string[],\n ): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const hash of requestedHashes) {\n if (allTranslations[hash]) {\n result[hash] = allTranslations[hash];\n }\n }\n\n return result;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"translation-service.mjs","names":["config: TranslationServiceConfig","logger: Logger","filteredMetadata: MetadataSchema","overriddenTranslations: Record<string, string>","hashesNeedingTranslation: string[]","newTranslations: Record<string, string>","errors: TranslationError[]","entries: Record<string, TranslatableEntry>","result: Record<string, string>"],"sources":["../../src/translators/translation-service.ts"],"sourcesContent":["/**\n * TranslationService - Main orchestrator for translation workflow\n *\n * Responsibilities:\n * - Coordinates between metadata, cache, and translator\n * - Determines what needs translation\n * - Handles caching strategy\n * - Manages partial failures\n */\n\nimport type { TranslationCache } from \"./cache\";\nimport type { TranslatableEntry, Translator } from \"./api\";\nimport type { LingoEnvironment, MetadataSchema } from \"../types\";\nimport {\n type PluralizationConfig,\n PluralizationService,\n} from \"./pluralization\";\nimport type { Logger } from \"../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\nimport { PseudoTranslator } from \"./pseudotranslator\";\nimport { LingoTranslator } from \"./lingo\";\nimport { type CacheConfig, createCache } from \"./cache-factory\";\nimport { MemoryTranslationCache } from \"./memory-cache\";\n\nexport type TranslationServiceConfig = {\n /**\n * Source locale (e.g., \"en\")\n */\n sourceLocale: LocaleCode;\n\n /**\n * Pluralization configuration\n * If provided, enables automatic pluralization of source messages\n */\n pluralization: Omit<PluralizationConfig, \"sourceLocale\">;\n models: \"lingo.dev\" | Record<string, string>;\n prompt?: string;\n environment: LingoEnvironment;\n dev?: {\n usePseudotranslator?: boolean;\n };\n} & CacheConfig;\n\nexport interface TranslationResult {\n /**\n * Successfully translated entries (hash -> translated text)\n */\n translations: Record<string, string>;\n\n errors: TranslationError[];\n\n stats: {\n total: number;\n cached: number;\n translated: number;\n failed: number;\n };\n}\n\nexport interface TranslationError {\n hash: string;\n sourceText: string;\n error: string;\n}\n\nexport class TranslationService {\n private pluralizationService?: PluralizationService;\n private translator: Translator<any>;\n private cache: TranslationCache;\n\n constructor(\n private config: TranslationServiceConfig,\n private logger: Logger,\n ) {\n const isDev = config.environment === \"development\";\n\n // 1. Explicit dev override takes precedence\n if (isDev && config.dev?.usePseudotranslator) {\n this.logger.info(\n \"š Using pseudotranslator (dev.usePseudotranslator enabled)\",\n );\n this.translator = new PseudoTranslator({ delayMedian: 100 }, logger);\n this.cache = new MemoryTranslationCache();\n } else {\n // 2. Try to create real translator\n // LingoTranslator constructor will validate and fetch API keys\n // If validation fails, it will throw an error with helpful message\n try {\n const models = config.models;\n\n this.logger.debug(\n `Creating Lingo translator with models: ${JSON.stringify(models)}`,\n );\n\n this.cache = createCache(config);\n this.translator = new LingoTranslator(\n {\n models,\n sourceLocale: config.sourceLocale,\n prompt: config.prompt,\n },\n this.logger,\n );\n\n if (this.config.pluralization?.enabled) {\n this.pluralizationService = new PluralizationService(\n {\n ...this.config.pluralization,\n sourceLocale: this.config.sourceLocale,\n },\n this.logger,\n );\n }\n } catch (error) {\n // 3. Auto-fallback in dev mode if creation fails\n if (isDev) {\n // Use console.error to ensure visibility in all contexts (loader, server, etc.)\n const errorMsg =\n error instanceof Error ? error.message : String(error);\n this.logger.warn(`ā ļø Translation setup error: \\n${errorMsg}\\n\nā ļø Auto-fallback to pseudotranslator in development mode.\nSet the required API keys for real translations.`);\n\n this.translator = new PseudoTranslator(\n { delayMedian: 100 },\n this.logger,\n );\n this.cache = new MemoryTranslationCache();\n } else {\n // 4. Fail in production\n throw error;\n }\n }\n }\n }\n\n /**\n * Translate entries to target locale\n *\n * @param locale Target locale (including source locale)\n * @param metadata Metadata schema with all entries\n * @param requestedHashes Optional: only translate specific hashes\n * @returns Translation result with translations and errors\n */\n async translate(\n locale: LocaleCode,\n metadata: MetadataSchema,\n requestedHashes?: string[],\n ): Promise<TranslationResult> {\n const startTime = performance.now();\n\n // Step 1: Determine which hashes we need to work with\n const workingHashes = requestedHashes || Object.keys(metadata);\n\n this.logger.debug(\n `Translation requested for ${workingHashes.length} hashes in locale: ${locale}`,\n );\n\n // Step 2: Check cache first (same for all locales, including source)\n const cachedTranslations = await this.cache.get(locale);\n\n // Step 3: Determine what needs translation/pluralization\n const uncachedHashes = workingHashes.filter(\n (hash) => !cachedTranslations[hash],\n );\n this.logger.debug(\n `${uncachedHashes.length} hashes need processing, ${workingHashes.length - uncachedHashes.length} are cached`,\n );\n\n const cachedCount = workingHashes.length - uncachedHashes.length;\n\n if (uncachedHashes.length === 0) {\n return {\n translations: this.pickTranslations(cachedTranslations, workingHashes),\n errors: [],\n stats: {\n total: workingHashes.length,\n cached: cachedCount,\n translated: 0,\n failed: 0,\n },\n };\n }\n\n this.logger.debug(\n `Generating translations for ${uncachedHashes.length} uncached hashes in ${locale}...`,\n );\n\n // Step 4: Filter metadata to only uncached entries\n const filteredMetadata: MetadataSchema = Object.fromEntries(\n uncachedHashes\n .map((hash) => [hash, metadata[hash]])\n .filter(([_, entry]) => entry !== undefined),\n );\n\n // Step 5: Process pluralization for filtered entries\n if (this.pluralizationService) {\n this.logger.debug(\n `Processing pluralization for ${Object.keys(filteredMetadata).length} entries...`,\n );\n const pluralStats =\n await this.pluralizationService.process(filteredMetadata);\n this.logger.debug(\n `Pluralization stats: ${pluralStats.pluralized} pluralized, ${pluralStats.rejected} rejected, ${pluralStats.failed} failed`,\n );\n }\n\n // Step 6: Separate overridden entries from entries that need translation\n const overriddenTranslations: Record<string, string> = {};\n const hashesNeedingTranslation: string[] = [];\n\n this.logger.debug(\n `Checking for overrides in ${uncachedHashes.length} entries`,\n );\n\n for (const hash of uncachedHashes) {\n const entry = filteredMetadata[hash];\n if (!entry) continue;\n\n // Check if this entry has an override for the current locale\n if (entry.overrides && entry.overrides[locale]) {\n overriddenTranslations[hash] = entry.overrides[locale];\n this.logger.debug(\n `Using override for ${hash} in locale ${locale}: \"${entry.overrides[locale]}\"`,\n );\n } else {\n hashesNeedingTranslation.push(hash);\n }\n }\n\n // Step 7: Prepare entries for translation (excluding overridden ones)\n const entriesToTranslate = this.prepareEntries(\n filteredMetadata,\n hashesNeedingTranslation,\n );\n\n // Step 8: Translate or return source text\n let newTranslations: Record<string, string> = { ...overriddenTranslations };\n const errors: TranslationError[] = [];\n\n if (locale === this.config.sourceLocale) {\n // For source locale, just return the (possibly pluralized) sourceText\n this.logger.debug(\n `Source locale detected, returning sourceText for ${hashesNeedingTranslation.length} entries`,\n );\n for (const [hash, entry] of Object.entries(entriesToTranslate)) {\n newTranslations[hash] = entry.text;\n }\n } else if (Object.keys(entriesToTranslate).length > 0) {\n // For other locales, translate only entries without overrides\n try {\n this.logger.debug(\n `Translating ${locale} with ${Object.keys(entriesToTranslate).length} entries`,\n );\n const translatedTexts = await this.translator.translate(\n locale,\n entriesToTranslate,\n );\n // Merge translated texts with overridden translations\n newTranslations = { ...overriddenTranslations, ...translatedTexts };\n } catch (error) {\n this.logger.error(`Translation failed:`, error);\n\n return {\n translations: this.pickTranslations(\n cachedTranslations,\n workingHashes,\n ),\n errors: [\n {\n hash: \"all\",\n sourceText: \"all\",\n error:\n error instanceof Error\n ? error.message\n : \"Unknown translation error\",\n },\n ],\n stats: {\n total: workingHashes.length,\n cached: cachedCount,\n translated: 0,\n failed: uncachedHashes.length,\n },\n };\n }\n\n // Check for partial failures (some hashes didn't get translated)\n for (const hash of uncachedHashes) {\n if (!newTranslations[hash]) {\n const entry = filteredMetadata[hash];\n errors.push({\n hash,\n sourceText: entry?.sourceText || \"\",\n error: \"Translator doesn't return translation\",\n });\n }\n }\n }\n\n // Step 5: Update cache with successful translations (skip for pseudo)\n if (Object.keys(newTranslations).length > 0) {\n try {\n await this.cache.update(locale, newTranslations);\n } catch (error) {\n this.logger.error(`Failed to update cache:`, error);\n // Don't fail the request if cache update fails\n }\n }\n\n // Step 6: Merge and return\n const allTranslations = { ...cachedTranslations, ...newTranslations };\n const result = this.pickTranslations(allTranslations, workingHashes);\n\n const endTime = performance.now();\n this.logger.debug(\n `Translation completed for ${locale}: ${Object.keys(newTranslations).length} new, ${cachedCount} cached, ${errors.length} errors in ${(endTime - startTime).toFixed(2)}ms`,\n );\n\n return {\n translations: result,\n errors,\n stats: {\n total: workingHashes.length,\n cached: cachedCount,\n translated: Object.keys(newTranslations).length,\n failed: errors.length,\n },\n };\n }\n\n /**\n * Prepare metadata entries for translation\n */\n private prepareEntries(\n metadata: MetadataSchema,\n hashes: string[],\n ): Record<string, TranslatableEntry> {\n const entries: Record<string, TranslatableEntry> = {};\n\n for (const hash of hashes) {\n const entry = metadata[hash];\n if (entry) {\n entries[hash] = {\n text: entry.sourceText,\n context: entry.context || {},\n };\n }\n }\n\n return entries;\n }\n\n /**\n * Pick only requested translations from the full set\n */\n // TODO (AleksandrSl 14/12/2025): SHould I use this in the build somehow?\n private pickTranslations(\n allTranslations: Record<string, string>,\n requestedHashes: string[],\n ): Record<string, string> {\n const result: Record<string, string> = {};\n\n for (const hash of requestedHashes) {\n if (allTranslations[hash]) {\n result[hash] = allTranslations[hash];\n }\n }\n\n return result;\n }\n}\n"],"mappings":"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"}
|