Chrome Extension Translation Tool — Developer Guide

19 min read

Build a Translation Tool Extension

A Chrome extension that translates selected text instantly, provides a popup interface for manual translation, and maintains translation history.

What You’ll Build

By the end of this tutorial, you’ll have created a fully functional translation extension with:

Prerequisites

Project Structure

translation-extension/
├── manifest.json
├── popup/
│   ├── popup.html
│   ├── popup.css
│   └── popup.js
├── content/
│   └── content.js
├── background/
│   └── service-worker.js
├── utils/
│   └── translator.js
└── icons/
    └── icon.png

Manifest Configuration

Create your manifest.json with the required permissions:

{
  "manifest_version": 3,
  "name": "Quick Translate",
  "version": "1.0.0",
  "description": "Translate selected text instantly",
  "permissions": [
    "activeTab",
    "storage",
    "contextMenus",
    "scripting",
    "notifications"
  ],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": "icons/icon.png"
  },
  "background": {
    "service_worker": "background/service-worker.js"
  },
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content/content.js"]
  }]
}

Step 1: Translation API Integration

We’ll use LibreTranslate as our primary API (free and self-hostable). Create utils/translator.js:

const API_BASE = 'https://libretranslate.com/translate';

const translationCache = new Map();

async function translate(text, sourceLang, targetLang) {
  const cacheKey = `${text}:${sourceLang}:${targetLang}`;
  
  if (translationCache.has(cacheKey)) {
    return translationCache.get(cacheKey);
  }

  try {
    const response = await fetch(API_BASE, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        q: text,
        source: sourceLang,
        target: targetLang,
        format: 'text'
      })
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }

    const data = await response.json();
    const translation = data.translatedText;
    
    translationCache.set(cacheKey, translation);
    return translation;
  } catch (error) {
    console.error('Translation failed:', error);
    throw error;
  }
}

async function detectLanguage(text) {
  try {
    const response = await fetch('https://libretranslate.com/detect', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ q: text })
    });
    const data = await response.json();
    return data[0]?.language || 'en';
  } catch (error) {
    console.error('Language detection failed:', error);
    return 'en';
  }
}

Step 2: Context Menu Translation

Set up the context menu in your service worker:

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: 'translateSelection',
    title: 'Translate "%s"',
    contexts: ['selection']
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId === 'translateSelection') {
    const selectedText = info.selectionText;
    
    try {
      const { sourceLang = 'auto', targetLang = 'en' } = await getLanguagePrefs();
      const translation = await translate(selectedText, sourceLang, targetLang);
      
      chrome.notifications.create({
        type: 'basic',
        iconUrl: 'icons/icon.png',
        title: 'Translation',
        message: translation,
        priority: 1
      });
      
      await saveToHistory(selectedText, translation, sourceLang, targetLang);
    } catch (error) {
      chrome.notifications.create({
        type: 'basic',
        iconUrl: 'icons/icon.png',
        title: 'Translation Error',
        message: error.message,
        priority: 1
      });
    }
  }
});

Step 3: Inline Translation Tooltip

Create content/content.js for the floating tooltip:

let tooltip = null;

document.addEventListener('mouseup', async (event) => {
  const selection = window.getSelection();
  const selectedText = selection.toString().trim();
  
  if (selectedText.length > 0 && selectedText.length < 5000) {
    setTimeout(() => {
      if (window.getSelection().toString().trim() === selectedText) {
        showTooltip(event, selectedText);
      }
    }, 300);
  }
});

function showTooltip(event, text) {
  removeTooltip();
  
  tooltip = document.createElement('div');
  tooltip.id = 'translation-tooltip';
  tooltip.innerHTML = '<span>Translating...</span>';
  
  const rect = window.getSelection().getRangeAt(0).getBoundingClientRect();
  tooltip.style.cssText = `
    position: fixed;
    top: ${rect.top + window.scrollY - 40}px;
    left: ${rect.left + (rect.width / 2)}px;
    transform: translateX(-50%);
    background: #333;
    color: white;
    padding: 8px 16px;
    border-radius: 4px;
    font-size: 14px;
    z-index: 999999;
    cursor: pointer;
  `;
  
  document.body.appendChild(tooltip);
  
  translateInline(text, tooltip);
}

