Files
shaka-player/ui/language_utils.js
T
Joey Parrish 562a2d567b chore: Strictly require jsdoc
This enables the eslint rule requiring jsdocs on all class
declarations, function declarations, and methods.

Unfortunately, there are two problems with this:

1. We don't use class _declarations_, we use class _expressions_,
which are not covered by this rule.  So it does not enforce jsdoc at
the class level.
2. We tend to document a class at the class-level, rather than at the
constructor.  But a constructor counts as a method for eslint, so it
requires docs on the constructor.  There is no way to configure it to
make an exception for trivial constructors.

So for all trivial (no-argument) constructors, we add empty jsdocs:
  /** */
  constructor() {

This was quicker and easier than setting up some alternative plugin in
eslint to make an exception for us.

The good news is that this rule caught several undocumented parameters
and places where the jsdoc comment was malformed.  So fixing those
also improves the compiler's ability to enforce types.

Change-Id: Icbc46ed690c94e53d354648a883119524f8fca45
2021-01-09 02:00:31 +00:00

217 lines
7.8 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.ui.LanguageUtils');
goog.require('mozilla.LanguageMapping');
goog.require('shaka.log');
goog.require('shaka.ui.Locales');
goog.require('shaka.ui.Overlay.TrackLabelFormat');
goog.require('shaka.ui.Utils');
goog.require('shaka.util.Dom');
goog.require('shaka.util.LanguageUtils');
goog.requireType('shaka.ui.Localization');
shaka.ui.LanguageUtils = class {
/**
* @param {!Array.<shaka.extern.Track>} tracks
* @param {!HTMLElement} langMenu
* @param {function(!shaka.extern.Track)} onTrackSelected
* @param {boolean} updateChosen
* @param {!HTMLElement} currentSelectionElement
* @param {shaka.ui.Localization} localization
* @param {shaka.ui.Overlay.TrackLabelFormat} trackLabelFormat
*/
static updateTracks(tracks, langMenu, onTrackSelected, updateChosen,
currentSelectionElement, localization, trackLabelFormat) {
// TODO: Do the benefits of having this common code in a method still
// outweigh the complexity of the parameter list?
// Using array.filter(f)[0] as an alternative to array.find(f) which is
// not supported in IE11.
const activeTracks = tracks.filter((track) => {
return track.active == true;
});
const selectedTrack = activeTracks[0];
// Remove old tracks
// 1. Save the back to menu button
const backButton = shaka.ui.Utils.getFirstDescendantWithClassName(
langMenu, 'shaka-back-to-overflow-button');
// 2. Remove everything
shaka.util.Dom.removeAllChildren(langMenu);
// 3. Add the backTo Menu button back
langMenu.appendChild(backButton);
// 4. Figure out which languages have multiple roles.
const getRolesString = (track) => {
if (track.type == 'variant') {
return track.audioRoles ? track.audioRoles.join(', ') : undefined;
} else {
return track.roles.join(', ');
}
};
const getCombination = (language, rolesString) => {
return language + ': ' + rolesString;
};
/** @type {!Map.<string, !Set.<string>>} */
const rolesByLanguage = new Map();
for (const track of tracks) {
if (!rolesByLanguage.has(track.language)) {
rolesByLanguage.set(track.language, new Set());
}
rolesByLanguage.get(track.language).add(getRolesString(track));
}
// 5. Add new buttons
/** @type {!Set.<string>} */
const combinationsMade = new Set();
const selectedCombination = selectedTrack ? getCombination(
selectedTrack.language, getRolesString(selectedTrack)) : '';
for (const track of tracks) {
const language = track.language;
const forced = track.forced;
const LocIds = shaka.ui.Locales.Ids;
const forcedString = localization.resolve(LocIds.SUBTITLE_FORCED);
const rolesString = getRolesString(track);
const combinationName = getCombination(language, rolesString);
if (combinationsMade.has(combinationName)) {
continue;
}
combinationsMade.add(combinationName);
const button = shaka.util.Dom.createButton();
button.addEventListener('click', () => {
onTrackSelected(track);
});
const span = shaka.util.Dom.createHTMLElement('span');
button.appendChild(span);
span.textContent =
shaka.ui.LanguageUtils.getLanguageName(language, localization);
switch (trackLabelFormat) {
case shaka.ui.Overlay.TrackLabelFormat.LANGUAGE:
if (forced) {
span.textContent += ' (' + forcedString + ')';
}
break;
case shaka.ui.Overlay.TrackLabelFormat.ROLE:
if (!rolesString) {
// Fallback behavior. This probably shouldn't happen.
shaka.log.alwaysWarn('Track #' + track.id + ' does not have a ' +
'role, but the UI is configured to only show role.');
span.textContent = '?';
} else {
span.textContent = rolesString;
}
if (forced) {
span.textContent += ' (' + forcedString + ')';
}
break;
case shaka.ui.Overlay.TrackLabelFormat.LANGUAGE_ROLE:
if (rolesString) {
span.textContent += ': ' + rolesString;
}
if (forced) {
span.textContent += ' (' + forcedString + ')';
}
break;
case shaka.ui.Overlay.TrackLabelFormat.LABEL:
if (track.label) {
span.textContent = track.label;
} else {
// Fallback behavior. This probably shouldn't happen.
shaka.log.alwaysWarn('Track #' + track.id + ' does not have a ' +
'label, but the UI is configured to only show labels.');
span.textContent = '?';
}
break;
}
if (updateChosen && (combinationName == selectedCombination)) {
button.appendChild(shaka.ui.Utils.checkmarkIcon());
span.classList.add('shaka-chosen-item');
button.setAttribute('aria-selected', 'true');
currentSelectionElement.textContent = span.textContent;
}
langMenu.appendChild(button);
}
}
/**
* Returns the language's name for itself in its own script (autoglottonym),
* if we have it.
*
* If the locale, including region, can be mapped to a name, we return a very
* specific name including the region. For example, "de-AT" would map to
* "Deutsch (Österreich)" or Austrian German.
*
* If only the language part of the locale is in our map, we append the locale
* itself for specificity. For example, "ar-EG" (Egyptian Arabic) would map
* to "ﺎﻠﻋﺮﺒﻳﺓ (ar-EG)". In this way, multiple versions of Arabic whose
* regions are not in our map would not all look the same in the language
* list, but could be distinguished by their locale.
*
* Finally, if language part of the locale is not in our map, we label it
* "unknown", as translated to the UI locale, and we append the locale itself
* for specificity. For example, "sjn" would map to "Unknown (sjn)". In this
* way, multiple unrecognized languages would not all look the same in the
* language list, but could be distinguished by their locale.
*
* @param {string} locale
* @param {shaka.ui.Localization} localization
* @return {string} The language's name for itself in its own script, or as
* close as we can get with the information we have.
*/
static getLanguageName(locale, localization) {
if (!locale && !localization) {
return '';
}
// Shorthand for resolving a localization ID.
const resolve = (id) => localization.resolve(id);
// Handle some special cases first. These are reserved language tags that
// are used to indicate something that isn't one specific language.
switch (locale) {
case 'mul':
return resolve(shaka.ui.Locales.Ids.MULTIPLE_LANGUAGES);
case 'und':
return resolve(shaka.ui.Locales.Ids.UNDETERMINED_LANGUAGE);
case 'zxx':
return resolve(shaka.ui.Locales.Ids.NOT_APPLICABLE);
}
// Extract the base language from the locale as a fallback step.
const language = shaka.util.LanguageUtils.getBase(locale);
// First try to resolve the full language name.
// If that fails, try the base.
// Finally, report "unknown".
// When there is a loss of specificity (either to a base language or to
// "unknown"), we should append the original language code.
// Otherwise, there may be multiple identical-looking items in the list.
if (locale in mozilla.LanguageMapping) {
return mozilla.LanguageMapping[locale].nativeName;
} else if (language in mozilla.LanguageMapping) {
return mozilla.LanguageMapping[language].nativeName +
' (' + locale + ')';
} else {
return resolve(shaka.ui.Locales.Ids.UNRECOGNIZED_LANGUAGE) +
' (' + locale + ')';
}
}
};