Skip to content

Commit c77c8c8

Browse files
feat(compiler): move translation service initialization to see the logs (#1705)
* 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. * fix: new ai sdk api compatibility * chore: add changeset * feat: add compiler e2e tests * fix: run tests preparation through turbo * fix: limit installed browsers to chromium and fix trubo config * fix: run build before tests * fix: get rid of noisy logs * fix: cleanup logs and unused functions * fix: compiler version in next demo --------- Co-authored-by: Max Prilutskiy <5614659+maxprilutskiy@users.noreply.github.com>
1 parent f2f71f8 commit c77c8c8

File tree

25 files changed

+317
-504
lines changed

25 files changed

+317
-504
lines changed

.changeset/sharp-yaks-cough.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@lingo.dev/compiler": patch
3+
---
4+
5+
Show logs of the translator initialization to notify about possible problems with LLM keys

.github/workflows/pr-check.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,47 @@ jobs:
7171
- name: Require changeset to be present in PR
7272
if: github.event.pull_request.user.login != 'dependabot[bot]'
7373
run: pnpm changeset status --since origin/main
74+
75+
compiler-e2e:
76+
needs: check
77+
timeout-minutes: 60
78+
runs-on: ubuntu-latest
79+
steps:
80+
- name: Checkout
81+
uses: actions/checkout@v4
82+
with:
83+
ref: ${{github.event.pull_request.head.sha}}
84+
fetch-depth: 0
85+
86+
- name: Use Node.js
87+
uses: actions/setup-node@v6
88+
with:
89+
node-version: "22"
90+
91+
- name: Install pnpm
92+
uses: pnpm/action-setup@v4
93+
with:
94+
version: 9.12.3
95+
96+
- name: Configure pnpm cache
97+
id: pnpm-cache
98+
run: echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
99+
- uses: actions/cache@v3
100+
with:
101+
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
102+
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
103+
restore-keys: |
104+
${{ runner.os }}-pnpm-store-
105+
106+
- name: Install dependencies
107+
run: pnpm install
108+
109+
- name: Install Playwright Browsers
110+
run: pnpm exec playwright install chromium --with-deps
111+
working-directory: packages/new-compiler
112+
113+
- name: Configure Turbo cache
114+
uses: dtinth/setup-github-actions-caching-for-turbo@v1
115+
116+
- name: Run E2E tests
117+
run: pnpm turbo run test:e2e --filter=./packages/new-compiler

packages/new-compiler/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,11 @@
123123
"clean": "rm -rf build",
124124
"test": "vitest --run",
125125
"test:watch": "vitest -w",
126-
"test:prepare": "pnpm build && tsx tests/helpers/prepare-fixtures.ts",
127-
"test:e2e": "playwright test --reporter=list",
128-
"test:e2e:next": "playwright test --grep next --reporter=list",
129-
"test:e2e:vite": "playwright test --grep vite --reporter=list",
130-
"test:e2e:shared": "playwright test tests/e2e/shared --reporter=list",
126+
"test:e2e:prepare": "tsx tests/helpers/prepare-fixtures.ts",
127+
"test:e2e": "playwright test",
128+
"test:e2e:next": "playwright test --grep next",
129+
"test:e2e:vite": "playwright test --grep vite",
130+
"test:e2e:shared": "playwright test tests/e2e/shared",
131131
"test:e2e:headed": "playwright test --headed",
132132
"test:e2e:ui": "playwright test --ui",
133133
"test:e2e:debug": "playwright test --debug",
@@ -186,4 +186,4 @@
186186
"optional": true
187187
}
188188
}
189-
}
189+
}