async function translateInline(text, tooltipElement) {
  try {
    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
    const response = await chrome.tabs.sendMessage(tab.id, {
      action: 'translate',
      text: text
    });
    
    tooltipElement.innerHTML = `<span>${response.translation}</span>`;
    tooltipElement.onclick = () => {
      navigator.clipboard.writeText(response.translation);
      tooltipElement.innerHTML = '<span>Copied!</span>';
    };
  } catch (error) {
    tooltipElement.innerHTML = '<span>Translation failed</span>';
  }
}

function removeTooltip() {
  if (tooltip) {
    tooltip.remove();
    tooltip = null;
  }
}

document.addEventListener('mousedown', removeTooltip);

Step 4: Popup UI

Create popup/popup.html:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <div class="language-row">
      <select id="sourceLang">
        <option value="auto">Auto Detect</option>
        <option value="en">English</option>
        <option value="es">Spanish</option>
        <option value="fr">French</option>
        <option value="de">German</option>
        <option value="it">Italian</option>
        <option value="pt">Portuguese</option>
        <option value="ru">Russian</option>
        <option value="zh">Chinese</option>
        <option value="ja">Japanese</option>
        <option value="ko">Korean</option>
      </select>
      <button id="swapBtn" title="Swap Languages"></button>
      <select id="targetLang">
        <option value="en">English</option>
        <option value="es">Spanish</option>
        <option value="fr">French</option>
        <option value="de">German</option>
        <option value="it">Italian</option>
        <option value="pt">Portuguese</option>
        <option value="ru">Russian</option>
        <option value="zh">Chinese</option>
        <option value="ja">Japanese</option>
        <option value="ko">Korean</option>
      </select>
    </div>
    
    <textarea id="sourceText" placeholder="Enter text to translate..."></textarea>
    <button id="translateBtn">Translate</button>
    
    <div id="result" class="result hidden">
      <p id="translationText"></p>
      <button id="copyBtn" title="Copy">📋 Copy</button>
    </div>
    
    <div class="history-section">
      <h3>History</h3>
      <div id="historyList"></div>
      <button id="clearHistory">Clear History</button>
    </div>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Style with popup.css:

.container {
  width: 350px;
  padding: 16px;
  font-family: Arial, sans-serif;
}

.language-row {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 12px;
}

select {
  flex: 1;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

#swapBtn {
  padding: 8px 12px;
  background: #f0f0f0;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

textarea {
  width: 100%;
  height: 100px;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  resize: vertical;
  box-sizing: border-box;
}

button#translateBtn {
  width: 100%;
  margin-top: 8px;
  padding: 10px;
  background: #4285f4;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.result {
  margin-top: 16px;
  padding: 12px;
  background: #f9f9f9;
  border-radius: 4px;
  position: relative;
}

.hidden {
  display: none;
}

#copyBtn {
  margin-top: 8px;
  padding: 4px 8px;
}

.history-section {
  margin-top: 16px;
  border-top: 1px solid #eee;
  padding-top: 12px;
}

.history-item {
  padding: 8px;
  margin: 4px 0;
  background: #f5f5f5;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
}

Popup logic in popup/popup.js:

