-
-
Notifications
You must be signed in to change notification settings - Fork 165
feat: add context menu translate option #718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
guoyongchang
wants to merge
12
commits into
mengxi-ream:main
Choose a base branch
from
guoyongchang:feat/context-menu-translate
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+340
−1
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
bf3841e
feat: add context menu translate option
guoyongchang b224925
fix: address context menu initialization and update issues
guoyongchang aa5c087
refactor: remove unused readEnabled from context menu config
guoyongchang 806a140
fix: only update menu title for active tab in auto-translation
guoyongchang 46b0b8c
fix: use storage listener instead of message handlers
guoyongchang cd8d7f5
feat: add validation for context menu translation
guoyongchang bca207f
fix: ensure context menu listeners persist across service worker life…
guoyongchang 9cdfac8
Merge branch 'main' into feat/context-menu-translate
mengxi-ream e47154f
fix: restore synchronous listener registration in background script
guoyongchang fd9aa1c
fix: register context menu listeners synchronously to prevent event loss
guoyongchang ba88b79
refactor: remove version migration to keep feature branch clean
guoyongchang dcbd358
Merge branch 'mengxi-ream:main' into feat/context-menu-translate
guoyongchang File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| --- | ||
| "@read-frog/extension": minor | ||
| --- | ||
|
|
||
| feat: add context menu translate option | ||
|
|
||
| Add right-click context menu option for translating pages directly from the browser context menu. | ||
|
|
||
| **Important**: This feature requires a new `contextMenus` permission. When upgrading, your browser may prompt you to approve this new permission and temporarily disable the extension until approved. This is normal browser behavior for permission changes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| import type { Browser } from '#imports' | ||
| import type { Config } from '@/types/config/config' | ||
| import { browser, i18n, storage } from '#imports' | ||
| import { isAPIProviderConfig } from '@/types/config/provider' | ||
| import { getProviderConfigById } from '@/utils/config/helpers' | ||
| import { CONFIG_STORAGE_KEY } from '@/utils/constants/config' | ||
| import { getTranslationStateKey, TRANSLATION_STATE_KEY_PREFIX } from '@/utils/constants/storage-keys' | ||
| import { logger } from '@/utils/logger' | ||
| import { sendMessage } from '@/utils/message' | ||
| import { ensureInitializedConfig } from './config' | ||
|
|
||
| const MENU_ID_TRANSLATE = 'read-frog-translate' | ||
|
|
||
| let currentConfig: Config | null = null | ||
|
|
||
| /** | ||
| * Register all context menu event listeners synchronously | ||
| * This must be called during main() execution to ensure listeners are registered | ||
| * before Chrome completes initialization | ||
| */ | ||
| export function registerContextMenuListeners() { | ||
| // Listen for config changes using native Chrome API for persistence | ||
| // This ensures the listener survives service worker sleep/wake cycles | ||
| browser.storage.local.onChanged.addListener(async (changes) => { | ||
| const configChange = changes[CONFIG_STORAGE_KEY] | ||
| if (configChange?.newValue) { | ||
| currentConfig = configChange.newValue as Config | ||
| await updateContextMenuItems(currentConfig) | ||
| } | ||
| }) | ||
|
|
||
| // Listen for tab activation to update menu title | ||
| browser.tabs.onActivated.addListener(async (activeInfo) => { | ||
| await updateTranslateMenuTitle(activeInfo.tabId) | ||
| }) | ||
|
|
||
| // Listen for tab updates (e.g., navigation) | ||
| browser.tabs.onUpdated.addListener(async (tabId, changeInfo) => { | ||
| if (changeInfo.status === 'complete') { | ||
| await updateTranslateMenuTitle(tabId) | ||
| } | ||
| }) | ||
|
|
||
| // Listen for translation state changes in storage | ||
| // This ensures menu updates when translation is toggled from any UI | ||
| // (floating button, auto-translate, etc.) without interfering with | ||
| // the translation logic in translation-signal.ts | ||
| browser.storage.session.onChanged.addListener(async (changes) => { | ||
| for (const [key, change] of Object.entries(changes)) { | ||
| // Check if this is a translation state change | ||
| if (key.startsWith(TRANSLATION_STATE_KEY_PREFIX.replace('session:', ''))) { | ||
| // Extract tabId from key (format: "translationState.{tabId}") | ||
| const parts = key.split('.') | ||
| const tabId = Number.parseInt(parts[1]) | ||
|
|
||
| if (!Number.isNaN(tabId)) { | ||
| // Only update menu if this is the active tab | ||
| const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true }) | ||
| if (activeTab?.id === tabId) { | ||
| const newValue = change.newValue as { enabled: boolean } | undefined | ||
| await updateTranslateMenuTitle(tabId, newValue?.enabled) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| // Handle menu item clicks | ||
| browser.contextMenus.onClicked.addListener(handleContextMenuClick) | ||
| } | ||
|
|
||
| /** | ||
| * Initialize context menu items based on config | ||
| * This can be called asynchronously after listeners are registered | ||
| */ | ||
| export async function initializeContextMenu() { | ||
| // Ensure config is initialized before setting up context menu | ||
| const config = await ensureInitializedConfig() | ||
| if (!config) { | ||
| return | ||
| } | ||
|
|
||
| currentConfig = config | ||
| await updateContextMenuItems(config) | ||
| } | ||
|
|
||
| /** | ||
| * Update context menu items based on config | ||
| */ | ||
| async function updateContextMenuItems(config: Config) { | ||
| // Remove all existing menu items first | ||
| await browser.contextMenus.removeAll() | ||
|
|
||
| const { translateEnabled } = config.contextMenu | ||
|
|
||
| if (translateEnabled) { | ||
| browser.contextMenus.create({ | ||
| id: MENU_ID_TRANSLATE, | ||
| title: i18n.t('contextMenu.translate'), | ||
| contexts: ['page'], | ||
| }) | ||
| } | ||
|
|
||
| // Update translate menu title for current tab | ||
| const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true }) | ||
| if (activeTab?.id) { | ||
| await updateTranslateMenuTitle(activeTab.id) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Update translate menu title based on current translation state | ||
| * @param tabId - The tab ID to check translation state for | ||
| * @param enabled - Optional: if provided, use this value instead of reading from storage | ||
| */ | ||
| async function updateTranslateMenuTitle(tabId: number, enabled?: boolean) { | ||
| if (!currentConfig?.contextMenu.translateEnabled) { | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| let isTranslated: boolean | ||
| if (enabled !== undefined) { | ||
| isTranslated = enabled | ||
| } | ||
| else { | ||
| const state = await storage.getItem<{ enabled: boolean }>( | ||
| getTranslationStateKey(tabId), | ||
| ) | ||
| isTranslated = state?.enabled ?? false | ||
| } | ||
|
|
||
| await browser.contextMenus.update(MENU_ID_TRANSLATE, { | ||
| title: isTranslated | ||
| ? i18n.t('contextMenu.showOriginal') | ||
| : i18n.t('contextMenu.translate'), | ||
| }) | ||
| } | ||
| catch { | ||
| // Menu item might not exist if translateEnabled is false | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle context menu item click | ||
| */ | ||
| async function handleContextMenuClick( | ||
| info: Browser.contextMenus.OnClickData, | ||
| tab?: Browser.tabs.Tab, | ||
| ) { | ||
| if (!tab?.id) { | ||
| return | ||
| } | ||
|
|
||
| if (info.menuItemId === MENU_ID_TRANSLATE) { | ||
| await handleTranslateClick(tab.id) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Validate translation configuration in background context | ||
| * This is a simplified version of validateTranslationConfig without toast notifications | ||
| */ | ||
| function validateTranslationConfigInBackground(config: Config): boolean { | ||
| const { providersConfig, translate: translateConfig, language: languageConfig } = config | ||
| const providerConfig = getProviderConfigById(providersConfig, translateConfig.providerId) | ||
|
|
||
| // Check if provider exists | ||
| if (!providerConfig) { | ||
| logger.warn('[ContextMenu] Translation provider not found') | ||
| return false | ||
| } | ||
|
|
||
| // Check if source and target languages are the same | ||
| if (languageConfig.sourceCode === languageConfig.targetCode) { | ||
| logger.warn('[ContextMenu] Source and target languages are the same') | ||
| return false | ||
| } | ||
|
|
||
| // Check if detected language and target language are the same in auto mode | ||
| // Note: Unlike the case above, this only warns but does NOT block translation | ||
| // This matches the behavior of validateTranslationConfig in translate-text.ts | ||
| if (languageConfig.sourceCode === 'auto' && languageConfig.detectedCode === languageConfig.targetCode) { | ||
| logger.warn('[ContextMenu] Detected language matches target language in auto mode') | ||
| // Don't return false - allow translation to proceed with warning | ||
| } | ||
|
|
||
| // Check if API key is configured for providers that require it | ||
| if (isAPIProviderConfig(providerConfig) && !providerConfig.apiKey?.trim() && !['deeplx', 'ollama'].includes(providerConfig.provider)) { | ||
| logger.warn('[ContextMenu] API key not configured for provider') | ||
| return false | ||
| } | ||
|
|
||
| return true | ||
| } | ||
|
|
||
| /** | ||
| * 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 newState = !isCurrentlyTranslated | ||
|
|
||
| // If enabling translation, validate configuration first | ||
| if (newState && currentConfig) { | ||
| if (!validateTranslationConfigInBackground(currentConfig)) { | ||
| logger.error('[ContextMenu] Translation config validation failed') | ||
| // Send a message to content script to show error notification | ||
| void sendMessage('showTranslationConfigError', undefined, tabId) | ||
| return | ||
| } | ||
| } | ||
|
|
||
| // Update storage directly (instead of sending message to self) | ||
| await storage.setItem(getTranslationStateKey(tabId), { enabled: newState }) | ||
|
|
||
| // Notify content script in that specific tab | ||
| void sendMessage('translationStateChanged', { enabled: newState }, tabId) | ||
|
|
||
| // Update menu title immediately | ||
| await updateTranslateMenuTitle(tabId) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
src/entrypoints/options/pages/floating-button-and-toolbar/context-menu-translate-toggle.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { i18n } from '#imports' | ||
| import { useAtom } from 'jotai' | ||
| import { Switch } from '@/components/shadcn/switch' | ||
| import { configFieldsAtomMap } from '@/utils/atoms/config' | ||
| import { ConfigCard } from '../../components/config-card' | ||
|
|
||
| export function ContextMenuTranslateToggle() { | ||
| const [contextMenu, setContextMenu] = useAtom( | ||
| configFieldsAtomMap.contextMenu, | ||
| ) | ||
|
|
||
| return ( | ||
| <ConfigCard | ||
| title={i18n.t('options.floatingButtonAndToolbar.contextMenu.translate.title')} | ||
| description={i18n.t('options.floatingButtonAndToolbar.contextMenu.translate.description')} | ||
| > | ||
| <div className="w-full flex justify-end"> | ||
| <Switch | ||
| checked={contextMenu.translateEnabled} | ||
| onCheckedChange={(checked) => { | ||
| void setContextMenu({ ...contextMenu, translateEnabled: checked }) | ||
| }} | ||
| /> | ||
| </div> | ||
| </ConfigCard> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Auto-mode same-language detection now blocks translation in the context menu, unlike the existing validation which only warns, so right-click translations get incorrectly rejected whenever the detected language equals the target language.
Prompt for AI agents
✅ Addressed in
bca207f