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/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index ed47026af..03edca0c6 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -71,3 +71,47 @@ 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: + needs: check + 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 chromium --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/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/packages/new-compiler/package.json b/packages/new-compiler/package.json index e6204c8fa..fada752df 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": { @@ -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", @@ -186,4 +186,4 @@ "optional": true } } -} \ No newline at end of file +} 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/packages/new-compiler/src/plugin/build-translator.ts b/packages/new-compiler/src/plugin/build-translator.ts index 379600f6d..efb4d9bf9 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"; @@ -67,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) { @@ -108,7 +101,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); }, @@ -175,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 b1c3a7f72..a92545694 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); }, @@ -298,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({ @@ -307,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/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/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/service.ts b/packages/new-compiler/src/translators/lingo/translator.ts similarity index 86% rename from packages/new-compiler/src/translators/lingo/service.ts rename to packages/new-compiler/src/translators/lingo/translator.ts index e77e99d6f..3450208c0 100644 --- a/packages/new-compiler/src/translators/lingo/service.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"; @@ -31,7 +21,7 @@ export interface LingoTranslatorConfig { /** * Lingo translator using AI models */ -export class Service implements Translator { +export class LingoTranslator implements Translator { private readonly validatedKeys: ValidatedApiKeys; constructor( @@ -51,7 +41,7 @@ export class Service 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 Service 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 Service 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 Service 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 Service 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 Service 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/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..c2064df50 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}`, + this.logger.debug( + `Initialized pluralization service with ${localeModel.provider}:${localeModel.name}`, ); } @@ -76,26 +72,27 @@ 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; } - this.logger.info( + this.logger.debug( `Processing ${uncachedCandidates.length} candidates (${candidates.length - uncachedCandidates.length} cached)`, ); @@ -103,7 +100,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 +164,7 @@ export class PluralizationService { ], }), DEFAULT_TIMEOUTS.AI_API * 2, // Double timeout for batch - `Pluralization with ${this.modelName}`, + `Pluralization with ${this.languageModel}`, ); const responseText = response.text.trim(); @@ -214,7 +211,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 +263,7 @@ export class PluralizationService { }; } - this.logger.info( + this.logger.debug( `Starting pluralization processing for ${totalEntries} entries`, ); @@ -280,7 +277,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 +349,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 +359,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 +372,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..f90e4a2d9 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,73 @@ 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(`āš ļø 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 }, + this.logger, + ); + this.cache = new MemoryTranslationCache(); + } else { + // 4. Fail in production + throw error; + } + } } } @@ -99,38 +152,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 = this.useCache - ? 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`, - ); + const cachedTranslations = await this.cache.get(locale); // 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: [], @@ -143,7 +182,7 @@ export class TranslationService { }; } - this.logger.info( + this.logger.debug( `Generating translations for ${uncachedHashes.length} uncached hashes in ${locale}...`, ); @@ -159,12 +198,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`, ); } @@ -174,7 +213,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) { @@ -185,7 +224,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); @@ -193,23 +232,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 }; @@ -218,7 +246,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; @@ -227,30 +255,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( @@ -283,27 +297,16 @@ export class TranslationService { errors.push({ hash, sourceText: entry?.sourceText || "", - error: "Translation not returned by translator", + error: "Translator doesn't return translation", }); } } } // 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 @@ -315,7 +318,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`, ); 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; - } -} 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/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 diff --git a/turbo.json b/turbo.json index 4082f08dd..d51528c77 100644 --- a/turbo.json +++ b/turbo.json @@ -17,6 +17,16 @@ "^build" ] }, + "test:e2e:prepare": { + "dependsOn": [ + "build" + ] + }, + "test:e2e": { + "dependsOn": [ + "test:e2e:prepare" + ] + }, "deploy": { "dependsOn": [ "build", @@ -25,4 +35,4 @@ ] } } -} \ No newline at end of file +}