How is bidirectional text handled, why should you isolate user-generated strings, and how do different locales write numbers?

24.3.2 · L3 · Interview

Predict first

What do the language directions, the number strings for four locales and an isolated Hebrew word report?

import Foundation

let ar = Locale.Language(identifier: "ar"), fa = Locale.Language(identifier: "fa"), en = Locale.Language(identifier: "en")
print(ar.characterDirection == .rightToLeft, fa.characterDirection == .rightToLeft, en.characterDirection == .leftToRight)
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
for id in ["en_US", "de_DE", "hi_IN", "ar_EG"] {
    formatter.locale = Locale(identifier: id)
    print(id, formatter.string(from: 1234567.5)!)
}
let isolated = "\u{2068}" + "שלום" + "\u{2069}"
print(isolated.unicodeScalars.count, "Order #12: \(isolated) paid".contains(isolated))
  • true true true; en_US 1,234,567.5; de_DE 1.234.567,5; hi_IN 12,34,567.5; ar_EG ١٬٢٣٤٬٥٦٧٫٥; then 6 true
  • All locales print 1,234,567.5
  • Only Arabic prints different digits

Mental model. A sentence in two languages is like a road that changes sides at a border. Isolating the foreign phrase is putting a fence around the part that drives on the other side.

Real text mixes directions: an Arabic or Hebrew sentence containing an English brand name, a number or a URL. The Unicode bidirectional algorithm decides how runs of text are displayed, and it can reorder things surprisingly: punctuation, parentheses and numbers next to opposite-direction text can jump to the wrong side. The system handles most of this when you let it, with a few rules. Align text with natural alignment, set a text view’s writing direction from the content, and keep the language of the surrounding string as the base direction. When you insert user-generated or unknown-direction text into a localised sentence (“Order #12: ⟨name⟩ paid”), wrap it in Unicode isolate characters (first-strong isolate U+2068 and pop U+2069, as in the check below) so a Hebrew name does not reorder the text around it; use .localizedStringWithFormat or String(localized:) with placeholders so translators can reorder the sentence (Q110). Ask the system for a language’s direction with Locale.Language.characterDirection rather than a hard-coded list of languages: the check shows Arabic and Persian are right-to-left and English is left-to-right.

Numbers and digits are locale-specific too. The same value, 1234567.5, is written 1,234,567.5 in en_US, 1.234.567,5 in de_DE, 12,34,567.5 in hi_IN (Indian digit grouping) and with Arabic-Indic digits in ar_EG, as the check prints. So never build number strings by concatenation or String(format:) for display, never parse user input assuming a dot decimal separator, and format currency, percent and units with NumberFormatter or FormatStyle (Q83) using the user’s locale. Digits inside RTL text still read left to right. Keep identifiers, order numbers and codes in a form that does not depend on the display locale, and keep code, file paths and technical text left to right inside RTL layouts. Testing with a real RTL language (and long names) is the only way to see these issues, because the pseudolanguage mirrors the layout but not the text behaviour.

Separators, grouping and digits are all locale data; direction comes from the language.

Common traps

  • Concatenating user text into a localised sentence without isolation. A name in the opposite direction can scramble the punctuation around it.
  • Building display numbers with string formatting. Separators, grouping and even the digits differ by locale; use formatters.

They might follow up with

  • How do you know a language is right to left? Ask Locale.Language(...).characterDirection.
  • Where do translators reorder words? In the format string’s placeholders, so the app need not know the order.

Related questions

  • 24.3.1 How do you support right-to-left languages: leading and trailing, semantic content attributes, flipping…
  • 24.4.2 What changes by locale beyond strings: sorting and searching, the first day of the week, calendars, names and…
  • 24.4.1 How do you test internationalisation before translations exist: pseudolanguages, launch arguments, text…
  • 2.6.4 How do you handle plurals and word order in localised strings?
  • 2.2.7 FormatStyle or DateFormatter: which do you reach for?

← How do you support right-to-left languages: leading and trailing…