Beginner Tutorial: Build an AI Translator Chrome Extension with n8n

:waving_hand: Welcome to the first installment of our Browser-to-Automation series!

In this beginner friendly guide, we are going to build a tool that captures text directly from your active browser tab and sends it to an n8n AI Agent for translation. By the end of this tutorial, you will understand how to connect a custom Chrome Extension to your n8n workflows securely.


The Vision & Architecture

Before we write any code, it is crucial to understand how the pieces fit together. We are using a Standard Popup Pattern.

Here is the high-level flow of data:

The Frontend (Chrome Extension)

We are building this extension using Manifest V3 (MV3), the current standard for Chrome extensions.

Why Manifest V3?

The biggest change in MV3 is the shift from persistent “Background Pages” to Service Workers. In older extensions, background scripts ran forever, eating up your computer’s memory. A Service Worker, however, is event-driven. It “wakes up” only when it receives a message (like clicking our translate button), does its job, and goes back to sleep. This is much better for performance, but it means we have to be deliberate about how our UI (popup.js) talks to our backend logic (background.js).

Let’s build out the files for our extension. Create a new folder on your computer and add the following files:

1. manifest.json

This is the “ID Card” of your app. It tells Chrome what permissions your extension needs and where your files are located.

{
  "manifest_version": 3,
  "name": "n8n AI Translator",
  "version": "1.0",
  "permissions": ["activeTab", "scripting"],
  "host_permissions": ["http://localhost:<port>/*", "https://*/*"],
  "action": {
    "default_popup": "popup.html"
  },
  "background": {
    "service_worker": "js/background.js"
  }
}

The User Interface (popup.html & content.css)

This creates the little window that drops down when you click the extension icon.

popup.html

HTML

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="css/content.css">
</head>
<body>
  <div class="header">n8n AI Translator</div>
  
  <label>Selected Text:</label>
  <textarea id="sourceText" placeholder="Highlight text on page first..."></textarea>
  
  <label>Target Language:</label>
  <select id="targetLang">
    <option value="Arabic">Arabic</option>
    <option value="Spanish">Spanish</option>
    <option value="French">French</option>
  </select>

  <button id="sendBtn">Translate with AI</button>

  <div id="status"></div>

  <label>Result:</label>
  <textarea id="resultText" readonly></textarea>

  <script src="js/popup.js"></script>
</body>
</html>

CSS

body {
  width: 640px;
  padding: 15px;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background-color: #f9fafb;
  color: #333;
}

.header {
  font-size: 18px;
  font-weight: bold;
  color: #ff6d5a; /* n8n Orange */
  margin-bottom: 15px;
  text-align: center;
  border-bottom: 1px solid #eee;
  padding-bottom: 10px;
}

label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  margin-bottom: 5px;
  color: #666;
}

textarea {
  width: 100%;
  height: 100px;
  border: 2px solid #ddd;
  border-radius: 6px;
  padding: 8px;
  font-size: 13px;
  margin-bottom: 10px;
  box-sizing: border-box;
  background: white;
}

select {
  width: 100%;
  padding: 8px;
  border-radius: 6px;
  border: 1px solid #ddd;
  margin-bottom: 15px;
}

button {
  width: 100%;
  background-color: #ff6d5a;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 6px;
  font-weight: bold;
  cursor: pointer;
  transition: background 0.2s;
}

button:hover {
  background-color: #e55c49;
}

#status {
  font-size: 11px;
  text-align: center;
  margin: 5px 0;
  color: #ff6d5a;
  min-height: 15px;
}

The Logic Scripts (popup.js & background.js)

Create a folder named js.

js/popup.js grabs the highlighted text from your browser and talks to the background script.

document.addEventListener('DOMContentLoaded', async () => {
  const sourceArea = document.getElementById('sourceText');
  
  // 1. Automatically grab highlighted text from the active tab
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => window.getSelection().toString(),
  }, (selection) => {
    if (selection && selection[0].result) {
      sourceArea.value = selection[0].result;
    }
  });

  // 2. Handle the Send Button
  document.getElementById('sendBtn').addEventListener('click', () => {
    const text = sourceArea.value;
    const lang = document.getElementById('targetLang').value;
    const status = document.getElementById('status');
    const resultArea = document.getElementById('resultText');

    status.innerText = "AI is thinking...";
    
    // 3. Send message to background.js
    chrome.runtime.sendMessage({
      action: "translate",
      payload: { text, targetLanguage: lang, tone: "academic" }
    }, (response) => {
      if (response && response.status === "success") {
        resultArea.value = response.data.translation;
        status.innerText = "Done!";
      } else {
        status.innerText = "Error: Check n8n connection.";
        console.error(response);
      }
    });
  });
});

js/background.js is your Service Worker. It receives the data from the popup and securely pushes it to n8n via a POST request.

IMPORTANT: Replace <YOUR_N8N_DOMAIN> and <YOUR_WEBHOOK_ID> with your actual n8n webhook URL. Change <YOUR_SECRET_KEY> to a secure password of your choosing.

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === "translate") {
    
    // Configure your specific n8n webhook path here
    const webhookUrl = "https://<YOUR_N8N_DOMAIN>/webhook/<YOUR_WEBHOOK_ID>";

    fetch(webhookUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-KEY": "<YOUR_SECRET_KEY>" // This must match n8n Header Auth!
      },
      body: JSON.stringify({
        payload: request.payload
      })
    })
    .then(res => res.json())
    .then(data => sendResponse(data))
    .catch(err => sendResponse({ status: "error", message: err.message }));

    return true; // Keeps the message channel open for the async fetch
  }
});

