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 globalconfiginstance usingObject.assign. - If the stored value is missing, empty, or invalid, the current
config(already containing defaults fromnew 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
Configinstance exported fromentrypoints/utils/config.tsthat serves as the single source of truth for all settings. - Defaults are defined in the
Configclass constructor (entrypoints/utils/model.ts) and UI presets live inentrypoints/utils/option.ts. - The system validates persisted data using
isConfigObjectValidbefore 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:configviastorage.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →