@lingo.dev/compiler 0.3.6 → 0.3.8
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 +76 -104
- package/build/metadata/manager.mjs.map +1 -1
- package/build/plugin/build-translator.cjs +6 -6
- package/build/plugin/build-translator.mjs +6 -6
- 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/react/shared/LocaleSwitcher.d.cts +2 -2
- package/build/react/shared/LocaleSwitcher.d.mts +2 -2
- package/build/translation-server/translation-server.cjs +16 -4
- package/build/translation-server/translation-server.mjs +17 -5
- package/build/translation-server/translation-server.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 +10 -12
- package/build/translators/translation-service.mjs +10 -12
- package/build/translators/translation-service.mjs.map +1 -1
- package/package.json +3 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-translator.mjs","names":["translationServer: TranslationServer | undefined","stats: BuildTranslationResult[\"stats\"]","errors: Array<{ locale: LocaleCode; error: string }>","missingLocales: string[]","incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>"],"sources":["../../src/plugin/build-translator.ts"],"sourcesContent":["/**\n * Build-time translation processor\n *\n * Handles translation generation and validation at build time\n * Supports two modes:\n * - \"translate\": Generate all translations, fail if translation fails\n * - \"cache-only\": Validate cache completeness, fail if incomplete\n */\n// TODO (AleksandrSl 08/12/2025): Add ICU validation for messages? The problem is that we don't know which will be rendered as a simple text\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport type { LingoConfig, MetadataSchema } from \"../types\";\nimport { logger } from \"../utils/logger\";\nimport { startTranslationServer, type TranslationServer, } from \"../translation-server\";\nimport { loadMetadata } from \"../metadata/manager\";\nimport { createCache, type TranslationCache, TranslationService, } from \"../translators\";\nimport { dictionaryFrom } from \"../translators/api\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\nexport interface BuildTranslationOptions {\n config: LingoConfig;\n publicOutputPath: string;\n metadataFilePath: string;\n}\n\nexport interface BuildTranslationResult {\n /**\n * Whether the build succeeded\n */\n success: boolean;\n\n /**\n * Error message if build failed\n */\n error?: string;\n\n /**\n * Translation statistics per locale\n */\n stats: Record<\n string,\n {\n total: number;\n translated: number;\n failed: number;\n }\n >;\n}\n\n/**\n * Process translations at build time\n *\n * @throws Error if validation or translation fails (causes build to fail)\n */\nexport async function processBuildTranslations(\n options: BuildTranslationOptions,\n): Promise<BuildTranslationResult> {\n const { config, publicOutputPath, metadataFilePath } = options;\n\n // Determine build mode (env var > options > config)\n const buildMode =\n (process.env.LINGO_BUILD_MODE as \"translate\" | \"cache-only\") ||\n config.buildMode;\n\n logger.info(`🌍 Build mode: ${buildMode}`);\n\n const metadata = await loadMetadata(metadataFilePath);\n\n if (!metadata || Object.keys(metadata.entries).length === 0) {\n logger.info(\"No translations to process (metadata is empty)\");\n return {\n success: true,\n stats: {},\n };\n }\n\n const totalEntries = Object.keys(metadata.entries).length;\n logger.info(`📊 Found ${totalEntries} translatable entries`);\n\n const cache = createCache(config);\n\n // Handle cache-only mode\n if (buildMode === \"cache-only\") {\n logger.info(\"🔍 Validating translation cache...\");\n await validateCache(config, metadata, cache);\n logger.info(\"✅ Cache validation passed\");\n\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n return {\n success: true,\n stats: buildCacheStats(config, metadata),\n };\n }\n\n // Handle translate mode\n logger.info(\"🔄 Generating translations...\");\n let translationServer: TranslationServer | undefined;\n\n try {\n translationServer = await startTranslationServer({\n translationService: new TranslationService(config, logger),\n onError: (err) => {\n logger.error(\"Translation server error:\", err);\n },\n config,\n });\n\n // When pluralization is enabled, we need to generate the source locale file too\n // because pluralization modifies the sourceText\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n logger.info(\n `Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? \" (including source locale for pluralization)\" : \"\"}...`,\n );\n\n const stats: BuildTranslationResult[\"stats\"] = {};\n const errors: Array<{ locale: LocaleCode; error: string }> = [];\n\n // Translate all locales in parallel\n const localePromises = allLocales.map(async (locale) => {\n logger.info(`Translating to ${locale}...`);\n\n const result = await translationServer!.translateAll(locale);\n\n stats[locale] = {\n total: totalEntries,\n translated: Object.keys(result.translations).length,\n failed: result.errors.length,\n };\n\n if (result.errors.length > 0) {\n logger.warn(\n `⚠️ ${result.errors.length} translation error(s) for ${locale}`,\n );\n errors.push({\n locale,\n error: `${result.errors.length} translation(s) failed`,\n });\n } else {\n logger.info(`✅ ${locale} completed successfully`);\n }\n });\n\n await Promise.all(localePromises);\n\n // Fail build if any translations failed in translate mode\n if (errors.length > 0) {\n const errorMsg = formatTranslationErrors(errors);\n logger.error(errorMsg);\n process.exit(1);\n }\n\n // Copy cache to public directory if requested\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n logger.info(\"✅ Translation generation completed successfully\");\n\n return {\n success: true,\n stats,\n };\n } catch (error) {\n logger.error(\n \"❌ Translation generation failed:\\n\",\n error instanceof Error ? error.message : error,\n );\n process.exit(1);\n } finally {\n if (translationServer) {\n await translationServer.stop();\n logger.info(\"✅ Translation server stopped\");\n }\n }\n}\n\n/**\n * Validate that all required translations exist in cache\n * @throws Error if cache is incomplete or missing\n */\nasync function validateCache(\n config: LingoConfig,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n const allHashes = Object.keys(metadata.entries);\n const missingLocales: string[] = [];\n const incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }> = [];\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n try {\n const entries = await cache.get(locale);\n\n if (Object.keys(entries).length === 0) {\n missingLocales.push(locale);\n logger.debug(`Cache file not found or empty for ${locale}`);\n continue;\n }\n\n const missingHashes = allHashes.filter((hash) => !entries[hash]);\n\n if (missingHashes.length > 0) {\n incompleteLocales.push({\n locale,\n missing: missingHashes.length,\n total: allHashes.length,\n });\n\n // Log first few missing hashes for debugging\n logger.debug(\n `Missing hashes in ${locale}: ${missingHashes.slice(0, 5).join(\", \")}${\n missingHashes.length > 5 ? \"...\" : \"\"\n }`,\n );\n }\n } catch (error) {\n missingLocales.push(locale);\n logger.debug(`Failed to read cache for ${locale}:`, error);\n }\n }\n\n if (missingLocales.length > 0 || incompleteLocales.length > 0) {\n const errorMsg = formatCacheValidationError(\n missingLocales,\n incompleteLocales,\n );\n logger.error(errorMsg);\n process.exit(1);\n }\n}\n\nfunction buildCacheStats(\n config: LingoConfig,\n metadata: MetadataSchema,\n): BuildTranslationResult[\"stats\"] {\n const totalEntries = Object.keys(metadata.entries).length;\n const stats: BuildTranslationResult[\"stats\"] = {};\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n stats[locale] = {\n total: totalEntries,\n translated: totalEntries, // Assumed complete if validation passed\n failed: 0,\n };\n }\n\n return stats;\n}\n\nasync function copyStaticFiles(\n config: LingoConfig,\n publicOutputPath: string,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n logger.info(`📦 Generating static translation files in ${publicOutputPath}`);\n\n await fs.mkdir(publicOutputPath, { recursive: true });\n\n const usedHashes = new Set(Object.keys(metadata.entries));\n logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n const publicFilePath = path.join(publicOutputPath, `${locale}.json`);\n\n try {\n const entries = await cache.get(locale, Array.from(usedHashes));\n const outputData = dictionaryFrom(locale, entries);\n\n await fs.writeFile(\n publicFilePath,\n JSON.stringify(outputData, null, 2),\n \"utf-8\",\n );\n\n logger.info(\n `✓ Generated ${locale}.json (${Object.keys(entries).length} translations)`,\n );\n } catch (error) {\n logger.error(`❌ Failed to generate ${locale}.json:`, error);\n process.exit(1);\n }\n }\n}\n\nfunction formatCacheValidationError(\n missingLocales: string[],\n incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>,\n): string {\n let msg = \"❌ Cache validation failed in cache-only mode:\\n\\n\";\n\n if (missingLocales.length > 0) {\n msg += ` 📁 Missing cache files:\\n`;\n msg += missingLocales.map((locale) => ` - ${locale}.json`).join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n if (incompleteLocales.length > 0) {\n msg += ` 📊 Incomplete cache:\\n`;\n msg += incompleteLocales\n .map(\n (item) =>\n ` - ${item.locale}: ${item.missing}/${item.total} translations missing`,\n )\n .join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n msg += ` 💡 To fix:\\n`;\n msg += ` 1. Set LINGO_BUILD_MODE=translate to generate translations\\n`;\n msg += ` 2. Commit the generated .lingo/cache/*.json files\\n`;\n msg += ` 3. Ensure translation API keys are available if generating translations`;\n\n return msg;\n}\n\nfunction formatTranslationErrors(\n errors: Array<{ locale: LocaleCode; error: string }>,\n): string {\n let msg = \"❌ Translation generation failed:\\n\\n\";\n\n msg += errors.map((err) => ` - ${err.locale}: ${err.error}`).join(\"\\n\");\n\n msg += \"\\n\\n\";\n msg += ` 💡 Translation errors must be resolved in \"translate\" mode.\\n`;\n msg += ` Check translation server logs for details.`;\n\n return msg;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"build-translator.mjs","names":["translationServer: TranslationServer | undefined","stats: BuildTranslationResult[\"stats\"]","errors: Array<{ locale: LocaleCode; error: string }>","missingLocales: string[]","incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>"],"sources":["../../src/plugin/build-translator.ts"],"sourcesContent":["/**\n * Build-time translation processor\n *\n * Handles translation generation and validation at build time\n * Supports two modes:\n * - \"translate\": Generate all translations, fail if translation fails\n * - \"cache-only\": Validate cache completeness, fail if incomplete\n */\n// TODO (AleksandrSl 08/12/2025): Add ICU validation for messages? The problem is that we don't know which will be rendered as a simple text\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport type { LingoConfig, MetadataSchema } from \"../types\";\nimport { logger } from \"../utils/logger\";\nimport {\n startTranslationServer,\n type TranslationServer,\n} from \"../translation-server\";\nimport { loadMetadata } from \"../metadata/manager\";\nimport {\n createCache,\n type TranslationCache,\n TranslationService,\n} from \"../translators\";\nimport { dictionaryFrom } from \"../translators/api\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\nexport interface BuildTranslationOptions {\n config: LingoConfig;\n publicOutputPath: string;\n metadataFilePath: string;\n}\n\nexport interface BuildTranslationResult {\n /**\n * Whether the build succeeded\n */\n success: boolean;\n\n /**\n * Error message if build failed\n */\n error?: string;\n\n /**\n * Translation statistics per locale\n */\n stats: Record<\n string,\n {\n total: number;\n translated: number;\n failed: number;\n }\n >;\n}\n\n/**\n * Process translations at build time\n *\n * @throws Error if validation or translation fails (causes build to fail)\n */\nexport async function processBuildTranslations(\n options: BuildTranslationOptions,\n): Promise<BuildTranslationResult> {\n const { config, publicOutputPath, metadataFilePath } = options;\n\n // Determine build mode (env var > options > config)\n const buildMode =\n (process.env.LINGO_BUILD_MODE as \"translate\" | \"cache-only\") ||\n config.buildMode;\n\n logger.info(`🌍 Build mode: ${buildMode}`);\n\n const metadata = await loadMetadata(metadataFilePath, true);\n\n if (!metadata || Object.keys(metadata).length === 0) {\n logger.info(\"No translations to process (metadata is empty)\");\n return {\n success: true,\n stats: {},\n };\n }\n\n const totalEntries = Object.keys(metadata).length;\n logger.info(`📊 Found ${totalEntries} translatable entries`);\n\n const cache = createCache(config);\n\n // Handle cache-only mode\n if (buildMode === \"cache-only\") {\n logger.info(\"🔍 Validating translation cache...\");\n await validateCache(config, metadata, cache);\n logger.info(\"✅ Cache validation passed\");\n\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n return {\n success: true,\n stats: buildCacheStats(config, metadata),\n };\n }\n\n // Handle translate mode\n logger.info(\"🔄 Generating translations...\");\n let translationServer: TranslationServer | undefined;\n\n try {\n translationServer = await startTranslationServer({\n translationService: new TranslationService(config, logger),\n onError: (err) => {\n logger.error(\"Translation server error:\", err);\n },\n config,\n });\n\n // When pluralization is enabled, we need to generate the source locale file too\n // because pluralization modifies the sourceText\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n logger.info(\n `Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? \" (including source locale for pluralization)\" : \"\"}...`,\n );\n\n const stats: BuildTranslationResult[\"stats\"] = {};\n const errors: Array<{ locale: LocaleCode; error: string }> = [];\n\n // Translate all locales in parallel\n const localePromises = allLocales.map(async (locale) => {\n logger.info(`Translating to ${locale}...`);\n\n const result = await translationServer!.translateAll(locale);\n\n stats[locale] = {\n total: totalEntries,\n translated: Object.keys(result.translations).length,\n failed: result.errors.length,\n };\n\n if (result.errors.length > 0) {\n logger.warn(\n `⚠️ ${result.errors.length} translation error(s) for ${locale}`,\n );\n errors.push({\n locale,\n error: `${result.errors.length} translation(s) failed`,\n });\n } else {\n logger.info(`✅ ${locale} completed successfully`);\n }\n });\n\n await Promise.all(localePromises);\n\n // Fail build if any translations failed in translate mode\n if (errors.length > 0) {\n const errorMsg = formatTranslationErrors(errors);\n logger.error(errorMsg);\n process.exit(1);\n }\n\n // Copy cache to public directory if requested\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n logger.info(\"✅ Translation generation completed successfully\");\n\n return {\n success: true,\n stats,\n };\n } catch (error) {\n logger.error(\n \"❌ Translation generation failed:\\n\",\n error instanceof Error ? error.message : error,\n );\n process.exit(1);\n } finally {\n if (translationServer) {\n await translationServer.stop();\n logger.info(\"✅ Translation server stopped\");\n }\n }\n}\n\n/**\n * Validate that all required translations exist in cache\n * @throws Error if cache is incomplete or missing\n */\nasync function validateCache(\n config: LingoConfig,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n const allHashes = Object.keys(metadata);\n const missingLocales: string[] = [];\n const incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }> = [];\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n try {\n const entries = await cache.get(locale);\n\n if (Object.keys(entries).length === 0) {\n missingLocales.push(locale);\n logger.debug(`Cache file not found or empty for ${locale}`);\n continue;\n }\n\n const missingHashes = allHashes.filter((hash) => !entries[hash]);\n\n if (missingHashes.length > 0) {\n incompleteLocales.push({\n locale,\n missing: missingHashes.length,\n total: allHashes.length,\n });\n\n // Log first few missing hashes for debugging\n logger.debug(\n `Missing hashes in ${locale}: ${missingHashes.slice(0, 5).join(\", \")}${\n missingHashes.length > 5 ? \"...\" : \"\"\n }`,\n );\n }\n } catch (error) {\n missingLocales.push(locale);\n logger.debug(`Failed to read cache for ${locale}:`, error);\n }\n }\n\n if (missingLocales.length > 0 || incompleteLocales.length > 0) {\n const errorMsg = formatCacheValidationError(\n missingLocales,\n incompleteLocales,\n );\n logger.error(errorMsg);\n process.exit(1);\n }\n}\n\nfunction buildCacheStats(\n config: LingoConfig,\n metadata: MetadataSchema,\n): BuildTranslationResult[\"stats\"] {\n const totalEntries = Object.keys(metadata).length;\n const stats: BuildTranslationResult[\"stats\"] = {};\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n stats[locale] = {\n total: totalEntries,\n translated: totalEntries, // Assumed complete if validation passed\n failed: 0,\n };\n }\n\n return stats;\n}\n\nasync function copyStaticFiles(\n config: LingoConfig,\n publicOutputPath: string,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n logger.info(`📦 Generating static translation files in ${publicOutputPath}`);\n\n await fs.mkdir(publicOutputPath, { recursive: true });\n\n const usedHashes = new Set(Object.keys(metadata));\n logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n const publicFilePath = path.join(publicOutputPath, `${locale}.json`);\n\n try {\n const entries = await cache.get(locale, Array.from(usedHashes));\n const outputData = dictionaryFrom(locale, entries);\n\n await fs.writeFile(\n publicFilePath,\n JSON.stringify(outputData, null, 2),\n \"utf-8\",\n );\n\n logger.info(\n `✓ Generated ${locale}.json (${Object.keys(entries).length} translations)`,\n );\n } catch (error) {\n logger.error(`❌ Failed to generate ${locale}.json:`, error);\n process.exit(1);\n }\n }\n}\n\nfunction formatCacheValidationError(\n missingLocales: string[],\n incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>,\n): string {\n let msg = \"❌ Cache validation failed in cache-only mode:\\n\\n\";\n\n if (missingLocales.length > 0) {\n msg += ` 📁 Missing cache files:\\n`;\n msg += missingLocales.map((locale) => ` - ${locale}.json`).join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n if (incompleteLocales.length > 0) {\n msg += ` 📊 Incomplete cache:\\n`;\n msg += incompleteLocales\n .map(\n (item) =>\n ` - ${item.locale}: ${item.missing}/${item.total} translations missing`,\n )\n .join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n msg += ` 💡 To fix:\\n`;\n msg += ` 1. Set LINGO_BUILD_MODE=translate to generate translations\\n`;\n msg += ` 2. Commit the generated .lingo/cache/*.json files\\n`;\n msg += ` 3. Ensure translation API keys are available if generating translations`;\n\n return msg;\n}\n\nfunction formatTranslationErrors(\n errors: Array<{ locale: LocaleCode; error: string }>,\n): string {\n let msg = \"❌ Translation generation failed:\\n\\n\";\n\n msg += errors.map((err) => ` - ${err.locale}: ${err.error}`).join(\"\\n\");\n\n msg += \"\\n\\n\";\n msg += ` 💡 Translation errors must be resolved in \"translate\" mode.\\n`;\n msg += ` Check translation server logs for details.`;\n\n return msg;\n}\n"],"mappings":"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"}
|
|
@@ -16,7 +16,6 @@ async function nextCompilerLoader(source) {
|
|
|
16
16
|
const callback = this.async();
|
|
17
17
|
try {
|
|
18
18
|
const config = this.getOptions();
|
|
19
|
-
const metadataManager = new require_manager.MetadataManager(config.metadataFilePath);
|
|
20
19
|
require_logger.logger.debug(`[Turbopack Loader] Processing: ${this.resourcePath}`);
|
|
21
20
|
const result = require_index.transformComponent({
|
|
22
21
|
code: source,
|
|
@@ -25,7 +24,7 @@ async function nextCompilerLoader(source) {
|
|
|
25
24
|
});
|
|
26
25
|
if (!result.transformed) return callback(null, source);
|
|
27
26
|
if (result.newEntries && result.newEntries.length > 0) {
|
|
28
|
-
await
|
|
27
|
+
await require_manager.saveMetadata(config.metadataFilePath, result.newEntries, config.environment !== "development");
|
|
29
28
|
require_logger.logger.debug(`[Turbopack Loader] Found ${result.newEntries.length} translatable text(s) in ${this.resourcePath}`);
|
|
30
29
|
}
|
|
31
30
|
const validMap = result.map && result.map.sources && Array.isArray(result.map.sources) && result.map.sources.every((s) => typeof s === "string") ? result.map : void 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { saveMetadata } from "../metadata/manager.mjs";
|
|
3
3
|
import { transformComponent } from "./transform/index.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/plugin/next-compiler-loader.ts
|
|
@@ -16,7 +16,6 @@ async function nextCompilerLoader(source) {
|
|
|
16
16
|
const callback = this.async();
|
|
17
17
|
try {
|
|
18
18
|
const config = this.getOptions();
|
|
19
|
-
const metadataManager = new MetadataManager(config.metadataFilePath);
|
|
20
19
|
logger.debug(`[Turbopack Loader] Processing: ${this.resourcePath}`);
|
|
21
20
|
const result = transformComponent({
|
|
22
21
|
code: source,
|
|
@@ -25,7 +24,7 @@ async function nextCompilerLoader(source) {
|
|
|
25
24
|
});
|
|
26
25
|
if (!result.transformed) return callback(null, source);
|
|
27
26
|
if (result.newEntries && result.newEntries.length > 0) {
|
|
28
|
-
await
|
|
27
|
+
await saveMetadata(config.metadataFilePath, result.newEntries, config.environment !== "development");
|
|
29
28
|
logger.debug(`[Turbopack Loader] Found ${result.newEntries.length} translatable text(s) in ${this.resourcePath}`);
|
|
30
29
|
}
|
|
31
30
|
const validMap = result.map && result.map.sources && Array.isArray(result.map.sources) && result.map.sources.every((s) => typeof s === "string") ? result.map : void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next-compiler-loader.mjs","names":["config: LingoConfig & { metadataFilePath: string }"],"sources":["../../src/plugin/next-compiler-loader.ts"],"sourcesContent":["import type { LingoConfig } from \"../types\";\nimport { transformComponent } from \"./transform\";\nimport { logger } from \"../utils/logger\";\nimport {
|
|
1
|
+
{"version":3,"file":"next-compiler-loader.mjs","names":["config: LingoConfig & { metadataFilePath: string }"],"sources":["../../src/plugin/next-compiler-loader.ts"],"sourcesContent":["import type { LingoConfig } from \"../types\";\nimport { transformComponent } from \"./transform\";\nimport { logger } from \"../utils/logger\";\nimport { saveMetadata } from \"../metadata/manager\";\n\n/**\n * Turbopack/Webpack loader for automatic translation\n *\n * This loader transforms React components to inject translation calls automatically.\n *\n * For production builds, translations are generated after compilation completes\n * via Next.js's runAfterProductionCompile hook (see next.ts plugin).\n */\nexport default async function nextCompilerLoader(\n this: any,\n source: string,\n): Promise<void> {\n // TODO (AleksandrSl 14/12/2025): Webpack doesn't like callback usage in async function.\n // But async function can return only code, so we have to use promises which is sad. It actually errors only when we catch an error, so it's not an urgent problem\n\n // Ensure we're running in loader context\n if (typeof this.async !== \"function\") {\n throw new Error(\"This module must be run as a loader\");\n }\n const callback = this.async();\n\n try {\n const config: LingoConfig & { metadataFilePath: string } =\n this.getOptions();\n\n logger.debug(`[Turbopack Loader] Processing: ${this.resourcePath}`);\n\n // Transform the component\n const result = transformComponent({\n code: source,\n filePath: this.resourcePath,\n config,\n });\n\n // If no transformation occurred, return original source\n if (!result.transformed) {\n return callback(null, source);\n }\n\n // Update metadata with new entries\n if (result.newEntries && result.newEntries.length > 0) {\n await saveMetadata(\n config.metadataFilePath,\n result.newEntries,\n config.environment !== \"development\",\n );\n\n logger.debug(\n `[Turbopack Loader] Found ${result.newEntries.length} translatable text(s) in ${this.resourcePath}`,\n );\n }\n\n // Validate source map before passing to webpack\n // Webpack crashes if sources array contains undefined values\n const validMap =\n result.map &&\n result.map.sources &&\n Array.isArray(result.map.sources) &&\n result.map.sources.every((s: any) => typeof s === \"string\")\n ? result.map\n : undefined;\n\n callback(null, result.code, validMap);\n } catch (error) {\n logger.error(`Compiler failed for ${this.resourcePath}:`);\n logger.error(\n \"Details:\",\n error,\n typeof error === \"object\" && error && \"message\" in error\n ? error.message\n : error,\n error instanceof Error ? error.stack : undefined,\n );\n callback(error as Error);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,eAA8B,mBAE5B,QACe;AAKf,KAAI,OAAO,KAAK,UAAU,WACxB,OAAM,IAAI,MAAM,sCAAsC;CAExD,MAAM,WAAW,KAAK,OAAO;AAE7B,KAAI;EACF,MAAMA,SACJ,KAAK,YAAY;AAEnB,SAAO,MAAM,kCAAkC,KAAK,eAAe;EAGnE,MAAM,SAAS,mBAAmB;GAChC,MAAM;GACN,UAAU,KAAK;GACf;GACD,CAAC;AAGF,MAAI,CAAC,OAAO,YACV,QAAO,SAAS,MAAM,OAAO;AAI/B,MAAI,OAAO,cAAc,OAAO,WAAW,SAAS,GAAG;AACrD,SAAM,aACJ,OAAO,kBACP,OAAO,YACP,OAAO,gBAAgB,cACxB;AAED,UAAO,MACL,4BAA4B,OAAO,WAAW,OAAO,2BAA2B,KAAK,eACtF;;EAKH,MAAM,WACJ,OAAO,OACP,OAAO,IAAI,WACX,MAAM,QAAQ,OAAO,IAAI,QAAQ,IACjC,OAAO,IAAI,QAAQ,OAAO,MAAW,OAAO,MAAM,SAAS,GACvD,OAAO,MACP;AAEN,WAAS,MAAM,OAAO,MAAM,SAAS;UAC9B,OAAO;AACd,SAAO,MAAM,uBAAuB,KAAK,aAAa,GAAG;AACzD,SAAO,MACL,YACA,OACA,OAAO,UAAU,YAAY,SAAS,aAAa,QAC/C,MAAM,UACN,OACJ,iBAAiB,QAAQ,MAAM,QAAQ,OACxC;AACD,WAAS,MAAe"}
|
package/build/plugin/next.cjs
CHANGED
|
@@ -152,9 +152,7 @@ async function withLingo(nextConfig = {}, lingoOptions) {
|
|
|
152
152
|
const translationServerUrl = process.env.LINGO_TRANSLATION_SERVER_URL;
|
|
153
153
|
if (isMainRunner()) {
|
|
154
154
|
require_manager.cleanupExistingMetadata(metadataFilePath);
|
|
155
|
-
require_cleanup.registerCleanupOnCurrentProcess({ cleanup: () =>
|
|
156
|
-
require_manager.cleanupExistingMetadata(metadataFilePath);
|
|
157
|
-
} });
|
|
155
|
+
require_cleanup.registerCleanupOnCurrentProcess({ cleanup: () => require_manager.cleanupExistingMetadata(metadataFilePath) });
|
|
158
156
|
}
|
|
159
157
|
const existingTurbopackConfig = getTurbopackConfig(nextConfig);
|
|
160
158
|
let mergedRules = mergeTurbopackRules(existingTurbopackConfig.rules ?? {}, Object.values(loaders({
|
package/build/plugin/next.mjs
CHANGED
|
@@ -153,9 +153,7 @@ async function withLingo(nextConfig = {}, lingoOptions) {
|
|
|
153
153
|
const translationServerUrl = process.env.LINGO_TRANSLATION_SERVER_URL;
|
|
154
154
|
if (isMainRunner()) {
|
|
155
155
|
cleanupExistingMetadata(metadataFilePath);
|
|
156
|
-
registerCleanupOnCurrentProcess({ cleanup: () =>
|
|
157
|
-
cleanupExistingMetadata(metadataFilePath);
|
|
158
|
-
} });
|
|
156
|
+
registerCleanupOnCurrentProcess({ cleanup: () => cleanupExistingMetadata(metadataFilePath) });
|
|
159
157
|
}
|
|
160
158
|
const existingTurbopackConfig = getTurbopackConfig(nextConfig);
|
|
161
159
|
let mergedRules = mergeTurbopackRules(existingTurbopackConfig.rules ?? {}, Object.values(loaders({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"next.mjs","names":["turbopackConfig: Partial<NextConfig>","webpack: NextJsWebpackConfig"],"sources":["../../src/plugin/next.ts"],"sourcesContent":["import type { NextConfig } from \"next\";\nimport type {\n NextJsWebpackConfig,\n TurbopackOptions,\n WebpackConfigContext,\n} from \"next/dist/server/config-shared\";\nimport { createLingoConfig } from \"../utils/config-factory\";\nimport { logger } from \"../utils/logger\";\nimport type { LingoConfig, PartialLingoConfig } from \"../types\";\nimport { processBuildTranslations } from \"./build-translator\";\nimport { startOrGetTranslationServer } from \"../translation-server/translation-server\";\nimport { cleanupExistingMetadata, getMetadataPath } from \"../metadata/manager\";\nimport { registerCleanupOnCurrentProcess } from \"./cleanup\";\nimport { useI18nRegex } from \"./transform/use-i18n\";\nimport { TranslationService } from \"../translators\";\n\nexport type LingoNextPluginOptions = PartialLingoConfig;\n\ntype RuleKey = \"compiler\" | \"devConfig\" | \"localeServer\" | \"localeClient\";\n\nfunction loaders({\n lingoConfig,\n metadataFilePath,\n translationServerUrl,\n}: {\n lingoConfig: LingoConfig;\n metadataFilePath: string;\n translationServerUrl?: string;\n}): Record<RuleKey, { turbopack?: any; webpack?: any }> {\n const common = {\n sourceRoot: lingoConfig.sourceRoot,\n lingoDir: lingoConfig.lingoDir,\n sourceLocale: lingoConfig.sourceLocale,\n };\n\n // TODO (AleksandrSl 14/12/2025): Type options.\n const compilerLoader = {\n loader: \"@lingo.dev/compiler/next-compiler-loader\",\n options: {\n ...common,\n useDirective: lingoConfig.useDirective,\n metadataFilePath,\n },\n };\n\n const configLoader = {\n loader: \"@lingo.dev/compiler/next-config-loader\",\n options: {\n ...common,\n dev: {\n translationServerUrl,\n ...lingoConfig.dev,\n },\n },\n };\n const localeServerLoader = {\n loader: \"@lingo.dev/compiler/next-locale-server-loader\",\n options: {\n ...common,\n localePersistence: lingoConfig.localePersistence,\n },\n };\n\n const localeClientLoader = {\n loader: \"@lingo.dev/compiler/next-locale-client-loader\",\n options: {\n ...common,\n localePersistence: lingoConfig.localePersistence,\n },\n };\n\n return {\n compiler: {\n turbopack: {\n pattern: \"*.{tsx,jsx}\",\n config: {\n condition: {\n content: lingoConfig.useDirective ? useI18nRegex : undefined,\n },\n loaders: [compilerLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /\\.(tsx|jsx)$/i,\n exclude: /node_modules/,\n use: [compilerLoader],\n },\n },\n\n devConfig: translationServerUrl\n ? {\n turbopack: {\n pattern: \"**/virtual/config.mjs\",\n config: {\n loaders: [configLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /virtual\\/config\\.mjs$/i,\n use: [configLoader],\n },\n }\n : {},\n\n localeServer: {\n turbopack: {\n pattern: \"**/virtual/locale/server.mjs\",\n config: {\n loaders: [localeServerLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /virtual\\/locale[\\\\/]server\\.mjs$/i,\n use: [localeServerLoader],\n },\n },\n\n localeClient: {\n turbopack: {\n pattern: \"**/virtual/locale/client.mjs\",\n config: {\n loaders: [localeClientLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /virtual\\/locale[\\\\/]client\\.mjs$/i,\n use: [localeClientLoader],\n },\n },\n };\n}\n\n/**\n * Check if Next.js supports stable turbopack config (Next.js 16+)\n */\nfunction hasStableTurboConfig(): boolean {\n try {\n const nextPackage = require(\"next/package.json\");\n const majorVersion = parseInt(nextPackage.version.split(\".\")[0], 10);\n return majorVersion >= 16;\n } catch {\n return false;\n }\n}\n\nfunction getTurbopackConfig(userConfig: NextConfig): TurbopackOptions {\n return (\n (hasStableTurboConfig()\n ? userConfig?.turbopack?.rules\n : // @ts-expect-error - experimental.turbo for Next.js <16\n userConfig?.experimental?.turbo) || {}\n );\n}\n\n/**\n * Merge Turbopack rules without mutating original\n */\nfunction mergeTurbopackRules(\n existingRules: Record<string, any>,\n newRules: ({ pattern: string; config: any } | undefined)[],\n): Record<string, any> {\n const mergedRules = { ...existingRules };\n\n for (const newRule of newRules) {\n if (!newRule) continue;\n const { pattern, config } = newRule;\n\n if (mergedRules[pattern]) {\n if (Array.isArray(mergedRules[pattern])) {\n mergedRules[pattern] = [...mergedRules[pattern], config];\n } else {\n mergedRules[pattern] = [mergedRules[pattern], config];\n }\n } else {\n mergedRules[pattern] = config;\n }\n }\n\n return mergedRules;\n}\n\n// Next read config several times. Once in the main runner,\n// and ~ twice in the build workers which cannot get the config otherwise, because it's not serializable.\n// Workers start in separate processed by get most of the env from the main loader\nfunction isMainRunner() {\n return (\n !process.env.NEXT_PRIVATE_BUILD_WORKER &&\n !process.env.IS_NEXT_WORKER &&\n !process.env.NEXT_PRIVATE_WORKER\n );\n}\n\nexport async function withLingo(\n nextConfig: NextConfig = {},\n lingoOptions: LingoNextPluginOptions,\n): Promise<NextConfig> {\n const lingoConfig = createLingoConfig(lingoOptions);\n let metadataFilePath = getMetadataPath(lingoConfig);\n const isDev = lingoConfig.environment === \"development\";\n\n logger.debug(\n `Initializing Lingo.dev compiler. Is dev mode: ${isDev}. Is main runner: ${isMainRunner()}`,\n );\n\n // Try to start up the translation server once.\n // We have two barriers, a simple one here and a more complex one inside the startTranslationServer which doesn't start the server if it can find one running.\n // We do not use isMainRunner here, because we need to start the server as early as possible, so the loaders get the translation server url. The main runner in dev mode runs after a dev server process is started.\n if (isDev && !process.env.LINGO_TRANSLATION_SERVER_URL) {\n const translationServer = await startOrGetTranslationServer({\n translationService: new TranslationService(lingoConfig, 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: lingoConfig,\n });\n process.env.LINGO_TRANSLATION_SERVER_URL = translationServer.url;\n if (translationServer.server) {\n // We start the server in the same process, so we should be fine without any sync cleanup. Server should be killed with the process.\n registerCleanupOnCurrentProcess({\n asyncCleanup: async () => {\n await translationServer.server.stop();\n },\n });\n }\n }\n\n const translationServerUrl = process.env.LINGO_TRANSLATION_SERVER_URL;\n\n if (isMainRunner()) {\n // We need to cleaup the file only once, to avoid having extra translation introduced into the build, or old translation to pile up.\n cleanupExistingMetadata(metadataFilePath);\n\n registerCleanupOnCurrentProcess({\n cleanup: () => {\n cleanupExistingMetadata(metadataFilePath);\n },\n });\n }\n\n const existingTurbopackConfig = getTurbopackConfig(nextConfig);\n let mergedRules = mergeTurbopackRules(\n existingTurbopackConfig.rules ?? {},\n Object.values(\n loaders({ lingoConfig, metadataFilePath, translationServerUrl }),\n ).map((rules) => rules.turbopack),\n );\n\n const existingResolveAlias = existingTurbopackConfig.resolveAlias;\n const mergedResolveAlias = {\n ...existingResolveAlias,\n // TODO (AleksandrSl 08/12/2025): Describe what have to be done to support custom resolvers\n };\n\n let turbopackConfig: Partial<NextConfig>;\n if (hasStableTurboConfig()) {\n turbopackConfig = {\n turbopack: {\n ...nextConfig.turbopack, // Preserve existing turbopack options\n rules: mergedRules,\n resolveAlias: mergedResolveAlias,\n },\n };\n } else {\n turbopackConfig = {\n experimental: {\n ...nextConfig.experimental, // Preserve ALL experimental options\n // @ts-expect-error - turbo for Next.js <16\n turbo: {\n // @ts-expect-error - turbo for Next.js <16\n ...nextConfig.experimental?.turbo, // Preserve existing turbo options\n rules: mergedRules,\n resolveAlias: mergedResolveAlias,\n },\n },\n };\n }\n\n const runAfterProductionCompile = async ({\n distDir,\n projectDir,\n }: {\n distDir: string;\n projectDir: string;\n }) => {\n if (typeof nextConfig.compiler?.runAfterProductionCompile === \"function\") {\n await nextConfig.compiler.runAfterProductionCompile({\n distDir,\n projectDir,\n });\n }\n\n logger.info(\"Running post-build translation generation...\");\n\n try {\n await processBuildTranslations({\n config: lingoConfig,\n publicOutputPath: distDir,\n metadataFilePath,\n });\n } catch (error) {\n logger.error(\n \"Translation generation failed:\",\n error instanceof Error ? error.message : error,\n );\n throw error;\n }\n };\n\n const webpack: NextJsWebpackConfig = (\n config: any,\n options: WebpackConfigContext,\n ) => {\n // Apply user's webpack config first if it exists\n let modifiedConfig = config;\n if (typeof nextConfig.webpack === \"function\") {\n modifiedConfig = nextConfig.webpack(config, options);\n }\n\n const lingoRules = Object.values(\n loaders({ lingoConfig, metadataFilePath, translationServerUrl }),\n )\n .map((rules) => rules.webpack)\n .filter(Boolean);\n\n // I tried using plugin from unplugin, but with all the loaders stuff it works poorly.\n // Failing with weird errors which appear on the later compilations, probably something with the cache and virtual modules\n modifiedConfig.module.rules.unshift(...lingoRules);\n\n return modifiedConfig;\n };\n\n return {\n ...nextConfig,\n ...turbopackConfig,\n compiler: {\n ...nextConfig.compiler,\n runAfterProductionCompile,\n },\n webpack,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"next.mjs","names":["turbopackConfig: Partial<NextConfig>","webpack: NextJsWebpackConfig"],"sources":["../../src/plugin/next.ts"],"sourcesContent":["import type { NextConfig } from \"next\";\nimport type {\n NextJsWebpackConfig,\n TurbopackOptions,\n WebpackConfigContext,\n} from \"next/dist/server/config-shared\";\nimport { createLingoConfig } from \"../utils/config-factory\";\nimport { logger } from \"../utils/logger\";\nimport type { LingoConfig, PartialLingoConfig } from \"../types\";\nimport { processBuildTranslations } from \"./build-translator\";\nimport { startOrGetTranslationServer } from \"../translation-server/translation-server\";\nimport { cleanupExistingMetadata, getMetadataPath } from \"../metadata/manager\";\nimport { registerCleanupOnCurrentProcess } from \"./cleanup\";\nimport { useI18nRegex } from \"./transform/use-i18n\";\nimport { TranslationService } from \"../translators\";\n\nexport type LingoNextPluginOptions = PartialLingoConfig;\n\ntype RuleKey = \"compiler\" | \"devConfig\" | \"localeServer\" | \"localeClient\";\n\nfunction loaders({\n lingoConfig,\n metadataFilePath,\n translationServerUrl,\n}: {\n lingoConfig: LingoConfig;\n metadataFilePath: string;\n translationServerUrl?: string;\n}): Record<RuleKey, { turbopack?: any; webpack?: any }> {\n const common = {\n sourceRoot: lingoConfig.sourceRoot,\n lingoDir: lingoConfig.lingoDir,\n sourceLocale: lingoConfig.sourceLocale,\n };\n\n // TODO (AleksandrSl 14/12/2025): Type options.\n const compilerLoader = {\n loader: \"@lingo.dev/compiler/next-compiler-loader\",\n options: {\n ...common,\n useDirective: lingoConfig.useDirective,\n metadataFilePath,\n },\n };\n\n const configLoader = {\n loader: \"@lingo.dev/compiler/next-config-loader\",\n options: {\n ...common,\n dev: {\n translationServerUrl,\n ...lingoConfig.dev,\n },\n },\n };\n const localeServerLoader = {\n loader: \"@lingo.dev/compiler/next-locale-server-loader\",\n options: {\n ...common,\n localePersistence: lingoConfig.localePersistence,\n },\n };\n\n const localeClientLoader = {\n loader: \"@lingo.dev/compiler/next-locale-client-loader\",\n options: {\n ...common,\n localePersistence: lingoConfig.localePersistence,\n },\n };\n\n return {\n compiler: {\n turbopack: {\n pattern: \"*.{tsx,jsx}\",\n config: {\n condition: {\n content: lingoConfig.useDirective ? useI18nRegex : undefined,\n },\n loaders: [compilerLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /\\.(tsx|jsx)$/i,\n exclude: /node_modules/,\n use: [compilerLoader],\n },\n },\n\n devConfig: translationServerUrl\n ? {\n turbopack: {\n pattern: \"**/virtual/config.mjs\",\n config: {\n loaders: [configLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /virtual\\/config\\.mjs$/i,\n use: [configLoader],\n },\n }\n : {},\n\n localeServer: {\n turbopack: {\n pattern: \"**/virtual/locale/server.mjs\",\n config: {\n loaders: [localeServerLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /virtual\\/locale[\\\\/]server\\.mjs$/i,\n use: [localeServerLoader],\n },\n },\n\n localeClient: {\n turbopack: {\n pattern: \"**/virtual/locale/client.mjs\",\n config: {\n loaders: [localeClientLoader],\n },\n },\n webpack: {\n enforce: \"pre\",\n test: /virtual\\/locale[\\\\/]client\\.mjs$/i,\n use: [localeClientLoader],\n },\n },\n };\n}\n\n/**\n * Check if Next.js supports stable turbopack config (Next.js 16+)\n */\nfunction hasStableTurboConfig(): boolean {\n try {\n const nextPackage = require(\"next/package.json\");\n const majorVersion = parseInt(nextPackage.version.split(\".\")[0], 10);\n return majorVersion >= 16;\n } catch {\n return false;\n }\n}\n\nfunction getTurbopackConfig(userConfig: NextConfig): TurbopackOptions {\n return (\n (hasStableTurboConfig()\n ? userConfig?.turbopack?.rules\n : // @ts-expect-error - experimental.turbo for Next.js <16\n userConfig?.experimental?.turbo) || {}\n );\n}\n\n/**\n * Merge Turbopack rules without mutating original\n */\nfunction mergeTurbopackRules(\n existingRules: Record<string, any>,\n newRules: ({ pattern: string; config: any } | undefined)[],\n): Record<string, any> {\n const mergedRules = { ...existingRules };\n\n for (const newRule of newRules) {\n if (!newRule) continue;\n const { pattern, config } = newRule;\n\n if (mergedRules[pattern]) {\n if (Array.isArray(mergedRules[pattern])) {\n mergedRules[pattern] = [...mergedRules[pattern], config];\n } else {\n mergedRules[pattern] = [mergedRules[pattern], config];\n }\n } else {\n mergedRules[pattern] = config;\n }\n }\n\n return mergedRules;\n}\n\n// Next read config several times. Once in the main runner,\n// and ~ twice in the build workers which cannot get the config otherwise, because it's not serializable.\n// Workers start in separate processed by get most of the env from the main loader\nfunction isMainRunner() {\n return (\n !process.env.NEXT_PRIVATE_BUILD_WORKER &&\n !process.env.IS_NEXT_WORKER &&\n !process.env.NEXT_PRIVATE_WORKER\n );\n}\n\nexport async function withLingo(\n nextConfig: NextConfig = {},\n lingoOptions: LingoNextPluginOptions,\n): Promise<NextConfig> {\n const lingoConfig = createLingoConfig(lingoOptions);\n let metadataFilePath = getMetadataPath(lingoConfig);\n const isDev = lingoConfig.environment === \"development\";\n\n logger.debug(\n `Initializing Lingo.dev compiler. Is dev mode: ${isDev}. Is main runner: ${isMainRunner()}`,\n );\n\n // Try to start up the translation server once.\n // We have two barriers, a simple one here and a more complex one inside the startTranslationServer which doesn't start the server if it can find one running.\n // We do not use isMainRunner here, because we need to start the server as early as possible, so the loaders get the translation server url. The main runner in dev mode runs after a dev server process is started.\n if (isDev && !process.env.LINGO_TRANSLATION_SERVER_URL) {\n const translationServer = await startOrGetTranslationServer({\n translationService: new TranslationService(lingoConfig, 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: lingoConfig,\n });\n process.env.LINGO_TRANSLATION_SERVER_URL = translationServer.url;\n if (translationServer.server) {\n // We start the server in the same process, so we should be fine without any sync cleanup. Server should be killed with the process.\n registerCleanupOnCurrentProcess({\n asyncCleanup: async () => {\n await translationServer.server.stop();\n },\n });\n }\n }\n\n const translationServerUrl = process.env.LINGO_TRANSLATION_SERVER_URL;\n\n if (isMainRunner()) {\n // We need to cleaup the file only once, to avoid having extra translation introduced into the build, or old translation to pile up.\n cleanupExistingMetadata(metadataFilePath);\n\n registerCleanupOnCurrentProcess({\n cleanup: () => cleanupExistingMetadata(metadataFilePath),\n });\n }\n\n const existingTurbopackConfig = getTurbopackConfig(nextConfig);\n let mergedRules = mergeTurbopackRules(\n existingTurbopackConfig.rules ?? {},\n Object.values(\n loaders({ lingoConfig, metadataFilePath, translationServerUrl }),\n ).map((rules) => rules.turbopack),\n );\n\n const existingResolveAlias = existingTurbopackConfig.resolveAlias;\n const mergedResolveAlias = {\n ...existingResolveAlias,\n // TODO (AleksandrSl 08/12/2025): Describe what have to be done to support custom resolvers\n };\n\n let turbopackConfig: Partial<NextConfig>;\n if (hasStableTurboConfig()) {\n turbopackConfig = {\n turbopack: {\n ...nextConfig.turbopack, // Preserve existing turbopack options\n rules: mergedRules,\n resolveAlias: mergedResolveAlias,\n },\n };\n } else {\n turbopackConfig = {\n experimental: {\n ...nextConfig.experimental, // Preserve ALL experimental options\n // @ts-expect-error - turbo for Next.js <16\n turbo: {\n // @ts-expect-error - turbo for Next.js <16\n ...nextConfig.experimental?.turbo, // Preserve existing turbo options\n rules: mergedRules,\n resolveAlias: mergedResolveAlias,\n },\n },\n };\n }\n\n const runAfterProductionCompile = async ({\n distDir,\n projectDir,\n }: {\n distDir: string;\n projectDir: string;\n }) => {\n if (typeof nextConfig.compiler?.runAfterProductionCompile === \"function\") {\n await nextConfig.compiler.runAfterProductionCompile({\n distDir,\n projectDir,\n });\n }\n\n logger.info(\"Running post-build translation generation...\");\n\n try {\n await processBuildTranslations({\n config: lingoConfig,\n publicOutputPath: distDir,\n metadataFilePath,\n });\n } catch (error) {\n logger.error(\n \"Translation generation failed:\",\n error instanceof Error ? error.message : error,\n );\n throw error;\n }\n };\n\n const webpack: NextJsWebpackConfig = (\n config: any,\n options: WebpackConfigContext,\n ) => {\n // Apply user's webpack config first if it exists\n let modifiedConfig = config;\n if (typeof nextConfig.webpack === \"function\") {\n modifiedConfig = nextConfig.webpack(config, options);\n }\n\n const lingoRules = Object.values(\n loaders({ lingoConfig, metadataFilePath, translationServerUrl }),\n )\n .map((rules) => rules.webpack)\n .filter(Boolean);\n\n // I tried using plugin from unplugin, but with all the loaders stuff it works poorly.\n // Failing with weird errors which appear on the later compilations, probably something with the cache and virtual modules\n modifiedConfig.module.rules.unshift(...lingoRules);\n\n return modifiedConfig;\n };\n\n return {\n ...nextConfig,\n ...turbopackConfig,\n compiler: {\n ...nextConfig.compiler,\n runAfterProductionCompile,\n },\n webpack,\n };\n}\n"],"mappings":"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"}
|
|
@@ -234,9 +234,8 @@ const lingoUnplugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
234
234
|
require_logger.logger.debug(`No transformation needed for ${id}`);
|
|
235
235
|
return null;
|
|
236
236
|
}
|
|
237
|
-
const metadataManager = new require_manager.MetadataManager(getMetadataPath$1());
|
|
238
237
|
if (result.newEntries && result.newEntries.length > 0) {
|
|
239
|
-
await
|
|
238
|
+
await require_manager.saveMetadata(getMetadataPath$1(), result.newEntries, !isDev);
|
|
240
239
|
totalEntriesCount += result.newEntries.length;
|
|
241
240
|
filesTransformedCount++;
|
|
242
241
|
require_logger.logger.debug(`Found ${result.newEntries.length} translatable text(s) in ${id}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.mjs";
|
|
2
2
|
import { createLingoConfig } from "../utils/config-factory.mjs";
|
|
3
3
|
import { TranslationService } from "../translators/translation-service.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { cleanupExistingMetadata, getMetadataPath, saveMetadata } from "../metadata/manager.mjs";
|
|
5
5
|
import { startTranslationServer } from "../translation-server/translation-server.mjs";
|
|
6
6
|
import { processBuildTranslations } from "./build-translator.mjs";
|
|
7
7
|
import { registerCleanupOnCurrentProcess } from "./cleanup.mjs";
|
|
@@ -231,9 +231,8 @@ const lingoUnplugin = createUnplugin((options) => {
|
|
|
231
231
|
logger.debug(`No transformation needed for ${id}`);
|
|
232
232
|
return null;
|
|
233
233
|
}
|
|
234
|
-
const metadataManager = new MetadataManager(getMetadataPath$1());
|
|
235
234
|
if (result.newEntries && result.newEntries.length > 0) {
|
|
236
|
-
await
|
|
235
|
+
await saveMetadata(getMetadataPath$1(), result.newEntries, !isDev);
|
|
237
236
|
totalEntriesCount += result.newEntries.length;
|
|
238
237
|
filesTransformedCount++;
|
|
239
238
|
logger.debug(`Found ${result.newEntries.length} translatable text(s) in ${id}`);
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"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 saveMetadata,\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 // Update metadata with new entries (thread-safe)\n if (result.newEntries && result.newEntries.length > 0) {\n await saveMetadata(getMetadataPath(), result.newEntries, !isDev);\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:\n error instanceof Error\n ? error.message\n : \"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,12 +1,12 @@
|
|
|
1
1
|
import { LingoProviderProps } from "../shared/LingoProvider.mjs";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime2 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_runtime2.JSX.Element>;
|
|
10
10
|
//#endregion
|
|
11
11
|
export { LingoProvider };
|
|
12
12
|
//# sourceMappingURL=ServerLingoProvider.d.mts.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 { PropsWithChildren } from "react";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime1 from "react/jsx-runtime";
|
|
3
3
|
import { LocaleCode } from "lingo.dev/spec";
|
|
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_runtime1.JSX.Element;
|
|
74
74
|
//#endregion
|
|
75
75
|
export { LingoProvider, LingoProviderProps };
|
|
76
76
|
//# sourceMappingURL=LingoProvider.d.mts.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,5 +1,5 @@
|
|
|
1
1
|
import { CSSProperties } from "react";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react_jsx_runtime3 from "react/jsx-runtime";
|
|
3
3
|
import { LocaleCode } from "lingo.dev/spec";
|
|
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_runtime3.JSX.Element;
|
|
69
69
|
//#endregion
|
|
70
70
|
export { LocaleSwitcher };
|
|
71
71
|
//# sourceMappingURL=LocaleSwitcher.d.mts.map
|
|
@@ -214,10 +214,10 @@ var TranslationServer = class {
|
|
|
214
214
|
async reloadMetadata() {
|
|
215
215
|
try {
|
|
216
216
|
this.metadata = await require_manager.loadMetadata(require_manager.getMetadataPath(this.config));
|
|
217
|
-
this.logger.debug(`Reloaded metadata: ${Object.keys(this.metadata
|
|
217
|
+
this.logger.debug(`Reloaded metadata: ${Object.keys(this.metadata).length} entries`);
|
|
218
218
|
} catch (error) {
|
|
219
219
|
this.logger.warn("Failed to reload metadata:", error);
|
|
220
|
-
this.metadata =
|
|
220
|
+
this.metadata = {};
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
/**
|
|
@@ -232,7 +232,7 @@ var TranslationServer = class {
|
|
|
232
232
|
if (!this.translationService) throw new Error("Translation server not initialized");
|
|
233
233
|
await this.reloadMetadata();
|
|
234
234
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
235
|
-
const allHashes = Object.keys(this.metadata
|
|
235
|
+
const allHashes = Object.keys(this.metadata);
|
|
236
236
|
this.logger.info(`Translating all ${allHashes.length} entries to ${locale}`);
|
|
237
237
|
const startTime = Date.now();
|
|
238
238
|
this.broadcast(require_ws_events.createEvent("batch:start", {
|
|
@@ -241,6 +241,18 @@ var TranslationServer = class {
|
|
|
241
241
|
hashes: allHashes
|
|
242
242
|
}));
|
|
243
243
|
const result = await this.translationService.translate(locale, this.metadata, allHashes);
|
|
244
|
+
if (result.errors.length > 0) {
|
|
245
|
+
this.logger.error(`${result.errors.length} translation error(s) for ${locale}:`);
|
|
246
|
+
for (const err of result.errors) {
|
|
247
|
+
const prefix = ` [${locale}]`;
|
|
248
|
+
if (err.hash === "all") {
|
|
249
|
+
this.logger.error(`${prefix} ${err.error}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const source = err.sourceText.length > 200 ? err.sourceText.slice(0, 200) + "..." : err.sourceText;
|
|
253
|
+
this.logger.error(`${prefix} hash=${err.hash} source="${source}" error="${err.error}"`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
244
256
|
const duration = Date.now() - startTime;
|
|
245
257
|
this.broadcast(require_ws_events.createEvent("batch:complete", {
|
|
246
258
|
locale,
|
|
@@ -461,7 +473,7 @@ var TranslationServer = class {
|
|
|
461
473
|
await this.reloadMetadata();
|
|
462
474
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
463
475
|
this.logger.info(`🌐 Requesting full dictionary for ${locale}`);
|
|
464
|
-
const allHashes = Object.keys(this.metadata
|
|
476
|
+
const allHashes = Object.keys(this.metadata);
|
|
465
477
|
const result = await this.translationService.translate(parsedLocale, this.metadata, allHashes);
|
|
466
478
|
res.writeHead(200, {
|
|
467
479
|
"Content-Type": "application/json",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getLogger } from "./logger.mjs";
|
|
2
2
|
import { TranslationService } from "../translators/translation-service.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { getMetadataPath, loadMetadata } from "../metadata/manager.mjs";
|
|
4
4
|
import { createEvent } from "./ws-events.mjs";
|
|
5
5
|
import { parseLocaleOrThrow } from "../utils/is-valid-locale.mjs";
|
|
6
6
|
import http from "http";
|
|
@@ -211,10 +211,10 @@ var TranslationServer = class {
|
|
|
211
211
|
async reloadMetadata() {
|
|
212
212
|
try {
|
|
213
213
|
this.metadata = await loadMetadata(getMetadataPath(this.config));
|
|
214
|
-
this.logger.debug(`Reloaded metadata: ${Object.keys(this.metadata
|
|
214
|
+
this.logger.debug(`Reloaded metadata: ${Object.keys(this.metadata).length} entries`);
|
|
215
215
|
} catch (error) {
|
|
216
216
|
this.logger.warn("Failed to reload metadata:", error);
|
|
217
|
-
this.metadata =
|
|
217
|
+
this.metadata = {};
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
/**
|
|
@@ -229,7 +229,7 @@ var TranslationServer = class {
|
|
|
229
229
|
if (!this.translationService) throw new Error("Translation server not initialized");
|
|
230
230
|
await this.reloadMetadata();
|
|
231
231
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
232
|
-
const allHashes = Object.keys(this.metadata
|
|
232
|
+
const allHashes = Object.keys(this.metadata);
|
|
233
233
|
this.logger.info(`Translating all ${allHashes.length} entries to ${locale}`);
|
|
234
234
|
const startTime = Date.now();
|
|
235
235
|
this.broadcast(createEvent("batch:start", {
|
|
@@ -238,6 +238,18 @@ var TranslationServer = class {
|
|
|
238
238
|
hashes: allHashes
|
|
239
239
|
}));
|
|
240
240
|
const result = await this.translationService.translate(locale, this.metadata, allHashes);
|
|
241
|
+
if (result.errors.length > 0) {
|
|
242
|
+
this.logger.error(`${result.errors.length} translation error(s) for ${locale}:`);
|
|
243
|
+
for (const err of result.errors) {
|
|
244
|
+
const prefix = ` [${locale}]`;
|
|
245
|
+
if (err.hash === "all") {
|
|
246
|
+
this.logger.error(`${prefix} ${err.error}`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const source = err.sourceText.length > 200 ? err.sourceText.slice(0, 200) + "..." : err.sourceText;
|
|
250
|
+
this.logger.error(`${prefix} hash=${err.hash} source="${source}" error="${err.error}"`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
241
253
|
const duration = Date.now() - startTime;
|
|
242
254
|
this.broadcast(createEvent("batch:complete", {
|
|
243
255
|
locale,
|
|
@@ -458,7 +470,7 @@ var TranslationServer = class {
|
|
|
458
470
|
await this.reloadMetadata();
|
|
459
471
|
if (!this.metadata) throw new Error("Failed to load metadata");
|
|
460
472
|
this.logger.info(`🌐 Requesting full dictionary for ${locale}`);
|
|
461
|
-
const allHashes = Object.keys(this.metadata
|
|
473
|
+
const allHashes = Object.keys(this.metadata);
|
|
462
474
|
const result = await this.translationService.translate(parsedLocale, this.metadata, allHashes);
|
|
463
475
|
res.writeHead(200, {
|
|
464
476
|
"Content-Type": "application/json",
|