Translate web pages on your own device. No cloud, no account, no telemetry.
If this project helps you, a coffee helps me keep working on it.
Glossa is a browser extension that translates pages without sending the text anywhere. It runs the same neural translation engine that Firefox uses for its built-in translator, compiled to WebAssembly, inside your browser. Page text never leaves the machine.
Every popular translator extension, including the ones marketed on privacy, ships with a cloud engine as the default. The one that leads the market keeps its source closed and had a public data leak in 2025. Browser built-in translators are convenient but you're trusting the vendor's word about what gets uploaded.
Glossa takes the opposite approach. There is no cloud engine in the code at all, so there is nothing to opt out of. The extension holds no permission to read websites on its own. It touches a page only when you ask, and the only hosts it can ever contact are the three Mozilla-operated locations that serve the language models, or a mirror of them that you run yourself. On Chrome you can go further and switch to Chrome's own on-device translator, and then translating a page makes no request at all.
- Translates a whole page, keeping the original in place and showing the translation under each block. A replace mode is one click away, with the original as a hover tooltip.
- Detects the page language locally. You can override it.
- Keeps inline links and formatting inside sentences, and leaves code blocks, brand names marked
translate="no", and form fields alone. Web addresses, email addresses and reference numbers come back exactly as they went in. - Follows content that arrives later, so infinite scroll and single-page apps get translated too. It notices revealed panels, text the page rewrites in place, open shadow roots, and frames from the same site.
- Handles a page written in more than one language. A quoted paragraph that declares its own
langis translated with that language's model, or left alone if you do not have it. - Restores the original page without a reload.
- Translates a selection from the context menu, and translates what you have typed into a text box from the same menu.
- Per-site rules. Turn Glossa off for a host and it will not even look at its pages. List the languages you read and those pages are never offered.
- Downloads each language model once (roughly 20 to 45 MB per direction), verifies it against Mozilla's published hashes, and keeps it on disk. You see the size before anything downloads and can delete models from the options page.
- Can use Chrome's built-in translator instead (Chrome 138 or newer, on a desktop with 22 GB of free disk). It runs on your device too. Chrome fetches its language packs from Google the first time you click Translate for a pair, and Glossa sends nothing to Mozilla while it's selected. Bergamot stays the default, and Firefox doesn't offer the choice.
Glossa's own interface follows your browser's language: English, Spanish, German, French, Japanese and Chinese are included, and anything missing falls back to English.
Coverage follows Firefox's catalog: about 60 languages, all pivoting through English. A Spanish to French translation therefore runs two models.
Glossa is not in any store yet. Load it unpacked from a release ZIP or from a local build. Every
release asset ships a .sha256 sidecar, so you can check a download before you trust it.
Chrome, Edge, Brave, and other Chromium browsers (Chrome 116 or newer)
- Download
glossa-chrome-vX.Y.Z.zipfrom the releases page and extract it to a folder you will keep. The browser loads the extension from that folder on every start. - Open
chrome://extensions, turn on Developer mode, click Load unpacked, and pick the folder.
A .crx is attached as well, for anyone who would rather keep one file. Chromium refuses to install
a self-signed CRX downloaded from the web, so the ZIP is the path that works.
Firefox (Firefox 142 or newer)
- Download
glossa-firefox-vX.Y.Z.zip. - Open
about:debugging#/runtime/this-firefox, click Load Temporary Add-on, and choose the ZIP. Temporary add-ons are removed when Firefox closes. A signed build for permanent installs is on the roadmap. - Open the Glossa popup. Firefox treats host permissions as optional and a temporary add-on starts with none at all, so the popup shows an "Allow model downloads" button the first time. One click and downloads work, with no reload.
Two honest comparisons, because you will notice both.
Speed. Glossa is slower than Firefox's own built-in translator on the same machine, and it always
will be. Firefox runs the engine's matrix multiplication through WebAssembly.mozIntGemm, which is
only available to privileged browser code, and it can use threads. An extension gets neither, so
Glossa runs a single-threaded SIMD build. Expect a long article to take a few seconds rather than
under one.
Quality. These are Mozilla's models, and on Mozilla's own evaluation they average about 4 COMET22 points below Google Translate across the 105 released pairs, and never come out ahead. Most pairs are close enough that you will not care. The weakest are English to Marathi, Hindi, Arabic, Telugu and Thai, and Marathi to English, where the gap is 7 to 9 points. Mozilla publishes the numbers at mozilla.github.io/translations/final-evals.
Hardware. The engine needs WebAssembly SIMD: any x86 CPU with SSE4.1 (Intel from 2008, AMD from 2011) or a 64-bit ARM machine. It never shipped for 32-bit ARM, so old Android phones cannot run it.
Requirements: Node 24 or newer, Python 3 with Pillow (only for regenerating icons).
npm install
npm run engine:fetch # downloads the 5 MB Bergamot WASM binary and verifies both hashes
npm run build # writes dist/chrome, dist/firefox, and one ZIP per targetnpm run verify runs the typecheck, lint, unit tests, and build. npm run smoke builds a test variant with a loopback host permission and runs the headless Chromium test, which downloads the Spanish to English model and translates a fixture page through the real popup. npm run smoke:firefox does the same in the system Firefox through Selenium (pip install selenium; geckodriver is fetched automatically). npm run screenshots refreshes the images above the same way. If your firewall blocks outbound traffic per binary, point the smoke at a Chromium build it does allow with GLOSSA_CHROMIUM_PATH.
npm run smoke:chrome-engine needs Chrome itself (138 or newer), because Chromium builds don't ship the built-in translator. Chrome ignores --load-extension, so the test loads the build over the DevTools protocol, switches the engine, downloads a pack from a real click in the popup, and fails if any request reaches a Mozilla host.
npm run smoke:mirror builds a small Spanish to English mirror under .tmp/mirror the first time, serves it from the loopback server, and translates the fixture through it. It fails if any request reaches a Mozilla or Google host, and also if the mirror's own files are missing from what the extension fetched.
npm run verify:release runs everything: the checks above, every browser smoke and the axe pass. GLOSSA_SMOKE_PIVOT=1 adds a Spanish to French run, which goes through English and downloads a second model. npm run smoke:a11y runs axe against the popup, the options page and a translated page, and fails on any violation.
npm run bump 0.3.0 moves every version string and dates the changelog heading. npm run release
builds the artifacts with their SHA-256 sidecars and a CRX; npm run release:publish also tags the
commit and creates the GitHub release. The CRX signing key is generated into a gitignored
glossa.pem on first use, and keeping that file is what keeps the extension id stable.
Single purpose. Glossa translates the text of a web page into another language, on the user's own machine. It does nothing else.
Why each permission is there.
activeTabandscripting: the content script is injected into the tab the user invokes Glossa on, and only then. There is no content script declared for any site.storageandunlimitedStorage: settings, and the language models, which are tens of megabytes each and live in the browser's Cache storage.contextMenus: the "Translate this page" and "Translate selection" entries.offscreen(Chrome only): the translation engine is a WebAssembly module in a Web Worker, and a service worker cannot keep one alive. Firefox's background page hosts it directly and the Firefox package contains no offscreen code at all.- Three host permissions, all Mozilla-operated:
firefox.settings.services.mozilla.comfor the model catalog,firefox-settings-attachments.cdn.mozilla.netand Mozilla's model bucket onstorage.googleapis.comfor the model files themselves. <all_urls>is optional and never requested at install. It is asked for, from a click, only when a user adds an "always translate" rule for a site, and only for that site, or when they point Glossa at their own model mirror, and only for that host.
About the model files. The engine is a WebAssembly binary inside the package; it is never fetched at runtime. What is fetched is data: the language model files Mozilla publishes for its own translator. Each file is checked against the SHA-256 hash in Mozilla's catalog before it is used, whichever of the two sources served it, and nothing is fetched until a user asks for a language.
What is sent. Nothing but those model requests. Page text is translated in a Web Worker inside the extension and never leaves the machine. There is no analytics, no telemetry, no account, no remote configuration and no error reporting. With the optional Chrome engine selected, translation goes through the browser's own on-device Translator API instead, and the extension makes no model requests at all.
Rebuilding the package. Every release carries glossa-source-vX.Y.Z.zip with the full source,
the lockfile, and a BUILDING.txt naming the exact steps and the engine hashes. The packages are
byte-reproducible: npm ci --ignore-scripts && npm rebuild esbuild && npm run engine:fetch && npm run build produces ZIPs whose SHA-256 matches the published ones exactly, which
npm run verify:source checks by unpacking the archive and doing it.
page ──(activeTab click)──▶ content script
│ block-level segments, inline tags kept
▼
background
│
Chrome: offscreen document Firefox: background page
│
Web Worker running Bergamot (WASM, SIMD, single thread)
│ models from Cache storage
│ (or Chrome's own Translator, if you picked it)
▼
translated HTML fragments back into the page
The engine binary ships inside the package. Models are data. The catalog at firefox.settings.services.mozilla.com says which files a pair needs and publishes a SHA-256 hash for each. The bytes then come from one of two Mozilla-operated sources that hold identical files: the Remote Settings CDN at firefox-settings-attachments.cdn.mozilla.net, or Mozilla's model registry bucket on Google Cloud Storage. The CDN answers 406 to any browser whose user agent says "Chrome", so Chromium browsers end up on the bucket after one refused request. Every file is verified against the catalog hash before it is stored, whichever source served it. All three locations are listed in the manifest and nothing else is.
- Translation happens in a Web Worker inside the extension. No page text is sent anywhere.
- The extension has no host permission for websites. It injects its script only into the tab you invoke it on.
- Network access is limited to three Mozilla-operated locations, and only for model downloads. You can verify this in the manifest and in the browser's network log. On Chromium browsers the model bytes come from a Mozilla bucket hosted on Google Cloud Storage, so Google's edge sees a download of a static file for a language pair. If you'd rather avoid even that, run your own mirror (next section).
- With Chrome's built-in translator selected, Glossa itself fetches nothing to translate a page. Chrome downloads its language packs through its own component updater, so that traffic is between you and Google, the same as for any site that uses the API.
- No analytics, no crash reporting, no account, no update checks beyond what the browser does for any extension.
Glossa can take its catalog and model files from a server you control instead of Mozilla's. Build the mirror from a checkout or from the source archive:
node tools/mirror-models.mjs --out ./glossa-mirror --pairs es:en,en:es
node tools/mirror-models.mjs --out ./glossa-mirror --all # every pair, several GBThe tool downloads each file from Mozilla once, checks it against the hashes in Mozilla's catalog, and writes a records.json that lists only what you mirrored. A pair that goes through English (es:fr) pulls in both halves. Run it again later and it skips every file that's already there and correct.
Serve the directory over HTTPS with any static file server. Then paste its address into Glossa's options, under Language models, and click Use this mirror. The browser asks you once for permission to reach that host. From then on the catalog and every model download come from your mirror, and nothing goes to Mozilla or Google. The server needs no CORS headers, since an extension's requests to a host it has permission for aren't subject to CORS.
The mirror's catalog becomes the authority for the hashes. A mirror someone else runs could serve models that match its own catalog, so only use one you trust.
Glossa is MIT. The Bergamot engine (bergamot-translator.js and bergamot-translator.wasm) and the language models are Mozilla Public License 2.0; the license text ships in every build as LICENSE.bergamot.txt. See vendor/bergamot/README.md for how the engine is pinned and updated.
The engine and models come from Mozilla's translations project, which grew out of the EU-funded Bergamot project at the University of Edinburgh and partners. Glossa would not exist without that work.



