Local Translate #
Translates posts on-device, using the same WASM engine (bergamot/browsermt) Firefox's own Translations feature uses — no text ever leaves the device for the local engine path. Falls back to a remote LLM endpoint (Ollama, any OpenAI-compatible API, or Anthropic) when either the host doesn't support local WASM translation yet, or the user prefers it.
The remote-endpoint fallback has no address baked in — not even a cloud
provider's. It always goes through app.requestFetchPermission/app.fetch:
the user types in whatever URL they want (local Ollama, their own
OpenAI-compatible proxy, a cloud API, anything) and grants that origin
network access before it's ever used. A local-first translation plugin
shipping with api.openai.com pre-allowlisted in its manifest would look
like it phones home by default, which it doesn't.
Requirements #
Local (on-device) translation needs a handful of core changes to the Impro
host itself — see the plugin-wasm-translation-support branch in impro/.
Without them this plugin still works, using the remote-endpoint backend
only; the settings tab explains this and hides the local option when it
detects the host doesn't support it.
The one piece that needs infrastructure of its own: the model registry
and per-language model files come from
mozilla/translations' real,
current production data (a Google Cloud Storage bucket). Verified directly:
this bucket does have a CORS policy, but it's an origin allowlist (Origin:
https://mozilla.github.io gets Access-Control-Allow-Origin back;
arbitrary other origins, including this plugin's, don't) rather than a
wildcard — there's no way to get this project's own origin added to that
list. infra/cors-relay-worker.js is a small Cloudflare Worker that relays
both the registry and the model files, adding CORS headers and edge-caching
every response (these files are content-addressed/immutable, so caching
forever at the edge is always correct). Combined with the plugin's own
app.binaryCache (each end user downloads a given file at most once, ever),
actual origin bandwidth through this relay is bounded by the number of
distinct files ever requested, not by request volume or user count — and
Cloudflare Workers pricing is request/CPU-based, not bandwidth-based, so
this is meaningfully cheaper to run than a naive proxy might suggest (worth
confirming against your actual plan before relying on that).
Deploy it, then point MODEL_CDN_BASE_URL in src/engine/modelCdn.js (and
the matching permissions.fetch entry in manifest.json) at wherever it
ends up. The engine .wasm itself is the one piece that doesn't need this —
jsdelivr's npm mirror sets CORS headers correctly and is fetched directly.
Development #
npm install --save ../impro/impro-plugin # point at the local SDK, per the workspace README
npm run watch
Symlink into impro/plugins-local/ and run impro's own dev server per the
workspace README's plugin-development workflow.
License note #
src/vendor/bergamot-translator-worker.js and the loading approach in
src/engine/bergamotEngine.js are vendored/adapted from
bergamot-translator
(MPL-2.0). See the comments at the top of each file for exactly what
changed and why.