Skip to content
Bugwise
Menu
Features & Flows

Localization and RTL

How apps adapt to other languages, scripts and regions, from plurals and prices to right-to-left layouts, what usually breaks, and how to test it on mobile and web.

40 min intermediate Mobile & web

A food delivery app launches in Germany. On day one, support gets screenshots: the Order button says “Zahlungspflichtig bes…”, the delivery time reads “2:30 PM” (Germans expect 14:30), and the total shows “1,299.00 €” where a German would write “1.299,00 €”. Nothing crashed. Every feature “works”. But to a German customer the app looks cheap, foreign and a little untrustworthy, and some of them won’t trust it with their card.

That’s what this lesson is about. Supporting another language is much more than translating words. Dates, numbers, plurals, names, sorting and even the direction of the whole screen change. Most of these bugs are invisible if you only ever test in English, and very easy to find once you know where to look.

How it works

Two words you’ll hear a lot, often shortened to numbers (the count of letters between the first and last one):

TermShortWhat it means
internationalization (i18n)Internationalization (i18n)Building a product so it can be adapted to any language or region without code changes.Open in glossary →i18nBuilding the app so it can support any language: no hard-coded text, flexible layouts, Unicode everywhere, locale-aware formatting. Done once, by developers.
localizationLocalizationAdapting an app to a language and region, including translations, dates, currency and layout.Open in glossary →l10nActually adapting it to one market: translations, local formats, currency, images, legal texts. Done per language.

A localeLocaleRegional settings that control language and formats for dates, numbers and currency.Open in glossary → is the combination of language and region, written like de-DE (German in Germany), de-AT (German in Austria), sr-Latn-RS (Serbian in Latin script, Serbia) or ar-AE (Arabic, United Arab Emirates). The same language can need different formats in different regions, and Serbian is written in two scripts: Latin (Korpa) and Cyrillic (Корпа).

Here’s what happens when someone opens a localized app:

  1. The app picks a locale. From the phone or browser language, from an in-app language setting, or from the user’s account. If the user’s language isn’t supported, it falls back to a default (usually English).
  2. Text comes from resource files, by key. The code doesn’t contain “Place order”. It asks for checkout.cta, and the file for the current language returns “Zahlungspflichtig bestellen” or “Наручи”.
  3. Numbers, dates and prices go through formatters. The app passes the raw value (1299, a date, RSD) to a locale-aware formatter that knows the separators, the order and the symbols.
  4. Plurals and variables use message patterns. Instead of gluing pieces together, the app gives the translator a full sentence with placeholders and plural rules.
  5. The layout adapts. Text wraps or the screen scrolls when strings get longer. For right-to-left languages the whole layout is mirrored.
  6. The server speaks the same language. Error messages, emails, push notifications, receipts and product data come from the backend, so it must know the user’s locale too.

What changes from one locale to another

Much more than the words. Every row in this table is a classic source of bugs:

WhatExamples
TextTranslations, but also gender: Serbian says Dobro došao to a man and Dobro došla to a woman.
PluralsEnglish: 1 item, 2 items. Serbian: 1 artikal, 2 artikla, 5 artikala. Arabic has six forms.
Dates and times10/16/2026 (US) vs 16.10.2026 (Germany, Serbia); 2:30 PM vs 14:30; some regions also use other calendars (Thailand’s Buddhist year is 543 years ahead).
First day of the weekSunday in the US, Monday in most of Europe, Saturday in several Middle Eastern countries. Watch the date picker.
Numbers1,299.50 (US), 1.299,50 (Germany, Serbia), 1 299,50 (France), 1’299.50 (Switzerland). Arabic may use Arabic-Indic digits: ١٬٢٩٩٫٥٠.
CurrencySymbol before or after, with or without a space: $1,299.00, 1.299,00 €, 13.990,00 RSD. Not every currency has 2 decimals (the yen has none).
UnitsKilometers or miles, kg or lb, °C or °F.
AddressesPostcode before the city (11000 Beograd) or after (New York, NY 10001); some countries don’t use postcodes at all.
Phone numbers(415) 555-0132 vs +381 64 123 4567. Different lengths, so don’t hard-code a mask.
NamesFamily name first (Hungarian, Chinese, Japanese), two surnames (Spanish), or a single name. “First name / Last name” doesn’t fit everyone.
SortingIn Serbian, Čačak comes after Cetinje, not after Zrenjanin. In Swedish, å, ä and ö come after z.
Time zones“Delivery at 14:00” must mean the customer’s time zoneTime zoneThe local time of a region. Bugs appear when apps mix up the user's time zone with the server's.Open in glossary →, not the server’s.

