Blog

Build a Chrome extension for DeepL translator API

today May 9, 2023

DeepL Translator is a great service for translating content between different languages. The company offers a Chrome extension to use the service in the browser, but we can build our own extension. Let's see how!

This tutorial deals only with the development of the extension, not with getting it published in the Chrome Web Store.
In this tutorial we will store the DeepL API key in the extension code. THIS IS NOT A BEST PRACTICE! Anyone who uses the extension can access the key and abuse it. If you want to offer the extension commercially then you should consider using a proxy server to issue API requests, or a similar solution.

Setup

Start things off by creating a new folder on your computer. In the folder place a square image file to serve as your icon file. Name the file "icon.png".

Create a free DeepL account and copy your API key from the account page. Save the key in a safe place. Do not share it with anyone that you don't trust.

The manifest

We need to let Chrome know about our extension. To do that, create a new file in the folder and name it "manifest.json". Place the following JSON object in it:

{
  "manifest_version": 3,
  "name": "DeepL Translation",
  "description": "Translate web content.",
  "version": "1.0",
  "action": {
    "default_icon": "icon.png",
    "default_popup": "popup.html"
  },
  "permissions": ["activeTab", "contextMenus", "storage"],
  "icons": { "16": "icon.png" },
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "run_at": "document_end",
      "js": ["contentScript.js", "popup.js"],
      "css": ["popup.css"]
    }
  ],
  "background": {
    "service_worker": "service-worker.js"
  }
}

The manifest file notes the name, description, and version of the extension. It notes our icon file, and the permissions that the extension requires. It also notes that the extension should run on any page. Finally it defines the three Javascript files that the extension will include. More on each file later.

The popup page

The extension need a simple HTML where the user can toggle between the two languages to translate (from and to). Create a "popup.html" file in the folder, and place the following code in it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="popup.css" />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <h1>Translate</h1>
      <div id="outer">
        <div id="header">
          <div>From</div>
          <button>🔄</button>
          <div>To</div>
        </div>
        <div id="inputs">
          <div id="from"></div>
          <div id="to"></div>
        </div>
      </div>
    </div>
    <script src="popup.js"></script>
  </body>
</html>

The popup references a CSS file for styling of the popup. Create a "popup.css" file and enter the following:

body {
  font-family: sans-serif, Arial;
  color: #444444;
}

h1 {
  text-align: center;
}

#from,
#to {
  font-weight: 700;
}

#header {
  align-items: center;
  display: flex;
  justify-content: space-between;
}

#header button {
  border: none;
  background-color: white;
  font-size: 28px;
  cursor: pointer;
}

#inputs {
  align-items: center;
  display: flex;
  justify-content: space-between;
}

#outer {
  border: 1px solid #cccccc;
  padding: 10px;
}

.container {
  width: 300px;
  margin: auto;
}

The popup javascript

We need to let the user toggle between the two languages. We also need to persist the user's selection to local storage, so it is remembered across pages and visits. Create a "popup.js" file and enter the following:

window.addEventListener("DOMContentLoaded", async () => {
  const fromDiv = document.querySelector("#from");
  const toDiv = document.querySelector("#to");

  const { from, to } = await chrome.storage.local.get(["from", "to"]);
  fromDiv.innerText = from || "French";
  toDiv.innerText = to || "English";

  document.querySelector("button").addEventListener("click", () => {
    const a = fromDiv.innerText;
    fromDiv.innerText = toDiv.innerText;
    toDiv.innerText = a;
    chrome.storage.local.set({
      from: fromDiv.innerText,
      to: toDiv.innerText,
    });
  });
});

Above, we wait for the page DOM to load, and when it does, we provide an async function to run. The function is async because it needs to "await" the local storage access. We also have an event listener for the button, which toggles the languages and persists them to local storage.

Note that I default my languages to from French to English. Feel free to change that as you see fit.

The service worker

We need some Javascript to run in the background, outside of any specific web page, for doing things like handling the context menu and calling DeepL. So create a new file called "service-worker.js" and add the following:

// Create context menu option
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "translate-menu",
    title: "Translate %s",
    contexts: ["selection"],
  });
});

