diff --git a/plugins/deepl/credentials.ts b/plugins/deepl/credentials.ts new file mode 100644 index 0000000..dab0cdf --- /dev/null +++ b/plugins/deepl/credentials.ts @@ -0,0 +1,3 @@ +export type DeepLCredentials = { + DEEPL_API_KEY?: string; +}; diff --git a/plugins/deepl/icon.tsx b/plugins/deepl/icon.tsx new file mode 100644 index 0000000..2eef759 --- /dev/null +++ b/plugins/deepl/icon.tsx @@ -0,0 +1,15 @@ + +export function DeepLIcon({ className }: { className?: string }) { + return ( + + DeepL + + + ); +} diff --git a/plugins/deepl/index.ts b/plugins/deepl/index.ts new file mode 100644 index 0000000..f264054 --- /dev/null +++ b/plugins/deepl/index.ts @@ -0,0 +1,133 @@ +import type { IntegrationPlugin } from "../registry"; +import { registerIntegration } from "../registry"; +import { DeepLIcon } from "./icon"; + +const deepLPlugin: IntegrationPlugin = { + type: "deepl", + label: "DeepL", + description: "Translate text with DeepL", + + icon: DeepLIcon, + + formFields: [ + { + id: "apiKey", + label: "API Key", + type: "password", + placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:fx", + configKey: "apiKey", + envVar: "DEEPL_API_KEY", + helpText: "Get your API key from ", + helpLink: { + text: "DeepL Account", + url: "https://www.deepl.com/account/summary", + }, + }, + ], + + testConfig: { + getTestFunction: async () => { + const { testDeepL } = await import("./test"); + return testDeepL; + }, + }, + + actions: [ + { + slug: "translate-text", + label: "Translate Text", + description: "Translate text from one language to another", + category: "DeepL", + stepFunction: "translateTextStep", + stepImportPath: "translate-text", + outputFields: [ + { field: "translatedText", description: "The translated text" }, + { field: "detectedSourceLang", description: "Detected source language code" }, + ], + configFields: [ + { + key: "text", + label: "Text to Translate", + type: "template-textarea", + placeholder: "Enter text or use {{NodeName.field}}", + example: "Hello, how are you?", + rows: 4, + required: true, + }, + { + key: "targetLang", + label: "Target Language", + type: "select", + options: [ + { value: "EN-US", label: "English (US)" }, + { value: "EN-GB", label: "English (UK)" }, + { value: "DE", label: "German" }, + { value: "FR", label: "French" }, + { value: "ES", label: "Spanish" }, + { value: "IT", label: "Italian" }, + { value: "PT-PT", label: "Portuguese (Portugal)" }, + { value: "PT-BR", label: "Portuguese (Brazil)" }, + { value: "NL", label: "Dutch" }, + { value: "PL", label: "Polish" }, + { value: "RU", label: "Russian" }, + { value: "JA", label: "Japanese" }, + { value: "ZH-HANS", label: "Chinese (Simplified)" }, + { value: "ZH-HANT", label: "Chinese (Traditional)" }, + { value: "KO", label: "Korean" }, + ], + required: true, + }, + { + key: "sourceLang", + label: "Source Language (optional)", + type: "select", + options: [ + { value: "auto", label: "Auto-detect" }, + { value: "EN", label: "English" }, + { value: "DE", label: "German" }, + { value: "FR", label: "French" }, + { value: "ES", label: "Spanish" }, + { value: "IT", label: "Italian" }, + { value: "PT", label: "Portuguese" }, + { value: "NL", label: "Dutch" }, + { value: "PL", label: "Polish" }, + { value: "RU", label: "Russian" }, + { value: "JA", label: "Japanese" }, + { value: "ZH", label: "Chinese" }, + { value: "KO", label: "Korean" }, + ], + defaultValue: "auto", + }, + { + key: "formality", + label: "Formality", + type: "select", + options: [ + { value: "default", label: "Default" }, + { value: "more", label: "More formal" }, + { value: "less", label: "Less formal" }, + { value: "prefer_more", label: "Prefer more formal" }, + { value: "prefer_less", label: "Prefer less formal" }, + ], + defaultValue: "default", + }, + { + key: "modelType", + label: "Model Type", + type: "select", + options: [ + { value: "default", label: "Default" }, + { value: "quality_optimized", label: "Better quality" }, + { value: "latency_optimized", label: "Faster translation" }, + { value: "prefer_quality_optimized", label: "Quality over speed" }, + ], + defaultValue: "default", + }, + ], + }, + ], +}; + +registerIntegration(deepLPlugin); + +export default deepLPlugin; diff --git a/plugins/deepl/steps/translate-text.ts b/plugins/deepl/steps/translate-text.ts new file mode 100644 index 0000000..ee53efd --- /dev/null +++ b/plugins/deepl/steps/translate-text.ts @@ -0,0 +1,151 @@ +import "server-only"; + +import { fetchCredentials } from "@/lib/credential-fetcher"; +import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; +import { getErrorMessage } from "@/lib/utils"; +import type { DeepLCredentials } from "../credentials"; + +function getBaseUrl(apiKey: string): string { + // Free API keys end with ":fx" + return apiKey.endsWith(":fx") + ? "https://api-free.deepl.com" + : "https://api.deepl.com"; +} + +type DeepLTranslation = { + text: string; + detected_source_language: string; +}; + +type DeepLResponse = { + translations: DeepLTranslation[]; +}; + +type TranslateTextResult = + | { + success: true; + translatedText: string; + detectedSourceLang: string; + } + | { success: false; error: string }; + +export type TranslateTextCoreInput = { + text: string; + targetLang: string; + sourceLang?: string; + formality?: string; + modelType?: string; +}; + +export type TranslateTextInput = StepInput & + TranslateTextCoreInput & { + integrationId?: string; + }; + +async function stepHandler( + input: TranslateTextCoreInput, + credentials: DeepLCredentials +): Promise { + const apiKey = credentials.DEEPL_API_KEY; + + if (!apiKey) { + return { + success: false, + error: + "DEEPL_API_KEY is not configured. Please add it in Project Integrations.", + }; + } + + if (!input.text) { + return { + success: false, + error: "Text to translate is required", + }; + } + + if (!input.targetLang) { + return { + success: false, + error: "Target language is required", + }; + } + + try { + const baseUrl = getBaseUrl(apiKey); + + const body: Record = { + text: [input.text], + target_lang: input.targetLang, + }; + + if (input.sourceLang && input.sourceLang !== "auto") { + body.source_lang = input.sourceLang; + } + + if (input.formality && input.formality !== "default") { + body.formality = input.formality; + } + + if (input.modelType && input.modelType !== "default") { + body.model_type = input.modelType; + } + + const response = await fetch(`${baseUrl}/v2/translate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `DeepL-Auth-Key ${apiKey}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const errorData = (await response.json().catch(() => ({}))) as { + message?: string; + }; + return { + success: false, + error: errorData.message || `HTTP ${response.status}`, + }; + } + + const result = (await response.json()) as DeepLResponse; + + if (!result.translations || result.translations.length === 0) { + return { + success: false, + error: "No translation returned from DeepL", + }; + } + + const translation = result.translations[0]; + + return { + success: true, + translatedText: translation.text, + // detected_source_language is always included in DeepL's response, but adding fallback for type safety + detectedSourceLang: + translation.detected_source_language || input.sourceLang || "unknown", + }; + } catch (error) { + return { + success: false, + error: `Failed to translate: ${getErrorMessage(error)}`, + }; + } +} + +export async function translateTextStep( + input: TranslateTextInput +): Promise { + "use step"; + + const credentials = input.integrationId + ? await fetchCredentials(input.integrationId) + : {}; + + return withStepLogging(input, () => stepHandler(input, credentials)); +} +translateTextStep.maxRetries = 0; + +export const _integrationType = "deepl"; diff --git a/plugins/deepl/test.ts b/plugins/deepl/test.ts new file mode 100644 index 0000000..1ee4310 --- /dev/null +++ b/plugins/deepl/test.ts @@ -0,0 +1,49 @@ +function getBaseUrl(apiKey: string): string { + // Free API keys end with ":fx" + return apiKey.endsWith(":fx") + ? "https://api-free.deepl.com" + : "https://api.deepl.com"; +} + +export async function testDeepL(credentials: Record) { + try { + const apiKey = credentials.DEEPL_API_KEY; + + if (!apiKey) { + return { + success: false, + error: "DEEPL_API_KEY is required", + }; + } + + const baseUrl = getBaseUrl(apiKey); + + // Use the usage endpoint to validate the API key + const response = await fetch(`${baseUrl}/v2/usage`, { + method: "GET", + headers: { + Authorization: `DeepL-Auth-Key ${apiKey}`, + }, + }); + + if (!response.ok) { + if (response.status === 401 || response.status === 403) { + return { + success: false, + error: "Invalid API key. Please check your DeepL API key.", + }; + } + return { + success: false, + error: `API validation failed: HTTP ${response.status}`, + }; + } + + return { success: true }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} diff --git a/plugins/index.ts b/plugins/index.ts index 495c6e3..dc21b6f 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -16,6 +16,7 @@ import "./ai-gateway"; import "./blob"; +import "./deepl"; import "./clerk"; import "./fal"; import "./firecrawl";