OLD | NEW |
| (Empty) |
1 // Copyright 2013 the V8 project authors. All rights reserved. | |
2 // Redistribution and use in source and binary forms, with or without | |
3 // modification, are permitted provided that the following conditions are | |
4 // met: | |
5 // | |
6 // * Redistributions of source code must retain the above copyright | |
7 // notice, this list of conditions and the following disclaimer. | |
8 // * Redistributions in binary form must reproduce the above | |
9 // copyright notice, this list of conditions and the following | |
10 // disclaimer in the documentation and/or other materials provided | |
11 // with the distribution. | |
12 // * Neither the name of Google Inc. nor the names of its | |
13 // contributors may be used to endorse or promote products derived | |
14 // from this software without specific prior written permission. | |
15 // | |
16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
17 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
18 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
19 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
20 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
21 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
22 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
23 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
24 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
25 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
26 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
27 // limitations under the License. | |
28 | |
29 // ECMAScript 402 API implementation is broken into separate files for | |
30 // each service. The build system combines them together into one | |
31 // Intl namespace. | |
32 | |
33 /** | |
34 * Adds bound method to the prototype of the given object. | |
35 */ | |
36 function addBoundMethod(obj, methodName, implementation, length) { | |
37 function getter() { | |
38 if (!this || typeof this !== 'object' || | |
39 this.__initializedIntlObject === undefined) { | |
40 throw new TypeError('Method ' + methodName + ' called on a ' + | |
41 'non-object or on a wrong type of object.'); | |
42 } | |
43 var internalName = '__bound' + methodName + '__'; | |
44 if (this[internalName] === undefined) { | |
45 var that = this; | |
46 var boundMethod; | |
47 if (length === undefined || length === 2) { | |
48 boundMethod = function(x, y) { | |
49 if (%_IsConstructCall()) { | |
50 throw new TypeError(ORDINARY_FUNCTION_CALLED_AS_CONSTRUCTOR); | |
51 } | |
52 return implementation(that, x, y); | |
53 } | |
54 } else if (length === 1) { | |
55 boundMethod = function(x) { | |
56 if (%_IsConstructCall()) { | |
57 throw new TypeError(ORDINARY_FUNCTION_CALLED_AS_CONSTRUCTOR); | |
58 } | |
59 return implementation(that, x); | |
60 } | |
61 } else { | |
62 boundMethod = function() { | |
63 if (%_IsConstructCall()) { | |
64 throw new TypeError(ORDINARY_FUNCTION_CALLED_AS_CONSTRUCTOR); | |
65 } | |
66 // DateTimeFormat.format needs to be 0 arg method, but can stil | |
67 // receive optional dateValue param. If one was provided, pass it | |
68 // along. | |
69 if (arguments.length > 0) { | |
70 return implementation(that, arguments[0]); | |
71 } else { | |
72 return implementation(that); | |
73 } | |
74 } | |
75 } | |
76 %FunctionSetName(boundMethod, internalName); | |
77 %FunctionRemovePrototype(boundMethod); | |
78 %SetNativeFlag(boundMethod); | |
79 this[internalName] = boundMethod; | |
80 } | |
81 return this[internalName]; | |
82 } | |
83 | |
84 %FunctionSetName(getter, methodName); | |
85 %FunctionRemovePrototype(getter); | |
86 %SetNativeFlag(getter); | |
87 | |
88 Object.defineProperty(obj.prototype, methodName, { | |
89 get: getter, | |
90 enumerable: false, | |
91 configurable: true | |
92 }); | |
93 } | |
94 | |
95 | |
96 /** | |
97 * Returns an intersection of locales and service supported locales. | |
98 * Parameter locales is treated as a priority list. | |
99 */ | |
100 function supportedLocalesOf(service, locales, options) { | |
101 if (service.match(SERVICE_RE) === null) { | |
102 throw new Error('Internal error, wrong service type: ' + service); | |
103 } | |
104 | |
105 // Provide defaults if matcher was not specified. | |
106 if (options === undefined) { | |
107 options = {}; | |
108 } else { | |
109 options = toObject(options); | |
110 } | |
111 | |
112 var matcher = options.localeMatcher; | |
113 if (matcher !== undefined) { | |
114 matcher = String(matcher); | |
115 if (matcher !== 'lookup' && matcher !== 'best fit') { | |
116 throw new RangeError('Illegal value for localeMatcher:' + matcher); | |
117 } | |
118 } else { | |
119 matcher = 'best fit'; | |
120 } | |
121 | |
122 var requestedLocales = initializeLocaleList(locales); | |
123 | |
124 // Cache these, they don't ever change per service. | |
125 if (AVAILABLE_LOCALES[service] === undefined) { | |
126 AVAILABLE_LOCALES[service] = getAvailableLocalesOf(service); | |
127 } | |
128 | |
129 // Use either best fit or lookup algorithm to match locales. | |
130 if (matcher === 'best fit') { | |
131 return initializeLocaleList(bestFitSupportedLocalesOf( | |
132 requestedLocales, AVAILABLE_LOCALES[service])); | |
133 } | |
134 | |
135 return initializeLocaleList(lookupSupportedLocalesOf( | |
136 requestedLocales, AVAILABLE_LOCALES[service])); | |
137 } | |
138 | |
139 | |
140 /** | |
141 * Returns the subset of the provided BCP 47 language priority list for which | |
142 * this service has a matching locale when using the BCP 47 Lookup algorithm. | |
143 * Locales appear in the same order in the returned list as in the input list. | |
144 */ | |
145 function lookupSupportedLocalesOf(requestedLocales, availableLocales) { | |
146 var matchedLocales = []; | |
147 for (var i = 0; i < requestedLocales.length; ++i) { | |
148 // Remove -u- extension. | |
149 var locale = requestedLocales[i].replace(UNICODE_EXTENSION_RE, ''); | |
150 do { | |
151 if (availableLocales[locale] !== undefined) { | |
152 // Push requested locale not the resolved one. | |
153 matchedLocales.push(requestedLocales[i]); | |
154 break; | |
155 } | |
156 // Truncate locale if possible, if not break. | |
157 var pos = locale.lastIndexOf('-'); | |
158 if (pos === -1) { | |
159 break; | |
160 } | |
161 locale = locale.substring(0, pos); | |
162 } while (true); | |
163 } | |
164 | |
165 return matchedLocales; | |
166 } | |
167 | |
168 | |
169 /** | |
170 * Returns the subset of the provided BCP 47 language priority list for which | |
171 * this service has a matching locale when using the implementation | |
172 * dependent algorithm. | |
173 * Locales appear in the same order in the returned list as in the input list. | |
174 */ | |
175 function bestFitSupportedLocalesOf(requestedLocales, availableLocales) { | |
176 // TODO(cira): implement better best fit algorithm. | |
177 return lookupSupportedLocalesOf(requestedLocales, availableLocales); | |
178 } | |
179 | |
180 | |
181 /** | |
182 * Returns a getOption function that extracts property value for given | |
183 * options object. If property is missing it returns defaultValue. If value | |
184 * is out of range for that property it throws RangeError. | |
185 */ | |
186 function getGetOption(options, caller) { | |
187 if (options === undefined) { | |
188 throw new Error('Internal ' + caller + ' error. ' + | |
189 'Default options are missing.'); | |
190 } | |
191 | |
192 var getOption = function getOption(property, type, values, defaultValue) { | |
193 if (options[property] !== undefined) { | |
194 var value = options[property]; | |
195 switch (type) { | |
196 case 'boolean': | |
197 value = Boolean(value); | |
198 break; | |
199 case 'string': | |
200 value = String(value); | |
201 break; | |
202 case 'number': | |
203 value = Number(value); | |
204 break; | |
205 default: | |
206 throw new Error('Internal error. Wrong value type.'); | |
207 } | |
208 if (values !== undefined && values.indexOf(value) === -1) { | |
209 throw new RangeError('Value ' + value + ' out of range for ' + caller + | |
210 ' options property ' + property); | |
211 } | |
212 | |
213 return value; | |
214 } | |
215 | |
216 return defaultValue; | |
217 } | |
218 | |
219 return getOption; | |
220 } | |
221 | |
222 | |
223 /** | |
224 * Compares a BCP 47 language priority list requestedLocales against the locales | |
225 * in availableLocales and determines the best available language to meet the | |
226 * request. Two algorithms are available to match the locales: the Lookup | |
227 * algorithm described in RFC 4647 section 3.4, and an implementation dependent | |
228 * best-fit algorithm. Independent of the locale matching algorithm, options | |
229 * specified through Unicode locale extension sequences are negotiated | |
230 * separately, taking the caller's relevant extension keys and locale data as | |
231 * well as client-provided options into consideration. Returns an object with | |
232 * a locale property whose value is the language tag of the selected locale, | |
233 * and properties for each key in relevantExtensionKeys providing the selected | |
234 * value for that key. | |
235 */ | |
236 function resolveLocale(service, requestedLocales, options) { | |
237 requestedLocales = initializeLocaleList(requestedLocales); | |
238 | |
239 var getOption = getGetOption(options, service); | |
240 var matcher = getOption('localeMatcher', 'string', | |
241 ['lookup', 'best fit'], 'best fit'); | |
242 var resolved; | |
243 if (matcher === 'lookup') { | |
244 resolved = lookupMatcher(service, requestedLocales); | |
245 } else { | |
246 resolved = bestFitMatcher(service, requestedLocales); | |
247 } | |
248 | |
249 return resolved; | |
250 } | |
251 | |
252 | |
253 /** | |
254 * Returns best matched supported locale and extension info using basic | |
255 * lookup algorithm. | |
256 */ | |
257 function lookupMatcher(service, requestedLocales) { | |
258 if (service.match(SERVICE_RE) === null) { | |
259 throw new Error('Internal error, wrong service type: ' + service); | |
260 } | |
261 | |
262 // Cache these, they don't ever change per service. | |
263 if (AVAILABLE_LOCALES[service] === undefined) { | |
264 AVAILABLE_LOCALES[service] = getAvailableLocalesOf(service); | |
265 } | |
266 | |
267 for (var i = 0; i < requestedLocales.length; ++i) { | |
268 // Remove all extensions. | |
269 var locale = requestedLocales[i].replace(ANY_EXTENSION_RE, ''); | |
270 do { | |
271 if (AVAILABLE_LOCALES[service][locale] !== undefined) { | |
272 // Return the resolved locale and extension. | |
273 var extensionMatch = requestedLocales[i].match(UNICODE_EXTENSION_RE); | |
274 var extension = (extensionMatch === null) ? '' : extensionMatch[0]; | |
275 return {'locale': locale, 'extension': extension, 'position': i}; | |
276 } | |
277 // Truncate locale if possible. | |
278 var pos = locale.lastIndexOf('-'); | |
279 if (pos === -1) { | |
280 break; | |
281 } | |
282 locale = locale.substring(0, pos); | |
283 } while (true); | |
284 } | |
285 | |
286 // Didn't find a match, return default. | |
287 if (DEFAULT_ICU_LOCALE === undefined) { | |
288 DEFAULT_ICU_LOCALE = %GetDefaultICULocale(); | |
289 } | |
290 | |
291 return {'locale': DEFAULT_ICU_LOCALE, 'extension': '', 'position': -1}; | |
292 } | |
293 | |
294 | |
295 /** | |
296 * Returns best matched supported locale and extension info using | |
297 * implementation dependend algorithm. | |
298 */ | |
299 function bestFitMatcher(service, requestedLocales) { | |
300 // TODO(cira): implement better best fit algorithm. | |
301 return lookupMatcher(service, requestedLocales); | |
302 } | |
303 | |
304 | |
305 /** | |
306 * Parses Unicode extension into key - value map. | |
307 * Returns empty object if the extension string is invalid. | |
308 * We are not concerned with the validity of the values at this point. | |
309 */ | |
310 function parseExtension(extension) { | |
311 var extensionSplit = extension.split('-'); | |
312 | |
313 // Assume ['', 'u', ...] input, but don't throw. | |
314 if (extensionSplit.length <= 2 || | |
315 (extensionSplit[0] !== '' && extensionSplit[1] !== 'u')) { | |
316 return {}; | |
317 } | |
318 | |
319 // Key is {2}alphanum, value is {3,8}alphanum. | |
320 // Some keys may not have explicit values (booleans). | |
321 var extensionMap = {}; | |
322 var previousKey = undefined; | |
323 for (var i = 2; i < extensionSplit.length; ++i) { | |
324 var length = extensionSplit[i].length; | |
325 var element = extensionSplit[i]; | |
326 if (length === 2) { | |
327 extensionMap[element] = undefined; | |
328 previousKey = element; | |
329 } else if (length >= 3 && length <=8 && previousKey !== undefined) { | |
330 extensionMap[previousKey] = element; | |
331 previousKey = undefined; | |
332 } else { | |
333 // There is a value that's too long, or that doesn't have a key. | |
334 return {}; | |
335 } | |
336 } | |
337 | |
338 return extensionMap; | |
339 } | |
340 | |
341 | |
342 /** | |
343 * Converts parameter to an Object if possible. | |
344 */ | |
345 function toObject(value) { | |
346 if (value === undefined || value === null) { | |
347 throw new TypeError('Value cannot be converted to an Object.'); | |
348 } | |
349 | |
350 return Object(value); | |
351 } | |
352 | |
353 | |
354 /** | |
355 * Populates internalOptions object with boolean key-value pairs | |
356 * from extensionMap and options. | |
357 * Returns filtered extension (number and date format constructors use | |
358 * Unicode extensions for passing parameters to ICU). | |
359 * It's used for extension-option pairs only, e.g. kn-normalization, but not | |
360 * for 'sensitivity' since it doesn't have extension equivalent. | |
361 * Extensions like nu and ca don't have options equivalent, so we place | |
362 * undefined in the map.property to denote that. | |
363 */ | |
364 function setOptions(inOptions, extensionMap, keyValues, getOption, outOptions) { | |
365 var extension = ''; | |
366 | |
367 var updateExtension = function updateExtension(key, value) { | |
368 return '-' + key + '-' + String(value); | |
369 } | |
370 | |
371 var updateProperty = function updateProperty(property, type, value) { | |
372 if (type === 'boolean' && (typeof value === 'string')) { | |
373 value = (value === 'true') ? true : false; | |
374 } | |
375 | |
376 if (property !== undefined) { | |
377 defineWEProperty(outOptions, property, value); | |
378 } | |
379 } | |
380 | |
381 for (var key in keyValues) { | |
382 if (keyValues.hasOwnProperty(key)) { | |
383 var value = undefined; | |
384 var map = keyValues[key]; | |
385 if (map.property !== undefined) { | |
386 // This may return true if user specifies numeric: 'false', since | |
387 // Boolean('nonempty') === true. | |
388 value = getOption(map.property, map.type, map.values); | |
389 } | |
390 if (value !== undefined) { | |
391 updateProperty(map.property, map.type, value); | |
392 extension += updateExtension(key, value); | |
393 continue; | |
394 } | |
395 // User options didn't have it, check Unicode extension. | |
396 // Here we want to convert strings 'true', 'false' into proper Boolean | |
397 // values (not a user error). | |
398 if (extensionMap.hasOwnProperty(key)) { | |
399 value = extensionMap[key]; | |
400 if (value !== undefined) { | |
401 updateProperty(map.property, map.type, value); | |
402 extension += updateExtension(key, value); | |
403 } else if (map.type === 'boolean') { | |
404 // Boolean keys are allowed not to have values in Unicode extension. | |
405 // Those default to true. | |
406 updateProperty(map.property, map.type, true); | |
407 extension += updateExtension(key, true); | |
408 } | |
409 } | |
410 } | |
411 } | |
412 | |
413 return extension === ''? '' : '-u' + extension; | |
414 } | |
415 | |
416 | |
417 /** | |
418 * Converts all OwnProperties into | |
419 * configurable: false, writable: false, enumerable: true. | |
420 */ | |
421 function freezeArray(array) { | |
422 array.forEach(function(element, index) { | |
423 Object.defineProperty(array, index, {value: element, | |
424 configurable: false, | |
425 writable: false, | |
426 enumerable: true}); | |
427 }); | |
428 | |
429 Object.defineProperty(array, 'length', {value: array.length, | |
430 writable: false}); | |
431 | |
432 return array; | |
433 } | |
434 | |
435 | |
436 /** | |
437 * It's sometimes desireable to leave user requested locale instead of ICU | |
438 * supported one (zh-TW is equivalent to zh-Hant-TW, so we should keep shorter | |
439 * one, if that was what user requested). | |
440 * This function returns user specified tag if its maximized form matches ICU | |
441 * resolved locale. If not we return ICU result. | |
442 */ | |
443 function getOptimalLanguageTag(original, resolved) { | |
444 // Returns Array<Object>, where each object has maximized and base properties. | |
445 // Maximized: zh -> zh-Hans-CN | |
446 // Base: zh-CN-u-ca-gregory -> zh-CN | |
447 // Take care of grandfathered or simple cases. | |
448 if (original === resolved) { | |
449 return original; | |
450 } | |
451 | |
452 var locales = %GetLanguageTagVariants([original, resolved]); | |
453 if (locales[0].maximized !== locales[1].maximized) { | |
454 return resolved; | |
455 } | |
456 | |
457 // Preserve extensions of resolved locale, but swap base tags with original. | |
458 var resolvedBase = new RegExp('^' + locales[1].base); | |
459 return resolved.replace(resolvedBase, locales[0].base); | |
460 } | |
461 | |
462 | |
463 /** | |
464 * Returns an Object that contains all of supported locales for a given | |
465 * service. | |
466 * In addition to the supported locales we add xx-ZZ locale for each xx-Yyyy-ZZ | |
467 * that is supported. This is required by the spec. | |
468 */ | |
469 function getAvailableLocalesOf(service) { | |
470 var available = %AvailableLocalesOf(service); | |
471 | |
472 for (var i in available) { | |
473 if (available.hasOwnProperty(i)) { | |
474 var parts = i.match(/^([a-z]{2,3})-([A-Z][a-z]{3})-([A-Z]{2})$/); | |
475 if (parts !== null) { | |
476 // Build xx-ZZ. We don't care about the actual value, | |
477 // as long it's not undefined. | |
478 available[parts[1] + '-' + parts[3]] = null; | |
479 } | |
480 } | |
481 } | |
482 | |
483 return available; | |
484 } | |
485 | |
486 | |
487 /** | |
488 * Defines a property and sets writable and enumerable to true. | |
489 * Configurable is false by default. | |
490 */ | |
491 function defineWEProperty(object, property, value) { | |
492 Object.defineProperty(object, property, | |
493 {value: value, writable: true, enumerable: true}); | |
494 } | |
495 | |
496 | |
497 /** | |
498 * Adds property to an object if the value is not undefined. | |
499 * Sets configurable descriptor to false. | |
500 */ | |
501 function addWEPropertyIfDefined(object, property, value) { | |
502 if (value !== undefined) { | |
503 defineWEProperty(object, property, value); | |
504 } | |
505 } | |
506 | |
507 | |
508 /** | |
509 * Defines a property and sets writable, enumerable and configurable to true. | |
510 */ | |
511 function defineWECProperty(object, property, value) { | |
512 Object.defineProperty(object, property, | |
513 {value: value, | |
514 writable: true, | |
515 enumerable: true, | |
516 configurable: true}); | |
517 } | |
518 | |
519 | |
520 /** | |
521 * Adds property to an object if the value is not undefined. | |
522 * Sets all descriptors to true. | |
523 */ | |
524 function addWECPropertyIfDefined(object, property, value) { | |
525 if (value !== undefined) { | |
526 defineWECProperty(object, property, value); | |
527 } | |
528 } | |
529 | |
530 | |
531 /** | |
532 * Returns titlecased word, aMeRricA -> America. | |
533 */ | |
534 function toTitleCaseWord(word) { | |
535 return word.substr(0, 1).toUpperCase() + word.substr(1).toLowerCase(); | |
536 } | |
OLD | NEW |