Security :guard: & The Handshake (CORS) :handshake:

This is where most beginners get stuck. Because your Extension lives on a different “Origin” (chrome-extension://…) than your n8n server (https://…), Chrome’s security model natively blocks them from talking to each other. This is called the Same-Origin Policy.

To bypass this safely, we use CORS (Cross-Origin Resource Sharing).

When your extension tries to send the translation request, Chrome first sends an invisible “Preflight” request to n8n asking, “Hey, are you allowed to talk to this extension?” If n8n doesn’t explicitly reply “Yes”, Chrome blocks the translation.

How to configure this in n8n:

  1. Find your Chrome Extension ID (Go to chrome://extensions/ in Chrome, ensure Developer Mode is on, and copy the ID).

  2. In your n8n Webhook Node, check the Options settings.

  3. Add Allowed Origins and paste: chrome-extension://<YOUR_EXTENSION_ID>. (Never use * in production!)

  4. Set up Header Auth in n8n. The Name should be X-API-KEY and the Value should be the secret key you put in background.js.


The Backend (n8n Workflow)

The backend is a simple 3-node workflow:

  1. Webhook Node: Set to POST and Response Mode: Using ‘Respond to Webhook’ Node.

:backhand_index_pointing_right: Webhook node documentation | n8n Docs

  1. AI Agent Node: Processes the $json.body.payload.text with strict rules to output ONLY the translation.

:backhand_index_pointing_right: AI Agent node documentation | n8n Docs

  1. Respond to Webhook Node: Safely formats the AI’s output back into a JSON object so the Chrome Extension can read it.

:backhand_index_pointing_right: Respond to Webhook | n8n Docs

Here is the full workflow code. You can copy this block and paste it directly into your n8n canvas!

Note: You will need to re-select your OpenAI credentials and add your specific Webhook ID/Extension ID once imported.



Conclusion

You have successfully built the foundational “Connectivity Layer” between a browser extension and n8n. You’ve mastered:

  • MV3 Service Workers for memory-efficient background tasks.

  • Message Passing between UI (popup.js) and Logic (background.js).

  • CORS Security to safely allow cross-origin communication.

  • JSON Response formatting inside n8n to ensure clean data delivery.

Load your unpacked extension into Chrome, highlight some text, and watch the automation magic happen!


Read more:

Cross-Origin Resource Sharing (CORS) - HTTP | MDN.


What is CORS? - Cross-Origin Resource Sharing Explained - AWS.


Fix n8n CORS Errors: Complete Guide for Webhooks and API Calls - CORSPROXY Blog.


1個讚

Hey @Benjamin_Behrens :waving_hand: , thanks for reading and for calling that out! You hit the nail on the head.

For this ‘Beginner’ phase, I kept things simple by assuming a personal setup running n8n locally. Hardcoding the key is an okay shortcut just to learn the plumbing, but as you rightly pointed out, it’s a major liability for a production or public extension.

Since this is an ongoing series of tutorials, I’m actually going to solve exactly this in the upcoming parts!

  • In the next mid-level tutorial, I’ll be walking through how to build an options.html settings page to move away from hardcoded credentials.

  • Then, in the 3rd tutorial, I will fully implement the chrome.storage.local approach to securely save, load, and rotate those keys.

This will give users a true BYOK “Bring Your Own Key” experience without ever needing to rebuild the extension. Really appreciate the great feedback—it perfectly tees up the next chapters!

1個讚

It’s a fantastic project for beginners since it includes several interesting technologies in one workflow: browser extensions, webhooks, automation, and AI. I appreciate that the tutorial does not make n8n sound like just any other backend automation platform, but rather presents it as an opportunity to create something meaningful for users. As AI workflows are designed around triggers, AI, and actions, creating a translator extension seems quite reasonable.

I would recommend adding information regarding the security aspects of building browser extensions as a part of the tutorial, since if your extension communicates with webhooks or some AI services directly, you will need to take care of securing your credentials and request validation as the project evolves. Recent community discussions of the Chrome extension.

1個讚

Thank you @Gunjan_Jeena for the kind words! I’m really glad you found the tutorial useful and that the combination of browser extensions, webhooks, automation, and AI came across as a practical learning project rather than just another backend automation example.

I completely agree with your point regarding security. While the original tutorial was intentionally focused on helping beginners get their first working prototype, security becomes increasingly important as such projects evolve beyond the proof-of-concept stage.

In fact, security has been a major focus of my follow-up work on this project. For anyone interested in taking the translator extension further, I have published several related discussions and tutorials covering secure architectures and browser-extension security practices:

These posts explore topics such as request authentication, HMAC validation, local security boundaries, zero-trust principles, credential protection, and reducing the attack surface when integrating browser extensions with n8n workflows.

Thanks again for the thoughtful feedback and for highlighting an important aspect that deserves attention as AI-powered extensions mature.

1個讚