Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hover Translate

A Chrome extension that translates whatever is under your pointer, text or image. Point at it, tap Ctrl, and it is translated in place. Tap it again to get the original back, Esc restores the whole page.

Hover Translate replacing a paragraph in place, then reading an image

Replace, bilingual, bubble, then an image read and translated.

No account, no API key, no paid service. Three free providers, tried in the order you choose:

Provider Notes
Tencent TranSmart A mainland China provider, and the strongest of the three on Chinese
Google Batches natively, fastest, widest language coverage
MyMemory Last resort: needs to be told the source language and caps each request at 500 characters

If one fails the next is tried, so a single outage is not a dead extension. The popup shows which provider served the last block and how long it took, text and images counted separately.

Images

Words in an image are pixels, so they have to be read before they can be translated. Pointing at an image and tapping Ctrl reads it and lays the translation over it. The page's own DOM is never touched: the result goes in a closed shadow root anchored above the image, so the <img> keeps its original src and nothing is inserted beside it.

A Chinese product banner being read and translated in place

This is off until you turn it on, because reading an image the page did not serve itself is a cross-origin fetch that needs a host permission. Setting Translate images to Yes in the popup asks for that permission there and then; declining leaves the feature off rather than switching it on into something that cannot work. Setting it back to No hands the permission back.

Recogniser Notes
Youdao Reads Chinese best by a distance, and translates as it reads. Returns a finished picture rather than boxes, so it replaces the image instead of annotating it
Google Lens Any script. Returns each line with a box, which the usual providers above then translate
Yandex Any script, and the only one that reports the text and background colours it found

Youdao's endpoint ignores the target language: Chinese always comes back English, and everything else always comes back Chinese. So it declines outright unless the target is English or Chinese, and the language it actually answered in is checked before the result is used. When it does not match, the failover moves on to Lens.

Install

  1. Open chrome://extensions and turn on Developer mode.
  2. Load unpacked, and pick this folder.
  3. Open the popup to choose your target language, display mode and provider order.

Use

Action Result
Hover text, tap Ctrl Translates the block under the pointer
Hover an image, tap Ctrl Reads the image and lays the translation over it
Hold Ctrl Translates nothing, and outlines the block that would be, if you asked for that
Tap Ctrl on a translated block or image Restores it
Esc Restores every block and image on the page

Nothing happens until you let go, so holding the key is how you aim and releasing it is how you commit; a press you think better of costs nothing. Shortcuts are unaffected: pressing any second key, clicking or scrolling while the trigger key is down cancels the press, so Ctrl+C, Ctrl+click and Ctrl+scroll all behave normally.

"Outline what would be translated" in the popup shows you what you are aiming at while the key is down. It is off by default, since the key is held for a moment before every translation and most of the time you already know what you are pointing at; turn it on while you are learning what a tap takes. It is the same block the trigger itself resolves, not a guess at it, so no outline means nothing would happen there. It is an outline rather than a border because an outline is painted outside the box and takes no space at all: the element keeps its size, nothing reflows, and no word moves under the pointer while you are aiming at it. On a double-tap trigger only the second press outlines anything, since the first one does nothing.

Display modes

Each one is shown in the demo above, in order, before the image at the end.

Mode What it does
Replace Rewrites the text where it stands. The default, and the only one that touches no nodes at all
Both Keeps the original and adds the translation after it, as a single text node
Bubble Leaves the page completely alone and shows the translation in an overlay, dismissed by moving the mouse

Bilingual mode appends the translated text and nothing else, in a single <span>, so the original is left exactly as it was.

Lines the page cut short

A page sizes its text boxes for the language it shipped with, and the same sentence is usually longer in another one, so part of the translation ends up where the page never meant to paint. It happens two ways. Sideways: one nowrap line with its tail behind an ellipsis, which is what a sidebar menu does. Downwards: the text wraps as it should and the box was simply given the height its own language needed, so the second line exists, is laid out, and is painted nowhere, which is what a product card does to a title. Neither cut is necessarily on the element holding the words, since a card sizes the row rather than the title inside it, so the boxes above the text are checked too.

There are only two honest answers, and Scroll clipped lines in the popup picks between them.

Setting What happens to text that no longer fits
Enable The default. It scrolls, unless the page left room around the box, which it takes instead
Disable The box always grows

There was a third setting that always scrolled and never took the room. It is gone, because next to nothing told the two apart: a menu row is built around its single line and has none to give, so it scrolls either way. They only parted company where a page left slack, and taking slack costs the page nothing. Anything still stored under the old name reads as the default.

