Globalization Gotchas

I'm preparing a presentation for the next Unicode conference in March, and have been thinking about doing one on the pitfalls that people stumble into when using Unicode in globalizing software.

I have the following draft list, and would like to collect others. If you have any suggestions for additions or improvements, I would appreciate them. (At this point I'm not worried about grammar, spelling, etc. And these are only bullet items; they would have some patter along with them.)

Newer items are marked in yellow.

Unicode

 
  1. Unicode encodes characters, not glyphs: U+0067 → g g g g g g g g g g...
  2. Unicode does not encode characters by language: French, German, English j have the same code point even though all have different pronunciations; Chinese 大 (da) has the same code point as Japanese 大 (dai).
  3. The word character means different things to different people: glyphs, code points, bytes, code units, or user-perceived characters (grapheme clusters). Say which one you mean.
  4. Some APIs/protocols will count lengths in code points, and others in bytes (or other code units). Make sure you don't mix them up.
  5. Character and block names may be misleading: U+034F COMBINING GRAPHEME JOINER doesn't join graphemes.
    ► http://www.unicode.org/faq/
  6. Never use unassigned code points; those will be used in future versions of Unicode. If you need your own characters, use private use or non-characters; there are plenty!
  7. Be prepared to handle (at least not corrupt!) any incoming code points from U+0000 to U+10FFFF: if your system is running a back-level version of Unicode, you may receive transmitted unassigned code points from later versions.
  8. Watch for "UCS-2" implementations. They use UTF-16 text, but don't support characters over U+10000; they also may accidentally cause isolated surrogates.
  9. Don't limit API parameters to a single character (and definitely not to a single code unit!). What users think of as a single character (e.g. ẍ, ch) may be a sequence in Unicode.
  10. Use U+2060 word joiner instead of U+FEFF for everything but the BOM function.
  11. Use the latest version of Unicode: as well as necessary characters, there are many corrections, and more processes are guaranteed to be stable.
    ► http://unicode.org/standard/stability_policy.html
  12. Avoid using private-use (PUA) characters. If you simply must use them, minimize the opportunity for collision by picking an unusual range.

Character Conversion

text ‎↔ 74 65 78 74 

  1. Length in bytes may not be N * length in characters
  2. One character in charset X may not be one character in Unicode; the ordering may also be different.
  3. UTF-8, UTF-16, and UTF-32 are all Unicode.
  4. Always use "shortest form" UTF-8. (1) It's the Law. (2) It reduces security attacks.
  5. If a protocol allows a choice of charsets, always tag with the correct one. Not all text is correctly tagged with its charset, so character detection may sometimes be necessary. But remember, it's always a guess.
  6. IANA / MIME charset names are ill-defined: vendors often convert same charset different ways. Shift-JIS 0x5C → U+005C or U+00A5: different, unrelated characters with unrelated glyphs.
    ► http://www.w3.org/TR/japanese-xml/
    ► http://icu.sourceforge.net/charts/charset/
  7. When converting, never simply omit characters that cannot be converted; at least substitute U+FFFD (when converting to Unicode) or 0x1A (when converting to bytes) to reduce security problems.

User Input

7: → text 

  1. If you develop your own text editor, use the OS APIs to handle IMEs (Input Method Engines) for Chinese, Japanese, Korean,...
  2. If you are using "type-ahead" to get to a position in a list (eg typing "Jo" gets to the first element starting with those characters), allow arbitrary input. This is often easiest with visible fields.
  3. If your password field can contain characters that require an IME, a screen pop-up box may reveal the password to onlookers.

Text Analysis

 text → i

Segmentation

  1. Combining characters always follow their base:  is  + , not  + .
  2. Words are not just sequences of letters.
    ► http://www.unicode.org/reports/tr29/   

Properties

Use properties such as Alphabetic, not hard-coded lists: isAlphabetic(), \p{Alphabetic} in regex

  1. Some General Category properties aren't what you think: use White_Space (not Zs), Alphabetic (not L), Lowercase (not Ll),...
  2. Generally use Script instead of Block: not all Greek characters are in the Greek block. Many characters (punctuation, symbols, accents) used to write a language are shared (Script=Common or Inherited).
  3. Characters may change property values between versions of Unicode (except for specific ones).
    ► http://unicode.org/standard/stability_policy.html

Identifiers

  1. When designing syntax, don't use characters outside of Pattern_Syntax for syntax characters; don't use characters outside of Pattern_Whitespace for whitespace characters.
  2. For user-visible identifiers, use XID_Start and XID_Continue as a basis. Profiles may expand or narrow from there.
  3. Watch out for security attacks: using visual similarity to slip bogus text (e.g. counterfeit URL’s) past human eyes (spoofing): writing “paypal.com” with a Cyrillic “a” to phish for users’ account information.
    ► http://unicode.org/reports/tr36/

Comparison (Collation): Searching, Sorting, Matching

  1. There are two binary orders: code point/UTF-8/UTF-32 and UTF16 order, where U+10000 < U+E000 (since U+10000 = D800 DC00)
  2. Only use binary order internally; no users expect A < Z < a < z < Ç < ä. Apply normalization to get a unique form, so C◌̧ = Ç.
  3. UCA Order is a far better base than binary to meet user-expectations: a < A < ä < Ç = C◌̧ < z < Z
    ► http://unicode.org/reports/tr10/
  4. Ordering depends on context and language:
  5. Real language-sensitive order requires tailoring on top of UCA
    ► http://unicode.org/cldr/
  6. Don't mix up "stable" and "deterministic" sorting; they are very different.
    ► http://unicode.org/notes/tn9/
  7. Protocols must precisely define the comparison operations: LDAP doesn't specify the comparison operation, so lookup may fail (or falsely succeed!) because of that. Aside from getting the wrong results, this opens up opportunity for security attacks.

Text Transformations

text → TEXT, τεξτ, …

Normalization

  1. The ordering of accents in a normalization form may not be the typical type-in order.
  2. Normalization is context independent; don't assume NFC(x + y) = NFC(x) + NFC(y)
  3. People assume that NFC always composes, but some characters decompose in NFC.
    Here are the current maximal expansion factors for each form (U4.1):
    Operation UTF Factor Sample
    NFC 8 3X 𝅘𝅥𝅮 U+1D160
    16, 32 3X U+FB2C
    NFD 8 3X ΐ U+0390
    16, 32 4X U+1F82
    NFKC / NFKD 8 11X U+FDFA
    16, 32 18X
  4. Trivia: In Unicode 4.0 there are exactly 3 characters that are different in all 4 normalization forms: ϓ, ϔ, ẛ

Case Conversion

  1. As well as Lower and upper case, there is also title case: dz ↔ DZ ↔ Dz
  2. Strings may expand: heiß → HEISS → heiss.
    Here is a table of the maximum possible expansions (U4.1):
    Operation UTF Factor Sample
    Lower 8 1.5X Ⱥ U+023A
    16, 32 1X A U+0041
    Upper / Title / Fold 8, 16, 32 3X ΐ U+0390
  3. Casing is context-dependent: ΌΣΟΣ → όσος
  4. Casing may be language-dependent: istanbul ↔ İSTANBUL.
    (But don't use language-dependent casing for language-independent structures, like file-system B-Trees.) Here is a table that shows the upper and lowercasing behavior of Turkic vs Normal case mappings:

    Case Operations for Dotted and Dotless I
    Uppercase Normal Lowercase Turkic Uppercase
    I
    0049
    i
    0069
    I + ˙
    0049 0307
    İ
    0130
    ı
    0131
    I
    0049
    İ
    0130
    i + ˙
    0069 0307
    İ + ˙
    0130 0307
    I + ˙
    0049 0307
  5. Up to Unicode 5.0 (not yet released), case folding was not stable. (Two versions of Unicode could have different results from toCaseFold(S), even though all the characters in S are in both versions.)
  6. Don't use the Lowercase_Letter (Ll) or Uppercase_Letter (Lt) of  General_Category; these were constrained to be in a partition. Use the separate binary properties Lowercase and Uppercase instead.
  7. There are two different types of lowercase:
    • Lowercase, the binary property. The character is lowercase in form, but not necessarily in function.
    • Functionally Lowercase. isCased(x) & isLowercase(x). See Section 3.13 of TUS.
    Lowercase Functionally
    Lowercase
    (Lowercase
    Letter, Ll)

    Count
    (U4.1)

    Example
    Y N N 114 U+02E0 (ˠ) MODIFIER LETTER SMALL GAMMA
    Y 705 U+00AA (ª) FEMININE ORDINAL INDICATOR
    Y N 43 U+2170 () SMALL ROMAN NUMERAL ONE
    Y 903 U+0061 (a) LATIN SMALL LETTER A
  8. Also three corresponding types of uppercase:
    Uppercase Functionally
    Uppercase
    (Uppercase
    Letter, Lu)

    Count
    (U4.1)

    Example
    Y N N 0 n/a
    Y 476 U+212C () SCRIPT CAPITAL B
    Y N 43 U+2160 () ROMAN NUMERAL ONE
    Y 820 U+0041 (A) LATIN CAPITAL LETTER A
  9. Uppercase and titlecase overlap:
  10. Functionally
    Uppercase
    Functionally
    Titlecase

    Count
    (U4.1)

    Examples
    Y N 4 U+01F1 (DZ) LATIN CAPITAL LETTER DZ
    N Y 31 U+01F2 (Dz) LATIN CAPITAL LETTER D WITH SMALL LETTER Z
    Y Y 858 U+0041 (A) LATIN CAPITAL LETTER A

Transliteration

  1. Transliteration (Ελληνικά ↔ Ellēniká) is not the same as Translation (Ελληνικά ↔ Greek)
    ► http://www.unicode.org/draft/reports/tr35/tr35.html
  2. Transliteration may vary by language: Путин ↔ Putin, Poutine, ...
    Горбачёв ↔ Gorbachev, Gorbacev, Gorbatchev, Gorbačëv, Gorbachov, Gorbatsov, Gorbatschow, ...
  3. Transcription is a lossy transliteration: Ελληνικά → Ellinika → Ελλινικα

Rendering

 text → text → i

  1. Rendering is context-dependent, and dependent on the font: going character-by-character gives the wrong results.
    1. Glyphs may change shape depending on their surroundings:
      ههه
    2. A single glyph may result from multiple characters:
      f i      ل ١ ‎لا
    3. Multiple glyphs may result from a single character:
    4. The memory storage order (logical order) may not be the same as the visual order. Don't assume that contiguous text = contiguous display:
      a b c א ב ג a b c ג ב א
  2. Good rendering systems will handle customary type-in order for text plus canonical order. Excellent ones will do any canonically-equivalent order, but those are rare.
  3. There may be differences in the customary glyphs for different languages; specify the font or the language where they have to be distinguished.
  4. Never render a missing glyph as "?": it can cause security problems.
  5. Combining characters normally stack outwards:  is  +  + , not   +  + . Don't simply overlay diacritics: it can cause security problems.
    ► http://www.unicode.org/notes/tn2/
  6. Linebreak is not just breaking at spaces
    ► http://unicode.org/reports/tr14/ 
     

Globalization

in de_DE: 1.234,00£ ↔ <GBP, 0.10011010010×212>

  1. Unicode ≠ Globalization. Unicode provides the basis for software globalization, but there's more work to be done...
  2. Don't simply concatenate strings to make messages: the order of components differs by language. Use Java MessageFormat or equivalent.
  3. Don't put any translatable strings into your code; make sure those are separated into a resource file.
  4. Don't assume everyone can read the Latin alphabet. Don't assume icons and symbols mean the same around the world.
  5. Store and transmit neutral-format data wherever possible. Convert that data to the user's preferred formats as "close" to the user as possible. Eg, use Windows Filetime for binary times.
    ► http://icu.sourceforge.net/userguide/universalTimeScale.html
  6. Tag all data explicitly. Trying to algorithmically determine character encoding and language isn't easy, and can never be exact.
  7. Don't confuse User-Interface language (menus, dialog, help-system,...) with Data language (body text, spreadsheet cells). Globalized programs need to handle, as data, more languages than they have localized UI for.
  8. Formatting and parsing of dates, times, numbers, currencies, ... are locale-dependent; even calendar systems may vary. Use globalization APIs that use appropriate data.
    ► http://unicode.org/cldr/
  9. Where OS facilities are not adequate or cross-platform solutions are needed, use ICU (International Components for Unicode) for C, C++, and Java. People have built wrappers for other languages, too.
    ► http://ibm.com/software/globalization/icu/
  10. Locale data typically uses "fallback": if the data isn't found in en_US, look in en. Beware of discrepancies between how this is done in different systems: Java ResourceBundle (J2SE), Java Standard Tag Library (JSTL), Java Server Face (JSF), Apache HTTP,...
  11. English is a relatively compact language; others may require more characters (eg in database fields) and more screen real estate (in UIs). Allocate space flexibly.

Identification

  1. Use RFC 3066 (or its successor) for language IDs, not ISO 639 alone.
    ► ftp://ftp.isi.edu/in-notes/rfc3066.txt
  2. Locale IDs are extensions of language IDs, they are not the same. Use CLDR for locale IDs.
    ► http://unicode.org/cldr/
  3. If you heuristically compute territory IDs, timezone IDs, currency IDs, etc. (eg from browser settings) make sure the user can override that and pick an explicit value.
  4. Always use an explicit currency ID (ISO 4217). Don't assume the currency is the same as the display locale: <RUR, 1.23457×10³> ↔ 1 234,57р. in Russian, but Rub 1,234.57 in English.
  5. Don't assume that a country always has the same currency.
  6. Don't assume the timezone ID is implied by the user's locale. For the best timezone information, use the TZ database; use CLDR for timezone names.
    ► http://www.twinsun.com/tz/tz-link.htm
  7. Computations with mixed timezones or missing timezones are tricky (XML Schema is missing real timezones: you'll have to roll your own datatype).
    ► http://w3.org/International/core/2005/09/timezone.html
  8. Be prepared for instabilities in currencies, territories, and timezones. Unfortunately, you also need to worry about instabilities in the IDs also: eg, ISO reused CS for two different territories.
    ► http://www.unicode.org/consortium/positions.html

Java

  1. In MessageFormat, watch for words like can't, since ASCII ' has syntactic meaning. Use a real apostrophe (U+2019) where possible: can’t.
  2. In Date and Calendar, the months are numbered from 0 (February is month number 1!). However, weeks and days are numbered from 1.
  3. Java serialized text isn't UTF-8, though it's close. U+0000 and supplementary code points are encoded differently.
  4. Java globalization support is pretty outdated: use ICU to supplement it.
  5. Java ResourceBundle (J2SE), Java Standard Tag Library (JSTL), Java Server Face (JSF), Apache HTTP server, etc. all provide some locale determination mechanism and facility; but they all differ in details.

JavaScript

  1. Always encode characters above U+007F with escapes (\uxxxx).
    • There is an HTML mechanism to specify the charset of the Javascript source, but it is not widely implemented. The JDK tool native2ascii can be used to convert the files to use escapes
Comments