bitmovin-player-ui icon indicating copy to clipboard operation
bitmovin-player-ui copied to clipboard

Add localization for strings

Open protyposis opened this issue 6 years ago • 2 comments

Text strings in the UI are currently all hardcoded in English. Most strings can be changed by creating a custom UI structure and setting the labels, but not all of them.

A decent localization interface would not require users at all to create a custom UI structure. It should work with the player's default UI by supplying localized strings to the player config.

protyposis avatar Oct 19 '17 23:10 protyposis

Here's a simple script to localize (static) labels of UI from the outside, without such a feature actually being implemented in the UI:

// German translations
var translationMap = {
  // Settings menu
  'Video Quality': 'Videoqualität',
  'Audio Quality': 'Audioqualität',
  'Audio Track': 'Audiospur',
  'Speed': 'Geschwindigkeit',
  // Subtitle settings menu
  'Subtitles': 'Untertitel',
  'Font size': 'Größe',
  'Font family': 'Schriftart',
  'Font color': 'Farbe',
  'Font opacity': 'Deckkraft',
  'Character edge': 'Ränder',
  'Background color': 'Hintergrundfarbe (Text)',
  'Background opacity': 'Hintergrunddeckkraft (Text)',
  'Window color': 'Hintergrundfarbe (Textbox)',
  'Window opacity': 'Hintergrunddeckkraft (Textbox)',
  'Back': 'Zurück',
  'Reset': 'Zurücksetzen',
};

function translateElementContent(element) {
  var currentText = element.innerHTML;
  var translatedText = translationMap[currentText];

  // If a translation exists, apply it
  if (translatedText) {
    element.innerHTML = translatedText;
  }
}

function translateUi(uicontainer) {
  // Gets the labels that are "Label" components
  var labelComponents = Array.from(uicontainer.getElementsByClassName('bmpui-ui-label'));
  // Gets labels that aren't distinct components but part of other components' DOM tree
  var innerLabels = Array.from(uicontainer.getElementsByClassName('bmpui-label'));

  // Concatenate all elements into a single array so we can iterate over all of them easily
  var elements = labelComponents.concat(innerLabels);

  // Translate the content of all elements
  // A label is usually a <span> with text inside
  elements.forEach(function(element) {
    translateElementContent(element);

    // Workaround for the "Subtitles" label
    // This label unfortunately does not follow the label-pattern and carries
    // its text inside a child <span>, so we check that too
    var innerSpans = element.getElementsByTagName('span');
    if (innerSpans.length > 0) {
      translateElementContent(innerSpans[0]);
    }
  });
}

function waitForUiAndTranslateOnceLoaded() {
  var uiContainers = player.getFigure().getElementsByClassName('bmpui-ui-uicontainer');

  // When the UI is loaded, there are one or more UI containers
  // (more if different UI variants are loaded, e.g. normal and ads UI)
  if(uiContainers.length > 0) {
    // UI container(s) found, translate them
    Array.from(uiContainers).forEach(function(uiContainer) {
      translateUi(uiContainer);
    });
  } else {
    // When no UI containers are found, the UI is probably not loaded yet.
    // There's no event that tells us when the UI is loaded, so we need to
    // poll it's existence again later.
    setTimeout(waitForUiAndTranslateOnceLoaded, 50);
  }
}

// This is where we will apply the translation
player.setup(config).then(function() {
  // The player is loaded, UI will soon be as well, so translate the UI
  waitForUiAndTranslateOnceLoaded();
}, function(errorEvent) {
  // Play setup failed, and there's no player UI yet to display the error in
  console.log(errorEvent);
});

The problem with this approach is that it does not work for dynamically generated UI elements, which includes

  • the "example subtitle" which is displayed while the subtitle settings are open and no actual subtitle is active
  • the cast overlay which displays the connection status with a Cast receiver
  • all selectboxes in settings and subtitle settings (e.g. font color)

We could extend the script above and listen to various player and/or DOM events to catch dynamic updates and retranslate labels by recalling waitForUiAndTranslateOnceLoaded. That would also be necessary when UI variants are switched, e.g, when the ads UI or mobile UI are loaded.

protyposis avatar Oct 19 '17 23:10 protyposis

The UIConfig should take a map of string translations that allows customers to translate the UI whose texts are currently hardcoded in english.

It may look like this (providing an exemplary German translation):

var uiconfig = {
  localization: {
    'Video Quality': 'Videoqualität',
    'Subtitles': 'Untertitel',
    'Connecting to <strong>:castDeviceName</strong>...': 'Verbindung mit <strong>:castDeviceName</strong> wird hergestellt...',
    ...
  },
};

A Component that uses a string would ideally call a simple translate function, that returns the translated string if a translation is available, or the input if no translation is available. The translation interface must also support placeholders. Examples:

// A simple translation
var translatedText = translate('Video Quality');
assert(translatedText === 'Videoqualität')

// A text without a translation
var translatedText2 = translate('foo');
assert(translatedText2 === 'foo');

// A translation with a placeholder
var translatedText3 = translate('Connecting to <strong>:castDeviceName</strong>...', { castDeviceName: 'Wohnzimmer' });
assert(translatedText3 === 'Verbindung mit <strong>Wohnzimmer</strong> wird hergestellt...');

The UIConfig could be easily passed in as a property of the Player config, e.g. config.ui and read from there. This would enable customers to also translate the default UI supplied by the player and save them the overhead of running their own UI (in case they don't need other customizations).

The approach outlined above would be simple but has one drawback: when e.g. calling label.setText('Video Quality'), it would get translated because there is a translation for the input string available. It can be argued though that when someone calls setText, he wants to set the supplied text and not have it automatically translated. The solution would be to use IDs for strings that should be translated instead, e.g. setText('settings.label.videoquality'), and we'd also define a default map for the English labels. The advantage would be that all translatable strings with their IDs would be collected in a single file which makes it much easier for customers to write their own translations. Example:

var defaultLocalization: {
  'settings.label.videoquality': 'Video Quality',
  'settings.label.subtitles': 'Subtitles',
  'castoverlay.status.connecting': 'Connecting to <strong>:castDeviceName</strong>...',
  ...
};

A customer can now easily copy this file and adjust it to his language. Added/changed/removed labels could easily be traced through the Git revision log.

While this seems to be easily implementable, the actual challenges that will have to be tackled are:

  • The DOM elements of a Component are created before configuration through component.configure happens, which is when the UIConfig is passed in and they get access to the localization strings. The constructor of a Component is called much earlier, at the time the UI structure is assembled. This means that all labels need to be updated once configuration happens. (unnecessary performance impact?)
  • Some components dynamically regenerate their DOM on certain events, but the UIConfig is only passed into configure. Storing the UIConfig into a field in every component seems not nice, also the translation functionality is not a feature of a component itself, so a nice pattern must be found to easily create a translate function (however that may look like in the end), without requiring boilerplate in the components. (A singleton does now work because we can have multiple different UI instances)
  • What happens if label.setText is called, and the onTextChanged event fires? Does it convey the input text, or the translated text? Or does setText not translate texts on its own? (it probably should not; i.e. we don't want an unnecessary translation lookup on the playback time label when the time updates)

protyposis avatar Oct 20 '17 19:10 protyposis

Localization is available for quite some time now, closing this issue.

dweinber avatar Feb 07 '23 16:02 dweinber