Configuration Schema Versioning and Migration#
The project uses a versioned configuration schema to manage changes and ensure backward compatibility as new features and providers are introduced. Each schema version is tracked by a constant (CONFIG_SCHEMA_VERSION), and every change that affects the configuration structure results in a version bump. Migration scripts and automated tests ensure that user configurations remain valid and up-to-date across upgrades.
Configuration Search (Command Palette)#
The extension includes a command palette feature that allows users to quickly search and navigate to specific configuration options. This feature helps users discover and access settings without manually browsing through the settings pages.
Accessing the command palette:
- Press
Cmd+K(macOS) orCtrl+K(Windows/Linux) from anywhere in the settings UI - Click the search input at the top of the sidebar
Using the search:
- Type keywords to filter configuration options by name or description
- Results are grouped by settings page (e.g., "General", "Translation", "Video Subtitles")
- Select an option to navigate directly to that section on its settings page
- The page automatically scrolls to the selected configuration section
Implementation details:
- The search index includes all major configuration sections with their localized titles and descriptions
- Search items are defined in
search-items.tswith route, section ID, and i18n keys - Navigation uses URL search parameters to trigger section scrolling after route changes
- The command palette state is managed through Jotai atoms for global access
Schema Versioning and Migration System#
Each configuration is versioned using a schemaVersion field, which is stored in the configuration's metadata (meta) rather than as a direct property of the configuration object. When importing or syncing a configuration, the system compares the stored schema version (from meta) to the current CONFIG_SCHEMA_VERSION.
- If the imported version is newer than supported:
- The system throws a
ConfigVersionTooNewErrorand rejects the import or sync. The user will see a clear message indicating that their extension must be upgraded to support the newer configuration format.
- The system throws a
- If the imported version is older:
- The configuration is migrated incrementally, version by version, using dedicated migration scripts until it matches the current schema version.
- After migration, the configuration is validated against the schema before being applied.
This process is handled automatically during import/export and through the UI's configuration sync features. Users can view their current configuration and schema version in the UI as formatted JSON, where the schema version is shown as a separate field (e.g., { schemaVersion: 59, config: { ... } }).
Note: The schema version and last modified time are tracked in the configuration's metadata (meta), not as fields inside the config object itself. This improves separation of user settings and system metadata, and is reflected in all import/export and sync operations.
Current schema version: The latest CONFIG_SCHEMA_VERSION is 66.
Configuration Initialization Optimization#
The initializeConfig() function optimizes storage operations by tracking changes during initialization:
- Tracks whether config actually changed (
didConfigChangeflag) during initialization, migration, validation, or dev environment modifications - Tracks whether meta needs updating (
didMetaNeedUpdateflag) whenschemaVersionorlastModifiedAtis missing - Only writes config to storage if
didConfigChangeis true - Only writes meta to storage if
didConfigChangeORdidMetaNeedUpdateis true - This prevents unnecessary storage writes when config is already up-to-date, reducing I/O operations
The dev environment helper functions (applyAPIKeysFromEnv() and applyDevBetaExperience()) have been refactored to pure functions that return { config: Config, changed: boolean } instead of performing storage writes directly. These functions are called during initialization and their results determine whether storage writes are needed.
Handling Too-New Configurations#
If a user attempts to import or sync a configuration whose schema version is newer than what their extension supports, the system will throw a ConfigVersionTooNewError. This error is propagated through all migration and Google Drive sync operations, and the user will see a message indicating that they need to upgrade their extension to use the newer configuration. This prevents incompatible or unrecognized settings from being applied.
Example Error Handling#
- During migration, if the remote config's schema version is greater than
CONFIG_SCHEMA_VERSION, aConfigVersionTooNewErroris thrown with a localized message (e.g., "Your configuration is too new. Please upgrade the extension."). - During Google Drive sync, this error is surfaced to the user, ensuring they are aware of the need to upgrade.
Migration Scripts#
Migration scripts are stored in the migration-scripts directory and named using the pattern v{from}-to-v{to}.ts (e.g., v035-to-v036.ts). The system dynamically discovers and loads these scripts. Each script exports a migrate function that takes the old configuration and returns the updated configuration for the next version. If a migration script is missing for a required version, the migration process fails with an error.
Example: v35 to v36 Migration (Pre-translate Range Configuration)#
The migration from v35 to v36 adds a new preload configuration object to translate.page. This allows users to control how much content below the viewport is pre-translated, optimizing API usage and scroll performance.
- The new
preloadfield contains:margin: Number of pixels below the viewport to start translating content (default: 1000, min: 0, max: 5000).threshold: Percentage of element visibility required to trigger translation (default: 0, min: 0, max: 1).
The migration script adds this field with default values to all existing configurations.
// migration-scripts/v035-to-v036.ts
import { DEFAULT_PRELOAD_MARGIN, DEFAULT_PRELOAD_THRESHOLD } from '@/utils/constants/translate'
export function migrate(oldConfig: any): any {
return {
...oldConfig,
translate: {
...oldConfig.translate,
page: {
...oldConfig.translate?.page,
preload: {
margin: DEFAULT_PRELOAD_MARGIN,
threshold: DEFAULT_PRELOAD_THRESHOLD,
},
},
},
}
}
Example: v36 to v37 Migration (Remove detectedCode from Config)#
The migration from v36 to v37 removes the detectedCode field from the language section of the configuration. This field is now stored separately in browser storage, as it represents transient state rather than user configuration.
// migration-scripts/v036-to-v037.ts
export function migrate(oldConfig: any): any {
const { detectedCode: _, ...restLanguage } = oldConfig.language ?? {}
return {
...oldConfig,
language: restLanguage,
}
}
Example: v37 to v38 Migration (Floating Button Click Action)#
The migration from v37 to v38 adds the clickAction field to the floatingButton section of the configuration. This field allows users to choose the action performed when clicking the floating button (e.g., open panel or toggle page translation).
// migration-scripts/v037-to-v038.ts
export function migrate(oldConfig: any): any {
return {
...oldConfig,
floatingButton: {
...oldConfig.floatingButton,
clickAction: 'panel',
},
}
}
Example: v30 to v31 Migration (Prompt Configuration Refactor)#
The migration from v30 to v31 refactors how translation prompts are stored and selected:
- The default prompt is no longer stored in the configuration. Only custom prompts are persisted.
- The config field is renamed from
promptsConfigtocustomPromptsConfig. - The selected prompt is now referenced by
promptId(nullable). IfpromptIdisnull, the default prompt (defined in code) is used. - Any stored prompt with the ID
defaultis removed from the patterns array. - If the previous selected prompt was
default, it is converted tonull.
// migration-scripts/v030-to-v031.ts
export function migrate(oldConfig: any): any {
const oldPatterns = oldConfig.translate?.promptsConfig?.patterns ?? []
const oldPromptValue = oldConfig.translate?.promptsConfig?.prompt
return {
...oldConfig,
translate: {
...oldConfig.translate,
promptsConfig: undefined, // Remove old field
customPromptsConfig: {
...oldConfig.translate?.promptsConfig,
prompt: undefined, // Remove old field
promptId: oldPromptValue === 'default' ? null : oldPromptValue,
patterns: oldPatterns.filter((p: any) => p.id !== 'default'),
},
},
}
}
Example: v31 to v32 Migration (LLM Language Detection Toggle)#
The migration from v31 to v32 adds the enableLLMDetection boolean field to the translate.page section. This field controls whether LLM-based language detection is enabled for auto-translate features. The migration script sets this field to false by default for all existing configurations.
// migration-scripts/v031-to-v032.ts
export function migrate(oldConfig: any): any {
return {
...oldConfig,
translate: {
...oldConfig.translate,
page: {
...oldConfig.translate?.page,
enableLLMDetection: false,
},
},
}
}
Example: v33 to v34 Migration (System Prompt for Custom Translation Prompts)#
The migration from v33 to v34 adds a systemPrompt field to each object in the translate.customPromptsConfig.patterns array. This field enables support for system prompts in personalized translation prompts. The migration script sets systemPrompt to an empty string for all existing patterns.
// migration-scripts/v033-to-v034.ts
export function migrate(oldConfig: any): any {
const patterns = oldConfig.translate?.customPromptsConfig?.patterns ?? []
return {
...oldConfig,
translate: {
...oldConfig.translate,
customPromptsConfig: {
...oldConfig.translate?.customPromptsConfig,
patterns: patterns.map((pattern: any) => ({
...pattern,
systemPrompt: '',
})),
},
},
}
}
Migration Scripts#
Example: v40 to v41 Migration (Minimum Characters Per Node)#
The migration from v40 to v41 adds a new minCharactersPerNode field to the translate.page section of the configuration. This field allows users to skip translating text nodes with fewer characters than a specified threshold, reducing translation noise from short fragments.
The migration script adds this field with a default value of 0 (disabled) to all existing configurations.
// migration-scripts/v040-to-v041.ts
export function migrate(oldConfig: any): any {
return {
...oldConfig,
translate: {
...oldConfig.translate,
page: {
...oldConfig.translate?.page,
minCharactersPerNode: 0,
},
},
}
}
Example: v52 to v53 Migration (Unified Provider Model and Per-Feature Provider Selection)#
The migration from v52 to v53 implements major architectural changes to unify the provider model and introduce per-feature provider selection. Note: The vocabulary insight feature introduced in this migration was removed in v1.31.1 (schema version 66). Dictionary custom actions are the supported replacement.
Key changes:
- Unified provider model: Each provider now has a single
modelfield instead of separatemodels.readandmodels.translatefields. - Removal of read feature: The entire
readconfiguration block is removed, as the read functionality has been deprecated. - Per-feature provider selection: New feature-specific provider configurations are added for
selectionToolbar.features.translate,selectionToolbar.features.vocabularyInsight(removed in v1.31.1),inputTranslation.providerId, andvideoSubtitles.providerId.
The migration script:
- Flattens provider model configuration from
provider.models.translatetoprovider.model - Removes the obsolete
readconfig block - Adds
selectionToolbar.featureswith per-feature provider IDs (translate and vocabularyInsight - note: vocabularyInsight was removed in v1.31.1) - Inherits the legacy
read.providerIdforvocabularyInsightwhen available (this feature was removed in v1.31.1) - Adds
providerIdfields toinputTranslationandvideoSubtitlesif not already present - Reorders providers to ensure Microsoft Translate appears before Google Translate
// migration-scripts/v052-to-v053.ts
export function migrate(oldConfig: any): any {
if (!isObject(oldConfig)) {
return oldConfig
}
const configWithoutRead = removeReadConfig(oldConfig)
const migratedProvidersConfig = reorderProviders(migrateProviderModels(configWithoutRead.providersConfig))
const translateProviderIds = getTranslateProviderIds(migratedProvidersConfig)
return {
...configWithoutRead,
providersConfig: migratedProvidersConfig,
selectionToolbar: migrateSelectionToolbarFeatures(oldConfig, migratedProvidersConfig),
inputTranslation: migrateInputTranslation(configWithoutRead, translateProviderIds),
videoSubtitles: migrateVideoSubtitles(configWithoutRead, translateProviderIds),
}
}
This migration enables a cleaner, more flexible provider architecture where each feature (translate, selection toolbar, input translation, TTS, video subtitles) can independently select its own provider.
Example: v53 to v54 Migration (TTS Configuration Refactor)#
The migration from v53 to v54 refactors the TTS configuration from OpenAI-specific fields to Edge TTS-based per-language voice configuration:
Key changes:
- Removal of OpenAI-specific fields: The
providerId,model,voice, andspeedfields are removed from thettsconfiguration. - Addition of Edge TTS fields: New fields are introduced to support Edge TTS with per-language voice mapping and flexible voice synthesis controls:
defaultVoice: The default Edge TTS voice (string, e.g.,'en-US-GuyNeural')languageVoices: An object mapping all ISO 639-3 language codes (300+ languages) to Microsoft Edge TTS voice names (e.g.,{"eng": "en-US-GuyNeural", "cmn": "zh-CN-YunxiNeural"}) for per-language voice selectionrate: Speech rate adjustment in the range -100 to 100 (replaces the oldspeedfield)pitch: Voice pitch adjustment in the range -100 to 100 (default: 0)volume: Voice volume adjustment in the range -100 to 100 (default: 0)
The migration script:
- Converts the old
speedfield (if present) to the newratefield using the formula:rate = (speed - 1) * 100, clamped to -100 to 100 - If the
ratefield already exists, it is validated and clamped to the valid range - If neither
speednorrateexists,ratedefaults to 0 - Sets
defaultVoiceto a known Edge TTS voice if the oldvoicefield matches a known Edge TTS voice name, or falls back to'en-US-GuyNeural' - Populates
languageVoiceswith default Edge TTS voices for all 300+ ISO 639-3 language codes, overriding the English (eng) entry with the migrateddefaultVoice - Initializes
pitchandvolumeto 0 if not already present (after validation and clamping) - Handles empty or missing TTS config by creating a new config with all fields set to defaults
// migration-scripts/v053-to-v054.ts
export function migrate(oldConfig: any): any {
if (!isObject(oldConfig)) {
return oldConfig
}
const oldTts = isObject(oldConfig.tts) ? oldConfig.tts : {}
const defaultVoice = toEdgeVoice(oldTts.voice)
const languageVoices = createDefaultTTSLanguageVoices()
languageVoices.eng = defaultVoice
const migratedRate = toClampedNumber(oldTts.rate) ?? toRateFromLegacySpeed(oldTts.speed)
const migratedPitch = toClampedNumber(oldTts.pitch) ?? 0
const migratedVolume = toClampedNumber(oldTts.volume) ?? 0
return {
...oldConfig,
tts: {
defaultVoice,
languageVoices,
rate: migratedRate,
pitch: migratedPitch,
volume: migratedVolume,
},
}
}
Migration behavior:
- The migration is self-contained and does not import runtime TTS mappings, ensuring immutable migration behavior across versions
- Invalid or unrecognized voices default to
'en-US-GuyNeural' - Invalid numeric values for rate, pitch, or volume are clamped to their valid ranges (-100 to 100)
- The
languageVoicesmapping covers all ISO 639-3 language codes, with fallback voices for languages without native Edge TTS support
This migration removes the dependency on OpenAI for TTS and introduces Edge TTS as a free, built-in provider with per-language voice customization. The tts feature is no longer tied to a providerId, and TTS is no longer listed in the feature provider definitions.
Example: v54 to v55 Migration (TTS Language Detection Mode)#
The migration from v54 to v55 adds a new detectLanguageMode field to the tts configuration, allowing users to choose between basic and LLM-based language detection for text-to-speech functionality.
Key changes:
- Adds
detectLanguageModefield tottsconfiguration - The field accepts two values:
'basic'(default) or'llm' - Basic mode uses the browser's built-in language detection
- LLM mode uses AI-based language detection for improved accuracy
- All other TTS configuration properties remain unchanged
The migration script adds this field with the default 'basic' mode to all existing configurations:
// migration-scripts/v054-to-v055.ts
export function migrate(oldConfig: any): any {
const detectLanguageMode = 'basic'
return {
...oldConfig,
tts: {
...oldConfig.tts,
detectLanguageMode,
},
}
}
This migration introduces flexible language detection for TTS while maintaining backward compatibility by defaulting to basic detection mode.
Example: v55 to v56 Migration (Custom AI Actions for Selection Toolbar)#
The migration from v55 to v56 introduces support for user-defined custom AI actions in the selection toolbar. Users can create personalized AI-powered tools that appear when text is selected, with full control over prompts, structured output, and provider selection.
Key changes:
- Adds
selectionToolbar.customActionsfield to the configuration - Each custom action includes:
id: Unique identifier for the actionname: Display name shown in the selection toolbarenabled: Boolean toggle to enable/disable the actionicon: Icon identifier (e.g.,'tabler:book-2')providerId: The LLM provider to use for this actionsystemPrompt: System instructions for the AIprompt: User prompt template with tokens ({{selection}},{{context}},{{targetLang}},{{title}})outputSchema: JSON schema defining the structured output fields (array of{id, name, type}objects where type is'string'or'number')notebaseConnection(optional): Configuration for saving action outputs to a notebase table
- Custom actions support structured output rendering as key-value cards in the selection toolbar UI
- The migration conditionally initializes
customActions: if the vocabulary insight provider is configured (removed in v1.31.1), it adds a default "Dictionary" action with that provider; otherwise it initializes as an empty array - Order persistence: The order of items in the
customActionsarray is user-controlled and persisted. Users can reorder custom actions via drag-and-drop in the UI, and this order is saved to the configuration. Similarly, output schema fields within each custom action can be reordered via drag-and-drop, with the order persisted in theoutputSchemaarray.
The migration script:
// migration-scripts/v055-to-v056.ts
export function migrate(oldConfig: any): any {
const dictionaryProviderId
= oldConfig.selectionToolbar?.features?.vocabularyInsight?.providerId
// When dictionaryProviderId exists, populate customActions with default dictionary action
// When it doesn't exist, leave customActions empty
const customActions = dictionaryProviderId
? [
{
id: "default-dictionary",
name: "Dictionary",
enabled: true,
icon: "tabler:book-2",
providerId: dictionaryProviderId,
systemPrompt:
"You are a dictionary assistant for language learners. Given a term and its surrounding context, provide a comprehensive and concise dictionary entry. When a term has multiple meanings, focus on the contextual meaning. Return the term in its base/canonical form. Respond in {{targetLang}}.",
prompt: "Term: {{selection}}\nContext: {{context}}\nTarget language: {{targetLang}}",
outputSchema: [
{ id: "default-dictionary-term", name: "Term", type: "string" },
{ id: "default-dictionary-definition", name: "Definition", type: "string" },
{ id: "default-dictionary-context", name: "Context", type: "string" },
{ id: "default-dictionary-examples", name: "Examples", type: "string" },
{ id: "default-dictionary-synonyms", name: "Synonyms", type: "string" },
{ id: "default-dictionary-antonyms", name: "Antonyms", type: "string" },
],
},
]
: []
return {
...oldConfig,
selectionToolbar: {
...oldConfig.selectionToolbar,
customActions: oldConfig.selectionToolbar?.customActions ?? customActions,
},
}
}
This migration enables a powerful new extensibility model where users can define their own AI-powered actions for text selection, with full control over the AI prompt, structured output format, and provider selection. Custom actions appear as buttons in the selection toolbar and stream AI responses with structured output rendered as key-value cards. Users can reorder both custom actions and their output schema fields via drag-and-drop, with ordering persisted to the configuration.
Notebase Connection Configuration (Custom AI Actions)#
Custom AI actions support an optional notebaseConnection field that enables saving structured action results to a notebase table. This feature is gated behind the betaExperience.enabled toggle and provides a workflow for storing AI-generated data in a user's notebase.
Configuration schema:
notebaseConnection?: {
tableId: string // Selected notebase table ID
tableNameSnapshot: string // Display name of the table (for UI)
mappings: Array<{ // Output-to-column mappings
id: string // Unique mapping ID
localFieldId: string // Output field ID from the action's outputSchema
remoteColumnId: string // Target column ID in the notebase table
remoteColumnNameSnapshot: string // Display name of the column (for UI)
}>
}
Key features:
- Table selection: Users select a notebase table from their account via a dropdown in the action configuration form
- Field mapping: Each mapping connects an action output field (from
outputSchema) to a notebase table column - Type compatibility: Mappings validate that output field types (
stringornumber) match the notebase column types - Validation and sanitization: The configuration includes validation to ensure:
- At least one mapping is configured
- Each mapping has both output and column fields specified
- No duplicate mappings for the same output field or column
- Output field IDs reference valid fields from the action's
outputSchema
- Schema refresh: Users can refresh the notebase schema to sync column names and validate mappings after schema changes
- Save workflow: When configured, a "Save to Notebase" button appears in the selection toolbar footer when the custom action runs. The button:
- Validates that the user is authenticated
- Checks that all mappings are valid before attempting to save
- Handles errors gracefully (unauthorized, table not found, validation failures)
- Shows success/error toasts with clear user feedback
Implementation details:
- The notebase connection field is displayed in the action configuration form (
notebase-connection-field.tsx) - Mapping status is tracked with enum values:
"valid","missing_local","missing_remote","missing_schema","incompatible" - Invalid mappings are preserved in the configuration but prevent saving until fixed
- Helper functions in
src/utils/notebase.tshandle mapping validation, sanitization, and row cell building - The save button (
save-to-notebase-button.tsx) only renders when beta experience is enabled
Schema considerations:
- No schema version bump was required for this feature addition, as the
notebaseConnectionfield is optional and backward-compatible - The configuration system automatically sanitizes notebase connections on load, removing invalid mappings
- When output fields are removed or reordered, the sanitization logic ensures mappings remain consistent
Example: v56 to v57 Migration (Custom AI Actions Schema Enhancement)#
The migration from v56 to v57 adds the optional description field to all output schema fields in custom AI actions. This enhancement improves the structured output contract by allowing users to specify what each output field should contain, guiding the AI to produce more accurate results.
Key changes:
- Adds
descriptionfield toSelectionToolbarCustomActionOutputFieldschema - The
descriptionfield is optional and defaults to an empty string - Field types (
stringandnumber) are now nullable in the AI response - The migration adds
description: ""to all existing output schema fields for backward compatibility - No changes are required to other custom action properties
Structured output behavior:
- The
buildSelectionToolbarCustomActionSystemPrompt()function automatically appends a detailed structured output contract to the user's system prompt - This contract includes field names, types, descriptions, and nullable requirements
- Users do not need to manually specify output format requirements in their prompts
- The structured output contract ensures consistent AI responses across all custom actions
The migration script:
// migration-scripts/v056-to-v057.ts
export function migrate(oldConfig: any): any {
const customActions = oldConfig.selectionToolbar?.customActions
if (!Array.isArray(customActions)) {
return oldConfig
}
const migratedActions = customActions.map((action: any) => {
const outputSchema = Array.isArray(action.outputSchema)
? action.outputSchema.map((field: any) => ({
...field,
description: field.description ?? "",
}))
: action.outputSchema
return {
...action,
outputSchema,
}
})
return {
...oldConfig,
selectionToolbar: {
...oldConfig.selectionToolbar,
customActions: migratedActions,
},
}
}
UI/UX improvements:
- Output schema fields are now managed via dedicated add/edit/delete dialogs
- Field description input is included in the field editor
- Template-based action creation: users can create actions from pre-built templates (e.g., "dictionary", "improve writing", "blank")
- Templates include pre-configured prompts, icons, output schemas, and field descriptions
- Custom actions can still be created from scratch using the "blank" template
- Paragraph context display has been removed from the custom action popover (only selection text is shown)
- Output schema fields can be reordered via drag-and-drop, with the order persisted in the configuration
This migration is backward-compatible and requires no user action. Existing custom actions will continue to work with empty descriptions, while new actions can benefit from the improved structured output guidance.
Example: v57 to v58 Migration (Subtitle Position Persistence)#
The migration from v57 to v58 adds a position field to the videoSubtitles configuration to persist the subtitle overlay's drag position across page navigations.
Key changes:
- Adds
positionfield tovideoSubtitlesconfiguration with the following schema:percent: Number (0-100) representing the vertical position as a percentage of the viewportanchor: Enum ("top" | "bottom") indicating which edge the percentage is relative to
- Default values:
{ percent: 10, anchor: "bottom" }(10% from the bottom of the viewport) - The subtitle overlay position is restored on initialization and after in-page navigation
- Users can drag the subtitle overlay to a custom position, and it will persist when navigating between pages
The migration script adds this field with default values if not already present:
// migration-scripts/v057-to-v058.ts
export function migrate(oldConfig: any): any {
const videoSubtitles = oldConfig.videoSubtitles
if (!videoSubtitles) {
return oldConfig
}
if (videoSubtitles.position) {
return oldConfig
}
return {
...oldConfig,
videoSubtitles: {
...videoSubtitles,
position: { percent: 10, anchor: "bottom" },
},
}
}
Implementation details:
- The
subtitlePositionSchematype is defined insrc/types/config/subtitles.ts - Position persistence decouples storage logic from the drag hook:
useVerticalDragstays pure and accepts anonDragEndcallback SubtitlesViewhandles saving the position to config storage when dragging endsUniversalVideoAdapterrestores the position during initialization and after navigation
This migration improves the user experience by eliminating the need to reposition subtitles after every page navigation, especially useful for users who prefer custom subtitle placement while watching videos.
Example: v58 to v59 Migration (Site Control Blacklist Mode)#
The migration from v58 to v59 refactors the site control configuration to support both blacklist and whitelist modes with separate pattern arrays:
Key changes:
- The
modeenum changed from["all", "whitelist"]to["blacklist", "whitelist"]- Blacklist mode: Extension is disabled on sites matching
blacklistPatterns, but enabled everywhere else - Whitelist mode: Extension is enabled only on sites matching
whitelistPatterns, and disabled everywhere else
- Blacklist mode: Extension is disabled on sites matching
- The single
patternsarray was split into two separate arrays:blacklistPatterns: Array of URL patterns where extension is disabled (used when mode is"blacklist")whitelistPatterns: Array of URL patterns where extension is enabled (used when mode is"whitelist")
- The legacy
"all"mode is migrated to"blacklist"mode (since "all" meant "enabled everywhere", which is the same as blacklist mode with no patterns) - Existing patterns (previously used only for whitelist) are preserved as
whitelistPatterns blacklistPatternsis initialized as an empty array
This change enables users to disable the extension on specific sites (blacklist mode) in addition to the existing whitelist-only functionality.
The migration script:
// migration-scripts/v058-to-v059.ts
export function migrate(oldConfig: any): any {
const siteControl = oldConfig.siteControl
if (!siteControl) {
return oldConfig
}
const mode = siteControl.mode === "all" ? "blacklist" : siteControl.mode
const patterns = siteControl.patterns ?? []
return {
...oldConfig,
siteControl: {
mode,
blacklistPatterns: [],
whitelistPatterns: patterns,
},
}
}
Migration behavior:
- If the old mode was
"all", it becomes"blacklist"with empty blacklist and whitelist arrays (extension works everywhere, as before) - If the old mode was
"whitelist", it remains"whitelist"and existing patterns are moved towhitelistPatterns - The
blacklistPatternsarray is always initialized as empty, allowing users to add sites after migration
This migration enables users to use the extension everywhere except on specific sites, complementing the existing whitelist mode where the extension only works on specified sites.
Example: v59 to v60 Migration (Rename Custom AI Features to Custom AI Actions)#
The migration from v59 to v60 renames the selectionToolbar.customFeatures configuration key to selectionToolbar.customActions. This change better reflects the purpose of these user-defined AI operations as actions rather than features.
Key changes:
- Renames
selectionToolbar.customFeaturestoselectionToolbar.customActions - All functionality remains identical; only the configuration key name changes
- The migration preserves all existing custom action configurations
- No changes to action structure, properties, or behavior
The migration script renames the field while preserving all existing custom actions:
// migration-scripts/v059-to-v060.ts
export function migrate(oldConfig: any): any {
const selectionToolbar = oldConfig.selectionToolbar
if (!selectionToolbar) {
return oldConfig
}
const { customFeatures: _customFeatures, ...restSelectionToolbar } = selectionToolbar
if (Array.isArray(selectionToolbar.customActions)) {
return {
...oldConfig,
selectionToolbar: {
...restSelectionToolbar,
customActions: selectionToolbar.customActions,
},
}
}
const customActions = Array.isArray(selectionToolbar.customFeatures)
? selectionToolbar.customFeatures
: []
return {
...oldConfig,
selectionToolbar: {
...restSelectionToolbar,
customActions,
},
}
}
Implementation details:
- The migration handles both cases: configurations that already have
customActions(idempotent), and those with the oldcustomFeatureskey - After migration, the old
customFeatureskey is removed from the configuration - All user-created custom actions are preserved exactly as they were
- Configuration import, export, and Google Drive sync handle this rename automatically
Example: v60 to v61 Migration (TTS Support for Dictionary Output Fields)#
The migration from v60 to v61 adds a speaking boolean field to all custom action output schema fields. This enables text-to-speech (TTS) support for dictionary results, allowing users to hear pronunciations directly from custom action outputs.
Key changes:
- Adds
speakingfield toSelectionToolbarCustomActionOutputFieldschema - The
speakingfield is a boolean indicating whether the field's content should be speakable via TTS - The migration automatically enables speaking for dictionary Term and Context fields (recognized by their field IDs)
- All other output fields default to
speaking: false
The migration script adds the speaking field to all output schema fields and enables it for known dictionary fields:
// migration-scripts/v060-to-v061.ts
export function migrate(oldConfig: any): any {
const selectionToolbar = oldConfig.selectionToolbar
if (!selectionToolbar || !Array.isArray(selectionToolbar.customActions)) {
return oldConfig
}
const customActions = selectionToolbar.customActions.map((action: any) => {
if (!Array.isArray(action.outputSchema)) {
return action
}
const outputSchema = action.outputSchema.map((field: any) => ({
...field,
speaking: field.speaking ?? (
field.id === "dictionary-term"
|| field.id === "dictionary-context"
|| field.id === "default-dictionary-term"
|| field.id === "default-dictionary-context"
),
}))
return {
...action,
outputSchema,
}
})
return {
...oldConfig,
selectionToolbar: {
...selectionToolbar,
customActions,
},
}
}
Implementation details:
- Dictionary output fields with recognized IDs (
default-dictionary-term,default-dictionary-context, etc.) are automatically marked asspeaking: true - All other fields default to
speaking: falseunless already configured - Users can toggle the speaking property for any field in custom action output schema configuration
- The speaking property adds a "Speak" button to structured output results in the selection toolbar
This migration enables TTS functionality for dictionary custom actions, allowing users to hear pronunciation and contextual examples directly from the selection toolbar.
Example: v61 to v62 Migration (Centralized Language Detection Configuration)#
The migration from v61 to v62 centralizes language detection configuration by consolidating three separate per-feature settings into a single root-level languageDetection configuration. This provides a unified control for language detection across all features (auto-translate, skip-languages, and TTS).
Key changes:
- Removed per-feature language detection settings:
translate.page.enableLLMDetection(boolean)translate.page.enableSkipLanguagesLLMDetection(boolean)tts.detectLanguageMode("basic" | "llm")
- Added root-level configuration: New
languageDetectionobject with the following structure:mode: Enum ("basic" | "llm") - Selects between browser-based or AI-based language detectionproviderId: Optional string - The LLM provider ID to use when mode is "llm"
- Unified behavior: Language detection mode now applies consistently across auto-translate, skip-languages, and TTS features
- Provider fallback logic: The migration intelligently selects a provider ID when LLM detection was enabled:
- Prefers the current
translate.providerIdif it's an enabled LLM provider - Falls back to the first enabled LLM provider if the translate provider is non-LLM
- Sets mode to "basic" if no enabled LLM provider is available
- Prefers the current
The migration script:
// migration-scripts/v061-to-v062.ts
export function migrate(oldConfig: any): any {
const enableLLMDetection = oldConfig?.translate?.page?.enableLLMDetection === true
const enableSkipLanguagesLLMDetection = oldConfig?.translate?.page?.enableSkipLanguagesLLMDetection === true
const ttsDetectLanguageMode = oldConfig?.tts?.detectLanguageMode
const anyLLMEnabled = enableLLMDetection
|| enableSkipLanguagesLLMDetection
|| ttsDetectLanguageMode === "llm"
// Non-LLM provider types at the time of this migration (frozen snapshot)
const NON_LLM_PROVIDERS = ["google-translate", "microsoft-translate", "deeplx"]
const providers = Array.isArray(oldConfig?.providersConfig) ? oldConfig.providersConfig : []
// Find an enabled LLM provider: prefer translate.providerId if it's enabled LLM,
// else first enabled LLM provider in the list.
const translateProviderId = oldConfig?.translate?.providerId
const translateProvider = providers.find((p: any) => p.id === translateProviderId)
const translateIsEnabledLLM = translateProvider?.enabled === true
&& !NON_LLM_PROVIDERS.includes(translateProvider.provider)
const providerId = translateIsEnabledLLM
? translateProviderId
: providers.find((p: any) => p.enabled && !NON_LLM_PROVIDERS.includes(p.provider))?.id
// Only keep llm mode when an enabled LLM provider is available.
const mode = anyLLMEnabled && providerId ? "llm" : "basic"
// Remove old fields from translate.page
const oldPage = oldConfig?.translate?.page ?? {}
const {
enableLLMDetection: _a,
enableSkipLanguagesLLMDetection: _b,
...restPage
} = oldPage
// Remove old field from tts
const oldTts = oldConfig?.tts ?? {}
const {
detectLanguageMode: _c,
...restTts
} = oldTts
return {
...oldConfig,
translate: {
...oldConfig?.translate,
page: restPage,
},
languageDetection: {
mode,
providerId,
},
tts: restTts,
}
}
Migration behavior:
- If any of the old language detection settings were enabled for LLM and an enabled LLM provider is available, the migration sets
mode: "llm"and assigns the provider ID - If LLM detection was enabled but no enabled LLM provider is found, the migration falls back to
mode: "basic" - The migration removes all old per-feature language detection fields from the configuration
- The new configuration is validated to ensure that when
modeis"llm", theproviderIdrefers to a valid, enabled LLM provider
UI/UX improvements:
- Language Detection settings are available in the General settings page alongside Feature Providers
- Users can choose between Basic and LLM detection modes with a single toggle
- When LLM mode is selected, users select a provider from a dropdown of enabled LLM providers
- Status indicators show whether language detection is active and which provider is being used
- Warnings appear if the selected provider is disabled or unavailable
This migration simplifies language detection configuration by eliminating redundant per-feature settings and providing a single, centralized control that applies across all features using language detection.
Example: v62 to v63 Migration (Individual Toggles for Built-in Selection Toolbar Features)#
The migration from v62 to v63 adds individual enabled toggles for each built-in selection toolbar feature (translate, speak, vocabulary insight), allowing users to control which features appear in the selection toolbar independently. Note: The vocabulary insight feature added in this migration was removed in v1.31.1 (schema version 66). Dictionary custom actions are the supported replacement.
Key changes:
- Adds
enabled: truefield toselectionToolbar.features.translate - Adds
enabled: truefield toselectionToolbar.features.vocabularyInsight(removed in v1.31.1) - Adds new
selectionToolbar.features.speakobject withenabled: true - All built-in features default to enabled for backward compatibility
- When all features (built-in + custom actions) are disabled, the toolbar is not mounted on text selection
- The speak feature toggle is hidden on Firefox (where speak is not supported)
The migration script adds these fields with enabled set to true to preserve existing behavior:
// migration-scripts/v062-to-v063.ts
export function migrate(oldConfig: any): any {
const oldFeatures = oldConfig?.selectionToolbar?.features ?? {}
return {
...oldConfig,
selectionToolbar: {
...oldConfig?.selectionToolbar,
features: {
translate: {
...oldFeatures.translate,
enabled: true,
},
speak: {
enabled: true,
},
vocabularyInsight: { // Removed in v1.31.1
...oldFeatures.vocabularyInsight,
enabled: true,
},
},
},
}
}
Implementation details:
- Feature toggles appear in the Selection Toolbar settings page with matching icons (translate, volume, zoom-scan)
- The toolbar only renders when at least one built-in feature is enabled OR at least one custom action is enabled
- Disabling all features and custom actions prevents the toolbar from mounting on text selection
- The speak feature is conditionally shown based on browser capabilities
This migration enables fine-grained control over selection toolbar features, allowing users to customize which built-in features appear when text is selected while maintaining backward compatibility by enabling all features by default.
Example: v63 to v64 Migration (Rename Prompt Tokens for Clarity)#
The migration from v63 to v64 renames prompt tokens used in custom AI actions and translation prompts to improve clarity and consistency.
Key changes:
- Renames
{{targetLang}}to{{targetLanguage}} - Renames
{{title}}to{{webTitle}} - Renames
{{summary}}to{{webSummary}} - Renames
{{context}}to{{paragraphs}} - All prompt templates in custom actions and system prompts are automatically updated
- No functionality changes; only token names are updated for better clarity
The migration script updates all custom action prompts and system prompts to use the new token names:
// migration-scripts/v063-to-v064.ts
export function migrate(oldConfig: any): any {
const selectionToolbar = oldConfig.selectionToolbar
if (!selectionToolbar || !Array.isArray(selectionToolbar.customActions)) {
return oldConfig
}
const customActions = selectionToolbar.customActions.map((action: any) => ({
...action,
systemPrompt: renameTokens(action.systemPrompt),
prompt: renameTokens(action.prompt),
}))
return {
...oldConfig,
selectionToolbar: {
...selectionToolbar,
customActions,
},
}
}
function renameTokens(text: string): string {
if (typeof text !== 'string') {
return text
}
return text
.replace(/\{\{targetLang\}\}/g, '{{targetLanguage}}')
.replace(/\{\{title\}\}/g, '{{webTitle}}')
.replace(/\{\{summary\}\}/g, '{{webSummary}}')
.replace(/\{\{context\}\}/g, '{{paragraphs}}')
}
Migration behavior:
- All occurrences of old token names in custom action
systemPromptandpromptfields are replaced with new names - Non-string values are left unchanged
- Token replacements are case-sensitive and only match the exact template syntax (e.g.,
{{targetLang}})
Rationale:
targetLanguage: More descriptive than the abbreviatedtargetLangwebTitle: Clarifies that this is the web page title, not a custom action or translation titlewebSummary: Clarifies that this is a web page summary, not a generic summaryparagraphs: More accurate thancontext, as it represents surrounding paragraphs rather than abstract context
This migration improves consistency in prompt token naming and makes custom action templates more self-documenting. All existing custom actions are automatically updated without user intervention.
Example: v64 to v65 Migration (Selection Toolbar Opacity Settings)#
The migration from v64 to v65 adds an opacity field to the selectionToolbar configuration, allowing users to customize the transparency of the selection toolbar and popover.
Key changes:
- Adds
opacityfield toselectionToolbarconfiguration - The field accepts values from 1 to 100 (representing percentage opacity)
- Default value is 100 (fully opaque)
- The opacity setting applies to both the selection toolbar and the popover content opened from it
- All other selection toolbar configuration properties remain unchanged
The migration script adds this field with the default value to all existing configurations:
// migration-scripts/v064-to-v065.ts
export function migrate(oldConfig: any): any {
const selectionToolbar = oldConfig.selectionToolbar
if (!selectionToolbar) {
return oldConfig
}
return {
...oldConfig,
selectionToolbar: {
...selectionToolbar,
opacity: 100,
},
}
}
Implementation details:
- Opacity constants are defined in
src/utils/constants/selection.ts:MIN_SELECTION_OVERLAY_OPACITY: 1MAX_SELECTION_OVERLAY_OPACITY: 100DEFAULT_SELECTION_OVERLAY_OPACITY: 100
- The opacity setting is controlled via a slider in the Selection Toolbar settings page
- Changes apply immediately to the selection toolbar UI and all popovers
- A command palette entry allows quick navigation to the opacity setting
This migration enables users to adjust selection toolbar visibility based on their preferences, making the toolbar less intrusive when needed while maintaining full functionality.
Example: v65 to v66 Migration (Page Translation Shortcut Format and Vocabulary Insight Removal)#
The migration from v65 to v66 performs two changes:
- Converts the
translate.page.shortcutfield from a string array format to a portable hotkey string format - Removes the
selectionToolbar.features.vocabularyInsightconfiguration (deprecated feature removed in v1.31.1)
Page Translation Shortcut Format:
This change standardizes keyboard shortcut representation using TanStack Hotkeys conventions.
Page Translation Shortcut key changes:
- Converts old array format (e.g.,
["alt", "e"]) to portable string format (e.g.,"Alt+E") - Normalizes modifier keys:
ctrl,control,command, andmeta→Mod(portable modifier that maps to Command on macOS, Control elsewhere)option→Alt
- Normalizes key names using standard aliases (e.g.,
esc→Escape,return→Enter) - Validates that shortcuts include at least one modifier key and exactly one non-modifier key
- Uses fallback shortcut
"Alt+E"for invalid configurations - Empty strings are allowed (indicating no shortcut configured)
Vocabulary Insight Removal:
- Removes the
selectionToolbar.features.vocabularyInsightconfiguration block - This feature was removed in v1.31.1 and replaced by Dictionary custom actions
- Users who previously used vocabulary insight can create equivalent functionality using custom actions with the "dictionary" template
Migration behavior:
- The migration script converts legacy array shortcuts to the new string format
- Invalid shortcuts (missing modifiers, multiple keys, etc.) default to
"Alt+E" - The new format uses
pageTranslationShortcutSchemawith validation viaisValidConfiguredPageTranslationShortcut() - Shortcuts are stored in portable format internally but displayed in platform-native format in the UI
The migration script:
// migration-scripts/v065-to-v066.ts
export function migrate(oldConfig: any): any {
const migratedConfig: any = {
...oldConfig,
}
// Remove vocabularyInsight feature from selection toolbar
const oldSelectionToolbar = oldConfig?.selectionToolbar
if (oldSelectionToolbar) {
migratedConfig.selectionToolbar = oldSelectionToolbar.features
? {
...oldSelectionToolbar,
features: removeVocabularyInsightFeature(oldSelectionToolbar.features),
}
: oldSelectionToolbar
}
// Migrate page translation shortcut
const translate = oldConfig.translate
const page = translate?.page
if (!page) {
return migratedConfig
}
return {
...migratedConfig,
translate: {
...translate,
page: {
...page,
shortcut: migrateLegacyShortcut(page.shortcut),
},
},
}
}
function removeVocabularyInsightFeature(oldFeatures: any): any {
const { vocabularyInsight: _removedVocabularyInsight, ...features } = oldFeatures ?? {}
return features
}
function migrateLegacyShortcut(legacyShortcut: unknown): string {
if (typeof legacyShortcut === "string") {
const trimmedShortcut = legacyShortcut.trim()
return trimmedShortcut || "Alt+E"
}
if (!Array.isArray(legacyShortcut) || legacyShortcut.length === 0) {
return "Alt+E"
}
let hasMod = false
let hasAlt = false
let hasShift = false
let key = ""
for (const token of legacyShortcut) {
if (typeof token !== "string") {
return "Alt+E"
}
const normalizedToken = token.trim().toLowerCase()
if (!normalizedToken) {
return "Alt+E"
}
if (normalizedToken === "ctrl" || normalizedToken === "control"
|| normalizedToken === "command" || normalizedToken === "meta") {
hasMod = true
continue
}
if (normalizedToken === "alt" || normalizedToken === "option") {
hasAlt = true
continue
}
if (normalizedToken === "shift") {
hasShift = true
continue
}
if (key) {
return "Alt+E"
}
key = normalizeLegacyHotkeyKey(token)
}
if (!key || (!hasMod && !hasAlt && !hasShift)) {
return "Alt+E"
}
return [
hasMod ? "Mod" : null,
hasAlt ? "Alt" : null,
hasShift ? "Shift" : null,
key,
].filter(Boolean).join("+")
}
Implementation details:
- Shortcut validation is handled by
isValidConfiguredPageTranslationShortcut()frompage-translation-shortcut.ts - The portable
Modkey ensures cross-platform compatibility (Command on macOS, Control elsewhere) - Keyboard recorder preserves physical keys for macOS Option shortcuts (e.g.,
Option+3→Alt+3) - UI displays shortcuts in platform-native format using
formatPageTranslationShortcut() - The vocabulary insight configuration is removed from
selectionToolbar.features - Custom actions created with the "dictionary" template provide equivalent functionality to the removed vocabulary insight feature
This migration ensures consistent shortcut storage format across the extension while maintaining backward compatibility by automatically converting existing array-based shortcuts to the new string format. It also removes the vocabulary insight feature configuration (removed in v1.31.1).
Example Configuration Files#
Example configuration files for each schema version are maintained in the example directory (e.g., v037.ts, v036.ts, v031.ts, v030.ts). Each file contains a testSeries object with one or more scenarios for comprehensive migration testing.
v36 Example (Pre-translate Range Configuration)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Add preload configuration to translate.page',
config: {
// ... other config fields
translate: {
// ...
page: {
// ...
preload: {
margin: 1000,
threshold: 0,
},
},
// ...
},
// ...
},
},
}
v38 Example (Floating Button Click Action)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Add clickAction to floatingButton config',
config: {
// ... other config fields
floatingButton: {
enabled: true,
position: 0.75,
disabledFloatingButtonPatterns: ['github.com'],
clickAction: 'panel', // New field added in v038
},
// ...
},
},
}
v37 Example (Remove detectedCode from Config)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Remove detectedCode from language config',
config: {
// ... other config fields
language: {
sourceCode: 'spa',
targetCode: 'eng',
level: 'advanced',
},
// ...
},
},
}
v31 Example (Prompt Configuration Refactor)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Remove default prompt from patterns array',
config: {
// ... other config fields
translate: {
// ...
customPromptsConfig: {
promptId: '123e4567-e89b-12d3-a456-426614174000',
patterns: [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'Technical Translation',
prompt: 'Technical translation from Spanish to {{targetLang}}. Preserve technical terms and accuracy:\n{{input}}',
},
],
},
// ...
},
// ...
},
},
}
v34 Example (System Prompt for Custom Translation Prompts)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Add systemPrompt field to custom prompt patterns',
config: {
// ... other config fields
translate: {
// ...
customPromptsConfig: {
promptId: '123e4567-e89b-12d3-a456-426614174000',
patterns: [
{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'Technical Translation',
systemPrompt: '', // New field added in v034
prompt: 'Technical translation from Spanish to {{targetLang}}. Preserve technical terms and accuracy:\n{{input}}',
},
],
},
// ...
},
// ...
},
},
}
v41 Example (Minimum Characters Per Node)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Add minCharactersPerNode to page config',
config: {
// ... other config fields
translate: {
// ...
page: {
// ...
minCharactersPerNode: 0, // New field added in v041
},
// ...
},
// ...
},
},
}
v53 Example (Unified Provider Model and Per-Feature Provider Selection)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Remove read config, unify provider model, add per-feature provider IDs',
config: {
language: {
sourceCode: 'spa',
targetCode: 'eng',
level: 'advanced',
},
providersConfig: [
{
id: 'openai-default',
enabled: true,
name: 'OpenAI',
provider: 'openai',
apiKey: 'sk-custom-prompt-key',
baseURL: 'https://api.openai.com/v1',
model: { // Unified model field (no longer models.translate)
model: 'gpt-4o-mini',
isCustomModel: true,
customModel: 'translate-gpt-custom',
},
},
// ... other providers
],
// read config removed entirely
translate: {
providerId: 'openai-default',
// ... other translate config
},
selectionToolbar: {
enabled: false,
disabledSelectionToolbarPatterns: [],
features: { // New per-feature provider selection
translate: {
providerId: 'openai-default',
},
vocabularyInsight: {
providerId: 'deepseek-default',
},
},
},
inputTranslation: {
enabled: true,
providerId: 'openai-default', // New providerId field
// ... other input translation config
},
videoSubtitles: {
enabled: false,
providerId: 'openai-default', // New providerId field
// ... other video subtitles config
},
// ... other config fields
},
},
}
v54 Example (TTS Configuration Refactor)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Refactor TTS config from OpenAI-specific to Edge TTS',
config: {
// ... other config fields
tts: {
defaultVoice: 'en-US-GuyNeural', // Edge TTS voice name
languageVoices: {
// Per-language voice mapping for all ISO 639-3 codes (300+ languages)
'eng': 'en-US-GuyNeural',
'cmn': 'zh-CN-YunxiNeural',
'cmn-Hant': 'zh-TW-YunJheNeural',
'spa': 'es-ES-AlvaroNeural',
'jpn': 'ja-JP-KeitaNeural',
'kor': 'ko-KR-InJoonNeural',
'rus': 'ru-RU-DmitryNeural',
'arb': 'ar-SA-HamedNeural',
'fra': 'fr-FR-HenriNeural',
'deu': 'de-DE-ConradNeural',
// ... (all ISO 639-3 language codes mapped to Microsoft Edge voices)
},
rate: 0, // -100 to 100 (replaces the old speed field)
pitch: 0, // -100 to 100
volume: 0, // -100 to 100
},
// ... other config fields
},
},
}
v55 Example (TTS Language Detection Mode)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Adds detectLanguageMode to TTS config',
config: {
// ... other config fields
tts: {
defaultVoice: 'en-US-GuyNeural',
languageVoices: {
'eng': 'en-US-GuyNeural',
'cmn': 'zh-CN-YunxiNeural',
// ... other language mappings
},
detectLanguageMode: 'basic', // New field added in v055
rate: 0,
pitch: 0,
volume: 0,
},
// ... other config fields
},
},
}
v56 Example (Custom AI Actions for Selection Toolbar)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Adds selectionToolbar.customActions',
config: {
// ... other config fields
selectionToolbar: {
enabled: false,
disabledSelectionToolbarPatterns: [],
customActions: [], // New field added in v056
features: {
translate: {
providerId: 'openai-default',
},
vocabularyInsight: {
providerId: 'deepseek-default',
},
},
},
// ... other config fields
},
},
}
v57 Example (Custom AI Actions Schema Enhancement)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Adds description to output schema fields',
config: {
// ... other config fields
selectionToolbar: {
enabled: false,
disabledSelectionToolbarPatterns: [],
customActions: [
{
id: 'default-dictionary',
name: 'Dictionary',
enabled: true,
icon: 'tabler:book-2',
providerId: 'deepseek-default',
systemPrompt: '...',
prompt: 'Term: {{selection}}\nContext: {{context}}...',
outputSchema: [
{ id: 'default-dictionary-term', name: 'Term', type: 'string', description: '' }, // description added in v057
{ id: 'default-dictionary-definition', name: 'Definition', type: 'string', description: '' },
{ id: 'default-dictionary-context', name: 'Context', type: 'string', description: '' },
// ... other fields
],
},
],
features: {
translate: {
providerId: 'openai-default',
},
vocabularyInsight: {
providerId: 'deepseek-default',
},
},
},
// ... other config fields
},
},
}
v59 Example (Site Control Blacklist Mode)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Replaces siteControl mode all with blacklist, splits patterns into independent arrays',
config: {
// ... other config fields
siteControl: {
mode: 'blacklist', // New mode (was "all")
blacklistPatterns: [], // New field added in v059
whitelistPatterns: [], // Replaces old "patterns" field
},
// ...
},
},
}
v60 Example (Rename Custom AI Features to Custom AI Actions)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Renames customFeatures to customActions',
config: {
// ... other config fields
selectionToolbar: {
enabled: false,
disabledSelectionToolbarPatterns: [],
customActions: [ // Renamed from customFeatures in v060
{
id: 'default-dictionary',
name: 'Dictionary',
enabled: true,
icon: 'tabler:book-2',
providerId: 'deepseek-default',
systemPrompt: '...',
prompt: 'Term: {{selection}}\nContext: {{context}}...',
outputSchema: [
{ id: 'default-dictionary-term', name: 'Term', type: 'string', description: '' },
{ id: 'default-dictionary-definition', name: 'Definition', type: 'string', description: '' },
// ... other fields
],
},
],
features: {
translate: {
providerId: 'openai-default',
},
vocabularyInsight: {
providerId: 'deepseek-default',
},
},
},
// ...
},
},
}
v61 Example (TTS Support for Dictionary Output Fields)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Adds speaking property to custom action output fields',
config: {
// ... other config fields
selectionToolbar: {
enabled: false,
disabledSelectionToolbarPatterns: [],
customActions: [
{
id: 'default-dictionary',
name: 'Dictionary',
enabled: true,
icon: 'tabler:book-2',
providerId: 'deepseek-default',
systemPrompt: '...',
prompt: 'Term: {{selection}}\nContext: {{context}}...',
outputSchema: [
{ id: 'default-dictionary-term', name: 'Term', type: 'string', description: '', speaking: true }, // speaking added in v061
{ id: 'default-dictionary-definition', name: 'Definition', type: 'string', description: '', speaking: false },
{ id: 'default-dictionary-context', name: 'Context', type: 'string', description: '', speaking: true },
// ... other fields
],
},
],
features: {
translate: {
providerId: 'openai-default',
},
vocabularyInsight: {
providerId: 'deepseek-default',
},
},
},
// ...
},
},
}
v62 Example (Centralized Language Detection Configuration)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Centralizes language detection config, removes per-feature settings',
config: {
// ... other config fields
languageDetection: { // New root-level field in v062
mode: 'basic', // or 'llm'
providerId: 'openai-default', // optional, required when mode is 'llm'
},
translate: {
// ...
page: {
// ...
autoTranslatePatterns: ['spanish-news.com', 'elmundo.es'],
autoTranslateLanguages: [],
skipLanguages: [],
// enableLLMDetection removed in v062
// enableSkipLanguagesLLMDetection removed in v062
},
},
tts: {
defaultVoice: 'en-US-GuyNeural',
languageVoices: {
'eng': 'en-US-GuyNeural',
// ... other language mappings
},
rate: 0,
pitch: 0,
volume: 0,
// detectLanguageMode removed in v062
},
// ...
},
},
}
v63 Example (Individual Toggles for Built-in Selection Toolbar Features)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Adds enabled toggle to selection toolbar built-in features and adds speak feature',
config: {
// ... other config fields
selectionToolbar: {
enabled: false,
disabledSelectionToolbarPatterns: [],
customActions: [
{
id: 'default-dictionary',
name: 'Dictionary',
enabled: true,
icon: 'tabler:book-2',
providerId: 'deepseek-default',
systemPrompt: '...',
prompt: 'Term: {{selection}}\nContext: {{context}}...',
outputSchema: [
{ id: 'default-dictionary-term', name: 'Term', type: 'string', description: '', speaking: true },
{ id: 'default-dictionary-definition', name: 'Definition', type: 'string', description: '', speaking: false },
// ... other fields
],
},
],
features: {
translate: {
enabled: true, // New field added in v063
providerId: 'openai-default',
},
speak: { // New feature added in v063
enabled: true,
},
vocabularyInsight: {
enabled: true, // New field added in v063
providerId: 'deepseek-default',
},
},
},
// ...
},
},
}
v65 Example (Selection Toolbar Opacity Settings)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Adds opacity to selection toolbar',
config: {
// ... other config fields
selectionToolbar: {
enabled: true,
disabledSelectionToolbarPatterns: [],
opacity: 100, // New field added in v065
customActions: [
{
id: 'default-dictionary',
name: 'Dictionary',
enabled: true,
icon: 'tabler:book-2',
providerId: 'deepseek-default',
systemPrompt: '...',
prompt: 'Term: {{selection}}\nParagraphs: {{paragraphs}}...',
outputSchema: [
{ id: 'default-dictionary-term', name: 'Term', type: 'string', description: '', speaking: true },
{ id: 'default-dictionary-definition', name: 'Definition', type: 'string', description: '', speaking: false },
{ id: 'default-dictionary-context', name: 'Paragraphs', type: 'string', description: '', speaking: true },
// ... other fields
],
},
],
features: {
translate: {
enabled: true,
providerId: 'openai-default',
},
speak: {
enabled: true,
},
vocabularyInsight: {
enabled: true,
providerId: 'deepseek-default',
},
},
},
// ...
},
},
}
v66 Example (Page Translation Shortcut Format and Vocabulary Insight Removal)#
export const testSeries = {
'complex-config-from-v020': {
description: 'Converts page translation shortcut from array to portable string format and removes vocabularyInsight',
config: {
// ... other config fields
translate: {
providerId: 'openai-default',
mode: 'translationOnly',
// ...
page: {
range: 'all',
autoTranslatePatterns: ['spanish-news.com', 'elmundo.es'],
autoTranslateLanguages: [],
shortcut: 'Alt+B', // Changed from string[] to portable string format in v066
preload: {
margin: 1000,
threshold: 0,
},
minCharactersPerNode: 0,
minWordsPerNode: 0,
skipLanguages: [],
},
// ...
},
selectionToolbar: {
enabled: true,
disabledSelectionToolbarPatterns: [],
opacity: 100,
customActions: [...],
features: {
translate: {
enabled: true,
providerId: 'openai-default',
},
speak: {
enabled: true,
},
// vocabularyInsight removed in v066
},
},
// ...
},
},
}
Automated Migration Testing#
The project includes an automated migration test system that discovers all schema versions, loads example configurations, executes migration scripts, and validates the results. This ensures that all migration scripts are present, executable, and that migrations produce the expected configuration for each version. Tests can be run using pnpm test migration-scripts and support verbose and coverage modes.
Safe Update Practices#
When new features or providers are added, the following practices ensure safe updates for user settings:
- Always back up your configuration before importing or applying changes.
- Use the UI's import/export features, which handle schema version checks and migrations automatically.
- If importing a configuration from an older version, the system will migrate it stepwise to the current version, applying all necessary transformations and validating the result.
- If a configuration is too new for your current version, the import will be rejected and a clear upgrade-needed message will be shown.
- After migration, the configuration is validated using the schema to catch any issues before applying changes.
- Example configurations and migration scripts are maintained for each version, ensuring that migrations are robust and well-tested.
- When new providers or features are added, migration scripts update user configurations with sensible defaults, requiring no manual intervention for most users.
Feature Provider Configuration UI#
The feature provider configuration system provides both centralized and per-provider management of feature assignments:
Centralized Feature Providers Section#
A dedicated "Feature Providers" settings page is available in the General settings (feature-providers-config.tsx), providing a centralized location where users can view and configure which provider handles each feature. This section includes:
- Feature provider assignments for all major features:
- Page Translation (
translate) - Selection Toolbar Translation (
selectionToolbar.translate) - Input Translation (
inputTranslation) - Video Subtitles (
videoSubtitles) - Language Detection (
languageDetection) - Optional provider assignment for AI-based language detection
- Page Translation (
Each feature displays a provider selector dropdown and shows a warning if the selected provider requires an API key that hasn't been configured. The needsApiKeyWarning helper function centralizes the logic for detecting when a provider requires an API key, checking for pure API providers (like DeepLX) that don't need keys.
Note: Text-to-Speech (TTS) is no longer included in the feature provider system as of schema version 54. TTS configuration is managed separately through the dedicated TTS settings page and uses Edge TTS as a free, built-in provider.
Site Control Settings Integration#
Site control settings (site control mode and patterns) have been merged into the General settings page as of v1.26.0. The standalone Site Control page has been removed, and these settings now appear as sections within the General page alongside Feature Providers and Translation Config. This consolidation simplifies the settings navigation by grouping related general configuration options together.
Theme Storage#
The extension's theme preference (system/light/dark) is stored separately from the main configuration object to optimize performance and avoid unnecessary re-renders.
Storage architecture:
- Storage location: Theme mode is stored at
chrome.storage.localunder the key"theme"(defined asTHEME_STORAGE_KEY) - Storage independence: Theme storage uses a separate storage key to avoid triggering
configAtomupdates when users switch themes - State management: Theme state is managed via
themeModeAtomusing Jotai, with hydration support for all content script and UI contexts - Available values:
"system"(follows OS preference),"light", or"dark"
Implementation details:
- The theme storage system uses
baseThemeModeAtomfor internal state andthemeModeAtomfor reactive read/write operations - The
ThemeProvidercomponent usesuseSyncExternalStoreto detect system theme preference changes in real-time - Theme is applied to shadow DOM containers and the main document via the
applyTheme()utility function - Content scripts hydrate theme state on initialization using
getLocalThemeMode()to ensure consistent theming across all contexts
No schema version bump required: Since theme mode is stored independently of the main config object (not as a field within the Config type), this change does not require incrementing CONFIG_SCHEMA_VERSION. Theme settings are orthogonal to user configuration and are managed through a separate storage mechanism to improve performance by preventing unnecessary config atom updates and network re-requests in content scripts.
Per-Provider Feature Assignment#
Within each provider's configuration form on the API Providers page, users can assign features to that provider through the FeatureProviderSection component—a collapsible "Feature Providers" section that:
- Shows toggles for all features compatible with the provider's type (e.g., translation providers show translation-related features)
- Displays assigned features with a disabled toggle (since a feature can only be assigned to one provider at a time)
- Allows users to quickly assign multiple features to a single provider by toggling them ON
- Uses feature-specific validation (some features support nullable provider selection, others require a provider)
- Uses the
buildFeatureProviderPatchhelper function to construct configuration patches when assigning features
Provider Badge System#
Each provider card on the API Providers page displays a badge indicating how many features are assigned to it:
- Badge display: Shows the count of assigned features (e.g., "3 features") using the i18n key
options.apiProviders.badges.featureCount - Tooltip: Hovering over the badge displays a list of all features assigned to that provider, including Language Detection if configured
- Feature names: Displayed using localized keys from
options.general.featureProviders.features.*
The badge system replaced the previous single "Translate" badge, providing a comprehensive view of each provider's role in the system. Language Detection is now included in the feature count when a provider is assigned to the languageDetection feature.
Feature Key I18n Mapping#
The FEATURE_KEY_I18N_MAP constant (defined in feature-providers.ts) maps feature keys with dots (like selectionToolbar.translate) to i18n-safe keys with underscores (like selectionToolbar_translate) for use in locale files. This ensures consistent naming across the UI while maintaining clean configuration structure.
Removed Components and Configuration:
- The
DefaultTranslateProviderSelectorcomponent has been removed. It previously allowed users to toggle a provider as the default for translation through a switch in the provider configuration form. - The
options.general.translationConfig.providerselector has been removed from the Translation Config section. Provider selection for page translation is now handled exclusively through the Feature Providers section. - Translation Config now focuses solely on range settings (preload configuration).
Extending the Schema#
To add new features or providers, increment the schema version, provide a migration script, update example configurations, and ensure localization files reflect the new structure. For structural changes (such as moving config fields or refactoring prompt storage), ensure the migration script transfers values to the new location and update example configuration files to match the new schema. The migration and validation system will handle the rest, maintaining backward compatibility and a smooth upgrade path for users.
Updating LLM Provider Models#
The project maintains a list of available LLM models for each provider in LLM_PROVIDER_MODELS (located in src/utils/constants/models.ts). This list is kept in sync with the Vercel AI SDK documentation through an automated scraping workflow.
Model Sync Workflow:
- The scraper script (
scripts/scrape-ai-sdk-provider-models.ts) fetches model lists from the Vercel AI SDK documentation - Run
pnpm scrape:ai-sdk-modelsto regenerate the model list based on the latest AI SDK docs - The scraper outputs to
scripts/output/ai-sdk-provider-models.jsonwith metadata on providers, models, and capabilities - Model additions and updates can be applied without bumping the configuration schema version
Migration Considerations:
- Adding new models: New models can be added to
LLM_PROVIDER_MODELSwithout requiring a configuration migration. Users' existing model selections remain valid. - Removing deprecated models: If models are removed from the list because they are no longer available, a migration script must be created to update user configurations that reference the deprecated models. The migration should replace deprecated models with appropriate defaults (see the Claude commands in
.claude/commands/update-ai-sdk-models.mdfor the full workflow). - Schema version bump: Only bump
CONFIG_SCHEMA_VERSIONif models are removed and users need to be migrated to new defaults. Adding new models does not require a version bump.
Important: Migration scripts that handle model changes must use hardcoded string literals for model names and defaults, not runtime constants. This ensures migrations are immutable snapshots of a specific point-in-time transformation, even as LLM_PROVIDER_MODELS evolves.
Example: When adding a new field (such as clickAction to floatingButton in v38), the migration script should add the field with a sensible default (e.g., 'panel'). Update the schema, migration script, and example configs accordingly.
// migration-scripts/v037-to-v038.ts
export function migrate(oldConfig: any): any {
return {
...oldConfig,
floatingButton: {
...oldConfig.floatingButton,
clickAction: 'panel',
},
}
}
See also: v38 Example Configuration.
Adding New Provider Types#
When adding a new provider type to the schema, follow these steps to ensure full integration:
- Add provider type constant: Update the relevant provider type arrays in
src/types/config/provider.ts:TRANSLATE_PROVIDER_TYPES: For providers supporting translation (includes both LLM and non-LLM providers)LLM_PROVIDER_TYPES: For LLM-based providers (e.g., "openai", "deepseek", "google", "anthropic", "alibaba", "moonshotai", "huggingface")NON_CUSTOM_LLM_PROVIDER_TYPES: For non-custom LLM providers (excludes custom providers like "openai-compatible", "siliconflow", "tensdaq", "ai302")API_PROVIDER_TYPES: For providers requiring API keys (includes all LLM providers plus "deeplx")ALL_PROVIDER_TYPES: Complete list of all provider types (same asTRANSLATE_PROVIDER_TYPES)
- Define provider schema: Add the provider to
providerConfigSchemaListwith its specific schema definition. - Update default configurations: Add an entry to
DEFAULT_PROVIDER_CONFIGinsrc/utils/constants/providers.tswith provider-specific defaults. If the provider requires connection options (such as region for Bedrock), include defaultconnectionOptionsin the configuration. - Define models and options: For LLM providers, add model lists to
LLM_PROVIDER_MODELSinsrc/utils/constants/models.ts. For specialized providers (like TTS), add model constants and voice/option definitions to the relevant type file (e.g.,src/types/config/tts.ts). - Update provider implementation: In
src/utils/providers/model.ts, import the provider's create function and add an entry toCREATE_AI_MAPPER. The mapper uses TypeScript'sas constassertion for type inference, eliminating the need for a separate interface definition. - Update feature provider definitions: If the provider type changes feature requirements, update
FEATURE_PROVIDER_DEFSinsrc/utils/constants/feature-providers.ts(e.g., changing nullable status). - Add provider metadata: Include logo, name, and website information in
PROVIDER_ITEMS. - Configure connection options (if needed): If the provider requires connection-specific settings (e.g., AWS region), add connection option field definitions to
PROVIDER_CONNECTION_OPTIONS_FIELDSinsrc/utils/constants/providers.ts.
Example: Edge TTS Provider Addition
Edge TTS was added as a built-in TTS provider, demonstrating the pattern for adding free, no-API-key providers with unique configuration requirements:
- TTS configuration is no longer provider-based; Edge TTS is integrated directly as the default and only TTS engine
- Defined 40+
EDGE_TTS_VOICEScovering Chinese, English, Japanese, and Korean languages - Added per-language voice mapping (
languageVoices) to support automatic voice selection based on detected language - Introduced new voice synthesis controls:
rate,pitch, andvolume(range -100 to 100) - Removed TTS from the feature provider system (
FEATURE_PROVIDER_DEFS) as it no longer requires aproviderId - Updated TTS schema to use Edge TTS voice names (e.g.,
'en-US-GuyNeural') instead of OpenAI voice identifiers
This refactor demonstrates how to integrate specialized functionality (like TTS) directly into the extension when a free, high-quality provider (Edge TTS) can replace API-based services. The migration from v53 to v54 handles converting existing OpenAI TTS configurations to the new Edge TTS format.
Example: Bedrock Provider with Connection Options
Amazon Bedrock demonstrates the pattern for providers that require connection-specific configuration beyond API keys:
- The Bedrock provider requires a
regionfield inconnectionOptionsto specify the AWS region (e.g.,us-east-1) - Default configuration in
DEFAULT_PROVIDER_CONFIGincludesconnectionOptions: { region: "us-east-1" } - Connection option fields are defined in
PROVIDER_CONNECTION_OPTIONS_FIELDSwith field metadata (key, label i18n key, input type, placeholder) - The provider configuration form displays connection option fields using the
ConnectionOptionsFieldcomponent - Connection options are passed to the provider's create function during initialization alongside API keys and base URLs
- Empty values in connection options are stripped before storage using the
compactObjectutility - Changes to connection options reset the Connection Test status, prompting users to re-verify their configuration
Pre-translate Range (Preload) Configuration#
Feature: Starting from schema version 36, the configuration supports a preload object under translate.page:
margin(number): How far below the viewport (in pixels) to start translating content. Lower values save API costs but may cause delays when scrolling. Range: 0–5000 (default: 1000).threshold(number): Percentage of element visibility required to trigger translation (0–1). Higher values delay translation until more of the element is visible. Range: 0–1 (default: 0).
This feature helps reduce API usage and lets users tune scroll performance for page translation. The migration script for v35→v36 automatically adds this field with default values to all user configs.
For more information, refer to the migration scripts and example configuration files in the repository.
Example (after migration to v36):
translate: {
// ...
page: {
// ...
preload: {
margin: 1000,
threshold: 0,
},
},
// ...
}
See also: Localization files for user-facing descriptions of the new fields.
Error Reference#
ConfigVersionTooNewError: Thrown when a configuration's schema version is newer than supported. Surfaces a clear upgrade-needed message to the user during import or sync.- All migration and sync operations propagate this error, ensuring users are informed and protected from incompatible configuration changes.
If you encounter a "configuration too new" error, upgrade your extension to the latest version to ensure compatibility.