Join early access

Software localization is not a translation project

The word covers four things a build settles before a translator sees a single string: the identifier it resolves, the grammar it computes at runtime, the space it gives the text, and the file the exchange travels in. Only one of the four is something anybody can sell you.

A tall panelled door held shut by four fasteners, the topmost a lock with the key turned and its bolt drawn back, the three barrel bolts below it still shot across into the frame, drawn in white line on a slate ground

What the word covers, and what internationalization settled first

Software localization is the adaptation of a product to the language, cultural and other requirements of a specific target market, which the W3C calls a locale 1. The same page notes what the word is usually taken to mean instead: it is “often thought of only as a synonym for translation of the user interface and documentation” 1.

The list of what it can entail makes the gap plain. Numeric, date and time formats. Currency. Keyboard layout and usage. Collation and sorting. Symbols, icons and colours. Text and graphics with cultural references. Varying legal requirements. Address and contact information formats. Script-specific readability requirements 1.

Nine items, and translating the strings is not one of them. That is not an oversight. Translation is the part that needs no explaining, and everything on the list is a decision your build makes without ever consulting a linguist.

The other half of the pair is internationalization, the design work that makes localization possible at all: removing barriers, supporting features that will be needed later, letting code follow local preferences, and separating localizable elements from source code 1. Both words get written as numeronyms, l10n and i18n, for the count of letters between the first and the last 1.

The division between them is not who does the work. It is when the decision was made. Internationalization is every choice fixed before any locale exists, and localization is what those choices left available. A product that skipped the first one does not have a translation problem later. It has a smaller set of things that can be said at all.

What follows is the four places that set gets decided. One of them is the part vendors sell.

A locale is an identifier before it is a language

Before anything is translated, something has to name the locale. The standard for that is BCP 47, currently RFC 5646, and it is stricter than the strings people paste into config files 2.

A tag is an ordered sequence: language, then an optional script, then an optional region, then variants, extensions and private use 2. There must be at most one script subtag and at most one region subtag, and every subtag valid anywhere in a tag comes from the IANA Language Subtag Registry rather than from whatever the ISO list seemed to say 2.

Two of those slots get confused constantly. zh-Hans names a script, Simplified Chinese. zh-CN names a region, mainland China. They are different facts about a string, they are not interchangeable, and the W3C’s rule for which to use is narrow: add a subtag only when it distinguishes the content from something else in the context where it is used, and otherwise keep the tag as short as possible 3. A build that resolves zh-Hant-TW by string equality against a list containing zh-TW fails, and it fails identically whatever the translation quality is.

The clearest illustration of the identifier being real data belongs to Microsoft. Pseudolocalization needs a locale to live in, and Windows originally used tk-TM, Turkmen as spoken in Turkmenistan: a well-formed tag that Windows was not shipping at the time. A later release shipped it as a real locale. The collision “caused many bugs, especially in the build and testing systems that assumed tk-TM was the pseudo-locale” 4.

The lesson they drew from it is the one worth keeping: use names that are well formed but will never describe a real language or locale 4. Windows pseudo locales now sit under qps, inside the range BCP 47 reserves for private use, and ICU uses en-XA and en-XB 4.

A locale identifier is a join key. Formatting, collation, plural selection, currency, calendar and text direction are all lookups on it. Get the key wrong and every one of those lookups returns a confident answer for the wrong place.

The grammar is computed at runtime instead of being chosen by a translator

The second decision is what happens to a sentence when a number goes into it. CLDR sorts every language into at most six plural categories: zero, one, two, few, many and other 5. A category is not a count. It is a rule the runtime evaluates against the value, and the rules are different per language.

LanguageCardinal categoriesCount
Arabiczero, one, two, few, many, other6
Polishone, few, many, other4
Russianone, few, many, other4
Englishone, other2
Japaneseother1
Cardinal plural categories per language, from the CLDR 47 plural rules chart. Japanese needs one form. Arabic needs all six.

This is where the standard example gets used, and the standard example undersells it. The usual line is that count === 1 ? "1 item" : count + " items" breaks in Polish, it does, but it also breaks in English, because English has two cardinal categories and four ordinal ones: one, two, few and other 5. That is 1st, 2nd, 3rd, 4th. The ternary was already wrong in the source language.

The industry answer is to move the selection out of the code and into the message, where a translator can supply the variants their language actually needs. That mechanism now has a stable specification: in March 2025 “the MessageFormat 2.0 specification has advanced from Final Candidate to Stable”, released in CLDR 47 and integrated into ICU 77 6.

text
.input {$count :number}.match $countone  {{You have {$count} unread message}}*    {{You have {$count} unread messages}}
One message, in the shape the runtime selects from. The developer writes the frame. The number of branches is a property of the target language, not of the code.

The important part is not the syntax. It is that the count of branches is not knowable when the string is written. An English source has two. Its Arabic target has six, and the six are not a stylistic preference a reviewer can wave through. A message format that only carries one alternative has destroyed the information before any translator sees it, which is the sense in which a localized string is structured data rather than prose.

The space the text gets is part of the translation

The third decision is physical. Translated text is a different length, and the expansion is not uniform. The table everyone quotes comes from IBM by way of the W3C 7.

Characters in EnglishAverage expansion
Up to 10200 to 300%
11 to 20180 to 200%
21 to 30160 to 180%
31 to 50140 to 160%
51 to 70151 to 170%
Over 70130%
IBM's average expansion rates for English into European languages, as published by the W3C. The figures are the translated length as a percentage of the source, so a ten-character label can arrive three times as wide.
The 51 to 70 row is reproduced exactly as the W3C prints it, and it is out of sequence with the rows on either side of it. Every other step descends. Treat that one as the typo it looks like rather than as a finding, and use the shape of the column instead of any single cell.

