Skip to content

Commit c225794

Browse files
committed
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.
1 parent b2d335b commit c225794

File tree

14 files changed

+185
-254
lines changed

14 files changed

+185
-254
lines changed

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

Lines changed: 3 additions & 6 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

@@ -108,7 +105,7 @@ export async function processBuildTranslations(
108105

109106
try {
110107
translationServer = await startTranslationServer({
111-
startPort: config.dev.translationServerStartPort,
108+
translationService: new TranslationService(config, logger),
112109
onError: (err) => {
113110
logger.error("Translation server error:", err);
114111
},

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

Lines changed: 2 additions & 3 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
},

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":

packages/new-compiler/src/translators/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ export type { Translator, TranslatableEntry } from "./api";
99

1010
// Translators
1111
export { PseudoTranslator } from "./pseudotranslator";
12-
export { Service } from "./lingo";
12+
export { LingoTranslator } from "./lingo";
1313
export type { LingoTranslatorConfig } from "./lingo";
14-
export { createTranslator } from "./translator-factory";
1514

1615
// Translation Service (orchestrator)
1716
export { TranslationService } from "./translation-service";

packages/new-compiler/src/translators/lingo/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
* Real AI-powered translation using various LLM providers
55
*/
66

7-
export { Service } from "./service";
8-
export type { LingoTranslatorConfig } from "./service";
7+
export { LingoTranslator } from "./translator";
8+
export type { LingoTranslatorConfig } from "./translator";

packages/new-compiler/src/translators/lingo/service.ts renamed to packages/new-compiler/src/translators/lingo/translator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface LingoTranslatorConfig {
3131
/**
3232
* Lingo translator using AI models
3333
*/
34-
export class Service implements Translator<LingoTranslatorConfig> {
34+
export class LingoTranslator implements Translator<LingoTranslatorConfig> {
3535
private readonly validatedKeys: ValidatedApiKeys;
3636

3737
constructor(
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { TranslationCache } from "./cache";
2+
import type { LocaleCode } from "lingo.dev/spec";
3+
4+
/**
5+
* In memory translation cache implementation
6+
*/
7+
export class MemoryTranslationCache implements TranslationCache {
8+
private cache: Map<LocaleCode, Map<string, string>> = new Map();
9+
10+
constructor() {}
11+
12+
async get(
13+
locale: LocaleCode,
14+
hashes?: string[],
15+
): Promise<Record<string, string>> {
16+
const localeCache = this.cache.get(locale);
17+
if (!localeCache) {
18+
return {};
19+
}
20+
if (hashes) {
21+
return hashes.reduce(
22+
(acc, hash) => ({ ...acc, [hash]: localeCache.get(hash) }),
23+
{},
24+
);
25+
}
26+
return Object.fromEntries(localeCache);
27+
}
28+
29+
/**
30+
* Update cache with new translations (merge)
31+
*/
32+
async update(
33+
locale: LocaleCode,
34+
translations: Record<string, string>,
35+
): Promise<void> {
36+
let localeCache = this.cache.get(locale);
37+
if (!localeCache) {
38+
localeCache = new Map();
39+
this.cache.set(locale, localeCache);
40+
}
41+
for (const [key, value] of Object.entries(translations)) {
42+
localeCache.set(key, value);
43+
}
44+
}
45+
46+
/**
47+
* Replace entire cache for a locale
48+
*/
49+
async set(
50+
locale: LocaleCode,
51+
translations: Record<string, string>,
52+
): Promise<void> {
53+
this.cache.set(locale, new Map(Object.entries(translations)));
54+
}
55+
56+
async has(locale: LocaleCode): Promise<boolean> {
57+
return this.cache.has(locale);
58+
}
59+
60+
async clear(locale: LocaleCode): Promise<void> {
61+
this.cache.delete(locale);
62+
}
63+
64+
async clearAll(): Promise<void> {
65+
this.cache.clear();
66+
}
67+
}

0 commit comments

Comments
 (0)