Product releases

The Packaging Was the Monolingual Part

A person in Madrid could already have a whole working relationship with their assistant in Spanish. Then they opened the pricing page. Localising everything around a conversation that was never the problem turned out to be a study in what source-reading tools cannot see.


10 September 2026·16 min read

Two weeks ago we set out to answer a question that sounds like one thing and is six: what does it cost to make 1Presence available to every corner of the globe? Available bundles at least six independent capabilities. Can the assistant talk to me in my language? Does the interface read in it? Can my script render at all? Does the layout work in my writing direction? Can I find and evaluate the product before I sign up? Can I legally pay, in the way I actually pay? They have wildly different costs, different owners and different reversibility, and the trap is to collapse them into "extract the strings and hire translators", which addresses two of the six and not the hard ones.

The audit that came back reframed the whole exercise. The part of a conventional product that is hardest to internationalise is the conversation, and ours already worked: the language models underneath are multilingual, and the assistant had been inferring a person's language at onboarding and speaking it ever since. Two days after the audit the rule became explicit for every agent: answer in the language of the message, and switch back as readily. What was monolingual was the packaging. The public site was English by an accepted decision, the app's interface had fifteen translated strings out of roughly three thousand, the catalog of Kits had none, and a font stack built for a warm Latin-script brand could not have set a Russian user's own name. That inverts the usual build order. Most products localise the chrome first because the chrome is all they have; here the capability came free and the frame was the work.

One constraint shaped everything that followed, and it came from us rather than from a market: every page in every language had to score the same on page speed as the English one, and the English site was not allowed to pay for any of it. Read strictly, that is only satisfiable if a localised page is the same page carrying different words, and it turned "which languages" into a performance question before it was a market one.

The same page with different words

The public site is prerendered, one static document per page. Localising it meant one static document per page per language, with the English tree keeping its unprefixed addresses and every other language living under a short prefix. The parity constraint then answers itself: a Spanish page references exactly the stylesheets, script chunks, preloads and font files its English original does, and only the text differs. On these pages the largest paint is gated on fonts, so the font set is the whole mechanism, and the font set is decided by one field in the locale registry: which subsets a language's text needs. Spanish, German and French need only the Latin pair the English site already ships, so they download byte-identical font files and land on the same paint. A locale that needs anything outside that pair is the one that has to be measured before it ships. Cyrillic would be nearly free for body text and cost only the display face; Japanese or Arabic is a type commission, not a translation.

We measured rather than assumed. Three interleaved lab runs per locale against the production build put the four scores within a point of each other across English, Spanish and German, the largest paint within two milliseconds, and layout shift at zero everywhere. The one-point spread also occurs within English's own runs. What the lab cannot equalise is field data: the same bytes arrive more slowly in São Paulo than in London, and a page-speed report blends that in.

The English tree's protection is a guard, not care. The build asserts on the produced bytes that no alternate-language link, no hiding directive and no locale reference reaches an English page, and prints the tree's total size every build so a drift shows as a number. The one thing that legitimately touches English pages is the reciprocal hreflang link, which is a head tag and costs no request; the day the second indexable language arrived, the guard's clause for it was already conditional on that.

The first day taught the sharpest lesson of the fortnight, and it is one about which layer a fact belongs to. The pages hydrate after they paint, and a document's language is a property of its address, not of its reader. The lookup that fetched a page's content was keyed on the reader's browser preference while the route it was given was the English one, and the content map deliberately has no fall-through, because an untranslated page must never be served under a translated address. So a browser set to US English, German or Spanish painted the correct English page and then, a few hundred milliseconds later, replaced it with a 404. Every automated check we own inspected the built HTML, which was right; only a browser observing the swap could see it, and a browser set to British English, the one preference that resolves to the base, never did. Content is now addressed by the URL and only the chrome follows the reader, the function that decides is written so the browser preference cannot be reintroduced without changing its signature, and the site's public pages carry a real-browser gate that waits for hydration before it asserts anything.

Offer, never redirect

A reader who lands on the Spanish page from a search result is signed out, so the account menu's picker is not on their screen. A localised page with no way back is a trapdoor, and the obvious fix, redirecting people to the language their browser reports, is wrong twice over. It is wrong for the reader who wanted English. And it is wrong for the crawler: Google's own guidance says its crawler usually originates from the United States and sends no Accept-Language header at all, and asks sites not to redirect on a guessed language, so an automatic switch would leave every non-English variant unindexable. The mechanism we built offers instead. A small banner, injected client-side and fixed so it puts no number on a layout shift measured at zero, offers a translation of the page the reader is on, in that language, only when one exists. A footer picker navigates rather than setting state, because each language is a different file at a different address, and it lists only the languages the page exists in.

Two small decisions in that control turned out to be positions. There are no flags: a flag is a country, not a language, and asking a Mexican reader to click the flag of Spain says the product was built without them in mind. And the hreflang tag names the language, not the regional variety the prose is written in, because a language tag on the document describes the text while the alternate link targets an audience, and narrowing the audience to Britain would have left every other English reader arriving only through the default.

The strings the type checker cannot see

Then the long pole: the interface. The mechanism was small and sound, a base dictionary with typed keys, per-language overlays, CLDR plural rules through the platform's own Intl API, lazy per-language chunks. The content was not written, and string extraction is engineering rather than translation, because a string only becomes translatable once someone has decided what it means in context. Roughly three thousand candidates across several hundred component files. The standing rule was set before the first screen: extract a screen completely or not at all, because a half-extracted screen translates some labels and not others and reads to the user as a bug rather than as work in progress. And English must not regress, which four mechanical layers guarantee: a key that does not exist in the base dictionary cannot compile, a component using the translator without its hook cannot compile, dictionary values are copied from source bytes by a codemod rather than retyped, and a coverage test holds every language to full coverage of the base with declared, reasoned waivers for the product names that never translate.

