@lingo.dev/compiler 0.1.2 → 0.1.4
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/build/plugin/build-translator.cjs +3 -3
- package/build/plugin/build-translator.mjs +3 -3
- package/build/plugin/build-translator.mjs.map +1 -1
- package/build/plugin/next.cjs +3 -3
- package/build/plugin/next.d.cts.map +1 -1
- package/build/plugin/next.d.mts.map +1 -1
- package/build/plugin/next.mjs +3 -3
- package/build/plugin/next.mjs.map +1 -1
- package/build/plugin/unplugin.cjs +81 -2
- package/build/plugin/unplugin.d.cts.map +1 -1
- package/build/plugin/unplugin.d.mts.map +1 -1
- package/build/plugin/unplugin.mjs +81 -2
- package/build/plugin/unplugin.mjs.map +1 -1
- package/build/react/server/ServerLingoProvider.d.cts +2 -2
- package/build/react/shared/LingoProvider.d.cts +2 -2
- package/build/react/shared/LocaleSwitcher.d.cts +2 -2
- package/build/translation-server/translation-server.cjs +7 -17
- package/build/translation-server/translation-server.mjs +7 -17
- package/build/translation-server/translation-server.mjs.map +1 -1
- package/build/translators/cache-factory.mjs.map +1 -1
- package/build/translators/lingo/model-factory.cjs +5 -10
- package/build/translators/lingo/model-factory.mjs +5 -10
- package/build/translators/lingo/model-factory.mjs.map +1 -1
- package/build/translators/lingo/provider-details.cjs +69 -0
- package/build/translators/lingo/provider-details.mjs +69 -0
- package/build/translators/lingo/provider-details.mjs.map +1 -0
- package/build/translators/lingo/{service.cjs → translator.cjs} +11 -13
- package/build/translators/lingo/{service.mjs → translator.mjs} +12 -14
- package/build/translators/lingo/translator.mjs.map +1 -0
- package/build/translators/memory-cache.cjs +47 -0
- package/build/translators/memory-cache.mjs +47 -0
- package/build/translators/memory-cache.mjs.map +1 -0
- package/build/translators/pluralization/service.cjs +19 -44
- package/build/translators/pluralization/service.mjs +19 -44
- package/build/translators/pluralization/service.mjs.map +1 -1
- package/build/translators/pseudotranslator/index.cjs +2 -10
- package/build/translators/pseudotranslator/index.mjs +2 -10
- package/build/translators/pseudotranslator/index.mjs.map +1 -1
- package/build/translators/translation-service.cjs +55 -57
- package/build/translators/translation-service.mjs +55 -57
- package/build/translators/translation-service.mjs.map +1 -1
- package/build/utils/observability.cjs +84 -0
- package/build/utils/observability.mjs +83 -0
- package/build/utils/observability.mjs.map +1 -0
- package/build/utils/rc.cjs +21 -0
- package/build/utils/rc.mjs +17 -0
- package/build/utils/rc.mjs.map +1 -0
- package/build/utils/repository-id.cjs +64 -0
- package/build/utils/repository-id.mjs +64 -0
- package/build/utils/repository-id.mjs.map +1 -0
- package/build/utils/tracking-events.cjs +28 -0
- package/build/utils/tracking-events.mjs +25 -0
- package/build/utils/tracking-events.mjs.map +1 -0
- package/package.json +12 -8
- package/build/translators/lingo/service.mjs.map +0 -1
- package/build/translators/translator-factory.cjs +0 -49
- package/build/translators/translator-factory.mjs +0 -50
- package/build/translators/translator-factory.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unplugin.mjs","names":["translationServer: TranslationServer","webpackMode: \"development\" | \"production\" | undefined","getMetadataPath","rawGetMetadataPath"],"sources":["../../src/plugin/unplugin.ts"],"sourcesContent":["import { createUnplugin } from \"unplugin\";\nimport { transformComponent } from \"./transform\";\nimport type {\n LingoConfig,\n LingoInternalFields,\n PartialLingoConfig,\n} from \"../types\";\nimport {\n startTranslationServer,\n type TranslationServer,\n} from \"../translation-server\";\nimport {\n cleanupExistingMetadata,\n getMetadataPath as rawGetMetadataPath,\n MetadataManager,\n} from \"../metadata/manager\";\nimport { createLingoConfig } from \"../utils/config-factory\";\nimport { logger } from \"../utils/logger\";\nimport { useI18nRegex } from \"./transform/use-i18n\";\nimport {\n generateClientLocaleModule,\n generateConfigModule,\n generateServerLocaleModule,\n} from \"../virtual/code-generator\";\nimport { processBuildTranslations } from \"./build-translator\";\nimport { registerCleanupOnCurrentProcess } from \"./cleanup\";\nimport path from \"path\";\nimport fs from \"fs\";\n\nexport type LingoPluginOptions = PartialLingoConfig;\n\nlet translationServer: TranslationServer;\n\nconst PLUGIN_NAME = \"lingo-compiler\";\n\nfunction tryLocalOrReturnVirtual(\n config: LingoConfig,\n fileName: string,\n virtualName: string,\n) {\n const customPath = path.join(config.sourceRoot, config.lingoDir, fileName);\n if (fs.existsSync(customPath)) {\n return customPath;\n }\n return virtualName;\n}\n\n/**\n * Single source of truth for virtual modules\n * Each entry defines both resolver (import path → virtual ID) and loader (virtual ID → code)\n *\n * If customFileCheck is defined, the specified file will be first searched for, and if not found virtual module will be used.\n */\nconst virtualModules = {\n \"@lingo.dev/compiler/virtual/config\": {\n virtualId: \"\\0virtual:lingo-config\",\n loader: (config: LingoConfig) => generateConfigModule(config),\n customFileCheck: undefined,\n },\n \"@lingo.dev/compiler/virtual/locale/server\": {\n virtualId: \"\\0virtual:locale-resolver.server\" as const,\n loader: (config: LingoConfig) => generateServerLocaleModule(config),\n customFileCheck: \"locale-resolver.server.ts\" as const,\n },\n \"@lingo.dev/compiler/virtual/locale/client\": {\n virtualId: \"\\0virtual:locale-resolver.client\" as const,\n loader: (config: LingoConfig) => generateClientLocaleModule(config),\n customFileCheck: \"locale-resolver.client.ts\" as const,\n },\n} as const;\n\n// Derive resolver and loader maps from the single source\nconst virtualModulesResolvers = Object.fromEntries(\n Object.entries(virtualModules).map(([importPath, module]) => [\n importPath,\n (config: LingoConfig) =>\n module.customFileCheck\n ? tryLocalOrReturnVirtual(\n config,\n module.customFileCheck,\n module.virtualId,\n )\n : module.virtualId,\n ]),\n);\n\nconst virtualModulesLoaders = Object.fromEntries(\n Object.values(virtualModules).map((value) => [value.virtualId, value.loader]),\n);\n\n/**\n * Universal plugin for Lingo.dev compiler\n * Supports Vite, Webpack\n */\nexport const lingoUnplugin = createUnplugin<\n LingoPluginOptions & Partial<Pick<LingoConfig, LingoInternalFields>>\n>((options) => {\n const config = createLingoConfig(options);\n\n // Won't work for webpack most likely. Use mode there to set correct environment in configs.\n const isDev = config.environment === \"development\";\n const startPort = config.dev.translationServerStartPort;\n\n // For webpack: store the actual mode and use it to compute the correct metadata path\n let webpackMode: \"development\" | \"production\" | undefined;\n // Should be dynamic, because webpack only tells us the mode inside the plugin, not inside the config.\n const getMetadataPath = () => {\n return rawGetMetadataPath(\n webpackMode ? { ...config, environment: webpackMode } : config,\n );\n };\n\n async function startServer() {\n const server = await startTranslationServer({\n startPort,\n onError: (err) => {\n logger.error(\"Translation server error:\", err);\n },\n onReady: (port) => {\n logger.info(`Translation server started successfully on port: ${port}`);\n },\n config,\n });\n // I don't like this quite a lot. But starting server inside the loader seems lame.\n config.dev.translationServerUrl = server.getUrl();\n registerCleanupOnCurrentProcess({\n asyncCleanup: async () => {\n await translationServer.stop();\n },\n });\n return server;\n }\n\n return {\n name: PLUGIN_NAME,\n enforce: \"pre\", // Run before other plugins (especially before React plugin)\n\n vite: {\n // Vite handles deep merge\n config() {\n // Required for custom virtual like modules to be resolved; otherwise they are bundled with raw source code.\n return {\n optimizeDeps: {\n exclude: [\"@lingo.dev/compiler\"],\n },\n };\n },\n async buildStart() {\n const metadataFilePath = getMetadataPath();\n\n cleanupExistingMetadata(metadataFilePath);\n registerCleanupOnCurrentProcess({\n cleanup: () => cleanupExistingMetadata(metadataFilePath),\n });\n\n if (isDev && !translationServer) {\n translationServer = await startServer();\n }\n },\n\n async buildEnd() {\n const metadataFilePath = getMetadataPath();\n if (!isDev) {\n try {\n await processBuildTranslations({\n config,\n publicOutputPath: \"public/translations\",\n metadataFilePath,\n });\n } catch (error) {\n logger.error(\"Build-time translation processing failed:\", error);\n }\n }\n },\n },\n\n webpack(compiler) {\n webpackMode =\n compiler.options.mode === \"development\" ? \"development\" : \"production\";\n const metadataFilePath = getMetadataPath();\n // Yes, this is dirty play, but webpack runs only for this plugin, and this way we save people from using wrong config\n config.environment = webpackMode;\n\n compiler.hooks.initialize.tap(PLUGIN_NAME, () => {\n cleanupExistingMetadata(metadataFilePath);\n registerCleanupOnCurrentProcess({\n cleanup: () => cleanupExistingMetadata(metadataFilePath),\n });\n });\n\n compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async () => {\n if (webpackMode === \"development\" && !translationServer) {\n translationServer = await startServer();\n }\n });\n\n compiler.hooks.additionalPass.tapPromise(PLUGIN_NAME, async () => {\n if (webpackMode === \"production\") {\n try {\n await processBuildTranslations({\n config,\n publicOutputPath: \"public/translations\",\n metadataFilePath,\n });\n } catch (error) {\n logger.error(\"Build-time translation processing failed:\", error);\n throw error;\n }\n }\n });\n\n // Duplicates the cleanup process handlers does, but won't hurt since cleanup is idempotent.\n compiler.hooks.shutdown.tapPromise(PLUGIN_NAME, async () => {\n cleanupExistingMetadata(metadataFilePath);\n await translationServer?.stop();\n });\n },\n\n resolveId(id) {\n const handler = virtualModulesResolvers[id];\n if (handler) {\n return handler(config);\n }\n return null;\n },\n\n load: {\n filter: {\n // Without the filter webpack goes mad\n id: /virtual:/,\n },\n handler(id: string) {\n const handler = virtualModulesLoaders[id];\n if (handler) {\n return handler(config);\n }\n return null;\n },\n },\n\n transform: {\n filter: {\n id: {\n include: [/\\.[tj]sx$/],\n exclude: /node_modules/,\n },\n // If useDirective is enabled, only process files with \"use i18n\"\n // This is more efficient than checking in the handler\n code: config.useDirective ? useI18nRegex : undefined,\n },\n async handler(code, id) {\n try {\n // Transform the component\n const result = transformComponent({\n code,\n filePath: id,\n config,\n });\n\n // If no transformation occurred, return original code\n if (!result.transformed) {\n logger.debug(`No transformation needed for ${id}`);\n return null;\n }\n const metadataManager = new MetadataManager(getMetadataPath());\n\n // Update metadata with new entries (thread-safe)\n if (result.newEntries && result.newEntries.length > 0) {\n await metadataManager.saveMetadataWithEntries(result.newEntries);\n\n logger.debug(\n `Found ${result.newEntries.length} translatable text(s) in ${id}`,\n );\n }\n\n logger.debug(`Returning transformed code for ${id}`);\n return {\n code: result.code,\n map: result.map,\n };\n } catch (error) {\n logger.error(`Transform error in ${id}:`, error);\n return null;\n }\n },\n },\n };\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"unplugin.mjs","names":["translationServer: TranslationServer","buildStartTime: number | null","currentFramework: \"vite\" | \"webpack\" | \"next\" | null","webpackMode: \"development\" | \"production\" | undefined","getMetadataPath","rawGetMetadataPath"],"sources":["../../src/plugin/unplugin.ts"],"sourcesContent":["import { createUnplugin } from \"unplugin\";\nimport { transformComponent } from \"./transform\";\nimport type {\n LingoConfig,\n LingoInternalFields,\n PartialLingoConfig,\n} from \"../types\";\nimport {\n startTranslationServer,\n type TranslationServer,\n} from \"../translation-server\";\nimport {\n cleanupExistingMetadata,\n getMetadataPath as rawGetMetadataPath,\n MetadataManager,\n} from \"../metadata/manager\";\nimport { createLingoConfig } from \"../utils/config-factory\";\nimport { logger } from \"../utils/logger\";\nimport { useI18nRegex } from \"./transform/use-i18n\";\nimport {\n generateClientLocaleModule,\n generateConfigModule,\n generateServerLocaleModule,\n} from \"../virtual/code-generator\";\nimport { processBuildTranslations } from \"./build-translator\";\nimport { registerCleanupOnCurrentProcess } from \"./cleanup\";\nimport path from \"path\";\nimport fs from \"fs\";\nimport { TranslationService } from \"../translators\";\nimport trackEvent from \"../utils/observability\";\nimport {\n TRACKING_EVENTS,\n sanitizeConfigForTracking,\n} from \"../utils/tracking-events\";\n\nexport type LingoPluginOptions = PartialLingoConfig;\n\nlet translationServer: TranslationServer;\n\nconst PLUGIN_NAME = \"lingo-compiler\";\n\n// Tracking state\nlet alreadySentBuildStartEvent = false;\nlet buildStartTime: number | null = null;\nlet filesTransformedCount = 0;\nlet totalEntriesCount = 0;\nlet hasTransformErrors = false;\nlet currentFramework: \"vite\" | \"webpack\" | \"next\" | null = null;\n\nfunction tryLocalOrReturnVirtual(\n config: LingoConfig,\n fileName: string,\n virtualName: string,\n) {\n const customPath = path.join(config.sourceRoot, config.lingoDir, fileName);\n if (fs.existsSync(customPath)) {\n return customPath;\n }\n return virtualName;\n}\n\n/**\n * Single source of truth for virtual modules\n * Each entry defines both resolver (import path → virtual ID) and loader (virtual ID → code)\n *\n * If customFileCheck is defined, the specified file will be first searched for, and if not found virtual module will be used.\n */\nconst virtualModules = {\n \"@lingo.dev/compiler/virtual/config\": {\n virtualId: \"\\0virtual:lingo-config\",\n loader: (config: LingoConfig) => generateConfigModule(config),\n customFileCheck: undefined,\n },\n \"@lingo.dev/compiler/virtual/locale/server\": {\n virtualId: \"\\0virtual:locale-resolver.server\" as const,\n loader: (config: LingoConfig) => generateServerLocaleModule(config),\n customFileCheck: \"locale-resolver.server.ts\" as const,\n },\n \"@lingo.dev/compiler/virtual/locale/client\": {\n virtualId: \"\\0virtual:locale-resolver.client\" as const,\n loader: (config: LingoConfig) => generateClientLocaleModule(config),\n customFileCheck: \"locale-resolver.client.ts\" as const,\n },\n} as const;\n\n// Derive resolver and loader maps from the single source\nconst virtualModulesResolvers = Object.fromEntries(\n Object.entries(virtualModules).map(([importPath, module]) => [\n importPath,\n (config: LingoConfig) =>\n module.customFileCheck\n ? tryLocalOrReturnVirtual(\n config,\n module.customFileCheck,\n module.virtualId,\n )\n : module.virtualId,\n ]),\n);\n\nconst virtualModulesLoaders = Object.fromEntries(\n Object.values(virtualModules).map((value) => [value.virtualId, value.loader]),\n);\n\n/**\n * Send build start tracking event\n */\nfunction sendBuildStartEvent(\n framework: \"vite\" | \"webpack\" | \"next\",\n config: LingoConfig,\n) {\n if (alreadySentBuildStartEvent) return;\n alreadySentBuildStartEvent = true;\n\n trackEvent(TRACKING_EVENTS.BUILD_START, {\n framework,\n configuration: sanitizeConfigForTracking(config),\n environment: config.environment,\n });\n}\n\n/**\n * Universal plugin for Lingo.dev compiler\n * Supports Vite, Webpack\n */\nexport const lingoUnplugin = createUnplugin<\n LingoPluginOptions & Partial<Pick<LingoConfig, LingoInternalFields>>\n>((options) => {\n const config = createLingoConfig(options);\n\n // Won't work for webpack most likely. Use mode there to set correct environment in configs.\n const isDev = config.environment === \"development\";\n const startPort = config.dev.translationServerStartPort;\n\n // For webpack: store the actual mode and use it to compute the correct metadata path\n let webpackMode: \"development\" | \"production\" | undefined;\n // Should be dynamic, because webpack only tells us the mode inside the plugin, not inside the config.\n const getMetadataPath = () => {\n return rawGetMetadataPath(\n webpackMode ? { ...config, environment: webpackMode } : config,\n );\n };\n\n async function startServer() {\n const server = await startTranslationServer({\n translationService: new TranslationService(config, logger),\n onError: (err) => {\n logger.error(\"Translation server error:\", err);\n },\n onReady: (port) => {\n logger.info(`Translation server started successfully on port: ${port}`);\n },\n config,\n });\n // I don't like this quite a lot. But starting server inside the loader seems lame.\n config.dev.translationServerUrl = server.getUrl();\n registerCleanupOnCurrentProcess({\n asyncCleanup: async () => {\n await translationServer.stop();\n },\n });\n return server;\n }\n\n return {\n name: PLUGIN_NAME,\n enforce: \"pre\", // Run before other plugins (especially before React plugin)\n\n vite: {\n // Vite handles deep merge\n config() {\n // Required for custom virtual like modules to be resolved; otherwise they are bundled with raw source code.\n return {\n optimizeDeps: {\n exclude: [\"@lingo.dev/compiler\"],\n },\n };\n },\n async buildStart() {\n const metadataFilePath = getMetadataPath();\n\n // Track build start\n currentFramework = \"vite\";\n sendBuildStartEvent(\"vite\", config);\n buildStartTime = Date.now();\n filesTransformedCount = 0;\n totalEntriesCount = 0;\n hasTransformErrors = false;\n\n cleanupExistingMetadata(metadataFilePath);\n registerCleanupOnCurrentProcess({\n cleanup: () => cleanupExistingMetadata(metadataFilePath),\n });\n\n if (isDev && !translationServer) {\n translationServer = await startServer();\n }\n },\n\n async buildEnd() {\n const metadataFilePath = getMetadataPath();\n if (!isDev) {\n try {\n await processBuildTranslations({\n config,\n publicOutputPath: \"public/translations\",\n metadataFilePath,\n });\n\n if (buildStartTime && !hasTransformErrors) {\n trackEvent(TRACKING_EVENTS.BUILD_SUCCESS, {\n framework: \"vite\",\n stats: {\n totalEntries: totalEntriesCount,\n filesTransformed: filesTransformedCount,\n buildDuration: Date.now() - buildStartTime,\n },\n environment: config.environment,\n });\n }\n } catch (error) {\n logger.error(\"Build-time translation processing failed:\", error);\n }\n } else if (buildStartTime && !hasTransformErrors) {\n trackEvent(TRACKING_EVENTS.BUILD_SUCCESS, {\n framework: \"vite\",\n stats: {\n totalEntries: totalEntriesCount,\n filesTransformed: filesTransformedCount,\n buildDuration: Date.now() - buildStartTime,\n },\n environment: config.environment,\n });\n }\n },\n },\n\n webpack(compiler) {\n webpackMode =\n compiler.options.mode === \"development\" ? \"development\" : \"production\";\n const metadataFilePath = getMetadataPath();\n // Yes, this is dirty play, but webpack runs only for this plugin, and this way we save people from using wrong config\n config.environment = webpackMode;\n\n compiler.hooks.initialize.tap(PLUGIN_NAME, () => {\n // Track build start\n currentFramework = \"webpack\";\n sendBuildStartEvent(\"webpack\", config);\n buildStartTime = Date.now();\n filesTransformedCount = 0;\n totalEntriesCount = 0;\n hasTransformErrors = false;\n\n cleanupExistingMetadata(metadataFilePath);\n registerCleanupOnCurrentProcess({\n cleanup: () => cleanupExistingMetadata(metadataFilePath),\n });\n });\n\n compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async () => {\n if (webpackMode === \"development\" && !translationServer) {\n translationServer = await startServer();\n }\n });\n\n compiler.hooks.additionalPass.tapPromise(PLUGIN_NAME, async () => {\n if (webpackMode === \"production\") {\n try {\n await processBuildTranslations({\n config,\n publicOutputPath: \"public/translations\",\n metadataFilePath,\n });\n\n if (buildStartTime && !hasTransformErrors) {\n trackEvent(TRACKING_EVENTS.BUILD_SUCCESS, {\n framework: \"webpack\",\n stats: {\n totalEntries: totalEntriesCount,\n filesTransformed: filesTransformedCount,\n buildDuration: Date.now() - buildStartTime,\n },\n environment: config.environment,\n });\n }\n } catch (error) {\n logger.error(\"Build-time translation processing failed:\", error);\n throw error;\n }\n } else if (buildStartTime && !hasTransformErrors) {\n trackEvent(TRACKING_EVENTS.BUILD_SUCCESS, {\n framework: \"webpack\",\n stats: {\n totalEntries: totalEntriesCount,\n filesTransformed: filesTransformedCount,\n buildDuration: Date.now() - buildStartTime,\n },\n environment: config.environment,\n });\n }\n });\n\n // Duplicates the cleanup process handlers does, but won't hurt since cleanup is idempotent.\n compiler.hooks.shutdown.tapPromise(PLUGIN_NAME, async () => {\n cleanupExistingMetadata(metadataFilePath);\n await translationServer?.stop();\n });\n },\n\n resolveId(id) {\n const handler = virtualModulesResolvers[id];\n if (handler) {\n return handler(config);\n }\n return null;\n },\n\n load: {\n filter: {\n // Without the filter webpack goes mad\n id: /virtual:/,\n },\n handler(id: string) {\n const handler = virtualModulesLoaders[id];\n if (handler) {\n return handler(config);\n }\n return null;\n },\n },\n\n transform: {\n filter: {\n id: {\n include: [/\\.[tj]sx$/],\n exclude: /node_modules/,\n },\n // If useDirective is enabled, only process files with \"use i18n\"\n // This is more efficient than checking in the handler\n code: config.useDirective ? useI18nRegex : undefined,\n },\n async handler(code, id) {\n try {\n // Transform the component\n const result = transformComponent({\n code,\n filePath: id,\n config,\n });\n\n // If no transformation occurred, return original code\n if (!result.transformed) {\n logger.debug(`No transformation needed for ${id}`);\n return null;\n }\n const metadataManager = new MetadataManager(getMetadataPath());\n\n // Update metadata with new entries (thread-safe)\n if (result.newEntries && result.newEntries.length > 0) {\n await metadataManager.saveMetadataWithEntries(result.newEntries);\n\n // Track stats for observability\n totalEntriesCount += result.newEntries.length;\n filesTransformedCount++;\n\n logger.debug(\n `Found ${result.newEntries.length} translatable text(s) in ${id}`,\n );\n }\n\n logger.debug(`Returning transformed code for ${id}`);\n return {\n code: result.code,\n map: result.map,\n };\n } catch (error) {\n hasTransformErrors = true;\n\n // Track error event\n if (currentFramework) {\n trackEvent(TRACKING_EVENTS.BUILD_ERROR, {\n framework: currentFramework,\n errorType: \"transform\",\n errorMessage: error instanceof Error ? error.message : \"Unknown transform error\",\n filePath: id,\n environment: config.environment,\n });\n }\n\n logger.error(`Transform error in ${id}:`, error);\n return null;\n }\n },\n },\n };\n});\n"],"mappings":"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"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { LingoProviderProps } from "../shared/LingoProvider.cjs";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
3
3
|
|
|
4
4
|
//#region src/react/server/ServerLingoProvider.d.ts
|
|
5
5
|
declare function LingoProvider({
|
|
6
6
|
initialLocale,
|
|
7
7
|
initialTranslations,
|
|
8
8
|
...rest
|
|
9
|
-
}: LingoProviderProps): Promise<
|
|
9
|
+
}: LingoProviderProps): Promise<react_jsx_runtime1.JSX.Element>;
|
|
10
10
|
//#endregion
|
|
11
11
|
export { LingoProvider };
|
|
12
12
|
//# sourceMappingURL=ServerLingoProvider.d.cts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocaleCode } from "lingo.dev/spec";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime3 from "react/jsx-runtime";
|
|
3
3
|
import { PropsWithChildren } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/react/shared/LingoProvider.d.ts
|
|
@@ -70,7 +70,7 @@ declare function LingoProvider__Dev({
|
|
|
70
70
|
router,
|
|
71
71
|
devWidget,
|
|
72
72
|
children
|
|
73
|
-
}: LingoProviderProps):
|
|
73
|
+
}: LingoProviderProps): react_jsx_runtime3.JSX.Element;
|
|
74
74
|
//#endregion
|
|
75
75
|
export { LingoProvider, LingoProviderProps };
|
|
76
76
|
//# sourceMappingURL=LingoProvider.d.cts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LocaleCode } from "lingo.dev/spec";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime2 from "react/jsx-runtime";
|
|
3
3
|
import { CSSProperties } from "react";
|
|
4
4
|
|
|
5
5
|
//#region src/react/shared/LocaleSwitcher.d.ts
|
|
@@ -65,7 +65,7 @@ declare function LocaleSwitcher({
|
|
|
65
65
|
style,
|
|
66
66
|
className,
|
|
67
67
|
showLoadingState
|
|
68
|
-
}: LocaleSwitcherProps):
|
|
68
|
+
}: LocaleSwitcherProps): react_jsx_runtime2.JSX.Element;
|
|
69
69
|
//#endregion
|
|
70
70
|
export { LocaleSwitcher };
|
|
71
71
|
//# sourceMappingURL=LocaleSwitcher.d.cts.map
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
2
|
const require_logger = require('./logger.cjs');
|
|
3
|
-
const require_translator_factory = require('../translators/translator-factory.cjs');
|
|
4
3
|
const require_translation_service = require('../translators/translation-service.cjs');
|
|
5
|
-
const require_cache_factory = require('../translators/cache-factory.cjs');
|
|
6
4
|
const require_manager = require('../metadata/manager.cjs');
|
|
7
5
|
const require_ws_events = require('./ws-events.cjs');
|
|
8
6
|
const require_is_valid_locale = require('../utils/is-valid-locale.cjs');
|
|
@@ -34,7 +32,6 @@ var TranslationServer = class {
|
|
|
34
32
|
startPort;
|
|
35
33
|
onReadyCallback;
|
|
36
34
|
onErrorCallback;
|
|
37
|
-
translationService = null;
|
|
38
35
|
metadata = null;
|
|
39
36
|
connections = /* @__PURE__ */ new Set();
|
|
40
37
|
wss = null;
|
|
@@ -43,10 +40,12 @@ var TranslationServer = class {
|
|
|
43
40
|
isBusy = false;
|
|
44
41
|
busyTimeout = null;
|
|
45
42
|
BUSY_DEBOUNCE_MS = 500;
|
|
43
|
+
translationService;
|
|
46
44
|
constructor(options) {
|
|
47
45
|
this.config = options.config;
|
|
48
46
|
this.configHash = hashConfig(options.config);
|
|
49
|
-
this.
|
|
47
|
+
this.translationService = options.translationService ?? new require_translation_service.TranslationService(options.config, require_logger.getLogger(options.config));
|
|
48
|
+
this.startPort = options.config.dev.translationServerStartPort;
|
|
50
49
|
this.onReadyCallback = options.onReady;
|
|
51
50
|
this.onErrorCallback = options.onError;
|
|
52
51
|
this.logger = require_logger.getLogger(this.config);
|
|
@@ -57,10 +56,6 @@ var TranslationServer = class {
|
|
|
57
56
|
async start() {
|
|
58
57
|
if (this.server) throw new Error("Server is already running");
|
|
59
58
|
this.logger.info(`🔧 Initializing translator...`);
|
|
60
|
-
this.translationService = new require_translation_service.TranslationService(require_translator_factory.createTranslator(this.config, this.logger), require_cache_factory.createCache(this.config), {
|
|
61
|
-
sourceLocale: this.config.sourceLocale,
|
|
62
|
-
pluralization: this.config.pluralization
|
|
63
|
-
}, this.logger);
|
|
64
59
|
const port = await this.findAvailablePort(this.startPort);
|
|
65
60
|
return new Promise((resolve, reject) => {
|
|
66
61
|
this.server = http.default.createServer((req, res) => {
|
|
@@ -178,7 +173,7 @@ var TranslationServer = class {
|
|
|
178
173
|
* Start a new server or get the URL of an existing one on the preferred port.
|
|
179
174
|
*
|
|
180
175
|
* This method optimizes for the common case where a translation server is already
|
|
181
|
-
* running on port
|
|
176
|
+
* running on a preferred port. If that port is taken, it checks if it's our service
|
|
182
177
|
* by calling the health check endpoint. If it is, we reuse it instead of starting
|
|
183
178
|
* a new server on a different port.
|
|
184
179
|
*
|
|
@@ -429,7 +424,6 @@ var TranslationServer = class {
|
|
|
429
424
|
}));
|
|
430
425
|
return;
|
|
431
426
|
}
|
|
432
|
-
if (!this.translationService) throw new Error("Translation service not initialized");
|
|
433
427
|
await this.reloadMetadata();
|
|
434
428
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
435
429
|
this.logger.info(`🔄 Translating ${hashes.length} hashes to ${locale}`);
|
|
@@ -464,7 +458,6 @@ var TranslationServer = class {
|
|
|
464
458
|
async handleDictionaryRequest(locale, res) {
|
|
465
459
|
try {
|
|
466
460
|
const parsedLocale = require_is_valid_locale.parseLocaleOrThrow(locale);
|
|
467
|
-
if (!this.translationService) throw new Error("Translation service not initialized");
|
|
468
461
|
await this.reloadMetadata();
|
|
469
462
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
470
463
|
this.logger.info(`🌐 Requesting full dictionary for ${locale}`);
|
|
@@ -516,9 +509,6 @@ function hashConfig(config) {
|
|
|
516
509
|
const serialized = stableStringify(config);
|
|
517
510
|
return crypto.default.createHash("md5").update(serialized).digest("hex").slice(0, 12);
|
|
518
511
|
}
|
|
519
|
-
/**
|
|
520
|
-
* Create and start a translation server
|
|
521
|
-
*/
|
|
522
512
|
async function startTranslationServer(options) {
|
|
523
513
|
const server = new TranslationServer(options);
|
|
524
514
|
await server.start();
|
|
@@ -527,10 +517,10 @@ async function startTranslationServer(options) {
|
|
|
527
517
|
/**
|
|
528
518
|
* Create a translation server and start it or reuse an existing one on the preferred port
|
|
529
519
|
*
|
|
530
|
-
* Since we have little control over the dev server start in next, we can start the translation server only in the loader,
|
|
531
|
-
*
|
|
520
|
+
* 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,
|
|
521
|
+
* they both could be run in different processes, and we need a way to avoid starting multiple servers.
|
|
532
522
|
* This one will try to start a server on the preferred port (which seems to be an atomic operation), and if it fails,
|
|
533
|
-
* it checks if the server already started is ours and returns its url.
|
|
523
|
+
* it checks if the server that is already started is ours and returns its url.
|
|
534
524
|
*
|
|
535
525
|
* @returns Object containing the server instance and its URL
|
|
536
526
|
*/
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "./logger.mjs";
|
|
2
|
-
import { createTranslator } from "../translators/translator-factory.mjs";
|
|
3
2
|
import { TranslationService } from "../translators/translation-service.mjs";
|
|
4
|
-
import { createCache } from "../translators/cache-factory.mjs";
|
|
5
3
|
import { createEmptyMetadata, getMetadataPath, loadMetadata } from "../metadata/manager.mjs";
|
|
6
4
|
import { createEvent } from "./ws-events.mjs";
|
|
7
5
|
import { parseLocaleOrThrow } from "../utils/is-valid-locale.mjs";
|
|
@@ -31,7 +29,6 @@ var TranslationServer = class {
|
|
|
31
29
|
startPort;
|
|
32
30
|
onReadyCallback;
|
|
33
31
|
onErrorCallback;
|
|
34
|
-
translationService = null;
|
|
35
32
|
metadata = null;
|
|
36
33
|
connections = /* @__PURE__ */ new Set();
|
|
37
34
|
wss = null;
|
|
@@ -40,10 +37,12 @@ var TranslationServer = class {
|
|
|
40
37
|
isBusy = false;
|
|
41
38
|
busyTimeout = null;
|
|
42
39
|
BUSY_DEBOUNCE_MS = 500;
|
|
40
|
+
translationService;
|
|
43
41
|
constructor(options) {
|
|
44
42
|
this.config = options.config;
|
|
45
43
|
this.configHash = hashConfig(options.config);
|
|
46
|
-
this.
|
|
44
|
+
this.translationService = options.translationService ?? new TranslationService(options.config, getLogger(options.config));
|
|
45
|
+
this.startPort = options.config.dev.translationServerStartPort;
|
|
47
46
|
this.onReadyCallback = options.onReady;
|
|
48
47
|
this.onErrorCallback = options.onError;
|
|
49
48
|
this.logger = getLogger(this.config);
|
|
@@ -54,10 +53,6 @@ var TranslationServer = class {
|
|
|
54
53
|
async start() {
|
|
55
54
|
if (this.server) throw new Error("Server is already running");
|
|
56
55
|
this.logger.info(`🔧 Initializing translator...`);
|
|
57
|
-
this.translationService = new TranslationService(createTranslator(this.config, this.logger), createCache(this.config), {
|
|
58
|
-
sourceLocale: this.config.sourceLocale,
|
|
59
|
-
pluralization: this.config.pluralization
|
|
60
|
-
}, this.logger);
|
|
61
56
|
const port = await this.findAvailablePort(this.startPort);
|
|
62
57
|
return new Promise((resolve, reject) => {
|
|
63
58
|
this.server = http.createServer((req, res) => {
|
|
@@ -175,7 +170,7 @@ var TranslationServer = class {
|
|
|
175
170
|
* Start a new server or get the URL of an existing one on the preferred port.
|
|
176
171
|
*
|
|
177
172
|
* This method optimizes for the common case where a translation server is already
|
|
178
|
-
* running on port
|
|
173
|
+
* running on a preferred port. If that port is taken, it checks if it's our service
|
|
179
174
|
* by calling the health check endpoint. If it is, we reuse it instead of starting
|
|
180
175
|
* a new server on a different port.
|
|
181
176
|
*
|
|
@@ -426,7 +421,6 @@ var TranslationServer = class {
|
|
|
426
421
|
}));
|
|
427
422
|
return;
|
|
428
423
|
}
|
|
429
|
-
if (!this.translationService) throw new Error("Translation service not initialized");
|
|
430
424
|
await this.reloadMetadata();
|
|
431
425
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
432
426
|
this.logger.info(`🔄 Translating ${hashes.length} hashes to ${locale}`);
|
|
@@ -461,7 +455,6 @@ var TranslationServer = class {
|
|
|
461
455
|
async handleDictionaryRequest(locale, res) {
|
|
462
456
|
try {
|
|
463
457
|
const parsedLocale = parseLocaleOrThrow(locale);
|
|
464
|
-
if (!this.translationService) throw new Error("Translation service not initialized");
|
|
465
458
|
await this.reloadMetadata();
|
|
466
459
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
467
460
|
this.logger.info(`🌐 Requesting full dictionary for ${locale}`);
|
|
@@ -513,9 +506,6 @@ function hashConfig(config) {
|
|
|
513
506
|
const serialized = stableStringify(config);
|
|
514
507
|
return crypto.createHash("md5").update(serialized).digest("hex").slice(0, 12);
|
|
515
508
|
}
|
|
516
|
-
/**
|
|
517
|
-
* Create and start a translation server
|
|
518
|
-
*/
|
|
519
509
|
async function startTranslationServer(options) {
|
|
520
510
|
const server = new TranslationServer(options);
|
|
521
511
|
await server.start();
|
|
@@ -524,10 +514,10 @@ async function startTranslationServer(options) {
|
|
|
524
514
|
/**
|
|
525
515
|
* Create a translation server and start it or reuse an existing one on the preferred port
|
|
526
516
|
*
|
|
527
|
-
* Since we have little control over the dev server start in next, we can start the translation server only in the loader,
|
|
528
|
-
*
|
|
517
|
+
* 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,
|
|
518
|
+
* they both could be run in different processes, and we need a way to avoid starting multiple servers.
|
|
529
519
|
* This one will try to start a server on the preferred port (which seems to be an atomic operation), and if it fails,
|
|
530
|
-
* it checks if the server already started is ours and returns its url.
|
|
520
|
+
* it checks if the server that is already started is ours and returns its url.
|
|
531
521
|
*
|
|
532
522
|
* @returns Object containing the server instance and its URL
|
|
533
523
|
*/
|
|
@@ -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 {\n createCache,\n createTranslator,\n TranslationService,\n} 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 /**\n * Starting port to try (will find next available if taken)\n * @default 3456\n */\n startPort?: number;\n\n /**\n * Configuration for translation generation\n */\n config: TranslationMiddlewareConfig;\n\n /**\n * Callback when server is ready\n */\n onReady?: (port: number) => void;\n\n /**\n * Callback on error\n */\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 config: TranslationMiddlewareConfig;\n private configHash: string;\n private startPort: number;\n private onReadyCallback?: (port: number) => void;\n private onErrorCallback?: (error: Error) => void;\n private translationService: TranslationService | null = null;\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\n constructor(options: TranslationServerOptions) {\n this.config = options.config;\n this.configHash = hashConfig(options.config);\n this.startPort = options.startPort || 60000;\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 translator = createTranslator(this.config, this.logger);\n const cache = createCache(this.config);\n\n this.translationService = new TranslationService(\n translator,\n cache,\n {\n sourceLocale: this.config.sourceLocale,\n pluralization: this.config.pluralization,\n },\n this.logger,\n );\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 port 60000. 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 instance already has a server running, return its URL\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 // Check if response is valid and has the expected structure\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\n if (!this.translationService) {\n throw new Error(\"Translation service not initialized\");\n }\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 if (!this.translationService) {\n throw new Error(\"Translation service not initialized\");\n }\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\n/**\n * Create and start a translation server\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 loader,\n * and loaders could be started from multiple processes (it seems) or similar 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 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 {\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-factory.mjs","names":[],"sources":["../../src/translators/cache-factory.ts"],"sourcesContent":["/**\n * Cache factory for creating cache instances from config\n */\n\nimport type { LingoConfig, PathConfig } from \"../types\";\nimport type { TranslationCache } from \"./cache\";\nimport { LocalTranslationCache } from \"./local-cache\";\nimport { logger } from \"../utils/logger\";\nimport { getCacheDir } from \"../utils/path-helpers\";\n\n/**\n * Create a cache instance based on the config\n *\n * @param config - LingoConfig with cacheType and lingoDir\n * @returns TranslationCache instance\n *\n * @example\n * ```typescript\n * const cache = createCache(config);\n * const translations = await cache.get(\"de\");\n * ```\n */\nexport function createCache(\n config:
|
|
1
|
+
{"version":3,"file":"cache-factory.mjs","names":[],"sources":["../../src/translators/cache-factory.ts"],"sourcesContent":["/**\n * Cache factory for creating cache instances from config\n */\n\nimport type { LingoConfig, PathConfig } from \"../types\";\nimport type { TranslationCache } from \"./cache\";\nimport { LocalTranslationCache } from \"./local-cache\";\nimport { logger } from \"../utils/logger\";\nimport { getCacheDir } from \"../utils/path-helpers\";\n\nexport type CacheConfig = Pick<LingoConfig, \"cacheType\"> & PathConfig;\n\n/**\n * Create a cache instance based on the config\n *\n * @param config - LingoConfig with cacheType and lingoDir\n * @returns TranslationCache instance\n *\n * @example\n * ```typescript\n * const cache = createCache(config);\n * const translations = await cache.get(\"de\");\n * ```\n */\nexport function createCache(\n config: CacheConfig,\n): TranslationCache {\n switch (config.cacheType) {\n case \"local\":\n return new LocalTranslationCache(\n {\n cacheDir: getCacheDir(config),\n },\n logger,\n );\n\n default:\n // This should never happen due to TypeScript types, but provides a safeguard\n throw new Error(\n `Unknown cache type: ${config.cacheType}. Only \"local\" is currently supported.`,\n );\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,SAAgB,YACd,QACkB;AAClB,SAAQ,OAAO,WAAf;EACE,KAAK,QACH,QAAO,IAAI,sBACT,EACE,UAAU,YAAY,OAAO,EAC9B,EACD,OACD;EAEH,QAEE,OAAM,IAAI,MACR,uBAAuB,OAAO,UAAU,wCACzC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_provider_details = require('./provider-details.cjs');
|
|
2
3
|
let path = require("path");
|
|
3
4
|
path = require_rolldown_runtime.__toESM(path);
|
|
4
5
|
let _ai_sdk_groq = require("@ai-sdk/groq");
|
|
@@ -121,7 +122,7 @@ function parseModelString(modelString) {
|
|
|
121
122
|
*/
|
|
122
123
|
function validateAndGetApiKeys(config) {
|
|
123
124
|
const keys = {};
|
|
124
|
-
const
|
|
125
|
+
const missingProviders = [];
|
|
125
126
|
let providersToValidate;
|
|
126
127
|
if (config === "lingo.dev") providersToValidate = ["lingo.dev"];
|
|
127
128
|
else {
|
|
@@ -134,19 +135,13 @@ function validateAndGetApiKeys(config) {
|
|
|
134
135
|
}
|
|
135
136
|
for (const provider of providersToValidate) {
|
|
136
137
|
const providerConfig = providerDetails[provider];
|
|
137
|
-
if (!providerConfig) throw new Error(`⚠️
|
|
138
|
+
if (!providerConfig) throw new Error(`⚠️ Unknown provider "${provider}". Supported providers: ${Object.keys(providerDetails).join(", ")}`);
|
|
138
139
|
if (!providerConfig.apiKeyEnvVar) continue;
|
|
139
140
|
const key = getKeyFromEnv(providerConfig.apiKeyEnvVar);
|
|
140
141
|
if (key) keys[provider] = key;
|
|
141
|
-
else
|
|
142
|
-
provider: providerConfig.name,
|
|
143
|
-
envVar: providerConfig.apiKeyEnvVar
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (missingKeys.length > 0) {
|
|
147
|
-
const errorLines = missingKeys.map(({ provider, envVar }) => ` - ${provider}: ${envVar}`);
|
|
148
|
-
throw new Error(`⚠️ Missing API keys for configured providers:\n${errorLines.join("\n")}\n\nPlease set the required environment variables.`);
|
|
142
|
+
else missingProviders.push(provider);
|
|
149
143
|
}
|
|
144
|
+
if (missingProviders.length > 0) throw new Error(require_provider_details.formatNoApiKeysError(missingProviders));
|
|
150
145
|
return keys;
|
|
151
146
|
}
|
|
152
147
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatNoApiKeysError } from "./provider-details.mjs";
|
|
1
2
|
import * as path$1 from "path";
|
|
2
3
|
import { createGroq } from "@ai-sdk/groq";
|
|
3
4
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
@@ -118,7 +119,7 @@ function parseModelString(modelString) {
|
|
|
118
119
|
*/
|
|
119
120
|
function validateAndGetApiKeys(config) {
|
|
120
121
|
const keys = {};
|
|
121
|
-
const
|
|
122
|
+
const missingProviders = [];
|
|
122
123
|
let providersToValidate;
|
|
123
124
|
if (config === "lingo.dev") providersToValidate = ["lingo.dev"];
|
|
124
125
|
else {
|
|
@@ -131,19 +132,13 @@ function validateAndGetApiKeys(config) {
|
|
|
131
132
|
}
|
|
132
133
|
for (const provider of providersToValidate) {
|
|
133
134
|
const providerConfig = providerDetails[provider];
|
|
134
|
-
if (!providerConfig) throw new Error(`⚠️
|
|
135
|
+
if (!providerConfig) throw new Error(`⚠️ Unknown provider "${provider}". Supported providers: ${Object.keys(providerDetails).join(", ")}`);
|
|
135
136
|
if (!providerConfig.apiKeyEnvVar) continue;
|
|
136
137
|
const key = getKeyFromEnv(providerConfig.apiKeyEnvVar);
|
|
137
138
|
if (key) keys[provider] = key;
|
|
138
|
-
else
|
|
139
|
-
provider: providerConfig.name,
|
|
140
|
-
envVar: providerConfig.apiKeyEnvVar
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
if (missingKeys.length > 0) {
|
|
144
|
-
const errorLines = missingKeys.map(({ provider, envVar }) => ` - ${provider}: ${envVar}`);
|
|
145
|
-
throw new Error(`⚠️ Missing API keys for configured providers:\n${errorLines.join("\n")}\n\nPlease set the required environment variables.`);
|
|
139
|
+
else missingProviders.push(provider);
|
|
146
140
|
}
|
|
141
|
+
if (missingProviders.length > 0) throw new Error(formatNoApiKeysError(missingProviders));
|
|
147
142
|
return keys;
|
|
148
143
|
}
|
|
149
144
|
/**
|