A lap carries the line from off the right edge of the box to off the left one, at one steady 50px/s, for as long as you are pointing at the row. Because both ends of a lap are off screen the wrap has no seam, and because it never stops there is nothing for a reader to wait out. It holds still for a second first, so the beginning can be read where you were already looking, and it keeps the page's own ellipsis until it actually sets off.

Text cut downwards moves the same way, once the shape of the window says how. A window one line tall is a line, whichever way the page cut it, so the wrap comes out and it reads across exactly like the one above: the box was showing a single line before and it shows a single line now, so nothing about its height changes. A window several lines tall is a paragraph cut short instead, and undoing the wrap there would leave one line rattling around in a box built for three, so it rises through the window and comes back from below. That one is timed a line at a time, roughly one every two seconds, because reading down a paragraph is waiting for the next line rather than following words across.

Sliding is a transform on a wrapper span, the one node this extension adds to a page, and only inside a box whose text it has already replaced. That is what makes it smooth: text-indent is a layout property, so animating it relaid the line out on the main thread every frame, and anything else busy there left the text standing still and then arriving late in one jump. A transform on its own layer belongs to the compositor and page script cannot stall it. The wrapper is stripped when the block is reverted, along with the animation.

It runs while the pointer is anywhere in the block rather than only on the box itself; a menu row is 130px wide and 24px tall, so anything smaller stops every time a hand drifts, and leaving is given 300ms of grace before the line is handed back. That is also why the movement is driven from the content script rather than by a :hover rule: a CSS animation starts over every time its selector matches again. Under prefers-reduced-motion nothing moves.

Growing wraps the line inside the width the page gave it, so the column stays where it is and only the height changes.

Only if no space means room the page already had, not room it can be made to give. A sidebar row is as tall as the one line in it, so a second line pushes every row below it down and the column reflows under the reader; that is not free, so those lines slide. A box with slack, a row taller than its text, grows into it. What that costs is measured rather than guessed: the box has to end up showing the whole line, the block it sits in has to end exactly where it ended before, and nothing above it may clip the new lines away. When any of that fails the line slides instead.

While a request is in flight a blue gradient sweeps along the bottom edge of the block. It is painted as a background, not a border, an outline or an appended element, so it adds no height, shifts nothing and inserts no node. Once the text is replaced nothing is left behind.

An image cannot use that trick, since a painted background sits behind the image's own pixels and would never be seen. It gets the same sweep drawn on top instead, full width across the middle of the image, on a track with a pale hairline so it stays legible over a light picture and a dark one alike.

Deciding what is already in the target language

  • Every result is judged, not just the first. When a block's Chinese sits inside styled spans, the string carrying the sentence can be almost entirely brand names, and a provider reads that one as English. Judging the block on it alone made whole paragraphs vanish into "already in English". The majority of the detected languages now decides.
  • The writing system overrules the detector. Han characters mean the text is not English whatever a detector claims, so a block is never skipped when it contains script the target language does not use. Latin is ignored as evidence, since brand names, model numbers and dates appear in every language. See src/lib/script.js.

Tests

The one that matters most installs the extension in a real headless Chrome and drives it with real mouse and keyboard events. It serves the folder, launches its own throwaway profile and cleans up:

node test/extension.test.mjs

Every provider against its live endpoint:

node test/remote.test.mjs

Package sanity, no browser needed:

node test/package.test.mjs

Two browser tests need the folder served over http, because ES module imports do not work from file://:

python -m http.server 8731
  • http://localhost:8731/test/harness.html runs block.js, richtext.js, marquee.js and script.js against the fixture DOM.
  • http://localhost:8731/test/e2e.html runs the whole content script over the fixture, translating through live endpoints, with only the chrome.* APIs stubbed.
  • test/fixture.html is the manual page: open it with the extension loaded and hover through it.

Limits

  • These are undocumented endpoints. They can rate-limit or change without notice, which is exactly why there are three of them and why failures surface in the popup rather than passing silently.
  • The Lens recogniser is the least documented of the lot: its request shape was derived from the server's own error messages rather than a spec, so it is the first thing that will break if Google renumbers those fields. The failover means that degrades to Yandex rather than to nothing.
  • A block over 5000 characters is refused, so a stray hover cannot rewrite half a page.
  • An image over 12 MB is refused.
  • Reloading or updating the extension leaves the copy already injected into open tabs with nothing behind it, and only a page load gets a fresh one. Rather than repeating Chrome's "Extension context invalidated" on every trigger, it says so once and asks you to reload the page. Undo still works, since restoring a block touches nothing but the DOM.

About

Point at any text, tap Ctrl, read it in your language. A Chrome extension that translates in place without touching the page's DOM. Free, no account, no API key.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages