<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akmal Urunboev</title>
    <description>The latest articles on DEV Community by Akmal Urunboev (@akmalurunboev).</description>
    <link>https://dev.to/akmalurunboev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4158673%2F46cd7a36-76f3-437e-b960-cd1276225bc6.jpg</url>
      <title>DEV Community: Akmal Urunboev</title>
      <link>https://dev.to/akmalurunboev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akmalurunboev"/>
    <language>en</language>
    <item>
      <title>How to QA a translation in a language you can't read</title>
      <dc:creator>Akmal Urunboev</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:26:14 +0000</pubDate>
      <link>https://dev.to/akmalurunboev/how-to-qa-a-translation-in-a-language-you-cant-read-4jji</link>
      <guid>https://dev.to/akmalurunboev/how-to-qa-a-translation-in-a-language-you-cant-read-4jji</guid>
      <description>&lt;p&gt;I ship in ten languages. I speak two of them.&lt;/p&gt;

&lt;p&gt;For a long time my quality process was "the translator seemed competent," which is not a process, it is a hope. Here is the system I use now. None of it requires me to read the target language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the translator from the reviewer
&lt;/h2&gt;

&lt;p&gt;This is the single highest-value change, and it is organisational rather than technical.&lt;/p&gt;

&lt;p&gt;Nobody is a reliable reviewer of their own work, and that is doubly true in translation, where the translator has already made a hundred interpretive decisions they have stopped being able to see. One person translates. A different native speaker reviews. They should not be the same person and ideally should not be in the same room.&lt;/p&gt;

&lt;p&gt;If budget only allows one native speaker per language, spend them on &lt;em&gt;review&lt;/em&gt; and use a lower-cost first pass for the draft. Reviewing a flawed translation is faster than producing a good one, and the output is better than one unreviewed expert pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate everything that doesn't need a human
&lt;/h2&gt;

&lt;p&gt;A surprising share of translation defects are mechanical, and mechanical defects can be caught by a script that does not speak the language either. I run all of these in CI against the translated resource files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Placeholder integrity.&lt;/strong&gt; Every placeholder in the source exists in the target, with the same name, and no extras. This is the single most common serious bug, and it crashes at runtime rather than reading oddly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ICU syntax validity.&lt;/strong&gt; Plural and select blocks parse, and the categories present are the ones that locale actually requires. A translation missing a required plural category will fall back silently and look fine until the number is right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Untranslated strings.&lt;/strong&gt; Target identical to source, across a whole file, usually means a pipeline failure rather than a word that happens to be the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length budget.&lt;/strong&gt; Flag anything substantially longer than source. It will not throw, it will just overflow a button somewhere you aren't looking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminology adherence.&lt;/strong&gt; If you have a glossary — and you should — check that the agreed term was actually used. This one catches real meaning drift, not just mechanics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Punctuation and spacing conventions.&lt;/strong&gt; Some locales take a space before a colon, some use different quotation marks, some use a different decimal separator. These are rule-checkable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this tells you whether the translation is &lt;em&gt;good&lt;/em&gt;. All of it tells you whether it is &lt;em&gt;broken&lt;/em&gt;, and it removes the broken ones from the human reviewer's plate so their attention goes where it is actually needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review in context, never in a spreadsheet
&lt;/h2&gt;

&lt;p&gt;Translators are usually handed a column of strings with no indication of where any of them appear. Then we are surprised when a word that means "free of charge" shows up where we meant "unoccupied."&lt;/p&gt;

&lt;p&gt;Give the reviewer the actual screens. A build, a staging link, or at minimum a screenshot attached to each string. The defects that only show up in context — wrong register, wrong sense of an ambiguous word, a button label that is grammatically fine and situationally absurd — are exactly the ones that automation cannot catch and that users notice first.&lt;/p&gt;

&lt;p&gt;If you do nothing else from this post, do this one. It is the highest ratio of quality gained to effort spent of anything I have tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the review a rubric
&lt;/h2&gt;

&lt;p&gt;"Does this look right?" produces a thumbs up. Ask four separate questions and you get usable answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Accuracy.&lt;/strong&gt; Does it say what the source says?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminology.&lt;/strong&gt; Does it use the agreed terms from the glossary?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Register.&lt;/strong&gt; Is the formality level right and consistent? Does it address the reader the way we intend to?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fluency.&lt;/strong&gt; Would a native speaker write it this way, or does it read as translated?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Scoring those separately matters because they have different fixes and different severities. A fluency complaint is a polish item. An accuracy or terminology failure is a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample, don't boil the ocean
&lt;/h2&gt;

