Boost.Locale
设计原理

为什么我需要 Boost.Locale?

当标准 C++ facets(应该)提供大部分所需功能时,为什么我们需要本地化库?

  • 大小写转换是使用 std::ctype facet 完成的
  • std::collate 支持排序,并与 std::locale 良好集成
  • std::num_putstd::num_getstd::money_putstd::money_getstd::time_putstd::time_get 用于数字、时间、货币格式化和解析。
  • 有一个 std::messages 类,它支持本地化消息格式化。

如果我们在标准库中拥有所有功能,那么为什么我们需要这样一个库?

几乎每个(!) facet 都有设计缺陷

  • std::collate 仅支持一个级别的排序;它不允许您选择是否应执行区分大小写或区分重音符号的比较。
  • 负责大小写转换的 std::ctype 假定所有转换都可以在每个字符的基础上完成。对于许多语言来说,这可能是正确的,但在一般情况下是不正确的。
    1. 大小写转换可能会改变字符串的长度。例如,德语单词 "grüßen" 应转换为大写 "GRÜSSEN":字母 "ß" 应转换为 "SS",但 toupper 函数在单个字符的基础上工作。
    2. 大小写转换是上下文相关的。例如,希腊语单词 "ὈΔΥΣΣΕΎΣ" 应转换为 "ὀδυσσεύς",其中希腊字母 "Σ" 根据其在单词中的位置转换为 "σ" 或 "ς"。
    3. 大小写转换不能假定字符是单个代码点,这对于 UTF-8 和 UTF-16 编码都是不正确的,在这些编码中,单个代码点由最多 4 个 char 或 Windows 平台上的两个 wchar_t 表示。这使得 std::ctype 在这些编码中完全无用。
  • std::numpunctstd::moneypunct 根本没有指定数字表示的代码点,因此它们无法使用阿拉伯语区域设置下使用的数字格式化数字。例如,数字 "103" 在 ar_EG 区域设置中应显示为 "١٠٣"。
    std::numpunctstd::moneypunct 假定千位分隔符是单个字符。这对于 UTF-8 编码是不正确的,在 UTF-8 编码中,只有 Unicode 0-0x7F 范围可以表示为单个字符。因此,在使用 Unicode "EN SPACE" 字符作为千位分隔符的区域设置(例如俄语)下,本地化数字无法正确表示。
    这实际上在 GCC 和 SunStudio 编译器下引起了实际问题,在俄语区域设置下格式化数字会创建无效的 UTF-8 序列。
  • std::time_putstd::time_get 有几个缺陷
    1. 它们假定日历始终是公历,通过使用 std::tm 进行时间表示,忽略了在许多国家/地区,日期可以使用不同的日历显示这一事实。
    2. 它们始终使用全局时区,不允许指定格式化时区。标准 std::tm 甚至根本不包含时区字段。
    3. std::time_getstd::time_put 不对称,因此您无法解析使用 std::time_put 创建的日期和时间。(此问题在 C++11 和一些 STL 实现(如 Apache 标准 C++ 库)中已得到解决。)
  • std::messages 不提供对复数形式的支持,这使得正确本地化诸如 “目录中有 X 个文件” 这样的简单字符串变得不可能。

此外,std::locale 根本不支持许多功能:时区(如上所述)、文本边界分析、数字拼写等等。因此,很明显,标准 C++ 区域设置对于实际应用来说是有问题的。

为什么使用 ICU 包装器而不是 ICU?

ICU 是一个非常好的本地化库,但它有几个严重的缺陷

  • 它对 C++ 开发人员绝对不友好。它忽略了流行的 C++ 习惯用法(STL、RTTI、异常等),而是主要模仿 Java API。
  • 它仅支持一种字符串类型 UTF-16,而有些用户可能想要其他 Unicode 编码。例如,对于 XML 或 HTML 处理,UTF-8 更方便,而 UTF-32 更易于使用。也没有对仍然非常流行的 “窄” 编码(如 ISO-8859 编码)的支持。

