From 2afaf8d771ddb4e5a58cb1133f20e1671dc05b5d Mon Sep 17 00:00:00 2001 From: riccio82 Date: Thu, 30 Apr 2026 12:45:16 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat(lara):=20upgrade=20sdk=20a?= =?UTF-8?q?nd=20add=20streaming=20translator=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bump @translated/lara from 1.8.0-beta.3 to ^1.9.0 - introduce LaraTranslator override to stream partial responses and return final chunk - update lockfile to match resolved dependency graph --- package.json | 2 +- public/js/api/laraTranslate/laraTranslate.js | 58 +++++++++++- yarn.lock | 94 ++++++++++---------- 3 files changed, 106 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index fcc154a4fe..90458f9da0 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-popover": "^1.1.15", - "@translated/lara": "1.8.0-beta.3", + "@translated/lara": "^1.9.0", "classnames": "^2.2.6", "crypto-js": "^4.1.1", "diff-match-patch": "^1.0.5", diff --git a/public/js/api/laraTranslate/laraTranslate.js b/public/js/api/laraTranslate/laraTranslate.js index 2655468116..803177bfd3 100644 --- a/public/js/api/laraTranslate/laraTranslate.js +++ b/public/js/api/laraTranslate/laraTranslate.js @@ -1,5 +1,61 @@ import {AuthToken, Translator} from '@translated/lara' +class LaraTranslator extends Translator { + async translate(text, source, target, options, callback) { + const headers = {}; + + if (options?.headers) { + for (const [name, value] of Object.entries(options.headers)) { + headers[name] = value; + } + } + + if (options?.noTrace) { + headers["X-No-Trace"] = "true"; + } + + const response = this.client.postAndGetStream( + "/v2/translate", + { + q: text, + source, + target, + source_hint: options?.sourceHint, + content_type: options?.contentType, + multiline: options?.multiline !== false, + adapt_to: options?.adaptTo, + glossaries: options?.glossaries, + instructions: options?.instructions, + timeout: options?.timeoutInMillis, + priority: options?.priority, + use_cache: options?.useCache, + cache_ttl: options?.cacheTTLSeconds, + verbose: options?.verbose, + style: options?.style, + reasoning: options?.reasoning, + metadata: options?.metadata, + profanities_detect: options?.profanitiesDetect, + profanities_handling: options?.profanitiesHandling, + styleguide_id: options?.styleguideId, + styleguide_reasoning: options?.styleguideReasoning, + styleguide_explanation_language: options?.styleguideExplanationLanguage + }, + undefined, + headers + ); + + let lastResult; + for await (const partial of response) { + if (options?.reasoning && callback) callback(partial); + lastResult = partial; + } + + if (!lastResult) throw new Error("No translation result received."); + + return lastResult; + } +} + export const laraTranslate = async ({ token, source, @@ -13,7 +69,7 @@ export const laraTranslate = async ({ }) => { const credentials = new AuthToken(token, null) - const lara = new Translator(credentials, { + const lara = new LaraTranslator(credentials, { connectionTimeoutMs: 30000, }) let textBlocks = [ diff --git a/yarn.lock b/yarn.lock index 6f545dc734..15317db567 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1708,10 +1708,10 @@ dependencies: "@opentelemetry/semantic-conventions" "^1.29.0" -"@opentelemetry/core@2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.6.1.tgz#a59d22a9ae3be80bb41b280bbbe1fe9fbdb6c2a5" - integrity sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g== +"@opentelemetry/core@2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-2.7.1.tgz#162bfab46d6ff4da1bef240ea52e23a926b0fdbc" + integrity sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw== dependencies: "@opentelemetry/semantic-conventions" "^1.29.0" @@ -1756,11 +1756,11 @@ "@opentelemetry/semantic-conventions" "^1.29.0" "@opentelemetry/resources@^2.2.0": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.6.1.tgz#e1b02772c5f65c0e074d59e4743188f7575e97c7" - integrity sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA== + version "2.7.1" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-2.7.1.tgz#3b2a9179f6119bb1f2cddefe41ba9b2855504a5d" + integrity sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ== dependencies: - "@opentelemetry/core" "2.6.1" + "@opentelemetry/core" "2.7.1" "@opentelemetry/semantic-conventions" "^1.29.0" "@opentelemetry/sdk-logs@0.208.0", "@opentelemetry/sdk-logs@^0.208.0": @@ -1903,15 +1903,17 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@posthog/core@1.25.2": - version "1.25.2" - resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.25.2.tgz#94a7b7ae44739fa945eaf688126f9af3435473d7" - integrity sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg== +"@posthog/core@1.27.9": + version "1.27.9" + resolved "https://registry.yarnpkg.com/@posthog/core/-/core-1.27.9.tgz#6486534530c9650101f6853e1b7e3b2f7431296b" + integrity sha512-7FFWWYWvRFxQqDXYzv8klCjk0Pox1IpuPr61eeOCBsKkmt6xvvHwH0jc3ObvwDXZj2NSAWg+V9N2E2F1ul2CRQ== + dependencies: + "@posthog/types" "1.372.5" -"@posthog/types@1.369.2": - version "1.369.2" - resolved "https://registry.yarnpkg.com/@posthog/types/-/types-1.369.2.tgz#a641437f5de16e61d6325a65faefb964ffc1a0c2" - integrity sha512-PJqkqPCFnnbCZslH2jHSvXlasRqvke6YAsYPhPALy4zy2hldor8A0O2wIlpAefEJ7fVz6wR5ZbRJzQP6nwujyw== +"@posthog/types@1.372.5": + version "1.372.5" + resolved "https://registry.yarnpkg.com/@posthog/types/-/types-1.372.5.tgz#45bfa38f95290c9c7afea78681c77c43031335f5" + integrity sha512-6sYOISiHjfr50FNlFcd8Zw/zCDJzxRCdC7aZzwTCvJABEOLWf41kcsiozi2c3q1cNXYL018X7DAGkUukrNLVIw== "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -1923,10 +1925,10 @@ resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== +"@protobufjs/codegen@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659" + integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" @@ -1946,10 +1948,10 @@ resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== +"@protobufjs/inquire@^1.1.0", "@protobufjs/inquire@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.1.tgz#6cb936f4ac50965230af1e9d0bbfd57ea3675aa4" + integrity sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew== "@protobufjs/path@^1.1.2": version "1.1.2" @@ -1961,10 +1963,10 @@ resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== +"@protobufjs/utf8@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.1.tgz#eaee5900122c110a3dbcb728c0597014a2621774" + integrity sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg== "@radix-ui/primitive@1.1.3": version "1.1.3" @@ -2467,10 +2469,10 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.6.1.tgz#13e09a32d7a8b7060fe38304788ebf4197cd2149" integrity sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== -"@translated/lara@1.8.0-beta.3": - version "1.8.0-beta.3" - resolved "https://registry.yarnpkg.com/@translated/lara/-/lara-1.8.0-beta.3.tgz#6ff1ff03ca878b6a41123bb7b477b859eb3cf5be" - integrity sha512-BfgOFwI8Ffe8QHrzS9u3n6UDQPW10G0KL6mnq1ASrevcRZZ5aHwcco911mnoV3LXgBLWpAPSDJtlMzxSDCyUBA== +"@translated/lara@^1.9.0": + version "1.9.1" + resolved "https://registry.yarnpkg.com/@translated/lara/-/lara-1.9.1.tgz#932fbb21607eb2938851305ef30fbef42a3decb3" + integrity sha512-HhDepQShdLCRSRw2TZ3nn1SGQ1FHc1meTXrbnIQ/rJr2IvfrJ7hjtZFqCLrePN/7lGFGjmT+QEouc8QfAtMk0Q== dependencies: form-data "^4.0.4" @@ -4319,9 +4321,9 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: domelementtype "^2.2.0" dompurify@^3.3.2: - version "3.4.0" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.0.tgz#b1fc33ebdadb373241621e0a30e4ad81573dfd0b" - integrity sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg== + version "3.4.1" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.1.tgz#521d04483ac12631b2aedf434a5f5390933b8789" + integrity sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw== optionalDependencies: "@types/trusted-types" "^2.0.7" @@ -7412,17 +7414,17 @@ postcss@^8.5.10: source-map-js "^1.2.1" posthog-js@^1.57.2: - version "1.369.2" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.369.2.tgz#e11f736f925cf94c827ee97fd9565a861797cec0" - integrity sha512-pY+SvNvRp3C2XW80h/jwVLTgoruK15C6klo9bYYoO6DCK9EbcwS6YzjgxBHx1dIN0XBZM3KWJPmuaSimU65HQQ== + version "1.372.5" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.372.5.tgz#4de62223a56a03f21a246bb088cd119fdbd63290" + integrity sha512-0Wq4yRTX8rg2/SOTo3T/0tt2EIE0usBDJKxWPY6eRTGxWAajNmPWZwK4vREn2ANZGdPhUHQ+hg4kLEUdQnzs/Q== dependencies: "@opentelemetry/api" "^1.9.0" "@opentelemetry/api-logs" "^0.208.0" "@opentelemetry/exporter-logs-otlp-http" "^0.208.0" "@opentelemetry/resources" "^2.2.0" "@opentelemetry/sdk-logs" "^0.208.0" - "@posthog/core" "1.25.2" - "@posthog/types" "1.369.2" + "@posthog/core" "1.27.9" + "@posthog/types" "1.372.5" core-js "^3.38.1" dompurify "^3.3.2" fflate "^0.4.8" @@ -7521,20 +7523,20 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, react-is "^16.13.1" protobufjs@^7.3.0: - version "7.5.5" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.5.tgz#b7089ca4410374c75150baf277353ef76db69f96" - integrity sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg== + version "7.5.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.6.tgz#11af832ebc4b4326f658a5b1308e6141eb57edfd" + integrity sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" + "@protobufjs/codegen" "^2.0.5" "@protobufjs/eventemitter" "^1.1.0" "@protobufjs/fetch" "^1.1.0" "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" + "@protobufjs/inquire" "^1.1.1" "@protobufjs/path" "^1.1.2" "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" + "@protobufjs/utf8" "^1.1.1" "@types/node" ">=13.7.0" long "^5.0.0" From 1be60a5721de11c9a59c5c3fe05d32a9d21409c5 Mon Sep 17 00:00:00 2001 From: Federico Ricciuti Date: Thu, 30 Apr 2026 12:54:16 +0200 Subject: [PATCH 2/3] Update public/js/api/laraTranslate/laraTranslate.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- public/js/api/laraTranslate/laraTranslate.js | 92 ++++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/public/js/api/laraTranslate/laraTranslate.js b/public/js/api/laraTranslate/laraTranslate.js index 803177bfd3..b4db85c535 100644 --- a/public/js/api/laraTranslate/laraTranslate.js +++ b/public/js/api/laraTranslate/laraTranslate.js @@ -2,58 +2,58 @@ import {AuthToken, Translator} from '@translated/lara' class LaraTranslator extends Translator { async translate(text, source, target, options, callback) { - const headers = {}; + const headers = {} - if (options?.headers) { - for (const [name, value] of Object.entries(options.headers)) { - headers[name] = value; - } - } + if (options?.headers) { + for (const [name, value] of Object.entries(options.headers)) { + headers[name] = value + } + } - if (options?.noTrace) { - headers["X-No-Trace"] = "true"; - } + if (options?.noTrace) { + headers['X-No-Trace'] = 'true' + } - const response = this.client.postAndGetStream( - "/v2/translate", - { - q: text, - source, - target, - source_hint: options?.sourceHint, - content_type: options?.contentType, - multiline: options?.multiline !== false, - adapt_to: options?.adaptTo, - glossaries: options?.glossaries, - instructions: options?.instructions, - timeout: options?.timeoutInMillis, - priority: options?.priority, - use_cache: options?.useCache, - cache_ttl: options?.cacheTTLSeconds, - verbose: options?.verbose, - style: options?.style, - reasoning: options?.reasoning, - metadata: options?.metadata, - profanities_detect: options?.profanitiesDetect, - profanities_handling: options?.profanitiesHandling, - styleguide_id: options?.styleguideId, - styleguide_reasoning: options?.styleguideReasoning, - styleguide_explanation_language: options?.styleguideExplanationLanguage - }, - undefined, - headers - ); + const response = this.client.postAndGetStream( + '/v2/translate', + { + q: text, + source, + target, + source_hint: options?.sourceHint, + content_type: options?.contentType, + multiline: options?.multiline !== false, + adapt_to: options?.adaptTo, + glossaries: options?.glossaries, + instructions: options?.instructions, + timeout: options?.timeoutInMillis, + priority: options?.priority, + use_cache: options?.useCache, + cache_ttl: options?.cacheTTLSeconds, + verbose: options?.verbose, + style: options?.style, + reasoning: options?.reasoning, + metadata: options?.metadata, + profanities_detect: options?.profanitiesDetect, + profanities_handling: options?.profanitiesHandling, + styleguide_id: options?.styleguideId, + styleguide_reasoning: options?.styleguideReasoning, + styleguide_explanation_language: options?.styleguideExplanationLanguage, + }, + undefined, + headers, + ) - let lastResult; - for await (const partial of response) { - if (options?.reasoning && callback) callback(partial); - lastResult = partial; - } + let lastResult + for await (const partial of response) { + if (options?.reasoning && callback) callback(partial) + lastResult = partial + } - if (!lastResult) throw new Error("No translation result received."); + if (!lastResult) throw new Error('No translation result received.') - return lastResult; - } + return lastResult + } } export const laraTranslate = async ({ From 98172cd72a24b534959f9de1e46493bbc120c55e Mon Sep 17 00:00:00 2001 From: riccio82 Date: Thu, 30 Apr 2026 12:59:20 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=85=20test(lara):=20add=20unit=20test?= =?UTF-8?q?s=20for=20laraTranslate=20streaming=20translator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mock @translated/lara SDK to verify payload mapping and headers - test context block assembly, option forwarding, and error handling - verify streaming returns last partial result --- .../api/laraTranslate/laraTranslate.test.js | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 public/js/api/laraTranslate/laraTranslate.test.js diff --git a/public/js/api/laraTranslate/laraTranslate.test.js b/public/js/api/laraTranslate/laraTranslate.test.js new file mode 100644 index 0000000000..baa5582b4d --- /dev/null +++ b/public/js/api/laraTranslate/laraTranslate.test.js @@ -0,0 +1,170 @@ +import {laraTranslate} from './laraTranslate' + +const mockPostAndGetStream = jest.fn() + +jest.mock('@translated/lara', () => { + class MockTranslator { + constructor() { + this.client = {postAndGetStream: mockPostAndGetStream} + } + } + return { + AuthToken: jest.fn(), + Translator: MockTranslator, + } +}) + +global.config = { + source_rfc: 'en-US', + target_rfc: 'it-IT', +} + +beforeEach(() => { + mockPostAndGetStream.mockReset() +}) + +async function* asyncIterableOf(...items) { + for (const item of items) { + yield item + } +} + +test('Returns last streamed translation result', async () => { + mockPostAndGetStream.mockReturnValue( + asyncIterableOf({translation: 'partial'}, {translation: 'Ciao mondo'}), + ) + + const result = await laraTranslate({ + token: 'fake-token', + source: 'Hello world', + contextListBefore: [], + contextListAfter: [], + sid: '1', + jobId: '100', + glossaries: [], + style: undefined, + reasoning: false, + }) + + expect(result).toEqual({translation: 'Ciao mondo'}) +}) + +test('Builds text blocks with context before and after', async () => { + mockPostAndGetStream.mockReturnValue( + asyncIterableOf({translation: 'result'}), + ) + + await laraTranslate({ + token: 'fake-token', + source: 'Main segment', + contextListBefore: ['ctx before 1', 'ctx before 2'], + contextListAfter: ['ctx after 1'], + sid: '5', + jobId: '200', + glossaries: [], + style: undefined, + reasoning: false, + }) + + const callArgs = mockPostAndGetStream.mock.calls[0] + const endpoint = callArgs[0] + const payload = callArgs[1] + + expect(endpoint).toBe('/v2/translate') + expect(payload.q).toEqual([ + {text: 'ctx before 1', translatable: false}, + {text: 'ctx before 2', translatable: false}, + {text: 'Main segment', translatable: true}, + {text: 'ctx after 1', translatable: false}, + ]) +}) + +test('Passes correct translation options', async () => { + mockPostAndGetStream.mockReturnValue( + asyncIterableOf({translation: 'result'}), + ) + + await laraTranslate({ + token: 'fake-token', + source: 'Hello', + contextListBefore: [], + contextListAfter: [], + sid: '3', + jobId: '50', + glossaries: [{id: 'g1'}], + style: 'formal', + reasoning: true, + }) + + const callArgs = mockPostAndGetStream.mock.calls[0] + const payload = callArgs[1] + const headers = callArgs[3] + + expect(payload.source).toBe('en-US') + expect(payload.target).toBe('it-IT') + expect(payload.multiline).toBe(false) + expect(payload.content_type).toBe('application/xliff+xml') + expect(payload.glossaries).toEqual([{id: 'g1'}]) + expect(payload.reasoning).toBe(true) + expect(payload.style).toBe('formal') + expect(headers).toEqual({'X-Lara-Engine-Tuid': '50:3'}) +}) + +test('Sets X-Lara-Engine-Tuid header from jobId and sid', async () => { + mockPostAndGetStream.mockReturnValue( + asyncIterableOf({translation: 'result'}), + ) + + await laraTranslate({ + token: 'token', + source: 'text', + contextListBefore: [], + contextListAfter: [], + sid: '42', + jobId: '999', + glossaries: [], + style: undefined, + reasoning: false, + }) + + const headers = mockPostAndGetStream.mock.calls[0][3] + expect(headers).toEqual({'X-Lara-Engine-Tuid': '999:42'}) +}) + +test('Throws error when stream yields no results', async () => { + mockPostAndGetStream.mockReturnValue(asyncIterableOf()) + + await expect( + laraTranslate({ + token: 'token', + source: 'text', + contextListBefore: [], + contextListAfter: [], + sid: '1', + jobId: '1', + glossaries: [], + style: undefined, + reasoning: false, + }), + ).rejects.toThrow('No translation result received.') +}) + +test('Returns final result when multiple partials are streamed with reasoning', async () => { + const partial1 = {translation: 'thinking...', reasoning: 'step 1'} + const partial2 = {translation: 'final answer', reasoning: 'step 2'} + mockPostAndGetStream.mockReturnValue(asyncIterableOf(partial1, partial2)) + + const result = await laraTranslate({ + token: 'token', + source: 'text', + contextListBefore: [], + contextListAfter: [], + sid: '1', + jobId: '1', + glossaries: [], + style: undefined, + reasoning: true, + }) + + expect(result).toEqual(partial2) +})