International Components for Unicode

0x00 前言

这篇文章是自己在学习 CFHipsterRef Chapter 12 International Components for Unicode 时做的笔记。

0x01 International Components for Unicode

至少就 Apple 的 SDK 而言,Unicode 扮演的角色很难去夸大。本章将讨论在 Foundation 和 Core Foundation 中如何使用 Unicode 的一个方面,特别是 ICU 或 Unicode 的国际组件。

0x02 Unicode

Unicode 是国际计算的基石。这一切始于 1987 年,有三个人:来自 Xerox 的 Joe Becker 和来自 Apple 的 Lee Collins & Mark Davis。

他们对 Unicode 的目标很简单,但是雄心勃勃:

  • 广泛,满足世界语言的需求。
  • 统一,带有固定宽度的代码,用于有效访问。
  • 唯一,每个 bit 序列都仅具有一个解释。

自成立以来,Unicode 已经成功地创建了一个普遍采用的标准,超过 100,000 个字符表示为数十亿人使用的语言。

0x03 ICU

ICU 或 Unicode 的国际组件(International Components for Unicode),是用于在软件中提供 Unicode 和全球化支持的行业标准。

它是 IBM 在 90 年代出创建的,自那以后一直保持着。

ICU4C,是 C/C++ 库,以 libicucore 形式构成了 Apple 操作系统的支柱,是 Core Foundation 和 Foundation 广泛使用的私有化框架,但是不可公共使用。虽然可以提供 libicucore,但是比起简单使用基于它构建的 SDK API(例如 NSLocaleNSCalendarCFStringTransform),没有什么实际优势。

因此,本章将研究 ICU,以便更好地理解这些高级 API 的工作原理,以及如何利用此信息来利用(exploit)未记录的 API 功能。

0x04 CLDR

CLDR 或 通用本地化数据仓库(Common Locale Data Repository),是使 ICU 如此引人注目的一项技术。包含超过 400 MB 的 JSON 数据,CLDR 包含了所有人类文化习俗的权威编码。

CLDR 的 main 目录包含了多个子目录 —— 每个可用语言环境一个目录(one for each available locale)。在每个语言环境目录中的是描述该语言环境的一个特殊方面的文件的集合:

0x0401 Calendars

在 CLDR 中有 18 个不同的日历表示,从标准的 Gregorian,到古代,宗教和模糊系统的所有规矩(manners)。

  • Calendar
  • Buddhist
  • Chinese
  • Coptic (a.k.a Alexandrian)
  • Dangi
  • Ethiopic
  • Ethiopic (Amete Alem)
  • Hebrew
  • Indian
  • Islamic
  • Islamic (Civil)
  • Islamic (Saudi Arabia)
  • Islamic (Tabular)
  • Islamic (Um al-Qura)
  • Minguo (Republic of China)
  • Japanses
  • Persian

每个日历在每个语言环境的单独文件中表示。每个文件几百行长,包含月份,星期在各种级别的缩写,以及日期和时间间隔的格式化规则。

NSCalendarNSDateFormatter 使用此信息来将日期解析和格式化成与语言环境相对于的格式:

0x0402 Characters

对于语言环境中说的每种语言,提供了字符的清单,以及省略的排序索引(collation indexes)和格式化规则。

字符清单(Character inventories)可能被 NSLinguisticTagger 用作评估字符串的 NSLinguisticTagSchemeLanguage 的一个低通(low pass)。具有超出语言拼写库(orthographic inventory)的字符的字符串不太可能匹配。相反,示例字符(exemplar characters)的相对频率可能在两个可能的候选之间是有用的。

可以使用 NSLocaleExemplarCharacterSet key 检索 NSLocale 的示例字符。

UILocalizedIndexedCollation 使用排序索引来对当前语言环境适当地分段语言记录。美式英语使用拉丁字母(Latin alphabet)来整理信息,如名字:

1
[A B C D E F G H I J K L M N O P Q R S T U V W X Y Z]

然而,瑞典语扩展了拉丁字母表,还有一些额外的字符:

