# FluentRead's Plugin-Based Architecture for Adding Translation Services: A Complete Guide

> Discover FluentRead's plugin-based architecture for easy translation service integration. Add new services by implementing a standard function signature and updating config files. No core logic modification needed.

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

---

**FluentRead's plugin-based architecture isolates each translation engine into self-contained modules registered via a central map, enabling developers to add new services by simply implementing a standard function signature and updating two configuration files without modifying core logic.**

The open-source FluentRead project (available at `bistutu/fluentread`) demonstrates how a modular design can simplify the integration of diverse translation APIs. By treating every engine—from Google Translate to custom LLM endpoints—as a discrete plugin, the codebase maintains clean separation between core translation orchestration and service-specific implementation details.

## How FluentRead's Plugin-Based Architecture Works

At the heart of the system lies a **central registry** ([`entrypoints/service/_service.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/service/_service.ts)) that maps symbolic service keys to their corresponding implementation functions. Each plugin resides in its own file under `entrypoints/service/` and exports a function conforming to the `ServiceFunction` signature.

The [`entrypoints/utils/option.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/option.ts) file defines the metadata for every supported service, including capability flags (such as `useToken`, `useModel`, or `useProxy`) that automatically configure the UI. When a user selects a service, the runtime retrieves the appropriate function from the registry and executes it without any hard-coded logic specific to that engine.

## Key Benefits of the Plugin-Based Design

### Loose Coupling: Add Services Without Touching Core Logic

Each translation engine lives in an isolated file (e.g., [`google.ts`](https://github.com/bistutu/fluentread/blob/main/google.ts) for Google Translate or [`deepl.ts`](https://github.com/bistutu/fluentread/blob/main/deepl.ts) for DeepL). The core dispatcher only imports the aggregated map from [`_service.ts`](https://github.com/bistutu/fluentread/blob/main/_service.ts):

```typescript
// entrypoints/service/_service.ts
export const _service: ServiceMap = {
    [services.microsoft]: microsoft,
    [services.deepL]: deepL,
    // … additional entries
};

```

To add a new service, you create a file implementing the `ServiceFunction` signature, import it into [`_service.ts`](https://github.com/bistutu/fluentread/blob/main/_service.ts), and add it to the map. No other part of the application—neither the UI components nor the translation queue—requires modification.

### Centralized Metadata with Extensible Capability Sets

The [`option.ts`](https://github.com/bistutu/fluentread/blob/main/option.ts) file serves as the single source of truth for service capabilities. It defines which services require API tokens, model selection, proxy configuration, or custom endpoints:

```typescript
// entrypoints/utils/option.ts
export const services = {
    google: "google",
    deepL: "deepL",
    openai: "openai",
    // …
};

export const useToken = new Set([services.deepL, services.openai]);
export const useModel = new Set([services.openai, services.anthropic]);

```

When you add a new service, you update the relevant capability sets. The UI automatically enables or disables configuration fields based on these sets, eliminating the need for conditional UI logic scattered across components.

### Plugin Isolation Guarantees Safety and Maintainability

Each plugin handles its own request formatting, error handling, and service-specific quirks. For example, the DeepL plugin manages language code conversions and token-based authentication internally, while the Google plugin handles simple GET requests. This isolation prevents failures in one engine from cascading into others and allows developers to unit-test individual plugins without loading the entire application context.

### Dynamic Runtime Selection

The application stores the user's selected service key in `config.service`. At runtime, the translation dispatcher retrieves the corresponding function from the registry:

```typescript
import { _service } from "@/entrypoints/service/_service";
import { config } from "@/entrypoints/utils/config";

async function translateSelection(text: string) {
  const message = { origin: text, context: "" };
  const translator = _service[config.service]; // e.g., "deepL", "google"
  return await translator(message);
}

```

This enables instant switching between services without application restarts and supports feature-gated rollouts where new plugins ship dormant and activate via configuration updates.

### Future-Proofing for New Translation Paradigms

Because the architecture treats every engine as a black-box function conforming to `(message) => Promise<string>`, future paradigms—such as on-device LLMs, hybrid translation pipelines, or streaming responses—can be integrated by implementing the same interface. The core translation queue, caching layer, and UI components remain unchanged regardless of the underlying technology.

## Implementing a New Translation Service Plugin

To add a custom service such as "MyAwesomeAI," create a new file implementing the `ServiceFunction` signature:

```typescript
// entrypoints/service/myawesomeai.ts
import { method } from "../utils/constant";
import { config } from "@/entrypoints/utils/config";

async function myawesomeai(message: any) {
  const resp = await fetch("https://api.myawesome.ai/v1/translate", {
    method: method.POST,
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.token.myawesomeai}`,
    },
    body: JSON.stringify({
      text: message.origin,
      target: config.to,
    }),
  });

  if (!resp.ok) throw new Error("Translation failed");
  const data = await resp.json();
  return data.translation;
}

export default myawesomeai;

```

Next, register the plugin in the central service map:

```typescript
// entrypoints/service/_service.ts
import myawesomeai from "./myawesomeai";
import { services } from "../utils/option";

export const _service: ServiceMap = {
  // … existing entries
  [services.myawesomeai]: myawesomeai,
};

```

Finally, declare the service metadata and capabilities:

```typescript
// entrypoints/utils/option.ts
export const services = {
  // … existing entries
  myawesomeai: "myawesomeai",
};

// If this service requires an API token:
export const useToken = new Set([
  services.deepL,
  services.openai,
  services.myawesomeai, // Add here
]);

```

The UI will now automatically display "MyAwesomeAI" in the service selector and prompt for an API token when selected.

## Summary

- **Loose coupling** allows developers to add or remove translation engines by modifying only the plugin file and the central registry in [`_service.ts`](https://github.com/bistutu/fluentread/blob/main/_service.ts), without touching core logic.
- **Centralized metadata** in [`option.ts`](https://github.com/bistutu/fluentread/blob/main/option.ts) uses capability sets (`useToken`, `useModel`, etc.) to automatically configure the UI for each service.
- **Plugin isolation** ensures that service-specific bugs, authentication handling, and API quirks remain contained within individual modules.
- **Dynamic runtime selection** enables instant switching between registered services via a simple map lookup, supporting experimentation and feature flags.
- **Standardized interface** future-proofs the architecture for emerging translation technologies by enforcing a consistent `(message) => Promise<string>` contract.

## Frequently Asked Questions

### How do I add a new translation service to FluentRead?

Create a new TypeScript file in `entrypoints/service/` that exports a default function conforming to the `ServiceFunction` signature. Import this function into [`entrypoints/service/_service.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/service/_service.ts) and add it to the `_service` map using a unique key from [`entrypoints/utils/option.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/option.ts). Finally, update [`option.ts`](https://github.com/bistutu/fluentread/blob/main/option.ts) to include the new service in the `services` constant and any relevant capability sets (such as `useToken` or `useModel`).

### What is the ServiceFunction signature that plugins must implement?

Each plugin must implement a function that accepts a message object (typically containing `origin` text and optional `context`) and returns a Promise that resolves to a translated string. The exact TypeScript interface is defined in the codebase as `ServiceFunction`, effectively requiring the signature `(message: any) => Promise<string>`. This standardization allows the core dispatcher to invoke any plugin interchangeably without knowing implementation details.

### How does FluentRead handle service-specific configuration like API tokens?

FluentRead centralizes service capabilities in [`entrypoints/utils/option.ts`](https://github.com/bistutu/fluentread/blob/main/entrypoints/utils/option.ts) using Sets such as `useToken`, `useModel`, `useProxy`, and `useCustomUrl`. When a service identifier is added to one of these Sets, the UI automatically enables the corresponding configuration field. The actual token values are stored in `config.token[serviceName]`, which plugins access at runtime to authenticate their requests.

### Can I use multiple translation services simultaneously in FluentRead?

While the runtime dispatcher in [`_service.ts`](https://github.com/bistutu/fluentread/blob/main/_service.ts) selects a single plugin based on `config.service`, the architecture supports rapid switching between services without code changes. Users can change the active service via the UI, and developers can extend the system to call multiple services sequentially or in parallel by modifying the dispatcher logic to iterate over an array of service keys rather than a single value.