Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/deepl/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type DeepLCredentials = {
DEEPL_API_KEY?: string;
};
15 changes: 15 additions & 0 deletions plugins/deepl/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

export function DeepLIcon({ className }: { className?: string }) {
return (
<svg
aria-label="DeepL logo"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>DeepL</title>
<path d="M20.907 4.93953 12.68543.18573a1.3577 1.3577 0 0 0-1.3709 0L3.09298 4.9565a1.3766 1.3766 0 0 0-.68639 1.18233v9.52646a1.3766 1.3766 0 0 0 .68639 1.19363l8.22157 4.75946.06223.03583 4.04856 2.3458-.01131-2.06106.0075-1.1446.0038.01885v-.38467c0-.23006.1188-.43371.29605-.56005l.264-.15086.12633-.06977h-.0075l4.80283-2.7795a1.3803 1.3803 0 0 0 .68639-1.19551V6.13505a1.3803 1.3803 0 0 0-.68642-1.19552m-9.85269 9.68863a1.4275 1.4275 0 0 1-.39976 1.3841 1.4086 1.4086 0 0 1-1.97054 0 1.4199 1.4199 0 0 1 0-2.06294 1.4086 1.4086 0 0 1 2.0422.07543l3.32822-1.91585.6864.38656zm5.77019-2.41367a1.4086 1.4086 0 0 1-1.97054 0 1.4256 1.4256 0 0 1-.3696-1.47837l-.0132.0075-3.7525-2.1723-.05657.05656a1.4086 1.4086 0 0 1-1.97053 0 1.4199 1.4199 0 0 1 0-2.06293 1.4086 1.4086 0 0 1 1.97242 0c.3941.37713.52422.91832.39033 1.40672l3.7808 2.20059.01886-.01886a1.4086 1.4086 0 0 1 1.97242 0 1.42746 1.42746 0 0 1 0 2.06105z"/>
</svg>
);
}
133 changes: 133 additions & 0 deletions plugins/deepl/index.ts
Original file line number Diff line number Diff line change
@@ -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;
151 changes: 151 additions & 0 deletions plugins/deepl/steps/translate-text.ts
Original file line number Diff line number Diff line change
@@ -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<TranslateTextResult> {
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<string, unknown> = {
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<TranslateTextResult> {
"use step";

const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};

return withStepLogging(input, () => stepHandler(input, credentials));
}
translateTextStep.maxRetries = 0;

export const _integrationType = "deepl";
49 changes: 49 additions & 0 deletions plugins/deepl/test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>) {
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),
};
}
}
1 change: 1 addition & 0 deletions plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import "./ai-gateway";
import "./blob";
import "./deepl";
import "./clerk";
import "./fal";
import "./firecrawl";
Expand Down