Text expansion

English is short. When text is translated, it usually grows:

  • German and Finnish are often 30–50% longer than English, and a single compound word can be very long (Geräuschunterdrückung means “noise canceling”).
  • Short labels grow the most: “Save” (4 letters) becomes “Speichern” (9). A button label can easily double.
  • Some languages are shorter but taller: Thai and Hindi have marks above and below the letters, so a fixed line height cuts them off.
  • Chinese and Japanese are short in characters, but each character needs to be big enough to read.

Anything with a fixed width breaks first: buttons, tabs, segmented controlsSegmented controlA row of connected buttons where exactly one option is selected at a time.Open in glossary →, table headers, badges, and text inside images. Typical symptoms are truncationTruncationText that gets cut off because it doesn't fit its space.Open in glossary → (“Zahlungspflichtig bes…”), text overflowing its box, or two lines squashed into one.

Pseudo-localization: test translations before they exist

Real translations often arrive late, a day before release. Pseudo-localization lets you test the layout now. A tool replaces every translatable string with a fake, still readable version:

Place order   →   [Þļåçé öŕðéŕ one two]
  • Accents check that fonts and encoding handle non-English letters.
  • Extra length (about 30–50%) simulates German and shows what gets cut off.
  • Brackets show where each string starts and ends. A missing ] means the text is truncated.
  • Text without accents wasn’t translated at all: it’s hard-coded in the code (or comes from the server).

Android has two built-in pseudo-locales: en-XA (accented and longer) and ar-XB (right-to-left). Xcode offers pseudolanguages such as Double-Length, Accented and Right-to-Left. You’ll try both styles in the lab below.

Plurals: why “item(s)” is a bug

English has two forms (1 item, 2 items), so developers often write:

"You have " + n + " item(s)"            // lazy and ungrammatical
n === 1 ? "1 item" : n + " items"       // works only for English-like languages

Other languages have different rules:

LanguageForms for whole numbersExample
English, German21 item · 2 items
Serbian31 artikal · 2 artikla · 5 artikala · 21 artikal · 22 artikla
Polish, Russian3Polish: 1 plik · 2 pliki · 5 plików
Arabic6Separate forms for 0, 1, 2, 3–10, 11–99 and 100–102; bigger numbers follow their last two digits (103 is like 3)
Chinese, Japanese1The noun doesn’t change

The fix is a plural-aware message format (most apps use ICU MessageFormat or the platform’s plural resources), where the translator writes every form the language needs:

