npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

strapi-provider-translate-custom-api

v2.2.0

Published

Custom HTTP-endpoint translation provider for strapi-plugin-translate. Route translation requests through any URL you control instead of being locked into DeepL/Google/ChatGPT.

Readme

strapi-provider-translate-custom-api

A translation provider for strapi-plugin-translate that routes translation requests to any HTTP endpoint you control instead of a fixed third party (DeepL, Google, ChatGPT). You write the translation server; this provider handles the wire protocol.

⚠️ v2.0.0 contains breaking wire-contract changes. If you are upgrading from v1.x, see the Migration from v1.x section below before deploying. v1.x consumers must update their custom API server to read auth from a header and parse Content-Type before they can install v2.0.0.

Features

  • Bring-your-own translation endpoint — point at any URL that accepts POST and returns plain text.
  • HTML auto-detection — input is sniffed via is-html; HTML payloads are flagged on the wire so your server can handle them differently from plain text.
  • Strapi blocks (jsonb) round-trip — block editor content is converted to HTML for translation and back to blocks afterwards.
  • Locale fallbacks — built-in fallback table for providers that don't support specific locales (e.g. DeepL doesn't support es-419 → falls back to es).
  • Per-item resilience — when one item in a batch fails, the source text is returned for that slot and the rest of the batch still succeeds. If every item fails, the batch throws an AggregateError so the host plugin sees the failure instead of silently presenting source-text fallbacks.
  • Concurrency control — batched fan-out is throttled (default 5 in flight) so a large page doesn't fire dozens of simultaneous POSTs at your translation backend. Configurable via providerOptions.concurrency.
  • Markdown round-tripping — markdown fields are converted to HTML before sending and back to markdown after, so your custom API only ever sees plain text or HTML on the wire (never raw markdown semantics).

Installation

npm install strapi-provider-translate-custom-api

Configuration

Configure in config/plugins.js after installing strapi-plugin-translate:

module.exports = ({ env }) => ({
  translate: {
    enabled: true,
    config: {
      provider: "custom-api",
      providerOptions: {
        apiURL: env("TRANSLATION_API_URL"),     // required
        apiKey: env("TRANSLATION_API_KEY"),      // optional; sent as Bearer token
        translationProvider: "MyProvider",       // optional label, see fallback table
        timeoutMs: 30_000,                       // optional, default 30s
      },
      translatedFieldTypes: [
        "string",
        { type: "blocks", format: "jsonb" },
        { type: "text", format: "plain" },
        { type: "richtext", format: "markdown" },
        "component",
        "dynamiczone",
      ],
    },
  },
});

providerOptions

| Option | Type | Default | Description | |---|---|---|---| | apiURL | string | — (required) | POST endpoint for translations. Validated at init time via new URL(...). | | apiKey | string | undefined | Sent as Authorization: Bearer <apiKey> when set. | | translationProvider | string | undefined | Forwarded as ?provider=... and used to key the locale fallback table. | | timeoutMs | number | 30_000 | Per-request timeout. Hanging endpoints abort after this many milliseconds. | | concurrency | number | 5 | Max in-flight requests when translating a batch. Lower it if your translation backend rate-limits aggressively; raise it if your backend is fast and you have plenty of capacity. |

Wire contract (v2.0.0)

The provider issues one POST per item in the batch.

Request

POST {apiURL}?target={targetLocale}&source={sourceLocale}[&format=html][&provider={translationProvider}]

Headers:
  Content-Type: text/plain    (or text/html when the body is HTML)
  Authorization: Bearer <apiKey>   (only when apiKey is configured)

Body: the raw text or HTML to translate
  • Query parameters are encoded via URLSearchParams. Locale codes, provider names, and any other interpolated values are properly percent-encoded.
  • format=html is added when the input passes is-html(). Plain text omits the parameter.
  • The request aborts after timeoutMs (default 30s) via AbortSignal.timeout(...).
  • The provider runs at most concurrency items in flight at once (default 5) — large pages no longer fire 50+ simultaneous POSTs at your backend.

Per-format behavior

| Field type / format | What hits the wire | |---|---| | string, text, plain | The raw text. Content-Type: text/plain. | | html (input is already HTML) | The HTML. Content-Type: text/html, &format=html. | | markdown | Converted to HTML before sending and back to markdown after. Content-Type: text/html, &format=html. Your custom API never sees raw markdown. | | jsonb (Strapi blocks) | Blocks → HTML (via the host plugin's format service) → POST → HTML response → blocks. Content-Type: text/html, &format=html. |

Response

  • 2xx: the response body is read via response.text() and used as the translated value. The body must be plain text — no JSON envelope.
  • Non-2xx: throws. Per-item failures fall back to source text (with a logged error); a batch where every item fails throws an AggregateError.
  • Empty body: throws as if it were a non-2xx error.

Example custom API server (Express, v2.0.0)

import express from "express";
import { translate } from "your-translation-engine";

const app = express();
app.use(express.text({ type: ["text/plain", "text/html"] }));

app.post("/translate", async (req, res) => {
  const apiKey = req.headers.authorization?.replace(/^Bearer /, "");
  if (apiKey !== process.env.MY_API_KEY) return res.sendStatus(401);

  const { target, source, format } = req.query;
  const isHTML = format === "html";

  const translated = await translate(req.body, { target, source, isHTML });
  res.type(isHTML ? "text/html" : "text/plain").send(translated);
});

app.listen(3000);

Migration from v1.x

If you have a custom API server speaking the v1.x contract, you need to update it before installing v2.0.0. The differences:

| Concern | v1.x | v2.0.0 | |---|---|---| | API key location | ?apiKey=... query param | Authorization: Bearer <key> header | | Query encoding | Raw template-string interpolation | URLSearchParams (proper percent-encoding) | | Content-Type on POST | Not set | text/plain or text/html | | Timeout | None (could hang forever) | 30s default, configurable via timeoutMs | | Failures | Silently returned source text and reported success | Per-item: log + source-text fallback. Batch-level all-fail: throws |

Server-side migration example

Before (v1.x):

app.post("/translate", async (req, res) => {
  if (req.query.apiKey !== process.env.MY_API_KEY) return res.sendStatus(401);
  const { target, source, format } = req.query;
  const text = req.body; // assumed string from raw-body parsing
  const translated = await translate(text, { target, source });
  res.send(translated);
});

After (v2.0.0):

app.use(express.text({ type: ["text/plain", "text/html"] })); // honor Content-Type

app.post("/translate", async (req, res) => {
  const key = req.headers.authorization?.replace(/^Bearer /, "");
  if (key !== process.env.MY_API_KEY) return res.sendStatus(401);
  const { target, source, format } = req.query;
  const translated = await translate(req.body, {
    target,
    source,
    isHTML: format === "html",
  });
  res.type(req.headers["content-type"]).send(translated);
});

Compatibility

  • Requires Strapi v4. Strapi v5 is not yet supported because the host plugin (strapi-plugin-translate) does not yet ship a v5-compatible release.
  • Declared as a peerDependency on strapi-plugin-translate ^1.4.0. npm will warn if you install this provider against an incompatible host plugin version. (The same surface — format.blockToHtml, format.htmlToBlock, format.markdownToHtml, format.htmlToMarkdown — has been stable in the host plugin since v1.3.0; the ^1.4.0 pin matches what the provider has actually been tested against.)

License

MIT — see LICENSE.