Most of the work went into the tools rather than the strings, and the transferable finding is about what such tools can and cannot see. Every guard in a string migration inspects source text. The two failures that reached certified screens were not about source text at all. The first was about when a value is constructed: copy authored in a module-level table is built at import time, before any language is resolved, so it holds English for the life of the tab whatever the reader chose. Nine screens certified across three separate sessions carried that shape, and a German screenshot found the first of them; neither the detector, which looks for JSX text and attributes, nor the interpolated-prose report, which looks for prose broken by an expression, has any reason to look inside an object literal. The second was about what quoting a value sits in: the codemod rewrote a string inside a template literal that builds HTML, where a translator call is twenty-nine literal characters, and it would have shipped as the accessible name of every heading anchor on every shared document. It is the one extraction mistake the type checker cannot catch, because a string's contents are not typed. Both now have guards, but the lesson is about method: "the tool found nothing" and "there is nothing to find" are different claims, and a browser rendering the actual language is the only check that asks the question the user asks.

The strings themselves taught grammar the English source had been hiding. Sentences concatenated from fragments in code, a possessive built by appending an apostrophe-s, a relative time written as a number and a suffix: each one encoded English word order as control flow, and each was already in the codebase before any translation existed. German writes "vor 5 Minuten", preposition first, so "{n}m ago" is not translatable at all; Spanish and German both restructure a possessive into "the work of X"; a list of names joined with commas and "and" is wrong in every language that is not English. The fixes were structural rather than lexical: a translate function that returns a rendered tree so a sentence keeps its link, complete messages with named placeholders the translator can reorder, and the platform's list and relative-time formatters instead of our own.

A translation that yields to your own words

The catalog of Kits is the product describing itself, and it sits outside the interface entirely: seed data written into a user's own database when a Kit is installed, served back through an API, invisible to every mechanism above. It is also larger than the whole interface. The reader-facing half runs to well over a hundred thousand words per language, and it is the copy a new user reads at the exact moment they are deciding whether the product is for them.

Two problems share the name. The browse surface is ordinary display localisation. The installed copy is not display at all: installing a Kit copies text into the user's own records and derives each piece's permanent slug from its name, so translating the name at install would fork identity per language, and a user who later switched languages would be stranded with pieces in the language they installed under. Our first recommendation was translate-at-install with a canonical English slug, and it was overturned by a better question: can every catalog item be translated into every language, so that a switch simply reads the right one? The design that shipped resolves at read time. Storage never changes; an installed piece keeps storing canonical English exactly as before. A display field shows the overlay for the current language only while the field is untouched, and the stored value the moment the user has edited it. The user's copy of the catalog's words is copy-on-write: shared with the catalog until the first keystroke, theirs for good after it, in every language.

Two things stay English by decision. Agent names, because a name that changes with the interface language is not a name, and a keeper someone has been talking to for months must not become someone else when they switch to screen-share with a colleague. The sentence beside every name translates; the name does not. And everything the model reads rather than the user: agent identities, skill bodies, routine instructions. Not because a model reasons in English, but because a prompt fix that has to ship in four languages ships as four versions that drift, and the parts of an instruction that matter most, the negations and the "never, even when asked", are exactly the parts a translation blurs.

The language you cannot get out of

The stickiest field in the product turned out to be the language setting. A choice made in the app is promoted to the account and re-applied on every device, permanently, which is what a preference should do. It also meant that reading the Spanish docs once, signed in, set the account's product language to Spanish everywhere, and that deleting the stored choice on one device could not win, because the account wrote it straight back on reload. Fixing the write path, so that a public-site choice no longer promotes, was the easy half and did nothing for the accounts already carrying a language they had not chosen. Closing a write path is not a fix for the data it wrote. The fix was to count the writers and give each an exit: an Automatic row in both account menus, the desktop one having never had a language control at all, and a way for someone to ask an agent, in whatever language they can manage, to put the app back to their browser's language. A clear that fails the network is retried until the account confirms it, outranking the account row meanwhile, because the old code swallowed that failure on the reasoning that "the device choice holds", which is true of choosing a language and false of un-choosing one.

The public-site banner had the mirror defect, and its shape generalises. The policy checked whether the reader had stored any choice, a boolean, when the question was which language they chose. Accepting the offer of Spanish stored Spanish, so the strongest case for the banner was the one that silenced it for good. A boolean derived from richer data is a claim that the richer data was not needed; the fix was to carry the tag and prepend it to the preference list, after which every later clause of the policy handled every case with no special-casing at all. A day later the policy moved once more, to offer whenever the reader's language differs from the page's, with "not now" lasting one tab, because a footer picker seventeen thousand pixels down a page is only an acceptable fallback while the offer itself is always available.

What is still English

The catalog's words, for sixty-one of sixty-two Kits. The email the product sends, and its push notifications, which carry no locale in their send paths. The two legal pages, deliberately, because an in-house translation of a contract a reader is asked to agree to is a different risk from an in-house translation of a feature page. A native-speaker pass on every language, which is owed and no longer gates indexing. Non-Latin scripts and right-to-left layout, where the parity claim stops being free and has to be re-measured. A pseudolocale, which is how German's text expansion gets caught before a reader does. And the detector's own definition of done: the day the worklist read zero, a real browser found strings in a ternary, a template-literal prop and a default parameter that no pattern we run can see, which is the fortnight's lesson arriving one last time. They are the next unit of work, and they will be certified the way the rest now are: by a browser rendering the language, not by the scan that ran clean.

One letter a month, when there is something worth saying.

What we shipped, what we learned, and the occasional thing that did not work. No drip sequence, no launch countdowns.

Unsubscribe any time. Or take the RSS feed instead.