&lt;p&gt;Full review of every string on every release does not survive contact with a shipping schedule.&lt;/p&gt;

&lt;p&gt;I review everything once at launch for a locale, then sample afterwards — weighted toward strings that are new, strings that changed, and strings on screens that matter most. If a sample comes back with defects above your tolerance, widen it for that locale. If it comes back clean repeatedly, narrow it.&lt;/p&gt;

&lt;p&gt;The point is that you are measuring translation quality as an ongoing property, not certifying it once and assuming it holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back-translation is weaker than it looks
&lt;/h2&gt;

&lt;p&gt;A common suggestion is to have the target translated back into English and compare. I use it rarely and trust it less than I used to.&lt;/p&gt;

&lt;p&gt;It catches gross errors — a flipped negation, a dropped clause. It is poor at everything subtler, because a fluent back-translator will unconsciously repair an awkward target, and an awkward back-translator will make a perfectly good target look wrong. You end up investigating noise.&lt;/p&gt;

&lt;p&gt;Use it as a spot check on your highest-stakes strings. Do not build a quality process on it.&lt;/p&gt;




&lt;p&gt;The thing I wish someone had told me earlier: &lt;strong&gt;you cannot eyeball your way out of not speaking the language, and you do not have to.&lt;/strong&gt; Almost everything above is either a script or an organisational decision. The part that genuinely requires a native speaker is smaller than it first appears — but it is not zero, and the mistake is pretending it is.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>localization</category>
      <category>testing</category>
      <category>ux</category>
    </item>
    <item>
      <title>Right-to-left is not a flag you flip</title>
      <dc:creator>Akmal Urunboev</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:25:38 +0000</pubDate>
      <link>https://dev.to/akmalurunboev/right-to-left-is-not-a-flag-you-flip-463</link>
      <guid>https://dev.to/akmalurunboev/right-to-left-is-not-a-flag-you-flip-463</guid>
      <description>&lt;p&gt;The first time I shipped an Arabic locale, I thought the work was translation plus one &lt;code&gt;TextDirection&lt;/code&gt;. The translation was the easy half. What took the longest was everything that had quietly assumed text ran left to right.&lt;/p&gt;

&lt;p&gt;Here is what actually broke, and what I do now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard-coded sides
&lt;/h2&gt;

&lt;p&gt;This is the bulk of it, and it is boring, mechanical work that you cannot skip.&lt;/p&gt;

&lt;p&gt;Anywhere your layout says &lt;code&gt;left&lt;/code&gt; or &lt;code&gt;right&lt;/code&gt;, it is asserting a physical direction. What you almost always mean is &lt;em&gt;start&lt;/em&gt; or &lt;em&gt;end&lt;/em&gt; — the side the reader begins on, whichever side that is.&lt;/p&gt;

&lt;p&gt;In Flutter that means &lt;code&gt;EdgeInsetsDirectional&lt;/code&gt; instead of &lt;code&gt;EdgeInsets&lt;/code&gt;, &lt;code&gt;AlignmentDirectional&lt;/code&gt; instead of &lt;code&gt;Alignment&lt;/code&gt;, and &lt;code&gt;start&lt;/code&gt;/&lt;code&gt;end&lt;/code&gt; instead of &lt;code&gt;left&lt;/code&gt;/&lt;code&gt;right&lt;/code&gt; on anything that takes an alignment. On the web it is &lt;code&gt;margin-inline-start&lt;/code&gt; instead of &lt;code&gt;margin-left&lt;/code&gt;, &lt;code&gt;padding-inline-end&lt;/code&gt;, &lt;code&gt;inset-inline-start&lt;/code&gt;, and &lt;code&gt;text-align: start&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The failure mode is nasty because it is partial. The screen does not break; it goes subtly wrong. Labels drift away from the fields they belong to. Icons sit on the wrong edge of a row. Nothing throws. You just end up with an interface that feels slightly broken to the people using it and looks fine to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; make it a lint rule, not a code review habit. Ban the physical properties outright in anything that renders UI, and you stop re-fixing this every sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that should not mirror
&lt;/h2&gt;

&lt;p&gt;Mirroring everything is as wrong as mirroring nothing, and it is the mistake people make once they have discovered the problem and overcorrect.&lt;/p&gt;

