How FluentRead's Configuration System Works: A Deep Dive into Extension State Management

FluentRead's configuration system uses a singleton Config object that loads defaults from TypeScript classes, persists to browser storage under the key local:config, and synchronizes across extension components via live watchers.

FluentRead is an open-source browser extension that provides seamless translation capabilities. At the heart of its architecture lies a robust configuration system that manages user preferences, API tokens, and UI settings. Understanding how FluentRead's configuration system works reveals a well-designed pattern for state management in browser extensions using TypeScript and the WXT framework.

Architecture Overview

The architecture centers on a single source of truth: a global config instance exported from entrypoints/utils/config.ts. This singleton pattern ensures that every module reads from the same in-memory state while the underlying @wxt-dev/storage API handles persistence and cross-tab synchronization.

Core Components

Component Purpose Key file
Config class Holds every configurable field (service, languages, UI style, hotkeys, tokens, etc.) and defines default values. entrypoints/utils/model.ts
defaultOption Supplies UI-friendly default values for select boxes and other controls. entrypoints/utils/option.ts
Global config instance Singleton that the whole extension imports and reads from. entrypoints/utils/config.ts
storage integration Reads/writes the JSON representation under local:config. wxt.config.ts (permission declaration) and multiple utility modules.
Validation helpers Ensure the stored JSON represents a viable configuration before merging. entrypoints/utils/config.ts (function isConfigObjectValid)
Watcher Reacts to external changes (e.g., UI updates) and keeps the in-memory object up-to-date. entrypoints/utils/config.ts (call to storage.watch)

Configuration Lifecycle

1. Default Definition in model.ts

The Config class in entrypoints/utils/model.ts declares every possible option and supplies sensible defaults in its constructor. This ensures that the application always has valid fallback values even on first run.

2. UI Presets in option.ts

The defaultOption object in entrypoints/utils/option.ts provides preset values used by the interface, such as default service set to Microsoft and default hotkey set to Control.

3. Initial Load from Storage

Upon extension start, entrypoints/utils/config.ts reads the JSON string stored under the key local:config via @wxt-dev/storage.

  • If the stored value exists and parses to an object containing required fields (on, service, from, to), it is merged into the global config instance using Object.assign.
  • If the stored value is missing, empty, or invalid, the current config (already containing defaults from new Config()) is written back to storage, ensuring a persistent baseline.

4. Validation with isConfigObjectValid

The isConfigObjectValid function checks that the parsed object is a plain object and that essential keys are present. This guards against corrupted or partially-written data that could crash the extension.

5. Live Watching for Cross-Component Sync

storage.watch('local:config', …) registers a listener that fires whenever another part of the extension (such as the popup UI) updates the configuration. The listener parses the new JSON, validates it, and merges the changes into the in-memory config. Invalid updates are ignored with a warning, ensuring type safety.

6. Persistence on State Changes

Whenever the app modifies a setting (for example, after incrementing a successful translation count), the code explicitly calls storage.setItem('local:config', JSON.stringify(config)) to keep the persisted copy in sync. This pattern appears in entrypoints/main/trans.ts.

Working with Configuration: Code Examples

Accessing Configuration Values

Any module can import the singleton and read properties directly:

import { config } from '@/entrypoints/utils/config';

if (config.on && config.service === 'openai') {
  console.log(`Using OpenAI with model ${config.model?.openai}`);
}

Modifying Settings from UI Components

UI components update the configuration by writing to storage, which triggers the global watcher:

import { storage } from '@wxt-dev/storage';
import { config } from '@/entrypoints/utils/config';

function onTargetLanguageChange(newLang: string) {
  // Update the in-memory object
  config.to = newLang;
  // Persist the change
  storage.setItem('local:config', JSON.stringify(config));
}

Listening for External Changes

While the central watcher keeps config updated, you can register additional listeners for side effects:

import { storage } from '@wxt-dev/storage';

storage.watch('local:config', (newValue) => {
  const parsed = JSON.parse(newValue);
  console.log('Config was changed elsewhere:', parsed);
  // The global config is already updated by the central watcher
});

Resetting to Defaults

To restore factory settings, instantiate a fresh Config and overwrite both the singleton and storage:

import { storage } from '@wxt-dev/storage';
import { Config } from '@/entrypoints/utils/model';
import { config } from '@/entrypoints/utils/config';

async function resetConfig() {
  const defaultConfig = new Config(); // fresh defaults
  Object.assign(config, defaultConfig);
  await storage.setItem('local:config', JSON.stringify(defaultConfig));
}

Key Files and Implementation Details

File Role Direct link
entrypoints/utils/model.ts Defines the Config class and default constructor values. https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/model.ts
entrypoints/utils/option.ts Provides UI option tables and the defaultOption object. https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/option.ts
entrypoints/utils/config.ts Central loader, validator, watcher, and singleton export (config). https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/config.ts
wxt.config.ts Declares the storage permission used by the extension. https://github.com/bistutu/fluentread/blob/main/wxt.config.ts
entrypoints/main/trans.ts Example of persisting a mutable field (count). https://github.com/bistutu/fluentread/blob/main/entrypoints/main/trans.ts
UI components (e.g., components/CustomHotkeyInput.vue) Demonstrate reading/writing configuration via the singleton. https://github.com/bistutu/fluentread/blob/main/components/CustomHotkeyInput.vue

Summary

  • FluentRead's configuration system relies on a singleton Config instance exported from entrypoints/utils/config.ts that serves as the single source of truth for all settings.
  • Defaults are defined in the Config class constructor (entrypoints/utils/model.ts) and UI presets live in entrypoints/utils/option.ts.
  • The system validates persisted data using isConfigObjectValid before merging, preventing crashes from corrupted storage.
  • Live synchronization is achieved through storage.watch('local:config', …), ensuring that changes from the popup UI immediately reflect in the content script.
  • All mutations persist to local:config via storage.setItem, making settings durable across browser restarts.

Frequently Asked Questions

How does FluentRead handle corrupted configuration data?

When loading from storage, FluentRead validates the parsed JSON using the isConfigObjectValid function in entrypoints/utils/config.ts. This helper checks that the data is a plain object and contains required fields (on, service, from, to). If validation fails, the system discards the corrupted data and writes the default configuration back to storage, ensuring the extension always starts with valid settings.

Can multiple extension components modify settings simultaneously?

Yes, multiple components can safely modify settings because the configuration system uses storage.watch('local:config', …) to monitor changes. When one component calls storage.setItem, the watcher in entrypoints/utils/config.ts fires and merges the new values into the global singleton using Object.assign. This ensures all parts of the extension stay synchronized, with the last valid write persisting to storage.

Where are API tokens and sensitive settings stored?

API tokens and sensitive configuration fields are stored as properties of the Config class defined in entrypoints/utils/model.ts. The entire configuration object is serialized to JSON and persisted under the storage key local:config using the @wxt-dev/storage API. While the storage is local to the browser, this follows the standard browser extension storage model, with data encrypted at rest according to the browser's security policies.

How do I reset FluentRead to default settings programmatically?

To restore factory settings, instantiate a fresh Config object from entrypoints/utils/model.ts, which initializes all properties to their constructor defaults. Use Object.assign to overwrite the global singleton, then persist the defaults by calling storage.setItem('local:config', JSON.stringify(defaultConfig)). This ensures both the in-memory state and the persisted storage return to baseline values.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →