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 (
+
+ );
+}
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";