&lt;p&gt;Media playback controls do not flip — play still points the way it always did. Clocks run the same direction. Phone numbers, credit card numbers, ISBNs and version strings keep their order. Logos keep their orientation. Charts with a time axis usually keep time running the way your audience reads charts, which is not always the way they read sentences, and is worth asking about rather than assuming.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;does&lt;/em&gt; mirror: anything expressing sequence or direction of travel in the reading flow. Back and forward arrows. Progress bars. Breadcrumbs. Indentation. The side a drawer slides in from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; the question is never "is this app RTL?" It is "does this particular element encode reading order?" Most do. Some very much do not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classify the assets, not the components
&lt;/h2&gt;

&lt;p&gt;A reader suggested a better framing than the one I started with, and I think it is right: instead of deciding directionality at each place an icon is used, classify every asset once, into four buckets — &lt;strong&gt;mirror&lt;/strong&gt;, &lt;strong&gt;invariant&lt;/strong&gt;, &lt;strong&gt;custom RTL&lt;/strong&gt; (a separately drawn version, because mirroring the original produces something wrong), and &lt;strong&gt;unknown&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The fourth bucket is the one doing the work. Without it, anything untriaged silently inherits a default, and silent defaults are precisely how a play button ends up pointing the wrong way in production. With it, "unknown" is a state you can fail a build on.&lt;/p&gt;

&lt;p&gt;One refinement from doing this across a real icon set: directionality sometimes belongs to the &lt;em&gt;usage&lt;/em&gt; rather than the asset. The same arrow glyph mirrors when it means "next" and does not when it means "increase." So let a usage site override the asset's classification — but require that the override be written down explicitly, never inherited.&lt;/p&gt;

&lt;p&gt;Keep the classification in asset metadata, a manifest entry or a filename convention, and have the rendering layer read it. Then no individual component is deciding directionality on its own, which was the actual problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixed-direction strings
&lt;/h2&gt;

&lt;p&gt;This is the one that produces genuinely unreadable output, and it is invisible until it happens.&lt;/p&gt;

&lt;p&gt;The moment you interpolate a Latin-script value into an Arabic sentence — a username, a file name, a product code, a URL — the Unicode bidirectional algorithm has to guess where that run begins and ends. When it guesses wrong, trailing punctuation jumps to the other side of the string and the sentence becomes nonsense.&lt;/p&gt;

&lt;p&gt;The fix is to isolate the embedded run explicitly, with &lt;code&gt;FSI&lt;/code&gt; (U+2068) before and &lt;code&gt;PDI&lt;/code&gt; (U+2069) after. Most i18n libraries will do this for you if you let them format the string rather than concatenating it yourself. That is the real rule: &lt;strong&gt;never build a sentence with &lt;code&gt;+&lt;/code&gt;.&lt;/strong&gt; Pass the value to the formatter and let it wrap the isolate.&lt;/p&gt;

&lt;p&gt;Test this specifically. A good test string is an Arabic sentence with a Latin username and a trailing question mark, because a wrong answer is immediately visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Numerals
&lt;/h2&gt;

&lt;p&gt;Arabic locales may use Western digits (0123) or Arabic-Indic digits (٠١٢٣), and which one is correct depends on the region, not just the language. Do not hard-code either. Format numbers through the locale's number formatter and let it decide, and remember that this applies to dates, durations, prices and ordinals too — not only to the obvious integers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fonts
&lt;/h2&gt;

&lt;p&gt;Your Latin font almost certainly does not cover Arabic, and the fallback you get when it doesn't is rarely the one you want. Pick the Arabic face deliberately and check it at your smallest size, where the difference between a good and a bad Arabic font is most obvious.&lt;/p&gt;

&lt;p&gt;Line height is the other trap. Arabic script generally needs more vertical room than Latin at the same point size. A line height tuned for English will clip descenders and make dense paragraphs look cramped. Set it per script, not globally.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I test it now
&lt;/h2&gt;

&lt;p&gt;I stopped relying on reading the screens, because I cannot read Arabic and neither can most of the people reviewing a pull request.&lt;/p&gt;

&lt;p&gt;Two things that work without language knowledge:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pseudo-localization with forced RTL.&lt;/strong&gt; Render the interface with direction forced right-to-left and strings replaced by a marked-up version of the English — padded for length, wrapped in brackets, accented. Every layout bug shows up immediately, in a build anyone on the team can read. This catches the hard-coded-sides class of bug almost completely, before a translator sees anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Screenshot diffing.&lt;/strong&gt; Capture the same screens in both directions and look at them side by side. You are not reading the text; you are checking that the mirror image is structurally sensible. Wrong icons and stranded labels are obvious in that view and invisible in a code diff.&lt;/p&gt;

