The Problem That Started It All
One month before my Swedish language exam. What does a normal person do? Studies the language. What do I do? Write a Chrome extension! π€¦ββοΈ
But seriously, the problem was real. While learning Swedish, I constantly encountered unfamiliar words in articles, documentation, and social media. The standard process looked like this:
- Select a word
- Open DeepL in a new tab
- Paste the text
- Get the translation
- Open Anki
- Create a new card
- Copy the original and translation
- Save
By the time I finished this entire cycle, I had already forgotten the context in which I encountered the word. Classic context switching problem.
Meet Anki Translator β my solution to efficient language learning.
Technical Solution
The idea was simple: one click β ready flashcard in Anki. But the implementation turned out to be more interesting than it seemed.
Extension Architecture
Chrome Extensions have a specific architecture with several isolated contexts:
// Manifest V3 structure
{
"manifest_version": 3,
"background": {
"service_worker": "background.js" // Background process
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": ["content.js"] // Page script
}],
"action": {
"default_popup": "popup.html" // Extension UI
}
}
Background Script β central coordinator, handles API requests and inter-component communication.
Content Script β works in the web page context, tracks text selection and shows context menu.
Popup β React application for settings and manual translation input.
Text Selection Handling
The most important part β intercepting user text selection:
// content/content.ts
let selectedText = "";
document.addEventListener("mouseup", () => {
const selection = window.getSelection();
if (selection && selection.toString().trim()) {
selectedText = selection.toString().trim();
// Update context menu
chrome.runtime.sendMessage({
action: "updateContextMenu",
hasSelection: true,
});
}
});
// Context menu handling
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "getSelectedText") {
sendResponse({ text: selectedText });
}
});
DeepL API Integration
DeepL provides an excellent REST API with support for 30+ languages:
// background/deepl.ts
class DeepLTranslator {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, isPro: boolean) {
this.apiKey = apiKey;
this.baseUrl = isPro
? "https://api.deepl.com/v2"
: "https://api-free.deepl.com/v2";
}
async translate(text: string, sourceLang: string, targetLang: string) {
const response = await fetch(`${this.baseUrl}/translate`, {
method: "POST",
headers: {
Authorization: `DeepL-Auth-Key ${this.apiKey}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
text,
source_lang: sourceLang,
target_lang: targetLang,
}),
});
const data = await response.json();
return data.translations[0].text;
}
}
Anki Integration via AnkiConnect
AnkiConnect is an Anki plugin that provides an HTTP API for external applications:
// background/anki.ts
class AnkiConnector {
private readonly ankiConnectUrl = "http://localhost:8765";
async addNote(front: string, back: string, deck: string, tags: string[]) {
const note = {
deckName: deck,
modelName: "Basic",
fields: {
Front: front,
Back: back,
},
tags,
};
const response = await fetch(this.ankiConnectUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "addNote",
version: 6,
params: { note },
}),
});
const result = await response.json();
if (result.error) {
throw new Error(result.error);
}
return result.result; // Created card ID
}
}
React UI with TypeScript
For the interface, I chose React + TypeScript + TailwindCSS. The popup needs to be fast and intuitive:
// popup/TranslationPopup.tsx
interface TranslationState {
originalText: string;
translatedText: string;
isLoading: boolean;
error: string | null;
}
const TranslationPopup: React.FC = () => {
const [state, setState] = useState<TranslationState>({
originalText: "",
translatedText: "",
isLoading: false,
error: null,
});
const handleTranslate = async () => {
setState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const result = await chrome.runtime.sendMessage({
action: "translate",
text: state.originalText,
});
setState((prev) => ({
...prev,
translatedText: result.translation,
isLoading: false,
}));
} catch (error) {
setState((prev) => ({
...prev,
error: error.message,
isLoading: false,
}));
}
};
// JSX with TailwindCSS classes...
};
Webpack Build Configuration
Chrome Extensions require special Webpack configuration:
// webpack.config.js
module.exports = {
entry: {
background: "./src/background/background.ts",
content: "./src/content/content.ts",
popup: "./src/popup/index.tsx",
options: "./src/options/index.tsx",
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ["style-loader", "css-loader", "postcss-loader"],
},
],
},
plugins: [
new CopyWebpackPlugin({
patterns: [{ from: "manifest.json" }, { from: "public" }],
}),
],
};
Privacy and Security
One of the key principles is zero data collection. The extension:
- Does not collect any user data
- API keys are stored locally in the browser
- Translations are sent directly from browser to DeepL
- No external servers or analytics
All data remains on the user’s device.
Results
After one month of active use:
- Created 800+ cards without extra clicks
- Time to create one card: 15 seconds β 3 seconds
- Learned enough Swedish to pass the exam! π
Tech Stack
- TypeScript β for type safety and better DX
- React β for UI components
- TailwindCSS β for rapid styling
- Webpack β for building and bundling
- Chrome Extensions API β for browser integration
- DeepL API β for quality translations
- AnkiConnect β for Anki integration
Conclusions
Sometimes the best projects are born from personal pain. When existing tools don’t solve your problem efficiently β it’s time to write your own.
The project turned out to be not only useful for language learning, but also excellent practice for working with:
- Chrome Extensions API
- Inter-component communication in browsers
- External API integration
- React in the constrained context of extensions
And most importantly β it actually works and saves time every day!
π Check out the project: Anki Translator