1
[A Á B C Č D Đ E É F G H I J K L M N Ŋ O P Q R S Š T Ŧ U V W X Y Z Ž Ø Æ Å Ä Ö]

省略规则指定应如何格式化截断的文本,这取决于截断的位置是起始,中间还是结尾,以及在该点是否有字边界(word boundary):

Example Ellipsis Rules

Initial  
Medial  
Final  
Word Initial  
Word Medial  
Word Final  

0x0403 Currencies

忘记音乐或世界语; 金钱 才是真正通用的语言。每种货币根据其 ISO 4217 代码(USD,EUR,GBP等)被列出,并包括其地区特定符号($, €, £等)。大多数这些信息在不同的语言环境中是一致的,但是有内置的冗余来容纳诸如特定计数显示名称。

NSLocale 使用此信息来查找指定的语言环境中的货币代码和符号。反之,当使用 NSNumberFormatterCurrencyStyle 表示数字时,此信息被传递到 NSNumberFormatter 中。

0x0404 Date Fields

除了日历的详细信息,每个语言环境都有如何执行相对日期格式化的规则,例如“现在”,“昨天”或“上周”。惯用指示词(Idiomatic deictics)在不同的语言中有所不同。比如,德语中有 “vorgestern” 来描述 “前天”。除了惯用指示词,还有常规的 / 刻板的过去和未来指示词,如“1周前”和“4秒”。

doesRelativeDateFormatting 被设置为 YES 是,此信息由 NSDateFormatter 使用。

0x0405 Delimiters

每种语言都具有其自己的如何界定引用:

Quotation Delimiters

English “I can eat glass, it doesn’t harm me.”
German „Ich kann Glas essen, das tut mir nicht weh.“

CLDR 指定语言环境中使用的每种语言的主要(””)和可选备用(’’)的引号的开始和结束分隔符。

可以使用 NSLocaleNSLocaleQuotationBeginDelimiterKey / NSLocaleAlternateQuotationBeginDelimiterKeyNSLocaleQuotationEndDelimiterKey / NSLocaleAlternateQuotationEndDelimiterKey key 来获取引号分隔符。

0x0406 Languages

languages 文件指定了其各自的语言环境来参考该语言。

例如,NSLocale 在传递 NSLocaleLanguageCode key 时使用此信息作为 displayNameForKey

1
2
3
NSLocale *frLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"fr_FR"];
NSLog(@"fr: %@", [frLocale displayNameForKey:NSLocaleLanguageCode value:@"fr"]);
NSLog(@"en: %@", [frLocale displayNameForKey:NSLocaleLanguageCode valie:@"en"]);

NSLocaleLanguageCode

fr français
en anglais

0x0407 Layout

一个语言环境的布局详情非常简单:它们为每种语言指定字符顺序(从左到右或从右到左)和行顺序(从上到下或从下到上)。

0x0408 List Patterns

列表是宾格的心脏,主体和灵魂。在语言环境和语言之间用于一个列表中的 item 如何分隔不同的规则,和所有这些差异都在 CLDR 中列举。

从一个实现的角度来看,有趣的是看这些规则是如何被编码的。而不是指定,例如,划分字符和连词(conjunction),这至少对英语更有意义,CLDR 指定作为开始,中间和结束模式的格式。相反,语言可能具有唯一的模式集合以适应不同的上下文(context),就像 units(单元)。

在实践中,在语言环境之间最主要的区别是是否使用标准或全宽度逗号,是否使用 item 间(inter-item)间距,以及是否在最后使用一个连词。

Foundation 目前不使用这个信息,因为它没有提供任何类似 NSArrayFormatter 的东西。

0x0409 Locale Display Names

languageslocaleDisplayNames 文件定义了指定语言环境显示不同类型的信息的名称。排序,计数系统,文字系统,日历和排序方案都在这里表示。

在提供语言环境首选项时,此信息始终在 iOS 和 OS X 中使用。然而,并不是 CLDR 提供的所有信息都可以通过系统 API 来访问。

0x040A Measurement System Names

measurementSystemNames 文件指定每个测量系统的本地化名称(公制,US & UK)。

要确定一个给定的 NSLocale 是否使用度量标准系统,在 - objectForKey: 中使用 key NSLocaleUsesMetricSystem