The top row against the bottom one is the whole finding. The shortest strings grow the most, and the W3C states the consequence plainly: “the smaller the English text, the more likely it is to be squeezed into a small space, such as alongside a form entry field, or inside a graphic, or a set of width restricted tabs” 7. The strings that expand worst are the ones you put in the tightest furniture. Buttons. Tabs. Column headers. Labels beside inputs.

Microsoft’s independent guidance lands in the same place from the other direction: a 40% lengthening is a good heuristic for simulating translation from English, and in practice a string can run 200% or even 400% longer, with one and two-word strings growing proportionally more than long ones 4.

None of that needs a translation to exist. It is checkable the day the string is written, which is what pseudolocalization is for. A pseudotranslation pass replaces characters with accented and cross-script equivalents, pads the string to its expected expanded length, wraps it in delimiters so a truncation shows up as a missing bracket, and can carry a unique identifier back to the resource it came from 4.

It has one more property that is easy to miss and worth more than the rest: “Resources that haven’t been exposed to localization are readily apparent because they won’t be pseudo-translated” 4. A hardcoded string is the one thing on the screen still in plain English. That is a hardcoded-string detector that costs one build and reads the running product rather than the source, which is exactly the gap a static scan leaves open when it tries to decide which strings were ever translatable.

Direction is the same kind of problem. For right-to-left languages the interface itself mirrors, including control handles, pull-down buttons and the side the scroll bar sits on 8. What does not mirror is the part nobody plans for. Media playback controls stay as they are, because they “refer to the direction of the media being played, not the direction of time”. Clocks still turn clockwise, so a clock face or a clockwise refresh arrow is left alone. Numbers, phone numbers, physical objects like keyboards, and images containing slashes all stay put 9.

Every one of those is a rule about your assets and your layout engine. A translator can tell you the sentence reads right. Nobody in the translation pipeline can tell you the button still fits.

The file it travels in is where the contract lives

The fourth decision is the handoff. The standard for it is XLIFF, an OASIS Standard since 13 February 2018 at version 2.1, whose stated purpose is “to store localizable data and carry it from one step of the localization process to the other, while allowing interoperability between and among tools” 10.

The structure is worth knowing precisely, because guides get it wrong. In XLIFF 2.x the nesting runs xliff, file, unit, segment, and the segment holds exactly one source and optionally one target 10. The trans-unit element you will see in most explanations of “XLIFF 2” is from 1.2, a different decade of the format. If an article describes the current version using the previous version’s elements, it has not opened either spec.

What makes the format interesting here is not the core. It is the optional modules 2.1 defines around it: translation candidates, glossary, format style, metadata, resource data, change tracking, size and length restriction, validation, and ITS 10.

The seventh entry is the one that matters here. The interchange format has a place to record that a target must fit inside a maximum length. The mechanism exists, it is standardised, and every serious tool can read it. And the number in it can only come from one place, which is the build that owns the button. The format will carry your constraint to the translator. It cannot measure it for you, and a constraint nobody wrote down arrives as no constraint at all.

That is the whole argument in one attribute. The pipeline is a transport for facts your product already knows, and it is silent about the facts your product never recorded.

What is actually being bought

Four decisions, then. The identifier that names the locale. The grammar the runtime computes from a number. The space the layout gives the text. The file the exchange travels in. All four are settled inside the build, before a string is sent anywhere, and none of them improves when the translation improves.

This is why the industry’s framing of localization as a purchase is so slippery. What is on sale is throughput over a surface: words moved, matched, reviewed and returned, priced by what the meter counts as changed. That is a real service and it is worth paying for. It is also defined entirely by the surface you handed over. Buying more of it against a badly internationalized catalog buys you the same defects, faster, in more languages.

The practical consequence is an ordering. Fix the identifier, because everything else is a lookup on it. Move plural and gender selection into the message, because a branch that does not exist cannot be restored downstream. Pseudolocalize, because it prices the layout work and finds the hardcoded strings before anyone is paid per word. Record the constraints in the file, because the format has a slot for them and an empty slot is a promise nobody made. Then buy translation.

None of this makes the language work easy. A string can clear all four gates and still be fluent and wrong, and deciding which locales are worth having at all is a separate question with worse evidence behind it than anyone admits. But those are arguments about what to say and where. The four above are about whether your product can say it, and they are answered in your repository, months before the first invoice.

References

  1. 1.W3C Internationalization Working Group Localization vs. internationalization w3.org/International, Questions and answers
  2. 2.Phillips and Davis, 2009 Tags for Identifying Languages BCP 47, RFC 5646
  3. 3.W3C Internationalization Working Group Choosing a language tag w3.org/International, Questions and answers
  4. 4.Microsoft, 2022 Pseudolocalization Microsoft Learn, globalization methodology
  5. 5.Unicode CLDR Language Plural Rules CLDR 47 supplemental charts
  6. 6.Unicode, 2025 Unicode CLDR 47 Release: MessageFormat 2.0 Stable The Unicode Blog, 13 March 2025
  7. 7.W3C Internationalization Working Group Text size in translation w3.org/International, articles, citing IBM
  8. 8.Microsoft, 2024 Mirroring Microsoft Learn, globalization
  9. 9.Material Design Bidirectionality material.io, archived guidelines
  10. 10.OASIS XLIFF Technical Committee, 2018 XLIFF Version 2.1 OASIS Standard, 13 February 2018