packages/new-compiler/playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export default defineConfig({
3838
/* Configure projects for major browsers */
3939
projects: [
4040
{
41+
// If we need more than one browser at some point, add them to CI browser installation step too.
4142
name: "chromium",
4243
use: { ...devices["Desktop Chrome"] },
4344
},

packages/new-compiler/src/plugin/build-translator.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,9 @@ import fs from "fs/promises";
1111
import path from "path";
1212
import type { LingoConfig, MetadataSchema } from "../types";
1313
import { logger } from "../utils/logger";
14-
import {
15-
startTranslationServer,
16-
type TranslationServer,
17-
} from "../translation-server";
14+
import { startTranslationServer, type TranslationServer, } from "../translation-server";
1815
import { loadMetadata } from "../metadata/manager";
19-
import { createCache, type TranslationCache } from "../translators";
16+
import { createCache, type TranslationCache, TranslationService, } from "../translators";
2017
import { dictionaryFrom } from "../translators/api";
2118
import type { LocaleCode } from "lingo.dev/spec";
2219

@@ -67,10 +64,6 @@ export async function processBuildTranslations(
6764

6865
logger.info(`🌍 Build mode: ${buildMode}`);
6966

70-
if (metadataFilePath) {
71-
logger.info(`📋 Using build metadata file: ${metadataFilePath}`);
72-
}
73-
7467
const metadata = await loadMetadata(metadataFilePath);
7568

7669
if (!metadata || Object.keys(metadata.entries).length === 0) {
@@ -108,7 +101,7 @@ export async function processBuildTranslations(
108101

109102
try {
110103
translationServer = await startTranslationServer({
111-
startPort: config.dev.translationServerStartPort,
104+
translationService: new TranslationService(config, logger),
112105
onError: (err) => {
113106
logger.error("Translation server error:", err);
114107
},
@@ -175,7 +168,10 @@ export async function processBuildTranslations(
175168
stats,
176169
};
177170
} catch (error) {
178-
logger.error("❌ Translation generation failed:", error);
171+
logger.error(
172+
"❌ Translation generation failed:\n",
173+
error instanceof Error ? error.message : error,
174+
);
179175
process.exit(1);
180176
} finally {
181177
if (translationServer) {

packages/new-compiler/src/plugin/next.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { startOrGetTranslationServer } from "../translation-server/translation-s
1212
import { cleanupExistingMetadata, getMetadataPath } from "../metadata/manager";
1313
import { registerCleanupOnCurrentProcess } from "./cleanup";
1414
import { useI18nRegex } from "./transform/use-i18n";
15+
import { TranslationService } from "../translators";
1516

1617
export type LingoNextPluginOptions = PartialLingoConfig;
1718

@@ -205,14 +206,12 @@ export async function withLingo(
205206
`Initializing Lingo.dev compiler. Is dev mode: ${isDev}. Is main runner: ${isMainRunner()}`,
206207
);
207208

208-
// TODO (AleksandrSl 12/12/2025): Add API keys validation too, so we can log it nicely.
209-
210209
// Try to start up the translation server once.
211210
// 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.
212211
// 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.
213212
if (isDev && !process.env.LINGO_TRANSLATION_SERVER_URL) {
214213
const translationServer = await startOrGetTranslationServer({
215-
startPort: lingoConfig.dev.translationServerStartPort,
214+
translationService: new TranslationService(lingoConfig, logger),
216215
onError: (err) => {
217216
logger.error("Translation server error:", err);
218217
},
@@ -298,7 +297,6 @@ export async function withLingo(
298297
}
299298

300299
logger.info("Running post-build translation generation...");
301-
logger.info(`Build mode: Using metadata file: ${metadataFilePath}`);
302300

303301
try {
304302
await processBuildTranslations({
@@ -307,7 +305,10 @@ export async function withLingo(
307305
metadataFilePath,
308306
});
309307
} catch (error) {
310-
logger.error("Translation generation failed:", error);
308+
logger.error(
309+
"Translation generation failed:",
310+
error instanceof Error ? error.message : error,
311+
);
311312
throw error;
312313
}
313314
};

packages/new-compiler/src/plugin/unplugin.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { processBuildTranslations } from "./build-translator";
2626
import { registerCleanupOnCurrentProcess } from "./cleanup";
2727
import path from "path";
2828
import fs from "fs";
29+
import { TranslationService } from "../translators";
2930

3031
export type LingoPluginOptions = PartialLingoConfig;
3132

@@ -112,7 +113,7 @@ export const lingoUnplugin = createUnplugin<
112113

113114
async function startServer() {
114115
const server = await startTranslationServer({
115-
startPort,
116+
translationService: new TranslationService(config, logger),
116117
onError: (err) => {
117118
logger.error("Translation server error:", err);
118119
},

packages/new-compiler/src/translation-server/cli.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,6 @@ export async function main(): Promise<void> {
444444

445445
// Start server
446446
const { server, url } = await startOrGetTranslationServer({
447-
startPort,
448447
config,
449448
// requestTimeout: cliOpts.timeout || 30000,
450449
onError: (err) => {

packages/new-compiler/src/translation-server/translation-server.ts

Lines changed: 17 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,7 @@ import { URL } from "url";
1717
import { WebSocket, WebSocketServer } from "ws";
1818
import type { MetadataSchema, TranslationMiddlewareConfig } from "../types";
1919
import { getLogger } from "./logger";
20-
import {
21-
createCache,
22-
createTranslator,
23-
TranslationService,
24-
} from "../translators";
20+
import { TranslationService } from "../translators";
2521
import {
2622
createEmptyMetadata,
2723
getMetadataPath,
@@ -33,38 +29,21 @@ import type { LocaleCode } from "lingo.dev/spec";
3329
import { parseLocaleOrThrow } from "../utils/is-valid-locale";
3430

3531
export interface TranslationServerOptions {
36-
/**
37-
* Starting port to try (will find next available if taken)
38-
* @default 3456
39-
*/
40-
startPort?: number;
41-
42-
/**
43-
* Configuration for translation generation
44-
*/
4532
config: TranslationMiddlewareConfig;
46-
47-
/**
48-
* Callback when server is ready
49-
*/
33+
translationService?: TranslationService;
5034
onReady?: (port: number) => void;
51-
52-
/**
53-
* Callback on error
54-
*/
5535
onError?: (error: Error) => void;
5636
}
5737

5838
export class TranslationServer {
5939
private server: http.Server | null = null;
6040
private url: string | undefined = undefined;
6141
private logger;
62-
private config: TranslationMiddlewareConfig;
63-
private configHash: string;
64-
private startPort: number;
65-
private onReadyCallback?: (port: number) => void;
66-
private onErrorCallback?: (error: Error) => void;
67-
private translationService: TranslationService | null = null;
42+
private readonly config: TranslationMiddlewareConfig;
43+
private readonly configHash: string;
44+
private readonly startPort: number;
45+
private readonly onReadyCallback?: (port: number) => void;
46+
private readonly onErrorCallback?: (error: Error) => void;
6847
private metadata: MetadataSchema | null = null;
6948
private connections: Set<Socket> = new Set();
7049
private wss: WebSocketServer | null = null;
@@ -75,11 +54,16 @@ export class TranslationServer {
7554
private isBusy = false;
7655
private busyTimeout: NodeJS.Timeout | null = null;
7756
private readonly BUSY_DEBOUNCE_MS = 500; // Time after last translation to send "idle" event
57+
private readonly translationService: TranslationService;
7858

7959
constructor(options: TranslationServerOptions) {
8060
this.config = options.config;
8161
this.configHash = hashConfig(options.config);
82-
this.startPort = options.startPort || 60000;
62+
this.translationService =
63+
options.translationService ??
64+
// Fallback is for CLI start only.
65+
new TranslationService(options.config, getLogger(options.config));
66+
this.startPort = options.config.dev.translationServerStartPort;
8367
this.onReadyCallback = options.onReady;
8468
this.onErrorCallback = options.onError;
8569
this.logger = getLogger(this.config);
@@ -95,19 +79,6 @@ export class TranslationServer {
9579

9680
this.logger.info(`🔧 Initializing translator...`);
9781

98-
const translator = createTranslator(this.config, this.logger);
99-
const cache = createCache(this.config);
100-
101-
this.translationService = new TranslationService(
102-
translator,
103-
cache,
104-
{
105-
sourceLocale: this.config.sourceLocale,
106-
pluralization: this.config.pluralization,
107-
},
108-
this.logger,
109-
);
110-
11182
const port = await this.findAvailablePort(this.startPort);
11283

11384
return new Promise((resolve, reject) => {
@@ -281,14 +252,13 @@ export class TranslationServer {
281252
* Start a new server or get the URL of an existing one on the preferred port.
282253
*
283254
* This method optimizes for the common case where a translation server is already
284-
* running on port 60000. If that port is taken, it checks if it's our service
255+
* running on a preferred port. If that port is taken, it checks if it's our service
285256
* by calling the health check endpoint. If it is, we reuse it instead of starting
286257
* a new server on a different port.
287258
*
288259
* @returns URL of the running server (new or existing)
289260
*/
290261
async startOrGetUrl(): Promise<string> {
291-
// If this instance already has a server running, return its URL
292262
if (this.server && this.url) {
293263
this.logger.info(`Using existing server instance at ${this.url}`);
294264
return this.url;
@@ -527,7 +497,6 @@ export class TranslationServer {
527497

528498
res.on("end", () => {
529499
try {
530-
// Check if response is valid and has the expected structure
531500
if (res.statusCode === 200) {
532501
const json = JSON.parse(data);
533502
// Our translation server returns { status: "ok", port: ..., configHash: ... }
@@ -680,11 +649,6 @@ export class TranslationServer {
680649
);
681650
return;
682651
}
683-
684-
if (!this.translationService) {
685-
throw new Error("Translation service not initialized");
686-
}
687-
688652
// Reload metadata to ensure we have the latest entries
689653
// (new entries may have been added since server started)
690654
await this.reloadMetadata();
@@ -747,10 +711,6 @@ export class TranslationServer {
747711
try {
748712
const parsedLocale = parseLocaleOrThrow(locale);
749713

750-
if (!this.translationService) {
751-
throw new Error("Translation service not initialized");
752-
}
753-
754714
// Reload metadata to ensure we have the latest entries
755715
// (new entries may have been added since server started)
756716
await this.reloadMetadata();
@@ -842,9 +802,6 @@ export function hashConfig(config: Record<string, SerializableValue>): string {
842802
return crypto.createHash("md5").update(serialized).digest("hex").slice(0, 12);
843803
}
844804

845-
/**
846-
* Create and start a translation server
847-
*/
848805
export async function startTranslationServer(
849806
options: TranslationServerOptions,
850807
): Promise<TranslationServer> {
@@ -856,10 +813,10 @@ export async function startTranslationServer(
856813
/**
857814
* Create a translation server and start it or reuse an existing one on the preferred port
858815
*
859-
* Since we have little control over the dev server start in next, we can start the translation server only in the loader,
860-
* and loaders could be started from multiple processes (it seems) or similar we need a way to avoid starting multiple servers.
816+
* 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,
817+
* they both could be run in different processes, and we need a way to avoid starting multiple servers.
861818
* This one will try to start a server on the preferred port (which seems to be an atomic operation), and if it fails,
862-
* it checks if the server already started is ours and returns its url.
819+
* it checks if the server that is already started is ours and returns its url.
863820
*
864821
* @returns Object containing the server instance and its URL
865822
*/

packages/new-compiler/src/translators/cache-factory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { LocalTranslationCache } from "./local-cache";
88
import { logger } from "../utils/logger";
99
import { getCacheDir } from "../utils/path-helpers";
1010

11+
export type CacheConfig = Pick<LingoConfig, "cacheType"> & PathConfig;
12+
1113
/**
1214
* Create a cache instance based on the config
1315
*
@@ -21,7 +23,7 @@ import { getCacheDir } from "../utils/path-helpers";
2123
* ```
2224
*/
2325
export function createCache(
24-
config: Pick<LingoConfig, "cacheType"> & PathConfig,
26+
config: CacheConfig,
2527
): TranslationCache {
2628
switch (config.cacheType) {
2729
case "local":

0 commit comments

Comments
 (0)