ホームページ >ウェブフロントエンド >jsチュートリアル >国際的なプログラミングに直面する場合の JavaScript に関するいくつかの提案_基礎知識
国際化とは何ですか?
国際化 (Internationalization の略語は i18n——i、真ん中の 18 文字、n) は、さまざまな場所のさまざまな言語を話すユーザーが使いやすくするためにソフトウェアを処理するプロセスです。ユーザーが特定の場所から来て、特定の言語を話すと仮定すると、誤ってエラー メッセージが表示される可能性があります。特に、あなたはそのような仮定さえしていなかったので。
function formatDate(d) { // Everyone uses month/date/year...right? var month = d.getMonth() + 1; var date = d.getDate(); var year = d.getFullYear(); return month + "/" + date + "/" + year; } function formatMoney(amount) { // All money is dollars with two fractional digits...right? return "$" + amount.toFixed(2); } function sortNames(names) { function sortAlphabetically(a, b) { var left = a.toLowerCase(), right = b.toLowerCase(); if (left > right) return 1; if (left === right) return 0; return -1; } // Names always sort alphabetically...right? names.sort(sortAlphabetically); }
JavaScript の過去の i18n サポートはひどいものでした
従来の JS i18n プログラムは、書式設定に toLocaleString() メソッドを使用します。結果の文字列には、実装自体によって提供されるすべての詳細が含まれます。これを自分で選択する方法はありません (その日付形式の曜日が本当に必要ですか? 年は無関係ですか?)。対応する詳細が含まれている場合でも、パーセンテージを期待していたのに数値が取得されるなど、形式が間違っている可能性があります。また、まだロケールを選択することはできません。
並べ替えの場合、JS は基本的に役に立たないロケール依存のテキスト比較関数を提供します。 localeCompare() は存在しますが、そのインターフェイスはソートにはまったく適していません。また、地域設定や並べ替え方法を選択することもできません。
これらの制限はひどいものです (これに気づいたときはショックでした!)。i18n サポートを必要とする本格的な Web アプリケーション (通常、通貨を表示する金融サイト) はデータをパッケージ化してサーバーに送信し、サーバーが処理するためです。それを操作してクライアントに送り返します。データは通貨量を処理するためにのみサーバーとの間で送受信されます。そうだね。
新しい JS 国際化 API
新しい ECMAScript 国際化 API は、JS の i18n 機能を大幅に向上させます。 日付、数値、テキストの並べ替えをフォーマットするための、考えられるあらゆる方法を提供します。ロケールはオプションであり、要求されたロケールがサポートされていない場合はフォールバックできます。フォーマット要求では、含める特定のコンポーネントを指定できます。カスタマイズされたパーセンテージ、有効数字、通貨形式をサポートします。テキストを並べ替えるための多数の並べ替えオプションを開きます。パフォーマンスが気になる場合は、最初の操作としてロケールを選択し、次にオプション パラメーターを処理します。この操作は、ロケール関連の操作が実行されるたびに実行されるのではなく、1 回だけ実行されるようになります。
これは、この API が万能薬であると言っているわけではありませんが、「最善を尽くしてください」というだけです。正確な出力は、ほとんどの場合、意図的に指定されていません。実装では、oj ロケール (これは合法です) のみをサポートすることも、提供されている書式設定オプションを (ほぼすべて) 無視することもできます。ほとんどの実装には高品質のマルチリージョン サポートが含まれていますが、これは保証されていません (特に携帯電話などのリソースに制約のあるシステムでは)。
内部では、Firefox の実装は Unicode International Component Library (ICU) に依存しており、さらに、Unicode Common Regional Data Repository (CLDR) 地域データセットに依存しています。私たちの実装は自己ホスト型です。ICU 上の実装のほとんどは JS で書かれています。途中でいくつかの問題が発生しましたが (これほど大規模なセルフホストを行ったことはありません)、それらはほとんどが軽微でした。
国際インターフェース (数字の 1 ではなく、文字の l)
i18n は Intl オブジェクトの上に存在します。 Intl には、Intl.Collator、Intl.DateTimeFormat、および Intl.NumberFormat の 3 つのコンストラクターが含まれています。各コンストラクターは、関連する操作を提供するオブジェクトを作成し、それらの操作のロケールとオプションを効率的にキャッシュします。次のようにオブジェクトを作成します:
var ctor = "Collator"; // 或其他 var instance = new Intl[ctor](locales, options);
locales 是个字符串,指定单个语言标签,或者包含多个语言标签的类数组对象。语言标签如下面的字符串:en(普通英语),de-AT(奥地利德语),zh-Hant-TW(台湾使用的繁体中文)。语言标签可以包含一个“Unicode扩展”,形式为-u-key1-value1-key2-value2..., 其中每个key是“扩展key”。不同的构造函数对此进行具体解释。
opions 是个对象,其属性(如果不存在,就赋值为undefined)决定格式化器(formatter)和整理器(collator)的行为。精确的解释由构造函数决定。
给定区域信息和选项,实现会尝试生成近似理想行为的最接近行为。Firefox 支持用于整理(collation)的400+区域,用于date/time和数字格式化的600+区域,所以很可能(但不保证)你想要的区域是被支持的。
Intl 通常不保证某些特定行为。如果请求的区域不被支持,Intl 允诺“尽最大努力”的行为。即使区域是被支持的,行为也不是严格指定的。永远不要假设特定的选项集适用于某个特定格式。(围绕请求的组件)总体格式的用语可能因浏览器甚至浏览器的版本而不同。单个组件的格式是未指定的:weekday的短格式可以为“S”, “Sa”, 或“Sat”。Intl API并不用于公开精确的特定行为。
选项
date/time格式化的主要选项属性如下:
"narrow", "short", or "long". (era通常指历法系统中长于一年的分段,如现行日皇统治, 或者其他纪年法)
"2-digit", "numeric", "narrow", "short", or "long"
"2-digit" or "numeric"
"short" or "long"
区分大小写的"UTC"通过对应的toUTC进行格式化。有些值如"CEST"和"America/New_York"不是必须被支持的,它们确实在当前Firefox下没有效果。
这些值并不映射到特定格式:记住Intl API几乎不指定精确的行为。Intl的目的,举例来说是"narrow", "short", 和"long"生成对应大小的“S”/“Sa”, “Sat”, 和“Saturday”(输出可能不太准确,因为Saturday和Sunday都可以生成“S”)。 "2-digit"和"numeric"映射到2位数字的字符串或者全长度的数字字符串,如“70”和“1970”。
最终使用的选项大部分是请求的选项。但是,如果你不指定请求的 weekday/year/month/day/hour/minute/second,那么 year/month/day 将会被加入到你提供的选项。
除此之外,还有些特殊的选项:
指定hour采用12小时还是24小时格式。默认通常是依赖于区域设置的(某些细节,如午夜是0点,还是12点,以及是否存在前导0,都是依赖于区域设置的)。
还有另外2种特殊属性,localeMatcher (可选"lookup"或"best fit") 和formatMatcher (可选"basic"或"best fit"),两者默认值都是"best fit"。这些会影响正确的区域设置和格式的选取。它们的用例可能比较难懂,就不赘述了。
区域设置相关选项
DateTimeFormat也允许通过自定义历法和数字系统来格式化。具体细节存在于区域设置,所以它们可以在语言标签的Unicode扩展中找到。
例如,泰国的泰语中语言标签为th-TH。回一下Unicode扩展的格式-u-key1-value1-key2-value2.... 历法系统的key是ca, 数字系统的key时nu。泰语数字系统值为thai,中文历法系统值为chinese。因此用大体这样的方式来格式化date,我们把包含这些key/value对的Unicode追加到语言标签上去:th-TH-u-ca-chinese-nu-thai。
关于更多历法和数字系统的信息,查看DateTimeFormat的完整文档。
举例
创建DateTimeFormat对象后,下一步是通过方便的format()函数来格式化date。更方便的是,这个函数是有界函数(bound function):你不必在DateTimeFormat上直接调用。之后给它传递一个时间戳或者Date对象。
总结一下,下文是如何为特定用途创建DateTimeFormat选项的例子(在当前Firefox执行行为下)。
var msPerDay = 24 * 60 * 60 * 1000; // July 17, 2014 00:00:00 UTC. var july172014 = new Date(msPerDay * (44 * 365 + 11 + 197));
我们来格式化美国英语的date。我们先创建一个2位数字的month/day/year, 加上2位数字的hours/minutes, 还有一个短时区来确定这个时间。(结果肯定因不同时区而明显不同)
var options = { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", timeZoneName: "short" }; var americanDateTime = new Intl.DateTimeFormat("en-US", options).format; print(americanDateTime(july172014)); // 07/16/14, 5:00 PM PDT
或者类似的,对葡萄牙语,最好是巴西使用的葡语,但是用在葡萄牙作品中的。格式会稍长,因为包含完整的year和正式拼写的month,但是因移植性要转成UTC格式。
var options = { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", timeZoneName: "short", timeZone: "UTC" }; var portugueseTime = new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options); // 17 de julho de 2014 00:00 GMT print(portugueseTime.format(july172014));
那对于一个压缩的,UTC格式的瑞士火车每周调度表?我们尝试用正式语言按流行度从大到小来选择最易读的一个。
var swissLocales = ["de-CH", "fr-CH", "it-CH", "rm-CH"];var options = { weekday: "short", hour: "numeric", minute: "numeric", timeZone: "UTC", timeZoneName: "short" }; var swissTime = new Intl.DateTimeFormat(swissLocales, options).format; print(swissTime(july172014)); // Do. 00:00 GMT
或者我们尝试某个日本博物馆里的一幅画中的描述性文本中的date,这个date使用日本的year和era历法。
var jpYearEra = new Intl.DateTimeFormat("ja-JP-u-ca-japanese", { year: "numeric", era: "long" }); print(jpYearEra.format(july172014)); // 平成26年
对一些完全不同的、更长的date,用于泰国泰语,但是使用泰语数字系统和中国历法。(类似Firefox的高质量实现通常会将普通的th-TH当做th-TH-u-ca-buddhist-nu-latn, 因为泰国使用佛历系统和拉丁0-9数字)。
var options = { year: "numeric", month: "long", day: "numeric" }; var thaiDate = new Intl.DateTimeFormat("th-TH-u-nu-thai-ca-chinese", options); print(thaiDate.format(july172014)); // ?? 6 ??
撇开历法和数字系统,还是很简单的。只要选取自己的组件和长度。
选项
数字格式化的主要选项属性有:
"currency", "percent", or "decimal" (默认值).
3字母货币代码,如USD、CHF。需要style是"currency", 不然没有意义
"code", "symbol", or "name", 默认为"symbol". "code"使用格式字符串的3字母货币代码。"symbol"使用货币符号,如$或£。"name"通常使用某些正式拼写版本的货币。(Firefox 目前仅支持"symbol", 这个问题不就就修复)
范围1到21(包含)的整数,默认为1。结果字符串的整数部分如果没有这么长,它前面会用0来填充。 (若这个值为2,那么3的格式化形式为“03”。)
0-20(包含)的整数。结果字符串至少minimumFractionDigits, 至多maximumFractionDigits个有效数字。如果style是"currency",默认最小值跟货币有关(通常是2,很少0或者3),不然就是0。默认最大值,百分比是0,数字是3,货币的最大值跟货币有关。
1-21(包含)的整数。如果有,它们将会覆盖上文关于整数/分数的对数字的控制,而由它们以及数字的要求长度,共同确定格式化的数字字符串中最小/最大有效数字的值。 (注意对10的倍数,有效数字可能不准确,如100,它的1,2,3位有效数字。)
布尔值(默认true)决定格式化字符串是否包含分组分隔符(如,英语的千分隔符“,”)。
NumberFormat还识别难懂的、大多数可忽略的localeMatcher属性。
区域化选项
在Unicode扩展中,使用nu关键字可以使DateTimeFormat支持自定义数字系统,NumberFormat也是这样。 例如,在中国,中文的语言标签是zh-CN。 汉语十进制数字系统对应的值是hanidec。 为了格式化这些系统的数字,我们在这些语言标签上添加一些Unicode扩展:zh-CN-u-nu-hanidec。
关于不同数字系统标识的完整信息,见NumberFormat详细说明文档。
示例
NumberFormat对象有一个 format方法,这一点和 DateTimeFormat相同。 format方法是一个有界函数,它有时可以独立于 NumberFormat使用。
下面是在当前Firefox环境下为特定用途创建NumberFormat选项的例子。首先,我们来格式化中国大陆中文的货币格式,特别是使用汉字数字(而不是更普遍的拉丁数字)。选择"currency" style, 然后使用renminbi(yuan)的货币代码,数字分组默认,小数部分数字采用通常做法。
var tibetanRMBInChina = new Intl.NumberFormat("zh-CN-u-nu-hanidec", { style: "currency", currency: "CNY" }); print(tibetanRMBInChina.format(1314.25)); // ¥ 一,三一四.二五
或者我们来格式化美国的天然气价格,千分位有个古怪的9,用在美国英语中。
var gasPrice = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 3 }); print(gasPrice.format(5.259)); // $5.259
或者我们尝试埃及的阿拉伯语中的百分比。确定百分比有至少2个有效数字。(注意这个以及其他RTL例子可能在RTL上下文出现的顺序不一样,如?????? 而不是??????,RTL/right to left,从右到左的)
var arabicPercent = new Intl.NumberFormat("ar-EG", { style: "percent", minimumFractionDigits: 2 }).format; print(arabicPercent(0.438)); // ??????
或者假设我们格式化阿富汗波斯语,我们期望至少2位的整数部分,至多2位的小数部分。
var persianDecimal = new Intl.NumberFormat("fa-AF", { minimumIntegerDigits: 2, maximumFractionDigits: 2 }); print(persianDecimal.format(3.1416)); // ?????
最后,我们格式化巴林的阿拉伯语中的巴林第纳尔的数量。不同于大部分货币,巴林第纳尔等于1000费尔,因此我们需要三位小数位。(再次注意,不要太过相信表面的阅读顺序)?
var bahrainiDinars = new Intl.NumberFormat("ar-BH", { style: "currency", currency: "BHD" }); print(bahrainiDinars.format(3.17)); // ?.?.? ?????
整理
选项
下面是整理的主要选项属性:
"sort" or "search" (默认"sort"), 指定整理器(collator)的目的。(查找整理器可能比排序整理器更多考虑字符串是否相等。)
"base", "accent", "case", or "variant"。这个选项影响整理器对基本字符相同但是重音/变音不同的字符的敏感性。(基本字符与区域设置有关:“a”和“?”在德语中基本字符相同,但是瑞典语中是不同的。) "base"敏感性只考虑基本字符,忽略各种变体(如德语中“a”, “A”,和“?”被认为是相同的)。 "accent"敏感性考虑基本字符和重音,但是忽略大小写(如德语中的“a和“A”是相同的,“?”与两者不同)。"case"考虑基本字符和大小写,而忽略重音(如德语中的“a”和“?”相同,而与“A”不同)。"variant"考虑基本字符、重音、大小写(如德语的“a”, “?”和“A”均不同)。如果usage是"sort",默认"variant"; 否则与区域设置有关。
默认false的布尔值,决定字符串中的数字是否被当做数字参与比较。如当做数字的排序结果可能是"F-4 Phantom II", "F-14 Tomcat", "F-35 Lightning II"; 不当做数字的结果"F-14 Tomcat", "F-35 Lightning II", "F-4 Phantom II".
"upper", "lower", or "false" (默认)。决定比较时是否考虑大小写:"upper"把大写放在前面("B", "a", "c"), "lower"把小写放前面("a", "c", "B"), "false"完全忽略大小写("a", "B", "c")。 (注意,目前Firefox完全忽略这个属性)
默认false的布尔值。决定比较时是否忽略标点(如"biweekly"和"bi-weekly"是否相等)。
localeMatcher属性你可以忽略了。
区域相关选项
区域的Unicode扩展部分指定的整理器主要选项是co,它选择排序操作的方式:电话本(phonebk), 字典 (dict), 还有其他。
另外,kn和kf这两个key可以选择拷贝选项对象的numeric和caseFirst属性。但是并不保证可用于语言标签,而且选项比语言标签的组件更清晰。所以最好在选项内部进行调整。
key-value对(paris)嵌入到Unicode的方式跟DateTimeFormat和NumberFormat相同; 想知道如何在语言标签中指定它们,可以查看对应的章节。
举例
整理器Collator对象有个比较函数属性。这个函数接受2个参数x和y, 如果x6a026eaa792646456731363697f99c09y返回正值,x==y返回0。对于格式化函数,比较是个有界函数(bound function),可以抽取出来做其他用途。
我们尝试来给德国德语的姓氏进行排序。德语中实际上有2种排序方式,电话本和字典。电话本排序强调读音,比如“?”, “?”等近似被扩展成“ae”, “oe”等。
var names = ["Hochberg", "H?nigswald", "Holzman"]; var germanPhonebook = new Intl.Collator("de-DE-u-co-phonebk"); //就像对["Hochberg", "Hoenigswald", "Holzman"]排序 // Hochberg, H?nigswald, Holzman print(names.sort(germanPhonebook.compare).join(", "));
有些德语单词使用变音符进行词形变化,所以在字典中排序会忽略变音符(除了两个单词仅仅变音符不同:schon,sch?n)。
var germanDictionary = new Intl.Collator("de-DE-u-co-dict"); //就像对["Hochberg", "Hoenigswald", "Holzman"]排序 // Hochberg, H?nigswald, Holzman print(names.sort(germanDictionary.compare).join(", "));
或者我们来对美国英语的Firefox的版本进行排序,这些字符串故意打错(大小写不同,随机重音,变音标记,额外的连字符)。我们希望根据版本号进行排序,所以要做numeric排序,这样字符串中的数字才会作为数字参与比较,而不是逐字符的。
var firefoxen = ["FireF?x 3.6", "Fire-fox 1.0", "Firefox 29", "Fírefox 3.5", "Fírefox 18"]; var usVersion = new Intl.Collator("en-US", { sensitivity: "base", numeric: true, ignorePunctuation: true }); // Fire-fox 1.0, Fírefox 3.5, FireF?x 3.6, Fírefox 18, Firefox 29 print(firefoxen.sort(usVersion.compare).join(", "));
最后,我们来做跟区域有关的字符串查找,查找忽略大小写和重音,并用于美国英语。
// Comparisons work with both composed and decomposed forms. var decoratedBrowsers = [ "A\u0362maya", // A?maya "CH\u035Br?me", // CH?r?me "Firefóx", "sAfàri", "o\u0323pERA", // ?pERA "I\u0352E", // I?E ]; var fuzzySearch = new Intl.Collator("en-US", { usage: "search", sensitivity: "base" }); function findBrowser(browser) { function cmp(other) { return fuzzySearch.compare(browser, other) === 0; } return cmp; } print(decoratedBrowsers.findIndex(findBrowser("Firêfox"))); // 2 print(decoratedBrowsers.findIndex(findBrowser("Saf?ri"))); // 3 print(decoratedBrowsers.findIndex(findBrowser("?maya"))); // 0 print(decoratedBrowsers.findIndex(findBrowser("?pera"))); // 4 print(decoratedBrowsers.findIndex(findBrowser("Chromè"))); // 1 print(decoratedBrowsers.findIndex(findBrowser("I?"))); // 5
琐碎
检测某个操作是否支持特定区域,或者一个区域是否被支持,会很有用。Intl在每个构造函数上都提供了supportedLocales()函数,在每个原型上提供了resolvedOptions()函数来公开这些信息。
var navajoLocales = Intl.Collator.supportedLocalesOf(["nv"], { usage: "sort" }); print(navajoLocales.length > 0 ? "Navajo collation supported" : "Navajo collation not supported"); var germanFakeRegion = new Intl.DateTimeFormat("de-XX", { timeZone: "UTC" }); var usedOptions = germanFakeRegion.resolvedOptions(); print(usedOptions.locale); // de print(usedOptions.timeZone); // UTC
遗留行为
ES5的toLocaleString和localeCompar函数之前没有特定语义,不接受特定选项,基本上没有任何用处。因此i18n API根据Intl操作重组了它们。现在每个方法都接受附加的尾随locales和options参数,它们会像Intl构造函数一样被解释。(除了toLocaleTimeString 和 toLocaleDateString, 如果options没有,它们就会使用不同的默认组件)
对并不关心精确行为的简单应用,这没关系,老方法也可以用。但是如果需要更多控制或者多次格式化、比较的话,最好直接使用Intl。