Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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;
149 changes: 149 additions & 0 deletions plugins/deepl/steps/translate-text.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
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,
detectedSourceLang: translation.detected_source_language,
};
} 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),
};
}
}