&lt;p&gt;Neither needs you to speak the language. Both find the bugs that embarrass you.&lt;/p&gt;




&lt;p&gt;If you only take one thing: &lt;strong&gt;ban &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt; from your layout code today&lt;/strong&gt;, before you have an RTL locale. It is a one-hour change now and a two-week change later, and it costs nothing in the meantime.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>flutter</category>
      <category>localization</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Terminology drift is the quiet tax on a large content corpus</title>
      <dc:creator>Akmal Urunboev</dc:creator>
      <pubDate>Sat, 10 Oct 2026 06:24:35 +0000</pubDate>
      <link>https://dev.to/akmalurunboev/terminology-drift-is-the-quiet-tax-on-a-large-content-corpus-4pnn</link>
      <guid>https://dev.to/akmalurunboev/terminology-drift-is-the-quiet-tax-on-a-large-content-corpus-4pnn</guid>
      <description>&lt;p&gt;Translation bugs that crash are easy. You find them, you fix them, they stay fixed.&lt;/p&gt;

&lt;p&gt;The expensive problem is slower: the same concept, translated three different ways in three parts of your product, by three people, over two years. Nothing is wrong. Everything parses. And the reader cannot tell whether the three words mean three things.&lt;/p&gt;

&lt;p&gt;This is terminology drift, and on a large corpus it will cost you more than any individual mistake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happens
&lt;/h2&gt;

&lt;p&gt;None of the causes are anyone being careless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple translators, inevitably.&lt;/strong&gt; Any corpus large enough to matter is touched by more than one person, usually across years. Each makes a defensible choice. The choices differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Translation memory rewards local consistency, not global.&lt;/strong&gt; A TM suggests what was used in similar &lt;em&gt;strings&lt;/em&gt;. It has no concept of "this is the same domain concept as that thing over there, phrased completely differently."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The source drifts first.&lt;/strong&gt; This is the one people miss. If your English says "account" in one place, "profile" in another and "user" in a third for the same object, you have already lost. Every target language will faithfully reproduce your inconsistency, multiplied by the number of locales.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody owns it.&lt;/strong&gt; Terminology sits between engineering, content and translation, which means it belongs to no one's checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix the source first
&lt;/h2&gt;

&lt;p&gt;The cheapest terminology work happens before anything is translated.&lt;/p&gt;

&lt;p&gt;Write down the nouns your product uses for its own concepts, pick one word per concept, and enforce it in English. Do this and you have improved every locale simultaneously, at the cost of one document.&lt;/p&gt;

&lt;p&gt;Keep the list small. Twenty to fifty terms covers the concepts that actually recur. A list of four hundred is a list nobody reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; a target-language glossary built on an inconsistent source just encodes the inconsistency more durably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then build a real termbase
&lt;/h2&gt;

&lt;p&gt;For each term, a target-language glossary needs more than a word:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the source term, and the &lt;strong&gt;one&lt;/strong&gt; approved target equivalent&lt;/li&gt;
&lt;li&gt;a definition, in plain language, so a translator knows which sense is meant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;forbidden alternatives&lt;/strong&gt; — the near-synonyms you have decided against, which is the field that does the most work&lt;/li&gt;
&lt;li&gt;part of speech and any grammatical notes the target language needs&lt;/li&gt;
&lt;li&gt;a note on whether it is a brand term that should not be translated at all&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That forbidden-alternatives field is worth the effort on its own. "Use X" is advice. "Use X, never Y or Z" is checkable, and it is what turns a glossary from a document into a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce it mechanically
&lt;/h2&gt;

&lt;p&gt;A glossary nobody checks is a glossary nobody follows.&lt;/p&gt;

&lt;p&gt;Run a check over your translated resources: for every source string containing a glossary term, confirm the target contains the approved equivalent and none of the forbidden ones. Fail the build, or at minimum raise it in review.&lt;/p&gt;

&lt;p&gt;Two caveats from experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inflection.&lt;/strong&gt; In a language that declines nouns, the approved term will legitimately appear in half a dozen forms. Naive substring matching produces false failures, your team learns to ignore the check, and the check is now worse than nothing. Match on stems, or maintain the inflected forms explicitly for the languages that need it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legitimate exceptions.&lt;/strong&gt; Sometimes the approved term genuinely does not fit a sentence. Give the check an explicit opt-out that has to be written down. The goal is that every deviation is a decision someone made on purpose, not that there are no deviations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure it
&lt;/h2&gt;

