Compare commits

...

1 commit

Author SHA1 Message Date
MengXi
268ea72271 perf(extension): gate iframe content script injection 2026-04-28 13:16:42 -07:00
11 changed files with 376 additions and 142 deletions

View file

@ -0,0 +1,5 @@
---
"@read-frog/extension": patch
---
perf(extension): gate iframe content script injection

View file

@ -1,3 +1,4 @@
import { browser, storage } from "#imports"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { SITE_CONTROL_URL_WINDOW_KEY } from "@/utils/site-control"
@ -6,39 +7,12 @@ const webNavigationOnBeforeNavigateAddListenerMock = vi.fn()
const webNavigationOnCompletedAddListenerMock = vi.fn()
const getAllFramesMock = vi.fn()
const executeScriptMock = vi.fn()
const storageGetItemMock = vi.fn()
const getLocalConfigMock = vi.fn()
const loggerErrorMock = vi.fn()
const loggerWarnMock = vi.fn()
const browserMock = {
tabs: {
onRemoved: {
addListener: tabsOnRemovedAddListenerMock,
},
},
webNavigation: {
onBeforeNavigate: {
addListener: webNavigationOnBeforeNavigateAddListenerMock,
},
onCompleted: {
addListener: webNavigationOnCompletedAddListenerMock,
},
getAllFrames: getAllFramesMock,
},
scripting: {
executeScript: executeScriptMock,
},
}
vi.mock("#imports", () => ({
browser: browserMock,
}))
vi.mock("wxt/browser", () => ({
browser: browserMock,
}))
vi.mock("@/utils/config/storage", () => ({
getLocalConfig: getLocalConfigMock,
}))
@ -109,14 +83,32 @@ describe("setupIframeInjection", () => {
vi.clearAllMocks()
currentTabId += 1
browser.tabs.onRemoved.addListener = tabsOnRemovedAddListenerMock
browser.webNavigation.onBeforeNavigate.addListener = webNavigationOnBeforeNavigateAddListenerMock
browser.webNavigation.onCompleted.addListener = webNavigationOnCompletedAddListenerMock
browser.webNavigation.getAllFrames = getAllFramesMock
browser.scripting.executeScript = executeScriptMock
storage.getItem = storageGetItemMock
getLocalConfigMock.mockResolvedValue(null)
getAllFramesMock.mockResolvedValue([
createFrame(0, "https://example.com/app", -1),
createFrame(2, "https://example.com/frame"),
])
storageGetItemMock.mockResolvedValue({ enabled: true })
executeScriptMock.mockResolvedValue(undefined)
})
it("skips iframe injection when page translation is not enabled", async () => {
const { onCompleted } = await setupSubject()
storageGetItemMock.mockResolvedValue({ enabled: false })
await onCompleted(createDetails())
expect(getAllFramesMock).not.toHaveBeenCalled()
expect(executeScriptMock).not.toHaveBeenCalled()
})
it("injects each document once and targets documentIds when available", async () => {
const { onCompleted } = await setupSubject()
const details = createDetails()
@ -124,7 +116,7 @@ describe("setupIframeInjection", () => {
await onCompleted(details)
await onCompleted(details)
expect(executeScriptMock).toHaveBeenCalledTimes(3)
expect(executeScriptMock).toHaveBeenCalledTimes(2)
expect(executeScriptMock).toHaveBeenNthCalledWith(1, expect.objectContaining({
target: { tabId: currentTabId, documentIds: ["doc-1"] },
func: expect.any(Function),
@ -141,7 +133,7 @@ describe("setupIframeInjection", () => {
await onCompleted(createDetails({ documentId: undefined }))
expect(executeScriptMock).toHaveBeenCalledTimes(3)
expect(executeScriptMock).toHaveBeenCalledTimes(2)
for (const [call] of executeScriptMock.mock.calls) {
expect(call.target).toEqual({ tabId: currentTabId, frameIds: [2] })
}
@ -153,12 +145,12 @@ describe("setupIframeInjection", () => {
await onCompleted(details)
await onCompleted(details)
expect(executeScriptMock).toHaveBeenCalledTimes(3)
expect(executeScriptMock).toHaveBeenCalledTimes(2)
onBeforeNavigate({ tabId: currentTabId, frameId: 2 })
await onCompleted(details)
expect(executeScriptMock).toHaveBeenCalledTimes(6)
expect(executeScriptMock).toHaveBeenCalledTimes(4)
})
it("prunes injected records for frames that are no longer live", async () => {
@ -194,10 +186,10 @@ describe("setupIframeInjection", () => {
url: "https://example.com/old-frame",
}))
expect(executeScriptMock).toHaveBeenCalledTimes(9)
expect(executeScriptMock).toHaveBeenCalledTimes(6)
expect(executeScriptMock).toHaveBeenLastCalledWith(expect.objectContaining({
target: { tabId: currentTabId, documentIds: ["doc-stale"] },
files: ["/content-scripts/selection.js"],
files: ["/content-scripts/host.js"],
}))
})
@ -207,14 +199,14 @@ describe("setupIframeInjection", () => {
await onCompleted(details)
await onCompleted(details)
expect(executeScriptMock).toHaveBeenCalledTimes(3)
expect(executeScriptMock).toHaveBeenCalledTimes(2)
onBeforeNavigate({ tabId: currentTabId, frameId: 0 })
await onCompleted(details)
expect(executeScriptMock).toHaveBeenCalledTimes(6)
expect(executeScriptMock).toHaveBeenCalledTimes(4)
onRemoved(currentTabId)
await onCompleted(details)
expect(executeScriptMock).toHaveBeenCalledTimes(9)
expect(executeScriptMock).toHaveBeenCalledTimes(6)
})
})

View file

@ -0,0 +1,129 @@
import { browser, storage } from "#imports"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { getTranslationStateKey } from "@/utils/constants/storage-keys"
const sendMessageMock = vi.fn()
const onMessageMock = vi.fn()
const storageGetItemMock = vi.fn()
const storageSetItemMock = vi.fn()
const storageRemoveItemMock = vi.fn()
const tabsOnRemovedAddListenerMock = vi.fn()
const webNavigationOnCommittedAddListenerMock = vi.fn()
const injectHostContentIntoTabIframesMock = vi.fn()
const loggerErrorMock = vi.fn()
const loggerWarnMock = vi.fn()
const shouldEnableAutoTranslationMock = vi.fn()
const messageHandlers = new Map<string, (msg: any) => any>()
vi.mock("@/utils/message", () => ({
onMessage: onMessageMock,
sendMessage: sendMessageMock,
}))
vi.mock("@/utils/host/translate/auto-translation", () => ({
shouldEnableAutoTranslation: shouldEnableAutoTranslationMock,
}))
vi.mock("@/utils/logger", () => ({
logger: {
error: loggerErrorMock,
warn: loggerWarnMock,
},
}))
vi.mock("../iframe-injection", () => ({
injectHostContentIntoTabIframes: injectHostContentIntoTabIframesMock,
}))
function getHandler(name: string) {
const handler = messageHandlers.get(name)
if (!handler) {
throw new Error(`Expected message handler to be registered: ${name}`)
}
return handler
}
async function setupSubject() {
const { translationMessage } = await import("../translation-signal")
translationMessage()
}
describe("translationMessage", () => {
beforeEach(() => {
vi.clearAllMocks()
messageHandlers.clear()
browser.tabs.onRemoved.addListener = tabsOnRemovedAddListenerMock
browser.webNavigation.onCommitted.addListener = webNavigationOnCommittedAddListenerMock
storage.getItem = storageGetItemMock
storage.setItem = storageSetItemMock
storage.removeItem = storageRemoveItemMock
onMessageMock.mockImplementation((name: string, handler: (msg: any) => any) => {
messageHandlers.set(name, handler)
return vi.fn()
})
sendMessageMock.mockResolvedValue(undefined)
storageGetItemMock.mockResolvedValue(undefined)
storageSetItemMock.mockResolvedValue(undefined)
storageRemoveItemMock.mockResolvedValue(undefined)
shouldEnableAutoTranslationMock.mockResolvedValue(false)
})
it("persists manager-enabled state and injects current iframes from the top frame", async () => {
await setupSubject()
await getHandler("setAndNotifyPageTranslationStateChangedByManager")({
data: { enabled: true },
sender: { tab: { id: 42 }, frameId: 0 },
})
expect(storageSetItemMock).toHaveBeenCalledWith(getTranslationStateKey(42), { enabled: true })
expect(sendMessageMock).toHaveBeenCalledWith("notifyTranslationStateChanged", { enabled: true }, 42)
expect(injectHostContentIntoTabIframesMock).toHaveBeenCalledWith(42)
})
it("does not reinject every iframe when an iframe manager echoes enabled state", async () => {
await setupSubject()
await getHandler("setAndNotifyPageTranslationStateChangedByManager")({
data: { enabled: true },
sender: { tab: { id: 42 }, frameId: 7 },
})
expect(storageSetItemMock).toHaveBeenCalledWith(getTranslationStateKey(42), { enabled: true })
expect(sendMessageMock).toHaveBeenCalledWith("notifyTranslationStateChanged", { enabled: true }, 42)
expect(injectHostContentIntoTabIframesMock).not.toHaveBeenCalled()
})
it("clears state immediately when a tab-level request disables page translation", async () => {
await setupSubject()
await getHandler("tryToSetEnablePageTranslationByTabId")({
data: { tabId: 42, enabled: false },
})
expect(storageSetItemMock).toHaveBeenCalledWith(getTranslationStateKey(42), { enabled: false })
expect(sendMessageMock).toHaveBeenCalledWith("notifyTranslationStateChanged", { enabled: false }, 42)
expect(sendMessageMock).toHaveBeenCalledWith("askManagerToTogglePageTranslation", {
enabled: false,
analyticsContext: undefined,
}, 42)
})
it("waits for the top-frame manager to validate before enabling iframe injection", async () => {
await setupSubject()
await getHandler("tryToSetEnablePageTranslationByTabId")({
data: { tabId: 42, enabled: true },
})
expect(storageSetItemMock).not.toHaveBeenCalled()
expect(injectHostContentIntoTabIframesMock).not.toHaveBeenCalled()
expect(sendMessageMock).toHaveBeenCalledWith("askManagerToTogglePageTranslation", {
enabled: true,
analyticsContext: undefined,
}, 42)
})
})

View file

@ -7,6 +7,7 @@ import { CONFIG_STORAGE_KEY } from "@/utils/constants/config"
import { getTranslationStateKey, TRANSLATION_STATE_KEY_PREFIX } from "@/utils/constants/storage-keys"
import { sendMessage } from "@/utils/message"
import { ensureInitializedConfig } from "./config"
import { getPageTranslationEnabled, setPageTranslationEnabled } from "./page-translation-state"
export const MENU_ID_TRANSLATE = "read-frog-translate"
export const MENU_ID_SELECTION_TRANSLATE = "read-frog-selection-translate"
@ -194,14 +195,13 @@ async function handleContextMenuClick(
* Handle translate menu click - toggle page translation
*/
async function handleTranslateClick(tabId: number) {
const state = await storage.getItem<{ enabled: boolean }>(
getTranslationStateKey(tabId),
)
const isCurrentlyTranslated = state?.enabled ?? false
const isCurrentlyTranslated = await getPageTranslationEnabled(tabId)
const newState = !isCurrentlyTranslated
// Update storage directly (instead of sending message to self)
await storage.setItem(getTranslationStateKey(tabId), { enabled: newState })
if (!newState) {
await setPageTranslationEnabled(tabId, false)
void sendMessage("notifyTranslationStateChanged", { enabled: false }, tabId)
}
// Notify content script in that specific tab
void sendMessage("askManagerToTogglePageTranslation", {
@ -212,7 +212,7 @@ async function handleTranslateClick(tabId: number) {
}, tabId)
// Update menu title immediately
await updateTranslateMenuTitle(tabId)
await updateTranslateMenuTitle(tabId, newState)
}
async function handleSelectionTranslateClick(

View file

@ -1,19 +1,27 @@
import type { FrameInfoForSiteControl } from "./iframe-injection-utils"
import type { Config } from "@/types/config/config"
import { browser } from "#imports"
import { getLocalConfig } from "@/utils/config/storage"
import { logger } from "@/utils/logger"
import { isSiteEnabled, SITE_CONTROL_URL_WINDOW_KEY } from "@/utils/site-control"
import { resolveSiteControlUrl } from "./iframe-injection-utils"
import { getPageTranslationEnabled } from "./page-translation-state"
const pendingDocumentKeys = new Set<string>()
const injectedDocumentKeysByFrame = new Map<string, string>()
function getDocumentInjectionKey(details: { tabId: number, frameId: number, documentId?: string }) {
// Gracefully skip document-level deduplication when Chromium does not expose documentId.
if (!details.documentId) {
return null
}
interface FrameInjectionDetails {
tabId: number
frameId: number
documentId?: string
parentFrameId?: number
url?: string
}
return `${details.tabId}:${details.frameId}:${details.documentId}`
function getDocumentInjectionKey(details: FrameInjectionDetails) {
// documentId is best, but getAllFrames may not expose it. Fall back to URL so
// explicit per-tab injections still dedupe until the frame navigates.
return `${details.tabId}:${details.frameId}:${details.documentId ?? details.url ?? "unknown"}`
}
function getFrameInjectionKey(details: { tabId: number, frameId: number }) {
@ -63,7 +71,7 @@ function setInjectedSiteControlUrl(propertyName: string, siteControlUrl: string)
;(globalThis as Record<string, unknown>)[propertyName] = siteControlUrl
}
function getInjectionTarget(details: { tabId: number, frameId: number, documentId?: string }) {
function getInjectionTarget(details: FrameInjectionDetails) {
if (details.documentId) {
return { tabId: details.tabId, documentIds: [details.documentId] }
}
@ -71,6 +79,119 @@ function getInjectionTarget(details: { tabId: number, frameId: number, documentI
return { tabId: details.tabId, frameIds: [details.frameId] }
}
async function getFrameSnapshot(tabId: number): Promise<FrameInfoForSiteControl[]> {
return await browser.webNavigation.getAllFrames({ tabId }) ?? []
}
async function injectHostContentIntoFrame(
details: FrameInjectionDetails,
frames?: FrameInfoForSiteControl[],
existingConfig?: Config | null,
) {
const frameKey = getFrameInjectionKey(details)
const documentKey = getDocumentInjectionKey(details)
if (
pendingDocumentKeys.has(documentKey)
|| injectedDocumentKeysByFrame.get(frameKey) === documentKey
) {
return
}
pendingDocumentKeys.add(documentKey)
try {
let siteControlUrl: string | undefined
try {
const [config, frameSnapshot] = await Promise.all([
existingConfig === undefined ? getLocalConfig() : Promise.resolve(existingConfig),
frames === undefined ? getFrameSnapshot(details.tabId) : Promise.resolve(frames),
])
const liveFrameIds = new Set(frameSnapshot.map(frame => frame.frameId))
liveFrameIds.add(details.frameId)
pruneInjectedFrames(details.tabId, liveFrameIds)
siteControlUrl = resolveSiteControlUrl(
details.frameId,
details.url,
frameSnapshot,
getParentFrameIdHint(details),
)
if (!siteControlUrl || !isSiteEnabled(siteControlUrl, config)) {
return
}
}
catch (error) {
logger.error("[Background][IframeInjection] Failed to resolve iframe injection prerequisites", error)
return
}
try {
const target = getInjectionTarget(details) as Parameters<typeof browser.scripting.executeScript>[0]["target"]
await browser.scripting.executeScript({
target,
func: setInjectedSiteControlUrl,
args: [SITE_CONTROL_URL_WINDOW_KEY, siteControlUrl],
})
await browser.scripting.executeScript({
target,
files: ["/content-scripts/host.js"],
})
injectedDocumentKeysByFrame.set(frameKey, documentKey)
}
catch (error) {
logger.warn("[Background][IframeInjection] Failed to inject iframe content scripts", error)
}
}
finally {
pendingDocumentKeys.delete(documentKey)
}
}
export async function injectHostContentIntoTabIframes(tabId: number) {
let isEnabled: boolean
try {
isEnabled = await getPageTranslationEnabled(tabId)
}
catch (error) {
logger.warn("[Background][IframeInjection] Failed to read page translation state", error)
return
}
if (!isEnabled)
return
let config: Config | null
let frames: FrameInfoForSiteControl[]
try {
[config, frames] = await Promise.all([
getLocalConfig(),
getFrameSnapshot(tabId),
])
}
catch (error) {
logger.error("[Background][IframeInjection] Failed to resolve tab iframe injection prerequisites", error)
return
}
const liveFrameIds = new Set(frames.map(frame => frame.frameId))
pruneInjectedFrames(tabId, liveFrameIds)
await Promise.all(frames
.filter(frame => frame.frameId !== 0)
.map(frame => injectHostContentIntoFrame({
tabId,
frameId: frame.frameId,
parentFrameId: frame.parentFrameId,
url: frame.url,
}, frames, config)))
}
export function setupIframeInjection() {
browser.tabs.onRemoved.addListener(clearTabDocumentState)
browser.webNavigation.onBeforeNavigate.addListener((details) => {
@ -82,86 +203,24 @@ export function setupIframeInjection() {
clearFrameInjectedDocumentState(details.tabId, details.frameId)
})
// Listen for iframe loads and inject content scripts programmatically
// This catches iframes that Chrome's manifest-based all_frames: true misses
// (e.g., dynamically created iframes, sandboxed iframes like edX)
// Only inject into subframes after page translation is enabled for the tab.
// This keeps iframe-heavy pages and benchmarks from paying content-script cost
// before the feature is actually used.
browser.webNavigation.onCompleted.addListener(async (details) => {
// Skip main frame (frameId === 0), only handle iframes
if (details.frameId === 0)
return
const frameKey = getFrameInjectionKey(details)
const documentKey = getDocumentInjectionKey(details)
if (documentKey) {
if (
pendingDocumentKeys.has(documentKey)
|| injectedDocumentKeysByFrame.get(frameKey) === documentKey
) {
return
}
pendingDocumentKeys.add(documentKey)
}
try {
let siteControlUrl: string | undefined
try {
const config = await getLocalConfig()
const frames = await browser.webNavigation.getAllFrames({ tabId: details.tabId }) ?? []
const liveFrameIds = new Set(frames.map(frame => frame.frameId))
liveFrameIds.add(details.frameId)
pruneInjectedFrames(details.tabId, liveFrameIds)
siteControlUrl = resolveSiteControlUrl(
details.frameId,
details.url,
frames,
getParentFrameIdHint(details),
)
if (!siteControlUrl || !isSiteEnabled(siteControlUrl, config)) {
return
}
}
catch (error) {
logger.error("[Background][IframeInjection] Failed to resolve iframe injection prerequisites", error)
const isEnabled = await getPageTranslationEnabled(details.tabId)
if (!isEnabled)
return
}
try {
const target = getInjectionTarget(details) as Parameters<typeof browser.scripting.executeScript>[0]["target"]
await browser.scripting.executeScript({
target,
func: setInjectedSiteControlUrl,
args: [SITE_CONTROL_URL_WINDOW_KEY, siteControlUrl],
})
// Inject host.content script into the iframe
await browser.scripting.executeScript({
target,
files: ["/content-scripts/host.js"],
})
// Inject selection.content script into the iframe
await browser.scripting.executeScript({
target,
files: ["/content-scripts/selection.js"],
})
if (documentKey) {
injectedDocumentKeysByFrame.set(frameKey, documentKey)
}
}
catch (error) {
logger.warn("[Background][IframeInjection] Failed to inject iframe content scripts", error)
}
}
finally {
if (documentKey) {
pendingDocumentKeys.delete(documentKey)
}
catch (error) {
logger.warn("[Background][IframeInjection] Failed to read page translation state", error)
return
}
await injectHostContentIntoFrame(details)
})
}

View file

@ -109,7 +109,7 @@ export default defineBackground({
setupTTSPlaybackMessageHandlers()
void initMockData()
// Setup programmatic injection for iframes that Chrome's manifest-based all_frames misses
// Setup on-demand iframe injection after page translation is enabled.
setupIframeInjection()
},
})

View file

@ -0,0 +1,17 @@
import type { TranslationState } from "@/types/translation-state"
import { storage } from "#imports"
import { getTranslationStateKey } from "@/utils/constants/storage-keys"
export async function getPageTranslationEnabled(tabId: number): Promise<boolean> {
const state = await storage.getItem<TranslationState>(
getTranslationStateKey(tabId),
)
return state?.enabled ?? false
}
export async function setPageTranslationEnabled(tabId: number, enabled: boolean): Promise<void> {
await storage.setItem<TranslationState>(
getTranslationStateKey(tabId),
{ enabled },
)
}

View file

@ -1,5 +1,5 @@
import type { FeatureUsageContext } from "@/types/analytics"
import type { Config } from "@/types/config/config"
import type { TranslationState } from "@/types/translation-state"
import { browser, storage } from "#imports"
import { ANALYTICS_FEATURE, ANALYTICS_SURFACE } from "@/types/analytics"
import { createFeatureUsageContext } from "@/utils/analytics"
@ -8,6 +8,22 @@ import { getTranslationStateKey } from "@/utils/constants/storage-keys"
import { shouldEnableAutoTranslation } from "@/utils/host/translate/auto-translation"
import { logger } from "@/utils/logger"
import { onMessage, sendMessage } from "@/utils/message"
import { injectHostContentIntoTabIframes } from "./iframe-injection"
import { getPageTranslationEnabled, setPageTranslationEnabled } from "./page-translation-state"
function notifyPageTranslationStateChanged(tabId: number, enabled: boolean) {
void sendMessage("notifyTranslationStateChanged", { enabled }, tabId)
.catch(error => logger.warn("Failed to notify page translation state change", error))
}
function requestManagerToTogglePageTranslation(
tabId: number,
enabled: boolean,
analyticsContext?: FeatureUsageContext,
) {
void sendMessage("askManagerToTogglePageTranslation", { enabled, analyticsContext }, tabId)
.catch(error => logger.warn("Failed to ask page translation manager to toggle", error))
}
export function translationMessage() {
onMessage("getEnablePageTranslationByTabId", async (msg) => {
@ -26,7 +42,11 @@ export function translationMessage() {
onMessage("tryToSetEnablePageTranslationByTabId", async (msg) => {
const { tabId, enabled, analyticsContext } = msg.data
void sendMessage("askManagerToTogglePageTranslation", { enabled, analyticsContext }, tabId)
if (!enabled) {
await setPageTranslationEnabled(tabId, false)
notifyPageTranslationStateChanged(tabId, false)
}
requestManagerToTogglePageTranslation(tabId, enabled, analyticsContext)
})
onMessage("tryToSetEnablePageTranslationOnContentScript", async (msg) => {
@ -34,7 +54,11 @@ export function translationMessage() {
const { enabled, analyticsContext } = msg.data
if (typeof tabId === "number") {
logger.info("sending tryToSetEnablePageTranslationOnContentScript to manager", { enabled, tabId })
await sendMessage("askManagerToTogglePageTranslation", { enabled, analyticsContext }, tabId)
if (!enabled) {
await setPageTranslationEnabled(tabId, false)
notifyPageTranslationStateChanged(tabId, false)
}
requestManagerToTogglePageTranslation(tabId, enabled, analyticsContext)
}
else {
logger.error("tabId is not a number", msg)
@ -50,10 +74,11 @@ export function translationMessage() {
return
const shouldEnable = await shouldEnableAutoTranslation(url, detectedCodeOrUnd, config)
if (shouldEnable) {
void sendMessage("askManagerToTogglePageTranslation", {
enabled: true,
analyticsContext: createFeatureUsageContext(ANALYTICS_FEATURE.PAGE_TRANSLATION, ANALYTICS_SURFACE.PAGE_AUTO),
}, tabId)
requestManagerToTogglePageTranslation(
tabId,
true,
createFeatureUsageContext(ANALYTICS_FEATURE.PAGE_TRANSLATION, ANALYTICS_SURFACE.PAGE_AUTO),
)
}
}
})
@ -62,11 +87,13 @@ export function translationMessage() {
const tabId = msg.sender?.tab?.id
const { enabled } = msg.data
if (typeof tabId === "number") {
await storage.setItem<TranslationState>(
getTranslationStateKey(tabId),
{ enabled },
)
void sendMessage("notifyTranslationStateChanged", { enabled }, tabId)
await setPageTranslationEnabled(tabId, enabled)
notifyPageTranslationStateChanged(tabId, enabled)
const senderFrameId = msg.sender?.frameId
if (enabled && (senderFrameId === 0 || senderFrameId === undefined)) {
void injectHostContentIntoTabIframes(tabId)
}
}
else {
logger.error("tabId is not a number", msg)
@ -75,10 +102,7 @@ export function translationMessage() {
// === Helper Functions ===
async function getTranslationState(tabId: number): Promise<boolean> {
const state = await storage.getItem<TranslationState>(
getTranslationStateKey(tabId),
)
return state?.enabled ?? false
return await getPageTranslationEnabled(tabId)
}
// === Cleanup ===

View file

@ -12,7 +12,6 @@ declare global {
export default defineContentScript({
matches: ["*://*/*", "file:///*"],
cssInjectionMode: "manual",
allFrames: true,
async main(ctx) {
// Prevent double injection (manifest-based + programmatic injection)
if (window.__READ_FROG_HOST_INJECTED__)

View file

@ -78,6 +78,15 @@ export async function bootstrapHostContent(ctx: ContentScriptContext, initialCon
enabled ? void manager.start(window === window.top ? analyticsContext : undefined) : manager.stop()
})
const cleanupFrameTranslationStateListener = window === window.top
? () => {}
: onMessage("notifyTranslationStateChanged", (msg) => {
const { enabled } = msg.data
if (enabled === manager.isActive)
return
enabled ? void manager.start() : manager.stop()
})
ctx.onInvalidated(() => {
removeHostToast()
cleanupUrlListener()
@ -85,6 +94,7 @@ export async function bootstrapHostContent(ctx: ContentScriptContext, initialCon
cleanupPageTranslationTriggers()
cleanupTranslationShortcut()
cleanupTranslationStateListener()
cleanupFrameTranslationStateListener()
window.removeEventListener("extension:URLChange", handleExtensionUrlChange)
window.__READ_FROG_HOST_INJECTED__ = false
clearEffectiveSiteControlUrl()

View file

@ -85,7 +85,6 @@ async function mountSelectionUI(ctx: ContentScriptContext) {
export default defineContentScript({
matches: ["*://*/*", "file:///*"],
cssInjectionMode: "ui",
allFrames: true,
async main(ctx) {
// Prevent double injection (manifest-based + programmatic injection)
if (window.__READ_FROG_SELECTION_INJECTED__)