UNPKG

@openui5/sap.ui.core

Version:

OpenUI5 Core Library sap.ui.core

1,119 lines (1,030 loc) 51.2 kB
/*! * OpenUI5 * (c) Copyright 2009-2023 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ 'sap/base/assert', 'sap/base/Log', 'sap/base/strings/formatMessage', 'sap/base/util/Properties', 'sap/base/util/merge' ], function(assert, Log, formatMessage, Properties, merge) { "use strict"; /* global Promise */ /** * A regular expression that describes language tags according to BCP-47. * @see BCP47 "Tags for Identifying Languages" (http://www.ietf.org/rfc/bcp/bcp47.txt) * * The matching groups are * 0=all * 1=language (shortest ISO639 code + ext. language sub tags | 4digits (reserved) | registered language sub tags) * 2=script (4 letters) * 3=region (2letter language or 3 digits) * 4=variants (separated by '-', Note: capturing group contains leading '-' to shorten the regex!) * 5=extensions (including leading singleton, multiple extensions separated by '-') * 6=private use section (including leading 'x', multiple sections separated by '-') * * [-------------------- language ----------------------][--- script ---][------- region --------][------------- variants --------------][----------- extensions ------------][------ private use -------] */ var rLocale = /^((?:[A-Z]{2,3}(?:-[A-Z]{3}){0,3})|[A-Z]{4}|[A-Z]{5,8})(?:-([A-Z]{4}))?(?:-([A-Z]{2}|[0-9]{3}))?((?:-[0-9A-Z]{5,8}|-[0-9][0-9A-Z]{3})*)((?:-[0-9A-WYZ](?:-[0-9A-Z]{2,8})+)*)(?:-(X(?:-[0-9A-Z]{1,8})+))?$/i; /** * Resource bundles are stored according to the Java Development Kit conventions. * JDK uses old language names for a few ISO639 codes ("iw" for "he", "ji" for "yi" and "no" for "nb"). * Make sure to convert newer codes to older ones before creating file names. * @const * @private */ var M_ISO639_NEW_TO_OLD = { "he" : "iw", "yi" : "ji", "nb" : "no", "sr" : "sh" // for backward compatibility, as long as "sr (Cyrillic)" is not supported }; /** * Inverse of M_ISO639_NEW_TO_OLD. * @const * @private */ var M_ISO639_OLD_TO_NEW = { "iw" : "he", "ji" : "yi", "no" : "nb" }; /** * HANA XS Engine can't handle private extensions in BCP47 language tags. * Therefore, the agreed BCP47 codes for the technical languages 1Q..3Q * don't work as Accept-Header and need to be send as URL parameters as well. * @const * @private */ var M_SUPPORTABILITY_TO_XS = { "en_US_saptrc" : "1Q", "en_US_sappsd" : "2Q", "en_US_saprigi" : "3Q" }; /** * Default fallback locale is "en" (English) to stay backward compatible * @const * @private */ var sDefaultFallbackLocale = "en"; var rSAPSupportabilityLocales = /(?:^|-)(saptrc|sappsd|saprigi)(?:-|$)/i; /** * Helper to normalize the given locale (in BCP-47 syntax) to the java.util.Locale format. * * @param {string} sLocale Locale to normalize * @param {boolean} [bPreserveLanguage=false] Whether to keep the language untouched, otherwise * the language is mapped from modern to legacy ISO639 codes, e.g. "sr" to "sh" * @returns {string|undefined} Normalized locale or <code>undefined</code> if the locale can't be normalized * @private */ function normalize(sLocale, bPreserveLanguage) { var m; if ( typeof sLocale === 'string' && (m = rLocale.exec(sLocale.replace(/_/g, '-'))) ) { var sLanguage = m[1].toLowerCase(); if (!bPreserveLanguage) { sLanguage = M_ISO639_NEW_TO_OLD[sLanguage] || sLanguage; } var sScript = m[2] ? m[2].toLowerCase() : undefined; var sRegion = m[3] ? m[3].toUpperCase() : undefined; var sVariants = m[4] ? m[4].slice(1) : undefined; var sPrivate = m[6]; // recognize and convert special SAP supportability locales (overwrites m[]!) if ( (sPrivate && (m = rSAPSupportabilityLocales.exec(sPrivate))) || (sVariants && (m = rSAPSupportabilityLocales.exec(sVariants))) ) { return "en_US_" + m[1].toLowerCase(); // for now enforce en_US (agreed with SAP SLS) } // Chinese: when no region but a script is specified, use default region for each script if ( sLanguage === "zh" && !sRegion ) { if ( sScript === "hans" ) { sRegion = "CN"; } else if ( sScript === "hant" ) { sRegion = "TW"; } } if (sLanguage === "sr" && sScript === "latn") { if (bPreserveLanguage) { sLanguage = "sr_Latn"; } else { sLanguage = "sh"; } } return sLanguage + (sRegion ? "_" + sRegion + (sVariants ? "_" + sVariants.replace("-","_") : "") : ""); } } /** * Normalizes the given locale, unless it is an empty string (<code>""</code>). * * When locale is an empty string (<code>""</code>), it is returned without normalization. * @see normalize * @param {string} sLocale locale (aka 'language tag') to be normalized. * Can either be a BCP47 language tag or a JDK compatible locale string (e.g. "en-GB", "en_GB" or "fr"); * @param {boolean} [bPreserveLanguage=false] whether to keep the language untouched, otherwise * the language is mapped from modern to legacy ISO639 codes, e.g. "sr" to "sh" * @returns {string} normalized locale * @throws {TypeError} Will throw an error if the locale is not a valid BCP47 language tag. * @private */ function normalizePreserveEmpty(sLocale, bPreserveLanguage) { // empty string is valid and should not be normalized if (sLocale === "") { return sLocale; } var sNormalizedLocale = normalize(sLocale, bPreserveLanguage); if (sNormalizedLocale === undefined) { throw new TypeError("Locale '" + sLocale + "' is not a valid BCP47 language tag"); } return sNormalizedLocale; } /** * Returns the default locale (the locale defined in UI5 configuration if available, else fallbackLocale). * * @param {string} sFallbackLocale If the locale cannot be retrieved from the configuration * @returns {string} The default locale * @private */ function defaultLocale(sFallbackLocale) { var sLocale; // use the current session locale, if available if (window.sap && window.sap.ui && sap.ui.getCore) { sLocale = sap.ui.getCore().getConfiguration().getLanguage(); sLocale = normalize(sLocale); } // last fallback is fallbackLocale if no or no valid locale is given return sLocale || sFallbackLocale; } /** * Returns the supported locales from the configuration. * @returns {string[]} supported locales from the configuration. Otherwise, an empty array is returned. * @private */ function defaultSupportedLocales() { if (window.sap && window.sap.ui && sap.ui.getCore) { return sap.ui.getCore().getConfiguration().getSupportedLanguages(); } return []; } /** * Helper to normalize the given locale (java.util.Locale format) to the BCP-47 syntax. * * @param {string} sLocale locale to convert * @param {boolean} bConvertToModern whether to convert to modern language * @returns {string|undefined} Normalized locale or <code>undefined</code> if the locale can't be normalized */ function convertLocaleToBCP47(sLocale, bConvertToModern) { var m; if ( typeof sLocale === 'string' && (m = rLocale.exec(sLocale.replace(/_/g, '-'))) ) { var sLanguage = m[1].toLowerCase(); var sScript = m[2] ? m[2].toLowerCase() : undefined; // special case for "sr_Latn" language: "sh" should then be used if (bConvertToModern && sLanguage === "sh" && !sScript) { sLanguage = "sr_Latn"; } else if (!bConvertToModern && sLanguage === "sr" && sScript === "latn") { sLanguage = "sh"; } sLanguage = M_ISO639_OLD_TO_NEW[sLanguage] || sLanguage; return sLanguage + (m[3] ? "-" + m[3].toUpperCase() + (m[4] ? "-" + m[4].slice(1).replace("_","-") : "") : ""); } } /** * A regular expression to split a URL into * <ol> * <li>a part before the file extension</li> * <li>the file extension itself</li> * <li>any remaining part after the file extension (query, hash - optional)</li> * </ol>. * * Won't match for URLs without a file extension. * * [------- prefix ------][----ext----][-------suffix--------] * ?[--query--]#[--hash--] */ var rUrl = /^((?:[^?#]*\/)?[^\/?#]*)(\.[^.\/?#]+)((?:\?([^#]*))?(?:#(.*))?)$/; /** * List of supported file extensions. * * Could be enriched in future or even could be made * extensible to support other formats as well. * @const * @private */ var A_VALID_FILE_TYPES = [ ".properties", ".hdbtextbundle" ]; /** * Helper to split a URL with the above regex. * Either returns an object with the parts or undefined. * @param {string} sUrl URL to analyze / split into pieces. * @returns {object} an object with properties for the individual URL parts */ function splitUrl(sUrl) { var m = rUrl.exec(sUrl); if ( !m || A_VALID_FILE_TYPES.indexOf( m[2] ) < 0 ) { throw new Error("resource URL '" + sUrl + "' has unknown type (should be one of " + A_VALID_FILE_TYPES.join(",") + ")"); } return { url : sUrl, prefix : m[1], ext : m[2], query: m[4], hash: (m[5] || ""), suffix : m[2] + (m[3] || "") }; } /** * @class Contains locale-specific texts. * * If you need a locale-specific text within your application, you can use the * resource bundle to load the locale-specific file from the server and access * the texts of it. * * Use {@link module:sap/base/i18n/ResourceBundle.create} to create an instance of sap/base/i18n/ResourceBundle * (.properties without any locale information, e.g. "mybundle.properties"), and optionally * a locale. The locale is defined as a string of the language and an optional country code * separated by underscore (e.g. "en_GB" or "fr"). If no locale is passed, the default * locale is "en" if the SAPUI5 framework is not available. Otherwise the default locale is taken from * the SAPUI5 configuration. * * With the getText() method of the resource bundle, a locale-specific string value * for a given key will be returned. * * With the given locale, the resource bundle requests the locale-specific properties file * (e.g. "mybundle_fr_FR.properties"). If no file is found for the requested locale or if the file * does not contain a text for the given key, a sequence of fallback locales is tried one by one. * First, if the locale contains a region information (fr_FR), then the locale without the region is * tried (fr). If that also can't be found or doesn't contain the requested text, a fallback language * will be used, if given (defaults to en (English), assuming that most development projects contain * at least English texts). If that also fails, the file without locale (base URL of the bundle, * often called the 'raw' bundle) is tried. * * If none of the requested files can be found or none of them contains a text for the given key, * then the key itself is returned as text. * * Exception: Fallback for "zh_HK" is "zh_TW" before "zh". * * @since 1.58 * @alias module:sap/base/i18n/ResourceBundle * @public * @hideconstructor */ function ResourceBundle(sUrl, sLocale, bIncludeInfo, bAsync, aSupportedLocales, sFallbackLocale, bSkipFallbackLocaleAndRaw){ // locale to retrieve texts for (normalized) this.sLocale = normalize(sLocale) || defaultLocale(sFallbackLocale === undefined ? sDefaultFallbackLocale : sFallbackLocale); this.oUrlInfo = splitUrl(sUrl); this.bIncludeInfo = bIncludeInfo; // list of custom bundles this.aCustomBundles = []; // declare list of property files that are loaded, // along with a list of origins this.aPropertyFiles = []; this.aPropertyOrigins = []; this.aLocales = []; // list of calculated fallbackLocales // note: every locale which was loaded is removed from this list this._aFallbackLocales = calculateFallbackChain( this.sLocale, // bundle specific supported locales will be favored over configuration ones aSupportedLocales || defaultSupportedLocales(), sFallbackLocale, " of the bundle '" + this.oUrlInfo.url + "'", bSkipFallbackLocaleAndRaw ); // load the most specific, existing properties file if (bAsync) { var resolveWithThis = function() { return this; }.bind(this); return loadNextPropertiesAsync(this).then(resolveWithThis, resolveWithThis); } loadNextPropertiesSync(this); } /** * Enhances the resource bundle with a custom resource bundle. The bundle * can be enhanced with multiple resource bundles. The last enhanced resource * bundle wins against the previous ones and the original ones. This function * can be called several times. * * @param {module:sap/base/i18n/ResourceBundle} oCustomBundle an instance of a <code>sap/base/i18n/ResourceBundle</code> * @private */ ResourceBundle.prototype._enhance = function(oCustomBundle) { if (oCustomBundle instanceof ResourceBundle) { this.aCustomBundles.push(oCustomBundle); } else { // we report the error but do not break the execution Log.error("Custom resource bundle is either undefined or not an instanceof sap/base/i18n/ResourceBundle. Therefore this custom resource bundle will be ignored!"); } }; /** * Returns a locale-specific string value for the given key sKey. * * The text is searched in this resource bundle according to the fallback chain described in * {@link module:sap/base/i18n/ResourceBundle}. If no text could be found, the key itself is used * as text. * * * <h3>Placeholders</h3> * * A text can contain placeholders that will be replaced with concrete values when * <code>getText</code> is called. The replacement is triggered by the <code>aArgs</code> parameter. * * Whenever this parameter is given, then the text and the arguments are additionally run through * the {@link module:sap/base/strings/formatMessage} API to replace placeholders in the text with * the corresponding values from the arguments array. The resulting string is returned by * <code>getText</code>. * * As the <code>formatMessage</code> API imposes some requirements on the input text (regarding * curly braces and single apostrophes), text authors need to be aware of the specifics of the * <code>formatMessage</code> API. Callers of <code>getText</code>, on the other side, should only * supply <code>aArgs</code> when the text has been created with the <code>formatMessage</code> API * in mind. Otherwise, single apostrophes in the text might be removed unintentionally. * * When <code>getText</code> is called without <code>aArgs</code>, the <code>formatMessage</code> * API is not applied and the transformation reg. placeholders and apostrophes does not happen. * * For more details on the replacement mechanism refer to {@link module:sap/base/strings/formatMessage}. * * @param {string} sKey Key to retrieve the text for * @param {any[]} [aArgs] List of parameter values which should replace the placeholders "{<i>n</i>}" * (<i>n</i> is the index) in the found locale-specific string value. Note that the replacement * is done whenever <code>aArgs</code> is given, no matter whether the text contains placeholders * or not and no matter whether <code>aArgs</code> contains a value for <i>n</i> or not. * @param {boolean} [bIgnoreKeyFallback=false] * If set, <code>undefined</code> is returned instead of the key string, when the key is not found * in any bundle or fallback bundle. * @returns {string|undefined} * The value belonging to the key, if found; otherwise the key itself or <code>undefined</code> * depending on <code>bIgnoreKeyFallback</code>. * * @public */ ResourceBundle.prototype.getText = function(sKey, aArgs, bIgnoreKeyFallback){ // 1. try to retrieve text from properties (including custom properties) var sValue = this._getTextFromProperties(sKey, aArgs); if (sValue != null) { return sValue; } // 2. try to retrieve text from fallback properties (including custom fallback properties) sValue = this._getTextFromFallback(sKey, aArgs); if (sValue != null) { return sValue; } if (bIgnoreKeyFallback) { return undefined; } else { assert(false, "could not find any translatable text for key '" + sKey + "' in bundle file(s): '" + this.aPropertyOrigins.join("', '") + "'"); return this._formatValue(sKey, sKey, aArgs); } }; /** * Enriches the input value with originInfo if <code>this.bIncludeInfo</code> is truthy. * Uses args to format the message. * @param {string} sValue the given input value * @param {string} sKey the key within the bundle * @param {array} [aArgs] arguments to format the message * @returns {string|null} formatted string, <code>null</code> if sValue is not a string * @private */ ResourceBundle.prototype._formatValue = function(sValue, sKey, aArgs){ if (typeof sValue === "string") { if (aArgs) { sValue = formatMessage(sValue, aArgs); } if (this.bIncludeInfo) { // String object is created on purpose and must not be a string literal // eslint-disable-next-line no-new-wrappers sValue = new String(sValue); sValue.originInfo = { source: "Resource Bundle", url: this.oUrlInfo.url, locale: this.sLocale, key: sKey }; } } return sValue; }; /** * Recursively loads synchronously the fallback locale's properties and looks up the value by key. * The custom bundles are checked first in reverse order. * @param {string} sKey the key within the bundle * @param {array} [aArgs] arguments to format the message * @returns {string|null} the formatted value if found, <code>null</code> otherwise * @private */ ResourceBundle.prototype._getTextFromFallback = function(sKey, aArgs){ var sValue, i; // loop over the custom bundles before resolving this one // lookup the custom resource bundles (last one first!) for (i = this.aCustomBundles.length - 1; i >= 0; i--) { sValue = this.aCustomBundles[i]._getTextFromFallback(sKey, aArgs); // value found - so return it! if (sValue != null) { return sValue; // found! } } // value for this key was not found in the currently loaded property files, // load the fallback locales while ( typeof sValue !== "string" && this._aFallbackLocales.length ) { var oProperties = loadNextPropertiesSync(this); // check whether the key is included in the newly loaded property file if (oProperties) { sValue = oProperties.getProperty(sKey); if (typeof sValue === "string") { return this._formatValue(sValue, sKey, aArgs); } } } return null; }; /** * Recursively loads locale's properties and looks up the value by key. * The custom bundles are checked first in reverse order. * @param {string} sKey the key within the bundle * @param {array} [aArgs] arguments to format the message * @returns {string|null} the formatted value if found, <code>null</code> otherwise * @private */ ResourceBundle.prototype._getTextFromProperties = function(sKey, aArgs){ var sValue = null, i; // loop over the custom bundles before resolving this one // lookup the custom resource bundles (last one first!) for (i = this.aCustomBundles.length - 1; i >= 0; i--) { sValue = this.aCustomBundles[i]._getTextFromProperties(sKey, aArgs); // value found - so return it! if (sValue != null) { return sValue; // found! } } // loop over all loaded property files and return the value for the key if any for (i = 0; i < this.aPropertyFiles.length; i++) { sValue = this.aPropertyFiles[i].getProperty(sKey); if (typeof sValue === "string") { return this._formatValue(sValue, sKey, aArgs); } } return null; }; /** * Checks whether a text for the given key can be found in the first loaded * resource bundle or not. Neither the custom resource bundles nor the * fallback chain will be processed. * * This method allows to check for the existence of a text without triggering * requests for the fallback locales. * * When requesting the resource bundle asynchronously this check must only be * used after the resource bundle has been loaded. * * @param {string} sKey Key to check * @returns {boolean} Whether the text has been found in the concrete bundle * * @public */ ResourceBundle.prototype.hasText = function(sKey) { return this.aPropertyFiles.length > 0 && typeof this.aPropertyFiles[0].getProperty(sKey) === "string"; }; /* * Tries to load properties files asynchronously until one could be loaded * successfully or until there are no more fallback locales. */ function loadNextPropertiesAsync(oBundle) { if ( oBundle._aFallbackLocales.length ) { return tryToLoadNextProperties(oBundle, true).then(function(oProps) { // if props could not be loaded, try next fallback locale return oProps || loadNextPropertiesAsync(oBundle); }); } // no more fallback locales: give up return Promise.resolve(null); } /* * Tries to load properties files synchronously until one could be loaded * successfully or until there are no more fallback locales. */ function loadNextPropertiesSync(oBundle) { while ( oBundle._aFallbackLocales.length ) { var oProps = tryToLoadNextProperties(oBundle, false); if ( oProps ) { return oProps; } } return null; } /* * Tries to load the properties file for the next fallback locale. * * If there is no further fallback locale or when requests for the next fallback locale are * suppressed by configuration or when the file cannot be loaded, <code>null</code> is returned. * * @param {module:sap/base/i18n/ResourceBundle} oBundle ResourceBundle to extend * @param {boolean} [bAsync=false] Whether the resource should be loaded asynchronously * @returns {module:sap/base/util/Properties|null|Promise<module:sap/base/util/Properties|null>} * The newly loaded properties (sync mode) or a Promise on the properties (async mode); * value / Promise fulfillment will be <code>null</code> when the properties for the * next fallback locale should not be loaded or when loading failed or when there * was no more fallback locale * @private */ function tryToLoadNextProperties(oBundle, bAsync) { // get the next fallback locale var sLocale = oBundle._aFallbackLocales.shift(); if ( sLocale != null) { var oUrl = oBundle.oUrlInfo, sUrl, mHeaders; if ( oUrl.ext === '.hdbtextbundle' ) { if ( M_SUPPORTABILITY_TO_XS[sLocale] ) { // Add technical support languages also as URL parameter (as XS engine can't handle private extensions in Accept-Language header) sUrl = oUrl.prefix + oUrl.suffix + '?' + (oUrl.query ? oUrl.query + "&" : "") + "sap-language=" + M_SUPPORTABILITY_TO_XS[sLocale] + (oUrl.hash ? "#" + oUrl.hash : ""); } else { sUrl = oUrl.url; } // Alternative: add locale as query: // url: oUrl.prefix + oUrl.suffix + '?' + (oUrl.query ? oUrl.query + "&" : "") + "locale=" + sLocale + (oUrl.hash ? "#" + oUrl.hash : ""), mHeaders = { "Accept-Language": convertLocaleToBCP47(sLocale) || "" }; } else { sUrl = oUrl.prefix + (sLocale ? "_" + sLocale : "") + oUrl.suffix; } var vProperties = Properties.create({ url: sUrl, headers: mHeaders, async: !!bAsync, returnNullIfMissing: true }); var addProperties = function(oProps) { if ( oProps ) { oBundle.aPropertyFiles.push(oProps); oBundle.aPropertyOrigins.push(sUrl); oBundle.aLocales.push(sLocale); } return oProps; }; return bAsync ? vProperties.then( addProperties ) : addProperties( vProperties ); } return bAsync ? Promise.resolve(null) : null; } /** * Gets the URL either from the given resource bundle name or the given resource bundle URL. * * @param {string} [bundleUrl] * URL pointing to the base ".properties" file of a bundle (".properties" file without any * locale information, e.g. "../../i18n/mybundle.properties"); relative URLs are evaluated * relative to the document.baseURI * @param {string} [bundleName] * UI5 module name in dot notation referring to the base ".properties" file; this name is * resolved to a path like the paths of normal UI5 modules and ".properties" is then * appended (e.g. a name like "myapp.i18n.myBundle" can be given); relative module names are * not supported * @returns {string} * The resource bundle URL * * @private * @ui5-restricted sap.ui.model.resource.ResourceModel */ ResourceBundle._getUrl = function(bundleUrl, bundleName) { var sUrl = bundleUrl; if (bundleName) { bundleName = bundleName.replace(/\./g, "/"); sUrl = sap.ui.require.toUrl(bundleName) + ".properties"; } return sUrl; }; /** * @returns {module:sap/base/i18n/ResourceBundle[]} The list of ResourceBundles created from enhanceWith */ function getEnhanceWithResourceBundles(aActiveTerminologies, aEnhanceWith, sLocale, bIncludeInfo, bAsync, sFallbackLocale, aSupportedLocales) { if (!aEnhanceWith) { return []; } var aCustomBundles = []; aEnhanceWith.forEach(function (oEnhanceWith) { // inherit fallbackLocale and supportedLocales if not defined if (oEnhanceWith.fallbackLocale === undefined) { oEnhanceWith.fallbackLocale = sFallbackLocale; } if (oEnhanceWith.supportedLocales === undefined) { oEnhanceWith.supportedLocales = aSupportedLocales; } var sUrl = ResourceBundle._getUrl(oEnhanceWith.bundleUrl, oEnhanceWith.bundleName); var vResourceBundle = new ResourceBundle(sUrl, sLocale, bIncludeInfo, bAsync, oEnhanceWith.supportedLocales, oEnhanceWith.fallbackLocale); aCustomBundles.push(vResourceBundle); if (oEnhanceWith.terminologies) { aCustomBundles = aCustomBundles.concat(getTerminologyResourceBundles(aActiveTerminologies, oEnhanceWith.terminologies, sLocale, bIncludeInfo, bAsync)); } }); return aCustomBundles; } /** * @returns {module:sap/base/i18n/ResourceBundle[]} The list of ResourceBundles created from terminologies */ function getTerminologyResourceBundles(aActiveTerminologies, oTerminologies, sLocale, bIncludeInfo, bAsync) { if (!aActiveTerminologies) { return []; } // only take activeTerminologies which are present // creates a copy of the given array (is reversed later on) aActiveTerminologies = aActiveTerminologies.filter(function (sActiveTechnology) { return oTerminologies.hasOwnProperty(sActiveTechnology); }); // reverse // the terminology resource bundles are enhancements of the current bundle // the lookup order for enhancements starts with the last enhancement // therefore to ensure that the first element in the activeTerminologies array is looked up first // this array needs to be reversed. // Note: Array#reverse modifies the original array aActiveTerminologies.reverse(); return aActiveTerminologies.map(function (sActiveTechnology) { var mParamsTerminology = oTerminologies[sActiveTechnology]; var sUrl = ResourceBundle._getUrl(mParamsTerminology.bundleUrl, mParamsTerminology.bundleName); var aSupportedLocales = mParamsTerminology.supportedLocales; return new ResourceBundle(sUrl, sLocale, bIncludeInfo, bAsync, aSupportedLocales, null, true); }); } /** * ResourceBundle Configuration * * A ResourceBundle Configuration holds information on where to load the ResourceBundle from * using the fallback chain and terminologies. * The location is retrieved from the <code>bundleUrl</code> and <code>bundleName</code> parameters * The locale used is influenced by the <code>supportedLocales</code> and <code>fallbackLocale</code> parameters * Terminologies of this ResourceBundle are loaded via the <code>terminologies</code> parameter * * Note: If omitted, the supportedLocales and the fallbackLocale are inherited from the parent ResourceBundle Configuration * * @typedef {object} module:sap/base/i18n/ResourceBundle.Configuration * @property {string} [bundleUrl] URL pointing to the base .properties file of a bundle (.properties file without any locale information, e.g. "i18n/mybundle.properties") * @property {string} [bundleName] UI5 module name in dot notation pointing to the base .properties file of a bundle (.properties file without any locale information, e.g. "i18n.mybundle") * @property {string[]} [supportedLocales] List of supported locales (aka 'language tags') to restrict the fallback chain. * Each entry in the array can either be a BCP47 language tag or a JDK compatible locale string * (e.g. "en-GB", "en_GB" or "en"). An empty string (<code>""</code>) represents the 'raw' bundle. * <b>Note:</b> The given language tags can use modern or legacy ISO639 language codes. Whatever * language code is used in the list of supported locales will also be used when requesting a file * from the server. If the <code>locale</code> contains a legacy language code like "sh" and the * <code>supportedLocales</code> contains [...,"sr",...], "sr" will be used in the URL. * This mapping works in both directions. * @property {string} [fallbackLocale="en"] A fallback locale to be used after all locales * derived from <code>locale</code> have been tried, but before the 'raw' bundle is used. * Can either be a BCP47 language tag or a JDK compatible locale string (e.g. "en-GB", "en_GB" * or "en"), defaults to "en" (English). * To prevent a generic fallback, use the empty string (<code>""</code>). * E.g. by providing <code>fallbackLocale: ""</code> and <code>supportedLocales: ["en"]</code>, * only the bundle "en" is requested without any fallback. * @property {Object<string,module:sap/base/i18n/ResourceBundle.TerminologyConfiguration>} [terminologies] * An object, mapping a terminology identifier (e.g. "oil") to a <code>ResourceBundle.TerminologyConfiguration</code>. * A terminology is a resource bundle configuration for a specific use case (e.g. "oil"). * It does neither have a <code>fallbackLocale</code> nor can it be enhanced with <code>enhanceWith</code>. * @public */ /** * ResourceBundle Terminology Configuration * * Terminologies represent a variant of a ResourceBundle. * They can be used to provide domain specific texts, e.g. for industries, e.g. "oil", "retail" or "health". * While "oil" could refer to a user as "driller", in "retail" a user could be a "customer" and in "health" a "patient". * While "oil" could refer to a duration as "hitch", in "retail" a duration could be a "season" and in "health" an "incubation period". * * Note: Terminologies do neither support a fallbackLocale nor nested terminologies in their configuration. * * @typedef {object} module:sap/base/i18n/ResourceBundle.TerminologyConfiguration * @property {string} [bundleUrl] URL pointing to the base .properties file of a bundle (.properties file without any locale information, e.g. "i18n/mybundle.properties") * @property {string} [bundleName] UI5 module name in dot notation pointing to the base .properties file of a bundle (.properties file without any locale information, e.g. "i18n.mybundle") * @property {string[]} [supportedLocales] List of supported locales (aka 'language tags') to restrict the fallback chain. * Each entry in the array can either be a BCP47 language tag or a JDK compatible locale string * (e.g. "en-GB", "en_GB" or "en"). An empty string (<code>""</code>) represents the 'raw' bundle. * <b>Note:</b> The given language tags can use modern or legacy ISO639 language codes. Whatever * language code is used in the list of supported locales will also be used when requesting a file * from the server. If the <code>locale</code> contains a legacy language code like "sh" and the * <code>supportedLocales</code> contains [...,"sr",...], "sr" will be used in the URL. * This mapping works in both directions. * @public */ /** * Creates and returns a new instance of {@link module:sap/base/i18n/ResourceBundle} * using the given URL and locale to determine what to load. * * Before loading the ResourceBundle, the locale is evaluated with a fallback chain. * Sample fallback chain for locale="de-DE" and fallbackLocale="fr_FR" * <code>"de-DE" -> "de" -> "fr_FR" -> "fr" -> raw</code> * * Only those locales are considered for loading, which are in the supportedLocales array * (if the array is supplied and not empty). * * Note: The fallbackLocale should be included in the supportedLocales array. * * * @example <caption>Load a resource bundle</caption> * * sap.ui.require(["sap/base/i18n/ResourceBundle"], function(ResourceBundle){ * // ... * ResourceBundle.create({ * // specify url of the base .properties file * url : "i18n/messagebundle.properties", * async : true * }).then(function(oResourceBundle){ * // now you can access the bundle * }); * // ... * }); * * @example <caption>Load a resource bundle with supported locales and fallback locale</caption> * * sap.ui.require(["sap/base/i18n/ResourceBundle"], function(ResourceBundle){ * // ... * ResourceBundle.create({ * // specify url of the base .properties file * url : "i18n/messagebundle.properties", * async : true, * supportedLocales: ["de", "da"], * fallbackLocale: "de" * }).then(function(oResourceBundle){ * // now you can access the bundle * }); * // ... * }); * * @example <caption>Load a resource bundle with terminologies 'oil' and 'retail'</caption> * * sap.ui.require(["sap/base/i18n/ResourceBundle"], function(ResourceBundle){ * // ... * ResourceBundle.create({ * // specify url of the base .properties file * url : "i18n/messagebundle.properties", * async : true, * supportedLocales: ["de", "da"], * fallbackLocale: "de", * terminologies: { * oil: { * bundleUrl: "i18n/terminologies.oil.i18n.properties", * supportedLocales: [ * "da", "en", "de" * ] * }, * retail: { * bundleUrl: "i18n/terminologies.retail.i18n.properties", * supportedLocales: [ * "da", "de" * ] * } * }, * activeTerminologies: ["retail", "oil"] * }).then(function(oResourceBundle){ * // now you can access the bundle * }); * // ... * }); * * @example <caption>Load a resource bundle with enhancements</caption> * * sap.ui.require(["sap/base/i18n/ResourceBundle"], function(ResourceBundle){ * // ... * ResourceBundle.create({ * // specify url of the base .properties file * url : "i18n/messagebundle.properties", * async : true, * supportedLocales: ["de", "da"], * fallbackLocale: "de", * enhanceWith: [ * { * bundleUrl: "appvar1/i18n/i18n.properties", * supportedLocales: ["da", "en", "de"] * }, * { * bundleUrl: "appvar2/i18n/i18n.properties", * supportedLocales: ["da", "de"] * } * ] * }).then(function(oResourceBundle){ * // now you can access the bundle * }); * // ... * }); * * @public * @function * @param {object} [mParams] Parameters used to initialize the resource bundle * @param {string} [mParams.url=''] URL pointing to the base .properties file of a bundle (.properties * file without any locale information, e.g. "mybundle.properties") * if not provided, <code>bundleUrl</code> or <code>bundleName</code> can be used; if both are set, * <code>bundleName</code> wins * @param {string} [mParams.bundleUrl] URL pointing to the base .properties file of a bundle * (.properties file without any locale information, e.g. "i18n/mybundle.properties") * @param {string} [mParams.bundleName] UI5 module name in dot notation pointing to the base * .properties file of a bundle (.properties file without any locale information, e.g. "i18n.mybundle") * @param {string} [mParams.locale] Optional locale (aka 'language tag') to load the texts for. * Can either be a BCP47 language tag or a JDK compatible locale string (e.g. "en-GB", "en_GB" or "en"). * Defaults to the current session locale if <code>sap.ui.getCore</code> is available, otherwise * to the provided <code>fallbackLocale</code> * @param {boolean} [mParams.includeInfo=false] Whether to include origin information into the returned property values * @param {string[]} [mParams.supportedLocales] List of supported locales (aka 'language tags') to restrict the fallback chain. * Each entry in the array can either be a BCP47 language tag or a JDK compatible locale string * (e.g. "en-GB", "en_GB" or "en"). An empty string (<code>""</code>) represents the 'raw' bundle. * <b>Note:</b> The given language tags can use modern or legacy ISO639 language codes. Whatever * language code is used in the list of supported locales will also be used when requesting a file * from the server. If the <code>locale</code> contains a legacy language code like "sh" and the * <code>supportedLocales</code> contains [...,"sr",...], "sr" will be used in the URL. * This mapping works in both directions. * @param {string} [mParams.fallbackLocale="en"] A fallback locale to be used after all locales * derived from <code>locale</code> have been tried, but before the 'raw' bundle is used. * Can either be a BCP47 language tag or a JDK compatible locale string (e.g. "en-GB", "en_GB" * or "en"). * To prevent a generic fallback, use the empty string (<code>""</code>). * E.g. by providing <code>fallbackLocale: ""</code> and <code>supportedLocales: ["en"]</code>, * only the bundle "en" is requested without any fallback. * @param {Object<string,module:sap/base/i18n/ResourceBundle.TerminologyConfiguration>} [mParams.terminologies] map of terminologies. * The key is the terminology identifier and the value is a ResourceBundle terminology configuration. * A terminology is a resource bundle configuration for a specific use case (e.g. "oil"). * It does neither have a <code>fallbackLocale</code> nor can it be enhanced with <code>enhanceWith</code>. * @param {string[]} [mParams.activeTerminologies] The list of active terminologies, * e.g. <code>["oil", "retail"]</code>. The order in this array represents the lookup order. * @param {module:sap/base/i18n/ResourceBundle.Configuration[]} [mParams.enhanceWith] List of ResourceBundle configurations which enhance the current one. * The order of the enhancements is significant, because the lookup checks the last enhancement first. * Each enhancement represents a ResourceBundle with limited options ('bundleUrl', 'bundleName', 'terminologies', 'fallbackLocale', 'supportedLocales'). * Note: supportedLocales and fallbackLocale are inherited from the parent ResourceBundle if not present. * @param {boolean} [mParams.async=false] Whether the first bundle should be loaded asynchronously * Note: Fallback bundles loaded by {@link #getText} are always loaded synchronously. * @returns {module:sap/base/i18n/ResourceBundle|Promise<module:sap/base/i18n/ResourceBundle>} * A new resource bundle or a Promise on that bundle (in asynchronous case) * @SecSink {0|PATH} Parameter is used for future HTTP requests */ ResourceBundle.create = function(mParams) { mParams = merge({url: "", includeInfo: false}, mParams); // bundleUrl and bundleName parameters get converted into the url parameter if the url parameter is not present if (mParams.bundleUrl || mParams.bundleName) { mParams.url = mParams.url || ResourceBundle._getUrl(mParams.bundleUrl, mParams.bundleName); } // Hook implemented by Core.js; adds missing terminology information from the library manifest, if available mParams = ResourceBundle._enrichBundleConfig(mParams); // Note: ResourceBundle constructor returns a Promise in async mode! var vResourceBundle = new ResourceBundle(mParams.url, mParams.locale, mParams.includeInfo, !!mParams.async, mParams.supportedLocales, mParams.fallbackLocale); // aCustomBundles is a flat list of all "enhancements" var aCustomBundles = []; // handle terminologies if (mParams.terminologies) { aCustomBundles = aCustomBundles.concat(getTerminologyResourceBundles(mParams.activeTerminologies, mParams.terminologies, mParams.locale, mParams.includeInfo, !!mParams.async)); } // handle enhanceWith if (mParams.enhanceWith) { aCustomBundles = aCustomBundles.concat(getEnhanceWithResourceBundles(mParams.activeTerminologies, mParams.enhanceWith, mParams.locale, mParams.includeInfo, !!mParams.async, mParams.fallbackLocale, mParams.supportedLocales)); } if (aCustomBundles.length) { if (vResourceBundle instanceof Promise) { vResourceBundle = vResourceBundle.then(function (oResourceBundle) { // load all resource bundles in parallel for a better performance // but do the enhancement one after the other to establish a stable lookup order return Promise.all(aCustomBundles).then(function (aCustomBundles) { aCustomBundles.forEach(oResourceBundle._enhance, oResourceBundle); }).then(function () { return oResourceBundle; }); }); } else { aCustomBundles.forEach(vResourceBundle._enhance, vResourceBundle); } } return vResourceBundle; }; /** * Hook implemented by sap.ui.core.Core. to enrich bundle config with terminologies. * See also the documentation of the hook's implementation in Core.js. * * @see sap.ui.core.Core.getLibraryResourceBundle * * @params {object} the ResourceBundle.create bundle config * @private * @ui5-restricted sap.ui.core.Core */ ResourceBundle._enrichBundleConfig = function(mParams) { // Note: the ResourceBundle is a base module, which might be used standalone without the Core, // so the bundle config must remain untouched return mParams; }; // ---- handling of supported locales and fallback chain ------------------------------------------ /** * Check if the given locale is contained in the given list of supported locales. * * If no list is given or if it is empty, any locale is assumed to be supported and * the given locale is returned without modification. * * When the list contains the given locale, the locale is also returned without modification. * * If an alternative code for the language code part of the locale exists (e.g a modern code * if the language is a legacy code, or a legacy code if the language is a modern code), then * the language code is replaced by the alternative code. If the resulting alternative locale * is contained in the list, the alternative locale is returned. * * If there is no match, <code>undefined</code> is returned. * @param {string} sLocale Locale, using legacy ISO639 language code, e.g. sh_RS * @param {string[]} aSupportedLocales List of supported locales, e.g. ["sr_RS"] * @returns {string} The match in the supportedLocales (using either modern or legacy ISO639 language codes), * e.g. "sr_RS"; <code>undefined</code> if not matched */ function findSupportedLocale(sLocale, aSupportedLocales) { // if supportedLocales array is empty or undefined or if it contains the given locale, // return that locale (with a legacy ISO639 language code) if (!aSupportedLocales || aSupportedLocales.length === 0 || aSupportedLocales.indexOf(sLocale) >= 0) { return sLocale; } // determine an alternative locale, using a modern ISO639 language code // (converts "sh_RS" to "sr-RS") sLocale = convertLocaleToBCP47(sLocale, true); if (sLocale) { // normalize it to JDK syntax for easier comparison // (converts "sr-RS" to "sr_RS" - using an underscore ("_") between the segments) sLocale = normalize(sLocale, true); } if (aSupportedLocales.indexOf(sLocale) >= 0) { // return the alternative locale (with a modern ISO639 language code) return sLocale; } return undefined; } /** * Determines the sequence of fallback locales, starting from the given locale. * * The fallback chain starts with the given <code>sLocale</code> itself. If this locale * has multiple segments (region, variant), further entries are added to the fallback * chain, each one omitting the last (rightmost) segment of its predecessor, making the * new locale entry less specific than the previous one (e.g. "de" after "de_CH"). * * If <code>sFallbackLocale</code> is given, it will be added to the fallback chain next. * If it consists of multiple segments, multiple locales will be added, each less specific * than the previous one. If <code>sFallbackLocale</code> is omitted or <code>undefined</code>, * "en" (English) will be added instead. If <code>sFallbackLocale</code> is the empty string * (""), no generic fallback will be added. * * Last but not least, the 'raw' locale will be added, represented by the empty string (""). * * The returned list will contain no duplicates and all entries will be in normalized JDK file suffix * format (using an underscore ("_") as separator, a lowercase language and an uppercase region * (if any)). * * If <code>aSupportedLocales</code> is provided and not empty, only locales contained * in that array will be added to the result. This allows to limit the backend requests * to a certain set of files (e.g. those that are known to exist). * * @param {string} sLocale Locale to start the fallback sequence with, must be normalized already * @param {string[]} [aSupportedLocales] List of supported locales (either BCP47 or JDK legacy syntax, e.g. zh_CN, iw) * @param {string} [sFallbackLocale="en"] Last fallback locale; is ignored when <code>bSkipFallbackLocaleAndRaw</code> is <code>true</code> * @param {string} [sContextInfo] Describes the context in which this function is called, only used for logging * @param {boolean} [bSkipFallbackLocaleAndRaw=false] Whether to skip fallbackLocale and raw bundle * @returns {string[]} Sequence of fallback locales in JDK legacy syntax, decreasing priority * * @private */ function calculateFallbackChain(sLocale, aSupportedLocales, sFallbackLocale, sContextInfo, bSkipFallbackLocaleAndRaw) { // Defines which locales are supported (BCP47 language tags or JDK locale format using underscores). // Normalization of the case and of the separator char simplifies later comparison, but the language // part is not converted to a legacy ISO639 code, in order to enable the support of modern codes as well. aSupportedLocales = aSupportedLocales && aSupportedLocales.map(function (sSupportedLocale) { return normalizePreserveEmpty(sSupportedLocale, true); }); if (!bSkipFallbackLocaleAndRaw) { // normalize the fallback locale for sanitizing it and converting the language part to legacy ISO639 // because it is like the locale part of the fallback chain var bFallbackLocaleDefined = sFallbackLocale !== undefined; sFallbackLocale = bFallbackLocaleDefined ? sFallbackLocale : sDefaultFallbackLocale; sFallbackLocale = normalizePreserveEmpty(sFallbackLocale); // An empty fallback locale ("") is valid and means that a generic fallback should not be loaded. // The supportedLocales must contain the fallbackLocale, or else it will be ignored. if (sFallbackLocale !== "" && !findSupportedLocale(sFallbackLocale, aSupportedLocales)) { var sMessage = "The fallback locale '" + sFallbackLocale + "' is not contained in the list of supported locales ['" + aSupportedLocales.join("', '") + "']" + sContextInfo + " and will be ignored."; // configuration error should be thrown if an invalid configuration has been provided if (bFallbackLocaleDefined) { throw new Error(sMessage); } Log.error(sMessage); } } // Calculate the list of fallback locales, starting with the given locale. // // Note: always keep this in sync with the fallback mechanism in Java, ABAP (MIME & BSP) // resource handler (Java: Peter M., MIME: Sebastian A., BSP: Silke A.) // fallback logic: // locale with region -> locale language -> fallback with region -> fallback language -> raw // note: if no region is present, it is skipped // Sample fallback chains: // "de_CH" -> "de" -> "en_US" -> "en" -> "" // locale 'de_CH', fallbackLocale 'en_US' // "de_CH" -> "de" -> "de_DE" -> "de" -> "" // locale 'de_CH', fallbackLocale 'de_DE' // "en_GB" -> "en" -> "" // locale 'en_GB', fallbackLocale 'en' // note: the resulting list does neither contain any duplicates nor unsupported locales // fallback calculation var aLocales = [], sSupportedLocale; while ( sLocale != null ) { // check whether sLocale is supported, potentially using an alternative language code sSupportedLocale = findSupportedLocale(sLocale, aSupportedLocales); // only push if it is supported and is not already contained (avoid duplicates) if ( sSupportedLocale !== undefined && aLocales.indexOf(sSupportedLocale) === -1) { aLocales.push(sSupportedLocale); } // calculate next one if (!sLocale) { // there is no fallback for the 'raw' locale or for null/undefined sLocale = null; } else if (sLocale === "zh_HK") { // special (legacy) handling for zh_HK: // try zh_TW (for "Traditional Chinese") first before falling back to 'zh' sLocale = "zh_TW"; } else if (sLocale.lastIndexOf('_') >= 0) { // if sLocale contains more than one segment (region, variant), remove the last one sLocale = sLocale.slice(0, sLocale.lastIndexOf('_')); } else if (bSkipFallbackLocaleAndRaw) { // skip fallbackLocale and raw bundle sLocale = null; } else if (sFallbackLocale) { // if there's a fallbackLocale, add it first before th