&lt;p&gt;Terminology consistency is one of the few translation-quality properties you can actually count: of all the places a glossary term should appear, what share use the approved equivalent?&lt;/p&gt;

&lt;p&gt;Track it per locale. It tells you where to spend review budget, and it tells you whether a new translator is drifting before the complaints arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retrofit
&lt;/h2&gt;

&lt;p&gt;If you are reading this with a large corpus already translated inconsistently, you do not need to fix it all.&lt;/p&gt;

&lt;p&gt;Rank by exposure. A term on the first screen everyone sees matters more than one buried three levels deep. Fix the top of that list, freeze the glossary so new content is correct from now on, and let the long tail be corrected opportunistically when those strings are touched for other reasons.&lt;/p&gt;

&lt;p&gt;The goal is not a perfectly consistent corpus. It is a corpus that stops getting worse, with the most-seen parts cleaned up first.&lt;/p&gt;




&lt;p&gt;The mistake I made was treating terminology as a translation problem. It is a &lt;strong&gt;source-side product decision&lt;/strong&gt; that translation merely propagates — and the work to fix it is almost all in English, before a single word gets sent anywhere.&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>localization</category>
      <category>writing</category>
      <category>documentation</category>
    </item>
    <item>
      <title>What breaks when you ship to ten locales at once</title>
      <dc:creator>Akmal Urunboev</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:16:32 +0000</pubDate>
      <link>https://dev.to/akmalurunboev/what-breaks-when-you-ship-to-ten-locales-at-once-3ehc</link>
      <guid>https://dev.to/akmalurunboev/what-breaks-when-you-ship-to-ten-locales-at-once-3ehc</guid>
      <description>&lt;p&gt;I localized a mobile app into ten languages. Most of what I'd read beforehand was about &lt;em&gt;how&lt;/em&gt; to translate. Almost none of it was about what goes wrong after, which turned out to be the harder half.&lt;/p&gt;

&lt;p&gt;These are the mistakes I actually made.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. I translated the content and left the chrome in English
&lt;/h2&gt;

&lt;p&gt;This is the one I'd undo first.&lt;/p&gt;

&lt;p&gt;The core content — the part users came for — got translated properly. The surrounding interface did not, or did only partly. Settings, error states, the subscription screens, legal copy, empty states.&lt;/p&gt;

&lt;p&gt;The result is worse than an English-only app, because it sets an expectation and then breaks it. A user who has been reading comfortably in their own language hits a wall precisely when something has gone wrong or something is being asked of them. The experience doesn't degrade gradually; it fractures at the exact moments that matter most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; a locale is either done or it isn't. Partial coverage is not partial credit. If you can't finish a language, ship it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. I shipped locales before I knew anyone wanted them
&lt;/h2&gt;

&lt;p&gt;Adding a language feels like growth. It is cheap to start and expensive forever: every string you ever write afterwards is now ten strings, every feature ships at the speed of your slowest translator, every bug report might be a translation bug.&lt;/p&gt;

&lt;p&gt;I added languages because I &lt;em&gt;could&lt;/em&gt; reach those speakers, not because I had evidence they were waiting. Some were right. Some were an ongoing tax I'm still paying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; treat each locale as a permanent operational commitment, not a one-time content cost. Ask what evidence would justify it, then go find that evidence first.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. I couldn't review what I couldn't read
&lt;/h2&gt;

&lt;p&gt;Obvious in hindsight. I shipped languages I don't speak, with no process for knowing whether they were any good beyond "the translator seemed competent."&lt;/p&gt;

&lt;p&gt;You cannot eyeball your way out of this. You need either a native reviewer who is separate from the translator, or a sampling process, or both — and you need it before launch, because the alternative is finding out from a one-star review written in a language you'll have to machine-translate to read.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; budget for review as a separate line from translation. They are different jobs and the same person should not do both.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. I let terminology drift
&lt;/h2&gt;

&lt;p&gt;Several translators, one corpus, no glossary. The same domain term ended up rendered three different ways across the app.&lt;/p&gt;

&lt;p&gt;Each individual choice was defensible. Collectively it made the product feel like it had been assembled from parts, and worse, it made the material harder to learn from — the whole point of consistent terminology in educational content is that the learner builds one mental model, not three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; the glossary comes before the first word is translated. Retrofitting consistency across a large corpus is enormously more expensive than establishing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. I assumed text length was roughly constant
&lt;/h2&gt;