document.addEventListener('DOMContentLoaded', async () => {
  const sourceText = document.getElementById('sourceText');
  const translateBtn = document.getElementById('translateBtn');
  const result = document.getElementById('result');
  const translationText = document.getElementById('translationText');
  const copyBtn = document.getElementById('copyBtn');
  const swapBtn = document.getElementById('swapBtn');
  const clearHistory = document.getElementById('clearHistory');
  const historyList = document.getElementById('historyList');
  
  // Load saved preferences
  const prefs = await chrome.storage.local.get(['sourceLang', 'targetLang']);
  document.getElementById('sourceLang').value = prefs.sourceLang || 'auto';
  document.getElementById('targetLang').value = prefs.targetLang || 'en';
  
  // Load history
  loadHistory();
  
  // Translate button
  translateBtn.addEventListener('click', async () => {
    const text = sourceText.value.trim();
    if (!text) return;
    
    const sourceLang = document.getElementById('sourceLang').value;
    const targetLang = document.getElementById('targetLang').value;
    
    translateBtn.textContent = 'Translating...';
    
    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      const response = await chrome.tabs.sendMessage(tab.id, {
        action: 'translate',
        text,
        sourceLang,
        targetLang
      });
      
      translationText.textContent = response.translation;
      result.classList.remove('hidden');
      
      // Save to history
      await saveToHistory(text, response.translation, sourceLang, targetLang);
      loadHistory();
    } catch (error) {
      translationText.textContent = 'Error: ' + error.message;
      result.classList.remove('hidden');
    }
    
    translateBtn.textContent = 'Translate';
  });
  
  // Swap languages
  swapBtn.addEventListener('click', () => {
    const source = document.getElementById('sourceLang');
    const target = document.getElementById('targetLang');
    
    if (source.value !== 'auto') {
      const temp = source.value;
      source.value = target.value;
      target.value = temp;
    }
  });
  
  // Copy translation
  copyBtn.addEventListener('click', () => {
    navigator.clipboard.writeText(translationText.textContent);
    copyBtn.textContent = 'Copied!';
    setTimeout(() => copyBtn.textContent = '📋 Copy', 2000);
  });
  
  // Save preferences on change
  document.getElementById('sourceLang').addEventListener('change', async (e) => {
    await chrome.storage.local.set({ sourceLang: e.target.value });
  });
  
  document.getElementById('targetLang').addEventListener('change', async (e) => {
    await chrome.storage.local.set({ targetLang: e.target.value });
  });
  
  // Clear history
  clearHistory.addEventListener('click', async () => {
    await chrome.storage.local.set({ translationHistory: [] });
    loadHistory();
  });
});

async function saveToHistory(source, translation, sourceLang, targetLang) {
  const { translationHistory = [] } = await chrome.storage.local.get('translationHistory');
  
  const entry = {
    id: Date.now(),
    source,
    translation,
    sourceLang,
    targetLang,
    timestamp: new Date().toISOString()
  };
  
  translationHistory.unshift(entry);
  
  // Keep only last 100 entries
  if (translationHistory.length > 100) {
    translationHistory.pop();
  }
  
  await chrome.storage.local.set({ translationHistory });
}

async function loadHistory() {
  const { translationHistory = [] } = await chrome.storage.local.get('translationHistory');
  const historyList = document.getElementById('historyList');
  
  historyList.innerHTML = translationHistory.slice(0, 10).map(item => `
    <div class="history-item" data-source="${item.source}">
      <strong>${item.sourceLang}${item.targetLang}</strong><br>
      ${item.source.substring(0, 30)}${item.source.length > 30 ? '...' : ''}<br>
      <em>${item.translation.substring(0, 30)}${item.translation.length > 30 ? '...' : ''}</em>
    </div>
  `).join('');
  
  // Click to restore
  historyList.querySelectorAll('.history-item').forEach(item => {
    item.addEventListener('click', () => {
      const entry = translationHistory.find(h => h.id == item.dataset.id);
      if (entry) {
        document.getElementById('sourceText').value = entry.source;
        document.getElementById('translationText').textContent = entry.translation;
        document.getElementById('result').classList.remove('hidden');
      }
    });
  });
}

Step 5: Handling API Errors and Rate Limits

Add robust error handling in your service worker:

async function translateWithRetry(text, sourceLang, targetLang, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await translate(text, sourceLang, targetLang);
    } catch (error) {
      lastError = error;
      
      if (error.message.includes('429')) {
        // Rate limited - wait before retry
        await new Promise(resolve => setTimeout(resolve, attempt * 2000));
      } else if (error.message.includes('500')) {
        // Server error - might recover
        await new Promise(resolve => setTimeout(resolve, 1000));
      } else {
        // Client error - don't retry
        throw error;
      }
    }
  }
  
  throw lastError;
}

Testing Your Extension

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” in the top right
  3. Click “Load unpacked” and select your extension directory
  4. Test context menu: Select text on any page, right-click, choose “Translate”
  5. Test popup: Click the extension icon, enter text, select languages
  6. Test inline tooltip: Select text and wait for the floating tooltip

Best Practices

Next Steps

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

No previous article
No next article