0x040B Numbers

这个文件编码了关于一个语言环境的数据格式所有的规则:计数系统,十进制格式,科学,百分比和货币样式;首选的符号为小数(.),组(,),列表(;),加号(+),减号(-),百分号(%),千分号(‰),指数(E),无限(∞)和非数字(NaN);和数据范围的模式。

正如你可能期望的,这是源自 NSNumberFormatter 其格式化规则。

0x040C POSIX

这一个实际上相当有趣 —— 不是大多数说英语的计算机用户可能已经思考过。

当 Unix 命令提示确认时,它们期望一个是或否的回答。在英语中,这非常清楚:yes / yno / n

但是其他语言呢?

在意大利语中,选项为 / si / sno / n。在俄语中,да / днет / н 是可接受的回答。

CLDR 对每种语言都有规则,让使用 ICU 的软件开发者简单地在所有的语言环境中制作出令人愉快的软件。

由于大多数 iOS 和 OS X app 喜欢 CLI 更喜欢 GUI,这不是预知的事情,因此,SDK 不支持。

0x040D Scripts

在脚本(scripts)和语言之间是一个多对多的关系。有些语言有多个脚本,大多数脚本被多种语言使用。

ISO 15924 是用于标识脚本的标准。每个脚本都被分配了 4 个字符和 1 个数字标识符。

