Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

译读(ImmerRead)是一款本地安装的 Chrome Side Panel 扩展,为**各类英文文章**(技术博客、新闻报道、深度长文、Substack newsletter 等)提供沉浸式的整篇中文阅读体验。它在右侧渐进翻译全文,提供章节导读、划词翻译与解释、自定义术语库,并把密钥和缓存保存在你自己的浏览器中。

**已验证(2026-09-17):** `npm test` 32/32 通过,`npm run check` 通过。项目使用原生 HTML、CSS 和 JavaScript,无构建步骤。
**已验证(2026-09-17):** `npm test` 34/34 通过,`npm run check` 通过。项目使用原生 HTML、CSS 和 JavaScript,无构建步骤。

## 为什么是译读

Expand All @@ -30,7 +30,7 @@
| 沉浸式双语阅读 | 中文译文与英文原文各自完整,滚动原文时译文自动定位到对应段落 |
| 自定义术语库 | 固定自己认可的译法,跨文章复用,只重翻受影响的段落 |
| 章节导读 | 按原文章节生成中文标题和最多三句、150 字的概述,先把握全局再深入 |
| 划词助学 | 在原文选中英文,可即时翻译或让 LLM 结合语境解释,顺手存为固定译法 |
| 划词助学 | 在原文选中英文,可即时翻译或让 LLM 结合语境解释,顺手存为固定译法;仅在侧栏打开时启用 |
| 复杂内容保护 | 图片、图表、视频和交互模块留在原网页;可识别的表格叶子单元格在原位显示小号译文 |
| 本地缓存 | 译文和导读最多缓存 20 篇文章、保留 30 天,可随时清除 |
| 一致排版 | 右侧译文统一为纯文本,避免链接、粗体等原文样式被模型随机继承 |
Expand Down Expand Up @@ -145,7 +145,7 @@ Most translation tools mix the two languages on screen: overlaid paragraphs, ins
- Immersive bilingual reading: scroll the source and the translation follows paragraph by paragraph.
- A custom terminology glossary: pin your preferred translations, reused across articles, retranslating only affected paragraphs.
- Section-based reading guides: Chinese titles and summaries of at most three sentences / 150 characters per section.
- Learning aids on selection: translate or ask the LLM to explain selected English in context, and save terms to your glossary.
- Learning aids on selection: translate or ask the LLM to explain selected English in context, and save terms to your glossary. Active only while the side panel is open.
- Local translation and guide caches with a 20-article / 30-day limit.
- Plain-text translation rendering for consistent and safe typography.
- No account, developer-operated backend, analytics, advertising, or telemetry.
Expand Down Expand Up @@ -175,7 +175,7 @@ npm test
npm run check
```

Verified on 2026-09-17: 32 tests passed and syntax checks passed. The project uses plain HTML, CSS, and JavaScript with no build step.
Verified on 2026-09-17: 34 tests passed and syntax checks passed. The project uses plain HTML, CSS, and JavaScript with no build step.

## License

Expand Down
25 changes: 24 additions & 1 deletion background.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,40 @@ let summaryWriteChain = Promise.resolve();

void chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => undefined);

const panelOpenTabs = new Set();

chrome.runtime.onConnect.addListener((port) => {
const match = /^yidu-panel-open:(\d+)$/.exec(port.name || "");
if (!match) return;
const tabId = Number(match[1]);
panelOpenTabs.add(tabId);
notifyPanelState(tabId, true);
port.onDisconnect.addListener(() => {
panelOpenTabs.delete(tabId);
notifyPanelState(tabId, false);
});
});

function notifyPanelState(tabId, open) {
chrome.tabs.sendMessage(tabId, { type: "YIDU_PANEL_STATE", payload: { open } }).catch(() => undefined);
}

chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") void chrome.runtime.openOptionsPage();
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === "YIDU_OPEN_OPTIONS") {
chrome.runtime.openOptionsPage();
sendResponse({ ok: true });
return false;
}

if (message?.type === "YIDU_PANEL_STATE_QUERY") {
sendResponse({ ok: true, open: panelOpenTabs.has(sender.tab?.id) });
return false;
}

const handlers = {
YIDU_PREPARE_TAB: prepareTab,
YIDU_TRANSLATE_BATCH: translateBatch,
Expand Down
19 changes: 19 additions & 0 deletions content.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
let cellNotes = new Map();

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === "YIDU_PANEL_STATE") {
panelOpen = Boolean(message.payload?.open);
if (!panelOpen) hideSelectionUi();
sendResponse({ ok: true });
return false;
}
if (message?.type === "YIDU_REQUEST_SCROLL_SYNC") {
scheduleScrollSync();
sendResponse({ ok: true });
Expand Down Expand Up @@ -341,6 +347,15 @@
let selectedText = "";
let selectedRect = null;
let requestId = 0;
let panelOpen = false;

try {
void chrome.runtime.sendMessage({ type: "YIDU_PANEL_STATE_QUERY" })
.then((response) => { panelOpen = Boolean(response?.open); })
.catch(() => undefined);
} catch {
// 扩展上下文失效时保持浮窗关闭。
}

document.addEventListener("mouseup", onSelectionChange);
document.addEventListener("keyup", (event) => {
Expand All @@ -359,6 +374,10 @@
function onSelectionChange(event) {
if (event?.target && selectionRoot?.contains(event.target)) return;
emitSelectionSync();
if (!panelOpen) {
hideSelectionUi();
return;
}
if (selectionShadow?.querySelector(".yidu-result")) return;
const selection = window.getSelection();
const text = (selection?.toString() || "").replace(/\s+/g, " ").trim();
Expand Down
Loading
Loading