PHP V5.3 在 Unicode 方面有何新特性?

小编辑 发布于 2010/01/29 20:46
阅读 448
收藏 6
PHP

PHP 是一种十分流行的语言,但仍然缺少适当的 Unicode 支持。不过最新发布的 V5.3 添加了一个构建于著名的 ICU 库之上的新的国际化库。现在,利用这个新库,就可以适当地对多个本地语言环境的数字和日期进行整理、排序和格式化。了解如何使用这个新库来对应用程序进行 适当的国际化以及解决常见的 Unicode 问题。

Web 是一个用来开发全球性应用程序和服务的理想平台。要创建一个真正具有国际魅力的应用程序,必须对它进行调整以便能够以各种语言和编写系统处理和显示数据。

要针对另一种语言来调整应用程序,需要涉及几个阶段,第一个阶段是所谓的国际化,通常缩写为 i18n。国际化的目的 是为了确保用户能在此应用程序中使用其本国的语言和注释(包括数据输入和显示用到的特殊符号)、以正确的格式显示数值和日期,以及根据特定于语言的规则来 排序列表。

更为高级的方式还包括 本地化(缩写为 l10n)。在本地化阶段,应用程序被调整为支持特定的文化、语言和本 地习惯。这个过程涉及到本地语言的翻译;日期、数值和货币符号的正确设置;排序规则等。

本文展示了 PHP V5.3 的新特性,有助于提高您在 PHP 内创建国际化了的应用程序的能力。本文不会解决全部的本地化问题 — 尤其是对于翻译;这类任务可由额外的 PHP 库,比如 GNU gettext,进行很好地处理(参见 参 考资料)。

PHP 中的 Unicode 支持

一个被恰当国际化了的应用程序应能处理在不同的编写系统内写入的数据。英语及在西欧使用的其他语言都是以拉丁文字为基础,并且只使用拉丁字符 — 有时还会附加上重音符号(变音符号)。如果往东走,会遇到西里尔字母、希伯来和中东的阿拉伯语系,以及印度语。然后是中文、日文及十几种其他的东方语系。 大多数常用的字符系统均包括在 Unicode 字符集(更多信息,请参见 参 考资料)。

不过,Unicode 字符还是很抽象。当存储在内存或磁盘或是在网络上传输时,计算机系统必须对 Unicode 字符进行编码。有几种编码可用于 Unicode:最为常用的两个是 UTF-8 和 UTF-16。现代的开发环境,比如 Java™ 技术和 Microsoft® .NET Framework 使用 Unicode 并具备针对 Unicode 字符和字符串的数据类型。因而,处理使用 Unicode 字符的文本对于开发人员也变得完全透明了。库函数负责正确处理所有输入和输出(UI、HTML 表单、数据库、XML)并在需要时将其转换成用来表示 Unicode 字符串的内部编码。

遗憾的是,PHP 语言仍缺少适当的 Unicode 支持。虽然自 2001 年,核心 PHP 开发人员一直都在尝试将 Unicode 支持添加到 PHP 内,但即便 PHP V5.3,也仍未包括此项支持。不过,在下一个主要的发布版内有望加入该支持 — 即 PHP V6 。

克服 PHP 内无 Unicode 支持的缺点

虽然 PHP 内缺少对 Unicode 的支持令人不快,但是通过一些变通方法,您还是可以在 PHP 内开发适当国际化了的应用程序。需要解决的第一个问题是 Unicode 数据的恰当表示。PHP 使用所谓的二进制字符串 — 在 PHP 中,一个字符串 并不是一个 Unicode 字符的字符串,而是一个字节序列。可以使用 UTF-8 编码在内部存储所有字符串,并确保到此脚本的所有输入以及源自此脚本的所有输出都被恰当地编码和解码。

