Documents
Configuration Schema and Migration
Configuration Schema and Migration
Type
Document
Status
Published
Created
Sep 18, 2025
Updated
Mar 25, 2026
Updated by
Dosu Bot

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) or Ctrl+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.ts with 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 ConfigVersionTooNewError and 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.
  • 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 (didConfigChange flag) during initialization, migration, validation, or dev environment modifications
  • Tracks whether meta needs updating (didMetaNeedUpdate flag) when schemaVersion or lastModifiedAt is missing
  • Only writes config to storage if didConfigChange is true
  • Only writes meta to storage if didConfigChange OR didMetaNeedUpdate is 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, a ConfigVersionTooNewError is 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 preload field 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 promptsConfig to customPromptsConfig.
  • The selected prompt is now referenced by promptId (nullable). If promptId is null, the default prompt (defined in code) is used.
  • Any stored prompt with the ID default is removed from the patterns array.
  • If the previous selected prompt was default, it is converted to null.
// 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:

  1. Unified provider model: Each provider now has a single model field instead of separate models.read and models.translate fields.
  2. Removal of read feature: The entire read configuration block is removed, as the read functionality has been deprecated.
  3. 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, and videoSubtitles.providerId.

The migration script:

  • Flattens provider model configuration from provider.models.translate to provider.model
  • Removes the obsolete read config block
  • Adds selectionToolbar.features with per-feature provider IDs (translate and vocabularyInsight - note: vocabularyInsight was removed in v1.31.1)
  • Inherits the legacy read.providerId for vocabularyInsight when available (this feature was removed in v1.31.1)
  • Adds providerId fields to inputTranslation and videoSubtitles if 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:

  1. Removal of OpenAI-specific fields: The providerId, model, voice, and speed fields are removed from the tts configuration.
  2. 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 selection
    • rate: Speech rate adjustment in the range -100 to 100 (replaces the old speed field)
    • 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 speed field (if present) to the new rate field using the formula: rate = (speed - 1) * 100, clamped to -100 to 100
  • If the rate field already exists, it is validated and clamped to the valid range
  • If neither speed nor rate exists, rate defaults to 0
  • Sets defaultVoice to a known Edge TTS voice if the old voice field matches a known Edge TTS voice name, or falls back to 'en-US-GuyNeural'
  • Populates languageVoices with default Edge TTS voices for all 300+ ISO 639-3 language codes, overriding the English (eng) entry with the migrated defaultVoice
  • Initializes pitch and volume to 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 languageVoices mapping 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 detectLanguageMode field to tts configuration
  • 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.customActions field to the configuration
  • Each custom action includes:
    • id: Unique identifier for the action
    • name: Display name shown in the selection toolbar
    • enabled: Boolean toggle to enable/disable the action
    • icon: Icon identifier (e.g., 'tabler:book-2')
    • providerId: The LLM provider to use for this action
    • systemPrompt: System instructions for the AI
    • prompt: 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 customActions array 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 the outputSchema array.

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 (string or number) 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.ts handle 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 notebaseConnection field 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 description field to SelectionToolbarCustomActionOutputField schema
  • The description field is optional and defaults to an empty string
  • Field types (string and number) 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 position field to videoSubtitles configuration with the following schema:
    • percent: Number (0-100) representing the vertical position as a percentage of the viewport
    • anchor: 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 subtitlePositionSchema type is defined in src/types/config/subtitles.ts
  • Position persistence decouples storage logic from the drag hook: useVerticalDrag stays pure and accepts an onDragEnd callback
  • SubtitlesView handles saving the position to config storage when dragging ends
  • UniversalVideoAdapter restores 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 mode enum 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
  • The single patterns array 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
  • blacklistPatterns is 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 to whitelistPatterns
  • The blacklistPatterns array 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.customFeatures to selectionToolbar.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 old customFeatures key
  • After migration, the old customFeatures key 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 speaking field to SelectionToolbarCustomActionOutputField schema
  • The speaking field 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 as speaking: true
  • All other fields default to speaking: false unless 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:

  1. Removed per-feature language detection settings:
    • translate.page.enableLLMDetection (boolean)
    • translate.page.enableSkipLanguagesLLMDetection (boolean)
    • tts.detectLanguageMode ("basic" | "llm")
  2. Added root-level configuration: New languageDetection object with the following structure:
    • mode: Enum ("basic" | "llm") - Selects between browser-based or AI-based language detection
    • providerId: Optional string - The LLM provider ID to use when mode is "llm"
  3. Unified behavior: Language detection mode now applies consistently across auto-translate, skip-languages, and TTS features
  4. Provider fallback logic: The migration intelligently selects a provider ID when LLM detection was enabled:
    • Prefers the current translate.providerId if 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

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 mode is "llm", the providerId refers 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: true field to selectionToolbar.features.translate
  • Adds enabled: true field to selectionToolbar.features.vocabularyInsight (removed in v1.31.1)
  • Adds new selectionToolbar.features.speak object with enabled: 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 systemPrompt and prompt fields 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 abbreviated targetLang
  • webTitle: Clarifies that this is the web page title, not a custom action or translation title
  • webSummary: Clarifies that this is a web page summary, not a generic summary
  • paragraphs: More accurate than context, 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 opacity field to selectionToolbar configuration
  • 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: 1
    • MAX_SELECTION_OVERLAY_OPACITY: 100
    • DEFAULT_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:

  1. Converts the translate.page.shortcut field from a string array format to a portable hotkey string format
  2. Removes the selectionToolbar.features.vocabularyInsight configuration (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, and metaMod (portable modifier that maps to Command on macOS, Control elsewhere)
    • optionAlt
  • Normalizes key names using standard aliases (e.g., escEscape, returnEnter)
  • 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.vocabularyInsight configuration 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 pageTranslationShortcutSchema with validation via isValidConfiguredPageTranslationShortcut()
  • 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() from page-translation-shortcut.ts
  • The portable Mod key ensures cross-platform compatibility (Command on macOS, Control elsewhere)
  • Keyboard recorder preserves physical keys for macOS Option shortcuts (e.g., Option+3Alt+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

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.local under the key "theme" (defined as THEME_STORAGE_KEY)
  • Storage independence: Theme storage uses a separate storage key to avoid triggering configAtom updates when users switch themes
  • State management: Theme state is managed via themeModeAtom using 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 baseThemeModeAtom for internal state and themeModeAtom for reactive read/write operations
  • The ThemeProvider component uses useSyncExternalStore to 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 buildFeatureProviderPatch helper 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 DefaultTranslateProviderSelector component 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.provider selector 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-models to regenerate the model list based on the latest AI SDK docs
  • The scraper outputs to scripts/output/ai-sdk-provider-models.json with 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_MODELS without 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.md for the full workflow).
  • Schema version bump: Only bump CONFIG_SCHEMA_VERSION if 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:

  1. 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 as TRANSLATE_PROVIDER_TYPES)
  2. Define provider schema: Add the provider to providerConfigSchemaList with its specific schema definition.
  3. Update default configurations: Add an entry to DEFAULT_PROVIDER_CONFIG in src/utils/constants/providers.ts with provider-specific defaults. If the provider requires connection options (such as region for Bedrock), include default connectionOptions in the configuration.
  4. Define models and options: For LLM providers, add model lists to LLM_PROVIDER_MODELS in src/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).
  5. Update provider implementation: In src/utils/providers/model.ts, import the provider's create function and add an entry to CREATE_AI_MAPPER. The mapper uses TypeScript's as const assertion for type inference, eliminating the need for a separate interface definition.
  6. Update feature provider definitions: If the provider type changes feature requirements, update FEATURE_PROVIDER_DEFS in src/utils/constants/feature-providers.ts (e.g., changing nullable status).
  7. Add provider metadata: Include logo, name, and website information in PROVIDER_ITEMS.
  8. 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_FIELDS in src/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_VOICES covering 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, and volume (range -100 to 100)
  • Removed TTS from the feature provider system (FEATURE_PROVIDER_DEFS) as it no longer requires a providerId
  • 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 region field in connectionOptions to specify the AWS region (e.g., us-east-1)
  • Default configuration in DEFAULT_PROVIDER_CONFIG includes connectionOptions: { region: "us-east-1" }
  • Connection option fields are defined in PROVIDER_CONNECTION_OPTIONS_FIELDS with field metadata (key, label i18n key, input type, placeholder)
  • The provider configuration form displays connection option fields using the ConnectionOptionsField component
  • 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 compactObject utility
  • 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.