例如:Boost.Locale 提供与 iostream 的直接集成,允许更自然的数据格式化方式。例如

cout << "您在账户中拥有 "<<as::currency << 134.45 << ",截至 "<<as::datetime << std::time(0) << endl;

为什么是 ICU 包装器而不是从头开始实现?

ICU 是可用的最佳本地化/Unicode 库之一。它由大约 50 万行经过良好测试、经过生产验证的源代码组成,这些代码今天提供了最先进的本地化工具。

即使重新实现 ICU 能力的一小部分也是一个不可行的项目,这将需要许多人年。因此,问题不是我们是否需要从头开始重新实现 Unicode 和本地化算法,而是 “我们是否需要在 Boost 中使用一个好的本地化库?”

因此,Boost.Locale 使用现代 C++ 接口包装 ICU,允许将来用更好的替代方案重新实现部分内容,但在今天而不是在不近的将来(如果可能的话)将本地化支持带到 Boost。

为什么 ICU API 不对用户公开?

是的,整个 ICU API 都隐藏在不透明的指针后面,用户无法访问它。这样做有几个原因

  • 在某些时候,更好的本地化工具可能会被未来即将到来的 C++ 标准接受,因此它们可能不会直接使用 ICU。
  • 在某些时候,应该可以切换底层本地化引擎到其他东西,可能是原生操作系统 API 或其他工具包,例如 GLib 或 Qt,它们提供类似的功能。
  • 并非所有本地化都在 ICU 中完成。例如,消息格式化使用 GNU Gettext 消息目录。将来,更多功能可能会直接在 Boost.Locale 库中重新实现。
  • Boost.Locale 在设计时考虑了 ABI 稳定性,因为该库的开发不仅是为了 Boost,也是为了 CppCMS C++ Web 框架 的需求。

为什么使用 GNU Gettext 目录进行消息格式化?

有许多可用的本地化格式。到目前为止,最流行的是 OASIS XLIFF、GNU gettext po/mo 文件、POSIX 目录、Qt ts/tm 文件、Java 属性和 Windows 资源。但是,后三种仅在其特定领域有用,而 POSIX 目录太简单和有限,因此只有两个合理的选择

  1. 标准本地化格式 OASIS XLIFF。
  2. GNU Gettext 二进制目录。

第一个通常看起来是更正确的本地化解决方案,但它需要 XML 解析来加载文档,它是非常复杂的格式,甚至 ICU 也需要预先编译成 ICU 资源包。

另一方面

  • GNU Gettext 二进制目录具有非常简单、健壮且非常有用的文件格式。
  • 它是目前最流行和事实上的标准本地化格式(至少在开源世界中)。
  • 它对复数形式具有非常简单而强大的支持。
  • 它使用原始英文文本作为键,使国际化过程更加容易,因为至少始终有一个基本翻译可用。
  • 有许多用于编辑和管理 gettext 目录的工具,例如 Poedit、kbabel 等。

因此,即使 GNU Gettext mo 目录格式不是官方批准的文件格式

  • 它也是事实上的标准和最流行的标准。
  • 它的实现更加容易,不需要 XML 解析和验证。
注意
Boost.Locale 不使用任何 GNU Gettext 代码,它只是重新实现了用于读取和使用 mo 文件的工具,从而消除了当前 GNU Gettext 最大的缺陷 – 在使用多个区域设置时的线程安全性。

为什么使用普通数字来表示日期时间,而不是 Boost.DateTime date 或 Boost.DateTime ptime?

