UNPKG

24.9 kBJavaScriptView Raw
1// Sect 9.2 Abstract Operations
2// ============================
3
4import {
5 List,
6 toObject,
7 arrIndexOf,
8 arrPush,
9 arrSlice,
10 Record,
11 hop,
12 defineProperty,
13} from "./util.js";
14
15import {
16 IsStructurallyValidLanguageTag,
17 CanonicalizeLanguageTag,
18 DefaultLocale,
19} from "./6.locales-currencies-tz.js";
20
21const expUnicodeExSeq = /-u(?:-[0-9a-z]{2,8})+/gi; // See `extension` below
22
23export function /* 9.2.1 */CanonicalizeLocaleList (locales) {
24// The abstract operation CanonicalizeLocaleList takes the following steps:
25
26 // 1. If locales is undefined, then a. Return a new empty List
27 if (locales === undefined)
28 return new List();
29
30 // 2. Let seen be a new empty List.
31 let seen = new List();
32
33 // 3. If locales is a String value, then
34 // a. Let locales be a new array created as if by the expression new
35 // Array(locales) where Array is the standard built-in constructor with
36 // that name and locales is the value of locales.
37 locales = typeof locales === 'string' ? [ locales ] : locales;
38
39 // 4. Let O be ToObject(locales).
40 let O = toObject(locales);
41
42 // 5. Let lenValue be the result of calling the [[Get]] internal method of
43 // O with the argument "length".
44 // 6. Let len be ToUint32(lenValue).
45 let len = O.length;
46
47 // 7. Let k be 0.
48 let k = 0;
49
50 // 8. Repeat, while k < len
51 while (k < len) {
52 // a. Let Pk be ToString(k).
53 let Pk = String(k);
54
55 // b. Let kPresent be the result of calling the [[HasProperty]] internal
56 // method of O with argument Pk.
57 let kPresent = Pk in O;
58
59 // c. If kPresent is true, then
60 if (kPresent) {
61 // i. Let kValue be the result of calling the [[Get]] internal
62 // method of O with argument Pk.
63 let kValue = O[Pk];
64
65 // ii. If the type of kValue is not String or Object, then throw a
66 // TypeError exception.
67 if (kValue === null || (typeof kValue !== 'string' && typeof kValue !== 'object'))
68 throw new TypeError('String or Object type expected');
69
70 // iii. Let tag be ToString(kValue).
71 let tag = String(kValue);
72
73 // iv. If the result of calling the abstract operation
74 // IsStructurallyValidLanguageTag (defined in 6.2.2), passing tag as
75 // the argument, is false, then throw a RangeError exception.
76 if (!IsStructurallyValidLanguageTag(tag))
77 throw new RangeError("'" + tag + "' is not a structurally valid language tag");
78
79 // v. Let tag be the result of calling the abstract operation
80 // CanonicalizeLanguageTag (defined in 6.2.3), passing tag as the
81 // argument.
82 tag = CanonicalizeLanguageTag(tag);
83
84 // vi. If tag is not an element of seen, then append tag as the last
85 // element of seen.
86 if (arrIndexOf.call(seen, tag) === -1)
87 arrPush.call(seen, tag);
88 }
89
90 // d. Increase k by 1.
91 k++;
92 }
93
94 // 9. Return seen.
95 return seen;
96}
97
98/**
99 * The BestAvailableLocale abstract operation compares the provided argument
100 * locale, which must be a String value with a structurally valid and
101 * canonicalized BCP 47 language tag, against the locales in availableLocales and
102 * returns either the longest non-empty prefix of locale that is an element of
103 * availableLocales, or undefined if there is no such element. It uses the
104 * fallback mechanism of RFC 4647, section 3.4. The following steps are taken:
105 */
106export function /* 9.2.2 */BestAvailableLocale (availableLocales, locale) {
107 // 1. Let candidate be locale
108 let candidate = locale;
109
110 // 2. Repeat
111 while (candidate) {
112 // a. If availableLocales contains an element equal to candidate, then return
113 // candidate.
114 if (arrIndexOf.call(availableLocales, candidate) > -1)
115 return candidate;
116
117 // b. Let pos be the character index of the last occurrence of "-"
118 // (U+002D) within candidate. If that character does not occur, return
119 // undefined.
120 let pos = candidate.lastIndexOf('-');
121
122 if (pos < 0)
123 return;
124
125 // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate,
126 // then decrease pos by 2.
127 if (pos >= 2 && candidate.charAt(pos - 2) === '-')
128 pos -= 2;
129
130 // d. Let candidate be the substring of candidate from position 0, inclusive,
131 // to position pos, exclusive.
132 candidate = candidate.substring(0, pos);
133 }
134}
135
136/**
137 * The LookupMatcher abstract operation compares requestedLocales, which must be
138 * a List as returned by CanonicalizeLocaleList, against the locales in
139 * availableLocales and determines the best available language to meet the
140 * request. The following steps are taken:
141 */
142export function /* 9.2.3 */LookupMatcher (availableLocales, requestedLocales) {
143 // 1. Let i be 0.
144 let i = 0;
145
146 // 2. Let len be the number of elements in requestedLocales.
147 let len = requestedLocales.length;
148
149 // 3. Let availableLocale be undefined.
150 let availableLocale;
151
152 let locale, noExtensionsLocale;
153
154 // 4. Repeat while i < len and availableLocale is undefined:
155 while (i < len && !availableLocale) {
156 // a. Let locale be the element of requestedLocales at 0-origined list
157 // position i.
158 locale = requestedLocales[i];
159
160 // b. Let noExtensionsLocale be the String value that is locale with all
161 // Unicode locale extension sequences removed.
162 noExtensionsLocale = String(locale).replace(expUnicodeExSeq, '');
163
164 // c. Let availableLocale be the result of calling the
165 // BestAvailableLocale abstract operation (defined in 9.2.2) with
166 // arguments availableLocales and noExtensionsLocale.
167 availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
168
169 // d. Increase i by 1.
170 i++;
171 }
172
173 // 5. Let result be a new Record.
174 let result = new Record();
175
176 // 6. If availableLocale is not undefined, then
177 if (availableLocale !== undefined) {
178 // a. Set result.[[locale]] to availableLocale.
179 result['[[locale]]'] = availableLocale;
180
181 // b. If locale and noExtensionsLocale are not the same String value, then
182 if (String(locale) !== String(noExtensionsLocale)) {
183 // i. Let extension be the String value consisting of the first
184 // substring of locale that is a Unicode locale extension sequence.
185 let extension = locale.match(expUnicodeExSeq)[0];
186
187 // ii. Let extensionIndex be the character position of the initial
188 // "-" of the first Unicode locale extension sequence within locale.
189 let extensionIndex = locale.indexOf('-u-');
190
191 // iii. Set result.[[extension]] to extension.
192 result['[[extension]]'] = extension;
193
194 // iv. Set result.[[extensionIndex]] to extensionIndex.
195 result['[[extensionIndex]]'] = extensionIndex;
196 }
197 }
198 // 7. Else
199 else
200 // a. Set result.[[locale]] to the value returned by the DefaultLocale abstract
201 // operation (defined in 6.2.4).
202 result['[[locale]]'] = DefaultLocale();
203
204 // 8. Return result
205 return result;
206}
207
208/**
209 * The BestFitMatcher abstract operation compares requestedLocales, which must be
210 * a List as returned by CanonicalizeLocaleList, against the locales in
211 * availableLocales and determines the best available language to meet the
212 * request. The algorithm is implementation dependent, but should produce results
213 * that a typical user of the requested locales would perceive as at least as
214 * good as those produced by the LookupMatcher abstract operation. Options
215 * specified through Unicode locale extension sequences must be ignored by the
216 * algorithm. Information about such subsequences is returned separately.
217 * The abstract operation returns a record with a [[locale]] field, whose value
218 * is the language tag of the selected locale, which must be an element of
219 * availableLocales. If the language tag of the request locale that led to the
220 * selected locale contained a Unicode locale extension sequence, then the
221 * returned record also contains an [[extension]] field whose value is the first
222 * Unicode locale extension sequence, and an [[extensionIndex]] field whose value
223 * is the index of the first Unicode locale extension sequence within the request
224 * locale language tag.
225 */
226export function /* 9.2.4 */BestFitMatcher (availableLocales, requestedLocales) {
227 return LookupMatcher(availableLocales, requestedLocales);
228}
229
230/**
231 * The ResolveLocale abstract operation compares a BCP 47 language priority list
232 * requestedLocales against the locales in availableLocales and determines the
233 * best available language to meet the request. availableLocales and
234 * requestedLocales must be provided as List values, options as a Record.
235 */
236export function /* 9.2.5 */ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) {
237 if (availableLocales.length === 0) {
238 throw new ReferenceError('No locale data has been provided for this object yet.');
239 }
240
241 // The following steps are taken:
242 // 1. Let matcher be the value of options.[[localeMatcher]].
243 let matcher = options['[[localeMatcher]]'];
244
245 let r;
246
247 // 2. If matcher is "lookup", then
248 if (matcher === 'lookup')
249 // a. Let r be the result of calling the LookupMatcher abstract operation
250 // (defined in 9.2.3) with arguments availableLocales and
251 // requestedLocales.
252 r = LookupMatcher(availableLocales, requestedLocales);
253
254 // 3. Else
255 else
256 // a. Let r be the result of calling the BestFitMatcher abstract
257 // operation (defined in 9.2.4) with arguments availableLocales and
258 // requestedLocales.
259 r = BestFitMatcher(availableLocales, requestedLocales);
260
261 // 4. Let foundLocale be the value of r.[[locale]].
262 let foundLocale = r['[[locale]]'];
263
264 let extensionSubtags, extensionSubtagsLength;
265
266 // 5. If r has an [[extension]] field, then
267 if (hop.call(r, '[[extension]]')) {
268 // a. Let extension be the value of r.[[extension]].
269 let extension = r['[[extension]]'];
270 // b. Let split be the standard built-in function object defined in ES5,
271 // 15.5.4.14.
272 let split = String.prototype.split;
273 // c. Let extensionSubtags be the result of calling the [[Call]] internal
274 // method of split with extension as the this value and an argument
275 // list containing the single item "-".
276 extensionSubtags = split.call(extension, '-');
277 // d. Let extensionSubtagsLength be the result of calling the [[Get]]
278 // internal method of extensionSubtags with argument "length".
279 extensionSubtagsLength = extensionSubtags.length;
280 }
281
282 // 6. Let result be a new Record.
283 let result = new Record();
284
285 // 7. Set result.[[dataLocale]] to foundLocale.
286 result['[[dataLocale]]'] = foundLocale;
287
288 // 8. Let supportedExtension be "-u".
289 let supportedExtension = '-u';
290 // 9. Let i be 0.
291 let i = 0;
292 // 10. Let len be the result of calling the [[Get]] internal method of
293 // relevantExtensionKeys with argument "length".
294 let len = relevantExtensionKeys.length;
295
296 // 11 Repeat while i < len:
297 while (i < len) {
298 // a. Let key be the result of calling the [[Get]] internal method of
299 // relevantExtensionKeys with argument ToString(i).
300 let key = relevantExtensionKeys[i];
301 // b. Let foundLocaleData be the result of calling the [[Get]] internal
302 // method of localeData with the argument foundLocale.
303 let foundLocaleData = localeData[foundLocale];
304 // c. Let keyLocaleData be the result of calling the [[Get]] internal
305 // method of foundLocaleData with the argument key.
306 let keyLocaleData = foundLocaleData[key];
307 // d. Let value be the result of calling the [[Get]] internal method of
308 // keyLocaleData with argument "0".
309 let value = keyLocaleData['0'];
310 // e. Let supportedExtensionAddition be "".
311 let supportedExtensionAddition = '';
312 // f. Let indexOf be the standard built-in function object defined in
313 // ES5, 15.4.4.14.
314 let indexOf = arrIndexOf;
315
316 // g. If extensionSubtags is not undefined, then
317 if (extensionSubtags !== undefined) {
318 // i. Let keyPos be the result of calling the [[Call]] internal
319 // method of indexOf with extensionSubtags as the this value and
320 // an argument list containing the single item key.
321 let keyPos = indexOf.call(extensionSubtags, key);
322
323 // ii. If keyPos ≠ -1, then
324 if (keyPos !== -1) {
325 // 1. If keyPos + 1 < extensionSubtagsLength and the length of the
326 // result of calling the [[Get]] internal method of
327 // extensionSubtags with argument ToString(keyPos +1) is greater
328 // than 2, then
329 if (keyPos + 1 < extensionSubtagsLength
330 && extensionSubtags[keyPos + 1].length > 2) {
331 // a. Let requestedValue be the result of calling the [[Get]]
332 // internal method of extensionSubtags with argument
333 // ToString(keyPos + 1).
334 let requestedValue = extensionSubtags[keyPos + 1];
335 // b. Let valuePos be the result of calling the [[Call]]
336 // internal method of indexOf with keyLocaleData as the
337 // this value and an argument list containing the single
338 // item requestedValue.
339 let valuePos = indexOf.call(keyLocaleData, requestedValue);
340
341 // c. If valuePos ≠ -1, then
342 if (valuePos !== -1) {
343 // i. Let value be requestedValue.
344 value = requestedValue,
345 // ii. Let supportedExtensionAddition be the
346 // concatenation of "-", key, "-", and value.
347 supportedExtensionAddition = '-' + key + '-' + value;
348 }
349 }
350 // 2. Else
351 else {
352 // a. Let valuePos be the result of calling the [[Call]]
353 // internal method of indexOf with keyLocaleData as the this
354 // value and an argument list containing the single item
355 // "true".
356 let valuePos = indexOf(keyLocaleData, 'true');
357
358 // b. If valuePos ≠ -1, then
359 if (valuePos !== -1)
360 // i. Let value be "true".
361 value = 'true';
362 }
363 }
364 }
365 // h. If options has a field [[<key>]], then
366 if (hop.call(options, '[[' + key + ']]')) {
367 // i. Let optionsValue be the value of options.[[<key>]].
368 let optionsValue = options['[[' + key + ']]'];
369
370 // ii. If the result of calling the [[Call]] internal method of indexOf
371 // with keyLocaleData as the this value and an argument list
372 // containing the single item optionsValue is not -1, then
373 if (indexOf.call(keyLocaleData, optionsValue) !== -1) {
374 // 1. If optionsValue is not equal to value, then
375 if (optionsValue !== value) {
376 // a. Let value be optionsValue.
377 value = optionsValue;
378 // b. Let supportedExtensionAddition be "".
379 supportedExtensionAddition = '';
380 }
381 }
382 }
383 // i. Set result.[[<key>]] to value.
384 result['[[' + key + ']]'] = value;
385
386 // j. Append supportedExtensionAddition to supportedExtension.
387 supportedExtension += supportedExtensionAddition;
388
389 // k. Increase i by 1.
390 i++;
391 }
392 // 12. If the length of supportedExtension is greater than 2, then
393 if (supportedExtension.length > 2) {
394 // a.
395 let privateIndex = foundLocale.indexOf("-x-");
396 // b.
397 if (privateIndex === -1) {
398 // i.
399 foundLocale = foundLocale + supportedExtension;
400 }
401 // c.
402 else {
403 // i.
404 let preExtension = foundLocale.substring(0, privateIndex);
405 // ii.
406 let postExtension = foundLocale.substring(privateIndex);
407 // iii.
408 foundLocale = preExtension + supportedExtension + postExtension;
409 }
410 // d. asserting - skipping
411 // e.
412 foundLocale = CanonicalizeLanguageTag(foundLocale);
413 }
414 // 13. Set result.[[locale]] to foundLocale.
415 result['[[locale]]'] = foundLocale;
416
417 // 14. Return result.
418 return result;
419}
420
421/**
422 * The LookupSupportedLocales abstract operation returns the subset of the
423 * provided BCP 47 language priority list requestedLocales for which
424 * availableLocales has a matching locale when using the BCP 47 Lookup algorithm.
425 * Locales appear in the same order in the returned list as in requestedLocales.
426 * The following steps are taken:
427 */
428export function /* 9.2.6 */LookupSupportedLocales (availableLocales, requestedLocales) {
429 // 1. Let len be the number of elements in requestedLocales.
430 let len = requestedLocales.length;
431 // 2. Let subset be a new empty List.
432 let subset = new List();
433 // 3. Let k be 0.
434 let k = 0;
435
436 // 4. Repeat while k < len
437 while (k < len) {
438 // a. Let locale be the element of requestedLocales at 0-origined list
439 // position k.
440 let locale = requestedLocales[k];
441 // b. Let noExtensionsLocale be the String value that is locale with all
442 // Unicode locale extension sequences removed.
443 let noExtensionsLocale = String(locale).replace(expUnicodeExSeq, '');
444 // c. Let availableLocale be the result of calling the
445 // BestAvailableLocale abstract operation (defined in 9.2.2) with
446 // arguments availableLocales and noExtensionsLocale.
447 let availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
448
449 // d. If availableLocale is not undefined, then append locale to the end of
450 // subset.
451 if (availableLocale !== undefined)
452 arrPush.call(subset, locale);
453
454 // e. Increment k by 1.
455 k++;
456 }
457
458 // 5. Let subsetArray be a new Array object whose elements are the same
459 // values in the same order as the elements of subset.
460 let subsetArray = arrSlice.call(subset);
461
462 // 6. Return subsetArray.
463 return subsetArray;
464}
465
466/**
467 * The BestFitSupportedLocales abstract operation returns the subset of the
468 * provided BCP 47 language priority list requestedLocales for which
469 * availableLocales has a matching locale when using the Best Fit Matcher
470 * algorithm. Locales appear in the same order in the returned list as in
471 * requestedLocales. The steps taken are implementation dependent.
472 */
473export function /*9.2.7 */BestFitSupportedLocales (availableLocales, requestedLocales) {
474 // ###TODO: implement this function as described by the specification###
475 return LookupSupportedLocales(availableLocales, requestedLocales);
476}
477
478/**
479 * The SupportedLocales abstract operation returns the subset of the provided BCP
480 * 47 language priority list requestedLocales for which availableLocales has a
481 * matching locale. Two algorithms are available to match the locales: the Lookup
482 * algorithm described in RFC 4647 section 3.4, and an implementation dependent
483 * best-fit algorithm. Locales appear in the same order in the returned list as
484 * in requestedLocales. The following steps are taken:
485 */
486export function /*9.2.8 */SupportedLocales (availableLocales, requestedLocales, options) {
487 let matcher, subset;
488
489 // 1. If options is not undefined, then
490 if (options !== undefined) {
491 // a. Let options be ToObject(options).
492 options = new Record(toObject(options));
493 // b. Let matcher be the result of calling the [[Get]] internal method of
494 // options with argument "localeMatcher".
495 matcher = options.localeMatcher;
496
497 // c. If matcher is not undefined, then
498 if (matcher !== undefined) {
499 // i. Let matcher be ToString(matcher).
500 matcher = String(matcher);
501
502 // ii. If matcher is not "lookup" or "best fit", then throw a RangeError
503 // exception.
504 if (matcher !== 'lookup' && matcher !== 'best fit')
505 throw new RangeError('matcher should be "lookup" or "best fit"');
506 }
507 }
508 // 2. If matcher is undefined or "best fit", then
509 if (matcher === undefined || matcher === 'best fit')
510 // a. Let subset be the result of calling the BestFitSupportedLocales
511 // abstract operation (defined in 9.2.7) with arguments
512 // availableLocales and requestedLocales.
513 subset = BestFitSupportedLocales(availableLocales, requestedLocales);
514 // 3. Else
515 else
516 // a. Let subset be the result of calling the LookupSupportedLocales
517 // abstract operation (defined in 9.2.6) with arguments
518 // availableLocales and requestedLocales.
519 subset = LookupSupportedLocales(availableLocales, requestedLocales);
520
521 // 4. For each named own property name P of subset,
522 for (let P in subset) {
523 if (!hop.call(subset, P))
524 continue;
525
526 // a. Let desc be the result of calling the [[GetOwnProperty]] internal
527 // method of subset with P.
528 // b. Set desc.[[Writable]] to false.
529 // c. Set desc.[[Configurable]] to false.
530 // d. Call the [[DefineOwnProperty]] internal method of subset with P, desc,
531 // and true as arguments.
532 defineProperty(subset, P, {
533 writable: false, configurable: false, value: subset[P],
534 });
535 }
536 // "Freeze" the array so no new elements can be added
537 defineProperty(subset, 'length', { writable: false });
538
539 // 5. Return subset
540 return subset;
541}
542
543/**
544 * The GetOption abstract operation extracts the value of the property named
545 * property from the provided options object, converts it to the required type,
546 * checks whether it is one of a List of allowed values, and fills in a fallback
547 * value if necessary.
548 */
549export function /*9.2.9 */GetOption (options, property, type, values, fallback) {
550 // 1. Let value be the result of calling the [[Get]] internal method of
551 // options with argument property.
552 let value = options[property];
553
554 // 2. If value is not undefined, then
555 if (value !== undefined) {
556 // a. Assert: type is "boolean" or "string".
557 // b. If type is "boolean", then let value be ToBoolean(value).
558 // c. If type is "string", then let value be ToString(value).
559 value = type === 'boolean' ? Boolean(value)
560 : (type === 'string' ? String(value) : value);
561
562 // d. If values is not undefined, then
563 if (values !== undefined) {
564 // i. If values does not contain an element equal to value, then throw a
565 // RangeError exception.
566 if (arrIndexOf.call(values, value) === -1)
567 throw new RangeError("'" + value + "' is not an allowed value for `" + property +'`');
568 }
569
570 // e. Return value.
571 return value;
572 }
573 // Else return fallback.
574 return fallback;
575}
576
577/**
578 * The GetNumberOption abstract operation extracts a property value from the
579 * provided options object, converts it to a Number value, checks whether it is
580 * in the allowed range, and fills in a fallback value if necessary.
581 */
582export function /* 9.2.10 */GetNumberOption (options, property, minimum, maximum, fallback) {
583 // 1. Let value be the result of calling the [[Get]] internal method of
584 // options with argument property.
585 let value = options[property];
586
587 // 2. If value is not undefined, then
588 if (value !== undefined) {
589 // a. Let value be ToNumber(value).
590 value = Number(value);
591
592 // b. If value is NaN or less than minimum or greater than maximum, throw a
593 // RangeError exception.
594 if (isNaN(value) || value < minimum || value > maximum)
595 throw new RangeError('Value is not a number or outside accepted range');
596
597 // c. Return floor(value).
598 return Math.floor(value);
599 }
600 // 3. Else return fallback.
601 return fallback;
602}