@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
360 lines (331 loc) • 15.9 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
sap.ui.define([
"sap/base/Log"
], function(Log) {
"use strict";
const rSAPSupportabilityLocales = /(?:^|-)(saptrc|sappsd|saprigi)(?:-|$)/i;
/**
* 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 -------]
*/
const 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").
* This mapping determines the appropriate language suffix for SAP translations when resolving a locale in a ResourceBundle.
* @private
*/
const M_LOCALE_TO_SAP_LANG = {
"he" : "iw",
"yi" : "ji",
"nb" : "no"
};
/**
* This mapping converts the old ISO639 codes into the corresponding language code used by ABAP systems,
* particularly for processing the Accept-Language header
* @private
*/
const M_ISO639_OLD_TO_NEW = {
"iw" : "he",
"ji" : "yi"
};
// key: <locale>|<preserveLanguage>, value: normalized locale
const oNormalizeCache = new Map();
/**
* 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. "he" to "iw"
* @returns {string|undefined} Normalized locale or <code>undefined</code> if the locale can't be normalized
* @private
*/
const normalize = function(sLocale, bPreserveLanguage) {
let cacheKey = sLocale;
if (bPreserveLanguage) {
cacheKey += "|true";
}
let normalizedLocale = oNormalizeCache.get(cacheKey);
if (normalizedLocale) {
return normalizedLocale;
}
var m;
if ( typeof sLocale === 'string' && (m = rLocale.exec(sLocale.replace(/_/g, '-'))) ) {
var sLanguage = m[1].toLowerCase();
if (!bPreserveLanguage) {
sLanguage = M_LOCALE_TO_SAP_LANG[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))) ) {
normalizedLocale = "en_US_" + m[1].toLowerCase(); // for now enforce en_US (agreed with SAP SLS)
oNormalizeCache.set(cacheKey, normalizedLocale);
return normalizedLocale;
}
// 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";
}
}
normalizedLocale = sLanguage + (sRegion ? "_" + sRegion + (sVariants ? "_" + sVariants.replace("-","_") : "") : "");
}
// Also cache undefined results to avoid repeated parsing of invalid locales
oNormalizeCache.set(cacheKey, normalizedLocale);
return normalizedLocale;
};
/**
* 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. "he" to "iw"
* @returns {string} normalized locale
* @throws {TypeError} Will throw an error if the locale is not a valid BCP47 language tag.
* @private
*/
const normalizePreserveEmpty = function(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;
};
/**
* 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
*/
const convertLocaleToBCP47 = function(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("_","-") : "") : "");
}
};
/**
* 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. iw_IL
* @param {string[]} aSupportedLocales List of supported locales, e.g. ["he_IL"]
* @returns {string} The match in the supportedLocales (using either modern or legacy ISO639 language codes),
* e.g. "he_IL"; <code>undefined</code> if not matched
*/
const findSupportedLocale = function(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.includes(sLocale)) {
return sLocale;
}
// determine an alternative locale, using a modern ISO639 language code
// (converts "iw_IL" to "he-IL")
sLocale = convertLocaleToBCP47(sLocale, true);
if (sLocale) {
// normalize it to JDK syntax for easier comparison
// (converts "he-IL" to "he_IL" - using an underscore ("_") between the segments)
sLocale = normalize(sLocale, true);
}
if (aSupportedLocales.includes(sLocale)) {
// 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
*/
const calculateFallbackChain = function(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 : "en";
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 the 'raw' locale
sLocale = sFallbackLocale;
sFallbackLocale = null; // no more fallback in the next round
} else {
// last fallback to raw bundle
sLocale = "";
}
}
return aLocales;
};
/**
* 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)
* @param {string} [sFallbackLocale] Last fallback locale, defaults to "en"
* @returns {string[]} Sequence of fallback locales in JDK legacy syntax, decreasing priority
*
* @private
* @ui5-restricted sap.fiori, sap.support launchpad
*/
const getFallbackLocales = function(sLocale, aSupportedLocales, sFallbackLocale) {
return calculateFallbackChain(
normalize(sLocale),
aSupportedLocales,
sFallbackLocale,
/* no context info */ ""
);
};
/**
* Helper module for locale-related functions, including normalization, conversion, and fallback calculations.
*
* @returns {Object} An object containing the helper functions
*
* @private
* @ui5-restricted sap.ui.core, sap.fiori, sap.support launchpad
*/
return {
convertLocaleToBCP47: convertLocaleToBCP47,
calculate: calculateFallbackChain,
getFallbackLocales: getFallbackLocales,
normalize: normalize
};
});