// Call DeepL API
async function callDeepL(text, source_lang, target_lang) {
  const languageCodes = {
    Bulgarian: "BG",
    Czech: "CS",
    Danish: "DA",
    German: "DE",
    Greek: "EL",
    English: "EN",
    Spanish: "ES",
    Estonian: "ET",
    Finnish: "FI",
    French: "FR",
    Hungarian: "HU",
    Indonesian: "ID",
    Italian: "IT",
    Japanese: "JA",
    Korean: "KO",
    Lithuanian: "LT",
    Latvian: "LV",
    Norwegian: "NB",
    Dutch: "NL",
    Polish: "PL",
    Portuguese: "PT",
    Romanian: "RO",
    Russian: "RU",
    Slovak: "SK",
    Slovenian: "SL",
    Swedish: "SV",
    Turkish: "TR",
    Ukrainian: "UK",
    Chinese: "ZH",
  };

  // UNSAFE TO SHARE! DON'T SHARE THIS WITH ANYONE!
  const API_KEY = "paste-your-api-key-here";
  const API_URL = "https://api-free.deepl.com/v2/translate";

  const headers = new Headers();
  headers.append("Authorization", `DeepL-Auth-Key ${API_KEY}`);
  headers.append(
    "Content-Type",
    "application/x-www-form-urlencoded;charset=UTF-8"
  );

  const body =
    "text=" +
    encodeURIComponent(text) +
    "&source_lang=" +
    languageCodes[source_lang] +
    "&target_lang=" +
    languageCodes[target_lang];

  const options = {
    method: "post",
    headers,
    body,
  };

  const req = new Request(API_URL, options);

  try {
    const resp = await fetch(req);
    const jsn = await resp.json();
    if (jsn.message) {
      return `DeepL message: ${jsn.message}`;
    }
    return jsn.translations[0].text;
  } catch (err) {
    return `DeepL message: ${err.message}`;
  }
}

// Respond to translation requests
chrome.contextMenus.onClicked.addListener(async (info, tabs) => {
  let { from, to } = await chrome.storage.local.get(["from", "to"]);
  from = from || "French";
  to = to || "English";
  const translation = await callDeepL(info.selectionText, from, to);
  chrome.tabs.sendMessage(tabs.id, translation);
});

Above, we create the context menu option when the extension is installed. The title will include the selection in the browser tab.

In callDeepL, we make an API request with the selected text and our from and to languages. Finally, we respond to clicks on the context menu, by calling DeepL with the selection and sending the response to the active browser tab.

The content script

We need Javascript that will run in the context of the web page. Create a "contentScript.js" file with the following:

// Respond to requests to close the dialog box
let dialog;
const body = document.querySelector("body");

window.addEventListener("click", () => {
  if (dialog) {
    dialog.remove();
  }
});
window.addEventListener("keydown", (e) => {
  if (e.key === "Escape" && dialog) {
    dialog.remove();
  }
});

/*
 * Display dialog box with translation
 */
function showDialog(translation) {
  // Get selection to know where to position the dialog
  const selection = window.getSelection();
  if (!selection) {
    console.log("Nothing was selected");
    return;
  }
  const dialogHtml = `

${translation}

`; dialog = document.querySelector("#my-dialog"); if (dialog) { dialog.remove(); } dialog = document.createElement("dialog"); dialog.innerHTML = dialogHtml; dialog.id = "my-dialog"; dialog.open = true; dialog.style = "font-size: 16px; border: 1px solid #cccccc; z-index:999;"; const range = selection.getRangeAt(0); const parent = range.commonAncestorContainer.parentNode; parent.appendChild(dialog); dialog.addEventListener("click", () => { dialog.remove(); }); } // Receive clicks on context menu option chrome.runtime.onMessage.addListener((translation) => { showDialog(translation); });

Above, we have some code to remove the dialog box that will be injected with the translation. We can remove it by clicking the dialog, clicking anywhere on the page, or by pressing the Escape button.

The function showDialog gets the translation, creates a dialog box for it, and adds the box bellow the user's selection on the page. It has an event listener for messages coming from "service-worker", which triggers the dialog.

Deploy the extension

To deploy the extension on your local machine, click on the extensions icon in your menu bar, and then on "Manage extension". Click "Load unpacked" and select your folder. If you then reload any page, you should be able to right-click on the page, select the translation option, and have the translation dialog show up. Clicking on the extension icon should let you toggle the languages.

That's all there's to it. Happy translating!