-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
Copy pathlocalization.js
492 lines (430 loc) · 14.9 KB
/
localization.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.Localization');
goog.provide('shaka.ui.Localization.ConflictResolution');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.Iterables');
goog.require('shaka.util.LanguageUtils');
// TODO: link to the design and usage documentation here
// b/117679670
/**
* Localization system provided by the shaka ui library.
* It can be used to store the various localized forms of
* strings that are expected to be displayed to the user.
* If a string is not available, it will return the localized
* form in the closest related locale.
*
* @final
* @export
*/
shaka.ui.Localization = class extends shaka.util.FakeEventTarget {
/**
* @param {string} fallbackLocale
* The fallback locale that should be used. It will be assumed that this
* locale should have entries for just about every request.
*/
constructor(fallbackLocale) {
super();
/** @private {string} */
this.fallbackLocale_ = shaka.util.LanguageUtils.normalize(fallbackLocale);
/**
* The current mappings that will be used when requests are made. Since
* nothing has been loaded yet, there will be nothing in this map.
*
* @private {!Map<string, string>}
*/
this.currentMap_ = new Map();
/**
* The locales that were used when creating |currentMap_|. Since we don't
* have anything when we first initialize, an empty set means "no
* preference".
*
* @private {!Set<string>}
*/
this.currentLocales_ = new Set();
/**
* A map of maps where:
* - The outer map is a mapping from locale code to localizations.
* - The inner map is a mapping from id to localized text.
*
* @private {!Map<string, !Map<string, string>>}
*/
this.localizations_ = new Map();
}
/**
* @override
* @export
*/
release() {
// Placeholder so that readers know this implements IReleasable (via
// FakeEventTarget)
super.release();
}
/**
* Request the localization system to change which locale it serves. If any of
* of the preferred locales cannot be found, the localization system will fire
* an event identifying which locales it does not know. The localization
* system will then continue to operate using the closest matches it has.
*
* @param {!Iterable<string>} locales
* The locale codes for the requested locales in order of preference.
* @export
*/
changeLocale(locales) {
const Class = shaka.ui.Localization;
// Normalize the locale so that matching will be easier. We need to reset
// our internal set of locales so that we have the same order as the new
// set.
this.currentLocales_.clear();
for (const locale of locales) {
this.currentLocales_.add(shaka.util.LanguageUtils.normalize(locale));
}
this.updateCurrentMap_();
// Check if we have support for the exact locale requested. Even through we
// will do our best to return the most relevant results, we need to tell
// app that some data may be missing.
const missing = shaka.util.Iterables.filter(
this.currentLocales_,
(locale) => !this.localizations_.has(locale));
if (missing.length) {
this.dispatchEvent(new shaka.util.FakeEvent(
Class.UNKNOWN_LOCALES,
(new Map()).set('locales', missing)));
}
const found = shaka.util.Iterables.filter(
this.currentLocales_,
(locale) => this.localizations_.has(locale));
const data = (new Map()).set(
'locales', found.length ? found : [this.fallbackLocale_]);
this.dispatchEvent(new shaka.util.FakeEvent(
Class.LOCALE_CHANGED,
data));
}
/**
* Insert a set of localizations for a single locale. This will amend the
* existing localizations for the given locale.
*
* @param {string} locale
* The locale that the localizations should be added to.
* @param {!Map<string, string>} localizations
* A mapping of id to localized text that should used to modify the internal
* collection of localizations.
* @param {shaka.ui.Localization.ConflictResolution=} conflictResolution
* The strategy used to resolve conflicts when the id of an existing entry
* matches the id of a new entry. Default to |USE_NEW|, where the new
* entry will replace the old entry.
* @return {!shaka.ui.Localization}
* Returns |this| so that calls can be chained.
* @export
*/
insert(locale, localizations, conflictResolution) {
const Class = shaka.ui.Localization;
const ConflictResolution = shaka.ui.Localization.ConflictResolution;
const FakeEvent = shaka.util.FakeEvent;
// Normalize the locale so that matching will be easier.
locale = shaka.util.LanguageUtils.normalize(locale);
// Default |conflictResolution| to |USE_NEW| if it was not given. Doing it
// here because it would create too long of a parameter list.
if (conflictResolution === undefined) {
conflictResolution = ConflictResolution.USE_NEW;
}
// Make sure we have an entry for the locale because we are about to
// write to it.
const table = this.localizations_.get(locale) || new Map();
localizations.forEach((value, id) => {
// Set the value if we don't have an old value or if we are to replace
// the old value with the new value.
if (!table.has(id) || conflictResolution == ConflictResolution.USE_NEW) {
table.set(id, value);
}
});
this.localizations_.set(locale, table);
// The data we use to make our map may have changed, update the map we pull
// data from.
this.updateCurrentMap_();
this.dispatchEvent(new FakeEvent(Class.LOCALE_UPDATED));
return this;
}
/**
* Set the value under each key in |dictionary| to the resolved value.
* Convenient for apps with some kind of data binding system.
*
* Equivalent to:
* for (const key of dictionary.keys()) {
* dictionary.set(key, localization.resolve(key));
* }
*
* @param {!Map<string, string>} dictionary
* @export
*/
resolveDictionary(dictionary) {
for (const key of dictionary.keys()) {
// Since we are not changing what keys are in the map, it is safe to
// update the map while iterating it.
dictionary.set(key, this.resolve(key));
}
}
/**
* Request the localized string under the given id. If there is no localized
* version of the string, then the fallback localization will be given
* ("en" version). If there is no fallback localization, a non-null empty
* string will be returned.
*
* @param {string} id The id for the localization entry.
* @return {string}
* @export
*/
resolve(id) {
const Class = shaka.ui.Localization;
const FakeEvent = shaka.util.FakeEvent;
/** @type {string} */
const result = this.currentMap_.get(id);
// If we have a result, it means that it was found in either the current
// locale or one of the fall-backs.
if (result) {
return result;
}
// Since we could not find the result, it means it is missing from a large
// number of locales. Since we don't know which ones we actually checked,
// just tell them the preferred locale.
const data = new Map()
// Make a copy to avoid leaking references.
.set('locales', Array.from(this.currentLocales_))
.set('missing', id);
this.dispatchEvent(new FakeEvent(Class.UNKNOWN_LOCALIZATION, data));
return '';
}
/**
* The locales currently used. An empty set means "no preference".
*
* @return {!Set<string>}
* @export
*/
getCurrentLocales() {
return this.currentLocales_;
}
/**
* @private
*/
updateCurrentMap_() {
const LanguageUtils = shaka.util.LanguageUtils;
/** @type {!Map<string, !Map<string, string>>} */
const localizations = this.localizations_;
/** @type {string} */
const fallbackLocale = this.fallbackLocale_;
/** @type {!Iterable<string>} */
const preferredLocales = this.currentLocales_;
/**
* We want to create a single map that gives us the best possible responses
* for the current locale. To do this, we will go through be loosest
* matching locales to the best matching locales. By the time we finish
* flattening the maps, the best result will be left under each key.
*
* Get the locales we should use in order of preference. For example with
* preferred locales of "elvish-WOODLAND" and "dwarfish-MOUNTAIN" and a
* fallback of "common-HUMAN", this would look like:
*
* new Set([
* // Preference 1
* 'elvish-WOODLAND',
* // Preference 1 Base
* 'elvish',
* // Preference 1 Siblings
* 'elvish-WOODLAND', 'elvish-WESTWOOD', 'elvish-MARSH,
* // Preference 2
* 'dwarfish-MOUNTAIN',
* // Preference 2 base
* 'dwarfish',
* // Preference 2 Siblings
* 'dwarfish-MOUNTAIN', 'dwarfish-NORTH', "dwarfish-SOUTH",
* // Fallback
* 'common-HUMAN',
* ])
*
* @type {!Set<string>}
*/
const localeOrder = new Set();
for (const locale of preferredLocales) {
localeOrder.add(locale);
localeOrder.add(LanguageUtils.getBase(locale));
const siblings = shaka.util.Iterables.filter(
localizations.keys(),
(other) => LanguageUtils.areSiblings(other, locale));
// Sort the siblings so that they will always appear in the same order
// regardless of the order of |localizations|.
siblings.sort();
for (const locale of siblings) {
localeOrder.add(locale);
}
const children = shaka.util.Iterables.filter(
localizations.keys(),
(other) => LanguageUtils.getBase(other) == locale);
// Sort the children so that they will always appear in the same order
// regardless of the order of |localizations|.
children.sort();
for (const locale of children) {
localeOrder.add(locale);
}
}
// Finally we add our fallback (something that should have all expected
// entries).
localeOrder.add(fallbackLocale);
// Add all the sibling maps.
/** @type {!Array<!Map<string, string>>} */
const mergeOrder = [];
for (const locale of localeOrder) {
const map = localizations.get(locale);
if (map) {
mergeOrder.push(map);
}
}
// We need to reverse the merge order. We build the order based on most
// preferred to least preferred. However, the merge will work in the
// opposite order so we must reverse our maps so that the most preferred
// options will be applied last.
mergeOrder.reverse();
// Merge all the options into our current map.
this.currentMap_.clear();
for (const map of mergeOrder) {
map.forEach((value, key) => {
this.currentMap_.set(key, value);
});
}
// Go through every key we have and see if any preferred locales are
// missing entries. This will allow app developers to find holes in their
// localizations.
/** @type {!Iterable<string>} */
const allKeys = this.currentMap_.keys();
/** @type {!Set<string>} */
const missing = new Set();
for (const locale of this.currentLocales_) {
// Make sure we have a non-null map. The diff will be easier that way.
const map = this.localizations_.get(locale) || new Map();
shaka.ui.Localization.findMissingKeys_(map, allKeys, missing);
}
if (missing.size > 0) {
const data = new Map()
// Make a copy of the preferred locales to avoid leaking references.
.set('locales', Array.from(preferredLocales))
// Because more people like arrays more than sets, convert the set to
// an array.
.set('missing', Array.from(missing));
this.dispatchEvent(new shaka.util.FakeEvent(
shaka.ui.Localization.MISSING_LOCALIZATIONS,
data));
}
}
/**
* Go through a map and add all the keys that are in |keys| but not in
* |map| to |missing|.
*
* @param {!Map<string, string>} map
* @param {!Iterable<string>} keys
* @param {!Set<string>} missing
* @private
*/
static findMissingKeys_(map, keys, missing) {
for (const key of keys) {
// Check if the value is missing so that we are sure that it does not
// have a value. We get the value and not just |has| so that a null or
// empty string will fail this check.
if (!map.get(key)) {
missing.add(key);
}
}
}
};
/**
* An enum for how the localization system should resolve conflicts between old
* translations and new translations.
*
* @enum {number}
* @export
*/
shaka.ui.Localization.ConflictResolution = {
'USE_OLD': 0,
'USE_NEW': 1,
};
/**
* The event name for when locales were requested, but we could not find any
* entries for them. The localization system will continue to use the closest
* matches it has.
*
* @const {string}
* @export
*/
shaka.ui.Localization.UNKNOWN_LOCALES = 'unknown-locales';
/**
* The event name for when an entry could not be found in the preferred locale,
* related locales, or the fallback locale.
*
* @const {string}
* @export
*/
shaka.ui.Localization.UNKNOWN_LOCALIZATION = 'unknown-localization';
/**
* The event name for when entries are missing from the user's preferred
* locale, but we were able to find an entry in a related locale or the fallback
* locale.
*
* @const {string}
* @export
*/
shaka.ui.Localization.MISSING_LOCALIZATIONS = 'missing-localizations';
/**
* The event name for when a new locale has been requested and any previously
* resolved values should be updated.
*
* @const {string}
* @export
*/
shaka.ui.Localization.LOCALE_CHANGED = 'locale-changed';
/**
* The event name for when |insert| was called and it changed entries that could
* affect previously resolved values.
*
* @const {string}
* @export
*/
shaka.ui.Localization.LOCALE_UPDATED = 'locale-updated';
/**
* @event shaka.ui.Localization.UnknownLocalesEvent
* @property {string} type
* 'unknown-locales'
* @property {!Array<string>} locales
* The locales that the user wanted but could not be found.
* @exportDoc
*/
/**
* @event shaka.ui.Localization.MissingLocalizationsEvent
* @property {string} type
* 'unknown-localization'
* @property {!Array<string>} locales
* The locales that the user wanted.
* @property {string} missing
* The id of the unknown entry.
* @exportDoc
*/
/**
* @event shaka.ui.Localization.MissingLocalizationsEvent
* @property {string} type
* 'missing-localizations'
* @property {string} locale
* The locale that the user wanted.
* @property {!Array<string>} missing
* The ids of the missing entries.
* @exportDoc
*/
/**
* @event shaka.ui.Localization.LocaleChangedEvent
* @property {string} type
* 'locale-changed'
* @property {!Array<string>} locales
* The new set of locales that user wanted,
* and that were successfully found.
* @exportDoc
*/