有几个原因

  1. 根据定义,公历日期不能用于表示与区域设置无关的日期,因为并非所有日历都是公历。
  2. ptime – 绝对可以使用,但它有几个问题
    • 它是在 GMT 或本地时间时钟中创建的,而 time() 给出的表示形式与时区无关(通常是 GMT 时间),并且稍后才应以用户请求的时区表示。
      时区不是时间本身的属性,而是时间格式化的属性。
    • ptime 已经为时间格式化和解析定义了 operator<<operator>>
    • 现有的 ptime 格式化和解析 facets 没有以用户可以覆盖的方式设计。主要的格式化和解析函数不是虚拟的。除非 Boost.DateTime 库的开发人员决定更改它们,否则这使得重新实现 ptime 的格式化和解析函数变得不可能。
      此外,ptime 的 facets 在格式化信息和区域设置信息的划分方面没有 “正确” 设计。格式化信息应存储在 std::ios_base 中,而有关区域设置特定格式化的信息应存储在 facet 本身中。
      库的用户不应为了更改简单的格式化信息(如 “仅显示日期” 或 “同时显示日期和时间”)而创建新的 facets。

因此,在这一点上,ptime 不支持格式化本地化日期和时间。

为什么使用 POSIX 区域设置名称而不是像 BCP-47 IETF 语言标签这样的名称?

有几个原因

  • POSIX 区域设置名称具有一个非常重要的特性:字符编码。例如,当您指定 fr-FR 时,您实际上并不知道文本应该如何编码 – UTF-8、ISO-8859-1、ISO-8859-15 或许是 Windows-1252。这可能因不同的操作系统而异,并且取决于当前的安装。因此,提供所有必需的信息至关重要。
  • ICU 完全理解 POSIX 区域设置,并且知道如何正确处理它们。
  • 它们是大多数操作系统 API 的原生区域设置名称(Windows 除外)

为什么 Boost.Locale 的大多数部分仅在线性/连续的文本块上工作?

有两个原因

  • Boost.Locale 严重依赖于第三方 API,如 ICU、POSIX 或 Win32 API,所有这些 API 都仅在线性文本块上工作,因此提供非线性 API 只会隐藏真实情况并会损害性能。
  • 事实上,所有已知的处理 Unicode 的库:ICU、Qt、Glib、Win32 API、POSIX API 和其他库都接受单个线性文本块作为输入,这有充分的理由
    1. 大多数支持的文本操作(如排序、大小写处理)通常在小文本块上工作。例如:您可能永远不想比较一本书的两个章节,而是比较它们的标题。
    2. 我们应该记住,即使非常大的文本也只需要相当少的内存,例如,整本书《战争与和平》仅占用约 3MB 的内存。
      然而
  • 有些 API 支持流处理。例如:使用 std::codecvt API 进行字符集转换可以在任何大小的流上无问题地工作。
  • 当未来在 Boost.Locale 中引入新的 API 时,如果它可能在大型文本块上工作,将提供用于非线性文本处理的接口。

为什么所有 Boost.Locale 实现都隐藏在抽象接口之后,而不是使用模板元编程?

有几个主要原因

  • 这就是 C++ 的 std::locale 类的构建方式。每个功能都使用 std::locale::facet 的子类表示,该子类为它工作的特定操作提供抽象 API,请参阅 C++ 标准库本地化支持简介
  • 这种方法允许在运行时切换底层 API,而无需更改实际应用程序代码,具体取决于性能和本地化要求。
  • 这种方法显着缩短了编译时间。这对于可能在特定程序的几乎每个部分中使用的库非常重要。

为什么 Boost.Locale 不为非 C++11 平台提供 char16_t/char32_t?

有几个原因

  • C++11 将 char16_tchar32_t 定义为不同的类型,因此用类似 uint16_tuint32_t 的东西替换它将不起作用,例如将 uint16_t 写入 uint32_t 流会将数字写入流。
  • 只有当像 std::num_put 这样的标准 facets 安装到 std::locale 的现有实例中时,C++ 区域设置系统才能工作,但是在许多标准 C++ 库中,这些 facets 针对标准库支持的每个特定字符进行了专门化,因此尝试创建新的 facet 将会失败,因为它没有专门化。

这些正是 Boost.Locale 在 GCC-4.5(第二个原因)和 MSVC-2010(第一个原因)上使用当前有限的 C++11 字符支持失败的原因

基本上,不可能将非 C++ 字符与 C++ 的区域设置框架一起使用。

最好和最可移植的解决方案是使用 C++ 的 char 类型和 UTF-8 编码。