/*! * UI development toolkit for HTML5 (OpenUI5) * (c) Copyright 2009-2018 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' ], function(assert, Log, formatMessage, Properties) { "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", "in" for "id" and "sh" for "sr"). * 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", "id" : "in", "sr" : "sh", "nb" : "no" }; /** * Inverse of M_ISO639_NEW_TO_OLD. * @const * @private */ var M_ISO639_OLD_TO_NEW = { "iw" : "he", "ji" : "yi", "in" : "id", "sh" : "sr", "no" : "nb" }; /** * HANA XS Engine can't handle private extensions in BCP47 language tags. * Therefore, the agreed BCP47 codes for the technical languages 1Q and 2Q * 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" }; var rSAPSupportabilityLocales = /(?:^|-)(saptrc|sappsd)(?:-|$)/i; /** * Helper to normalize the given locale (in BCP-47 syntax) to the java.util.Locale format. * @param {string} sLocale locale to normalize * @returns {string} Normalized locale or undefined if the locale can't be normalized */ function normalize(sLocale) { var m; if ( typeof sLocale === 'string' && (m = rLocale.exec(sLocale.replace(/_/g, '-'))) ) { var sLanguage = m[1].toLowerCase(); 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"; } } return sLanguage + (sRegion ? "_" + sRegion + (sVariants ? "_" + sVariants.replace("-","_") : "") : ""); } } /** * Returns the default locale (the locale defined in UI5 configuration if available, else "en") * @returns {string} The default locale */ function defaultLocale() { 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 english if no or no valid locale is given return sLocale || "en"; } /** * Calculate the next fallback locale for 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.) * @param {string} sLocale Locale string in Java format (underscores) or null * @returns {string|null} Next fallback Locale or null if there is no more fallback * @private */ function nextFallbackLocale(sLocale) { // there is no fallback for the 'raw' locale or for null/undefined if ( !sLocale ) { return null; } // special (legacy) handling for zh_HK: try zh_TW (for Traditional Chinese) first before falling back to 'zh' if ( sLocale === "zh_HK" ) { return "zh_TW"; } // if there are multiple segments (separated by underscores), remove the last one var p = sLocale.lastIndexOf('_'); if ( p >= 0 ) { return sLocale.slice(0,p); } // invariant: only a single segment, must be a language // for any language but 'en', fallback to 'en' first before falling back to the 'raw' language (empty string) return sLocale !== 'en' ? 'en' : ''; } /** * Helper to normalize the given locale (java.util.Locale format) to the BCP-47 syntax. * @param {string} sLocale locale to convert * @returns {string} Normalized locale or undefined if the locale can't be normalized */ function convertLocaleToBCP47(sLocale) { var m; if ( typeof sLocale === 'string' && (m = rLocale.exec(sLocale.replace(/_/g, '-'))) ) { var sLanguage = m[1].toLowerCase(); 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 *
sap/base/i18n/ResourceBundle
* @private
*
* @function
* @name module:sap/base/i18n/ResourceBundle.prototype._enhance
*/
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.
*
* If the second parameteraArgs
is given, then any placeholder of the form "{n}"
* (with n being an integer) is replaced by the corresponding value from aArgs
* with index n. Note: This replacement is applied to the key if no text could be found.
* 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 {string[]} [aArgs] List of parameter values which should replace the placeholders "{n}"
* (n is the index) in the found locale-specific string value. Note that the replacement is done
* whenever aArgs
is given, no matter whether the text contains placeholders or not
* and no matter whether aArgs
contains a value for n or not.
* @param {boolean} bIgnoreKeyFallback If set, undefined
is returned when the key is not found in any bundle or fallback bundle, instead of the key string.
* @returns {string} The value belonging to the key, if found; Otherwise the key itself or undefined
depending on bIgnoreKeyFallback.
*
* @function
* @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;
}
assert(false, "could not find any translatable text for key '" + sKey + "' in bundle '" + this.oUrlInfo.url + "'");
if (bIgnoreKeyFallback){
return undefined;
} else {
return this._formatValue(sKey, sKey, aArgs);
}
};
/**
* Enriches the input value with originInfo if this.bIncludeInfo
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} formatted string, null
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) {
/* eslint-disable no-new-wrappers */
sValue = new String(sValue);
/* eslint-enable no-new-wrappers */
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} the formatted value if found, null
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._sNextLocale != null ) {
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} the formatted value if found, null
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} true if the text has been found in the concrete bundle
*
* @function
* @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._sNextLocale != null ) {
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._sNextLocale != null ) {
var oProps = tryToLoadNextProperties(oBundle, false);
if ( oProps ) {
return oProps;
}
}
return null;
}
/*
* Checks whether the given locale is supported by checking it
* against an array of supported locales.
* If the array is not given or is empty, any locale is supported.
*/
function isSupported(sLocale, aSupportedLocales) {
return !aSupportedLocales || aSupportedLocales.length === 0 || aSupportedLocales.indexOf(sLocale) >= 0;
}
/*
* 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, null
is returned.
*
* @param {ResourceBundle} oBundle ResourceBundle to extend
* @param {boolean} [bAsync=false] Whether the resource should be loaded asynchronously
* @returns The newly loaded properties (sync mode) or a Promise on the properties (async mode);
* value / Promise fulfillment will be null
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 and calculate the next but one locale
var sLocale = oBundle._sNextLocale;
oBundle._sNextLocale = nextFallbackLocale(sLocale);
var aSupportedLanguages = window.sap && window.sap.ui && sap.ui.getCore && sap.ui.getCore().getConfiguration().getSupportedLanguages();
if ( sLocale != null && isSupported(sLocale, aSupportedLanguages) ) {
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.aLocales.push(sLocale);
}
return oProps;
};
return bAsync ? vProperties.then( addProperties ) : addProperties( vProperties );
}
return bAsync ? Promise.resolve(null) : null;
}
/**
* Creates and returns a new instance of {@link module:sap/base/i18n/ResourceBundle}
* using the given URL and locale to determine what to load.
*
* @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")
* @param {string} [mParams.locale] Optional language (aka 'locale') 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 "fr");
* Defaults to the current session locale if sap.ui.getCore
is available, otherwise to 'en'
* @param {boolean} [mParams.includeInfo=false] Whether to include origin information into the returned property values
* @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} 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 = Object.assign({url: "", locale: undefined, includeInfo: false}, mParams);
// Note: ResourceBundle constructor returns a Promise in async mode!
return new ResourceBundle(mParams.url, mParams.locale, mParams.includeInfo, !!mParams.async);
};
/**
* Determine sequence of fallback locales, starting from the given locale and
* optionally taking the list of supported locales into account.
*
* Callers can use the result to limit requests to a set of existing locales.
*
* @param {string} sLocale Locale to start the fallback sequence with, should be a BCP47 language tag
* @param {string[]} [aSupportedLocales] List of supported locales (in JDK legacy syntax, e.g. zh_CN, iw)
* @returns {string[]} Sequence of fallback locales in JDK legacy syntax, decreasing priority
*
* @private
* @ui5-restricted sap.fiori, sap.support launchpad
*/
ResourceBundle._getFallbackLocales = function(sLocale, aSupportedLocales) {
var sTempLocale = normalize(sLocale),
aLocales = [];
while ( sTempLocale != null ) {
if ( isSupported(sTempLocale, aSupportedLocales) ) {
aLocales.push(sTempLocale);
}
sTempLocale = nextFallbackLocale(sTempLocale);
}
return aLocales;
};
return ResourceBundle;
});