项目结构
本文档详细说明 Annotate Translate 的目录组织和文件职责。
项目概览
Annotate Translate 是一个无构建流程的 Chrome Manifest V3 扩展,所有代码都是纯 Vanilla JavaScript,通过 manifest.json 按顺序加载。
annotate-translate/
├── manifest.json # 扩展清单(入口文件)
├── src/ # 源代码目录
├── _locales/ # 国际化资源
├── assets/ # 静态资源
├── test/ # 测试文件
├── scripts/ # 工具脚本
├── docs/ # VitePress 文档站点
└── README.mdmanifest.json
扩展的核心配置文件,定义了:
json
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "1.0.0",
"permissions": [
"activeTab",
"storage",
"contextMenus",
"scripting"
],
"host_permissions": ["<all_urls>"],
"background": {
"service_worker": "src/background/background.js"
},
"content_scripts": [{
"matches": ["<all_urls>"],
"js": [
"src/utils/constants.js",
"src/utils/html-sanitizer.js",
// ... 31 个文件按依赖顺序
"src/content/content.js" // 主入口最后加载
],
"css": [
"src/styles/translation-ui.css",
"src/content/content.css"
]
}]
}关键点:
- 所有 JS 文件必须在
content_scripts.js数组中按依赖顺序声明 - 依赖项必须在使用它们的文件之前加载
- 没有模块打包工具,依赖全局变量
src/ 源代码目录
总览
src/
├── background/ # 后台服务工作者
├── content/ # 内容脚本
├── services/ # 核心服务层
├── providers/ # 提供商实现
├── utils/ # 工具函数
├── options/ # 设置页面
├── popup/ # 扩展弹出窗口
├── styles/ # 全局样式
├── lib/ # 第三方库
└── data/ # 静态数据background/ - 后台服务工作者
文件: background.js (约 500 行)
职责:
- 扩展生命周期管理
- 上下文菜单创建
- CORS 代理(Youdao、DeepL、OpenAI)
- 消息路由
- 初始化默认设置
核心功能:
javascript
// 1. 上下文菜单
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'translate',
title: chrome.i18n.getMessage('translate'),
contexts: ['selection']
});
});
// 2. CORS 代理
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'youdaoTranslate') {
fetch(request.params.url, {...})
.then(r => r.json())
.then(data => sendResponse({ success: true, data }));
return true; // 保持消息通道开放
}
});
// 3. 初始化默认设置
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.get(null, (items) => {
if (Object.keys(items).length === 0) {
chrome.storage.sync.set(DEFAULT_SETTINGS);
}
});
});content/ - 内容脚本
核心文件:
content.js (约 1200 行)
职责: 主入口,协调所有功能
javascript
// 核心流程
document.addEventListener('mouseup', handleTextSelection);
document.addEventListener('dblclick', handleDoubleClick);
async function handleTextSelection() {
const selectedText = window.getSelection().toString().trim();
if (!selectedText || !$.enableTranslate) return;
showFloatingMenu(selectedText, position);
}
async function translateText(text) {
const result = await translationService.translate(
text,
$.targetLanguage,
'auto'
);
TranslationUI.render(result, position);
}
async function annotateText(text, result) {
const annotationText = translationService.generateAnnotationText(result);
const ruby = createRubyElement(text, annotationText);
replaceTextWithRuby(textNode, ruby);
saveAnnotation(result);
}状态管理:
settings- 本地缓存的设置currentTranslationCard- 当前显示的翻译卡片floatingMenu- 浮动菜单 DOM 引用
translation-ui.js (约 800 行)
职责: 翻译结果 UI 渲染
javascript
class TranslationUI {
static render(result, position) {
const card = this.createCard(result);
this.positionCard(card, position);
this.attachEventListeners(card, result);
document.body.appendChild(card);
}
static createCard(result) {
// 根据文本长度决定布局
const isSimplified = result.originalText.length > 100;
return `
<div class="translation-card">
<div class="header">...</div>
${isSimplified ? this.simplifiedLayout(result) : this.fullLayout(result)}
<div class="actions">...</div>
</div>
`;
}
static attachAudioButtonListeners(card, phonetics) {
// 三层 Fallback: ArrayBuffer → URL → TTS
}
}translation-integration.js (约 300 行)
职责: 翻译功能集成和辅助
其他文件:
content.css- 内容脚本样式vocabulary-ui.js- 词汇模式 UI
services/ - 核心服务层
translation-service.js (约 1500 行)
最核心的文件,实现翻译服务抽象层。
核心类:
javascript
// TranslationProvider 抽象基类
class TranslationProvider {
constructor(name, config) {}
async translate(text, targetLang, sourceLang) {}
async detectLanguage(text) {}
getSupportedLanguages() {}
}
// 5 个提供商实现
class GoogleTranslateProvider extends TranslationProvider { }
class YoudaoTranslateProvider extends TranslationProvider { }
class DeepLTranslateProvider extends TranslationProvider { }
class OpenAITranslateProvider extends TranslationProvider { }
class FreeDictionaryProvider extends TranslationProvider { }
// TranslationService 单例
class TranslationService {
constructor() {
this.providers = new Map();
this.activeProvider = null;
this.cache = new Map();
this.maxCacheSize = 100;
}
registerProvider(name, provider) { }
setActiveProvider(name) { }
async translate(text, targetLang, sourceLang, options) {
// 1. 检查缓存
// 2. 调用提供商
// 3. 音标补充
// 4. 生成标注文本
// 5. 写入缓存
// 6. 返回结果
}
async supplementPhoneticsFromFreeDictionary(result, text) { }
generateAnnotationText(result) { }
}
// 全局单例
const translationService = new TranslationService();vocabulary-service.js (约 800 行)
职责: 词库管理
javascript
class VocabularyService {
constructor() {
this.providers = new Map();
this.activeProvider = null;
this.cache = new Map();
}
async setActiveProvider(name, options) { }
shouldAnnotate(word, context) { }
batchCheck(words) { }
}词库提供商:
CETVocabularyProvider- CET-4/6 词库FrequencyVocabularyProvider- 词频词库UnifiedVocabularyProvider- 统一词库(推荐)
annotation-scanner.js (约 600 行)
职责: 页面扫描和批量标注
javascript
class AnnotationScanner {
constructor(vocabularyService, translationService) {}
async scanAndAnnotate(rootElement, options) {
this.abortController = new AbortController();
// 1. 提取文本节点
const textNodes = this.extractTextNodes(rootElement);
// 2. 分词并过滤
const words = this.extractWords(textNodes);
// 3. 批量检查
const toAnnotate = this.vocabularyService.batchCheck(words);
// 4. 批量翻译
for (const word of toAnnotate) {
if (this.abortController.signal.aborted) break;
const result = await this.translationService.translate(word);
this.injectAnnotation(word, result);
}
}
abort() { }
}ai-translation-service.js (约 400 行)
职责: AI 翻译统一管理
token-stats-service.js (约 200 行)
职责: AI Token 统计和成本计算
providers/ - 提供商实现
openai-provider.js (约 500 行)
OpenAI 兼容 API 适配器
javascript
class OpenAIProvider extends BaseAIProvider {
constructor(config) {
this.apiEndpoint = `${config.baseURL}/chat/completions`;
this.model = config.model || 'gpt-3.5-turbo';
this.temperature = config.temperature ?? 0.3;
}
async translate(text, sourceLang, targetLang, options) {
const prompts = PromptTemplates.buildPrompt({
text, sourceLang, targetLang,
format: this.promptFormat,
context: options.context
});
const response = await this.sendRequestViaBackground(...);
return this.parseResponse(response);
}
}base-ai-provider.js (约 300 行)
AI 提供商抽象基类
prompt-templates.js (约 600 行)
AI 提示词模板系统
javascript
class PromptTemplates {
static buildPrompt(options) {
const { format } = options;
return format === 'jsonFormat'
? this.buildJsonPrompt(options)
: this.buildSimplePrompt(options);
}
static buildJsonPrompt({ text, sourceLang, targetLang, context }) {
return {
system: '你是一个专业的翻译助手...',
user: `翻译以下${sourceLang}文本到${targetLang}:\n${text}\n上下文: ${context}`
};
}
}vocabulary/ - 词库提供商
vocabulary/
├── base-provider.js # 词库提供商基类
├── cet-provider.js # CET-4/6 提供商
├── frequency-provider.js # 词频提供商
└── unified-provider.js # 统一提供商(推荐)utils/ - 工具函数
settings-schema.js (约 500 行)
最重要的配置文件,定义默认设置结构
javascript
const DEFAULT_SETTINGS = {
general: {
enableTranslate: true,
targetLanguage: 'zh-CN',
enablePhoneticFallback: true
},
providers: {
current: 'google',
google: { enabled: true },
youdao: { appKey: '', appSecret: '' },
deepl: { apiKey: '', useFreeApi: true },
openai: { apiKey: '', model: 'gpt-3.5-turbo', baseURL: '...' },
aiProviders: [...]
},
annotation: {
showPhonetics: true,
showTranslation: true,
showDefinitions: false
},
translationCard: {
showPhonetics: true,
enableAudio: true,
showDefinitions: true,
showExamples: true
},
vocabulary: { ... },
performance: {
enableCache: true,
cacheSize: 100
},
debug: { enableDebugMode: false }
};cache-manager.js (约 300 行)
LRU 缓存管理器
javascript
class CacheManager {
constructor(options = {}) {
this.maxSize = options.maxSize || 100;
this.ttl = options.ttl || (30 * 60 * 1000); // 30分钟
this.cache = new Map();
}
get(key) { }
set(key, value, customTTL) { }
cleanup() { }
}html-sanitizer.js (约 200 行)
HTML 安全清理器
i18n-helper.js (约 100 行)
国际化辅助函数
javascript
function safeGetMessage(key, substitutions = null, fallback = '') {
if (!isExtensionContextValid()) return fallback;
return chrome.i18n.getMessage(key, substitutions) || fallback;
}其他工具:
message-helper.js- 扩展消息辅助event-manager.js- 事件管理input-validator.js- 输入验证constants.js- 常量定义secure-storage.js- 安全存储
options/ - 设置页面
options/
├── options.html # 设置页面 UI
├── options.js # 设置页面逻辑(约 1000 行)
├── options.css # 样式
└── ai-providers-manager.js # AI 提供商管理器options.js 核心功能:
- 加载和保存设置
- 表单绑定和验证
- 提供商连接测试
- 数据导入导出
popup/ - 扩展弹出窗口
popup/
├── popup.html # 弹出窗口 UI
├── popup.js # 逻辑(约 400 行)
└── popup.css # 样式功能:
- 快速开关功能
- 切换翻译提供商
- 查看词库统计
- 快捷设置访问
styles/ - 全局样式
styles/
├── translation-ui.css # 翻译卡片样式
└── common.css # 通用样式lib/ - 第三方库
lib/
└── lucide-icons.js # Lucide Icons (CDN 加载)data/ - 静态数据
data/
└── vocabularies/
├── vocabulary-core.json # 核心词库数据
├── cet4.json
├── cet6.json
├── toefl.json
├── ielts.json
└── gre.json数据格式:
json
{
"meta": {
"version": "1.0.0",
"totalWords": 10000
},
"words": {
"abandon": {
"tags": ["cet4", "cet6"],
"collins": 4,
"frequency": 5234
}
}
}_locales/ - 国际化资源
_locales/
├── en/
│ └── messages.json # 英文
├── zh_CN/
│ └── messages.json # 简体中文
├── zh_TW/
│ └── messages.json # 繁体中文
├── de/
├── es/
├── fr/
├── ja/
└── ko/消息格式:
json
{
"extName": {
"message": "Annotate Translate",
"description": "Extension name"
},
"translate": {
"message": "翻译",
"description": "Translate button text"
}
}使用方式:
javascript
// manifest.json 中
"name": "__MSG_extName__"
// JS 中
const text = chrome.i18n.getMessage('translate');
// 带占位符
chrome.i18n.getMessage('wordCount', [10]);assets/ - 静态资源
assets/
└── icons/
├── icon16.png
├── icon32.png
├── icon48.png
├── icon128.png
└── providers/ # 提供商 Logo
├── google.svg
├── youdao.svg
├── deepl.svg
└── openai.svgscripts/ - 工具脚本
scripts/
├── README.md # ECDICT 处理说明
└── convert-ecdict.js # ECDICT 数据转换脚本(Node.js)用途: 将 ECDICT CSV 数据转换为 JSON 格式
test/ - 测试文件
test/
├── test-ai-translation.html # AI 翻译功能测试
└── translation-test.html # 翻译功能测试在浏览器中打开进行手动测试。
docs/ - VitePress 文档站点
docs/
├── .vitepress/
│ └── config.mts
├── guide/
├── development/
├── api/
├── recipes/
├── design/
├── resources/
└── index.md文件加载顺序
Content Scripts 加载顺序(来自 manifest.json):
1. 常量和工具
├── constants.js
├── html-sanitizer.js
├── input-validator.js
├── secure-storage.js
└── event-manager.js
2. 辅助函数
├── i18n-helper.js
├── message-helper.js
└── settings-schema.js
3. 缓存管理
└── cache-manager.js
4. 提供商系统
├── prompt-templates.js
├── base-ai-provider.js
├── openai-provider.js
├── vocabulary/base-provider.js
├── vocabulary/cet-provider.js
├── vocabulary/frequency-provider.js
└── vocabulary/unified-provider.js
5. 核心服务
├── translation-service.js # 最核心
├── ai-translation-service.js
├── token-stats-service.js
├── vocabulary-service.js
└── annotation-scanner.js
6. UI 组件
├── translation-ui.js
└── vocabulary-ui.js
7. 集成层
├── translation-integration.js
└── content.js # 主入口(最后)为什么这个顺序很重要?
- 依赖项必须在使用它们的文件之前加载
- 无模块系统,依赖全局变量
- 错误的顺序会导致
ReferenceError
代码统计
总计 JavaScript 文件: 31
总计代码行数: ~15,000 行
核心文件:
- translation-service.js: ~1,500 行
- content.js: ~1,200 行
- options.js: ~1,000 行
- translation-ui.js: ~800 行
- vocabulary-service.js: ~800 行关键依赖关系
mermaid
graph TB
Content[content.js] --> TransService[translation-service.js]
Content --> VocabService[vocabulary-service.js]
Content --> TransUI[translation-ui.js]
Content --> AnnotScanner[annotation-scanner.js]
TransService --> Providers[5 个 Provider 类]
VocabService --> VocabProviders[3 个 Vocabulary Provider]
AnnotScanner --> TransService
AnnotScanner --> VocabService
TransService --> CacheManager[cache-manager.js]
TransService --> Settings[settings-schema.js]
Providers --> Background[background.js<br/>CORS Proxy]模块化策略
虽然没有构建工具,但代码通过以下方式保持模块化:
- 类封装 - 每个功能封装在类中
- 单一职责 - 每个文件负责一个明确的功能
- 全局单例 - 核心服务作为全局单例
- 依赖注入 - 通过构造函数传递依赖
javascript
// 示例:依赖注入
class AnnotationScanner {
constructor(vocabularyService, translationService) {
this.vocabularyService = vocabularyService;
this.translationService = translationService;
}
}
// 在 content.js 中初始化
const annotationScanner = new AnnotationScanner(
vocabularyService,
translationService
);