@lingo.dev/compiler 0.1.13 → 0.3.0

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.
@@ -59,7 +59,7 @@ async function processBuildTranslations(options) {
59
59
  },
60
60
  config
61
61
  });
62
- const needsSourceLocale = config.pluralization?.enabled !== false;
62
+ const needsSourceLocale = config.pluralization?.enabled === true;
63
63
  const allLocales = needsSourceLocale ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
64
64
  require_logger.logger.info(`Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? " (including source locale for pluralization)" : ""}...`);
65
65
  const stats = {};
@@ -110,7 +110,7 @@ async function validateCache(config, metadata, cache) {
110
110
  const allHashes = Object.keys(metadata.entries);
111
111
  const missingLocales = [];
112
112
  const incompleteLocales = [];
113
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
113
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
114
114
  for (const locale of allLocales) try {
115
115
  const entries = await cache.get(locale);
116
116
  if (Object.keys(entries).length === 0) {
@@ -140,7 +140,7 @@ async function validateCache(config, metadata, cache) {
140
140
  function buildCacheStats(config, metadata) {
141
141
  const totalEntries = Object.keys(metadata.entries).length;
142
142
  const stats = {};
143
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
143
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
144
144
  for (const locale of allLocales) stats[locale] = {
145
145
  total: totalEntries,
146
146
  translated: totalEntries,
@@ -153,7 +153,7 @@ async function copyStaticFiles(config, publicOutputPath, metadata, cache) {
153
153
  await fs_promises.default.mkdir(publicOutputPath, { recursive: true });
154
154
  const usedHashes = new Set(Object.keys(metadata.entries));
155
155
  require_logger.logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);
156
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
156
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
157
157
  for (const locale of allLocales) {
158
158
  const publicFilePath = path.default.join(publicOutputPath, `${locale}.json`);
159
159
  try {
@@ -56,7 +56,7 @@ async function processBuildTranslations(options) {
56
56
  },
57
57
  config
58
58
  });
59
- const needsSourceLocale = config.pluralization?.enabled !== false;
59
+ const needsSourceLocale = config.pluralization?.enabled === true;
60
60
  const allLocales = needsSourceLocale ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
61
61
  logger.info(`Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? " (including source locale for pluralization)" : ""}...`);
62
62
  const stats = {};
@@ -107,7 +107,7 @@ async function validateCache(config, metadata, cache) {
107
107
  const allHashes = Object.keys(metadata.entries);
108
108
  const missingLocales = [];
109
109
  const incompleteLocales = [];
110
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
110
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
111
111
  for (const locale of allLocales) try {
112
112
  const entries = await cache.get(locale);
113
113
  if (Object.keys(entries).length === 0) {
@@ -137,7 +137,7 @@ async function validateCache(config, metadata, cache) {
137
137
  function buildCacheStats(config, metadata) {
138
138
  const totalEntries = Object.keys(metadata.entries).length;
139
139
  const stats = {};
140
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
140
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
141
141
  for (const locale of allLocales) stats[locale] = {
142
142
  total: totalEntries,
143
143
  translated: totalEntries,
@@ -150,7 +150,7 @@ async function copyStaticFiles(config, publicOutputPath, metadata, cache) {
150
150
  await fsPromises.mkdir(publicOutputPath, { recursive: true });
151
151
  const usedHashes = new Set(Object.keys(metadata.entries));
152
152
  logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);
153
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
153
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
154
154
  for (const locale of allLocales) {
155
155
  const publicFilePath = path.join(publicOutputPath, `${locale}.json`);
156
156
  try {
@@ -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 }>","fs"],"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 !== false;\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 !== false;\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 !== false;\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 !== false;\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 }>","fs"],"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":";;;;;;;;;;;;;;;;;;;;;;;AAsDA,eAAsB,yBACpB,SACiC;CACjC,MAAM,EAAE,QAAQ,kBAAkB,qBAAqB;CAGvD,MAAM,YACH,QAAQ,IAAI,oBACb,OAAO;AAET,QAAO,KAAK,kBAAkB,YAAY;CAE1C,MAAM,WAAW,MAAM,aAAa,iBAAiB;AAErD,KAAI,CAAC,YAAY,OAAO,KAAK,SAAS,QAAQ,CAAC,WAAW,GAAG;AAC3D,SAAO,KAAK,iDAAiD;AAC7D,SAAO;GACL,SAAS;GACT,OAAO,EAAE;GACV;;CAGH,MAAM,eAAe,OAAO,KAAK,SAAS,QAAQ,CAAC;AACnD,QAAO,KAAK,YAAY,aAAa,uBAAuB;CAE5D,MAAM,QAAQ,YAAY,OAAO;AAGjC,KAAI,cAAc,cAAc;AAC9B,SAAO,KAAK,qCAAqC;AACjD,QAAM,cAAc,QAAQ,UAAU,MAAM;AAC5C,SAAO,KAAK,4BAA4B;AAExC,MAAI,iBACF,OAAM,gBAAgB,QAAQ,kBAAkB,UAAU,MAAM;AAGlE,SAAO;GACL,SAAS;GACT,OAAO,gBAAgB,QAAQ,SAAS;GACzC;;AAIH,QAAO,KAAK,gCAAgC;CAC5C,IAAIA;AAEJ,KAAI;AACF,sBAAoB,MAAM,uBAAuB;GAC/C,oBAAoB,IAAI,mBAAmB,QAAQ,OAAO;GAC1D,UAAU,QAAQ;AAChB,WAAO,MAAM,6BAA6B,IAAI;;GAEhD;GACD,CAAC;EAIF,MAAM,oBAAoB,OAAO,eAAe,YAAY;EAC5D,MAAM,aAAa,oBACf,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,SAAO,KACL,+BAA+B,WAAW,OAAO,YAAY,oBAAoB,iDAAiD,GAAG,KACtI;EAED,MAAMC,QAAyC,EAAE;EACjD,MAAMC,SAAuD,EAAE;EAG/D,MAAM,iBAAiB,WAAW,IAAI,OAAO,WAAW;AACtD,UAAO,KAAK,kBAAkB,OAAO,KAAK;GAE1C,MAAM,SAAS,MAAM,kBAAmB,aAAa,OAAO;AAE5D,SAAM,UAAU;IACd,OAAO;IACP,YAAY,OAAO,KAAK,OAAO,aAAa,CAAC;IAC7C,QAAQ,OAAO,OAAO;IACvB;AAED,OAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,WAAO,KACL,OAAO,OAAO,OAAO,OAAO,4BAA4B,SACzD;AACD,WAAO,KAAK;KACV;KACA,OAAO,GAAG,OAAO,OAAO,OAAO;KAChC,CAAC;SAEF,QAAO,KAAK,KAAK,OAAO,yBAAyB;IAEnD;AAEF,QAAM,QAAQ,IAAI,eAAe;AAGjC,MAAI,OAAO,SAAS,GAAG;GACrB,MAAM,WAAW,wBAAwB,OAAO;AAChD,UAAO,MAAM,SAAS;AACtB,WAAQ,KAAK,EAAE;;AAIjB,MAAI,iBACF,OAAM,gBAAgB,QAAQ,kBAAkB,UAAU,MAAM;AAGlE,SAAO,KAAK,kDAAkD;AAE9D,SAAO;GACL,SAAS;GACT;GACD;UACM,OAAO;AACd,SAAO,MACL,sCACA,iBAAiB,QAAQ,MAAM,UAAU,MAC1C;AACD,UAAQ,KAAK,EAAE;WACP;AACR,MAAI,mBAAmB;AACrB,SAAM,kBAAkB,MAAM;AAC9B,UAAO,KAAK,+BAA+B;;;;;;;;AASjD,eAAe,cACb,QACA,UACA,OACe;CACf,MAAM,YAAY,OAAO,KAAK,SAAS,QAAQ;CAC/C,MAAMC,iBAA2B,EAAE;CACnC,MAAMC,oBAID,EAAE;CAIP,MAAM,aADoB,OAAO,eAAe,YAAY,OAExD,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,MAAK,MAAM,UAAU,WACnB,KAAI;EACF,MAAM,UAAU,MAAM,MAAM,IAAI,OAAO;AAEvC,MAAI,OAAO,KAAK,QAAQ,CAAC,WAAW,GAAG;AACrC,kBAAe,KAAK,OAAO;AAC3B,UAAO,MAAM,qCAAqC,SAAS;AAC3D;;EAGF,MAAM,gBAAgB,UAAU,QAAQ,SAAS,CAAC,QAAQ,MAAM;AAEhE,MAAI,cAAc,SAAS,GAAG;AAC5B,qBAAkB,KAAK;IACrB;IACA,SAAS,cAAc;IACvB,OAAO,UAAU;IAClB,CAAC;AAGF,UAAO,MACL,qBAAqB,OAAO,IAAI,cAAc,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,GAClE,cAAc,SAAS,IAAI,QAAQ,KAEtC;;UAEI,OAAO;AACd,iBAAe,KAAK,OAAO;AAC3B,SAAO,MAAM,4BAA4B,OAAO,IAAI,MAAM;;AAI9D,KAAI,eAAe,SAAS,KAAK,kBAAkB,SAAS,GAAG;EAC7D,MAAM,WAAW,2BACf,gBACA,kBACD;AACD,SAAO,MAAM,SAAS;AACtB,UAAQ,KAAK,EAAE;;;AAInB,SAAS,gBACP,QACA,UACiC;CACjC,MAAM,eAAe,OAAO,KAAK,SAAS,QAAQ,CAAC;CACnD,MAAMH,QAAyC,EAAE;CAIjD,MAAM,aADoB,OAAO,eAAe,YAAY,OAExD,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,MAAK,MAAM,UAAU,WACnB,OAAM,UAAU;EACd,OAAO;EACP,YAAY;EACZ,QAAQ;EACT;AAGH,QAAO;;AAGT,eAAe,gBACb,QACA,kBACA,UACA,OACe;AACf,QAAO,KAAK,6CAA6C,mBAAmB;AAE5E,OAAMI,WAAG,MAAM,kBAAkB,EAAE,WAAW,MAAM,CAAC;CAErD,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,SAAS,QAAQ,CAAC;AACzD,QAAO,KAAK,gCAAgC,WAAW,KAAK,gBAAgB;CAI5E,MAAM,aADoB,OAAO,eAAe,YAAY,OAExD,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,MAAK,MAAM,UAAU,YAAY;EAC/B,MAAM,iBAAiB,KAAK,KAAK,kBAAkB,GAAG,OAAO,OAAO;AAEpE,MAAI;GACF,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,MAAM,KAAK,WAAW,CAAC;GAC/D,MAAM,aAAa,eAAe,QAAQ,QAAQ;AAElD,SAAMA,WAAG,UACP,gBACA,KAAK,UAAU,YAAY,MAAM,EAAE,EACnC,QACD;AAED,UAAO,KACL,eAAe,OAAO,SAAS,OAAO,KAAK,QAAQ,CAAC,OAAO,gBAC5D;WACM,OAAO;AACd,UAAO,MAAM,wBAAwB,OAAO,SAAS,MAAM;AAC3D,WAAQ,KAAK,EAAE;;;;AAKrB,SAAS,2BACP,gBACA,mBAKQ;CACR,IAAI,MAAM;AAEV,KAAI,eAAe,SAAS,GAAG;AAC7B,SAAO;AACP,SAAO,eAAe,KAAK,WAAW,SAAS,OAAO,OAAO,CAAC,KAAK,KAAK;AACxE,SAAO;;AAGT,KAAI,kBAAkB,SAAS,GAAG;AAChC,SAAO;AACP,SAAO,kBACJ,KACE,SACC,SAAS,KAAK,OAAO,IAAI,KAAK,QAAQ,GAAG,KAAK,MAAM,uBACvD,CACA,KAAK,KAAK;AACb,SAAO;;AAGT,QAAO;AACP,QAAO;AACP,QAAO;AACP,QAAO;AAEP,QAAO;;AAGT,SAAS,wBACP,QACQ;CACR,IAAI,MAAM;AAEV,QAAO,OAAO,KAAK,QAAQ,OAAO,IAAI,OAAO,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK;AAExE,QAAO;AACP,QAAO;AACP,QAAO;AAEP,QAAO"}
@@ -1,12 +1,12 @@
1
1
  import { LingoProviderProps } from "../shared/LingoProvider.cjs";
2
- import * as react_jsx_runtime1 from "react/jsx-runtime";
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<react_jsx_runtime1.JSX.Element>;
9
+ }: LingoProviderProps): Promise<react_jsx_runtime2.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 react_jsx_runtime1 from "react/jsx-runtime";
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<react_jsx_runtime1.JSX.Element>;
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 react_jsx_runtime3 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime1 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): react_jsx_runtime3.JSX.Element;
73
+ }: LingoProviderProps): react_jsx_runtime1.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 react_jsx_runtime2 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime3 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): react_jsx_runtime2.JSX.Element;
68
+ }: LocaleSwitcherProps): react_jsx_runtime3.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 react_jsx_runtime2 from "react/jsx-runtime";
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/LocaleSwitcher.d.ts
@@ -65,7 +65,7 @@ declare function LocaleSwitcher({
65
65
  style,
66
66
  className,
67
67
  showLoadingState
68
- }: LocaleSwitcherProps): react_jsx_runtime2.JSX.Element;
68
+ }: LocaleSwitcherProps): react_jsx_runtime1.JSX.Element;
69
69
  //#endregion
70
70
  export { LocaleSwitcher };
71
71
  //# sourceMappingURL=LocaleSwitcher.d.mts.map
@@ -7,6 +7,7 @@ let _ai_sdk_google = require("@ai-sdk/google");
7
7
  let _openrouter_ai_sdk_provider = require("@openrouter/ai-sdk-provider");
8
8
  let ai_sdk_ollama = require("ai-sdk-ollama");
9
9
  let _ai_sdk_mistral = require("@ai-sdk/mistral");
10
+ let _ai_sdk_openai = require("@ai-sdk/openai");
10
11
  let dotenv = require("dotenv");
11
12
  dotenv = require_rolldown_runtime.__toESM(dotenv);
12
13
 
@@ -105,7 +106,10 @@ function getLocaleModel(localeModels, sourceLocale, targetLocale) {
105
106
  * @throws Error if format is invalid
106
107
  */
107
108
  function parseModelString(modelString) {
108
- const [provider, name] = modelString.split(":", 2);
109
+ const colonIndex = modelString.indexOf(":");
110
+ if (colonIndex === -1) return;
111
+ const provider = modelString.substring(0, colonIndex);
112
+ const name = modelString.substring(colonIndex + 1);
109
113
  if (!provider || !name) return;
110
114
  return {
111
115
  provider,
@@ -156,10 +160,17 @@ function createAiModel(model, validatedKeys) {
156
160
  const providerConfig = providerDetails[model.provider];
157
161
  if (!providerConfig) throw new Error(`⚠️ Provider "${model.provider}" is not supported. Supported providers: ${Object.keys(providerDetails).join(", ")}`);
158
162
  const apiKey = providerConfig.apiKeyEnvVar ? validatedKeys[model.provider] : void 0;
159
- if (providerConfig.apiKeyEnvVar && !apiKey) throw new Error(`⚠️ ${providerConfig.name} API key not found. Please set ${providerConfig.apiKeyEnvVar} environment variable.\n\nThis should not happen if validateAndFetchApiKeys() was called. Please restart the service.`);
163
+ if (providerConfig.apiKeyEnvVar && !apiKey) throw new Error(`⚠️ ${providerConfig.name} API key not found. Please set ${providerConfig.apiKeyEnvVar} environment variable.\n\nThis should not happen if validateAndGetApiKeys() was called. Please restart the service.`);
160
164
  switch (model.provider) {
161
165
  case "groq": return (0, _ai_sdk_groq.createGroq)({ apiKey })(model.name);
162
166
  case "google": return (0, _ai_sdk_google.createGoogleGenerativeAI)({ apiKey })(model.name);
167
+ case "openai": {
168
+ const baseURL = getKeyFromEnv("OPENAI_BASE_URL");
169
+ return (0, _ai_sdk_openai.createOpenAI)({
170
+ apiKey,
171
+ ...baseURL && { baseURL }
172
+ }).chat(model.name);
173
+ }
163
174
  case "openrouter": return (0, _openrouter_ai_sdk_provider.createOpenRouter)({ apiKey })(model.name);
164
175
  case "ollama": return (0, ai_sdk_ollama.ollama)(model.name);
165
176
  case "mistral": return (0, _ai_sdk_mistral.createMistral)({ apiKey })(model.name);
@@ -5,6 +5,7 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
5
5
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
6
6
  import { ollama } from "ai-sdk-ollama";
7
7
  import { createMistral } from "@ai-sdk/mistral";
8
+ import { createOpenAI } from "@ai-sdk/openai";
8
9
  import * as dotenv from "dotenv";
9
10
 
10
11
  //#region src/translators/lingo/model-factory.ts
@@ -102,7 +103,10 @@ function getLocaleModel(localeModels, sourceLocale, targetLocale) {
102
103
  * @throws Error if format is invalid
103
104
  */
104
105
  function parseModelString(modelString) {
105
- const [provider, name] = modelString.split(":", 2);
106
+ const colonIndex = modelString.indexOf(":");
107
+ if (colonIndex === -1) return;
108
+ const provider = modelString.substring(0, colonIndex);
109
+ const name = modelString.substring(colonIndex + 1);
106
110
  if (!provider || !name) return;
107
111
  return {
108
112
  provider,
@@ -153,10 +157,17 @@ function createAiModel(model, validatedKeys) {
153
157
  const providerConfig = providerDetails[model.provider];
154
158
  if (!providerConfig) throw new Error(`⚠️ Provider "${model.provider}" is not supported. Supported providers: ${Object.keys(providerDetails).join(", ")}`);
155
159
  const apiKey = providerConfig.apiKeyEnvVar ? validatedKeys[model.provider] : void 0;
156
- if (providerConfig.apiKeyEnvVar && !apiKey) throw new Error(`⚠️ ${providerConfig.name} API key not found. Please set ${providerConfig.apiKeyEnvVar} environment variable.\n\nThis should not happen if validateAndFetchApiKeys() was called. Please restart the service.`);
160
+ if (providerConfig.apiKeyEnvVar && !apiKey) throw new Error(`⚠️ ${providerConfig.name} API key not found. Please set ${providerConfig.apiKeyEnvVar} environment variable.\n\nThis should not happen if validateAndGetApiKeys() was called. Please restart the service.`);
157
161
  switch (model.provider) {
158
162
  case "groq": return createGroq({ apiKey })(model.name);
159
163
  case "google": return createGoogleGenerativeAI({ apiKey })(model.name);
164
+ case "openai": {
165
+ const baseURL = getKeyFromEnv("OPENAI_BASE_URL");
166
+ return createOpenAI({
167
+ apiKey,
168
+ ...baseURL && { baseURL }
169
+ }).chat(model.name);
170
+ }
160
171
  case "openrouter": return createOpenRouter({ apiKey })(model.name);
161
172
  case "ollama": return ollama(model.name);
162
173
  case "mistral": return createMistral({ apiKey })(model.name);
@@ -1 +1 @@
1
- {"version":3,"file":"model-factory.mjs","names":["path","providerDetails: Record<string, ProviderConfig>","keys: ValidatedApiKeys","missingProviders: string[]","providersToValidate: string[]"],"sources":["../../../src/translators/lingo/model-factory.ts"],"sourcesContent":["/**\n * Shared utilities for creating AI model instances\n */\n\nimport { createGroq } from \"@ai-sdk/groq\";\nimport { createGoogleGenerativeAI } from \"@ai-sdk/google\";\nimport { createOpenRouter } from \"@openrouter/ai-sdk-provider\";\nimport { ollama } from \"ai-sdk-ollama\";\nimport { createMistral } from \"@ai-sdk/mistral\";\nimport type { LanguageModel } from \"ai\";\nimport * as dotenv from \"dotenv\";\nimport * as path from \"path\";\nimport { formatNoApiKeysError } from \"./provider-details\";\n\nexport type LocaleModel = {\n provider: string;\n name: string;\n};\n\nexport function getKeyFromEnv(envVarName: string): string | undefined {\n if (process.env[envVarName]) {\n return process.env[envVarName];\n }\n\n const projectRoot = process.cwd();\n\n const result = dotenv.config({\n path: [\n path.resolve(projectRoot, \".env\"),\n path.resolve(projectRoot, \".env.local\"),\n path.resolve(projectRoot, \".env.development\"),\n ],\n });\n\n return result?.parsed?.[envVarName];\n}\n\n/**\n * Pre-validated API keys for all providers\n * Keys are fetched and validated once at initialization\n */\nexport type ValidatedApiKeys = Record<string, string>;\n\n/**\n * Provider configuration including env var names and requirements\n */\ntype ProviderConfig = {\n name: string; // Display name (e.g., \"Groq\", \"Google\")\n apiKeyEnvVar?: string; // Environment variable name (e.g., \"GROQ_API_KEY\")\n apiKeyConfigKey?: string; // Config key if applicable (e.g., \"llm.groqApiKey\")\n getKeyLink: string; // Link to get API key\n docsLink: string; // Link to API docs for troubleshooting\n};\n\nexport const providerDetails: Record<string, ProviderConfig> = {\n groq: {\n name: \"Groq\",\n apiKeyEnvVar: \"GROQ_API_KEY\",\n apiKeyConfigKey: \"llm.groqApiKey\",\n getKeyLink: \"https://groq.com\",\n docsLink: \"https://console.groq.com/docs/errors\",\n },\n google: {\n name: \"Google\",\n apiKeyEnvVar: \"GOOGLE_API_KEY\",\n apiKeyConfigKey: \"llm.googleApiKey\",\n getKeyLink: \"https://ai.google.dev/\",\n docsLink: \"https://ai.google.dev/gemini-api/docs/troubleshooting\",\n },\n openai: {\n name: \"OpenAI\",\n apiKeyEnvVar: \"OPENAI_API_KEY\",\n apiKeyConfigKey: \"llm.openaiApiKey\",\n getKeyLink: \"https://platform.openai.com/account/api-keys\",\n docsLink: \"https://platform.openai.com/docs\",\n },\n anthropic: {\n name: \"Anthropic\",\n apiKeyEnvVar: \"ANTHROPIC_API_KEY\",\n apiKeyConfigKey: \"llm.anthropicApiKey\",\n getKeyLink: \"https://console.anthropic.com/get-api-key\",\n docsLink: \"https://console.anthropic.com/docs\",\n },\n openrouter: {\n name: \"OpenRouter\",\n apiKeyEnvVar: \"OPENROUTER_API_KEY\",\n apiKeyConfigKey: \"llm.openrouterApiKey\",\n getKeyLink: \"https://openrouter.ai\",\n docsLink: \"https://openrouter.ai/docs\",\n },\n ollama: {\n name: \"Ollama\",\n apiKeyEnvVar: undefined, // Ollama doesn't require an API key\n apiKeyConfigKey: undefined, // Ollama doesn't require an API key\n getKeyLink: \"https://ollama.com/download\",\n docsLink: \"https://github.com/ollama/ollama/tree/main/docs\",\n },\n mistral: {\n name: \"Mistral\",\n apiKeyEnvVar: \"MISTRAL_API_KEY\",\n apiKeyConfigKey: \"llm.mistralApiKey\",\n getKeyLink: \"https://console.mistral.ai\",\n docsLink: \"https://docs.mistral.ai\",\n },\n \"lingo.dev\": {\n name: \"Lingo.dev\",\n apiKeyEnvVar: \"LINGODOTDEV_API_KEY\",\n apiKeyConfigKey: \"auth.apiKey\",\n getKeyLink: \"https://lingo.dev\",\n docsLink: \"https://lingo.dev/docs\",\n },\n};\n\n/**\n * Get provider and model for a specific locale pair\n */\nexport function getLocaleModel(\n localeModels: Record<string, string>,\n sourceLocale: string,\n targetLocale: string,\n): LocaleModel | undefined {\n const localeKeys = [\n `${sourceLocale}:${targetLocale}`,\n `*:${targetLocale}`,\n `${sourceLocale}:*`,\n \"*:*\",\n ];\n\n const modelKey = localeKeys.find((key) => key in localeModels);\n if (!modelKey) {\n return undefined;\n }\n\n const value = localeModels[modelKey];\n if (!value) {\n return undefined;\n }\n\n return parseModelString(value);\n}\n\n/**\n * Parse provider and model from model string\n * Format: \"provider:model\" (e.g., \"groq:llama3-8b-8192\")\n *\n * @param modelString Model string to parse\n * @returns Object with provider and model\n * @throws Error if format is invalid\n */\nexport function parseModelString(modelString: string): LocaleModel | undefined {\n // Split on first colon only\n const [provider, name] = modelString.split(\":\", 2);\n\n if (!provider || !name) {\n return undefined;\n }\n\n return { provider, name };\n}\n\n/**\n * Validate and fetch all necessary API keys for the given configuration\n * This should be called once at initialization time\n *\n * @param config Model configuration (\"lingo.dev\" or locale-pair mapping)\n * @returns Validated API keys (provider ID -> API key)\n * @throws Error if required keys are missing\n */\nexport function validateAndGetApiKeys(\n config: \"lingo.dev\" | Record<string, string>,\n): ValidatedApiKeys {\n const keys: ValidatedApiKeys = {};\n const missingProviders: string[] = [];\n\n // Determine which providers are configured\n let providersToValidate: string[];\n\n if (config === \"lingo.dev\") {\n // Only need lingo.dev provider\n providersToValidate = [\"lingo.dev\"];\n } else {\n // Extract unique providers from model strings\n const providerSet = new Set<string>();\n Object.values(config).forEach((modelString) => {\n const model = parseModelString(modelString);\n if (model) {\n providerSet.add(model.provider);\n }\n });\n providersToValidate = Array.from(providerSet);\n }\n\n // Validate and fetch keys for each provider\n for (const provider of providersToValidate) {\n const providerConfig = providerDetails[provider];\n\n if (!providerConfig) {\n throw new Error(\n `⚠️ Unknown provider \"${provider}\". Supported providers: ${Object.keys(providerDetails).join(\", \")}`,\n );\n }\n\n // Skip providers that don't require keys (like Ollama)\n if (!providerConfig.apiKeyEnvVar) {\n continue;\n }\n\n const key = getKeyFromEnv(providerConfig.apiKeyEnvVar);\n if (key) {\n keys[provider] = key;\n } else {\n missingProviders.push(provider);\n }\n }\n\n // If any keys are missing, throw with detailed error\n if (missingProviders.length > 0) {\n throw new Error(formatNoApiKeysError(missingProviders));\n }\n\n return keys;\n}\n\n/**\n * Create AI model instance from provider and model ID\n *\n * @param model Provider name (groq, google, openrouter, ollama, mistral) and model identifier\n * @param validatedKeys Pre-validated API keys from validateAndFetchApiKeys()\n * @returns LanguageModel instance\n * @throws Error if provider is not supported or API key is missing\n */\nexport function createAiModel(\n model: LocaleModel,\n validatedKeys: ValidatedApiKeys,\n): LanguageModel {\n const providerConfig = providerDetails[model.provider];\n\n if (!providerConfig) {\n throw new Error(\n `⚠️ Provider \"${model.provider}\" is not supported. Supported providers: ${Object.keys(providerDetails).join(\", \")}`,\n );\n }\n\n // Get API key if required\n const apiKey = providerConfig.apiKeyEnvVar\n ? validatedKeys[model.provider]\n : undefined;\n\n // TODO (AleksandrSl 25/12/2025): Do we really need to make a second check? Maybe creation should be combined with validation.\n // Verify key is present for providers that require it\n if (providerConfig.apiKeyEnvVar && !apiKey) {\n throw new Error(\n `⚠️ ${providerConfig.name} API key not found. Please set ${providerConfig.apiKeyEnvVar} environment variable.\\n\\n` +\n `This should not happen if validateAndFetchApiKeys() was called. Please restart the service.`,\n );\n }\n\n // Create the appropriate model instance\n switch (model.provider) {\n case \"groq\":\n return createGroq({ apiKey: apiKey! })(model.name);\n\n case \"google\":\n return createGoogleGenerativeAI({ apiKey: apiKey! })(model.name);\n\n case \"openrouter\":\n return createOpenRouter({ apiKey: apiKey! })(model.name);\n\n case \"ollama\":\n return ollama(model.name);\n\n case \"mistral\":\n return createMistral({ apiKey: apiKey! })(model.name);\n\n default:\n // This should be unreachable due to check above\n throw new Error(`⚠️ Provider \"${model.provider}\" is not implemented`);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model-factory.mjs","names":["path","providerDetails: Record<string, ProviderConfig>","keys: ValidatedApiKeys","missingProviders: string[]","providersToValidate: string[]"],"sources":["../../../src/translators/lingo/model-factory.ts"],"sourcesContent":["/**\n * Shared utilities for creating AI model instances\n */\n\nimport { createGroq } from \"@ai-sdk/groq\";\nimport { createGoogleGenerativeAI } from \"@ai-sdk/google\";\nimport { createOpenRouter } from \"@openrouter/ai-sdk-provider\";\nimport { ollama } from \"ai-sdk-ollama\";\nimport { createMistral } from \"@ai-sdk/mistral\";\nimport { createOpenAI } from \"@ai-sdk/openai\";\nimport type { LanguageModel } from \"ai\";\nimport * as dotenv from \"dotenv\";\nimport * as path from \"path\";\nimport { formatNoApiKeysError } from \"./provider-details\";\n\nexport type LocaleModel = {\n provider: string;\n name: string;\n};\n\nexport function getKeyFromEnv(envVarName: string): string | undefined {\n if (process.env[envVarName]) {\n return process.env[envVarName];\n }\n\n const projectRoot = process.cwd();\n\n const result = dotenv.config({\n path: [\n path.resolve(projectRoot, \".env\"),\n path.resolve(projectRoot, \".env.local\"),\n path.resolve(projectRoot, \".env.development\"),\n ],\n });\n\n return result?.parsed?.[envVarName];\n}\n\n/**\n * Pre-validated API keys for all providers\n * Keys are fetched and validated once at initialization\n */\nexport type ValidatedApiKeys = Record<string, string>;\n\n/**\n * Provider configuration including env var names and requirements\n */\ntype ProviderConfig = {\n name: string; // Display name (e.g., \"Groq\", \"Google\")\n apiKeyEnvVar?: string; // Environment variable name (e.g., \"GROQ_API_KEY\")\n apiKeyConfigKey?: string; // Config key if applicable (e.g., \"llm.groqApiKey\")\n getKeyLink: string; // Link to get API key\n docsLink: string; // Link to API docs for troubleshooting\n};\n\nexport const providerDetails: Record<string, ProviderConfig> = {\n groq: {\n name: \"Groq\",\n apiKeyEnvVar: \"GROQ_API_KEY\",\n apiKeyConfigKey: \"llm.groqApiKey\",\n getKeyLink: \"https://groq.com\",\n docsLink: \"https://console.groq.com/docs/errors\",\n },\n google: {\n name: \"Google\",\n apiKeyEnvVar: \"GOOGLE_API_KEY\",\n apiKeyConfigKey: \"llm.googleApiKey\",\n getKeyLink: \"https://ai.google.dev/\",\n docsLink: \"https://ai.google.dev/gemini-api/docs/troubleshooting\",\n },\n openai: {\n name: \"OpenAI\",\n apiKeyEnvVar: \"OPENAI_API_KEY\",\n apiKeyConfigKey: \"llm.openaiApiKey\",\n getKeyLink: \"https://platform.openai.com/account/api-keys\",\n docsLink: \"https://platform.openai.com/docs\",\n },\n anthropic: {\n name: \"Anthropic\",\n apiKeyEnvVar: \"ANTHROPIC_API_KEY\",\n apiKeyConfigKey: \"llm.anthropicApiKey\",\n getKeyLink: \"https://console.anthropic.com/get-api-key\",\n docsLink: \"https://console.anthropic.com/docs\",\n },\n openrouter: {\n name: \"OpenRouter\",\n apiKeyEnvVar: \"OPENROUTER_API_KEY\",\n apiKeyConfigKey: \"llm.openrouterApiKey\",\n getKeyLink: \"https://openrouter.ai\",\n docsLink: \"https://openrouter.ai/docs\",\n },\n ollama: {\n name: \"Ollama\",\n apiKeyEnvVar: undefined, // Ollama doesn't require an API key\n apiKeyConfigKey: undefined, // Ollama doesn't require an API key\n getKeyLink: \"https://ollama.com/download\",\n docsLink: \"https://github.com/ollama/ollama/tree/main/docs\",\n },\n mistral: {\n name: \"Mistral\",\n apiKeyEnvVar: \"MISTRAL_API_KEY\",\n apiKeyConfigKey: \"llm.mistralApiKey\",\n getKeyLink: \"https://console.mistral.ai\",\n docsLink: \"https://docs.mistral.ai\",\n },\n \"lingo.dev\": {\n name: \"Lingo.dev\",\n apiKeyEnvVar: \"LINGODOTDEV_API_KEY\",\n apiKeyConfigKey: \"auth.apiKey\",\n getKeyLink: \"https://lingo.dev\",\n docsLink: \"https://lingo.dev/docs\",\n },\n};\n\n/**\n * Get provider and model for a specific locale pair\n */\nexport function getLocaleModel(\n localeModels: Record<string, string>,\n sourceLocale: string,\n targetLocale: string,\n): LocaleModel | undefined {\n const localeKeys = [\n `${sourceLocale}:${targetLocale}`,\n `*:${targetLocale}`,\n `${sourceLocale}:*`,\n \"*:*\",\n ];\n\n const modelKey = localeKeys.find((key) => key in localeModels);\n if (!modelKey) {\n return undefined;\n }\n\n const value = localeModels[modelKey];\n if (!value) {\n return undefined;\n }\n\n return parseModelString(value);\n}\n\n/**\n * Parse provider and model from model string\n * Format: \"provider:model\" (e.g., \"groq:llama3-8b-8192\")\n *\n * @param modelString Model string to parse\n * @returns Object with provider and model\n * @throws Error if format is invalid\n */\nexport function parseModelString(modelString: string): LocaleModel | undefined {\n // Split on first colon only to allow colons in model names\n const colonIndex = modelString.indexOf(\":\");\n if (colonIndex === -1) {\n return undefined;\n }\n\n const provider = modelString.substring(0, colonIndex);\n const name = modelString.substring(colonIndex + 1);\n\n if (!provider || !name) {\n return undefined;\n }\n\n return { provider, name };\n}\n\n/**\n * Validate and fetch all necessary API keys for the given configuration\n * This should be called once at initialization time\n *\n * @param config Model configuration (\"lingo.dev\" or locale-pair mapping)\n * @returns Validated API keys (provider ID -> API key)\n * @throws Error if required keys are missing\n */\nexport function validateAndGetApiKeys(\n config: \"lingo.dev\" | Record<string, string>,\n): ValidatedApiKeys {\n const keys: ValidatedApiKeys = {};\n const missingProviders: string[] = [];\n\n // Determine which providers are configured\n let providersToValidate: string[];\n\n if (config === \"lingo.dev\") {\n // Only need lingo.dev provider\n providersToValidate = [\"lingo.dev\"];\n } else {\n // Extract unique providers from model strings\n const providerSet = new Set<string>();\n Object.values(config).forEach((modelString) => {\n const model = parseModelString(modelString);\n if (model) {\n providerSet.add(model.provider);\n }\n });\n providersToValidate = Array.from(providerSet);\n }\n\n // Validate and fetch keys for each provider\n for (const provider of providersToValidate) {\n const providerConfig = providerDetails[provider];\n\n if (!providerConfig) {\n throw new Error(\n `⚠️ Unknown provider \"${provider}\". Supported providers: ${Object.keys(providerDetails).join(\", \")}`,\n );\n }\n\n // Skip providers that don't require keys (like Ollama)\n if (!providerConfig.apiKeyEnvVar) {\n continue;\n }\n\n const key = getKeyFromEnv(providerConfig.apiKeyEnvVar);\n if (key) {\n keys[provider] = key;\n } else {\n missingProviders.push(provider);\n }\n }\n\n // If any keys are missing, throw with detailed error\n if (missingProviders.length > 0) {\n throw new Error(formatNoApiKeysError(missingProviders));\n }\n\n return keys;\n}\n\n/**\n * Create AI model instance from provider and model ID\n *\n * @param model Provider name (groq, google, openrouter, ollama, mistral) and model identifier\n * @param validatedKeys Pre-validated API keys from validateAndFetchApiKeys()\n * @returns LanguageModel instance\n * @throws Error if provider is not supported or API key is missing\n */\nexport function createAiModel(\n model: LocaleModel,\n validatedKeys: ValidatedApiKeys,\n): LanguageModel {\n const providerConfig = providerDetails[model.provider];\n\n if (!providerConfig) {\n throw new Error(\n `⚠️ Provider \"${model.provider}\" is not supported. Supported providers: ${Object.keys(providerDetails).join(\", \")}`,\n );\n }\n\n // Get API key if required\n const apiKey = providerConfig.apiKeyEnvVar\n ? validatedKeys[model.provider]\n : undefined;\n\n // TODO (AleksandrSl 25/12/2025): Do we really need to make a second check? Maybe creation should be combined with validation.\n // Verify key is present for providers that require it\n if (providerConfig.apiKeyEnvVar && !apiKey) {\n throw new Error(\n `⚠️ ${providerConfig.name} API key not found. Please set ${providerConfig.apiKeyEnvVar} environment variable.\\n\\n` +\n `This should not happen if validateAndGetApiKeys() was called. Please restart the service.`,\n );\n }\n\n // Create the appropriate model instance\n switch (model.provider) {\n case \"groq\":\n return createGroq({ apiKey: apiKey! })(model.name);\n\n case \"google\":\n return createGoogleGenerativeAI({ apiKey: apiKey! })(model.name);\n\n case \"openai\": {\n // Support custom base URL for OpenAI-compatible providers (e.g., Nebius)\n const baseURL = getKeyFromEnv(\"OPENAI_BASE_URL\");\n\n const provider = createOpenAI({\n apiKey: apiKey!,\n ...(baseURL && { baseURL }),\n });\n\n return provider.chat(model.name);\n }\n\n case \"openrouter\":\n return createOpenRouter({ apiKey: apiKey! })(model.name);\n\n case \"ollama\":\n return ollama(model.name);\n\n case \"mistral\":\n return createMistral({ apiKey: apiKey! })(model.name);\n\n default:\n // This should be unreachable due to check above\n throw new Error(`⚠️ Provider \"${model.provider}\" is not implemented`);\n }\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@ type PluralizationConfig = {
8
8
  /**
9
9
  * LLM provider for pluralization detection
10
10
  * Format: "provider:model" (e.g., "groq:llama3-8b-8192")
11
- * @default "groq:llama3-8b-8192"
11
+ * If omitted in user config, the compiler can infer it from translation models.
12
12
  */
13
13
  model: string;
14
14
  };
@@ -8,7 +8,7 @@ type PluralizationConfig = {
8
8
  /**
9
9
  * LLM provider for pluralization detection
10
10
  * Format: "provider:model" (e.g., "groq:llama3-8b-8192")
11
- * @default "groq:llama3-8b-8192"
11
+ * If omitted in user config, the compiler can infer it from translation models.
12
12
  */
13
13
  model: string;
14
14
  };
package/build/types.d.cts CHANGED
@@ -31,11 +31,13 @@ type LocalePersistenceConfig = {
31
31
  */
32
32
  type LingoConfigRequiredFields = "sourceLocale" | "targetLocales";
33
33
  type LingoInternalFields = "environment" | "cacheType";
34
+ type PartialPluralizationConfig = Partial<Omit<PluralizationConfig, "sourceLocale">>;
34
35
  /**
35
36
  * Configuration for the Lingo compiler
36
37
  */
37
- type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields> & {
38
+ type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields | "pluralization"> & {
38
39
  dev: Partial<LingoConfig["dev"]>;
40
+ pluralization: PartialPluralizationConfig;
39
41
  }>;
40
42
  type LingoEnvironment = "development" | "production";
41
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAKY,UA9BK,YAAA,CA8Ba;EAAQ;;;;EAIhC,IAAA,EAAA,MAAA;EAAoC;;;;EAHxC,MAAA,EAAA,MAAA;;AASF;AAKA;;;AAoDiB,KA/EL,uBAAA,GA+EK;EAkBO,IAAA,EAAA,QAAA;EAYF,MAAA,EA7G0C,YA6G1C;CAAL;;;;KAxGL,yBAAA;KAEA,mBAAA;;;;KAKA,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;;KAIP,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
1
+ {"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAEY,UA3BK,YAAA,CA2BL;EACL;;;;EAMK,IAAA,EAAA,MAAA;EAA0B;;;;EAIhC,MAAA,EAAA,MAAA;;;;;;AAHJ,KAjBU,uBAAA,GAiBV;EAAO,IAAA,EAAA,QAAA;EAUG,MAAA,EA3BoD,YA2BpC;AAK5B,CAAA;;;;AAsEwB,KAjGZ,yBAAA,GAiGY,cAAA,GAAA,eAAA;AAYF,KA3GV,mBAAA,GA2GU,aAAA,GAAA,WAAA;AAAL,KAzGL,0BAAA,GAA6B,OAyGxB,CAxGf,IAwGe,CAxGV,mBAwGU,EAAA,cAAA,CAAA,CAAA;;;;KAlGL,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;iBACE;;KAIT,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
package/build/types.d.mts CHANGED
@@ -31,11 +31,13 @@ type LocalePersistenceConfig = {
31
31
  */
32
32
  type LingoConfigRequiredFields = "sourceLocale" | "targetLocales";
33
33
  type LingoInternalFields = "environment" | "cacheType";
34
+ type PartialPluralizationConfig = Partial<Omit<PluralizationConfig, "sourceLocale">>;
34
35
  /**
35
36
  * Configuration for the Lingo compiler
36
37
  */
37
- type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields> & {
38
+ type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields | "pluralization"> & {
38
39
  dev: Partial<LingoConfig["dev"]>;
40
+ pluralization: PartialPluralizationConfig;
39
41
  }>;
40
42
  type LingoEnvironment = "development" | "production";
41
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAKY,UA9BK,YAAA,CA8Ba;EAAQ;;;;EAIhC,IAAA,EAAA,MAAA;EAAoC;;;;EAHxC,MAAA,EAAA,MAAA;;AASF;AAKA;;;AAoDiB,KA/EL,uBAAA,GA+EK;EAkBO,IAAA,EAAA,QAAA;EAYF,MAAA,EA7G0C,YA6G1C;CAAL;;;;KAxGL,yBAAA;KAEA,mBAAA;;;;KAKA,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;;KAIP,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAEY,UA3BK,YAAA,CA2BL;EACL;;;;EAMK,IAAA,EAAA,MAAA;EAA0B;;;;EAIhC,MAAA,EAAA,MAAA;;;;;;AAHJ,KAjBU,uBAAA,GAiBV;EAAO,IAAA,EAAA,QAAA;EAUG,MAAA,EA3BoD,YA2BpC;AAK5B,CAAA;;;;AAsEwB,KAjGZ,yBAAA,GAiGY,cAAA,GAAA,eAAA;AAYF,KA3GV,mBAAA,GA2GU,aAAA,GAAA,WAAA;AAAL,KAzGL,0BAAA,GAA6B,OAyGxB,CAxGf,IAwGe,CAxGV,mBAwGU,EAAA,cAAA,CAAA,CAAA;;;;KAlGL,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;iBACE;;KAIT,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
@@ -17,11 +17,27 @@ const DEFAULT_CONFIG = {
17
17
  },
18
18
  models: "lingo.dev",
19
19
  pluralization: {
20
- enabled: true,
20
+ enabled: false,
21
21
  model: "groq:llama-3.1-8b-instant"
22
22
  },
23
23
  buildMode: "translate"
24
24
  };
25
+ function getModelStringForLocales(models, sourceLocale, targetLocale) {
26
+ const modelKey = (targetLocale ? [
27
+ `${sourceLocale}:${targetLocale}`,
28
+ `*:${targetLocale}`,
29
+ `${sourceLocale}:*`,
30
+ "*:*"
31
+ ] : [`${sourceLocale}:*`, "*:*"]).find((key) => key in models);
32
+ if (modelKey) return models[modelKey];
33
+ const sortedKeys = Object.keys(models).sort();
34
+ if (sortedKeys.length === 0) return;
35
+ return models[sortedKeys[0]];
36
+ }
37
+ function inferPluralizationModel(models, sourceLocale, targetLocales) {
38
+ if (models === "lingo.dev") return;
39
+ return getModelStringForLocales(models, sourceLocale, targetLocales[0]);
40
+ }
25
41
  /**
26
42
  * Create a LoaderConfig with defaults applied
27
43
  *
@@ -30,7 +46,7 @@ const DEFAULT_CONFIG = {
30
46
  *
31
47
  */
32
48
  function createLingoConfig(options) {
33
- return {
49
+ const config = {
34
50
  ...DEFAULT_CONFIG,
35
51
  ...options,
36
52
  environment: options.environment ?? (process.env.NODE_ENV === "development" ? "development" : "production"),
@@ -52,6 +68,22 @@ function createLingoConfig(options) {
52
68
  }
53
69
  }
54
70
  };
71
+ const explicitEnabled = options.pluralization?.enabled;
72
+ const explicitModel = options.pluralization?.model;
73
+ const hasExplicitModel = typeof explicitModel === "string" && explicitModel.trim().length > 0;
74
+ const pluralizationEnabled = typeof explicitEnabled === "boolean" ? explicitEnabled : hasExplicitModel;
75
+ let pluralizationModel = hasExplicitModel ? explicitModel.trim() : config.pluralization.model;
76
+ if (pluralizationEnabled && !hasExplicitModel) {
77
+ const inferredModel = inferPluralizationModel(config.models, config.sourceLocale, config.targetLocales);
78
+ if (!inferredModel) throw new Error("Pluralization is enabled but no \"pluralization.model\" is configured. Please set \"pluralization.model\" explicitly or use direct LLM models (not \"lingo.dev\") so the model can be inferred.");
79
+ pluralizationModel = inferredModel;
80
+ }
81
+ config.pluralization = {
82
+ ...config.pluralization,
83
+ enabled: pluralizationEnabled,
84
+ model: pluralizationModel
85
+ };
86
+ return config;
55
87
  }
56
88
 
57
89
  //#endregion
@@ -16,11 +16,27 @@ const DEFAULT_CONFIG = {
16
16
  },
17
17
  models: "lingo.dev",
18
18
  pluralization: {
19
- enabled: true,
19
+ enabled: false,
20
20
  model: "groq:llama-3.1-8b-instant"
21
21
  },
22
22
  buildMode: "translate"
23
23
  };
24
+ function getModelStringForLocales(models, sourceLocale, targetLocale) {
25
+ const modelKey = (targetLocale ? [
26
+ `${sourceLocale}:${targetLocale}`,
27
+ `*:${targetLocale}`,
28
+ `${sourceLocale}:*`,
29
+ "*:*"
30
+ ] : [`${sourceLocale}:*`, "*:*"]).find((key) => key in models);
31
+ if (modelKey) return models[modelKey];
32
+ const sortedKeys = Object.keys(models).sort();
33
+ if (sortedKeys.length === 0) return;
34
+ return models[sortedKeys[0]];
35
+ }
36
+ function inferPluralizationModel(models, sourceLocale, targetLocales) {
37
+ if (models === "lingo.dev") return;
38
+ return getModelStringForLocales(models, sourceLocale, targetLocales[0]);
39
+ }
24
40
  /**
25
41
  * Create a LoaderConfig with defaults applied
26
42
  *
@@ -29,7 +45,7 @@ const DEFAULT_CONFIG = {
29
45
  *
30
46
  */
31
47
  function createLingoConfig(options) {
32
- return {
48
+ const config = {
33
49
  ...DEFAULT_CONFIG,
34
50
  ...options,
35
51
  environment: options.environment ?? (process.env.NODE_ENV === "development" ? "development" : "production"),
@@ -51,6 +67,22 @@ function createLingoConfig(options) {
51
67
  }
52
68
  }
53
69
  };
70
+ const explicitEnabled = options.pluralization?.enabled;
71
+ const explicitModel = options.pluralization?.model;
72
+ const hasExplicitModel = typeof explicitModel === "string" && explicitModel.trim().length > 0;
73
+ const pluralizationEnabled = typeof explicitEnabled === "boolean" ? explicitEnabled : hasExplicitModel;
74
+ let pluralizationModel = hasExplicitModel ? explicitModel.trim() : config.pluralization.model;
75
+ if (pluralizationEnabled && !hasExplicitModel) {
76
+ const inferredModel = inferPluralizationModel(config.models, config.sourceLocale, config.targetLocales);
77
+ if (!inferredModel) throw new Error("Pluralization is enabled but no \"pluralization.model\" is configured. Please set \"pluralization.model\" explicitly or use direct LLM models (not \"lingo.dev\") so the model can be inferred.");
78
+ pluralizationModel = inferredModel;
79
+ }
80
+ config.pluralization = {
81
+ ...config.pluralization,
82
+ enabled: pluralizationEnabled,
83
+ model: pluralizationModel
84
+ };
85
+ return config;
54
86
  }
55
87
 
56
88
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"config-factory.mjs","names":[],"sources":["../../src/utils/config-factory.ts"],"sourcesContent":["/**\n * Config factory for creating LoaderConfig instances\n */\nimport type {\n LingoConfig,\n LingoConfigRequiredFields,\n LingoInternalFields,\n PartialLingoConfig,\n} from \"../types\";\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG = {\n sourceRoot: \"src\",\n lingoDir: \"lingo\",\n useDirective: false,\n dev: {\n translationServerStartPort: 60000,\n },\n localePersistence: {\n type: \"cookie\" as const,\n config: {\n name: \"locale\",\n maxAge: 31536000,\n },\n },\n models: \"lingo.dev\",\n pluralization: {\n enabled: true,\n model: \"groq:llama-3.1-8b-instant\",\n },\n buildMode: \"translate\",\n} satisfies Omit<\n LingoConfig,\n // Looks like we can use LingoInternalFields, but it's only a coincidence that the types match, we may want to provide default for internal fields\n LingoConfigRequiredFields | \"environment\" | \"cacheType\"\n>;\n\n/**\n * Create a LoaderConfig with defaults applied\n *\n * @param options - Partial config to override defaults\n * @returns Complete LoaderConfig with all defaults applied\n *\n */\nexport function createLingoConfig(\n options: PartialLingoConfig & Partial<Pick<LingoConfig, LingoInternalFields>>,\n): LingoConfig {\n return {\n ...DEFAULT_CONFIG,\n ...options,\n environment:\n options.environment ??\n (process.env.NODE_ENV === \"development\" ? \"development\" : \"production\"),\n cacheType: options.cacheType ?? \"local\",\n dev: {\n ...DEFAULT_CONFIG.dev,\n ...options.dev,\n },\n pluralization: {\n ...DEFAULT_CONFIG.pluralization,\n ...options.pluralization,\n },\n localePersistence: {\n ...DEFAULT_CONFIG.localePersistence,\n ...options.localePersistence,\n config: {\n ...DEFAULT_CONFIG.localePersistence.config,\n ...options.localePersistence?.config,\n },\n },\n };\n}\n"],"mappings":";;;;AAaA,MAAa,iBAAiB;CAC5B,YAAY;CACZ,UAAU;CACV,cAAc;CACd,KAAK,EACH,4BAA4B,KAC7B;CACD,mBAAmB;EACjB,MAAM;EACN,QAAQ;GACN,MAAM;GACN,QAAQ;GACT;EACF;CACD,QAAQ;CACR,eAAe;EACb,SAAS;EACT,OAAO;EACR;CACD,WAAW;CACZ;;;;;;;;AAaD,SAAgB,kBACd,SACa;AACb,QAAO;EACL,GAAG;EACH,GAAG;EACH,aACE,QAAQ,gBACP,QAAQ,IAAI,aAAa,gBAAgB,gBAAgB;EAC5D,WAAW,QAAQ,aAAa;EAChC,KAAK;GACH,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,eAAe;GACb,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,mBAAmB;GACjB,GAAG,eAAe;GAClB,GAAG,QAAQ;GACX,QAAQ;IACN,GAAG,eAAe,kBAAkB;IACpC,GAAG,QAAQ,mBAAmB;IAC/B;GACF;EACF"}
1
+ {"version":3,"file":"config-factory.mjs","names":["config: LingoConfig"],"sources":["../../src/utils/config-factory.ts"],"sourcesContent":["/**\n * Config factory for creating LoaderConfig instances\n */\nimport type {\n LingoConfig,\n LingoConfigRequiredFields,\n LingoInternalFields,\n PartialLingoConfig,\n} from \"../types\";\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG = {\n sourceRoot: \"src\",\n lingoDir: \"lingo\",\n useDirective: false,\n dev: {\n translationServerStartPort: 60000,\n },\n localePersistence: {\n type: \"cookie\" as const,\n config: {\n name: \"locale\",\n maxAge: 31536000,\n },\n },\n models: \"lingo.dev\",\n pluralization: {\n enabled: false,\n model: \"groq:llama-3.1-8b-instant\",\n },\n buildMode: \"translate\",\n} satisfies Omit<\n LingoConfig,\n // Looks like we can use LingoInternalFields, but it's only a coincidence that the types match, we may want to provide default for internal fields\n LingoConfigRequiredFields | \"environment\" | \"cacheType\"\n>;\n\nfunction getModelStringForLocales(\n models: Record<string, string>,\n sourceLocale: string,\n targetLocale: string | undefined,\n): string | undefined {\n const localeKeys = targetLocale\n ? [\n `${sourceLocale}:${targetLocale}`,\n `*:${targetLocale}`,\n `${sourceLocale}:*`,\n \"*:*\",\n ]\n : [`${sourceLocale}:*`, \"*:*\"];\n\n const modelKey = localeKeys.find((key) => key in models);\n if (modelKey) {\n return models[modelKey];\n }\n\n const sortedKeys = Object.keys(models).sort();\n if (sortedKeys.length === 0) {\n return undefined;\n }\n\n return models[sortedKeys[0]];\n}\n\nfunction inferPluralizationModel(\n models: \"lingo.dev\" | Record<string, string>,\n sourceLocale: string,\n targetLocales: string[],\n): string | undefined {\n if (models === \"lingo.dev\") {\n return undefined;\n }\n\n return getModelStringForLocales(\n models,\n sourceLocale,\n targetLocales[0],\n );\n}\n\n/**\n * Create a LoaderConfig with defaults applied\n *\n * @param options - Partial config to override defaults\n * @returns Complete LoaderConfig with all defaults applied\n *\n */\nexport function createLingoConfig(\n options: PartialLingoConfig & Partial<Pick<LingoConfig, LingoInternalFields>>,\n): LingoConfig {\n const config: LingoConfig = {\n ...DEFAULT_CONFIG,\n ...options,\n environment:\n options.environment ??\n (process.env.NODE_ENV === \"development\" ? \"development\" : \"production\"),\n cacheType: options.cacheType ?? \"local\",\n dev: {\n ...DEFAULT_CONFIG.dev,\n ...options.dev,\n },\n pluralization: {\n ...DEFAULT_CONFIG.pluralization,\n ...options.pluralization,\n },\n localePersistence: {\n ...DEFAULT_CONFIG.localePersistence,\n ...options.localePersistence,\n config: {\n ...DEFAULT_CONFIG.localePersistence.config,\n ...options.localePersistence?.config,\n },\n },\n };\n\n const explicitEnabled = options.pluralization?.enabled;\n const explicitModel = options.pluralization?.model;\n const hasExplicitModel =\n typeof explicitModel === \"string\" && explicitModel.trim().length > 0;\n const hasExplicitEnabled = typeof explicitEnabled === \"boolean\";\n\n const pluralizationEnabled = hasExplicitEnabled\n ? explicitEnabled\n : hasExplicitModel;\n\n let pluralizationModel = hasExplicitModel\n ? explicitModel!.trim()\n : config.pluralization.model;\n\n if (pluralizationEnabled && !hasExplicitModel) {\n const inferredModel = inferPluralizationModel(\n config.models,\n config.sourceLocale,\n config.targetLocales,\n );\n\n if (!inferredModel) {\n throw new Error(\n 'Pluralization is enabled but no \"pluralization.model\" is configured. Please set \"pluralization.model\" explicitly or use direct LLM models (not \"lingo.dev\") so the model can be inferred.',\n );\n }\n\n pluralizationModel = inferredModel;\n }\n\n config.pluralization = {\n ...config.pluralization,\n enabled: pluralizationEnabled,\n model: pluralizationModel,\n };\n\n return config;\n}\n"],"mappings":";;;;AAaA,MAAa,iBAAiB;CAC5B,YAAY;CACZ,UAAU;CACV,cAAc;CACd,KAAK,EACH,4BAA4B,KAC7B;CACD,mBAAmB;EACjB,MAAM;EACN,QAAQ;GACN,MAAM;GACN,QAAQ;GACT;EACF;CACD,QAAQ;CACR,eAAe;EACb,SAAS;EACT,OAAO;EACR;CACD,WAAW;CACZ;AAMD,SAAS,yBACP,QACA,cACA,cACoB;CAUpB,MAAM,YATa,eACf;EACE,GAAG,aAAa,GAAG;EACnB,KAAK;EACL,GAAG,aAAa;EAChB;EACD,GACD,CAAC,GAAG,aAAa,KAAK,MAAM,EAEJ,MAAM,QAAQ,OAAO,OAAO;AACxD,KAAI,SACF,QAAO,OAAO;CAGhB,MAAM,aAAa,OAAO,KAAK,OAAO,CAAC,MAAM;AAC7C,KAAI,WAAW,WAAW,EACxB;AAGF,QAAO,OAAO,WAAW;;AAG3B,SAAS,wBACP,QACA,cACA,eACoB;AACpB,KAAI,WAAW,YACb;AAGF,QAAO,yBACL,QACA,cACA,cAAc,GACf;;;;;;;;;AAUH,SAAgB,kBACd,SACa;CACb,MAAMA,SAAsB;EAC1B,GAAG;EACH,GAAG;EACH,aACE,QAAQ,gBACP,QAAQ,IAAI,aAAa,gBAAgB,gBAAgB;EAC5D,WAAW,QAAQ,aAAa;EAChC,KAAK;GACH,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,eAAe;GACb,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,mBAAmB;GACjB,GAAG,eAAe;GAClB,GAAG,QAAQ;GACX,QAAQ;IACN,GAAG,eAAe,kBAAkB;IACpC,GAAG,QAAQ,mBAAmB;IAC/B;GACF;EACF;CAED,MAAM,kBAAkB,QAAQ,eAAe;CAC/C,MAAM,gBAAgB,QAAQ,eAAe;CAC7C,MAAM,mBACJ,OAAO,kBAAkB,YAAY,cAAc,MAAM,CAAC,SAAS;CAGrE,MAAM,uBAFqB,OAAO,oBAAoB,YAGlD,kBACA;CAEJ,IAAI,qBAAqB,mBACrB,cAAe,MAAM,GACrB,OAAO,cAAc;AAEzB,KAAI,wBAAwB,CAAC,kBAAkB;EAC7C,MAAM,gBAAgB,wBACpB,OAAO,QACP,OAAO,cACP,OAAO,cACR;AAED,MAAI,CAAC,cACH,OAAM,IAAI,MACR,kMACD;AAGH,uBAAqB;;AAGvB,QAAO,gBAAgB;EACrB,GAAG,OAAO;EACV,SAAS;EACT,OAAO;EACR;AAED,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingo.dev/compiler",
3
- "version": "0.1.13",
3
+ "version": "0.3.0",
4
4
  "description": "Lingo.dev Compiler",
5
5
  "private": false,
6
6
  "repository": {
@@ -141,6 +141,7 @@
141
141
  "@ai-sdk/google": "3.0.1",
142
142
  "@ai-sdk/groq": "3.0.1",
143
143
  "@ai-sdk/mistral": "3.0.1",
144
+ "@ai-sdk/openai": "3.0.1",
144
145
  "@babel/core": "7.26.0",
145
146
  "@babel/generator": "7.28.5",
146
147
  "@babel/parser": "7.28.5",
@@ -161,7 +162,7 @@
161
162
  "posthog-node": "5.14.0",
162
163
  "proper-lockfile": "4.1.2",
163
164
  "ws": "8.18.3",
164
- "lingo.dev": "^0.125.1"
165
+ "lingo.dev": "^0.125.4"
165
166
  },
166
167
  "peerDependencies": {
167
168
  "next": "^15.0.0 || ^16.0.4",