localizationi18nai-translationengineering

Ten languages in three days: field notes from localizing an AI translation product

We localized Transept into German, Ukrainian, Chinese, Portuguese, French, Spanish, Czech, Italian, Polish, and Turkish in one long weekend, using machine translation the way we tell our users to use it: with context, terminology decisions, and human post-editing. These are the plural rules, register flips, typography inversions, and hreflang lessons we collected on the way.

Mariia Ivakhnenko
Mariia Ivakhnenko17 min read
Ten languages in three days: field notes from localizing an AI translation product
On this page

Between a Friday morning and a Sunday evening in early July, Transept went from an English-only product to one that speaks eleven languages. German, Ukrainian, and Chinese shipped first, with Brazilian Portuguese close behind; French, Spanish, Czech, Italian, and Polish landed the next day; Turkish closed the set on Sunday.

We build an AI translation tool. Staying English-only would have been a quiet admission that we don't believe our own pitch. So we localized the way we tell our users to: machine translation for the breadth, human judgment for the decisions, and a memory of every decision so it never has to be made twice.

Each language meant roughly 3,300 strings in the app, another 3,000 across the marketing site and help center, six legal pages, and every email the product sends. This post is the field notes: the things that surprised us, the traps we fell into, and a few we caught just in time. If you are about to localize a product, or you translate for a living and want to see what software localization looks like from inside the machine, this is for you.

Why we didn't just run it through Google Translate

The honest first question. Machine translation is cheap and instant; every string table on Earth can be shoved through an API in an afternoon. Why not do that?

Because we tried something close to it, and caught the failure in our own product before a user did. Our first pass at the marketing pages treated them the way most string pipelines do: as a spreadsheet of disconnected cells, batched by size, each line translated blind. The output was grammatical, plausible, and subtly dead. A headline and the accent phrase completing it were translated by two different calls that had never met. A FAQ answer didn't know its question. Two columns of a comparison table drifted apart because neither knew the other existed. Meanwhile the in-app UI, where every string had been authored with a context note, read noticeably better in every language. Same model, same day, same pipeline. The difference was context.

That became the rule for everything: the model sees coherent wholes, never shuffled fragments, and every string travels with a note about where it appears and what the user is doing when they see it. "Save button" is not a context note. "Generic save-button label on every form submit; the user has just edited a value and is committing it" is one. It's written for a translator who has never opened the app, and it doesn't matter whether that translator is a human or a model: both produce garbage without it, and both get startlingly good with it.

Translation quality is a function of context before it is a function of the translator. Any working translator will tell you this. It turns out to be just as true when the translator is a machine. That's convenient, because giving a machine more context scales better than giving it more scolding.

The industry name for the workflow we ended up with is MTPE, machine translation post-editing. The machine does the first pass across everything; humans review where trust matters. But MTPE only works in one order: the human decides, the machine executes, the human verifies. The decisions come first. Which brings me to German.

du oder Sie: register is a product decision

German was our first language, and it handed us our first lesson within hours of shipping.

German has two ways to say "you": the informal du and the formal Sie. We launched with du: friendly, startup-flavored, the register half the apps on your phone use. Then we looked at our actual audience (professional translators, agencies, legal teams) and flipped the entire product to Sie the same day.

The lesson was what a register flip costs: a fresh re-translation of everything. The pronoun choice cascades into verb conjugations, imperatives, possessives, even capitalization. Patch it string-by-string and you get half-converted residue everywhere: a Sie sentence with a du verb hiding in the middle. We re-translated every corpus from scratch and grepped for the tell-tale du/dein/dich stems afterward to prove the flip took.

The German homepage of Transept: "Wo jede Entscheidung zur Erinnerung wird"

The German homepage. The brand line, "Where every decision becomes memory," turned out to be a thesis statement about localization itself.

Since then, register is the first question we settle for every new language, before a single string is translated, because it bakes into six thousand of them. Spanish went formal usted, and European rather than Latin American, to sit consistently beside German's Sie and French's vous. Italian went formal Lei. Polish? Well, Polish deserves its own section.

The plural rules nobody warns you about

Every team that localizes English software walks into the same trap: English plurals are so simple that your string format probably only has two slots: one thing, many things. German behaves the same way, which lulls you deeper. Then you add a Slavic language and entire categories of your UI silently fall back to English.