理论上,可以使用除 UTF-8 外的其他编码,但比起其他系统,使用 UTF-8 麻烦要少得多。很多 PHP 库都默认字符串是以 UTF-8 编码的,包括处理 XML 的所有函数和新添加的 intl 库。要顺利地处理由 UTF-8 编码的字符串,最好是以 UTF-8 对字符进行编码并以 UTF-8 发送来自脚本的输出。

但是,将所有东西都转变为 UTF-8 并不能解决全部问题。如果对一个具有重音符号的拉丁字符或是一个非拉丁字符进行 UTF-8 编码,将会得到四字节中的两个、三个字节,这会让计算字符串长度或处理子字符串的 PHP 字符串函数不知所措。清单 1 展示了这个问题。


清单 1. 与 PHP 内无适当 Unicode 支持相关的问题

				

Header("Content-type: text/plain;charset=utf-8");

$text["en"] = "The Hitchhiker's Guide to the Galaxy";
$text["es"] = "Guía del autoestopista galáctico";
$text["cs"] = "Stopařův průvodce po Galaxii";
$text["ru"] = "Путеводитель хитч-хайкера по Галактике";
$text["ja"] = "銀河ヒッチハイク・ガイド";

foreach($text as $lang => $t)
{
echo $lang, ": ", $t, " (", strlen($t), " vs. ", mb_strlen($t, "utf-8"), ")\n";
}
?>

 

此清单的输出如图 1 所示。


图 1. 纯 PHP 字符串函数返回对用 UTF-8 编码的文本的不恰当结果
纯 PHP 字符串函数的输出结果

正如您所见,以各种编写系统编写的字符串的长度得到了错误的计算。只有包含拉丁字母的文本才会有正确的返回结果。在本例中,这个问题可以通过 使用来自 mbstring 库的函数得到解决。因而,要获得以 UTF-8 编码的字符串的正确长度,必须使用 mb_strlen(string, "utf-8"),而不只是 strlen(string)

当脚本在处理 UTF-8 格式的数据时,用 intl 库还可以添加更多的国际化特性。

安装 intl 库

intl 库是著名的 International Components for Unicode (ICU) 库的一个 PHP 包装器。很多应用程序使用 ICU 实现适当的 Unicode 和本地化支持。

自 V5.3,intl 库一直都是 PHP 的一部分。如果您使用的是 PHP V5.3 或更高版本,那么这个库应该是可用的。对于 PHP 的较老版本,通过 PECL 扩展仍可以使用到这个库。

处理 locale

语言、地区、编写系统和其他能控制本地化的参数组合在一起就是 locale。locale 通常由一个在 IETF Best Current Practices (BCP) 47 内定义的语言标签标识(更多信息,参见 参 考资料)。例如,英语由简单的标签 en 标识。有些语言在历史上涉及了不同的地区,因而现在也就具有明显的区别。为了处理这种情况,可以在语言标识符后面追加一个国家标识符。比如,用 pt_PT 表示葡萄牙本国使用的葡萄牙语,用 pt_BR 表示巴西葡萄牙语。BCP 47 提供了更多细致的控制,但是为了简便起见,本文没有提供有关 locale 标识符的进一步细节。

对 locale 敏感的所有 intl 函数和方法都接受一个语言标签作为一个 locale 标识符。而且,intl 库还提供了一个双重接口 — 函数式的和面向对象的。可以根据自己的 PHP 编码风格选择合适的接口。例如,有一个函数/方法能在给定语言内返回针对某个 locale 的语言名称。如果使用函数式注释,可以按如下所示调用此代码:

// return name of language used for "en" locale in French (fr)
echo locale_get_display_language("en", "fr"); // Anglais

 

如果更愿意采取面向对象的方式,可以使用相应的静态方法:

// return name of language used for "en" locale in French (fr)
echo Locale::getDisplayLanguage("en", "fr"); // Anglais

 

intl 库内提供的 Locale 类定义了方便的实用工具方法。清单 2 提供了几个例子。