例如,Latn 是 Latin 脚本,Hira 是日文平假名, `Brai`` 是盲文。

对于 CLDR 中的每个语言环境,每个脚本都有其本地化的名称。NSLocale 可以通过 NSLocaleScriptCode key 来访问它。

0x040E Territories

一个语言环境的 territories 文件包括根据其联合国(United Nations)地理编码 ID(geoscheme ID)的国家(根据 ISO 3166)和世界地区的名称。

这里的难点是争议的名称和地理如何政治上正确。一些国家可能不被其他国家承认,或可能有作为武装冲突一部分的领土吞并。

由于程序员在大多数地缘政治斗争中没有纠缠(don’t have a dog),采用 ICU 标准是一个聪明的选择,这最大限度地减少了无意中引发国际危机的可能性。

NSLocale 只公开了国家/地区代码,使用 NSLocaleCountryCode key。然而,AddressBook 和其他框架利用 CLDR 的数据库来在整个操作系统中本地化国家/地区的名称。

0x040F Time Zone Names

任何足够了解时区的程序员都知道他们不需要编写任何有关时区的代码。

时区范围从 UTC-12 到 UTC+14 —— 跨度总共 26 小时,这有点奇怪,一天只有 24 小时。有些时区遵守夏令时,而其他没有。在那些遵守中的有一些在有些情况下会使用 ±30 或 45 分钟的局部偏移。

某些跨越大范围经度的国家,像美国和加拿大,被分成许多不同的时区。其他国家,像中国,被标准化为只有一个单一的时区为一个同等跨度,这意味着,在早上8点当太阳升起在西部城市喀什,差不多是北京的中午。

这么多的边缘情况,就好像每个时区都是规则的例外。因此,每个 timezones 文件都有几千行长,并且包括世界上数百个地区,国家和城市列表。

幸运的是,NSTimeZone 都帮我们做好了。

0x0410 Transform Names

转换(Transform)是一个脚本或写作标准中的文本转换到另一个的过程。对于主脚本,有转换的标准化约定。
BNG 用于转换俄语(西里尔体,Cyrillic)为拉丁语。Jamo 用于转换韩文(Korean Hangul)为拉丁语,以及 Pinyin 用于转换中文为拉丁语。还有转换 CJK(Chinese, Japanese, Korean)字符在半宽和全宽之间表示。还有联合国地名专家组(United Nations Group of Experts on Geographical Names,UNGEGN)转换,使地名或音名的音译标准化。

这些标准变换中的每一个都有一个与它们相关联的名称,这些名称随着语言环境而变化。CLDR 具有每种语言的对应关系。

0x0411 Units

有许多不同类型的单元(Units), 每个都表示了一个特定的物理量(physical quantity),像加速度(acceleration),角度(angle),面积(area),持续时间(duration),长度(length),质量(mass),功率(power),压力(pressure),速度(speed),温度(temperature)或体积(volume)。由于一个语言环境对于如何格式化和表示这些单元可能有稍微不同的标准,因此 CLDR 为每个单元提供了模式(patterns)。

随着 HeathKit 的引入,Foundation 增加了能量(energy),质量和长度的格式化程序(formatters)。MapKit 也提供了用于以英里和公里为单位的格式化程序。每个都利用 CLDR 中的单元格式化规则。

0x0412 Variants

一个语言环境的 variants (变体)记录是 BCP 47 子标签的本地化名称的抓包,其包括方言,正文和音译方案。这些是对一个特定语言的可接受标准的重要替代,例如,对于汉语和日语的韦氏拼音(Wade-Giles)和赫本古罗马化策略(Hepburn romanization strategies),分别被拼音和 Rōmaji 淘汰。

0x0413 Supplemental

最后,在一个完全独立的顶层目录中存在一个补充的记录目录。在这里和在单个语言环境记录中几乎一样多,但是由于还没有通过 Objective-C API 提供的那么多,我们就浏览一下:

  • 日历数据:日历系统时代的时代(Epochs of calendar system eras),以及日历是否基于月球或太阳周期。
  • 日历偏好数据:每个语言环境中支持的有序日历列表,按偏好排序。
  • 字符回退(Character Fallbacks):没有很好支持的字符的更简单的替代方案,比如 (C) 为 “©” 或 1/2 为 “½”,以及韩语和希伯来语(Hebrew)中的货币符号,连字和复合字符。
  • 代码映射(Code Mappings):顶级域代码映射。
  • 货币数据:不同国家使用的货币历史,包括使用的开始和结束日期。
  • 日期(Day Periods):将一天的时间分割的各种方案,从简单的:“上午(a.m.) / 下午(p.m.)”到极度精确的:“凌晨(wee hours)/ 清晨(early morning)/ 早上(morning)/ 上午晚些时候(late morning)/ 中午(noon)/ 正午(mid day)/ 下午(afternoon)/ 傍晚(evening)/ 午夜(late evening) / 深夜(night)”。
  • 性别的复数规则:如何性别复数的规则。
  • 语言数据:语言列表及其各自的脚本和范围。
  • 语言匹配:关于如何交换类似语言的规则,如哈萨克语和俄语。
  • 可能的子标签:给定一个 BCP 47 语言标签,最有可能关联的子标签。
  • 测量数据:哪些国家使用公制与英制单位,或 A4 与美国信纸的纸张尺寸。
  • Metazones:为地区建立一个区域层次结构的记录。
  • 计数系统:替代编号系统的清单与规则,如阿拉伯语,罗马与,全角 CJK 和拼写的英语数字。
  • 序数(Ordinals):每种语言的规则或序数(即第一,第二,第三等)。
  • 父语言环境:建立从地区到父语言环境的一个有向图关系。
  • 多个规则:每种语言使用 6 中不同的 Unicode 计数规则中的任何一种:零,一,二,少,多和其他。
  • 邮政编码数据:描述国家/地区的邮政编码规则的正则表达式。
  • 主区域:主时区。
  • 参考文献:用于确定所有这些不同规则的来源的参考书目。
  • 电话代码数据:每个国家的国际拨号代码。
  • 区域控制:确定领土地理区域的空间关系。
  • 区域信息:区域统计数据的细目,包括人口,GDP,识字率和语言人口。
  • 时间数据:tl;dr -{"_allowed" : "H h", "_preferred" : "h"}
  • 星期数据:对于每个语言环境,一周中的最少天数,以及哪一天是一周的开始。
  • Windows Zones:时区信息的旧映射。

0x05 Transform

最初设计为将一个脚本中的文本转换为另一个脚本,ICU 变换已经发展成为使用 Unicode 文本的有力工具,具有大小写和宽度转换,复合字符序列标准化和删除重音和变音符号。

ICU 变换通过 Core Foundation 中的 CFStringTransform 函数被暴露出来。大约十几个字符串常量定义为常用操作,比如 CFStringTransformToLatin,方便地将文本音译成其相应的拉丁字母表示。不幸运的是,这些常量具有不透明值,这最终掩盖了 CFStringTransform 将接受任何有效的 ICU 变换的事实。

一个 ICU 变换由一个或多个分号分隔的映射组成。每个映射在左手边和右手边值之间是单项或双向的。

例如,一个在一个版权符号和其 ASCII 表示之间的双向变换可以被表示为:

1
(C) <> ©;

} 操作数将规则约束到特定的上下文,比如在此单向映射中,只删除小写字母后的连字符:

1
[:lowercase letter:] } '-' > '';

每个映射按顺序评估,因此应从最具体的规则开始列出,并以最一般的规则结束。

ICU 为常见和有用的操作提供了许多内置的音译,可以与其他规则组合以完成几乎任何自动文本转换任务。

0x0501 Text Processing

ICU 有用于基本的文本处理任务的音译比如改变大小写或规范化:

Text Processing Transforms

Any-Null 没有影响;留下不没有变的输入文本。
Any-Remove 删除输入的字符。当与限制要删除的字符的过滤器组合时,这就有用了。
Any-Lower, Any-Upper, Any-Title 转换为指定的大小写。更多信息请看大小写映射(Case Mappings)。
Any-NFD, Any-NFC, Any-NFKD, Any-NFKC, Any-FCD, Any-FCC 转换为指定的标准化形式。
Any-Publishing 在真实标点符号与打字机标点符号之间转换。

0x0502 Accent and Diacritic Stripping

最常见的规范化任务之一是去除重音和变音符号。ICU 变换提供了一个灵活的解决方案:

Normalization Transforms

“NFD; [:Mn:] Remove; NFC” 移除所有重音和变音符号。

使用这个变换,”Énġlišh långuãge läcks iñterêsţing diaçrïtičş” 变成 “English language lacks interesting diacritics”。

kCFStringTransformStripCombiningMarks 常量也可以用于相同的效果。

0x0503 Unicode Symbol Naming

Unicode 标准中的每一个代码点都有一个官方名词,其可以使用 ICU 变换来检索:

Unicode Symbol Naming Transforms

Any-Name 将每个字符替换为其 Unicode 名称。

将此变换应用与 “å” 产生 “{上面带有环的小写拉丁字母 A}”。

0x0504 Script Transliteration

不吹牛逼地说,脚本音译是 ICU 的一大杀手功能。对于世界上数十亿只能读懂或写作他们母语的人来说,将任何文本转换成可发音的能力本身就是对人类的变革。

ICU 包括以下几种音译:

  • Latin <-> Arabic, Armenian, Bopomofo, Cyrillic, Georgian, Greek, Han, Hangul, Hebrew, Hiragana, Indic (Devanagari, Gujarati, Gurmukhi, Kannada, Malayalam, Oriya, Tamil, & Telegu), Jamo, Katakana, Syriac, Thaana, & Thai.
  • Indic <-> Indic
  • Hiragana <-> Katakana
  • Simplified Chinese (Hans) <-> Traditional Chinese (Hant)

源和目标说明符可以是脚本标识符(”Latin” / “Latn”),Unicode 语言标识符(fren_USzh_Hant)或特殊标签(AnyHex)。

以下是一些以有用的方式链接在一起的音译示例:

Script Transliteration Transforms

Any-Latin 将文本音译成拉丁脚本,也许是为了让英语为母语的人能够读。
Any-Latin; Latin-Hangul 将文本音译成韩文,使用拉丁文作为中间表示。
Any-Latin; Latin-ASCII; [:^ASCII:] Remove 将文本从中间拉丁表示音译为 ASCII,过程中去掉了任何非 ASCII 字符。
[:Latin:]; NFKD; Lower; Latin-Katakana; Fillwidth-Halfwidth 对于所有拉丁字符,根据标准化表单兼容性进行标准化分解,改为小写,音译为片假名,然后转换为半宽表示。
Any-Latin; Latin-NumericPinyin 将文本音译为拉丁,将拼音重音改为其等效的数字。