Ukrainian, my native language and the one where I had the least excuse, has four plural categories. One document. Two, three, four documents in a different form. Five through twenty in a third. Fractions in a fourth. Our string table carried only the English-shaped two, so for counts of 2, 3, 4, 5, 11, 22 (the majority of numbers a user actually sees), the app served English. About eighty strings (credits, documents, members, files), every one a visible seam down the middle of a Ukrainian sentence.

Engineering notes from the Ukrainian build-out, describing how counts of 2, 3, 4, 5, 11, 22 fell through to English before the plural forms were filled in

The build note from the day we found it. "Falls through to English — a very visible bug" is engineering understatement.

The fix is conceptually simple (supply every plural form the language's grammar defines, not just the two English has), but the tour of what those forms are became my favorite collection of trivia from the whole project:

LanguagePlural categoriesThe surprise
German2None. That's the trap: it teaches you the world has two.
Ukrainian421 takes the singular form: «21 крок».
Czech4The fourth form is for decimals only, a ghost category your UI never renders.
Polish4Same four categories as Czech, completely different distribution. 21 is plural: „21 kroków”.
French3Zero is singular. And the third form fires only at exact millions.
Spanish3Zero is plural, the exact opposite of French.
Turkish2Nouns stay singular after any numeral: "5 belge", never "5 belgeler".
Chinese1One form for everything. The easiest language in this table.

Two of these deserve a closer look.

Polish is not Czech. They're sibling West Slavic languages, so we assumed Polish would follow the Czech pattern. It doesn't: Polish's plural distribution matches Ukrainian, its East Slavic cousin, despite the family tree. Twenty-one steps is „21 kroków” in Polish but «21 крок» in Ukrainian: plural in one, singular in the other, in two languages that agree on almost everything else. The lesson generalizes: check the grammar, never the family. Linguistic relatedness is only a suggestion.

French has a plural form that exists only at one million. Not "around a million": at exactly 1,000,000, 2,000,000, and so on. For every other number French behaves like English. We now handle it, and I hope someday a user with a round-million word balance notices.

One more trap from this chapter, for the engineers: Ukrainian's language code is uk, not ua. ua is the country code, and if you use it, the plural machinery doesn't error; it resolves to British English rules and everything above happens again. The same split bites Czech (cs, not cz) and Danish (da, not dk).

«Воркфлоу» чи «робочий процес»: terminology is a set of decisions

The hardest problems weren't grammatical. They were the small wars over single words.

Take workflow. Ukrainian offers a native calque («робочий процес», literally "working process"), and it's what a dictionary would give you. But translators who live in this software all day don't say «робочий процес»; they say «воркфлоу», the loanword, the same way English absorbed rendezvous. We went back and forth and shipped the loanword. It reads like the industry actually talks.

Or take Memory, our own feature name. Our German convention keeps a handful of terms in English: Credits, Translation Memory. A German professional reads those as industry vocabulary, the way they read Update or Login. So when Ukrainian inherited the same never-translate list, "Translation Memory" sat in English in the middle of Cyrillic sentences, and there it read as someone forgot to translate this. A Latin word inside Cyrillic text announces itself. Worse, Ukrainian is an inflected language: «Пам'ять» needs to decline by case as the sentence bends around it, which an untranslated English noun refuses to do. We corrected it to «перекладацька пам'ять», and learned that a never-translate list is a per-language decision. Chinese made the same point from the other side: there, everything translates (翻译记忆库 for Translation Memory, 术语库 for glossary, 工作流 for workflow), and only the product names themselves stay Latin.

Our whole product is built on this lesson: terminology is an accumulating set of decisions with reasons. This word because the industry says it, that word in English because it's a brand, this one translated because the script demands it. Make the decision once, record the reason, and every future document inherits it. That is what a glossary is actually for, and why we treat translation memory as decision context, not just a pile of matched sentences.

Sibling languages disagree about everything

If plurals were the grammar tour, typography was the etiquette tour, and the firmest lesson was that nothing transfers between neighbors.

French insists on a space before every colon, semicolon, exclamation and question mark (a non-breaking one, s'il vous plaît) and wraps quotations in « guillemets with inner spaces ». Spanish, right across the border, does the exact opposite: punctuation sits tight, guillemets hug their contents «like this», and questions must open upside down. ¿ and ¡ are mandatory grammar.

Italian gave us the subtlest one. Formal Italian capitalizes the courtesy pronoun (Lei, Suo, Sua), partly to distinguish the formal "you" from lowercase lei, which means "she." This mattered more than it might have elsewhere, because our copy constantly refers to Literess, who is very much a lei, right next to sentences addressing the user as Lei. One capital letter is doing load-bearing disambiguation. Italian also broke the button convention: French and Spanish label buttons with the infinitive (Enregistrer, Guardar), but Italian uses a bare imperative (Salva, Accedi) that looks informal and isn't; it's simply how Italian software speaks.

Polish posed a problem no earlier language had: its polite form is gendered. German Sie, French vous, Spanish usted: one form fits every user. Polish politeness is Pan for a man, Pani for a woman, and even past-tense verbs inflect for the gender of the person you're addressing. We don't know a user's gender and have no business guessing, so the Polish product speaks the way well-made Polish software does: impersonal constructions ("Zapisano", saved), a warm first-person-plural company voice ("Zapraszamy", we invite you), and direct gendered address only where it's truly unavoidable.

Turkish, our last language, looked easy (one gender-neutral formal siz, no grammatical gender at all) and then billed us in orthography. The Turkish capital of i is İ, dotted, so "iptal" (cancel) capitalizes to "İptal"; an I without the dot is a different letter and a visible error. Language names capitalize ("Türkçe"), where every Romance and Slavic language lowercases them. And because Turkish is agglutinative, even our never-translate brand names inflect: a case suffix attaches with an apostrophe, so users upgrade "Pro'ya" and import "Transept'e". The brand itself declines. That one isn't in any i18n handbook.

Chinese, for balance, was the physics lesson: full-width punctuation(,。!?), a thin breathing space where CJK meets Latin ("使用 Transept 翻译"), and text that lands at roughly half the length of the English. After German, which swells and breaks your layouts, Chinese shrinks and leaves your buttons feeling oversized. Your UI has to survive both.

The stowaways: strings that never went through translation at all

Localization has a special class of bug: the string that never entered the system in the first place.

Our translation coverage is enforced by the compiler: a string with a missing language literally doesn't build. But that check can only see strings that asked to be translated. A badge that renders a raw database value (owner, straight from the enum) never asked. It sailed through every build, English in all eleven languages, invisible for one simple reason: while the app was English-only, hardcoded English is camouflage.

The first Ukrainian user session found the owner badge. Then the access-level labels on the share dialog. Then the entire comment gutter: some twenty-five strings that had never met the translation layer. Then a screen-reader label here, a placeholder there. Each new language became an audit of everything before it: the moment the surrounding text flips into Ukrainian, every stowaway lights up like a flare.

Shipping a language is not a translation task; it's a census. You don't know which of your strings are real, findable, translatable strings until a second language forces every last one of them to answer roll call. Plan the audit pass into the schedule, because the compiler will not save you from the string that never called for translation.

Literess refused to be smug in Ukrainian

My favorite bug of the entire project isn't, strictly speaking, a bug. We still can't fully explain it.

Literess, our in-house editor-assistant (you may know her from her own article), has a repertoire of forty-eight moods. Her avatar reacts while she works: she chooses her expression the way she chooses her words, as part of the reply. And her signature look is smug: the little self-satisfied smirk she wears when she catches an error you missed.

She does it in every language. Caught-your-typo smugness in German, in French, in Chinese, in Polish. Then we switched her to Ukrainian and ran the same situations, the ones that reliably produce the smirk everywhere else, and she wouldn't. Same model, same prompt, same personality, same forty-eight moods on the menu. In Ukrainian she stays happy, or at most goes serious. She simply refuses to be smug in Ukrainian.

Our best theory is the one a translator would offer. In English, smug can be affectionate: aimed at a cartoon assistant who just found your typo, it's practically a compliment. Ukrainian has no such word: the closest equivalent, «самовдоволена», is only an insult. Flat, unlikable self-satisfaction, no wink, no charm. And a model thinking in Ukrainian appears to know that. Formulating her reply in a language where fond smugness doesn't exist as a concept, she doesn't reach for it as a feeling either. The word exists in her vocabulary; the reluctance lives in her behavior.

I find this equal parts funny and profound. This whole post has been about strings (plurals, registers, punctuation), and here is a case where every string was correct and the product still changed. A language is not a skin you stretch over an AI product; it steers the model wearing it. Localizing an agent means checking her personality per language, on top of her labels: you have to go and meet who she is in each one.

(The labels needed care too, for the record: the mood content, as in contented, nearly shipped in German as "Inhalt," content-the-noun, until one line of translator context pinned the sense. The cheapest quality intervention in localization is still a sentence telling the translator what a string is.)

The unglamorous half: hreflang, sitemaps, and not competing with yourself

None of the above matters if nobody finds the pages, so here is the SEO half, compressed.

The moment you publish the same page in eleven languages, Google's default assumption is that you've published eleven near-duplicates competing against each other. The mechanism that prevents this is the hreflang annotation, and it's less forgiving than any compiler:

  • It must be reciprocal. The English page points to the German twin and the German twin points back with the identical set, or Google ignores the whole arrangement. One-directional hreflang is no hreflang.
  • x-default points at your canonical version: the fallback for users whose language matches nothing you offer.
  • Never annotate a page that doesn't exist. If the Ukrainian version of a page isn't live yet, it gets no alternate link; a dangling hreflang pointing at a 404 is worse than none at all. This sounds obvious until you have partially-translated surfaces, and then it requires real bookkeeping: our journal articles, for instance, are translated one by one, so each article only advertises the languages it actually exists in.
  • Sitemaps carry the same annotations. We restructured ours into an index of per-language sitemaps, which has the pleasant side effect of per-language indexing stats in Search Console: you can watch each language's pages enter the index separately, and see immediately if one lags.

All of this is plumbing. Localization without the plumbing just fragments your own authority across eleven competing copies. The entire SEO benefit of speaking a user's language depends on Google understanding that they're one page, not eleven.

What we refused to machine-translate

For all the machine-translation enthusiasm above, three things were deliberately kept away from the pipeline.

These articles. The journal's chrome (labels, bylines, navigation) is machine-translated like everything else. The articles themselves are translated by hand, in Transept, by us. Long-form writing with a byline is exactly the case where machine output isn't good enough yet, and translating our own essays in our own editor is the most honest product testing we know how to do. The version of this article you may be reading in German or Ukrainian went through the very editor, glossary, and review workflow we sell.

Legal pages. Machine translation drafts them (with every company name, address, date, and clause number pinned), but Terms and Privacy are binding documents, and a lawyer reviews them per language. A translation error in marketing copy costs style points; in a contract it costs actual money.

Final say on trust surfaces. Pricing, billing, authentication, emails: a native speaker reads those before we consider a language done. This is MTPE working as intended: the machine writes every word so that humans can spend their scarce, expensive attention only on the words that carry risk.

Behind all three sits the same judgment: machine translation is a first pass, and deciding where a first pass is enough is itself the core localization skill. Ten languages in three days was possible because we knew precisely what the machine shouldn't do.

Where every decision becomes memory

Look back over this list. Formal or informal register. «Воркфлоу», not «робочий процес». Translation Memory in English for Germans, «перекладацька пам'ять» for Ukrainians. Capital Lei, impersonal Polish, an apostrophe before Turkish suffixes, a plural form reserved for round millions. An assistant who smirks in nine languages and refuses to in the tenth.

Almost none of this could have been looked up in advance, and the parts that matter most are decisions: argued about, settled, and then relied on by thousands of strings and every future document. Losing the decision means paying for the argument again; keeping only the decision without the reason means the next person re-litigates it anyway.

That, in the end, is why we built our product the way we did: translation memory as decision context, glossaries and styleguides as the place where register and terminology live, and an editor whose whole job is making sure nothing that was decided gets forgotten. Localizing Transept was the first time we ran the entire philosophy against our own product at full scale. The tooling held. The lessons above are the ones we're feeding back in, because ten languages was the rehearsal, not the finale.

The author

Mariia Ivakhnenko

Co-founder of Transept. Three degrees in English Language and Literature — Kyiv, Ostrava, and a year in Salzburg — and a Ukrainian native who lives most of her writing life in English. Came into AI as a prompt engineer, then product and lifecycle marketing. She writes semi-fictional stories about real people, and keeps circling the question of what gets lost between languages.