From c2257943dcbea90346e8318434bb0820d51323f8 Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Tue, 23 Dec 2025 23:09:04 +0100 Subject: [PATCH 01/10] feat: move translation service initialization to see the logs We don't really need to initialize it inside the server, since server is only a way to use the service in the dev mode. Logs of the server are written to a file due to it being started from process which doesn't have access to console. But we want to see TranslationService initialization logs in the console, and the clearest way is to start it early, rather than extracting validation into a separate function. --- .../src/plugin/build-translator.ts | 9 +- packages/new-compiler/src/plugin/next.ts | 5 +- packages/new-compiler/src/plugin/unplugin.ts | 3 +- .../src/translation-server/cli.ts | 1 - .../translation-server/translation-server.ts | 77 +++--------- .../src/translators/cache-factory.ts | 4 +- .../new-compiler/src/translators/index.ts | 3 +- .../src/translators/lingo/index.ts | 4 +- .../lingo/{service.ts => translator.ts} | 2 +- .../src/translators/memory-cache.ts | 67 +++++++++++ .../src/translators/pluralization/service.ts | 45 ++----- .../src/translators/pseudotranslator/index.ts | 22 ---- .../src/translators/translation-service.ts | 110 ++++++++++++------ .../src/translators/translator-factory.ts | 87 -------------- 14 files changed, 185 insertions(+), 254 deletions(-) rename packages/new-compiler/src/translators/lingo/{service.ts => translator.ts} (99%) create mode 100644 packages/new-compiler/src/translators/memory-cache.ts delete mode 100644 packages/new-compiler/src/translators/translator-factory.ts diff --git a/packages/new-compiler/src/plugin/build-translator.ts b/packages/new-compiler/src/plugin/build-translator.ts index 379600f6d..740f05654 100644 --- a/packages/new-compiler/src/plugin/build-translator.ts +++ b/packages/new-compiler/src/plugin/build-translator.ts @@ -11,12 +11,9 @@ import fs from "fs/promises"; import path from "path"; import type { LingoConfig, MetadataSchema } from "../types"; import { logger } from "../utils/logger"; -import { - startTranslationServer, - type TranslationServer, -} from "../translation-server"; +import { startTranslationServer, type TranslationServer, } from "../translation-server"; import { loadMetadata } from "../metadata/manager"; -import { createCache, type TranslationCache } from "../translators"; +import { createCache, type TranslationCache, TranslationService, } from "../translators"; import { dictionaryFrom } from "../translators/api"; import type { LocaleCode } from "lingo.dev/spec"; @@ -108,7 +105,7 @@ export async function processBuildTranslations( try { translationServer = await startTranslationServer({ - startPort: config.dev.translationServerStartPort, + translationService: new TranslationService(config, logger), onError: (err) => { logger.error("Translation server error:", err); }, diff --git a/packages/new-compiler/src/plugin/next.ts b/packages/new-compiler/src/plugin/next.ts index b1c3a7f72..dfa45e2fe 100644 --- a/packages/new-compiler/src/plugin/next.ts +++ b/packages/new-compiler/src/plugin/next.ts @@ -12,6 +12,7 @@ import { startOrGetTranslationServer } from "../translation-server/translation-s import { cleanupExistingMetadata, getMetadataPath } from "../metadata/manager"; import { registerCleanupOnCurrentProcess } from "./cleanup"; import { useI18nRegex } from "./transform/use-i18n"; +import { TranslationService } from "../translators"; export type LingoNextPluginOptions = PartialLingoConfig; @@ -205,14 +206,12 @@ export async function withLingo( `Initializing Lingo.dev compiler. Is dev mode: ${isDev}. Is main runner: ${isMainRunner()}`, ); - // TODO (AleksandrSl 12/12/2025): Add API keys validation too, so we can log it nicely. - // Try to start up the translation server once. // 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. // 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. if (isDev && !process.env.LINGO_TRANSLATION_SERVER_URL) { const translationServer = await startOrGetTranslationServer({ - startPort: lingoConfig.dev.translationServerStartPort, + translationService: new TranslationService(lingoConfig, logger), onError: (err) => { logger.error("Translation server error:", err); }, diff --git a/packages/new-compiler/src/plugin/unplugin.ts b/packages/new-compiler/src/plugin/unplugin.ts index 028a9d57c..d04d9c99d 100644 --- a/packages/new-compiler/src/plugin/unplugin.ts +++ b/packages/new-compiler/src/plugin/unplugin.ts @@ -26,6 +26,7 @@ import { processBuildTranslations } from "./build-translator"; import { registerCleanupOnCurrentProcess } from "./cleanup"; import path from "path"; import fs from "fs"; +import { TranslationService } from "../translators"; export type LingoPluginOptions = PartialLingoConfig; @@ -112,7 +113,7 @@ export const lingoUnplugin = createUnplugin< async function startServer() { const server = await startTranslationServer({ - startPort, + translationService: new TranslationService(config, logger), onError: (err) => { logger.error("Translation server error:", err); }, diff --git a/packages/new-compiler/src/translation-server/cli.ts b/packages/new-compiler/src/translation-server/cli.ts index 99c60a724..41643a962 100644 --- a/packages/new-compiler/src/translation-server/cli.ts +++ b/packages/new-compiler/src/translation-server/cli.ts @@ -444,7 +444,6 @@ export async function main(): Promise { // Start server const { server, url } = await startOrGetTranslationServer({ - startPort, config, // requestTimeout: cliOpts.timeout || 30000, onError: (err) => { diff --git a/packages/new-compiler/src/translation-server/translation-server.ts b/packages/new-compiler/src/translation-server/translation-server.ts index 01fc00831..7c9695cd6 100644 --- a/packages/new-compiler/src/translation-server/translation-server.ts +++ b/packages/new-compiler/src/translation-server/translation-server.ts @@ -17,11 +17,7 @@ import { URL } from "url"; import { WebSocket, WebSocketServer } from "ws"; import type { MetadataSchema, TranslationMiddlewareConfig } from "../types"; import { getLogger } from "./logger"; -import { - createCache, - createTranslator, - TranslationService, -} from "../translators"; +import { TranslationService } from "../translators"; import { createEmptyMetadata, getMetadataPath, @@ -33,25 +29,9 @@ import type { LocaleCode } from "lingo.dev/spec"; import { parseLocaleOrThrow } from "../utils/is-valid-locale"; export interface TranslationServerOptions { - /** - * Starting port to try (will find next available if taken) - * @default 3456 - */ - startPort?: number; - - /** - * Configuration for translation generation - */ config: TranslationMiddlewareConfig; - - /** - * Callback when server is ready - */ + translationService?: TranslationService; onReady?: (port: number) => void; - - /** - * Callback on error - */ onError?: (error: Error) => void; } @@ -59,12 +39,11 @@ export class TranslationServer { private server: http.Server | null = null; private url: string | undefined = undefined; private logger; - private config: TranslationMiddlewareConfig; - private configHash: string; - private startPort: number; - private onReadyCallback?: (port: number) => void; - private onErrorCallback?: (error: Error) => void; - private translationService: TranslationService | null = null; + private readonly config: TranslationMiddlewareConfig; + private readonly configHash: string; + private readonly startPort: number; + private readonly onReadyCallback?: (port: number) => void; + private readonly onErrorCallback?: (error: Error) => void; private metadata: MetadataSchema | null = null; private connections: Set = new Set(); private wss: WebSocketServer | null = null; @@ -75,11 +54,16 @@ export class TranslationServer { private isBusy = false; private busyTimeout: NodeJS.Timeout | null = null; private readonly BUSY_DEBOUNCE_MS = 500; // Time after last translation to send "idle" event + private readonly translationService: TranslationService; constructor(options: TranslationServerOptions) { this.config = options.config; this.configHash = hashConfig(options.config); - this.startPort = options.startPort || 60000; + this.translationService = + options.translationService ?? + // Fallback is for CLI start only. + new TranslationService(options.config, getLogger(options.config)); + this.startPort = options.config.dev.translationServerStartPort; this.onReadyCallback = options.onReady; this.onErrorCallback = options.onError; this.logger = getLogger(this.config); @@ -95,19 +79,6 @@ export class TranslationServer { this.logger.info(`šŸ”§ Initializing translator...`); - const translator = createTranslator(this.config, this.logger); - const cache = createCache(this.config); - - this.translationService = new TranslationService( - translator, - cache, - { - sourceLocale: this.config.sourceLocale, - pluralization: this.config.pluralization, - }, - this.logger, - ); - const port = await this.findAvailablePort(this.startPort); return new Promise((resolve, reject) => { @@ -281,14 +252,13 @@ export class TranslationServer { * Start a new server or get the URL of an existing one on the preferred port. * * This method optimizes for the common case where a translation server is already - * running on port 60000. If that port is taken, it checks if it's our service + * running on a preferred port. If that port is taken, it checks if it's our service * by calling the health check endpoint. If it is, we reuse it instead of starting * a new server on a different port. * * @returns URL of the running server (new or existing) */ async startOrGetUrl(): Promise { - // If this instance already has a server running, return its URL if (this.server && this.url) { this.logger.info(`Using existing server instance at ${this.url}`); return this.url; @@ -527,7 +497,6 @@ export class TranslationServer { res.on("end", () => { try { - // Check if response is valid and has the expected structure if (res.statusCode === 200) { const json = JSON.parse(data); // Our translation server returns { status: "ok", port: ..., configHash: ... } @@ -680,11 +649,6 @@ export class TranslationServer { ); return; } - - if (!this.translationService) { - throw new Error("Translation service not initialized"); - } - // Reload metadata to ensure we have the latest entries // (new entries may have been added since server started) await this.reloadMetadata(); @@ -747,10 +711,6 @@ export class TranslationServer { try { const parsedLocale = parseLocaleOrThrow(locale); - if (!this.translationService) { - throw new Error("Translation service not initialized"); - } - // Reload metadata to ensure we have the latest entries // (new entries may have been added since server started) await this.reloadMetadata(); @@ -842,9 +802,6 @@ export function hashConfig(config: Record): string { return crypto.createHash("md5").update(serialized).digest("hex").slice(0, 12); } -/** - * Create and start a translation server - */ export async function startTranslationServer( options: TranslationServerOptions, ): Promise { @@ -856,10 +813,10 @@ export async function startTranslationServer( /** * Create a translation server and start it or reuse an existing one on the preferred port * - * Since we have little control over the dev server start in next, we can start the translation server only in the loader, - * and loaders could be started from multiple processes (it seems) or similar we need a way to avoid starting multiple servers. + * Since we have little control over the dev server start in next, we can start the translation server only in the async config or in the loader, + * they both could be run in different processes, and we need a way to avoid starting multiple servers. * This one will try to start a server on the preferred port (which seems to be an atomic operation), and if it fails, - * it checks if the server already started is ours and returns its url. + * it checks if the server that is already started is ours and returns its url. * * @returns Object containing the server instance and its URL */ diff --git a/packages/new-compiler/src/translators/cache-factory.ts b/packages/new-compiler/src/translators/cache-factory.ts index d13cef1ea..e4a197f66 100644 --- a/packages/new-compiler/src/translators/cache-factory.ts +++ b/packages/new-compiler/src/translators/cache-factory.ts @@ -8,6 +8,8 @@ import { LocalTranslationCache } from "./local-cache"; import { logger } from "../utils/logger"; import { getCacheDir } from "../utils/path-helpers"; +export type CacheConfig = Pick & PathConfig; + /** * Create a cache instance based on the config * @@ -21,7 +23,7 @@ import { getCacheDir } from "../utils/path-helpers"; * ``` */ export function createCache( - config: Pick & PathConfig, + config: CacheConfig, ): TranslationCache { switch (config.cacheType) { case "local": diff --git a/packages/new-compiler/src/translators/index.ts b/packages/new-compiler/src/translators/index.ts index 115c51884..5bb61fabb 100644 --- a/packages/new-compiler/src/translators/index.ts +++ b/packages/new-compiler/src/translators/index.ts @@ -9,9 +9,8 @@ export type { Translator, TranslatableEntry } from "./api"; // Translators export { PseudoTranslator } from "./pseudotranslator"; -export { Service } from "./lingo"; +export { LingoTranslator } from "./lingo"; export type { LingoTranslatorConfig } from "./lingo"; -export { createTranslator } from "./translator-factory"; // Translation Service (orchestrator) export { TranslationService } from "./translation-service"; diff --git a/packages/new-compiler/src/translators/lingo/index.ts b/packages/new-compiler/src/translators/lingo/index.ts index 3a5165cac..a09f9adff 100644 --- a/packages/new-compiler/src/translators/lingo/index.ts +++ b/packages/new-compiler/src/translators/lingo/index.ts @@ -4,5 +4,5 @@ * Real AI-powered translation using various LLM providers */ -export { Service } from "./service"; -export type { LingoTranslatorConfig } from "./service"; +export { LingoTranslator } from "./translator"; +export type { LingoTranslatorConfig } from "./translator"; diff --git a/packages/new-compiler/src/translators/lingo/service.ts b/packages/new-compiler/src/translators/lingo/translator.ts similarity index 99% rename from packages/new-compiler/src/translators/lingo/service.ts rename to packages/new-compiler/src/translators/lingo/translator.ts index e77e99d6f..d93fd41cf 100644 --- a/packages/new-compiler/src/translators/lingo/service.ts +++ b/packages/new-compiler/src/translators/lingo/translator.ts @@ -31,7 +31,7 @@ export interface LingoTranslatorConfig { /** * Lingo translator using AI models */ -export class Service implements Translator { +export class LingoTranslator implements Translator { private readonly validatedKeys: ValidatedApiKeys; constructor( diff --git a/packages/new-compiler/src/translators/memory-cache.ts b/packages/new-compiler/src/translators/memory-cache.ts new file mode 100644 index 000000000..74fa5ae6e --- /dev/null +++ b/packages/new-compiler/src/translators/memory-cache.ts @@ -0,0 +1,67 @@ +import type { TranslationCache } from "./cache"; +import type { LocaleCode } from "lingo.dev/spec"; + +/** + * In memory translation cache implementation + */ +export class MemoryTranslationCache implements TranslationCache { + private cache: Map> = new Map(); + + constructor() {} + + async get( + locale: LocaleCode, + hashes?: string[], + ): Promise> { + const localeCache = this.cache.get(locale); + if (!localeCache) { + return {}; + } + if (hashes) { + return hashes.reduce( + (acc, hash) => ({ ...acc, [hash]: localeCache.get(hash) }), + {}, + ); + } + return Object.fromEntries(localeCache); + } + + /** + * Update cache with new translations (merge) + */ + async update( + locale: LocaleCode, + translations: Record, + ): Promise { + let localeCache = this.cache.get(locale); + if (!localeCache) { + localeCache = new Map(); + this.cache.set(locale, localeCache); + } + for (const [key, value] of Object.entries(translations)) { + localeCache.set(key, value); + } + } + + /** + * Replace entire cache for a locale + */ + async set( + locale: LocaleCode, + translations: Record, + ): Promise { + this.cache.set(locale, new Map(Object.entries(translations))); + } + + async has(locale: LocaleCode): Promise { + return this.cache.has(locale); + } + + async clear(locale: LocaleCode): Promise { + this.cache.delete(locale); + } + + async clearAll(): Promise { + this.cache.clear(); + } +} diff --git a/packages/new-compiler/src/translators/pluralization/service.ts b/packages/new-compiler/src/translators/pluralization/service.ts index 3371131f8..8ad7272d0 100644 --- a/packages/new-compiler/src/translators/pluralization/service.ts +++ b/packages/new-compiler/src/translators/pluralization/service.ts @@ -31,7 +31,6 @@ import { validateICU } from "./icu-validator"; */ export class PluralizationService { private readonly languageModel: LanguageModel; - private readonly modelName: string; private cache = new Map(); private readonly prompt: string; private readonly sourceLocale: string; @@ -42,26 +41,23 @@ export class PluralizationService { ) { const localeModel = parseModelString(config.model); if (!localeModel) { - throw new Error(`Invalid model format: "${config.model}"`); + throw new Error(`Invalid model format in pluralization service: "${config.model}"`); } // Validate and fetch API keys for the pluralization provider // We need to create a models config that validateAndFetchApiKeys can use const modelsConfig: Record = { - "*:*": config.model, // Single model for pluralization + "*:*": config.model, }; - this.logger.info("Validating API keys for pluralization..."); const validatedKeys = validateAndGetApiKeys(modelsConfig); - this.logger.info("āœ… API keys validated for pluralization"); this.languageModel = createAiModel(localeModel, validatedKeys); - this.modelName = `${localeModel.provider}:${localeModel.name}`; this.sourceLocale = config.sourceLocale; this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale }); this.logger.info( - `Initialized pluralization service with ${this.modelName}`, + `Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`, ); } @@ -95,7 +91,7 @@ export class PluralizationService { return results; } - this.logger.info( + this.logger.debug( `Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`, ); @@ -103,7 +99,7 @@ export class PluralizationService { for (let i = 0; i < uncachedCandidates.length; i += batchSize) { const batch = uncachedCandidates.slice(i, i + batchSize); - this.logger.info( + this.logger.debug( `Processing batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(uncachedCandidates.length / batchSize)} (${batch.length} candidates)`, ); @@ -167,7 +163,7 @@ export class PluralizationService { ], }), DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch - `Pluralization with ${this.modelName}`, + `Pluralization with ${this.languageModel.provider}`, ); const responseText = response.text.trim(); @@ -214,7 +210,7 @@ export class PluralizationService { for (const candidate of candidates) { if (!results.has(candidate.hash)) { this.logger.warn( - `No result returned for candidate: ${candidate.sourceText}`, + `No result returned for a candidate: ${candidate.sourceText}`, ); results.set(candidate.hash, { success: false, @@ -266,7 +262,7 @@ export class PluralizationService { }; } - this.logger.info( + this.logger.debug( `Starting pluralization processing for ${totalEntries} entries`, ); @@ -280,7 +276,7 @@ export class PluralizationService { const candidates = detectPluralCandidates(entriesMap, this.logger); - this.logger.info( + this.logger.debug( `Found ${candidates.length} plural candidates (${((candidates.length / totalEntries) * 100).toFixed(1)}%)`, ); @@ -352,8 +348,7 @@ export class PluralizationService { continue; } - // Update metadata entry in-place - this.logger.info( + this.logger.debug( `Pluralizing: "${entry.sourceText}" -> "${result.icuText}"`, ); entry.sourceText = result.icuText; @@ -363,7 +358,7 @@ export class PluralizationService { const endTime = performance.now(); const duration = endTime - startTime; - this.logger.info( + this.logger.debug( `Pluralization completed: ${pluralized} pluralized, ${rejected} rejected, ${failed} failed in ${duration.toFixed(0)}ms`, ); @@ -376,22 +371,4 @@ export class PluralizationService { durationMs: duration, }; } - - /** - * Clear the cache - */ - clearCache(): void { - this.cache.clear(); - this.logger.debug("Pluralization cache cleared"); - } - - /** - * Get cache statistics - */ - getCacheStats(): { size: number; hits: number } { - return { - size: this.cache.size, - hits: 0, // We don't track hits currently - }; - } } diff --git a/packages/new-compiler/src/translators/pseudotranslator/index.ts b/packages/new-compiler/src/translators/pseudotranslator/index.ts index 5e1eff1cd..78d6ca19f 100644 --- a/packages/new-compiler/src/translators/pseudotranslator/index.ts +++ b/packages/new-compiler/src/translators/pseudotranslator/index.ts @@ -21,25 +21,11 @@ export class PseudoTranslator implements Translator { ) {} translate(locale: LocaleCode, entries: Record) { - this.logger.debug( - `[TRACE-PSEUDO] translate() ENTERED for ${locale} with ${Object.keys(entries).length} entries`, - ); const delay = this.config?.delayMedian ?? 0; const actualDelay = this.getRandomDelay(delay); - this.logger.debug( - `[TRACE-PSEUDO] Config delay: ${delay}ms, actual delay: ${actualDelay}ms`, - ); - return new Promise>((resolve) => { - this.logger.debug( - `[TRACE-PSEUDO] Promise created, scheduling setTimeout for ${actualDelay}ms`, - ); - setTimeout(() => { - this.logger.debug( - `[TRACE-PSEUDO] setTimeout callback fired for ${locale}, processing entries`, - ); const result = Object.fromEntries( Object.entries(entries).map(([hash, entry]) => { @@ -47,16 +33,8 @@ export class PseudoTranslator implements Translator { }), ); - this.logger.debug( - `[TRACE-PSEUDO] Pseudolocalization complete, resolving with ${Object.keys(result).length} translations`, - ); resolve(result); - this.logger.debug(`[TRACE-PSEUDO] Promise resolved for ${locale}`); }, actualDelay); - - this.logger.debug( - `[TRACE-PSEUDO] setTimeout scheduled, returning promise`, - ); }); } diff --git a/packages/new-compiler/src/translators/translation-service.ts b/packages/new-compiler/src/translators/translation-service.ts index 645300863..960770560 100644 --- a/packages/new-compiler/src/translators/translation-service.ts +++ b/packages/new-compiler/src/translators/translation-service.ts @@ -10,7 +10,7 @@ import type { TranslationCache } from "./cache"; import type { TranslatableEntry, Translator } from "./api"; -import type { MetadataSchema } from "../types"; +import type { LingoEnvironment, MetadataSchema } from "../types"; import { type PluralizationConfig, PluralizationService, @@ -18,8 +18,11 @@ import { import type { Logger } from "../utils/logger"; import type { LocaleCode } from "lingo.dev/spec"; import { PseudoTranslator } from "./pseudotranslator"; +import { LingoTranslator } from "./lingo"; +import { type CacheConfig, createCache } from "./cache-factory"; +import { MemoryTranslationCache } from "./memory-cache"; -export interface TranslationServiceConfig { +export type TranslationServiceConfig = { /** * Source locale (e.g., "en") */ @@ -30,7 +33,13 @@ export interface TranslationServiceConfig { * If provided, enables automatic pluralization of source messages */ pluralization: Omit; -} + models: "lingo.dev" | Record; + prompt?: string; + environment: LingoEnvironment; + dev?: { + usePseudotranslator?: boolean; + }; +} & CacheConfig; export interface TranslationResult { /** @@ -55,29 +64,75 @@ export interface TranslationError { } export class TranslationService { - private useCache = true; private pluralizationService?: PluralizationService; + private translator: Translator; + private cache: TranslationCache; constructor( - private translator: Translator, - private cache: TranslationCache, private config: TranslationServiceConfig, private logger: Logger, ) { - const isPseudo = this.translator instanceof PseudoTranslator; - this.useCache = !isPseudo; - - // Initialize pluralization service if enabled - // Do this once at construction to avoid repeated API key validation and model creation - if (this.config.pluralization?.enabled !== false && !isPseudo) { - this.logger.info("Initializing pluralization service..."); - this.pluralizationService = new PluralizationService( - { - ...this.config.pluralization, - sourceLocale: this.config.sourceLocale, - }, - this.logger, + const isDev = config.environment === "development"; + + // 1. Explicit dev override takes precedence + if (isDev && config.dev?.usePseudotranslator) { + this.logger.info( + "šŸ“ Using pseudotranslator (dev.usePseudotranslator enabled)", ); + this.translator = new PseudoTranslator({ delayMedian: 100 }, logger); + this.cache = new MemoryTranslationCache(); + } else { + // 2. Try to create real translator + // LingoTranslator constructor will validate and fetch API keys + // If validation fails, it will throw an error with helpful message + try { + const models = config.models; + + this.logger.debug( + `Creating Lingo translator with models: ${JSON.stringify(models)}`, + ); + + this.cache = createCache(config); + this.translator = new LingoTranslator( + { + models, + sourceLocale: config.sourceLocale, + prompt: config.prompt, + }, + this.logger, + ); + + if (this.config.pluralization?.enabled) { + this.pluralizationService = new PluralizationService( + { + ...this.config.pluralization, + sourceLocale: this.config.sourceLocale, + }, + this.logger, + ); + } + } catch (error) { + // 3. Auto-fallback in dev mode if creation fails + if (isDev) { + // Use console.error to ensure visibility in all contexts (loader, server, etc.) + const errorMsg = + error instanceof Error ? error.message : String(error); + this.logger.warn(`\nāš ļø Translation setup error: ${errorMsg}\n`); + this.logger.warn( + `āš ļø Auto-fallback to pseudotranslator in development mode.\n` + + ` Set the required API keys for real translations.\n`, + ); + + this.translator = new PseudoTranslator( + { delayMedian: 100 }, + this.logger, + ); + this.cache = new MemoryTranslationCache(); + } else { + // 4. Fail in production + throw error; + } + } } } @@ -106,9 +161,7 @@ export class TranslationService { // Step 2: Check cache first (same for all locales, including source) this.logger.debug(`[TRACE] Checking cache for locale: ${locale}`); const cacheStartTime = performance.now(); - const cachedTranslations = this.useCache - ? await this.cache.get(locale) - : {}; + const cachedTranslations = await this.cache.get(locale); const cacheEndTime = performance.now(); this.logger.debug( `[TRACE] Cache check completed in ${(cacheEndTime - cacheStartTime).toFixed(2)}ms, found ${Object.keys(cachedTranslations).length} entries`, @@ -290,20 +343,9 @@ export class TranslationService { } // Step 5: Update cache with successful translations (skip for pseudo) - if (this.useCache && Object.keys(newTranslations).length > 0) { + if (Object.keys(newTranslations).length > 0) { try { - this.logger.debug( - `[TRACE] Updating cache with ${Object.keys(newTranslations).length} translations for ${locale}`, - ); - const updateStartTime = performance.now(); await this.cache.update(locale, newTranslations); - const updateEndTime = performance.now(); - this.logger.debug( - `[TRACE] Cache update completed in ${(updateEndTime - updateStartTime).toFixed(2)}ms`, - ); - this.logger.info( - `Updated cache with ${Object.keys(newTranslations).length} translations for ${locale}`, - ); } catch (error) { this.logger.error(`Failed to update cache:`, error); // Don't fail the request if cache update fails diff --git a/packages/new-compiler/src/translators/translator-factory.ts b/packages/new-compiler/src/translators/translator-factory.ts deleted file mode 100644 index b401a2d3e..000000000 --- a/packages/new-compiler/src/translators/translator-factory.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Factory for creating translators based on configuration - */ - -import type { Translator } from "./api"; -import { PseudoTranslator } from "./pseudotranslator"; -import { Service } from "./lingo"; -import { Logger } from "../utils/logger"; -import type { LocaleCode } from "lingo.dev/spec"; -import type { LingoEnvironment } from "../types"; - -interface TranslatorFactoryConfig { - sourceLocale: LocaleCode; - models: "lingo.dev" | Record; - prompt?: string; - environment: LingoEnvironment; - dev?: { - usePseudotranslator?: boolean; - }; -} - -/** - * Create a translator instance based on configuration - * - * Development mode behavior: - * - If translator is "pseudo" or dev.usePseudotranslator is true, use pseudotranslator - * - If API keys are missing, auto-fallback to pseudotranslator (with warning) - * - Otherwise, create real translator - * - * Production mode behavior: - * - Always require real translator with valid API keys - * - Throw error if API keys are missing - * - * Note: Translators are stateless and don't handle caching. - * Caching is handled by TranslationService layer. - * - * API key validation is now done in the LingoTranslator constructor - * which validates and fetches all keys once at initialization. - */ -export function createTranslator( - config: TranslatorFactoryConfig, - logger: Logger, -): Translator { - const isDev = config.environment === "development"; - - // 1. Explicit dev override takes precedence - if (isDev && config.dev?.usePseudotranslator) { - logger.info("šŸ“ Using pseudotranslator (dev.usePseudotranslator enabled)"); - return new PseudoTranslator({ delayMedian: 100 }, logger); - } - - // 2. Try to create real translator - // LingoTranslator constructor will validate and fetch API keys - // If validation fails, it will throw an error with helpful message - try { - const models = config.models; - - logger.info( - `Creating Lingo translator with models: ${JSON.stringify(models)}`, - ); - - return new Service( - { - models, - sourceLocale: config.sourceLocale, - prompt: config.prompt, - }, - logger, - ); - } catch (error) { - // 3. Auto-fallback in dev mode if creation fails - if (isDev) { - // Use console.error to ensure visibility in all contexts (loader, server, etc.) - const errorMsg = error instanceof Error ? error.message : String(error); - logger.error(`\nāŒ [Lingo] Translation setup error: ${errorMsg}\n`); - logger.warn( - `āš ļø [Lingo] Auto-fallback to pseudotranslator in development mode.\n` + - ` Set the required API keys for real translations.\n`, - ); - - return new PseudoTranslator({ delayMedian: 100 }, logger); - } - - // 4. Fail in production - throw error; - } -} From 861ca01efe81de8fb217f8435a034d7c002c8391 Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 00:22:39 +0100 Subject: [PATCH 02/10] fix: new ai sdk api compatibility --- packages/new-compiler/src/translators/pluralization/service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/new-compiler/src/translators/pluralization/service.ts b/packages/new-compiler/src/translators/pluralization/service.ts index 8ad7272d0..8bf25b564 100644 --- a/packages/new-compiler/src/translators/pluralization/service.ts +++ b/packages/new-compiler/src/translators/pluralization/service.ts @@ -163,7 +163,7 @@ export class PluralizationService { ], }), DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch - `Pluralization with ${this.languageModel.provider}`, + `Pluralization with ${this.languageModel}`, ); const responseText = response.text.trim(); From bed9c1fbfa0a3b255e7fb35d971671cc77394686 Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 00:35:18 +0100 Subject: [PATCH 03/10] chore: add changeset --- .changeset/sharp-yaks-cough.md | 5 +++++ packages/new-compiler/package.json | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/sharp-yaks-cough.md diff --git a/.changeset/sharp-yaks-cough.md b/.changeset/sharp-yaks-cough.md new file mode 100644 index 000000000..8a56e9781 --- /dev/null +++ b/.changeset/sharp-yaks-cough.md @@ -0,0 +1,5 @@ +--- +"@lingo.dev/compiler": patch +--- + +Show logs of the translator initialization to notify about possible problems with LLM keys diff --git a/packages/new-compiler/package.json b/packages/new-compiler/package.json index e6204c8fa..f10d8ff75 100644 --- a/packages/new-compiler/package.json +++ b/packages/new-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@lingo.dev/compiler", - "version": "0.1.1", + "version": "0.1.2", "description": "Lingo.dev Compiler", "private": false, "repository": { @@ -186,4 +186,4 @@ "optional": true } } -} \ No newline at end of file +} From 6aa0e9a39238f10142eb4f174a1b2c571eff00be Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 22:14:54 +0100 Subject: [PATCH 04/10] feat: add compiler e2e tests --- .github/workflows/pr-check.yml | 43 ++++++++++++++++++++++++++++++ packages/new-compiler/package.json | 10 +++---- turbo.json | 7 ++++- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ed47026af..e67cc27dc 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -71,3 +71,46 @@ jobs: - name: Require changeset to be present in PR if: github.event.pull_request.user.login != 'dependabot[bot]' run: pnpm changeset status --since origin/main + + compiler-e2e: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{github.event.pull_request.head.sha}} + fetch-depth: 0 + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.12.3 + + - name: Configure pnpm cache + id: pnpm-cache + run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install + + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + working-directory: packages/new-compiler + + - name: Configure Turbo cache + uses: dtinth/setup-github-actions-caching-for-turbo@v1 + + - name: Run E2E tests + run: pnpm turbo run test:e2e --filter=./packages/new-compiler diff --git a/packages/new-compiler/package.json b/packages/new-compiler/package.json index f10d8ff75..fada752df 100644 --- a/packages/new-compiler/package.json +++ b/packages/new-compiler/package.json @@ -123,11 +123,11 @@ "clean": "rm -rf build", "test": "vitest --run", "test:watch": "vitest -w", - "test:prepare": "pnpm build && tsx tests/helpers/prepare-fixtures.ts", - "test:e2e": "playwright test --reporter=list", - "test:e2e:next": "playwright test --grep next --reporter=list", - "test:e2e:vite": "playwright test --grep vite --reporter=list", - "test:e2e:shared": "playwright test tests/e2e/shared --reporter=list", + "test:e2e:prepare": "tsx tests/helpers/prepare-fixtures.ts", + "test:e2e": "playwright test", + "test:e2e:next": "playwright test --grep next", + "test:e2e:vite": "playwright test --grep vite", + "test:e2e:shared": "playwright test tests/e2e/shared", "test:e2e:headed": "playwright test --headed", "test:e2e:ui": "playwright test --ui", "test:e2e:debug": "playwright test --debug", diff --git a/turbo.json b/turbo.json index 4082f08dd..1b0450617 100644 --- a/turbo.json +++ b/turbo.json @@ -17,6 +17,11 @@ "^build" ] }, + "test:e2e": { + "dependsOn": [ + "^build" + ] + }, "deploy": { "dependsOn": [ "build", @@ -25,4 +30,4 @@ ] } } -} \ No newline at end of file +} From fce5c3aac9e3dd23d8515020f26128b8dc1460f0 Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 22:57:48 +0100 Subject: [PATCH 05/10] fix: run tests preparation through turbo --- packages/new-compiler/tests/QUICK_START.md | 20 +++---------------- packages/new-compiler/tests/README.md | 10 +++++----- .../tests/helpers/fixture-integrity.ts | 2 +- .../tests/helpers/setup-fixture.ts | 4 ++-- turbo.json | 7 ++++++- 5 files changed, 17 insertions(+), 26 deletions(-) diff --git a/packages/new-compiler/tests/QUICK_START.md b/packages/new-compiler/tests/QUICK_START.md index 2aeab94d4..dbfb9ec65 100644 --- a/packages/new-compiler/tests/QUICK_START.md +++ b/packages/new-compiler/tests/QUICK_START.md @@ -10,7 +10,7 @@ pnpm install pnpm playwright:install # 3. Prepare test fixtures (takes 2-3 minutes) -pnpm test:prepare +pnpm test:e2e:prepare ``` ## Running Tests @@ -37,7 +37,7 @@ Instead of installing dependencies on every test run, we use a two-stage approac ``` ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” -│ Stage 1: Preparation (ONE TIME - run pnpm test:prepare) │ +│ Stage 1: Preparation (ONE TIME - run pnpm test:e2e:prepare) │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ │ │ demo/next16/ ──────┐ │ @@ -79,7 +79,7 @@ Instead of installing dependencies on every test run, we use a two-stage approac **10x faster test execution!** -## When to Re-run `pnpm test:prepare` +## When to Re-run `pnpm test:e2e:prepare` - When demo app `package.json` changes - When you update demo app dependencies @@ -124,17 +124,3 @@ test("my test", async ({ page }) => { } }); ``` - -## CI/CD Integration - -In CI, run both stages: - -```yaml -- name: Prepare fixtures - run: pnpm test:prepare - -- name: Run E2E tests - run: pnpm test:e2e -``` - -You could also cache the `tests/fixtures/` directory to speed up CI runs. diff --git a/packages/new-compiler/tests/README.md b/packages/new-compiler/tests/README.md index 2ae0b2da7..f14b181d5 100644 --- a/packages/new-compiler/tests/README.md +++ b/packages/new-compiler/tests/README.md @@ -19,7 +19,7 @@ pnpm playwright:install 3. **Prepare test fixtures** (one-time setup): ```bash -pnpm test:prepare +pnpm test:e2e:prepare ``` This will: @@ -28,7 +28,7 @@ This will: - Install dependencies in each fixture - Takes 2-3 minutes but only needs to be run once -**Note:** You only need to re-run `pnpm test:prepare` if: +**Note:** You only need to re-run `pnpm test:e2e:prepare` if: - Demo app dependencies change - You want to test with updated demo apps @@ -86,7 +86,7 @@ tests/ 1. **Preparation Stage** (once): - Demo apps are copied to `tests/fixtures/` - Dependencies are installed in each fixture - - Run with: `pnpm test:prepare` + - Run with: `pnpm test:e2e:prepare` 2. **Test Execution** (fast): - Each test copies the prepared fixture (with node_modules) to a temp directory @@ -158,7 +158,7 @@ test("my test", async ({ page }) => { ### "Fixture not found" error -Run `pnpm test:prepare` to prepare the fixtures before running tests. +Run `pnpm test:e2e:prepare` to prepare the fixtures before running tests. ### Port conflicts @@ -178,4 +178,4 @@ Failed tests may leave temp directories. They're in your OS temp folder with `li ### Outdated fixtures -If demo apps change significantly, re-run `pnpm test:prepare` to update the fixtures. +If demo apps change significantly, re-run `pnpm test:e2e:prepare` to update the fixtures. diff --git a/packages/new-compiler/tests/helpers/fixture-integrity.ts b/packages/new-compiler/tests/helpers/fixture-integrity.ts index 9ebddcba1..af6f048d6 100644 --- a/packages/new-compiler/tests/helpers/fixture-integrity.ts +++ b/packages/new-compiler/tests/helpers/fixture-integrity.ts @@ -130,7 +130,7 @@ export async function verifyFixtureIntegrity( const checksumsPath = getChecksumFilePath(fixturePath); if (!fsSync.existsSync(checksumsPath)) { errors.push( - "Checksums file not found. Please run 'pnpm test:prepare' to generate it.", + "Checksums file not found. Please run 'pnpm test:e2e:prepare' to generate it.", ); return { valid: false, errors }; } diff --git a/packages/new-compiler/tests/helpers/setup-fixture.ts b/packages/new-compiler/tests/helpers/setup-fixture.ts index 314cf98e2..696dc089c 100644 --- a/packages/new-compiler/tests/helpers/setup-fixture.ts +++ b/packages/new-compiler/tests/helpers/setup-fixture.ts @@ -117,7 +117,7 @@ export async function setupFixture( const fixturePath = path.join(process.cwd(), "tests", "fixtures", framework); if (!fsSync.existsSync(fixturePath)) { throw new Error( - `Fixture for ${framework} not found. Run "pnpm test:prepare" first.`, + `Fixture for ${framework} not found. Run "pnpm test:e2e:prepare" first.`, ); } @@ -126,7 +126,7 @@ export async function setupFixture( console.error(`āŒ Fixture integrity check failed for ${framework}:`); errors.forEach((error) => console.error(` - ${error}`)); throw new Error( - `Fixture integrity check failed. Please run "pnpm test:prepare" to recreate fixtures.`, + `Fixture integrity check failed. Please run "pnpm test:e2e:prepare" to recreate fixtures.`, ); } console.log( diff --git a/turbo.json b/turbo.json index 1b0450617..b9e768204 100644 --- a/turbo.json +++ b/turbo.json @@ -17,11 +17,16 @@ "^build" ] }, - "test:e2e": { + "prepare:e2e": { "dependsOn": [ "^build" ] }, + "test:e2e": { + "dependsOn": [ + "prepare:e2e" + ] + }, "deploy": { "dependsOn": [ "build", From 790182e0ae911ae26e3d9cfd9c2e3da954133743 Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 23:07:09 +0100 Subject: [PATCH 06/10] fix: limit installed browsers to chromium and fix trubo config --- .github/workflows/pr-check.yml | 4 ++-- packages/new-compiler/playwright.config.ts | 1 + turbo.json | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index e67cc27dc..6f301efd1 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -100,13 +100,13 @@ jobs: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-pnpm-store- + ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps + run: pnpm exec playwright install chromium --with-deps working-directory: packages/new-compiler - name: Configure Turbo cache diff --git a/packages/new-compiler/playwright.config.ts b/packages/new-compiler/playwright.config.ts index 763d6b43b..b6718a58e 100644 --- a/packages/new-compiler/playwright.config.ts +++ b/packages/new-compiler/playwright.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ /* Configure projects for major browsers */ projects: [ { + // If we need more than one browser at some point, add them to CI browser installation step too. name: "chromium", use: { ...devices["Desktop Chrome"] }, }, diff --git a/turbo.json b/turbo.json index b9e768204..5a95d93ed 100644 --- a/turbo.json +++ b/turbo.json @@ -17,14 +17,14 @@ "^build" ] }, - "prepare:e2e": { + "test:e2e:prepare": { "dependsOn": [ "^build" ] }, "test:e2e": { "dependsOn": [ - "prepare:e2e" + "test:e2e:prepare" ] }, "deploy": { From 55431feb8c14cc7df84f303b488493900f59731c Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 23:28:26 +0100 Subject: [PATCH 07/10] fix: run build before tests --- .github/workflows/pr-check.yml | 1 + turbo.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 6f301efd1..03edca0c6 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -73,6 +73,7 @@ jobs: run: pnpm changeset status --since origin/main compiler-e2e: + needs: check timeout-minutes: 60 runs-on: ubuntu-latest steps: diff --git a/turbo.json b/turbo.json index 5a95d93ed..d51528c77 100644 --- a/turbo.json +++ b/turbo.json @@ -19,7 +19,7 @@ }, "test:e2e:prepare": { "dependsOn": [ - "^build" + "build" ] }, "test:e2e": { From fb8ca8cd147c5fed263a05f4b01d8aa21f528e5d Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Wed, 24 Dec 2025 23:52:48 +0100 Subject: [PATCH 08/10] fix: get rid of noisy logs --- .../src/translators/pluralization/service.ts | 31 +++++----- .../src/translators/translation-service.ts | 61 ++++--------------- 2 files changed, 28 insertions(+), 64 deletions(-) diff --git a/packages/new-compiler/src/translators/pluralization/service.ts b/packages/new-compiler/src/translators/pluralization/service.ts index 8bf25b564..c2064df50 100644 --- a/packages/new-compiler/src/translators/pluralization/service.ts +++ b/packages/new-compiler/src/translators/pluralization/service.ts @@ -56,7 +56,7 @@ export class PluralizationService { this.sourceLocale = config.sourceLocale; this.prompt = getSystemPrompt({ sourceLocale: config.sourceLocale }); - this.logger.info( + this.logger.debug( `Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`, ); } @@ -72,22 +72,23 @@ export class PluralizationService { candidates: PluralCandidate[], batchSize: number = 10, ): Promise> { - const results = new Map(); - - // Check cache first - const uncachedCandidates = candidates.filter((c) => { - const cached = this.cache.get(c.hash); - if (cached) { - results.set(c.hash, cached); - return false; - } - return true; - }); + const { uncachedCandidates, results } = candidates.reduce( + (acc, c) => { + const cached = this.cache.get(c.hash); + if (cached) { + acc.results.set(c.hash, cached); + } else { + acc.uncachedCandidates.push(c); + } + return acc; + }, + { + uncachedCandidates: [] as PluralCandidate[], + results: new Map(), + }, + ); if (uncachedCandidates.length === 0) { - this.logger.debug( - `All ${candidates.length} candidates found in cache, skipping LLM call`, - ); return results; } diff --git a/packages/new-compiler/src/translators/translation-service.ts b/packages/new-compiler/src/translators/translation-service.ts index 960770560..20416a91a 100644 --- a/packages/new-compiler/src/translators/translation-service.ts +++ b/packages/new-compiler/src/translators/translation-service.ts @@ -154,36 +154,24 @@ export class TranslationService { // Step 1: Determine which hashes we need to work with const workingHashes = requestedHashes || Object.keys(metadata.entries); - this.logger.info( + this.logger.debug( `Translation requested for ${workingHashes.length} hashes in locale: ${locale}`, ); // Step 2: Check cache first (same for all locales, including source) - this.logger.debug(`[TRACE] Checking cache for locale: ${locale}`); - const cacheStartTime = performance.now(); const cachedTranslations = await this.cache.get(locale); - const cacheEndTime = performance.now(); - this.logger.debug( - `[TRACE] Cache check completed in ${(cacheEndTime - cacheStartTime).toFixed(2)}ms, found ${Object.keys(cachedTranslations).length} entries`, - ); // Step 3: Determine what needs translation/pluralization const uncachedHashes = workingHashes.filter( (hash) => !cachedTranslations[hash], ); this.logger.debug( - `[TRACE] ${uncachedHashes.length} hashes need processing, ${workingHashes.length - uncachedHashes.length} are cached`, + `${uncachedHashes.length} hashes need processing, ${workingHashes.length - uncachedHashes.length} are cached`, ); const cachedCount = workingHashes.length - uncachedHashes.length; if (uncachedHashes.length === 0) { - // All cached! - const endTime = performance.now(); - this.logger.info( - `Cache hit for all ${workingHashes.length} hashes in ${locale} in ${(endTime - startTime).toFixed(2)}ms`, - ); - return { translations: this.pickTranslations(cachedTranslations, workingHashes), errors: [], @@ -196,7 +184,7 @@ export class TranslationService { }; } - this.logger.info( + this.logger.debug( `Generating translations for ${uncachedHashes.length} uncached hashes in ${locale}...`, ); @@ -212,12 +200,12 @@ export class TranslationService { // Step 5: Process pluralization for filtered entries if (this.pluralizationService) { - this.logger.info( + this.logger.debug( `Processing pluralization for ${Object.keys(filteredMetadata.entries).length} entries...`, ); const pluralStats = await this.pluralizationService.process(filteredMetadata); - this.logger.info( + this.logger.debug( `Pluralization stats: ${pluralStats.pluralized} pluralized, ${pluralStats.rejected} rejected, ${pluralStats.failed} failed`, ); } @@ -227,7 +215,7 @@ export class TranslationService { const hashesNeedingTranslation: string[] = []; this.logger.debug( - `[TRACE] Checking for overrides in ${uncachedHashes.length} entries`, + `Checking for overrides in ${uncachedHashes.length} entries`, ); for (const hash of uncachedHashes) { @@ -238,7 +226,7 @@ export class TranslationService { if (entry.overrides && entry.overrides[locale]) { overriddenTranslations[hash] = entry.overrides[locale]; this.logger.debug( - `[TRACE] Using override for ${hash} in locale ${locale}: "${entry.overrides[locale]}"`, + `Using override for ${hash} in locale ${locale}: "${entry.overrides[locale]}"`, ); } else { hashesNeedingTranslation.push(hash); @@ -246,23 +234,12 @@ export class TranslationService { } const overrideCount = Object.keys(overriddenTranslations).length; - if (overrideCount > 0) { - this.logger.info( - `Found ${overrideCount} override(s) for locale ${locale}, skipping AI translation for these entries`, - ); - } // Step 7: Prepare entries for translation (excluding overridden ones) - this.logger.debug( - `[TRACE] Preparing ${hashesNeedingTranslation.length} entries for translation (after overrides)`, - ); const entriesToTranslate = this.prepareEntries( filteredMetadata, hashesNeedingTranslation, ); - this.logger.debug( - `[TRACE] Prepared ${Object.keys(entriesToTranslate).length} entries`, - ); // Step 8: Translate or return source text let newTranslations: Record = { ...overriddenTranslations }; @@ -271,7 +248,7 @@ export class TranslationService { if (locale === this.config.sourceLocale) { // For source locale, just return the (possibly pluralized) sourceText this.logger.debug( - `[TRACE] Source locale detected, returning sourceText for ${hashesNeedingTranslation.length} entries`, + `Source locale detected, returning sourceText for ${hashesNeedingTranslation.length} entries`, ); for (const [hash, entry] of Object.entries(entriesToTranslate)) { newTranslations[hash] = entry.text; @@ -280,30 +257,16 @@ export class TranslationService { // For other locales, translate only entries without overrides try { this.logger.debug( - `[TRACE] Calling translator.translate() for ${locale} with ${Object.keys(entriesToTranslate).length} entries`, + `Translating ${locale} with ${Object.keys(entriesToTranslate).length} entries`, ); - this.logger.debug(`[TRACE] About to await translator.translate()...`); - const translateStartTime = performance.now(); - this.logger.debug(`[TRACE] Executing translator.translate() NOW`); const translatedTexts = await this.translator.translate( locale, entriesToTranslate, ); - this.logger.debug(`[TRACE] translator.translate() returned`); - // Merge translated texts with overridden translations newTranslations = { ...overriddenTranslations, ...translatedTexts }; - - const translateEndTime = performance.now(); - this.logger.debug( - `[TRACE] translator.translate() completed in ${(translateEndTime - translateStartTime).toFixed(2)}ms`, - ); - this.logger.debug( - `[TRACE] Received ${Object.keys(translatedTexts).length} translations (+ ${overrideCount} overrides)`, - ); } catch (error) { - // Complete failure - log and return what we have from cache - this.logger.error(`Translation failed completely:`, error); + this.logger.error(`Translation failed:`, error); return { translations: this.pickTranslations( @@ -336,7 +299,7 @@ export class TranslationService { errors.push({ hash, sourceText: entry?.sourceText || "", - error: "Translation not returned by translator", + error: "Translator doesn't return translation", }); } } @@ -357,7 +320,7 @@ export class TranslationService { const result = this.pickTranslations(allTranslations, workingHashes); const endTime = performance.now(); - this.logger.info( + this.logger.debug( `Translation completed for ${locale}: ${Object.keys(newTranslations).length} new, ${cachedCount} cached, ${errors.length} errors in ${(endTime - startTime).toFixed(2)}ms`, ); From 196b8f86959638f416c5d12dd0114fb292ab3d23 Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Thu, 25 Dec 2025 00:47:16 +0100 Subject: [PATCH 09/10] fix: cleanup logs and unused functions --- .../src/plugin/build-translator.ts | 9 +- packages/new-compiler/src/plugin/next.ts | 6 +- .../src/translators/lingo/model-factory.ts | 20 ++-- .../src/translators/lingo/provider-details.ts | 113 +----------------- .../src/translators/lingo/translator.ts | 34 ++---- .../src/translators/translation-service.ts | 8 +- 6 files changed, 31 insertions(+), 159 deletions(-) diff --git a/packages/new-compiler/src/plugin/build-translator.ts b/packages/new-compiler/src/plugin/build-translator.ts index 740f05654..efb4d9bf9 100644 --- a/packages/new-compiler/src/plugin/build-translator.ts +++ b/packages/new-compiler/src/plugin/build-translator.ts @@ -64,10 +64,6 @@ export async function processBuildTranslations( logger.info(`šŸŒ Build mode: ${buildMode}`); - if (metadataFilePath) { - logger.info(`šŸ“‹ Using build metadata file: ${metadataFilePath}`); - } - const metadata = await loadMetadata(metadataFilePath); if (!metadata || Object.keys(metadata.entries).length === 0) { @@ -172,7 +168,10 @@ export async function processBuildTranslations( stats, }; } catch (error) { - logger.error("āŒ Translation generation failed:", error); + logger.error( + "āŒ Translation generation failed:\n", + error instanceof Error ? error.message : error, + ); process.exit(1); } finally { if (translationServer) { diff --git a/packages/new-compiler/src/plugin/next.ts b/packages/new-compiler/src/plugin/next.ts index dfa45e2fe..a92545694 100644 --- a/packages/new-compiler/src/plugin/next.ts +++ b/packages/new-compiler/src/plugin/next.ts @@ -297,7 +297,6 @@ export async function withLingo( } logger.info("Running post-build translation generation..."); - logger.info(`Build mode: Using metadata file: ${metadataFilePath}`); try { await processBuildTranslations({ @@ -306,7 +305,10 @@ export async function withLingo( metadataFilePath, }); } catch (error) { - logger.error("Translation generation failed:", error); + logger.error( + "Translation generation failed:", + error instanceof Error ? error.message : error, + ); throw error; } }; diff --git a/packages/new-compiler/src/translators/lingo/model-factory.ts b/packages/new-compiler/src/translators/lingo/model-factory.ts index d3d543f45..8bb7e5203 100644 --- a/packages/new-compiler/src/translators/lingo/model-factory.ts +++ b/packages/new-compiler/src/translators/lingo/model-factory.ts @@ -10,6 +10,7 @@ import { createMistral } from "@ai-sdk/mistral"; import type { LanguageModel } from "ai"; import * as dotenv from "dotenv"; import * as path from "path"; +import { formatNoApiKeysError } from "./provider-details"; export type LocaleModel = { provider: string; @@ -169,7 +170,7 @@ export function validateAndGetApiKeys( config: "lingo.dev" | Record, ): ValidatedApiKeys { const keys: ValidatedApiKeys = {}; - const missingKeys: Array<{ provider: string; envVar: string }> = []; + const missingProviders: string[] = []; // Determine which providers are configured let providersToValidate: string[]; @@ -195,7 +196,7 @@ export function validateAndGetApiKeys( if (!providerConfig) { throw new Error( - `āš ļø Unknown provider "${provider}". Supported providers: ${Object.keys(providerDetails).join(", ")}`, + `āš ļø Unknown provider "${provider}". Supported providers: ${Object.keys(providerDetails).join(", ")}`, ); } @@ -208,21 +209,13 @@ export function validateAndGetApiKeys( if (key) { keys[provider] = key; } else { - missingKeys.push({ - provider: providerConfig.name, - envVar: providerConfig.apiKeyEnvVar, - }); + missingProviders.push(provider); } } // If any keys are missing, throw with detailed error - if (missingKeys.length > 0) { - const errorLines = missingKeys.map( - ({ provider, envVar }) => ` - ${provider}: ${envVar}`, - ); - throw new Error( - `āš ļø Missing API keys for configured providers:\n${errorLines.join("\n")}\n\nPlease set the required environment variables.`, - ); + if (missingProviders.length > 0) { + throw new Error(formatNoApiKeysError(missingProviders)); } return keys; @@ -253,6 +246,7 @@ export function createAiModel( ? validatedKeys[model.provider] : undefined; + // TODO (AleksandrSl 25/12/2025): Do we really need to make a second check? Maybe creation should be combined with validation. // Verify key is present for providers that require it if (providerConfig.apiKeyEnvVar && !apiKey) { throw new Error( diff --git a/packages/new-compiler/src/translators/lingo/provider-details.ts b/packages/new-compiler/src/translators/lingo/provider-details.ts index d7005db23..211a51311 100644 --- a/packages/new-compiler/src/translators/lingo/provider-details.ts +++ b/packages/new-compiler/src/translators/lingo/provider-details.ts @@ -55,111 +55,6 @@ export const providerDetails: Record = { }, }; -/** - * Get provider details by ID - */ -export function getProviderDetails(providerId: string): ProviderDetails | null { - return providerDetails[providerId] || null; -} - -/** - * Get all providers that require API keys - */ -export function getProvidersRequiringKeys(): string[] { - return Object.keys(providerDetails).filter( - (id) => providerDetails[id].apiKeyEnvVar !== undefined, - ); -} - -/** - * Format a helpful error message when API key is missing - */ -export function formatMissingApiKeyError(providerId: string): string { - const details = providerDetails[providerId]; - - if (!details) { - return `Unknown provider: ${providerId}`; - } - - if (!details.apiKeyEnvVar) { - // Provider doesn't need API key (like Ollama) - return `Provider ${details.name} doesn't require an API key. Check connection at ${details.getKeyLink}`; - } - - return [ - `āš ļø ${details.name} API key not found.`, - ``, - `To use ${details.name} for translations, you need to set ${details.apiKeyEnvVar}.`, - ``, - `šŸ‘‰ Set the API key:`, - ` 1. Add to .env file: ${details.apiKeyEnvVar}=`, - ` 2. Or export in terminal: export ${details.apiKeyEnvVar}=`, - ``, - `ā­ļø Get your API key:`, - ` ${details.getKeyLink}`, - ``, - `šŸ“š Documentation:`, - ` ${details.docsLink}`, - ].join("\n"); -} - -/** - * Format a helpful error message when API call fails - */ -export function formatApiCallError( - providerId: string, - targetLocale: string, - errorMessage: string, -): string { - const details = providerDetails[providerId]; - - if (!details) { - return `Translation failed with unknown provider "${providerId}": ${errorMessage}`; - } - - // Check for common error patterns - const isInvalidApiKey = - errorMessage.toLowerCase().includes("invalid api key") || - errorMessage.toLowerCase().includes("unauthorized") || - errorMessage.toLowerCase().includes("authentication failed"); - - if (isInvalidApiKey) { - return [ - `āš ļø ${details.name} API key is invalid.`, - ``, - `Error details: ${errorMessage}`, - ``, - `šŸ‘‰ Please check your API key:`, - details.apiKeyEnvVar - ? ` Environment variable: ${details.apiKeyEnvVar}` - : "", - ``, - `ā­ļø Get a new API key:`, - ` ${details.getKeyLink}`, - ``, - `šŸ“š Troubleshooting:`, - ` ${details.docsLink}`, - ] - .filter(Boolean) - .join("\n"); - } - - // Generic API error - return [ - `āš ļø Translation to "${targetLocale}" failed via ${details.name}.`, - ``, - `Error details: ${errorMessage}`, - ``, - `šŸ“š Check ${details.name} documentation for more details:`, - ` ${details.docsLink}`, - ``, - `šŸ’” Tips:`, - ` - Check your API quota/credits`, - ` - Verify the model is available for your account`, - ` - Check ${details.name} status page for outages`, - ].join("\n"); -} - /** * Format error message when API keys are missing for configured providers * @param missingProviders List of providers that are missing API keys @@ -179,14 +74,12 @@ export function formatNoApiKeysError( // Header if (allProviders && allProviders.length > missingProviders.length) { lines.push( - `āš ļø Missing API keys for ${missingProviders.length} of ${allProviders.length} configured providers.`, + `Missing API keys for ${missingProviders.length} of ${allProviders.length} configured providers.`, ); } else { - lines.push(`āš ļø Missing API keys for configured translation providers.`); + lines.push(`Missing API keys for configured translation providers.`); } - lines.push(``); - // List missing providers with their environment variables and links lines.push(`Missing API keys for:`); for (const providerId of missingProviders) { @@ -210,8 +103,6 @@ export function formatNoApiKeysError( ` 1. Add to .env file (recommended)`, ` 2. Or export in terminal: export API_KEY_NAME=`, ``, - `šŸ’” In development mode, the app will auto-fallback to pseudotranslator.`, - ` In production, all configured providers must have valid API keys.`, ); return lines.join("\n"); diff --git a/packages/new-compiler/src/translators/lingo/translator.ts b/packages/new-compiler/src/translators/lingo/translator.ts index d93fd41cf..3450208c0 100644 --- a/packages/new-compiler/src/translators/lingo/translator.ts +++ b/packages/new-compiler/src/translators/lingo/translator.ts @@ -1,20 +1,10 @@ import { generateText } from "ai"; import { LingoDotDevEngine } from "lingo.dev/sdk"; -import { - dictionaryFrom, - type DictionarySchema, - type TranslatableEntry, - type Translator, -} from "../api"; +import { dictionaryFrom, type DictionarySchema, type TranslatableEntry, type Translator, } from "../api"; import { getSystemPrompt } from "./prompt"; import { obj2xml, parseXmlFromResponseText } from "../parse-xml"; import { shots } from "./shots"; -import { - createAiModel, - getLocaleModel, - validateAndGetApiKeys, - type ValidatedApiKeys, -} from "./model-factory"; +import { createAiModel, getLocaleModel, validateAndGetApiKeys, type ValidatedApiKeys, } from "./model-factory"; import { Logger } from "../../utils/logger"; import { DEFAULT_TIMEOUTS, withTimeout } from "../../utils/timeout"; import type { LocaleCode } from "lingo.dev/spec"; @@ -51,7 +41,7 @@ export class LingoTranslator implements Translator { entriesMap: Record, ): Promise> { this.logger.debug( - `[TRACE-LINGO] translate() called for ${locale} with ${Object.keys(entriesMap).length} entries`, + `translate() called for ${locale} with ${Object.keys(entriesMap).length} entries`, ); const sourceDictionary: DictionarySchema = dictionaryFrom( @@ -62,7 +52,7 @@ export class LingoTranslator implements Translator { ); this.logger.debug( - `[TRACE-LINGO] Created source dictionary with ${Object.keys(sourceDictionary.entries).length} entries`, + `Created source dictionary with ${Object.keys(sourceDictionary.entries).length} entries`, ); const translated = await this.translateDictionary(sourceDictionary, locale); @@ -76,18 +66,17 @@ export class LingoTranslator implements Translator { sourceDictionary: DictionarySchema, targetLocale: string, ): Promise { + const chunks = this.chunkDictionary(sourceDictionary); this.logger.debug( - `[TRACE-LINGO] Chunking dictionary with ${Object.keys(sourceDictionary.entries).length} entries`, + `Split dictionary with ${Object.keys(sourceDictionary.entries).length} into ${chunks.length} chunks`, ); - const chunks = this.chunkDictionary(sourceDictionary); - this.logger.debug(`[TRACE-LINGO] Split into ${chunks.length} chunks`); const translatedChunks: DictionarySchema[] = []; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; this.logger.debug( - `[TRACE-LINGO] Translating chunk ${i + 1}/${chunks.length} with ${Object.keys(chunk.entries).length} entries`, + `Translating chunk ${i + 1}/${chunks.length} with ${Object.keys(chunk.entries).length} entries`, ); const chunkStartTime = performance.now(); @@ -95,16 +84,15 @@ export class LingoTranslator implements Translator { const chunkEndTime = performance.now(); this.logger.debug( - `[TRACE-LINGO] Chunk ${i + 1}/${chunks.length} completed in ${(chunkEndTime - chunkStartTime).toFixed(2)}ms`, + `Chunk ${i + 1}/${chunks.length} completed in ${(chunkEndTime - chunkStartTime).toFixed(2)}ms`, ); translatedChunks.push(translatedChunk); } - this.logger.debug(`[TRACE-LINGO] All chunks translated, merging results`); const result = this.mergeDictionaries(translatedChunks); this.logger.debug( - `[TRACE-LINGO] Merge completed, final dictionary has ${Object.keys(result.entries).length} entries`, + `Merge completed, final dictionary has ${Object.keys(result.entries).length} entries`, ); return result; @@ -139,7 +127,7 @@ export class LingoTranslator implements Translator { ); } - this.logger.info( + this.logger.debug( `Using Lingo.dev Engine to localize from "${this.config.sourceLocale}" to "${targetLocale}"`, ); @@ -184,7 +172,7 @@ export class LingoTranslator implements Translator { ); } - this.logger.info( + this.logger.debug( `Using LLM ("${localeModel.provider}":"${localeModel.name}") to translate from "${this.config.sourceLocale}" to "${targetLocale}"`, ); diff --git a/packages/new-compiler/src/translators/translation-service.ts b/packages/new-compiler/src/translators/translation-service.ts index 20416a91a..f90e4a2d9 100644 --- a/packages/new-compiler/src/translators/translation-service.ts +++ b/packages/new-compiler/src/translators/translation-service.ts @@ -117,11 +117,9 @@ export class TranslationService { // Use console.error to ensure visibility in all contexts (loader, server, etc.) const errorMsg = error instanceof Error ? error.message : String(error); - this.logger.warn(`\nāš ļø Translation setup error: ${errorMsg}\n`); - this.logger.warn( - `āš ļø Auto-fallback to pseudotranslator in development mode.\n` + - ` Set the required API keys for real translations.\n`, - ); + this.logger.warn(`āš ļø Translation setup error: \n${errorMsg}\n +āš ļø Auto-fallback to pseudotranslator in development mode. +Set the required API keys for real translations.`); this.translator = new PseudoTranslator( { delayMedian: 100 }, From 688a1c5aef3c390691c49b332dacff04de29f74b Mon Sep 17 00:00:00 2001 From: Aleksandr Slepchenkov Date: Thu, 25 Dec 2025 00:47:39 +0100 Subject: [PATCH 10/10] fix: compiler version in next demo --- demo/new-compiler-next16/package.json | 2 +- pnpm-lock.yaml | 242 +++++++++++++++++++++----- 2 files changed, 196 insertions(+), 48 deletions(-) diff --git a/demo/new-compiler-next16/package.json b/demo/new-compiler-next16/package.json index d5e54fcdc..4b81cadb9 100644 --- a/demo/new-compiler-next16/package.json +++ b/demo/new-compiler-next16/package.json @@ -9,7 +9,7 @@ "lint": "eslint" }, "dependencies": { - "@lingo.dev/compiler": "workspace:^1.0.0-beta", + "@lingo.dev/compiler": "workspace:*", "next": "^16.0.4", "react": "19.2.0", "react-dom": "19.2.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8e95a3dc..32ae6f79a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,7 +58,7 @@ importers: demo/new-compiler-next16: dependencies: '@lingo.dev/compiler': - specifier: workspace:^1.0.0-beta + specifier: workspace:* version: link:../../packages/new-compiler next: specifier: ^16.0.4 @@ -172,7 +172,7 @@ importers: version: link:../../packages/cli next: specifier: 15.3.8 - version: 15.3.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.3.8(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -805,7 +805,7 @@ importers: version: 19.2.7 next: specifier: 15.3.8 - version: 15.3.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.3.8(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tsup: specifier: 8.5.1 version: 8.5.1(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) @@ -864,13 +864,13 @@ importers: dependencies: '@ai-sdk/google': specifier: ^3.0.1 - version: 3.0.1(zod@3.25.76) + version: 3.0.1(zod@4.1.12) '@ai-sdk/groq': specifier: ^3.0.1 - version: 3.0.1(zod@3.25.76) + version: 3.0.1(zod@4.1.12) '@ai-sdk/mistral': specifier: ^3.0.1 - version: 3.0.1(zod@3.25.76) + version: 3.0.1(zod@4.1.12) '@babel/core': specifier: ^7.26.0 version: 7.28.5 @@ -897,13 +897,13 @@ importers: version: 3.1.1 '@openrouter/ai-sdk-provider': specifier: ^1.5.4 - version: 1.5.4(ai@6.0.3(zod@3.25.76))(zod@3.25.76) + version: 1.5.4(ai@6.0.3(zod@4.1.12))(zod@4.1.12) ai: specifier: ^6.0.3 - version: 6.0.3(zod@3.25.76) + version: 6.0.3(zod@4.1.12) ai-sdk-ollama: specifier: ^3.0.0 - version: 3.0.0(ai@6.0.3(zod@3.25.76))(zod@3.25.76) + version: 3.0.0(ai@6.0.3(zod@4.1.12))(zod@4.1.12) dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -915,7 +915,7 @@ importers: version: 11.0.6 lingo.dev: specifier: ^0.117.0 - version: 0.117.21(@types/node@25.0.3)(@types/react@19.2.7)(encoding@0.1.13)(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 0.117.21(@types/node@25.0.3)(@types/react@19.2.7)(encoding@0.1.13)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1010,7 +1010,7 @@ importers: version: 3.0.0 next: specifier: 15.3.8 - version: 15.3.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 15.3.8(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -10407,12 +10407,12 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/gateway@3.0.2(zod@3.25.76)': + '@ai-sdk/gateway@3.0.2(zod@4.1.12)': dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.1(zod@4.1.12) '@vercel/oidc': 3.0.5 - zod: 3.25.76 + zod: 4.1.12 '@ai-sdk/google@1.2.19(zod@3.25.76)': dependencies: @@ -10420,11 +10420,11 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/google@3.0.1(zod@3.25.76)': + '@ai-sdk/google@3.0.1(zod@4.1.12)': dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.1(zod@4.1.12) + zod: 4.1.12 '@ai-sdk/groq@1.2.3(zod@3.25.76)': dependencies: @@ -10432,11 +10432,11 @@ snapshots: '@ai-sdk/provider-utils': 2.2.3(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/groq@3.0.1(zod@3.25.76)': + '@ai-sdk/groq@3.0.1(zod@4.1.12)': dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.1(zod@4.1.12) + zod: 4.1.12 '@ai-sdk/mistral@1.2.8(zod@3.25.76)': dependencies: @@ -10444,11 +10444,11 @@ snapshots: '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) zod: 3.25.76 - '@ai-sdk/mistral@3.0.1(zod@3.25.76)': + '@ai-sdk/mistral@3.0.1(zod@4.1.12)': dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@3.25.76) - zod: 3.25.76 + '@ai-sdk/provider-utils': 4.0.1(zod@4.1.12) + zod: 4.1.12 '@ai-sdk/openai@1.3.22(zod@3.25.76)': dependencies: @@ -10470,12 +10470,12 @@ snapshots: secure-json-parse: 2.7.0 zod: 3.25.76 - '@ai-sdk/provider-utils@4.0.1(zod@3.25.76)': + '@ai-sdk/provider-utils@4.0.1(zod@4.1.12)': dependencies: '@ai-sdk/provider': 3.0.0 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 3.25.76 + zod: 4.1.12 '@ai-sdk/provider@1.1.0': dependencies: @@ -12483,12 +12483,18 @@ snapshots: dependencies: iso-639-3: 3.0.1 - '@lingo.dev/_react@0.7.5(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@lingo.dev/_react@0.7.5(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: js-cookie: 3.0.5 lodash: 4.17.21 next: 16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@lingo.dev/_react@0.7.5(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + dependencies: + js-cookie: 3.0.5 + lodash: 4.17.21 + next: 16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@lingo.dev/_sdk@0.13.4': dependencies: '@lingo.dev/_spec': 0.44.4 @@ -12898,11 +12904,11 @@ snapshots: ai: 4.3.15(react@19.2.3)(zod@3.25.76) zod: 3.25.76 - '@openrouter/ai-sdk-provider@1.5.4(ai@6.0.3(zod@3.25.76))(zod@3.25.76)': + '@openrouter/ai-sdk-provider@1.5.4(ai@6.0.3(zod@4.1.12))(zod@4.1.12)': dependencies: '@openrouter/sdk': 0.1.27 - ai: 6.0.3(zod@3.25.76) - zod: 3.25.76 + ai: 6.0.3(zod@4.1.12) + zod: 4.1.12 '@openrouter/sdk@0.1.27': dependencies: @@ -15088,11 +15094,11 @@ snapshots: agent-base@7.1.4: {} - ai-sdk-ollama@3.0.0(ai@6.0.3(zod@3.25.76))(zod@3.25.76): + ai-sdk-ollama@3.0.0(ai@6.0.3(zod@4.1.12))(zod@4.1.12): dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@3.25.76) - ai: 6.0.3(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.1(zod@4.1.12) + ai: 6.0.3(zod@4.1.12) ollama: 0.6.3 transitivePeerDependencies: - zod @@ -15121,13 +15127,13 @@ snapshots: optionalDependencies: react: 19.2.3 - ai@6.0.3(zod@3.25.76): + ai@6.0.3(zod@4.1.12): dependencies: - '@ai-sdk/gateway': 3.0.2(zod@3.25.76) + '@ai-sdk/gateway': 3.0.2(zod@4.1.12) '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@3.25.76) + '@ai-sdk/provider-utils': 4.0.1(zod@4.1.12) '@opentelemetry/api': 1.9.0 - zod: 3.25.76 + zod: 4.1.12 ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: @@ -16301,7 +16307,7 @@ snapshots: '@next/eslint-plugin-next': 16.0.3 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) @@ -16324,33 +16330,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.39.1(jiti@2.6.1) get-tsconfig: 4.13.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.1(jiti@2.6.1) + eslint: 9.39.2(jiti@2.6.1) get-tsconfig: 4.13.0 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.50.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -16365,13 +16371,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -16415,7 +16421,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -17857,6 +17863,122 @@ snapshots: lines-and-columns@1.2.4: {} + lingo.dev@0.117.21(@types/node@25.0.3)(@types/react@19.2.7)(encoding@0.1.13)(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + dependencies: + '@ai-sdk/anthropic': 1.2.11(zod@3.25.76) + '@ai-sdk/google': 1.2.19(zod@3.25.76) + '@ai-sdk/mistral': 1.2.8(zod@3.25.76) + '@ai-sdk/openai': 1.3.22(zod@3.25.76) + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@biomejs/js-api': 3.0.0(@biomejs/wasm-nodejs@2.3.7) + '@biomejs/wasm-nodejs': 2.3.7 + '@datocms/cma-client-node': 4.0.1 + '@gitbeaker/rest': 39.34.3 + '@inkjs/ui': 2.0.0(ink@4.2.0(@types/react@19.2.7)(react@19.2.3)) + '@inquirer/prompts': 7.8.0(@types/node@25.0.3) + '@lingo.dev/_compiler': 0.8.8(react@19.2.3) + '@lingo.dev/_locales': 0.3.1 + '@lingo.dev/_react': 0.7.5(next@16.1.1(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@lingo.dev/_sdk': 0.13.4 + '@lingo.dev/_spec': 0.44.4 + '@markdoc/markdoc': 0.5.4(@types/react@19.2.7)(react@19.2.3) + '@modelcontextprotocol/sdk': 1.22.0 + '@openrouter/ai-sdk-provider': 0.7.1(ai@4.3.15(react@19.2.3)(zod@3.25.76))(zod@3.25.76) + '@paralleldrive/cuid2': 2.2.2 + '@types/ejs': 3.1.5 + ai: 4.3.15(react@19.2.3)(zod@3.25.76) + bitbucket: 2.12.0(encoding@0.1.13) + chalk: 5.6.2 + chokidar: 4.0.3 + cli-progress: 3.12.0 + cli-table3: 0.6.5 + cors: 2.8.5 + csv-parse: 5.6.0 + csv-stringify: 6.6.0 + date-fns: 4.1.0 + dedent: 1.7.0 + diff: 7.0.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + dotenv: 16.4.7 + ejs: 3.1.10 + express: 5.1.0 + external-editor: 3.1.0 + figlet: 1.9.4 + flat: 6.0.1 + gettext-parser: 8.0.0 + glob: 11.1.0 + gradient-string: 3.0.0 + gray-matter: 4.0.3 + htmlparser2: 10.0.0 + ini: 5.0.0 + ink: 4.2.0(@types/react@19.2.7)(react@19.2.3) + ink-progress-bar: 3.0.0 + ink-spinner: 5.0.0(ink@4.2.0(@types/react@19.2.7)(react@19.2.3))(react@19.2.3) + inquirer: 12.6.0(@types/node@25.0.3) + interactive-commander: 0.5.194(@types/node@25.0.3) + is-url: 1.2.4 + jsdom: 25.0.1 + json5: 2.2.3 + jsonc-parser: 3.3.1 + jsonrepair: 3.13.1 + listr2: 8.3.2 + lodash: 4.17.21 + marked: 15.0.6 + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + node-machine-id: 1.1.12 + node-webvtt: 1.9.4 + object-hash: 3.0.0 + octokit: 4.0.2 + ollama-ai-provider: 1.2.0(zod@3.25.76) + open: 10.2.0 + ora: 8.1.1 + p-limit: 6.2.0 + php-array-reader: 2.1.2 + plist: 3.1.0 + posthog-node: 5.14.0 + prettier: 3.6.2 + react: 19.2.3 + rehype-stringify: 10.0.1 + remark-disable-tokenizers: 1.1.1 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + remark-mdx: 3.1.1 + remark-mdx-frontmatter: 5.2.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + remark-stringify: 11.0.0 + sax: 1.4.3 + srt-parser-2: 1.2.3 + unified: 11.0.5 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + xliff: 6.2.2 + xml2js: 0.6.2 + xpath: 0.0.34 + yaml: 2.8.1 + zod: 3.25.76 + transitivePeerDependencies: + - '@biomejs/wasm-bundler' + - '@biomejs/wasm-web' + - '@cfworker/json-schema' + - '@types/node' + - '@types/react' + - babel-plugin-macros + - bufferutil + - canvas + - encoding + - next + - react-devtools-core + - supports-color + - utf-8-validate + lingo.dev@0.117.21(@types/node@25.0.3)(@types/react@19.2.7)(encoding@0.1.13)(next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: '@ai-sdk/anthropic': 1.2.11(zod@3.25.76) @@ -18654,7 +18776,7 @@ snapshots: negotiator@1.0.0: {} - next@15.3.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@15.3.8(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 15.3.8 '@swc/counter': 0.1.3 @@ -18733,6 +18855,32 @@ snapshots: - '@babel/core' - babel-plugin-macros + next@16.1.1(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@next/env': 16.1.1 + '@swc/helpers': 0.5.15 + baseline-browser-mapping: 2.9.11 + caniuse-lite: 1.0.30001761 + postcss: 8.4.31 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.3) + optionalDependencies: + '@next/swc-darwin-arm64': 16.1.1 + '@next/swc-darwin-x64': 16.1.1 + '@next/swc-linux-arm64-gnu': 16.1.1 + '@next/swc-linux-arm64-musl': 16.1.1 + '@next/swc-linux-x64-gnu': 16.1.1 + '@next/swc-linux-x64-musl': 16.1.1 + '@next/swc-win32-arm64-msvc': 16.1.1 + '@next/swc-win32-x64-msvc': 16.1.1 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.57.0 + sharp: 0.34.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0