AnkiWeb
- Rating
- 0 (π 0 Β· π 0)
- Updated
- 2026-08-19
- Anki versions
- 25.09.4~
- Description language
- en
AnkiWeb addon 637051768
Select a word, phrase, or sentence while reviewing to see a popup with translation, example sentences, and speech, with cached results and multiple providers.
Open on AnkiWeb GitHub Ask about alternatives
active
| Min Anki | Max Anki | Updated |
|---|---|---|
| 25.02 | 25.09.4+ | 2026-08-19 |
Loadingβ¦
Provides instant popup translation on text highlight and double-click dictionary lookups in Anki, supporting nine languages, audio pronunciation, contextual details, and Ctrl-click inline field editing.
Provides instant English dictionary popups in Anki with Portuguese (BR) translation, definitions, audio, phonetics, synonyms, offline cache, dark mode, and previewer support.
Pronounces highlighted words or phrases in Anki using Merriam-Webster and Google TTS across 27 languages, with customizable hotkeys, button options, and phrase playback modes.
Pronounces selected text or a fallback field via Alt+C using Azure/Edge neural voices, automatic language detection, and local audio caching.
Lets you right-click selected card text to instantly search Google Images, Reverso, WordReference, Google Translate, Wikipedia, Wiktionary, and Cambridge Dictionary in your browser.
Generates complete Anki flashcards from any word with native audio, example sentences, translations, and related words, syncing review metrics; supports several languages, with setup via API key.
An Anki Desktop add-on for Windows 11. Select a word, phrase, or sentence while reviewing and a compact popup appears next to it, already showing the translation β plus real example sentences so you can see the word in use.
The header carries three icon buttons (translate, speaker, clipboard) and a close button. All have tooltips and ARIA labels.
By default the lookup runs automatically on selection, which means the
selected text is transmitted as soon as you select it. auto_translate and
auto_pronounce turn that off if you would rather click.
The entire interface is in English.
Developed and verified against:
| Component | Version |
|---|---|
| Anki | 25.09.4 (int_version 250904) |
| Qt / PyQt | Qt 6.8, PyQt6, QtWebEngine 6.8 |
| Python | 3.13.5 (bundled with Anki) |
requests |
2.32.4 (bundled with Anki β no extra dependency added) |
manifest.json declares min_point_version: 250200 (Anki 25.02). Only 25.09.4
was actually tested; the floor is a conservative estimate, not a verified claim.
Every Anki API used is a documented, currently supported one β no deprecated hooks:
| API | Purpose |
|---|---|
gui_hooks.webview_will_set_content |
Inject CSS/JS into the reviewer and previewer |
gui_hooks.webview_did_receive_js_message |
Receive pycmd() calls |
gui_hooks.state_shortcuts_will_change / focus_did_change |
Keep speech keys alive when reviewer focus moves to Qt |
AddonManager.setWebExports |
Serve web/ under /_addons/ |
AddonManager.getConfig / setConfigUpdatedAction |
Configuration |
AddonManager.get_logger |
Add-on-scoped logging |
aqt.operations.QueryOp |
Run network calls off the UI thread |
Version-specific behaviour that was verified empirically, not assumed:
speechSynthesis exists in Anki's QtWebEngine build (it is not enabled in
every Qt build). Confirmed: typeof window.speechSynthesis === "object".getVoices() returned [] for roughly 3β4
seconds after page load. The add-on warms the list at startup and waits for
voiceschanged.speak() needs a transient user gesture. A real click works; a synthetic
one fails with not-allowed. Warming the voice list keeps the click-to-speak
path inside Chromium's activation window.Reviewer._shortcutKeys()
contains no Escape entry, and there is no Key_Escape handler in aqt/main.py),
so the popup can close on Escape without stealing an Anki shortcut._showQuestion() in JS, so the injected script persists across cards and must
survive them. It does.anki_translate_popup.ankiaddon.anki_translate_popup.ankiaddon.Copy or symlink the anki_translate_popup folder into your add-ons directory:
%APPDATA%\Anki2\addons21\anki_translate_popup
A symlink lets you edit in place (run as Administrator, or with Developer Mode on):
New-Item -ItemType SymbolicLink `
-Path "$env:APPDATA\Anki2\addons21\anki_translate_popup" `
-Target "C:\path\to\anki_translate_popup"
Restart Anki. Confirm it loaded via Tools β Add-ons β it appears as Translate & Pronounce Popup.
Start reviewing any card, select some text with the mouse, and the popup should appear. Translate and Pronounce both work immediately β the default provider needs no API key. See the Terms-of-Service caveat under Configuration.
Tools β Add-ons β Translate & Pronounce Popup β Config.
Full per-option documentation is in config.md, shown next to the editor in
Anki's config dialog. Changes take effect immediately β no restart.
Out of the box translation_provider is "google_unofficial", which needs no
API key and no signup. German β English works the moment you install it.
The trade-off, stated plainly: that endpoint is undocumented and unsupported. Google can rate-limit or break it without notice, and using it may breach their Terms of Service. Translation quality for German is also a little below DeepL. If either matters to you, switch provider below.
:fx)."api_key": "your-key-here:fx" and "translation_provider": "deepl".The free and paid DeepL hosts differ; the add-on picks the right one from the
:fx suffix automatically.
fallback_provider retries with a second backend when the first fails β no key,
quota gone, rate-limited, offline, or the unofficial endpoint broken:
"translation_provider": "deepl",
"fallback_provider": "google_unofficial"
DeepL quality while your quota lasts, Google keeping it working when it doesn't. Reverse the two for free-by-default with DeepL as insurance.
When the fallback answers, the popup shows via Google (fallback) β you are
always told which service received your text, and each provider caches
separately so results never cross over. Disabled by default ("").
Run LibreTranslate locally, then:
{
"translation_provider": "libretranslate",
"libretranslate_endpoint": "http://localhost:5000"
}
No text leaves your machine.
With the default auto_translate: true, selected text is transmitted as soon
as you select it. auto_pronounce fetches online audio: with the default
tts_provider: google_unofficial, nothing is spoken offline, because a
card cannot use a system voice at all and a split between the two produced two
different voices for the same word. Turn both off for click-to-act behavior. Copy is always
local. DeepL and LibreTranslate probe their language-list API when the reviewer
opens; this sends no card text, though DeepL authenticates the probe with your
API key.
One case sends text you never selected: with both source_language and the
side's voice language on auto, card auto-pronounce asks the provider to
identify the card side, because nothing in the configuration names its
language. A 200-character sample goes, the answer is cached so a card costs at
most one detection, and naming a real source_language β or turning off
auto_pronounce_card β stops it entirely.
| Provider | Where text goes |
|---|---|
google_unofficial (default) |
translate.googleapis.com. Undocumented endpoint, no service agreement, no stated retention policy. May breach Google's Terms of Service. Set enable_google_unofficial: false to block it entirely. |
deepl |
api.deepl.com / api-free.deepl.com over HTTPS. DeepL states API text is not used for training and is deleted after translation. |
libretranslate |
Whichever endpoint you configure. Self-host for full privacy. |
Your API key is stored by Anki in meta.json in the add-on folder, in plain
text. It is never logged and never sent to the reviewer page β verified by test.
anki_translate_popup/
βββ __init__.py Anki hooks, bridge, threading, wiring
βββ config.py Typed + validated view over Anki's config dict
βββ config.json Defaults
βββ config.md User-facing option documentation
βββ manifest.json Add-on metadata
βββ cache.py SQLite translation/example cache with TTL
βββ tts.py Online speech, for languages with no usable voice
βββ examples.py Usage examples from the Tatoeba corpus
βββ translation/
β βββ base.py Translator ABC, error hierarchy, shared HTTP
β βββ deepl.py DeepL API v2
β βββ libretranslate.py
β βββ google_unofficial.py Opt-in only, isolated
βββ web/
β βββ reviewer.js Selection, popup, speech, clipboard
β βββ reviewer.css Popup styling, light + dark
βββ tests/ Unit tests with all network calls stubbed
user selects text
β (nothing is sent anywhere)
βΌ
reviewer.js shows the popup
β
β user presses Translate
βΌ
pycmd("anki_translate_popup:translate:{id,text}")
β
βΌ
webview_did_receive_js_message βββ main thread, returns immediately
β
βΌ
QueryOp(...).without_collection().run_in_background()
β
β ββ worker thread ββββββββββββββββββββββββββ
β cache lookup β provider HTTP call β cache store
β βββββββββββββββββββββββββββββββββββββββββ
βΌ
success/failure callback (main thread)
β
βΌ
web.eval("...onTranslationResponse({...})")
β
βΌ
popup renders result or error via textContent
Why one implementation covers reviewer and previewer. The browser's
previewer renders a card with the same Reviewer.revHtml() the reviewer uses,
so the injected JS and CSS work unchanged; the two only differ in which
attribute holds the webview (Reviewer.web vs Previewer._web), which
_webview_for() resolves. The answer-button bar is excluded because it holds
no card text, and the card-layout and note editors because they are text-editing
surfaces where a selection popup would fight with typing.
Why the UI thread never blocks. Translation, Tatoeba, TTS, provider
language-capability probes, and SQLite work happen inside QueryOp.op, which
Anki runs on a worker thread. without_collection() keeps them from being
serialised behind collection operations. Bridge handlers return immediately.
Why injection is safe. reviewer.js writes every external value β
the selection, the translation, error strings β with textContent, never
innerHTML. The only innerHTML assignment is a constant skeleton with no
interpolation. On the Python side, _js_json() uses ensure_ascii=True (which
escapes umlauts and the U+2028/U+2029 separators that would otherwise terminate
a JS string literal) and rewrites </ as <\/ so a value containing
</script> cannot close the injected tag early. Both are covered by tests.
Why the CSS is full of !important. The popup lives in the same document as
the card, so the card template's CSS applies to it. Templates routinely use
broad selectors with !important (div { color: hotpink !important }), which
beat an add-on rule at any specificity. The stylesheet re-states every inherited
typography and colour property so the popup looks identical on every deck.
Why the selection is read from the DOM, not Selection.toString().
toString() returns the rendered text: on a card styled with
text-transform: uppercase it hands back GROSS for groΓ and GRΓSSE for
GrΓΌΓe β destroying exactly the German characters this add-on exists to handle.
The add-on clones the range instead, strips <style>/<script> (Anki puts the
card's stylesheet inside #qa, and textContent would otherwise capture the
entire CSS and post it to a paid API), re-inserts line breaks at block
boundaries, and collapses whitespace.
Why the cache opens a connection per call. The cache is touched from Anki's
worker threads, and a sqlite3 connection cannot be shared across threads.
Per-call connections avoid a lock, and the cost is irrelevant for a
user-triggered action. Note that sqlite3.Connection used as a context manager
commits the transaction but does not close the connection β cache.py wraps
it so the handle is always released.
Adding a provider. Subclass Translator in translation/, implement
translate() / validate() and optionally supported_languages(), then
register it in PROVIDERS and build_translator(). The popup, cache, bridge
and threading need no changes.
Adding a TTS backend. Subclass TextToSpeech in tts.py and implement
synthesize(text, lang) -> bytes; the caching, playback and error plumbing in
_synthesize_blocking are backend-agnostic. Audio plays through Anki's own
av_player, which uses the bundled mpv, so any format mpv understands works.
The browser path is separate: pronounce() in reviewer.js is the only place
that touches speechSynthesis, with loadVoices() and pickVoice() split out
as testable functions.
314 tests, no network access and no paid API calls β every HTTP call is stubbed.
Run from the directory that contains anki_translate_popup:
& "$env:LOCALAPPDATA\AnkiProgramFiles\.venv\Scripts\python.exe" `
-m unittest discover -s anki_translate_popup/tests -t .
Any Python 3.9+ with requests installed also works:
python -m unittest discover -s anki_translate_popup/tests -t .
Coverage:
| File | Covers |
|---|---|
test_config.py |
Defaults, deck pairs, provider-filtered pickers, language-code normalisation, auto, type/range errors, API key never reaching the webview |
test_cache.py |
Translation/example read/write and key scoping, Unicode, TTL and row limits, corrupt-database resilience, connection-close regression |
test_translation.py |
Provider parsing/capabilities, DeepL Chinese aliases, auto-detection, malformed responses, HTTP status/network failures, Unicode, provider gating, JS escaping |
test_examples.py |
ISO 639-1β3 mapping, spaced/Chinese phrase gating, limits, unsupported languages, HTTP/malformed responses, Unicode and safe markup handling |
test_tts.py |
Word-boundary chunking, hard-splitting overlong words, multi-segment joining, MP3/ID3 sniffing, HTML-error-page rejection, empty body, rate limit, HTTP errors, timeouts, connection failure, Unicode |
test_fallback.py |
Fallback on network/quota/missing-key failures, no fallback when the primary succeeds, both-failed message naming both causes, fallback results cached under the fallback provider, no cache leakage between providers, fallback logged not silent, config validation |
Set up a deck with German cards, then work through these.
Haus) β popup appears next to itdas groΓe Haus) β full phrase shownauto β the detected language becomes the target, never autode β en, deck B to es β en, then switch between them β each pair returnsenable_in_previewer: false β popup no longer appears in the previewer, still works in the reviewerx β the front is spoken, with nothing selectedc β silence; the answer is not out yetc β the back is spoken, without repeating the frontx then c quickly β the second interrupts the first, they do not queuec twice β it speaks twice; the auto-pronounce dedupe does not swallow itx still speaks the prompt, c still speaks what you were recallingx speaks English, c speaks German β not the pair's way roundfront_speech_language β that side is spoken in it with no identification requestpronounce_answer_shortcut: "" β c does nothing, x still worksde β en pair, press x then c β German voice for the front, English voice for the backx β the voice swaps with itspeech_language: de-AT with a de β en pair β the front keeps the Austrian voiceGen. on an English back β spoken as "gen", not "Genitiv"z β audio stops at oncez, press x β speaks again; z mutes nothing permanentlyz mid-clip β that clip stops toox β still speaksx β still speaks, with the edited textx β still speakstts_provider: system β click the card before x; no off-focus fallback sends text onlinee still opens the editor, u still undoespopup_font_size β takes effect without restartingshow_examples: false β no examples section, no Tatoeba requestauto_translate: false β nothing is sent until the translate icon is pressedauto_pronounce: false β no audio until the speaker is presseddeepl with a valid key β translation appearsdeepl with no API key β clear message naming api_key, nothing sentenable_google_unofficial: false while Google is selected β clear message, nothing sentfallback_provider and break the primary (bad key / airplane mode) β translation still appears, labelled via β¦ (fallback)fallback_provider to the same value as translation_provider β config error explaining they must differrequest_timeout_seconds: 1 against a slow endpoint β timeout message names the valuesource_language: "auto" β detected language is displayed and examples still appearζΏε from zh β translation and a short Tatoeba lookup workzh / zh-TW β simplified / traditional translation succeeds<b>, &, <script> β shown literally as text, nothing executes, no layout break"request_timeout_seconds": "ten") β pressing Translate explains the problem[sound:] audio β Anki's clip plays first, the spoken text follows, neither is cut offauto pair, let the card auto-pronounce, then press x β the same language, not the speech_language fallbackder Aspekt, -e) with card_speech_scope: first-line β identified from the whole side, spoken as Germanspeech_languagesource_language: "auto" on an English deck β the card is spoken in English, not the speech_language Germandie Aktie, -n under an auto pair β spoken as German, not as Englishcard_speech_scope: full β each line in its own voicedebug_logging: true β the log names each line and the language chosen for itdebug_logging: true)source_language: "auto" with no network β the card still speaks, using speech_languagefront_speech_language to a language β no detection happens at alltts_provider: "system" β card auto-pronounce stops (needs a user gesture the browser will not grant)tts_provider: "google_unofficial") β every pronunciation says Spoken by Google (online voice), selections and cards alikeenable_google_unofficial: false β no new audio is fetched, with a message naming the switchuser_files/tts/ still playsuser_files/tts/)[sound:] is playing β the clip stops and the side is spoken, not both at once[sound:] plays β the clip keeps going; the popup only cancels what it startedspeech_language: "de-AT" with a DE β EN pair β still the Austrian voice, not a bare de oneauto_translate: false β nothing is sent merely to detect a language; speech uses speech_languagepreferred_voice to an installed voice β it wins over the gender preferencetts_provider: "system" with no German voice β clear message, nothing sent anywheretts_provider: "system", turn off Wi-Fi β still works if a voice existstts_provider: "google_unofficial", turn off Wi-Fi β clear could not reach messagespeech_rate to 0.5 β noticeably slowerpreferred_voice to an installed voice name β that voice is usedpreferred_voice to a nonsense name β falls back to speech_languagecache_enabled: false β every translation hits the networkuser_files/cache.sqlite β cache rebuilds without errorMicrosoftWindows.Voice.de-DE.Katja
and friends) are reserved for Narrator and are never registered as voice
tokens. You can install German speech, see it listed in Settings, and still
have no German voice available here. This is a Windows limitation, not an
add-on bug β and it is one reason tts_provider defaults to
google_unofficial rather than to a system voice that may not exist. Use the
Add-WindowsCapability command in config.md to install a classic voice if
you want system or auto to be worth choosing.tts_provider, so the caveats are no
longer a fallback's caveats: undocumented endpoint, no service agreement,
may break or
rate-limit. Audio is cached in user_files/tts/; tts_cache_max_mb limits
its size.deepl or a self-hosted
libretranslate for a supported API, or set enable_google_unofficial: false to block it. Verified working against the live endpoint at build time
(~0.2β0.6 s per request), but that is not a guarantee it will keep working.meta.json, which is how
Anki's configuration system works. build_ankiaddon.py deliberately excludes
meta.json from the package so a key cannot be shipped by accident.enable_in_previewer.lookup_shortcut
re-opens the popup for a selection you already made; it cannot create one.QTextToSpeech was probed on
both its Windows engines and reports exactly the same voices the webview
already sees β sapi gives ['en_US'] (David, Zira) and winrt gives
['en_US'] (David, Zira, Mark). Neither exposes a German voice, so routing
speech through Qt would not reach the Narrator natural voices either. Only
installing a classic voice, or the online provider, actually helps.tts_provider: system needs the keypress inside Chromium. Click the card to
restore focus; the fallback never weakens the no-network promise.auto pair, x and c use a detection the card's own auto-pronounce
already paid for. With auto_pronounce_card off there is nothing in the
cache and they fall back to speech_language, because they are handled on
the UI thread where a network call would freeze the reviewer. Pin
front_speech_language / back_speech_language, or name a real
source_language, if you want them exact without card auto-pronounce.auto_translate, auto_pronounce and show_examples
each turn their own request off.From the directory containing anki_translate_popup:
& "$env:LOCALAPPDATA\AnkiProgramFiles\.venv\Scripts\python.exe" build_ankiaddon.py
This writes anki_translate_popup.ankiaddon β a zip with manifest.json at the
archive root (not inside a subfolder), which is what Anki requires.
Deliberately excluded:
| Excluded | Why |
|---|---|
meta.json |
Holds the user's saved config including the API key |
user_files/ |
Local translation, example, and speech caches |
__pycache__/, *.pyc |
Build artefacts |
To do it by hand instead: select the contents of the anki_translate_popup
folder (not the folder itself), send to a zip archive, and rename it to
.ankiaddon.
Install with Tools β Add-ons β Install from fileβ¦
To publish on AnkiWeb, upload the .ankiaddon at
https://ankiweb.net/shared/addons/ β the package field in manifest.json
must stay stable across versions, and human_version should be bumped.
python release.py does the whole sequence β tests, version bump, build,
commit, tag, push, GitHub release with the package attached:
python release.py # 1.0.0 -> 1.0.1
python release.py 1.1.0 # or say which version
Any python on your PATH will do, from any directory. The suite needs Anki's
own aqt, which a system Python does not have, so the script locates Anki's
bundled interpreter itself rather than making you type its path.
It refuses to start unless the tree is clean, the branch is main, the version
is newer than the released one, and no commit since the last tag carries a
co-author trailer. Tests run before the bump, so a failure leaves the tree
untouched.
AnkiWeb is not automated β it has no API. The script ends by printing which file to upload to which listing. Re-upload on the existing listing rather than creating a second one, or nobody receives the update.
AGPL-3.0-or-later β the same licence as Anki itself, which this add-on imports
at runtime. The full text is in LICENSE, and a copy ships inside every
.ankiaddon build.