From d791a9b35079be5d25acfcd214ab0917bdd35dfe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 20 Feb 2025 10:35:27 -0600 Subject: [PATCH] Fix DeepL Response parsing, mock DeepL tests so they can always run without API keys --- packages/translators/DeepLTranslator.test.ts | 69 ++++++++++++++------ packages/translators/DeepLTranslator.ts | 17 ++++- packages/translators/schema.test.ts | 7 ++ 3 files changed, 70 insertions(+), 23 deletions(-) create mode 100644 packages/translators/schema.test.ts diff --git a/packages/translators/DeepLTranslator.test.ts b/packages/translators/DeepLTranslator.test.ts index ae1565c9..8e37e44b 100644 --- a/packages/translators/DeepLTranslator.test.ts +++ b/packages/translators/DeepLTranslator.test.ts @@ -1,21 +1,20 @@ -import { DittoConf } from '@ditto/conf'; import { detectLanguage } from '@ditto/lang'; import { assert, assertEquals } from '@std/assert'; import { DeepLTranslator } from './DeepLTranslator.ts'; -const { - deeplBaseUrl: baseUrl, - deeplApiKey: apiKey, - translationProvider, -} = new DittoConf(Deno.env); - -const deepl = 'deepl'; - -Deno.test('DeepL translation with source language omitted', { - ignore: !(translationProvider === deepl && apiKey), -}, async () => { - const translator = new DeepLTranslator({ fetch: fetch, baseUrl, apiKey: apiKey! }); +Deno.test('DeepL translation with source language omitted', async () => { + const translator = mockDeepL({ + translations: [ + { detected_source_language: 'PT', text: 'Good morning friends' }, + { detected_source_language: 'PT', text: 'My name is Patrick' }, + { + detected_source_language: 'PT', + text: + 'I will live in America, I promise. But first, I should mention that lande is interpreting this text as Italian, how strange.', + }, + ], + }); const data = await translator.translate( [ @@ -33,10 +32,18 @@ Deno.test('DeepL translation with source language omitted', { assertEquals(detectLanguage(data.results[2], 0), 'en'); }); -Deno.test('DeepL translation with source language set', { - ignore: !(translationProvider === deepl && apiKey), -}, async () => { - const translator = new DeepLTranslator({ fetch: fetch, baseUrl, apiKey: apiKey as string }); +Deno.test('DeepL translation with source language set', async () => { + const translator = mockDeepL({ + translations: [ + { detected_source_language: 'PT', text: 'Good morning friends' }, + { detected_source_language: 'PT', text: 'My name is Patrick' }, + { + detected_source_language: 'PT', + text: + 'I will live in America, I promise. But first, I should mention that lande is interpreting this text as Italian, how strange.', + }, + ], + }); const data = await translator.translate( [ @@ -54,10 +61,16 @@ Deno.test('DeepL translation with source language set', { assertEquals(detectLanguage(data.results[2], 0), 'en'); }); -Deno.test("DeepL translation doesn't alter Nostr URIs", { - ignore: !(translationProvider === deepl && apiKey), -}, async () => { - const translator = new DeepLTranslator({ fetch: fetch, baseUrl, apiKey: apiKey as string }); +Deno.test("DeepL translation doesn't alter Nostr URIs", async () => { + const translator = mockDeepL({ + translations: [ + { + detected_source_language: 'EN', + text: + 'Graças ao trabalho de nostr:nprofile1qy2hwumn8ghj7erfw36x7tnsw43z7un9d3shjqpqgujeqakgt7fyp6zjggxhyy7ft623qtcaay5lkc8n8gkry4cvnrzqep59se e nostr:nprofile1qy2hwumn8ghj7erfw36x7tnsw43z7un9d3shjqpqe6tnvlr46lv3lwdu80r07kanhk6jcxy5r07w9umgv9kuhu9dl5hsz44l8s , agora é possível filtrar o feed global por idioma no #Ditto!', + }, + ], + }); const patrick = 'nostr:nprofile1qy2hwumn8ghj7erfw36x7tnsw43z7un9d3shjqpqgujeqakgt7fyp6zjggxhyy7ft623qtcaay5lkc8n8gkry4cvnrzqep59se'; @@ -72,3 +85,17 @@ Deno.test("DeepL translation doesn't alter Nostr URIs", { assert(output.includes(patrick)); assert(output.includes(danidfra)); }); + +interface DeepLResponse { + translations: { + detected_source_language: string; + text: string; + }[]; +} + +function mockDeepL(json: DeepLResponse): DeepLTranslator { + return new DeepLTranslator({ + apiKey: 'deepl', + fetch: () => Promise.resolve(new Response(JSON.stringify(json))), + }); +} diff --git a/packages/translators/DeepLTranslator.ts b/packages/translators/DeepLTranslator.ts index f4b6f918..93da8ad7 100644 --- a/packages/translators/DeepLTranslator.ts +++ b/packages/translators/DeepLTranslator.ts @@ -72,7 +72,13 @@ export class DeepLTranslator implements DittoTranslator { const json = await response.json(); if (!response.ok) { - throw new Error(json['message']); + const result = DeepLTranslator.errorSchema().safeParse(json); + + if (result.success) { + throw new Error(result.data.message); + } else { + throw new Error(`Unexpected DeepL error: ${response.statusText} (${response.status})`); + } } return DeepLTranslator.schema().parse(json); @@ -84,10 +90,17 @@ export class DeepLTranslator implements DittoTranslator { return z.object({ translations: z.array( z.object({ - detected_source_language: languageSchema, + detected_source_language: z.string().transform((val) => val.toLowerCase()).pipe(languageSchema), text: z.string(), }), ), }); } + + /** DeepL error response schema. */ + private static errorSchema() { + return z.object({ + message: z.string(), + }); + } } diff --git a/packages/translators/schema.test.ts b/packages/translators/schema.test.ts new file mode 100644 index 00000000..6d37992c --- /dev/null +++ b/packages/translators/schema.test.ts @@ -0,0 +1,7 @@ +import { assertEquals } from '@std/assert'; + +import { languageSchema } from './schema.ts'; + +Deno.test('languageSchema', () => { + assertEquals(languageSchema.safeParse('en').success, true); +});