Skip to content

Commit bb3ec79

Browse files
authored
feat(sdk): add vNext API support to LingoDotDevEngine (#2027)
1 parent a6aa9f2 commit bb3ec79

File tree

4 files changed

+266
-203
lines changed

4 files changed

+266
-203
lines changed

.changeset/bold-bushes-relax.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"lingo.dev": patch
3+
"@lingo.dev/_sdk": patch
4+
---
5+
6+
Add vNext API support to LingoDotDevEngine. When engineId is provided, the engine routes requests to the vNext API. Refactor vNext localizer to use LingoDotDevEngine from SDK instead of custom inline implementation

packages/cli/src/cli/localizer/lingodotdev-vnext.ts

Lines changed: 4 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -2,185 +2,8 @@ import dedent from "dedent";
22
import { ILocalizer, LocalizerData } from "./_types";
33
import chalk from "chalk";
44
import { colors } from "../constants";
5+
import { LingoDotDevEngine } from "@lingo.dev/_sdk";
56
import { getSettings } from "../utils/settings";
6-
import { createId } from "@paralleldrive/cuid2";
7-
8-
/**
9-
* Creates a custom engine for Lingo.dev vNext that sends requests to:
10-
* https://api.lingo.dev/process/<processId>/localize
11-
*/
12-
function createVNextEngine(config: {
13-
apiKey: string;
14-
apiUrl: string;
15-
processId: string;
16-
sessionId: string;
17-
triggerType: "cli" | "ci";
18-
}) {
19-
const endpoint = `${config.apiUrl}/process/${config.processId}/localize`;
20-
21-
return {
22-
async localizeChunk(
23-
sourceLocale: string | null,
24-
targetLocale: string,
25-
payload: {
26-
data: Record<string, any>;
27-
reference?: Record<string, Record<string, any>>;
28-
hints?: Record<string, string[]>;
29-
},
30-
fast: boolean,
31-
filePath?: string,
32-
signal?: AbortSignal,
33-
): Promise<Record<string, string>> {
34-
const res = await fetch(endpoint, {
35-
method: "POST",
36-
headers: {
37-
"Content-Type": "application/json; charset=utf-8",
38-
"X-API-Key": config.apiKey,
39-
},
40-
body: JSON.stringify(
41-
{
42-
params: { fast },
43-
sourceLocale,
44-
targetLocale,
45-
data: payload.data,
46-
reference: payload.reference,
47-
hints: payload.hints,
48-
sessionId: config.sessionId,
49-
triggerType: config.triggerType,
50-
metadata: filePath ? { filePath } : undefined,
51-
},
52-
null,
53-
2,
54-
),
55-
signal,
56-
});
57-
58-
if (!res.ok) {
59-
if (res.status >= 500 && res.status < 600) {
60-
const errorText = await res.text();
61-
throw new Error(
62-
`Server error (${res.status}): ${res.statusText}. ${errorText}. This may be due to temporary service issues.`,
63-
);
64-
} else if (res.status === 400) {
65-
throw new Error(`Invalid request: ${res.statusText}`);
66-
} else {
67-
const errorText = await res.text();
68-
throw new Error(errorText);
69-
}
70-
}
71-
72-
const jsonResponse = await res.json();
73-
74-
if (!jsonResponse.data && jsonResponse.error) {
75-
throw new Error(jsonResponse.error);
76-
}
77-
78-
return jsonResponse.data || {};
79-
},
80-
81-
async whoami(
82-
signal?: AbortSignal,
83-
): Promise<{ email: string; id: string } | null> {
84-
// vNext uses a simple response for whoami
85-
return { email: "vnext-user", id: config.processId };
86-
},
87-
88-
async localizeObject(
89-
obj: Record<string, any>,
90-
params: {
91-
sourceLocale: string | null;
92-
targetLocale: string;
93-
fast?: boolean;
94-
reference?: Record<string, Record<string, any>>;
95-
hints?: Record<string, string[]>;
96-
filePath?: string;
97-
},
98-
progressCallback?: (
99-
progress: number,
100-
sourceChunk: Record<string, string>,
101-
processedChunk: Record<string, string>,
102-
) => void,
103-
signal?: AbortSignal,
104-
): Promise<Record<string, string>> {
105-
const chunkedPayload = extractPayloadChunks(obj);
106-
const processedPayloadChunks: Record<string, string>[] = [];
107-
108-
for (let i = 0; i < chunkedPayload.length; i++) {
109-
const chunk = chunkedPayload[i];
110-
const percentageCompleted = Math.round(
111-
((i + 1) / chunkedPayload.length) * 100,
112-
);
113-
114-
const processedPayloadChunk = await this.localizeChunk(
115-
params.sourceLocale,
116-
params.targetLocale,
117-
{ data: chunk, reference: params.reference, hints: params.hints },
118-
params.fast || false,
119-
params.filePath,
120-
signal,
121-
);
122-
123-
if (progressCallback) {
124-
progressCallback(percentageCompleted, chunk, processedPayloadChunk);
125-
}
126-
127-
processedPayloadChunks.push(processedPayloadChunk);
128-
}
129-
130-
return Object.assign({}, ...processedPayloadChunks);
131-
},
132-
};
133-
}
134-
135-
/**
136-
* Helper functions for chunking payloads
137-
*/
138-
function extractPayloadChunks(
139-
payload: Record<string, string>,
140-
batchSize: number = 25,
141-
idealBatchItemSize: number = 250,
142-
): Record<string, string>[] {
143-
const result: Record<string, string>[] = [];
144-
let currentChunk: Record<string, string> = {};
145-
let currentChunkItemCount = 0;
146-
147-
const payloadEntries = Object.entries(payload);
148-
for (let i = 0; i < payloadEntries.length; i++) {
149-
const [key, value] = payloadEntries[i];
150-
currentChunk[key] = value;
151-
currentChunkItemCount++;
152-
153-
const currentChunkSize = countWordsInRecord(currentChunk);
154-
if (
155-
currentChunkSize > idealBatchItemSize ||
156-
currentChunkItemCount >= batchSize ||
157-
i === payloadEntries.length - 1
158-
) {
159-
result.push(currentChunk);
160-
currentChunk = {};
161-
currentChunkItemCount = 0;
162-
}
163-
}
164-
165-
return result;
166-
}
167-
168-
function countWordsInRecord(
169-
payload: any | Record<string, any> | Array<any>,
170-
): number {
171-
if (Array.isArray(payload)) {
172-
return payload.reduce((acc, item) => acc + countWordsInRecord(item), 0);
173-
} else if (typeof payload === "object" && payload !== null) {
174-
return Object.values(payload).reduce(
175-
(acc: number, item) => acc + countWordsInRecord(item),
176-
0,
177-
);
178-
} else if (typeof payload === "string") {
179-
return payload.trim().split(/\s+/).filter(Boolean).length;
180-
} else {
181-
return 0;
182-
}
183-
}
1847

1858
export default function createLingoDotDevVNextLocalizer(
1869
processId: string,
@@ -207,15 +30,12 @@ export default function createLingoDotDevVNextLocalizer(
20730
// Use LINGO_API_URL from environment or default to api.lingo.dev
20831
const apiUrl = process.env.LINGO_API_URL || "https://api.lingo.dev";
20932

210-
const sessionId = createId();
21133
const triggerType = process.env.CI ? "ci" : "cli";
21234

213-
const engine = createVNextEngine({
35+
const engine = new LingoDotDevEngine({
21436
apiKey,
21537
apiUrl,
216-
processId,
217-
sessionId,
218-
triggerType,
38+
engineId: processId,
21939
});
22040

22141
return {
@@ -250,6 +70,7 @@ export default function createLingoDotDevVNextLocalizer(
25070
},
25171
hints: input.hints,
25272
filePath: input.filePath,
73+
triggerType,
25374
},
25475
onProgress,
25576
);

0 commit comments

Comments
 (0)