&lt;p&gt;German and Russian run substantially longer than English. Some languages run shorter. Either way, layouts built around English string lengths break — truncation, wrapping into two lines, buttons growing past their container, labels colliding.&lt;/p&gt;

&lt;p&gt;This is not caught by review, because reviewers read text, they don't measure it. It is caught by pseudo-localization — rendering your UI with artificially lengthened strings before you have any real translations — which costs almost nothing and would have saved me a round of layout fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. I treated RTL as "a language"
&lt;/h2&gt;

&lt;p&gt;Arabic isn't another locale in the list; it's a different layout direction. Mirrored navigation, flipped icons that indicate direction, number and punctuation handling inside mixed text.&lt;/p&gt;

&lt;p&gt;Most frameworks handle the basics. The basics are not the problem — the problem is every place someone hardcoded a left margin, every directional icon that shouldn't flip (a play button) sitting next to one that should (a back arrow), and every string that mixes scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; if RTL is in your plan, put it in early. Adding it after a year of LTR-only development means auditing every layout you've written.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;Pick two languages. Do them completely — content, interface, store listing, support. Build the glossary, the review process and the pseudo-localization step while the surface area is small enough to hold in your head.&lt;/p&gt;

&lt;p&gt;Then add the third.&lt;/p&gt;

&lt;p&gt;The instinct is to go wide early because the marginal translation cost looks low. The translation is not the cost. Everything downstream of it is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and operate multilingual mobile applications, shipped across ten languages. More at &lt;a href="https://akmalu.com" rel="noopener noreferrer"&gt;akmalu.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>localization</category>
      <category>i18n</category>
      <category>mobile</category>
      <category>lessonslearned</category>
    </item>
    <item>
      <title>Designing for people reading in their second language</title>
      <dc:creator>Akmal Urunboev</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:15:45 +0000</pubDate>
      <link>https://dev.to/akmalurunboev/designing-for-people-reading-in-their-second-language-lno</link>
      <guid>https://dev.to/akmalurunboev/designing-for-people-reading-in-their-second-language-lno</guid>
      <description>&lt;p&gt;Most people using the products I work on are reading in a language that isn't their first. Many are doing it at the end of a long day, on a phone, in a situation where getting it wrong costs them something real.&lt;/p&gt;

&lt;p&gt;The thing I keep relearning: &lt;strong&gt;they are not bad at the task. The task is being delivered through a layer they have to fight.&lt;/strong&gt; Every bit of that layer you remove comes straight back to them.&lt;/p&gt;

&lt;p&gt;Here is what I've learned about removing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Negation is the single most expensive construction
&lt;/h2&gt;

&lt;p&gt;"Which of the following is &lt;strong&gt;not&lt;/strong&gt; required when…"&lt;/p&gt;

&lt;p&gt;A fluent reader handles this without noticing. A second-language reader has to hold the entire option list in working memory and invert it, in a language they are translating as they go, often with time pressure on top. The interaction stops measuring whether they understand the subject and starts measuring processing load.&lt;/p&gt;

&lt;p&gt;Formal source material is full of negation and you often can't change that — but everything around it is yours. Explanations, hints, onboarding, settings labels. I write those in the positive wherever there's a choice, and when I can't, the negation gets visual weight so it is impossible to skim past.&lt;/p&gt;

&lt;p&gt;Double negation — "which is not permitted unless…" — I treat as a defect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short sentences are not dumbing down
&lt;/h2&gt;

&lt;p&gt;There's a reflex, especially when the source material is legal or technical, to write in its register. Long clauses, stacked conditions, formal connectives.&lt;/p&gt;

&lt;p&gt;That register exists for precision, not comprehension. You can keep the precision and lose the sentence length: one idea per sentence, conditions as a list rather than a chain, the subject near the verb.&lt;/p&gt;

&lt;p&gt;What this is &lt;strong&gt;not&lt;/strong&gt; is simplification of content. These are adults doing something that matters to them. Patronizing them is its own failure, and they notice it faster than you'd think. The goal is to make the language transparent so the difficulty lives entirely in the subject, which is where it belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translation is not the same as localization
&lt;/h2&gt;

&lt;p&gt;Translating a sentence correctly and making it &lt;em&gt;usable&lt;/em&gt; are different jobs.&lt;/p&gt;

