From ac8f37dc7c32df9fe90242abf46005a2e2f79479 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Sat, 8 Oct 2016 17:13:22 -0700 Subject: [PATCH] Move Locale to its own file and rewrite it to follow the spec --- index.js | 3 + src/11.numberformat.js | 2 +- src/14.locale.js | 157 +++++++++++++++++++++++++++++++++++++++++ src/8.intl.js | 152 +-------------------------------------- src/9.negotiation.js | 2 +- src/core.js | 2 + 6 files changed, 165 insertions(+), 153 deletions(-) create mode 100644 src/14.locale.js diff --git a/index.js b/index.js index 849aed552..e726f7968 100644 --- a/index.js +++ b/index.js @@ -23,5 +23,8 @@ let loc3 = new global.IntlPolyfill.Locale(x, { numeric: false, caseFirst: "false" }); + +loc3.foo = 'foo'; + console.log(loc3); console.log(loc3.toString()); diff --git a/src/11.numberformat.js b/src/11.numberformat.js index 87186e7f7..601a577fa 100644 --- a/src/11.numberformat.js +++ b/src/11.numberformat.js @@ -898,7 +898,7 @@ function ToRawFixed(x, minInteger, minFraction, maxFraction) { // Sect 11.3.2 Table 2, Numbering systems // ====================================== -let numSys = { +export let numSys = { arab: ['\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668', '\u0669'], arabext: ['\u06F0', '\u06F1', '\u06F2', '\u06F3', '\u06F4', '\u06F5', '\u06F6', '\u06F7', '\u06F8', '\u06F9'], bali: ['\u1B50', '\u1B51', '\u1B52', '\u1B53', '\u1B54', '\u1B55', '\u1B56', '\u1B57', '\u1B58', '\u1B59'], diff --git a/src/14.locale.js b/src/14.locale.js new file mode 100644 index 000000000..753dd3274 --- /dev/null +++ b/src/14.locale.js @@ -0,0 +1,157 @@ +import { + Intl +} from "./8.intl.js"; + +import { + UnicodeExtensionSubtags, + expUnicodeExSeq +} from "./9.negotiation.js"; + +import { + IsStructurallyValidLanguageTag, + CanonicalizeLanguageTag, + toLatinUpperCase, + IsWellFormedCurrencyCode +} from "./6.locales-currencies-tz.js"; + +import { + numSys +} from "./11.numberformat.js"; + +import { + defineProperty, + Record +} from "./util.js"; + +const keyToOptionMap = { + 'ca': 'calendar', + 'co': 'collation', + 'cu': 'currency', + 'hc': 'hourCycle', + 'kf': 'caseFirst', + 'kn': 'numeric', + 'nu': 'numberingSystem', + 'tz': 'timeZone' +}; + +const optionToKeyMap = {}; +for (let key in keyToOptionMap) { + let option = keyToOptionMap[key]; + optionToKeyMap[option] = key; +} + +function DeconstructLanguageTag(locale) { + let result = new Record(); + + let noExtensionsLoc = String(locale).replace(expUnicodeExSeq, ''); + let extension = locale.match(expUnicodeExSeq)[0]; + result['[[Locale]]'] = noExtensionsLoc; + result['[[Extension]]'] = extension; + + return result; +} + +function IsOptionValueSupported(option, value) { + switch (option) { + case 'calendar': + //XXX: Get the list of available calendars + return true; + case 'collation': + //XXX: Get the list of available collations + return true; + case 'currency': + return IsWellFormedCurrencyCode(value); + case 'hourCycle': + return ['h12', 'h24'].includes(value); + case 'caseFirst': + return ['upper', 'lower', false].includes(value); + case 'numeric': + return typeof value === 'boolean'; + case 'numberingSystem': + return numSys.hasOwnProperty(value); + case 'timeZone': + //XXX: accept more time zones + return toLatinUpperCase(value) === 'UTC'; + } + throw new RangeError(`Unknown option ${option}`); +} + +class Locale { + constructor(tag, options = {}) { + if (!IsStructurallyValidLanguageTag(tag)) { + throw new RangeError(`${tag} is not a structurally valid language tag`); + } + let locale = this; + let canonicalizedTag = CanonicalizeLanguageTag(tag); + let r = DeconstructLanguageTag(canonicalizedTag); + this.locale = r['[[Locale]]']; + + let optionKeys = Object.keys(options); + for (let key in optionKeys) { + if (optionToKeyMap.hasOwnProperty(key)) { + let value = options[key]; + if (!IsOptionValueSupported(key, value)) { + throw new RangeError(`Invalid value ${value} for option ${key}`); + } + locale[key] = value; + } + } + if (r['[[Extension]]']) { + let unicodeSubtags = UnicodeExtensionSubtags(r['[[Extension]]']); + let i = 0; + let len = unicodeSubtags.length; + while (i < len) { + let key = unicodeSubtags[i]; + if (keyToOptionMap.hasOwnProperty(key)) { + i += 1; + let value = unicodeSubtags[i]; + + if (value === 'true') value = true; + if (value === 'false') value = false; + let name = keyToOptionMap[key]; + if (!IsOptionValueSupported(name, value)) { + throw new RangeError(`Invalid value ${value} for option ${name}`); + } + if (!locale.hasOwnProperty(name)) { + locale[name] = value; + } + } + i += 1; + } + } + Object.freeze(locale); + } + + toString() { + let loc = this; + + if (typeof loc !== 'object') { + throw new TypeError(); + } + let locale = loc.locale; + let unicodeExt = ''; + + for (let name of Object.keys(optionToKeyMap)) { + let key = optionToKeyMap[name]; + if (loc.hasOwnProperty(name)) { + let value = loc[name]; + let kvp = `${key}-${value}`; + if (unicodeExt.length) { + unicodeExt = `${unicodeExt}-`; + } + unicodeExt = `${unicodeExt}${kvp}`; + } + } + //XXX: Consider caching the value since the object is frozen + if (unicodeExt.length) { + return `${locale}-${unicodeExt}`; + } + return locale; + } +} + +defineProperty(Intl, 'Locale', { + configurable: true, + writable: true, + value: Locale +}); diff --git a/src/8.intl.js b/src/8.intl.js index 57a125905..2ab6f5de3 100644 --- a/src/8.intl.js +++ b/src/8.intl.js @@ -1,13 +1,7 @@ import { - CanonicalizeLocaleList, - UnicodeExtensionSubtags + CanonicalizeLocaleList } from "./9.negotiation.js"; -import { - IsStructurallyValidLanguageTag, - CanonicalizeLanguageTag -} from "./6.locales-currencies-tz.js"; - // 8 The Intl Object export const Intl = {}; @@ -40,147 +34,3 @@ Object.defineProperty(Intl, 'getCanonicalLocales', { writable: true, value: getCanonicalLocales }); - -const optionValues = { - calendar: { - values: ['buddhist', 'chinese', 'coptic', 'ethioaa', 'ethiopic', 'gregory', 'hebrew', 'indian', 'islamic', 'islamicc', 'iso8601', 'japanese', 'persian', 'roc'], - key: 'ca' - }, - collation: { - values: ["big5han", "dict", "direct", "ducet", "gb2312", "phonebk", "phonetic", "pinyin", "reformed", "searchjl", "stroke", "trad", "unihan", "standard", "search"], - key: 'co' - }, - currency: { - values: ["eur", "usd", "pln"], - key: 'cu' - }, - hourCycle: { - values: ['h12', 'h24'], - key: 'hc' - }, - caseFirst: { - values: ["upper", "lower", "false"], - key: 'kf' - }, - numeric: { - values: ["true", "false"], - key: 'kn' - }, - numberingSystem: { - values: ["arab", "arabext", "bali", "beng", "deva", "fullwide", "gujr", "guru", "hanidec", "khmr", "knda", "laoo", "latn", "limb", "mlym", "mong", "mymr", "orya", "tamldec", "telu", "thai", "tibt"], - key: 'nu' - }, - timeZone: { - key: 'tz' - } -}; - -const keyToOptionMap = { - 'ca': 'calendar', - 'co': 'collation', - 'cu': 'currency', - 'hc': 'hourCycle', - 'kf': 'caseFirst', - 'kn': 'numeric', - 'nu': 'numberingSystem', - 'tz': 'timeZone' -}; - - -const expExSeq = /-([xtu])(?:-[0-9a-z]{2,8})+/gi; - -function deconstructLocaleString(locale) { - let res = { - locale: null, - extensions: {} - }; - - let r; - let extensionIndex; - while ((r = expExSeq.exec(locale)) !== null) { - if (extensionIndex === undefined) { - extensionIndex = r.index; - } - res.extensions[r[1]] = r[0]; - } - if (extensionIndex !== undefined) { - res.locale = locale.substring(0, extensionIndex); - } else { - res.locale = locale; - } - return res; -} - -class Locale { - constructor(locale, options = {}) { - if (!IsStructurallyValidLanguageTag(locale)) { - throw new RangeError(`${locale} is not a structurally valid language tag`); - } - let loc = CanonicalizeLanguageTag(locale); - let res = deconstructLocaleString(loc); - this.locale = res.locale; - - for (let name in options) { - if (!optionValues.hasOwnProperty(name)) { - continue; - } - - let value = options[name]; - - if (value === false) value = "false"; - if (value === true) value = "true"; - - if (optionValues[name].hasOwnProperty('values') && - !optionValues[name].values.includes(value)) { - throw new RangeError(`Invalid value ${value} for option ${name}`); - } - this[name] = options[name]; - } - - if (!res.extensions.hasOwnProperty('u')) { - return; - } - let extensionSubtags = UnicodeExtensionSubtags(res.extensions['u']); - - for (let i = 0; i < extensionSubtags.length; i++) { - let key = extensionSubtags[i]; - if (keyToOptionMap.hasOwnProperty(key)) { - let value = extensionSubtags[++i]; - - let name = keyToOptionMap[key]; - if (this.hasOwnProperty(name)) { - continue; - } - if (optionValues[name].hasOwnProperty('values') && - !optionValues[name].values.includes(value)) { - throw new RangeError(`Invalid value ${value} for key ${key}`); - } - this[name] = value; - } - } - Object.freeze(this); - } - - toString() { - let exts = []; - - for (let key in keyToOptionMap) { - let name = keyToOptionMap[key]; - if (!this.hasOwnProperty(name)) { - continue; - } - exts.push(`${key}-${this[name]}`); - } - if (exts.length) { - return `${this.locale}-u-${exts.join('-')}`; - } - return this.locale; - } -} - -Object.defineProperty(Intl, 'Locale', { - enumerable: false, - configurable: true, - writable: true, - value: Locale -}); diff --git a/src/9.negotiation.js b/src/9.negotiation.js index 4d8818362..03681068f 100644 --- a/src/9.negotiation.js +++ b/src/9.negotiation.js @@ -19,7 +19,7 @@ import { DefaultLocale } from "./6.locales-currencies-tz.js"; -const expUnicodeExSeq = /-u(?:-[0-9a-z]{2,8})+/gi; // See `extension` below +export const expUnicodeExSeq = /-u(?:-[0-9a-z]{2,8})+/gi; // See `extension` below export function /* 9.2.1 */CanonicalizeLocaleList (locales) { // The abstract operation CanonicalizeLocaleList takes the following steps: diff --git a/src/core.js b/src/core.js index 985bc17b4..316ec6a2f 100644 --- a/src/core.js +++ b/src/core.js @@ -33,6 +33,8 @@ import "./12.datetimeformat.js"; import ls from "./13.locale-sensitive-functions.js"; +import "./14.locale.js"; + defineProperty(Intl, '__applyLocaleSensitivePrototypes', { writable: true, configurable: true,