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

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) 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 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 for Google Translate or deepl.ts for DeepL). The core dispatcher only imports the aggregated map from _service.ts:

// 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, 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 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:

// 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:

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:

// 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:

// 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:

// 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, without touching core logic.
  • Centralized metadata in 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 and add it to the _service map using a unique key from entrypoints/utils/option.ts. Finally, update 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 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 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.

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 →