engineering
Three languages, one content model
Pulse has been publishing across English, Indonesian, and Japanese since March 2026. Almost none of the work that made that possible was translation work. It was deciding, once, what a field means when it has no value in the locale you asked for, and then making sure everyone downstream could see the answer.
Fallback is per field, and it is per field on purpose
Atlas stores translations per field rather than per document. Fetching an entry for a locale merges that locale's translations over the base values field by field, and pages do the same thing with their SEO and their block data. A page where the headline is translated and the body is not renders with a Japanese headline over English body copy.
The alternative is per-document fallback: a page is either fully translated or it is not there. It sounds tidier and it is worse. Translation arrives unevenly, always, and per-document means that at 40 percent coverage 60 percent of your Japanese routes are 404s. Navigation breaks, the sitemap disagrees with the site, and internal links go nowhere. Per-field fallback means the route always exists and the content improves in place.
The place that position is wrong is legal text. A privacy policy that is half Japanese and half English is worse than one that is only English, because a reader who understands the first three sections reasonably assumes they have read the document. For those pages the correct behaviour is to serve the base locale with an explicit notice, or not to route the locale at all. That is a per-content-type decision, and it is the one worth making before the second locale rather than after the third.
Fallback nobody can see is a silent failure
Fallback that renders is the easy half. The half that decides whether a site is actually trilingual is whether the fallback is visible.
It has to be visible in the editor. A translation form that prefills every empty field with the base value looks like a finished translation. A translator opens the Japanese version of a page, sees English in all fourteen boxes, and cannot tell which of them are untranslated and which are English on purpose, because product names, code identifiers, and a few brand phrases legitimately stay in English. Showing the base value as a dimmed reference beside an empty field, marked as not translated, costs a small amount of interface and removes the entire class of confusion.
It has to be visible in the build. Fallback coverage per locale is a number: how many fields on how many pages are still serving base-locale values. Print it at the end of the build. Once it is a number, somebody owns it, and "we are at 62 percent Japanese" replaces "the Japanese site is mostly done". Without that number, the failure mode is not a broken page. It is a site that looks complete in three languages and ships half of one of them in the wrong language, and nobody notices because everyone who checks reads English.
What the route shape multiplies out to
Routes are /[locale]/..., with the locale list in one module and every path built through a localizePath helper so a hand-written link cannot skip the prefix. The layout's generateStaticParams returns one entry per locale. Each dynamic child route returns the product: loop the locales, list the entries for each, push a locale and slug pair per item.
That product is the number to look at before adding a locale. A site with 160 pages of source content is a site with roughly 500 prerendered routes at three locales, and roughly 500 sets of content fetches to produce them. Adding a language is not a translation cost plus a small integration cost. It multiplies the build.
It also settles a question that comes up every time: should generateStaticParams emit a route for a slug with no translation? Yes, because per-field fallback already decided that. The route exists in all three locales, and the page renders whatever mix of translated and base content it has. Emitting only fully-translated routes would put the coverage gap into the URL space, where it becomes 404s, broken navigation, and a different site structure per language.
hreflang when the translation is not real
Our SEO helper emits one alternate per locale plus an x-default pointing at the base locale, generated from the same locale list the routes come from, so the two cannot drift. That is correct for a translated page and dishonest for a fallback one. Telling a search engine that /ja/some-page/ is the Japanese alternate of a page, when it is English served under a Japanese prefix, produces exactly the result you would expect: a Japanese-language search result that turns out to be in English.
Two ways to be honest about it. Point the fallback page's canonical at the base-locale URL, which removes it from the index entirely. Or keep the self-canonical and drop the ja alternate for that page, which says the URL exists but is not offered as a translation. We prefer the second. Canonicalising away is the stronger signal and the more brittle one, because the day a real translation lands, someone has to remember to flip it back, and nothing fails if they do not.
Either approach needs the build to know, per page and per locale, whether the content is a real translation or a fallback. That is the same coverage data as the editor warning and the build report. One signal, three consumers. If you build the fallback mechanism without exposing that flag, you will build it again.
What actually breaks in Japanese
Everything above is language-neutral. Japanese is where the layout assumptions get audited.
Line breaking
Japanese has no spaces, so the browser may break a line almost anywhere. Word-break rules tuned for English do the wrong thing here, and break-all in particular will split at positions that read as errors. Use line-break: strict so the browser applies kinsoku rules, which keep a line from starting with a closing bracket or a small kana and from ending with an opening bracket. Anything you did by hand to control English wrapping is now noise: a manual br at an English phrase boundary, or a balanced-wrap heading tuned to a specific English string, has no counterpart in the Japanese text and will land somewhere arbitrary.
Length, in both directions
Japanese usually needs fewer characters than English for the same meaning, and each of those characters is full width, so a twelve-character Japanese label can be physically wider than a twenty-character English one. Any width derived from an English string is now a guess. The three locales we run pull in opposite directions, which is useful: Indonesian runs long, where Contact becomes Hubungi Kami and Custom Software Development becomes Pengembangan Perangkat Lunak Kustom, and a navigation bar sized to the English labels wraps to two lines. Size on content with a maximum, and check the longest string per locale rather than the average.
Fonts
A subset Latin webfont is tens of kilobytes. A Japanese face covering the common kanji plus kana is measured in megabytes. There are two real options: serve it split across many unicode-range slices so the browser downloads only the ranges a page uses, or use the system stack and accept that the Japanese site does not look identical to the English one.
For a content site, use the system stack for Japanese body text. The platform faces are good, they are already on the device, and a multi-megabyte font download on a mobile connection costs more than the inconsistency does. Where that is wrong: a brand site whose identity lives in its display type. Subset the display face to the exact glyphs the headings use, which for a fixed set of headings is a few hundred characters rather than thousands, and leave body text on the system stack.
Two smaller things that are easy to get wrong. Do not set a light weight on CJK body text: many faces have no light weight, the browser synthesises one, and synthesised light strokes on dense kanji at body size are hard to read. And set the lang attribute correctly on the root element, which our locale layout does from the route param. Han unification means a single codepoint has different regional glyph shapes, and a page without a correct language tag can render Chinese-preferred forms of characters in Japanese text.
Machine translation as a first draft
Machine translation is a reasonable first draft for body copy that a fluent human then edits, and it is genuinely useful for reading incoming content you do not speak. Used that way it removes the blank page, which is most of the cost of a long article.
It is not acceptable, unedited, for legal and contractual text, pricing, compliance or safety claims, error messages that instruct someone to take an action, or destructive-action confirmations where a wrong verb changes what the button appears to do. The reason is specific: the failure mode is not gibberish, which anyone can spot. It is fluent and wrong, and a reviewer who does not read the language cannot detect it at all. Machine output with no term glossary will also translate the same product name three different ways across three pages, which reads worse than leaving it in English.
One workflow consequence. If a field can hold a machine draft, that has to be a state on the field, alongside who approved it and which version of the source it came from. Otherwise the English gets edited, both the draft and the human translation go stale, and there is no way to find out which fields need review other than reading all of them. That is the same per-field metadata as the fallback flag, filled in by a different process.
The rule we would give anyone adding a second language: write down the fallback policy per content type before the second locale ships, not per page after the third. Marketing pages fall back. Legal pages do not. Product names and error strings get a glossary. Those three decisions take an afternoon at the start and are a hundred-page audit at the end.