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

> Explore FluentRead's configuration system. Learn how it manages extension state using a singleton Config object, default loading, browser storage persistence, and live watchers for seamless synchronization.

- Repository: [ThinkStu/fluentread](https://github.com/bistutu/fluentread)
- Tags: deep-dive
- Published: 2026-02-26

---

**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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/model.ts) |
| `defaultOption` | Supplies UI-friendly default values for select boxes and other controls. | [`entrypoints/utils/option.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/option.ts) |
| Global `config` instance | Singleton that the whole extension imports and reads from. | [`entrypoints/utils/config.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/config.ts) |
| `storage` integration | Reads/writes the JSON representation under `local:config`. | [`wxt.config.ts`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/config.ts) (call to `storage.watch`) |

## Configuration Lifecycle

### 1. Default Definition in model.ts

The `Config` class in [`entrypoints/utils/model.ts`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/entrypoints/main/trans.ts).

## Working with Configuration: Code Examples

### Accessing Configuration Values

Any module can import the singleton and read properties directly:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/model.ts)) and UI presets live in [`entrypoints/utils/option.ts`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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`](https://github.com/bistutu/fluentread/blob/main/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.