mirror of
https://github.com/mengxi-ream/read-frog.git
synced 2026-09-01 15:36:33 +08:00
feat(translation-hub): open the Translation Hub with a keyboard shortcut (#2050)
The hub is only reachable from the popup icon and the options sidebar, so people who use it as their free-form translation input have to dig for it every time. Adds a configurable hotkey (default Alt+Shift+H) bound in the host content script alongside the existing translation shortcuts, which asks the background to open the hub through the same `openPage` route the popup button and sidebar link already use. - New `translationHub.shortcut` config key, validated by the shared shortcut schema; migration v093 -> v094 backfills it and falls back to Alt+Shift+U when the primary key is already bound elsewhere. - New row on the Shortcuts settings page, registered in the command palette's search items and translated into all 9 locales. - The popup hub button's tooltip now shows the key hint, matching the translate button. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@read-frog/extension": patch
|
||||
---
|
||||
|
||||
feat(translation-hub): open the Translation Hub with a keyboard shortcut (Alt+Shift+H)
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Hotkey } from "@tanstack/hotkeys"
|
||||
import { HotkeyManager } from "@tanstack/hotkeys"
|
||||
import { browser } from "#imports"
|
||||
import { getLocalConfig } from "@/utils/config/storage"
|
||||
import { TRANSLATION_HUB_PAGE_PATH } from "@/utils/constants/translation-hub"
|
||||
import { sendMessage } from "@/utils/message"
|
||||
import {
|
||||
isPageTranslationShortcutEmpty,
|
||||
isValidConfiguredPageTranslationShortcut,
|
||||
} from "@/utils/page-translation-shortcut"
|
||||
|
||||
/**
|
||||
* Opens the Translation Hub from any page. A content script cannot create a
|
||||
* tab itself, so the background does it — the same `openPage` route the popup
|
||||
* button and the options sidebar link already use.
|
||||
*/
|
||||
export async function bindTranslationHubShortcutKey() {
|
||||
const config = await getLocalConfig()
|
||||
if (!config || isPageTranslationShortcutEmpty(config.translationHub.shortcut)) {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
const shortcut = config.translationHub.shortcut
|
||||
if (!isValidConfiguredPageTranslationShortcut(shortcut)) {
|
||||
return () => {}
|
||||
}
|
||||
|
||||
const registration = HotkeyManager.getInstance().register(
|
||||
shortcut as Hotkey,
|
||||
() => {
|
||||
void sendMessage("openPage", {
|
||||
url: browser.runtime.getURL(TRANSLATION_HUB_PAGE_PATH),
|
||||
active: true,
|
||||
})
|
||||
},
|
||||
{
|
||||
ignoreInputs: true,
|
||||
preventDefault: true,
|
||||
stopPropagation: true,
|
||||
},
|
||||
)
|
||||
|
||||
return () => {
|
||||
registration.unregister()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { logger } from "@/utils/logger"
|
||||
import { onMessage, sendMessage } from "@/utils/message"
|
||||
import { clearEffectiveSiteControlUrl } from "@/utils/site-control"
|
||||
import { areSamePageTranslationOrigin } from "@/utils/url"
|
||||
import { bindTranslationHubShortcutKey } from "./bind-translation-hub-shortcut"
|
||||
import { setupUrlChangeListener } from "./listen"
|
||||
import { mountHostToast } from "./mount-host-toast"
|
||||
import { bindTranslationModeShortcutKey } from "./translation-control/bind-translation-mode-shortcut"
|
||||
@@ -40,6 +41,8 @@ export async function bootstrapHostContent(
|
||||
|
||||
const cleanupTranslationModeShortcut = await bindTranslationModeShortcutKey()
|
||||
|
||||
const cleanupTranslationHubShortcut = await bindTranslationHubShortcutKey()
|
||||
|
||||
const detectAndReportPageLanguage = async (url: string) => {
|
||||
const { detectedCodeOrUnd } = await detectPageLanguageLightweight()
|
||||
void sendMessage("reportDetectedPageLanguage", { url, detectedCodeOrUnd })
|
||||
@@ -127,6 +130,7 @@ export async function bootstrapHostContent(
|
||||
cleanupPageTranslationTriggers()
|
||||
cleanupTranslationShortcut()
|
||||
cleanupTranslationModeShortcut()
|
||||
cleanupTranslationHubShortcut()
|
||||
cleanupTranslationStateListener()
|
||||
cleanupFrameTranslationStateListener()
|
||||
cleanupDetectedLanguageRefreshListener()
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
} from "@/components/ui/base-ui/sidebar"
|
||||
import { TRANSLATION_HUB_PAGE_PATH } from "@/utils/constants/translation-hub"
|
||||
import { i18n } from "@/utils/i18n"
|
||||
|
||||
const OVERLAY_TOOLS_PATHS = ["/floating-button", "/selection-toolbar", "/context-menu"] as const
|
||||
@@ -137,7 +138,7 @@ export function FeaturesNav() {
|
||||
<SidebarMenuButton
|
||||
render={
|
||||
<a
|
||||
href={browser.runtime.getURL("/translation-hub.html")}
|
||||
href={browser.runtime.getURL(TRANSLATION_HUB_PAGE_PATH)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
/>
|
||||
|
||||
@@ -135,6 +135,13 @@ export const SEARCH_ITEMS: SearchItem[] = [
|
||||
descriptionKey: "options.shortcuts.nodeTranslation.description",
|
||||
pageKey: "options.shortcuts.title",
|
||||
},
|
||||
{
|
||||
sectionId: "translation-hub-shortcut",
|
||||
route: "/shortcuts",
|
||||
titleKey: "options.shortcuts.translationHub.title",
|
||||
descriptionKey: "options.shortcuts.translationHub.description",
|
||||
pageKey: "options.shortcuts.title",
|
||||
},
|
||||
|
||||
// API Providers page
|
||||
{
|
||||
|
||||
@@ -33,6 +33,10 @@ vi.mock("../node-translation-hotkey", () => ({
|
||||
NodeTranslationHotkey: () => <section data-section="node-translation-hotkey" />,
|
||||
}))
|
||||
|
||||
vi.mock("../translation-hub-shortcut", () => ({
|
||||
TranslationHubShortcut: () => <section data-section="translation-hub-shortcut" />,
|
||||
}))
|
||||
|
||||
describe("shortcuts page", () => {
|
||||
it("lists every shortcut, widest scope first", () => {
|
||||
const { container } = render(<ShortcutsPage />)
|
||||
@@ -47,6 +51,7 @@ describe("shortcuts page", () => {
|
||||
"selection-translation-shortcut",
|
||||
"subtitles-toggle-shortcut",
|
||||
"node-translation-hotkey",
|
||||
"translation-hub-shortcut",
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,11 +4,14 @@ import { NodeTranslationHotkey } from "./node-translation-hotkey"
|
||||
import { PageTranslationShortcut } from "./page-translation-shortcut"
|
||||
import { SelectionTranslationShortcut } from "./selection-translation-shortcut"
|
||||
import { SubtitlesToggleShortcut } from "./subtitles-toggle-shortcut"
|
||||
import { TranslationHubShortcut } from "./translation-hub-shortcut"
|
||||
import { TranslationModeShortcut } from "./translation-mode-shortcut"
|
||||
|
||||
/**
|
||||
* Every shortcut in one flat list. The page is short enough that sections would only add
|
||||
* headings between four rows.
|
||||
* headings between four rows. Reading shortcuts come first, narrowing scope as the list
|
||||
* goes down; the Translation Hub row is last because it opens a page instead of acting on
|
||||
* what is already on screen.
|
||||
*/
|
||||
export function ShortcutsPage() {
|
||||
return (
|
||||
@@ -22,6 +25,7 @@ export function ShortcutsPage() {
|
||||
<SelectionTranslationShortcut />
|
||||
<SubtitlesToggleShortcut />
|
||||
<NodeTranslationHotkey />
|
||||
<TranslationHubShortcut />
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useAtom } from "jotai"
|
||||
import { configFieldsAtomMap } from "@/utils/atoms/config"
|
||||
import { i18n } from "@/utils/i18n"
|
||||
import { ShortcutConfigItem } from "./shortcut-config-item"
|
||||
|
||||
export function TranslationHubShortcut() {
|
||||
const [translationHub, setTranslationHub] = useAtom(configFieldsAtomMap.translationHub)
|
||||
|
||||
return (
|
||||
<ShortcutConfigItem
|
||||
id="translation-hub-shortcut"
|
||||
title={i18n.t("options.shortcuts.translationHub.title")}
|
||||
description={i18n.t("options.shortcuts.translationHub.description")}
|
||||
shortcut={translationHub.shortcut}
|
||||
onChange={(nextShortcut) => {
|
||||
void setTranslationHub({ shortcut: nextShortcut })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,30 @@
|
||||
import { Icon } from "@iconify/react"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { browser } from "#imports"
|
||||
import { Button } from "@/components/ui/base-ui/button"
|
||||
import { Kbd, KbdGroup } from "@/components/ui/base-ui/kbd"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/base-ui/tooltip"
|
||||
import { configFieldsAtomMap } from "@/utils/atoms/config"
|
||||
import { TRANSLATION_HUB_PAGE_PATH } from "@/utils/constants/translation-hub"
|
||||
import { i18n } from "@/utils/i18n"
|
||||
import { formatHotkeyParts } from "@/utils/os"
|
||||
import { isPageTranslationShortcutEmpty } from "@/utils/page-translation-shortcut"
|
||||
|
||||
export function TranslationHubButton() {
|
||||
const translationHub = useAtomValue(configFieldsAtomMap.translationHub)
|
||||
|
||||
const handleClick = async () => {
|
||||
await browser.tabs.create({
|
||||
url: browser.runtime.getURL("/translation-hub.html"),
|
||||
url: browser.runtime.getURL(TRANSLATION_HUB_PAGE_PATH),
|
||||
})
|
||||
}
|
||||
|
||||
// The hub is buried enough that people ask where it lives; the tooltip is the
|
||||
// one place that can teach the shortcut at the moment they reach for it.
|
||||
const shortcutParts = isPageTranslationShortcutEmpty(translationHub.shortcut)
|
||||
? []
|
||||
: formatHotkeyParts(translationHub.shortcut)
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<Button variant="ghost" size="icon" onClick={handleClick} />}>
|
||||
@@ -18,6 +32,13 @@ export function TranslationHubButton() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[200px] text-wrap">
|
||||
{i18n.t("popup.hub.tooltip")}
|
||||
{shortcutParts.length > 0 && (
|
||||
<KbdGroup className="mt-1.5 flex">
|
||||
{shortcutParts.map((part) => (
|
||||
<Kbd key={part}>{part}</Kbd>
|
||||
))}
|
||||
</KbdGroup>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -458,6 +458,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: Paragraph translation
|
||||
description: Hover or long-press a paragraph to translate just that one.
|
||||
translationHub:
|
||||
title: Translation Hub
|
||||
description: Open the Translation Hub in a new tab.
|
||||
overlayTools:
|
||||
title: Overlay Tools
|
||||
inputTranslation:
|
||||
|
||||
@@ -458,6 +458,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: Traducción de párrafo
|
||||
description: Pasa el cursor o mantén pulsado un párrafo para traducir solo ese.
|
||||
translationHub:
|
||||
title: Centro de traducción
|
||||
description: Abre el Centro de traducción en una pestaña nueva.
|
||||
overlayTools:
|
||||
title: Herramientas superpuestas
|
||||
inputTranslation:
|
||||
|
||||
@@ -457,6 +457,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: 段落翻訳
|
||||
description: 段落にホバーまたは長押しすると、その段落だけ翻訳します。
|
||||
translationHub:
|
||||
title: 翻訳ハブ
|
||||
description: 翻訳ハブを新しいタブで開きます。
|
||||
overlayTools:
|
||||
title: オーバーレイツール
|
||||
inputTranslation:
|
||||
|
||||
@@ -457,6 +457,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: 단락 번역
|
||||
description: 단락에 마우스를 올리거나 길게 누르면 그 단락만 번역합니다.
|
||||
translationHub:
|
||||
title: 번역 허브
|
||||
description: 번역 허브를 새 탭에서 엽니다.
|
||||
overlayTools:
|
||||
title: 오버레이 도구
|
||||
inputTranslation:
|
||||
|
||||
@@ -457,6 +457,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: Перевод абзаца
|
||||
description: Наведите курсор на абзац или нажмите и удерживайте его, чтобы перевести только его.
|
||||
translationHub:
|
||||
title: Центр переводов
|
||||
description: Открывает Центр переводов в новой вкладке.
|
||||
overlayTools:
|
||||
title: Инструменты наложения
|
||||
inputTranslation:
|
||||
|
||||
@@ -457,6 +457,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: Paragraf çevirisi
|
||||
description: Bir paragrafın üzerine gelin veya uzun basın; yalnızca o paragraf çevrilir.
|
||||
translationHub:
|
||||
title: Çeviri Merkezi
|
||||
description: Çeviri Merkezini yeni bir sekmede açar.
|
||||
overlayTools:
|
||||
title: Kaplama Araçları
|
||||
inputTranslation:
|
||||
|
||||
@@ -457,6 +457,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: Dịch đoạn văn
|
||||
description: Di chuột hoặc nhấn giữ một đoạn để chỉ dịch đoạn đó.
|
||||
translationHub:
|
||||
title: Trung tâm Dịch thuật
|
||||
description: Mở Trung tâm Dịch thuật trong tab mới.
|
||||
overlayTools:
|
||||
title: Công cụ nổi
|
||||
inputTranslation:
|
||||
|
||||
@@ -458,6 +458,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: 段落翻译
|
||||
description: 悬停或长按段落,只翻译这一段。
|
||||
translationHub:
|
||||
title: 翻译中心
|
||||
description: 在新标签页中打开翻译中心。
|
||||
overlayTools:
|
||||
title: 悬浮工具
|
||||
inputTranslation:
|
||||
|
||||
@@ -458,6 +458,9 @@ options:
|
||||
nodeTranslation:
|
||||
title: 段落翻譯
|
||||
description: 滑鼠停留或長按段落,只翻譯這一段。
|
||||
translationHub:
|
||||
title: 翻譯中心
|
||||
description: 在新分頁中開啟翻譯中心。
|
||||
overlayTools:
|
||||
title: 懸浮工具
|
||||
inputTranslation:
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MIN_SELECTION_OVERLAY_OPACITY,
|
||||
} from "@/utils/constants/selection"
|
||||
import { MIN_SIDE_CONTENT_WIDTH } from "@/utils/constants/side"
|
||||
import { DEFAULT_TRANSLATION_HUB_SHORTCUT_KEY } from "@/utils/constants/translation-hub"
|
||||
import {
|
||||
doesProviderSupportsCapability,
|
||||
getProviderIdsForCapability,
|
||||
@@ -75,6 +76,16 @@ const sideContentSchema = z.object({
|
||||
width: z.number().min(MIN_SIDE_CONTENT_WIDTH),
|
||||
})
|
||||
|
||||
// Translation Hub schema. `.default()` mirrors `uiLanguageSchema`: it lets a
|
||||
// config stored before this field existed still parse in UI contexts that load
|
||||
// ahead of the background migration, instead of falling back to DEFAULT_CONFIG
|
||||
// and writing that over the user's settings.
|
||||
const translationHubSchema = z
|
||||
.object({
|
||||
shortcut: pageTranslationShortcutSchema,
|
||||
})
|
||||
.default({ shortcut: DEFAULT_TRANSLATION_HUB_SHORTCUT_KEY })
|
||||
|
||||
// beta experience schema
|
||||
const betaExperienceSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
@@ -141,6 +152,7 @@ export const configSchema = z
|
||||
siteControl: siteControlSchema,
|
||||
siteRules: siteRulesConfigSchema,
|
||||
uiLanguage: uiLanguageSchema,
|
||||
translationHub: translationHubSchema,
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
for (const featureKey of FEATURE_KEYS) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { migrate } from "../../migration-scripts/v093-to-v094"
|
||||
|
||||
function configWith({
|
||||
pageShortcut = "Alt+E",
|
||||
modeShortcut = "Alt+Shift+M",
|
||||
selectionShortcut = "Alt+T",
|
||||
subtitlesShortcut = "Alt+C",
|
||||
}: {
|
||||
pageShortcut?: string
|
||||
modeShortcut?: string
|
||||
selectionShortcut?: string
|
||||
subtitlesShortcut?: string
|
||||
} = {}) {
|
||||
return {
|
||||
translate: {
|
||||
modeShortcut,
|
||||
page: { shortcut: pageShortcut },
|
||||
},
|
||||
selectionToolbar: {
|
||||
features: {
|
||||
translate: { shortcut: selectionShortcut },
|
||||
},
|
||||
},
|
||||
videoSubtitles: { enabled: true, toggleShortcut: subtitlesShortcut },
|
||||
}
|
||||
}
|
||||
|
||||
describe("v093 to v094 migration", () => {
|
||||
it("adds the primary hub shortcut when nothing else claims it", () => {
|
||||
const result = migrate(configWith())
|
||||
|
||||
expect(result.translationHub).toEqual({ shortcut: "Alt+Shift+H" })
|
||||
})
|
||||
|
||||
it("leaves the rest of the config untouched", () => {
|
||||
const oldConfig = configWith()
|
||||
const snapshot = structuredClone(oldConfig)
|
||||
|
||||
const result = migrate(oldConfig)
|
||||
|
||||
expect(result).toEqual({ ...snapshot, translationHub: { shortcut: "Alt+Shift+H" } })
|
||||
expect(oldConfig).toEqual(snapshot)
|
||||
})
|
||||
|
||||
it.each([
|
||||
["page translation", { pageShortcut: "Alt+Shift+H" }],
|
||||
["translation mode", { modeShortcut: "Alt+Shift+H" }],
|
||||
["selection translation", { selectionShortcut: "Alt+Shift+H" }],
|
||||
["subtitles toggle", { subtitlesShortcut: "Alt+Shift+H" }],
|
||||
])("falls back when %s already uses the primary key", (_label, overrides) => {
|
||||
const result = migrate(configWith(overrides))
|
||||
|
||||
expect(result.translationHub.shortcut).toBe("Alt+Shift+U")
|
||||
})
|
||||
|
||||
it("ignores case and surrounding whitespace when detecting a collision", () => {
|
||||
const result = migrate(configWith({ modeShortcut: " alt+shift+h " }))
|
||||
|
||||
expect(result.translationHub.shortcut).toBe("Alt+Shift+U")
|
||||
})
|
||||
|
||||
it("leaves the shortcut unbound when both candidates are taken", () => {
|
||||
const result = migrate(configWith({ pageShortcut: "Alt+Shift+H", modeShortcut: "Alt+Shift+U" }))
|
||||
|
||||
expect(result.translationHub.shortcut).toBe("")
|
||||
})
|
||||
|
||||
it("is idempotent", () => {
|
||||
const once = migrate(configWith({ pageShortcut: "Alt+Shift+H" }))
|
||||
const twice = migrate(once)
|
||||
|
||||
expect(twice).toEqual(once)
|
||||
})
|
||||
|
||||
it("does not overwrite an existing translationHub config", () => {
|
||||
const oldConfig = { ...configWith(), translationHub: { shortcut: "" } }
|
||||
|
||||
expect(migrate(oldConfig)).toBe(oldConfig)
|
||||
})
|
||||
|
||||
it.each([
|
||||
["null", null],
|
||||
["a non-object", "config"],
|
||||
["an array", []],
|
||||
])("returns %s configs unchanged", (_label, oldConfig) => {
|
||||
expect(migrate(oldConfig)).toBe(oldConfig)
|
||||
})
|
||||
|
||||
it("still adds the shortcut when the sibling shortcut fields are missing", () => {
|
||||
const result = migrate({ language: { targetCode: "cmn" } })
|
||||
|
||||
expect(result.translationHub).toEqual({ shortcut: "Alt+Shift+H" })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Migration script from v093 to v094
|
||||
* - Adds the `translationHub` config with a `shortcut` that opens the
|
||||
* Translation Hub page, defaulting to "Alt+Shift+H".
|
||||
* - Falls back to "Alt+Shift+U" when the user already bound "Alt+Shift+H" to
|
||||
* another shortcut, and leaves it unbound when both candidates are taken.
|
||||
* The fallback key deliberately exists nowhere else in the codebase: it only
|
||||
* ever applies to configs that predate this field.
|
||||
*
|
||||
* IMPORTANT: All values are hardcoded inline. Migration scripts are frozen
|
||||
* snapshots - never import constants or helpers that may change.
|
||||
*/
|
||||
|
||||
const PRIMARY_SHORTCUT = "Alt+Shift+H"
|
||||
const FALLBACK_SHORTCUT = "Alt+Shift+U"
|
||||
|
||||
/**
|
||||
* Shortcuts are stored already normalized (fixed modifier order, canonical key
|
||||
* name), so comparing the lowercased strings is enough to spot a collision.
|
||||
*/
|
||||
function collectBoundShortcuts(oldConfig: any): Set<string> {
|
||||
const bound = new Set<string>()
|
||||
const candidates = [
|
||||
oldConfig?.translate?.page?.shortcut,
|
||||
oldConfig?.translate?.modeShortcut,
|
||||
oldConfig?.selectionToolbar?.features?.translate?.shortcut,
|
||||
oldConfig?.videoSubtitles?.toggleShortcut,
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate !== "string") {
|
||||
continue
|
||||
}
|
||||
|
||||
const trimmed = candidate.trim()
|
||||
if (trimmed) {
|
||||
bound.add(trimmed.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
return bound
|
||||
}
|
||||
|
||||
export function migrate(oldConfig: any): any {
|
||||
if (!oldConfig || typeof oldConfig !== "object" || Array.isArray(oldConfig)) {
|
||||
return oldConfig
|
||||
}
|
||||
|
||||
if ("translationHub" in oldConfig) {
|
||||
return oldConfig
|
||||
}
|
||||
|
||||
const boundShortcuts = collectBoundShortcuts(oldConfig)
|
||||
const shortcut = !boundShortcuts.has(PRIMARY_SHORTCUT.toLowerCase())
|
||||
? PRIMARY_SHORTCUT
|
||||
: !boundShortcuts.has(FALLBACK_SHORTCUT.toLowerCase())
|
||||
? FALLBACK_SHORTCUT
|
||||
: ""
|
||||
|
||||
return {
|
||||
...oldConfig,
|
||||
translationHub: {
|
||||
shortcut,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
DEFAULT_SELECTION_TRANSLATION_SHORTCUT_KEY,
|
||||
DEFAULT_TRANSLATION_MODE_SHORTCUT_KEY,
|
||||
} from "./translate"
|
||||
import { DEFAULT_TRANSLATION_HUB_SHORTCUT_KEY } from "./translation-hub"
|
||||
import { TRANSLATION_NODE_STYLE_ON_INSTALLED } from "./translation-node-style"
|
||||
import { DEFAULT_TTS_CONFIG } from "./tts"
|
||||
|
||||
@@ -48,7 +49,7 @@ export const GOOGLE_DRIVE_TOKEN_STORAGE_KEY = "__googleDriveToken"
|
||||
|
||||
export const THEME_STORAGE_KEY = "theme"
|
||||
export const DEFAULT_DETECTED_CODE = "eng" as const
|
||||
export const CONFIG_SCHEMA_VERSION = 93
|
||||
export const CONFIG_SCHEMA_VERSION = 94
|
||||
|
||||
export const DEFAULT_FLOATING_BUTTON_POSITION = 0.66
|
||||
export const DEFAULT_FLOATING_BUTTON_SIDE: FloatingButtonSide = "right"
|
||||
@@ -223,6 +224,9 @@ export const DEFAULT_CONFIG: Config = {
|
||||
disabledBuiltInRules: [],
|
||||
},
|
||||
uiLanguage: "auto",
|
||||
translationHub: {
|
||||
shortcut: DEFAULT_TRANSLATION_HUB_SHORTCUT_KEY,
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// The Translation Hub lives at its own extension page. Every entry point
|
||||
// (popup button, options sidebar, keyboard shortcut) opens this path.
|
||||
export const TRANSLATION_HUB_PAGE_PATH = "/translation-hub.html"
|
||||
|
||||
// Three keys on purpose: the hub is opened far less often than the reading
|
||||
// shortcuts, so it takes the roomier `Alt+Shift+` prefix rather than a bare
|
||||
// `Alt+<letter>` that users would rather keep for something else.
|
||||
export const DEFAULT_TRANSLATION_HUB_SHORTCUT_KEY = "Alt+Shift+H"
|
||||
Reference in New Issue
Block a user