清单 2. Locale 类内的方法使用

				
Header("Content-type: text/plain; charset=utf-8");

// display name of Portuguese as used in Brazil in different languages
echo Locale::getDisplayName("pt_BR", "en"), "\n";
echo Locale::getDisplayName("pt_BR", "de"), "\n";
echo Locale::getDisplayName("pt_BR", "ru"), "\n";
echo Locale::getDisplayName("pt_BR", "ja"), "\n";

// return preferred locale set in user's browser
echo "Preferred locale from browser: ",
Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);
?>

 

此脚本会输出几个不同的 locale 内的语言名称 (Portuguese) 及国家名称 (Brazil)。此外,还显示了 acceptFromHttp() 方法,此方法可用来阅读用户设置好的首选 locale。图 2 给出了上述代码清单的输出。


图 2. 清单 2 所示代码的输出
显示 Locale 类输出

格式化数字

初看上去,格式化数字似乎是一件很容易的任务。但是您仍需要处理所有的繁琐细节 — 比如,在不同语言内使用的不同的小数和分组分隔符 — 若 intl 能代劳的话,您自然会非常乐意。除了数字和货币格式化之外,intl 还能处理更为复杂的任务,比如拼写出数字 — 同样地,不只是针对英语,很多其他受支持的 locale 也可以。

格式化可以通过以 numfmt_ 前缀开头的函数实现,也可以通过如 NumberFormatter 类内所示的方法实现。本文中的例子使用的是面向对象的风格,但使用函数式方式可以获得同样的结果。

在格式化之前,必须创建 NumberFormatter 的一个新的实例。必须以参数的形式向这个构建方法提供一个 locale 标识符和 formatter 的一个样式。可以使用几个预先定义好的常量指定样式,比如 NumberFormatter::DECIMAL (小数格式)、NumberFormatter::CURRENCY(货币格式)、NumberFormatter::SCIENTIFIC(科 学格式)和 NumberFormatter::SPELLOUT(数值必须被拼写出)。比如:

$fmt = new NumberFormatter("en", NumberFormatter::SPELLOUT);

 

在新创建的 formatter 之上,可以调用几个方法。最有用的方法是:用来格式化数字的 format() 和用来格式化金额的 formatCurrency()。后者的这个方法接受货币代码作为其第二个参数。这些方法的使用如清 单 3 所示。


清单 3. 在 FormatNumber 类内使用这些方法

				
Header("Content-type: text/plain; charset=utf-8");

// Locale-aware number formatting
$fmt = new NumberFormatter("en", NumberFormatter::DECIMAL);
echo $fmt->format(19841984.123456), "\n";

// Spelling out numbers in English
$fmt = new NumberFormatter("en", NumberFormatter::SPELLOUT);
echo $fmt->format(1984), "\n";

// Spelling out numbers in Russian
$fmt = new NumberFormatter("ru", NumberFormatter::SPELLOUT);
echo $fmt->format(1984), "\n";

// Formatting Euro and Czech crowns in German
$fmt = new NumberFormatter("de", NumberFormatter::CURRENCY);
echo $fmt->formatCurrency(123456.789, "EUR"), "\n";
echo $fmt->formatCurrency(123456.789, "CZK"), "\n";

// Formatting Euro and Czech crowns in Czech
$fmt = new NumberFormatter("cs", NumberFormatter::CURRENCY);
echo $fmt->formatCurrency(123456.789, "EUR"), "\n";
echo $fmt->formatCurrency(123456.789, "CZK"), "\n";

?>

 

如图 3 中的结果所示,在格式化小数时,使用了适当的分组和小数分隔符。如果您会英语和俄语,那么您就可以验证该数是否被正确拼出。最后,可以看到有些货币代码, 比如 EUR,被自动格式化成 ,而其他的则被表示为本地化的格式 — 比如,Czech crown (CZK) 在捷克语中被表示为


