Building a help centre in 11 languages

Company

Kool's app is offered in eleven languages, one of them right-to-left. A help centre in only one of them is not a help centre, it is a help centre for English speakers and a dead end for everybody else. So the whole thing is translated, automatically, and kept that way.

The machinery is one sentence long: one article, one language, one Workers AI call, stored by path. Everything interesting is in what surrounds it.

Three things can trigger a translation, and they exist because "translate the help centre" is not one event. A cron sweep looks for anything missing or out of date on every tick, which is what keeps the corpus whole without anybody thinking about it. Saving an article in the CMS translates it immediately, because the moment you have just written something is the moment you want to see it land. And there is a button on the Languages page, for when someone wants a specific thing now and does not want to wait for a sweep. Same code underneath, three ways in.

Two rules do the real work.

The first: a machine draft never overwrites wording a person approved. Translation quality from a good model is high enough to publish and not high enough to be final, so the two states have to be distinguishable and the automatic path must always lose to the human one. Without that rule, every improvement a native speaker makes is on a timer, waiting to be silently reverted by the next sweep. Nobody edits a system that does that twice.

The second: a translation records a hash of the English it came from. That is what makes "out of date" a fact rather than a guess. Edit the English, and its translations no longer match the hash they were built from, so they are stale by definition and the sweep redoes them. No timestamps to compare, no manual invalidation to forget. The dependency is content-addressed, so it cannot drift.

One implementation note that cost us time and is worth passing on. The translator reaches the help centre through a service binding rather than an ordinary fetch. A plain fetch to a hostname on your own zone is not dependable from inside a Worker, and the failure is intermittent enough to look like anything else. If you are doing this, bind the service.

The corpus follows the app's own language list rather than keeping a list of its own. That is deliberate: a language added to Kool is a language the help centre owes an answer in, without anybody remembering to ask for it. Arabic is the only right-to-left one so far.

All posts