&lt;p&gt;A term of art may have no equivalent in the target language. The honest handling is often to give the translation and keep the original term alongside it — because they will meet the original out in the world regardless. Someone who only ever sees the translated version encounters the real one for the first time at the worst possible moment.&lt;/p&gt;

&lt;p&gt;That means some things deliberately stay untranslated. Working out which is domain judgment, not a language decision, and it's the part that can't be outsourced to a translator who doesn't know the domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Icons help less than you want them to
&lt;/h2&gt;

&lt;p&gt;Icons feel like the obvious fix for a language barrier. In practice they are their own vocabulary, and one that is strongly culturally conditioned. A floppy disk means save to people who have never seen a floppy disk, and means nothing to people who have never seen the convention.&lt;/p&gt;

&lt;p&gt;Where icons genuinely help: reinforcing a label you have already written in words. Where they fail: replacing it. I use them as redundancy, not as compression — the exception being domain symbols the user has to recognize anyway, where showing them &lt;em&gt;is&lt;/em&gt; the content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time pressure multiplies everything
&lt;/h2&gt;

&lt;p&gt;Every problem above gets worse under a clock, and a great many interfaces have one — a session that expires, a form that times out, a decision someone is making while a queue builds behind them. A sentence that costs a fluent reader two seconds and a second-language reader eight is a rounding error once and a catastrophe across a whole flow.&lt;/p&gt;

&lt;p&gt;So I treat reading time as a real budget. If a screen can be understood in one pass without re-reading, it's done. If my eye goes back, it isn't — and my eye is the easy case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't make them read anything they don't have to
&lt;/h2&gt;

&lt;p&gt;The highest-leverage work isn't rewriting sentences. It's removing screens.&lt;/p&gt;

&lt;p&gt;Every piece of interface copy is a tax paid in a second language. Onboarding that explains a feature the user could discover in two taps, a confirmation dialog for a reversible action, a tooltip restating the label it's attached to — in English these are mild clutter. Here they are a cost with no return.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond this audience
&lt;/h2&gt;

&lt;p&gt;Everything above is just good interface writing with the volume turned up. Short sentences, positive constructions, words doing real work, fewer screens — none of it is special pleading for non-native speakers. It's what helps a tired native speaker on a phone at eleven at night too.&lt;/p&gt;

&lt;p&gt;Designing for the harder case makes the easy case better. It just makes the failures impossible to ignore, which is the useful part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and operate multilingual mobile applications, shipped across ten languages. More at &lt;a href="https://akmalu.com" rel="noopener noreferrer"&gt;akmalu.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ux</category>
      <category>a11y</category>
      <category>localization</category>
      <category>writing</category>
    </item>
    <item>
      <title>Most languages don't have two plural forms. Your i18n setup assumes they do.</title>
      <dc:creator>Akmal Urunboev</dc:creator>
      <pubDate>Sat, 03 Oct 2026 00:08:54 +0000</pubDate>
      <link>https://dev.to/akmalurunboev/russian-has-four-plural-forms-your-i18n-setup-probably-handles-one-3ib6</link>
      <guid>https://dev.to/akmalurunboev/russian-has-four-plural-forms-your-i18n-setup-probably-handles-one-3ib6</guid>
      <description>&lt;p&gt;If you localize an app into Russian and your string files only have &lt;code&gt;one&lt;/code&gt; and &lt;code&gt;other&lt;/code&gt;, your Russian is wrong. Not awkward — wrong, in a way native speakers notice immediately and most English-speaking teams never see.&lt;/p&gt;

&lt;p&gt;I build multilingual mobile apps that run in ten languages, and this was the first thing that genuinely embarrassed me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing English teaches you to assume
&lt;/h2&gt;

&lt;p&gt;English has two plural forms. One question, two questions. The rule is "is it 1?" and after that you stop thinking about it. Every i18n tutorial reflects that, so the ARB or JSON you write looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questionCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{count, plural, one{{count} question} other{{count} questions}}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a complete specification of English. It is not a complete specification of anything else, and the structure you write in your source language is the structure translators are handed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Russian actually needs
&lt;/h2&gt;