图 3. 清单 3 所示脚本的输出
显示 FormatNumber 类的输出

排序

在很多应用程序中,数据应该在显示之前先进行排序。但是除英语之外的其他语言的排序规则可能会非常复杂。带重音符号的字符通常都是以一种特殊 的方式处理;有些语言会将两个字符的特定序列视为一个字母进行排序(比如,捷克语和传统西班牙语中的 ch )。所幸的是,intl 库提供了 Collator 类(以及名字以 collator_ 开头的影子函数),可用来根据特定的 locale 对比和排序字符串。

在对比和排序之前,必须创建新的 collator 并为它指定一个 locale:$coll = new Collator("en_US");

现在,可以在所创建的对象上调用各种方法。比如,compare() 方法比较两个字符串;sort()asort() 方法以与相应的 PHP 数组函数相同的方式对数组或关联数组进行排序。清单 4 内的代码显示了如何基于为这个 collator 所用的 locale 来对由捷克文组成的一个数组进行排序。


清单 4. 针对不同的 locale 进行排序

				

Header("Content-type: text/plain; charset=utf-8");

// words to sort
$words = array("čočka", "čekanka", "cena", "chalupa",
"ťululum", "dálnopis", "tyfus", "traktor");

// sort using built-in PHP sort function
sort($words);
echo "Words sorted using built-in sort function:\n";
var_export($words);

// sort according to English rules
$coll = new Collator("en_US");
$coll->sort($words);
echo "\n\nWords sorted according to English rules:\n";
var_export($words);

// sort according to Czech rules
$coll = new Collator("cs");
$coll->sort($words);
echo "\n\nWords sorted according to Czech rules:\n";
var_export($words);

?>

 

若打算使用内置的 PHP sort() 函数排序单词,那么以加了重音符号的字母开头的那些单词将会位于最末端,因为 PHP 会按二进制值对比字符串,并且不会对加了重音符号的字母进行特殊处理。不过,如果使用针对英语创建的一个 collator 来排序相同的单词,那么这些单词的排序就好像这些重音符号不存在一样 — 如果在英文中夹杂着几个外文单词,那么这是一个很理想的结果。最后,使用了一个针对捷克语的 collator。正如您所见,应用的规则有点奇怪:有一些被加了重音符号的字符现在都被视为没有加重音符号(例如,ť),而有些则被视为 单独的字符(例如 cč),ch 则被视为一个特殊的字符。图 4 显示了清单 4 内代码的结果。


图 4. 清单 4 脚本的输出
之前脚本的输出

高级话题

Unicode 和国际化是一个很大的主题,但有一件很重要的事情您应该知道。由于历史的原因,Unicode 允许某些字符具有替代表示。例如,á 可以被写作一个 Unicode 码点为 U+00E1 的预构字符 á,也可以被视 为是字母 a (U+0061) 与重音符号 ´ (U+0301)的分解组合。为了对比和排序的目的,这两个表示应被视为等同。

为了解决这个问题,intl 库提供了 Normalizer 类。这个类反过来又提供了 normalize() 方法,可用来将一个字符串转变为一个规范化了的组合或分解格式。在执行比较之前,应用程序应该一致地将所有字符串都转变成一个或另一个格式。

echo Normalizer::normalize("a´, Normalizer::FORM_C); // á 
echo Normalizer::normalize("á", Normalizer::FORM_D); // a´

结束语

这篇短文介绍了 intl 库所提供的最重要也是最有用的功能。这个库成为了 PHP V5.3 的标准部分。该库所具有的功能还有很多,远不止本文所介绍的这些 — 比如,日期格式化以及对以本地化格式存储的值的解析。更多信息,请参考 PHP 手册。

下载本文代码

加载中
0
dreamhack
dreamhack
小编辑的文章竟然没人顶,被我发现了,哈哈
0
张天福
支持一下,各编程语言这方面做得好的就是c#了
返回顶部
顶部