{count, plural, one {# artikal u korpi} few {# artikla u korpi} other {# artikala u korpi}}

Notice “21 artikal”: in Serbian, numbers ending in 1 (except 11) use the singular, and numbers ending in 2–4 (except 12–14) use the “few” form. Testers should try 0, 1, 2, 5, 11, 12, 21, 22 and 101, not just 1 and 2.

Concatenation: sentences glued from pieces

Building a sentence from fragments breaks as soon as another language orders words differently:

Glued from pieces

"Arrives " + weekday + ", " + month + " " + day

English: Arrives Friday, October 16. German: Kommt Freitag, Oktober 16. Wrong order, and the translator only ever saw “Arrives ” with no context.

One message with placeholders

"Arrives {date}" with a locale-aware date formatter.

German: Lieferung am Freitag, 16. Oktober. The translator sees the full sentence and can move the placeholder wherever the grammar needs it.

Missing translations and fallbacks

New features often ship before every translation is in. What the user sees depends on the fallback:

  • Good: the missing string falls back to a sensible language (Serbian Cyrillic → Serbian Latin → English, if the team agreed on that order) and someone gets notified.
  • Bad: the screen shows the raw key, like checkout.title or cart.free_delivery_note, or an empty space.
  • Also bad: a Cyrillic screen with one Latin sentence in the middle, or half the screen in English after an update.

Right-to-left (RTL)

Arabic, Hebrew, Persian and Urdu are read from right to left. An Right-to-left (RTL)Right-to-left (RTL)Languages like Arabic and Hebrew that read right to left; layouts and controls must be mirrored.Open in glossary → layout isn’t just right-aligned text, the whole screen is mirrored: the back button moves to the top right, the list chevron points left, text fields start on the right, progress bars fill from right to left, and swipe directions flip.

But not everything flips. Icons that show a direction in reading order mirror. Icons that show something real-world never do:

Mirror in RTL

  • Back arrowPoints to where you came from, which is now on the right.
  • List chevronPoints forward, into the next screen.
  • SendShows the message moving in the reading direction.
  • Undo / redoThey point backward and forward in time, which follows reading direction.

Never mirror

  • Play, fast-forwardMedia controls follow tape direction, the same worldwide.
  • CheckmarkA symbol, not a direction.
  • ClockClock hands turn the same way everywhere.
  • Download, uploadVertical arrows have no left or right.

Also never mirrored: logos, brand marks, photos, and text inside images (those need translated versions instead).

Bidirectional (bidi) text is the tricky part. Numbers, order IDs, URLs, email addresses and English brand names stay left-to-right inside an Arabic sentence. Without proper isolation, text like Order #A-2048 or a phone number +381 64 123 4567 can come out scrambled, with the # or the + on the wrong end. Look closely at punctuation, prices and anything that mixes the two directions.

Try it: the localization lab

The same cart screen, correctly localized. Switch languages, change the quantity and try the small phone. Watch what changes: words, length, plural forms, separators, the date and, in Arabic and ar-XB, the direction of the whole layout. This is your “expected result” for the bug hunt further down.

9:41

Warenkorb

Kopfhörer mit Geräuschunterdrückung

Mitternachtsschwarz

2

2 Artikel im Warenkorb

Lieferung

Fr., 16. Okt. · 14:30

Kostenlose Lieferung für diese Bestellung

Bestellung#A-2048
Gesamtsumme239,80 €

App language

Quantity (try different plural forms)

Direction
Left-to-right
Script
Latin
Plural form
"other" for 2. This language has 2 forms: one, other
Price
239,80 €
Date and time
Fr., 16. Okt. · 14:30
Button text vs English
+145% characters

German is long: the product name wraps to more lines and the button text is more than twice as long as in English. The layout grows instead of cutting text.

In-app language vs device language

Many apps let users pick a language inside the app, and Android 13+ and iOS also offer a per-app language in system settings. That creates extra combinations to test:

  • System parts stay in the device language: permission dialogs, the share sheet, the keyboard, the store’s purchase sheet. That’s expected.
  • Server texts must follow the app’s language: error messages, emails, push notifications, receipts and product descriptions. If the server doesn’t know the user’s chosen language, they arrive in the wrong one.
  • Switching while the app is open should update every screen, including screens further back in the navigation stack, without losing the cart or form data.
  • Formats may follow the region rather than the language. Check what the spec says.

Example requirements

Criterion 8 links to another feature. Search and sorting by language is covered in Search, filters and sorting (see diacriticsDiacriticsAccent marks on letters, like é, ü, č or ž. Search usually should ignore them.Open in glossary →).

What usually goes wrong

#BugWhat you seeTypical severity
1Truncated or overflowing text“Zahlungspflichtig bes…”, labels on top of each other, buttons taller than the barMedium (High on a buy button)
2Hard-coded stringsEnglish words in an otherwise translated screenMedium
3Raw keys or empty textcheckout.title, {{name}}, or a blank buttonHigh
4Wrong plurals“1 items”, “2 artikala”, “21 artikala”Low–Medium
5Concatenated sentencesWrong word order, a lone untranslated fragmentMedium
6Wrong number or currency format1,299.00 RSD instead of 1.299,00 RSD, or € swapped for $ without convertingHigh (money)
7Wrong date or time format10/11 read as 10 November instead of 11 October; 12-hour clock in GermanyMedium–High
8RTL not mirrored, or mirrored wronglyBack arrow pointing into the screen, play button flipped, logo mirroredMedium
9Broken bidi textPhone numbers, prices or order IDs scrambled inside Arabic textMedium–High
10Clipped or missing glyphsThai or Hindi marks cut off at the top or bottom, empty boxes (□) instead of lettersMedium
11Mixed languages from the serverScreen in Serbian, error message or push notification in EnglishMedium
12Language switch doesn’t stickApp goes back to English after a restart or an updateMedium

Find the bug

ShopNest is launching in Germany, Serbia and the UAE. Its cart screen below has six localization bugs. You don’t need to speak these languages: use the lab above and the spec under the phone as your test oracleTest oracleAny source you use to decide whether a result is right or wrong.Open in glossary →.

  • Switch through every language in Settings › App language and compare with the lab.
  • Change the quantity with − and +. Try 2, 3 and 5 in Serbian.
  • Read the delivery date, the total and the button carefully in German and Serbian.
  • In Arabic, look at every icon. Which way should each one point?

🐞 Bug hunt: ShopNest cart in five languages

This demo has 6 hidden bugs. Explore it like a tester, then check your findings.

Found0/6
9:41

Cart

Noise-canceling headphones

Midnight black

1

1 item in your cart

Delivery

Friday, October 16 · 2:30 PM

Free delivery on this order

Order#A-2048
Total$129.90

Settings › App language

From the localization spec

  • Dates, times and prices follow the language's conventions (German and Serbian: day before month, 24-hour clock, 1.234,56).
  • Serbian item counts: 1 artikal · 2–4 artikla · 5–20 artikala · 21 artikal · 22 artikla.
  • Arabic is right-to-left: the screen is mirrored, direction icons flip, media icons never do.
  • No text may be cut off or left untranslated in any language.
  1. 1

    Hint: Switch to Deutsch and read the blue button.

  2. 2

    Hint: Switch to Srpski and set the quantity to 2, 3 or 4.

  3. 3

    Hint: Switch to العربية and compare the back arrow and the play button with the lab.

  4. 4

    Hint: Compare the delivery line in English and Deutsch.

  5. 5

    Hint: Switch to Српски (ћирилица) and read every line.

  6. 6

    Hint: Look at the total in Srpski, then in Deutsch.

Always include the language, script and region in a localization bug report, plus a screenshot. Developers often can’t read the language, so describe what’s wrong in English too. More tips in Writing bug reports.

Test scenarios

You won’t test every screen in every language by hand. That’s a combinatorial explosionCombinatorial explosionThe rapid growth in the number of combinations as conditions are added.Open in glossary →. A practical strategy: run the whole app once in a pseudo-locale (catches hard-coded text and truncation everywhere), then test the critical journeys (sign up, search, cart, checkout, receipts) in each real language, and add a native speaker review for wording.

Functional

  • The app starts in the device language if it’s supported, and falls back to the agreed default if it isn’t (try a language the app doesn’t support, like Finnish).
  • The in-app language setting changes every screen, and the choice survives a restart and an app update.
  • Per-app language from system settings (Android 13+, iOS) is respected.
  • Server content follows the app language: error messages, product data, emails, push notifications, PDF receipts.
  • Search and sorting work with local letters (č, ć, đ, ß, ä) and with or without diacritics.
  • Users can type and save names and addresses in their script (Cyrillic, Arabic, Chinese), and they show up correctly everywhere, including on other devices and in emails (no garbled text like ÄŒaÄak).

Text and layout

  • No truncation, overlapping or clipped text in German, Finnish or a pseudo-locale with expansion.
  • Buttons, tabs and badges grow or wrap instead of cutting text. Check the small phone and the largest text size (Dynamic TypeDynamic TypeThe phone setting that lets users make text bigger or smaller across apps.Open in glossary →) together.
  • No hard-coded strings: in the pseudo-locale, every piece of text is accented and bracketed.
  • No raw keys, placeholders like {name}, or empty labels.
  • Fonts render every script: no □ boxes, Thai and Hindi marks are not clipped, line height is comfortable.
  • Text inside images and videos is localized too (or avoided).

Formats

  • Plurals for 0, 1, 2, 5, 11, 12, 21, 22, 101 in each language.
  • Prices: separators, symbol position, decimals and the correct currency amount.
  • Dates, times (12/24-hour), time zones and the first day of the week in the date picker.
  • Phone numbers, postcodes and addresses accept local formats (no US-only masks).
  • Units: km or miles, kg or lb, °C or °F, as the spec says.

Right-to-left

  • The whole layout is mirrored: navigation, lists, forms, tab order, progress bars, carousels and swipe gestures.
  • Direction icons flip; play buttons, checkmarks, clocks, logos and photos don’t.
  • Mixed text reads correctly: prices, order numbers, phone numbers, emails and URLs inside Arabic or Hebrew sentences.
  • Text fields: typing Arabic starts on the right; typing an email address in an RTL screen still works and reads left-to-right.
  • Charts and timelines: check with the spec whether time flows right-to-left or stays left-to-right.

Edge cases

  • Switch language in the middle of a flow (cart full, form half-filled). Nothing is lost, every screen updates.
  • Device language is unsupported, or a regional variant (de-CH, en-IN, sr-ME).
  • Language English with region Germany (formats vs words).
  • Very long names and addresses (German street names, Arabic names with several parts).
  • Translations updated after release (if the app downloads strings from a server): what does an old app version show?

Accessibility

  • Screen readers use the right voice: on the web, the page has a correct lang attribute (and dir="rtl" for RTL); in apps, VoiceOver and TalkBack read the text in the app language.
  • Content descriptions and accessibility labels are translated too, not only visible text.
  • Large text plus a long language is the worst case for layout: test them together.
  • Reading and focus order follows the language direction (right to left in Arabic).

Example test cases

TC-L10N-001Pseudo-locale reveals no hard-coded or truncated text

Preconditions

  • Debug build with pseudo-locales enabled
  • Developer options on (Android)

Steps

  1. Set the device language to English (XA)
  2. Open every main screen: Home, Search, Product, Cart, Checkout, Profile
  3. Open every dialog and error message you can trigger

Expected result

All text is accented and wrapped in [brackets]. No plain English text (hard-coded), no missing ] (truncated), no overlapping elements.

TC-L10N-002Serbian plural forms in the cart

Preconditions

  • App language: Srpski (latinica)

Steps

  1. Add 1 item to the cart
  2. Increase the quantity to 2, 5, 11, 21 and 22, reading the summary each time

Test data

Quantities: 1, 2, 5, 11, 21, 22

Expected result

1 artikal · 2 artikla · 5 artikala · 11 artikala · 21 artikal · 22 artikla.

TC-L10N-003Prices and dates follow the German locale

Preconditions

  • Device language Deutsch (Deutschland)
  • Cart contains items worth more than 1,000 euros

Steps

  1. Open the cart
  2. Read the total and the delivery slot
  3. Open the delivery date picker

Expected result

Total like "1.199,00 €"; delivery like "Freitag, 16. Oktober · 14:30"; the calendar week starts on Monday.

TC-L10N-004German checkout button shows its full text

Preconditions

  • App language Deutsch
  • Smallest supported phone and largest text size

Steps

  1. Open the cart
  2. Read the checkout button

Expected result

"Zahlungspflichtig bestellen" is fully visible (wrapped or in a taller button), still tappable, and not covered by other elements.

TC-L10N-005Arabic layout and icons are mirrored correctly

Preconditions

  • App language العربية

Steps

  1. Open the product screen and the cart
  2. Check the back button, list chevrons and quantity stepper
  3. Check the video play button and the logo
  4. Swipe back

Expected result

Back button is on the right and points right; chevrons point left; play icon and logo are not mirrored; swipe back works from the right edge (iOS).

TC-L10N-006Mixed-direction text reads correctly in Arabic

Preconditions

  • App language العربية
  • An order with number A-2048 exists

Steps

  1. Open the order details
  2. Read the order number, price, support phone number and email address

Test data

Phone: +381 11 555 0199 / email: help@shopnest.example

Expected result

Each value reads in the correct order (e.g. "#A-2048", "+381 11 555 0199") and punctuation stays on the correct side.

TC-L10N-007Changing the language mid-flow keeps the data

Preconditions

  • Cart with 3 items
  • Checkout address form half filled

Steps

  1. Go to Settings › Language and switch from English to Srpski (ћирилица)
  2. Return to checkout

Expected result

All screens, including the ones further back, are in Cyrillic Serbian. Cart and form data are unchanged. No Latin text or raw keys.

TC-L10N-008Server messages follow the app language

Preconditions

  • Device in English, app language set to Deutsch

Steps

  1. Enter an invalid coupon code
  2. Place an order
  3. Check the order email and push notification

Expected result

The coupon error, order email and push notification are all in German.

Tip: click a test case to expand it.

Mobile vs web

  • Device language: iOS: Settings › General › Language & Region. Android: Settings › System › Languages. Also change the region separately to see which formats follow it.
  • Per-app language: on Android 13+, Settings › System › Languages › App languages (the app must declare its supported languages). On iOS, the app’s page in Settings has a Language option when the app supports more than one language. Quicker than switching the whole phone.
  • Pseudo-locales on Android: in a debug build with pseudoLocalesEnabled true, turn on Developer options and add English (XA) or Arabic (XB) in the language list.
  • Pseudolanguages on iOS: in Xcode, Edit Scheme › Run › Options › App Language offers Double-Length, Accented, Right-to-Left and other pseudolanguages, plus a “Show non-localized strings” option. Simulators make switching languages fast (see Simulators and emulators).
  • RTL gestures: swipe-to-go-back starts from the right edge in RTL on iOS; carousels and swipe actions in lists should flip too.
  • System UI (permission dialogs, the store sheet, the keyboard) follows the device language. That’s normal; mention it in reports only if the spec says otherwise.
  • Push notifications are built on the server: check they arrive in the user’s chosen language, also when the app is closed (see Push notifications).
  • Install a different keyboard (Serbian Cyrillic, Arabic) to type real data into fields and search.
  • Browser language: Chrome: Settings › Languages (move a language to the top). Firefox: Settings › General › Language. The browser sends it in the Accept-Language request header, which you can see in the DevToolsDevToolsDeveloper tools built into browsers for inspecting pages, network traffic and errors.Open in glossary → Network tab.
  • Locale override in DevTools: Chrome’s Sensors panel (More tools › Sensors) can override the locale for one tab, which changes how JavaScript formats dates and numbers.
  • Language switcher and URLs: many sites use /de/, /sr-latn/ or ?lang=de. Check that links, redirects, the back button and shared links keep the language, and that switching language keeps you on the same page.
  • lang and dir attributes: inspect the <html> element. It should say lang="ar" dir="rtl" for Arabic. A quick RTL smoke test: add dir="rtl" yourself in the Elements panel and see what breaks.
  • CSS: look for icons and padding stuck on the wrong side.
    Dev note
    Layouts built with left/right margins don’t mirror; logical properties (start/end) do.
  • Responsive + long text: German at a phone width is a classic overflow (see Responsive design testing).
  • Fonts: web fonts often contain only Latin letters. Check Cyrillic, Arabic and accented letters don’t fall back to a mismatched font or show □ boxes.
  • SEO and sharing: each language version has its own title and description; shared links preview in the right language.

Checklist

Localization and RTL checklist

0 of 15 checked

Check your understanding

Quick quiz

0/6 answered · 0 correct

  1. 1.What is the difference between internationalization (i18n) and localization (l10n)?

  2. 2.You run the app in the en-XA pseudo-locale and see the plain text "Place order" on one button, without accents. What does it most likely mean?

  3. 3.Which set of quantities is the best for testing Serbian plurals?

  4. 4.In an Arabic version of your app, which icon should NOT be mirrored?

  5. 5.The total in the Serbian version reads "13,990.00 RSD". What is the bug?

  6. 6.Why is "You have " + n + " item(s)" a localization bug?

Key takeaways

  • i18n makes an app ready for any language; l10n adapts it to one market. Both are full of bugs that are invisible in English.
  • Much more than words changes: plurals, dates, numbers, currency, units, addresses, names, sorting and the first day of the week.
  • Text grows 30–50% in German or Finnish, short labels even more. Use pseudo-localization to find truncation and hard-coded strings before translations exist.
  • Beware of concatenated sentences, English-only plural logic, raw keys and wrong separators in prices.
  • In RTL the whole layout mirrors, but play buttons, logos, checkmarks and clocks don’t. Watch numbers and IDs inside RTL text.
  • Test with the device language, the per-app language, the browser language and the pseudo-locales, and always note language, script and region in your bug reports.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its review cards are now in Bites ⚡
Esc

Type to search the whole site.