Translator.create() requires transient user activation whenever the language pack still has to download, which is every fresh profile. Call it from a speech callback and it rejects with NotAllowedError, on the machines where it matters most: the ones that have never run it before. The warm-up has to happen synchronously inside a click.
Why it works on your machine
Once the pack is on disk, create resolves without activation and the whole thing looks fine. So the bug is invisible to whoever built it, on the second run onwards, and appears only for people opening the feature for the first time. That is the worst possible distribution: it is broken exactly for new users and working for everyone testing it.
What transient activation actually allows
It is a short window opened by a real user gesture, and it is spent by the first await. That is the part that catches people. A click handler that awaits getUserMedia and then calls create has already lost the activation by the time it gets there, even though the code is still inside the handler and reads as if it is not.
// Inside the click handler. No await before Translator.create, because the
// first await spends the activation and the call then rejects.
function onStart() {
// Wrong: await getUserMedia() here and the activation is gone.
const warm = Translator.create({ sourceLanguage: 'es', targetLanguage: 'en' });
warm.then((t) => (translator = t)).catch(() => (translator = null));
// Everything that can wait, waits after the create call is already in flight.
void startCaptureAndJoin();
}So the ordering rule is narrow: start create before the first await, and let it settle in the background. A speech recognition callback is far too late; by then the gesture is minutes gone.
Design the feature around the gesture
This is not a workaround to bolt on, it constrains the interface. There has to be a moment where the person deliberately turns translation on, and that moment has to be the one that warms the pack. If translation is meant to start silently when speech is detected, there is no gesture to spend and the feature cannot work on a first visit.
I prefer this path anyway when it is available, because it costs nothing and nothing leaves the machine. Chrome desktop can do speech recognition locally and translate locally. Everything else falls back to a hosted service, which works and is the fallback rather than the default.
How to test it
- Test on a fresh profile every time. A warm profile cannot reproduce the failure at all.
- Emulate the rule rather than trusting it: a stub that rejects with NotAllowedError unless a gesture happened in the last few seconds catches the regression in CI, without downloading anything.
- Assert the failure path too. If create rejects, captions must still appear untranslated rather than disappearing, because a missing caption reads as a broken microphone.