跪拜 Guibai
← Back to the summary

Build a Chrome Side Panel That AI-Translates English Pages to Markdown

This article only covers "how to build it", not the code principles. You only need to know how to create folders, create files, type commands, and click buttons. Follow along and you'll build the complete skeleton of this extension. Each core logic file is labeled with "what it does and what to watch out for" — just code accordingly.

0. Before you start, prepare four things

Item Requirement How to prepare
① Node.js 20.19 or higher (22 LTS recommended) Go to nodejs.org download the LTS version, double-click to install, click "Next" all the way through
② Editor VS Code recommended Go to code.visualstudio.com download and install
③ Browser Chrome or Edge A relatively new version (Chrome 114+)
④ An AI Key DeepSeek, etc. Go to platform.deepseek.com register → Console → "API Keys" → Create, copy that sk-... string and save it

Confirm Node is installed: Press Win + R, type cmd and hit Enter. In the black window, type node -v — if it prints a version number (like v22.x.x), you're good. Then type npm -v to confirm npm is also there.

1. Create the project folder

  1. Create a new folder chrome-ai-translation anywhere on your computer (use English, no Chinese characters or spaces).
  2. In VS Code, go to "File → Open Folder" and select it.

2. First, see which files you'll end up with

A total of 17 files: 4 config files + 13 source files. First, create the folders according to this structure:

chrome-ai-translation/
├── package.json          # Config file (copy below)
├── tsconfig.json         # Config file (copy below)
├── vite.config.ts        # Config file (copy below)
├── manifest.json         # Config file (copy below)
└── src/
    ├── shared/           # Cross-context reuse
    │   ├── constants.ts
    │   ├── types.ts
    │   ├── messages.ts
    │   └── storage.ts
    ├── content/          # Extract body text from web pages
    │   ├── extractor.ts
    │   └── index.ts
    ├── background/       # Orchestration + AI calls
    │   ├── translator.ts
    │   └── index.ts
    ├── components/       # Shared components
    │   └── MarkdownView.tsx
    └── panel/            # Side panel UI
        ├── index.html
        ├── main.tsx
        ├── App.tsx
        └── style.css

How to create folders: In VS Code's left sidebar Explorer, right-click → "New Folder". First create src, then inside src create the five subfolders shared, content, background, components, panel. Use "New File" to create files.

3. Four config files (copy exactly)

These 4 are "config files" — they must be exact. Just copy and paste directly.

File 1/17: package.json (project root)

{
  "name": "chrome-ai-translation",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "clean": "node --input-type=module -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
    "build": "npm run clean && tsc --noEmit && vite build",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@mozilla/readability": "^0.6.0",
    "dompurify": "^3.4.14",
    "md-wx": "^1.0.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "turndown": "^7.2.4"
  },
  "devDependencies": {
    "@crxjs/vite-plugin": "^2.7.1",
    "@types/chrome": "^0.2.7",
    "@types/react": "^18.3.31",
    "@types/react-dom": "^18.3.7",
    "@types/turndown": "^5.0.6",
    "@vitejs/plugin-react": "^5.2.0",
    "typescript": "^5.9.3",
    "vite": "^7.3.6"
  }
}

File 2/17: tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "types": ["chrome"]
  },
  "include": ["src"]
}

File 3/17: vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'

export default defineConfig({
  plugins: [react(), crx({ manifest })],
  build: {
    emptyOutDir: true,
  },
})

File 4/17: manifest.json

{
  "manifest_version": 3,
  "name": "英文网页 AI 翻译",
  "version": "0.1.0",
  "description": "一键提取英文网页正文,调用 AI 模型翻译并转为 Markdown",
  "action": {
    "default_title": "英文网页 AI 翻译"
  },
  "background": {
    "service_worker": "src/background/index.ts",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["http://*/*", "https://*/*"],
      "js": ["src/content/index.ts"],
      "run_at": "document_idle"
    }
  ],
  "side_panel": {
    "default_path": "src/panel/index.html"
  },
  "permissions": ["activeTab", "scripting", "storage", "sidePanel"],
  "host_permissions": ["https://api.deepseek.com/*", "http://*/*", "https://*/*"]
}

This one is the most critical: The side_panel field makes the extension open a side panel from the right (instead of a traditional popup), and action also has no default_popup. This is how you make it a side panel.

4. Thirteen source files (only list responsibilities, no code pasted)

These 13 are "logic source files" — implement them according to their responsibilities. Each file is labeled with "what it does + what to watch out for". Code accordingly.

4.1 src/shared/ — Cross-context reusable foundation (4 files)

File What it does
constants.ts Constants: default API URL https://api.deepseek.com, default model deepseek-chat, storage keys, side panel communication port name panel
types.ts Domain types: ExtractPayload (markdown/title/author/url), TranslationResult, Settings (apiKey/baseUrl/model), status enum (idle/extracting/translating/done/error)
messages.ts Message protocol: strongly-typed definitions for all messages between background / content script / side panel (extraction request and result, translation request and streaming chunks, settings read/write)
storage.ts Storage layer: wraps reads/writes to chrome.storage.local — "latest translation result" and "settings", settings read merges with defaults

4.2 src/content/ — Extract body text from web pages (2 files)

File What it does
extractor.ts Core extraction pipeline: clone DOM → normalize lazy-loaded image URLs (data-src/srcset → absolute src) → Readability identifies main content → length check (body < 50 chars falls back to selector-based extraction) → DOMPurify sanitize → Turndown convert to Markdown → get title / author
index.ts Content script entry: listens for background "extract body text" message, calls extractor and returns result

4.3 src/background/ — Orchestration + AI calls (2 files)

File What it does
translator.ts Translation core: uses native fetch to call OpenAI-compatible streaming API (stream: true), parses SSE increments line by line (data: prefix, [DONE] ends); System Prompt constrains "only translate body text, preserve Markdown structure and images"; assembles # Title / > Author / > Link / Body
index.ts Background entry: click icon opens side panel, manages Port connections, orchestrates "extract → translate → stream push → store" full flow; when content script is not injected, uses scripting.executeScript as fallback injection

4.4 src/components/ + src/panel/ — UI (5 files)

File What it does
components/MarkdownView.tsx Uses md-wx's MarkdownRenderer to render Markdown, disables extra UI like settings / copy / theme toggle
panel/index.html Side panel entry HTML, mounts #root + imports main.tsx
panel/main.tsx React mount entry, renders App
panel/App.tsx Main interface: translate button + settings panel (Key / Base URL / Model) + streaming typewriter display (60ms throttle) + download .md / copy; connects Port to receive background pushes
panel/style.css Side panel styles (header toolbar, buttons, status bar, error bar, body area)

Implementation notes (keep these in mind when writing logic)

5. Install dependencies

In VS Code, press Ctrl + ` (backtick) to open the terminal, paste and hit Enter:

npm install

Wait 1–3 minutes, seeing added N packages means success. If you get a network error, switch to a domestic mirror:

npm install --registry=https://registry.npmmirror.com

6. Build

npm run build

Seeing ✓ built in xx.xxs means success. Key point: The output is in the newly generated dist folder at the project root — what you'll load later is dist, not src, and not the project root.

7. Load into the browser

  1. Type chrome://extensions in the address bar (Edge: edge://extensions), hit Enter.
  2. Turn on the "Developer mode" toggle in the top right.
  3. Click "Load unpacked" in the top left.
  4. Navigate into the project folder, select the dist folder inside it, click "Select Folder".

When the "英文网页 AI 翻译" card appears, it's installed.

8. Configure API Key and first use

  1. Click the extension icon in the browser toolbar (it might be tucked inside the puzzle 🧩 icon; click that to "pin" it to the toolbar).
  2. The side panel slides out from the right.
  3. Expand "Settings (API Key / Model)" and fill in:
    • API Key: The sk-... string you created at DeepSeek in step 0;
    • Base URL: Keep the default https://api.deepseek.com;
    • Model: Keep the default deepseek-chat.
  4. Click "Save Settings".
  5. Open any English article → click the extension icon → click "Translate Current Page".
  6. The translation will type out character by character. Once done, you can "Download .md" or "Copy".

Done 🎉

9. FAQ

Problem Cause & Solution
No dist folder Run npm run build first; it only appears after a successful build
"Invalid manifest" error on load You selected the wrong folder — select dist/, not src/ or the project root
Clicking the icon does nothing / side panel doesn't open Browser version too old (needs Chrome 114+); or click "Reload" on the extensions page and try again
Translation returns HTTP 403 API Key is wrong / expired, or model name is wrong (use a chat model like deepseek-chat, not an embedding type)
"Cannot read current page" prompt Refresh the page and click translate again; confirm you're on an http/https page
"Please fill in API Key in extension settings first" prompt You didn't click "Save Settings" in step 8, or didn't fill in the Key
Side panel is too narrow Manually drag the left edge of the side panel to widen it (height auto-fills)

10. Want to make it your own?


At this point you've built the complete skeleton of this extension from scratch. Implement the core logic files (body text extraction, streaming translation, side panel UI) according to the responsibility descriptions in section 4. If you want to understand the underlying implementation principles and development pitfalls, you can read the technical retrospective article.