A cross-browser extension (Chrome & Firefox) that helps you hear the pronunciation of words on any webpage. Select a word and a popup appears with a play button. The extension detects the language from surrounding text context and pronounces the word accordingly.
- Features
- Installation
- Usage
- How Language Detection Works
- Supported Languages
- Browser Compatibility
- Accessibility
- Privacy
- Building a Release
- Project Structure
- Technical Notes
- License
- Selection popup — Select any word and a small popup appears above it with a play button
- Context menu — Right-click a selected word and choose "Pronounce" from the menu
- Language detection — Analyzes surrounding words to detect the language (13 languages supported)
- Native AI detection — Progressively enhances with Chrome's built-in LanguageDetector API (138+) when available
- Web Speech API — Uses the browser's built-in speech synthesis for pronunciation
- Cross-browser — Works in both Chrome (MV3) and Firefox (MV3, 109+)
- Accessible — ARIA live region announcements, keyboard dismissal, focus-visible styles
- Respects preferences — Adapts to light/dark color scheme and reduced motion settings
- Lightweight — No external dependencies, no network requests, fully offline
Go to the Releases page on GitHub and download the file for your browser:
| Browser | File to download |
|---|---|
| Chrome / Edge / Brave | pronunciation-helper-chrome-vX.X.X.zip |
| Firefox | pronunciation-helper-firefox-vX.X.X.xpi |
Tip: Always pick the latest release at the top of the page.
The extension installs permanently — it stays active even after you restart Chrome.
- Download the
pronunciation-helper-chrome-vX.X.X.zipfile from the Releases page - Extract/unzip the file:
- On Windows: Right-click the ZIP → "Extract All..." → choose a location you'll keep (e.g.,
Documents/pronunciation-helper) - On macOS: Double-click the ZIP file — a folder appears next to it
- On Linux: Right-click → "Extract Here"
- On Windows: Right-click the ZIP → "Extract All..." → choose a location you'll keep (e.g.,
- Open Chrome and type
chrome://extensionsin the address bar, then press Enter - Turn on "Developer mode" using the toggle in the top-right corner of the page
- Click the "Load unpacked" button that appears
- Navigate to the folder you just extracted (the one containing
manifest.json) and select it - Done — the extension appears in your list and stays installed permanently
Important: Do not delete or move the extracted folder after loading it. Chrome reads the extension files from that location. If you move the folder, Chrome will disable the extension and you'll need to reload it from the new location.
Same process as Chrome — the extension stays installed permanently.
- Download the
pronunciation-helper-chrome-vX.X.X.zipfile (Edge uses the same format as Chrome) - Extract/unzip the file to a permanent location (e.g.,
Documents/pronunciation-helper) - Open Edge and type
edge://extensionsin the address bar, then press Enter - Turn on "Developer mode" using the toggle in the bottom-left of the page
- Click "Load unpacked"
- Navigate to the extracted folder and select it
- Done — the extension stays installed across restarts
Important: Same as Chrome — don't delete or move the folder after loading.
Firefox requires extensions to be signed for permanent installation. The .xpi file in the release is already signed, so it installs permanently.
- Download the
pronunciation-helper-firefox-vX.X.X.xpifile from the Releases page - Open Firefox and type
about:addonsin the address bar, then press Enter - Click the gear icon (⚙️) near the top of the page
- Select "Install Add-on From File..."
- Navigate to the
.xpifile you downloaded and select it - Firefox will ask you to confirm — click "Add"
- Done — the extension is permanently installed and survives browser restarts
Alternative (unsigned builds): If you're using an unsigned
.xpi(e.g., built locally without signing), you need Firefox Developer Edition or Firefox Nightly:
- Open
about:configin the address bar- Search for
xpinstall.signatures.required- Set it to
false- Then install the
.xpias described aboveRegular Firefox requires signed extensions — this workaround only works in Developer Edition or Nightly.
- Download the new
.zipfrom the Releases page - Extract it, replacing the old folder contents (same location)
- Go to
chrome://extensions(oredge://extensions) - Find "Pronunciation Helper" and click the reload icon (🔄) on its card
- Download the new
.xpifrom the Releases page - Go to
about:addons - Use the gear icon → "Install Add-on From File..." and select the new
.xpi - Firefox will update the extension in place
| Problem | Solution |
|---|---|
| Chrome says "Extension not found" after restart | The extracted folder was moved or deleted. Re-extract and reload from the new location. |
| Firefox says "Add-on could not be installed" | The .xpi may be unsigned. Use Firefox Developer Edition with signature checking disabled (see above). |
| Extension doesn't appear after loading | Make sure you selected the correct folder (the one directly containing manifest.json, not a parent folder). |
| No sound when clicking play | Your system may not have speech synthesis voices installed for that language. Check your OS text-to-speech settings. |
- Select a word on any webpage by clicking and dragging, or double-clicking
- A small popup appears above the selection showing a play button and the detected language
- Click the play button to hear the pronunciation
- Press Escape to dismiss the popup
- Alternatively, right-click a selected word and choose "Pronounce" from the context menu
The extension uses a multi-signal approach with progressive enhancement:
- Native API (Chrome 138+) — Uses the browser's built-in
LanguageDetectorAPI when available for high-accuracy detection - Selected text priority — Scores the selected text itself with 3x weight, so the language of what you selected takes precedence over the surrounding page content
- Surrounding context — Analyzes ~200 characters around the selected word as a secondary signal
- Common word matching — Checks for function words (articles, prepositions, conjunctions) typical of each language
- Morphological patterns — Looks for language-specific suffixes and character patterns (e.g., German umlauts, French accents, Cyrillic characters)
- Confidence threshold — Falls back to the page's
langattribute if detection confidence is too low - Page language fallback — Uses the HTML
langattribute as a baseline; defaults to German if unset
| Code | Language |
|---|---|
| de | German |
| en | English |
| fr | French |
| es | Spanish |
| it | Italian |
| pt | Portuguese |
| tr | Turkish |
| hu | Hungarian |
| uk | Ukrainian |
| ru | Russian |
| el | Greek |
| la | Latin |
| he | Hebrew |
| Browser | Minimum Version | Notes |
|---|---|---|
| Chrome | 88+ | Full Manifest V3 support; native LanguageDetector from 138+ |
| Firefox | 109+ | MV3 support with browser_specific_settings |
| Edge | 88+ | Chromium-based, same as Chrome |
- Screen reader support — Uses
aria-live="polite"region to announce pronunciation status - Keyboard navigation — Press Escape to dismiss the popup; play button is focusable with visible focus indicator
- ARIA roles — Popup uses
role="toolbar"with descriptivearia-label - Reduced motion — Animations are disabled when
prefers-reduced-motion: reduceis active - Color scheme — Popup adapts to light/dark mode via
prefers-color-scheme - Contrast — Focus indicators meet 3:1 non-text contrast ratio
This extension collects no data and makes no network requests.
What it accesses:
- Selected text — The word you select, passed to the browser's speech synthesis engine
- Surrounding context — ~200 characters around your selection, analyzed locally for language detection (never stored or transmitted)
- Page language attribute — The
<html lang="...">value as a fallback
What it does NOT do:
- No data collection, storage, or transmission
- No network requests — fully offline
- No cookies, analytics, or telemetry
- No browsing history tracking
- No persistent storage (
chrome.storageis not used) - No device fingerprinting
Permissions explained:
| Permission | Why it's needed |
|---|---|
contextMenus |
Registers the "Pronounce" right-click menu item |
Content script on <all_urls> |
Enables the selection popup on any webpage you visit |
All speech synthesis is handled by your operating system's built-in text-to-speech engine (Web Speech API). No audio data leaves your device.
The project includes a build.sh script that produces both browser packages and optionally publishes them to GitHub:
- A
.zipfor Chrome/Edge (load unpacked) - A signed
.xpifor Firefox (permanent install)
- Node.js and npm — needed to install
web-ext(the script installs it automatically if missing) - Mozilla API credentials — required to sign the Firefox extension
- Create a free account at addons.mozilla.org
- Generate API keys at: https://addons.mozilla.org/developers/addon/api/key/
- Add them to your
~/.zshenv:export MOZILLA_JWT_ISSUER="your-jwt-issuer" export MOZILLA_JWT_SECRET="your-jwt-secret"
- Restart your terminal (or run
source ~/.zshenv)
- GitHub CLI (only for
--releaseflag)- Install:
brew install gh - Authenticate — see GitHub CLI authentication below
- Install:
# Build only (produces files in dist/)
./build.sh
# Build and publish to GitHub Releases in one step
./build.sh --releaseThe script will:
- Read the version from
manifest.json - Install
web-extglobally if not already present - Verify that
MOZILLA_JWT_ISSUERandMOZILLA_JWT_SECRETare set - Create a
dist/folder with:pronunciation-helper-chrome-vX.X.X.zippronunciation-helper-firefox-vX.X.X.xpi(signed asunlisted)
- With
--release: create a git tag, push it, and publish a GitHub Release with both files attached
Note: If the signed
.xpialready exists indist/for the current version, the script skips the signing step. This avoids the "already submitted" error from Mozilla when re-running the script.
Mozilla requires a --channel when signing extensions. The build script uses unlisted. Here's what the options mean:
| Channel | What it means | When to use |
|---|---|---|
unlisted |
Signed by Mozilla but not published on addons.mozilla.org. You distribute the .xpi yourself (e.g., via GitHub Releases). |
Self-distributed extensions like this one. |
listed |
Signed and published on addons.mozilla.org. Goes through Mozilla's review process and becomes publicly searchable. | When you want the extension available in the Firefox Add-ons store. |
We use unlisted because the extension is distributed through GitHub Releases, not the Firefox Add-ons store. Users install it manually from the .xpi file. Mozilla still signs it, which means it works in regular Firefox without disabling signature checks.
# 1. Bump the version in manifest.json (e.g., "1.0.0" → "1.1.0")
# 2. Commit and push
git add -A
git commit -m "chore: prepare v1.1.0 release"
git push origin main
# 3. Build and publish
./build.sh --releaseThat's it. If there's nothing to commit (code is already pushed), skip straight to step 3.
The --release flag requires the GitHub CLI (gh) to be installed and authenticated.
Recommended approach — personal access token:
- Go to https://github.com/settings/tokens/new
- Give it a name (e.g.,
gh-cli) - Set an expiration (e.g., 90 days)
- Select these scopes: repo, workflow, read:org
- Click Generate token and copy it
- Run:
echo "YOUR_TOKEN" | gh auth login -h github.com --with-token
- Verify:
gh auth status
Alternative — browser-based login:
gh auth login -h github.com -s workflow -wThis opens a browser for OAuth authentication. Pick the correct GitHub account when prompted.
If the browser flow hangs (terminal shows no progress after completing the browser steps): this is a known issue on macOS with multiple GitHub accounts or keychain conflicts. Cancel with Ctrl+C, then use the personal access token approach above instead.
| Problem | Cause | Solution |
|---|---|---|
This upload has already been submitted |
You already signed this exact version with Mozilla | The script skips signing if the .xpi exists in dist/. If you deleted it, bump the version in manifest.json and rebuild. |
MOZILLA_JWT_ISSUER and MOZILLA_JWT_SECRET must be set |
Environment variables not loaded | Add them to ~/.zshenv and run source ~/.zshenv, or restart your terminal. |
GitHub CLI is not authenticated |
gh has no valid token |
Run the token-based auth (see above). |
missing required scope 'read:org' |
Token doesn't have enough permissions | Regenerate the token with scopes: repo, workflow, read:org. |
Failed to create release, "workflow" scope may be required |
Token missing workflow scope |
Same fix — regenerate with all three scopes. |
| Firefox signing hangs or times out | Mozilla's signing queue is slow | Wait a few minutes and retry. The --timeout flag can be added to web-ext sign if needed. |
pronunciation-helper/
├── manifest.json # Extension manifest (Manifest V3)
├── background.js # Service worker — context menu registration
├── content.js # Content script — selection handling and popup UI
├── content.css # Popup styles (namespaced, responsive to user preferences)
├── language-detector.js # Language detection with native API progressive enhancement
├── build.sh # Build script — produces Chrome .zip and signed Firefox .xpi
├── icons/
│ ├── icon-16.svg # Source SVG (16px)
│ ├── icon-16.png # Generated PNG for toolbar
│ ├── icon-48.svg # Source SVG (48px)
│ ├── icon-48.png # Generated PNG for extensions page
│ ├── icon-96.svg # Source SVG (96px)
│ ├── icon-96.png # Generated PNG for high-DPI
│ ├── icon-128.svg # Source SVG (128px)
│ └── icon-128.png # Generated PNG for Chrome Web Store
├── dist/ # Build output (git-ignored)
└── README.md
- Uses Manifest V3 for both Chrome and Firefox compatibility
- Background script declares both
service_worker(Chrome/Edge) andscripts(Firefox) — each browser uses its supported field and ignores the other - Content scripts use namespaced CSS classes to avoid conflicts with page styles
- CSS uses rem units, logical properties, and CSS custom properties for theming
- Progressively enhances with Chrome's LanguageDetector API (138+) without requiring it
- The
browser_specific_settingsfield is ignored by Chrome and required by Firefox for extension ID - No
eval(), no inline scripts — fully CSP-compliant - All async operations use
async/await(no.then()chains) - Decorative SVGs use
aria-hidden="true"
Chrome does not support SVG for extension icons — it shows a generic puzzle-piece icon if you only provide SVGs. The manifest must reference PNG files.
The SVG files are the source of truth. They use inline <style> blocks with CSS classes for maintainability. To optimize them, run them through SVGOMG.
Regenerating PNGs from SVGs:
If you modify the SVG icons, regenerate the PNGs using sharp-cli:
npx sharp-cli --input icons/icon-16.svg --output icons/icon-16.png resize 16 16
npx sharp-cli --input icons/icon-48.svg --output icons/icon-48.png resize 48 48
npx sharp-cli --input icons/icon-96.svg --output icons/icon-96.png resize 96 96
npx sharp-cli --input icons/icon-128.svg --output icons/icon-128.png resize 128 128The manifest references the PNGs:
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"96": "icons/icon-96.png",
"128": "icons/icon-128.png"
}| Size | Used for |
|---|---|
| 16px | Toolbar icon, favicon in extension pages |
| 48px | Extensions management page (chrome://extensions) |
| 96px | Firefox add-ons page |
| 128px | Chrome Web Store listing |
MIT