&lt;p&gt;Russian uses four CLDR plural categories: &lt;code&gt;one&lt;/code&gt;, &lt;code&gt;few&lt;/code&gt;, &lt;code&gt;many&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Applies to&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;one&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1, 21, 31, 41, 101 …&lt;/td&gt;
&lt;td&gt;1 вопрос&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;few&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2–4, 22–24, 32–34 …&lt;/td&gt;
&lt;td&gt;2 вопроса&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;many&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0, 5–20, 25–30 …&lt;/td&gt;
&lt;td&gt;5 вопросов&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;other&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fractions&lt;/td&gt;
&lt;td&gt;1.5 вопроса&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read the &lt;code&gt;one&lt;/code&gt; row again, because it is the part that breaks code rather than translations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;21 is &lt;code&gt;one&lt;/code&gt; in Russian.&lt;/strong&gt; So is 31, 41, 101, 1021. If anywhere in your codebase you have written &lt;code&gt;if (count == 1)&lt;/code&gt; to decide singular versus plural, that branch is wrong in Russian for roughly one number in ten. It will look fine in every test you write, because your tests use 1, 2 and 5.&lt;/p&gt;

&lt;p&gt;Arabic goes further with six categories, including distinct &lt;code&gt;zero&lt;/code&gt; and &lt;code&gt;two&lt;/code&gt;. Polish, Czech, Ukrainian and Lithuanian each have their own arrangement. The point is not to memorize them. The point is that the number of forms is a property of the language, not of your code, and CLDR already knows all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually goes wrong
&lt;/h2&gt;

&lt;p&gt;Three failure modes, in the order I hit them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The source string constrains the translation.&lt;/strong&gt; You ship an ARB with only &lt;code&gt;one&lt;/code&gt; and &lt;code&gt;other&lt;/code&gt;. The translator opens it and has nowhere to put &lt;code&gt;few&lt;/code&gt; and &lt;code&gt;many&lt;/code&gt;. A good translator flags it. A translator working through a spreadsheet at volume picks the closest slot and moves on, and you have now shipped "5 вопроса" — which reads roughly like "5 questions-of" — to every user who answers five of anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Manual pluralization in code.&lt;/strong&gt; Somewhere there is a helper like &lt;code&gt;count == 1 ? singular : plural&lt;/code&gt;. It predates the i18n work, it is three lines long, and nobody remembers it exists. It is correct in English forever and wrong in Russian from the moment you launch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Interpolated counts inside longer sentences.&lt;/strong&gt; The plural form often has to agree with a verb or adjective elsewhere in the sentence, which means the whole sentence is the translation unit, not the noun. Splitting a sentence into fragments and concatenating them produces text no amount of good translation can save.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the source string with every category you might ever need&lt;/strong&gt;, even though English collapses most of them. &lt;code&gt;Intl.plural&lt;/code&gt; in Dart accepts &lt;code&gt;zero&lt;/code&gt;, &lt;code&gt;one&lt;/code&gt;, &lt;code&gt;two&lt;/code&gt;, &lt;code&gt;few&lt;/code&gt;, &lt;code&gt;many&lt;/code&gt;, &lt;code&gt;other&lt;/code&gt;. Fill in what English needs and leave the rest for the languages that use them, rather than handing translators a shape that cannot hold their grammar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete every manual plural branch.&lt;/strong&gt; Grep for &lt;code&gt;== 1&lt;/code&gt; near anything user-facing. This is a twenty-minute job and it finds things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never build a sentence by concatenation.&lt;/strong&gt; One string, one complete sentence, placeholders inside it. If that produces an awkward English string, the awkwardness is a signal about word order you'd otherwise discover in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with 1, 2, 5, 21 and 0.&lt;/strong&gt; Those five numbers hit every Russian category including the one that catches &lt;code&gt;== 1&lt;/code&gt;. Add 11 if you want to be thorough, since 11 is &lt;code&gt;many&lt;/code&gt; despite ending in 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth internalizing
&lt;/h2&gt;

&lt;p&gt;This is not really a Russian problem. It is what happens when a structure designed around one language's grammar becomes the container every other language has to fit into. Plurals are just the most legible instance — the same thing happens with date formats, name order, honorifics and text expansion.&lt;/p&gt;

&lt;p&gt;The fix is cheap if you do it before translation and expensive afterwards, because by then the wrong shape is baked into thousands of strings and a translation memory that will faithfully reproduce it.&lt;/p&gt;

&lt;p&gt;If you are about to localize something, spend an afternoon on CLDR plural rules first. It is the highest-return afternoon in the whole project.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build and operate multilingual mobile applications, shipped across ten languages. More at &lt;a href="https://akmalu.com" rel="noopener noreferrer"&gt;akmalu.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>i18n</category>
      <category>flutter</category>
      <category>localization</category>
      <category>dart</category>
    </item>
  </channel>
</rss>
