@ui5/webcomponents-localization
Version:
Localization for UI5 Web Components
1,188 lines (1,173 loc) • 101 kB
JavaScript
import Core from "./Core.js"; /*!
* OpenUI5
* (c) Copyright 2009-2024 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
//Provides the locale object sap.ui.core.LocaleData
import CalendarType from "./CalendarType.js";
import Locale from "./Locale.js";
import assert from "../../base/assert.js";
import LanguageTag from "../../base/i18n/LanguageTag.js";
import Localization from "../../base/i18n/Localization.js";
import extend from "../../base/util/extend.js";
import LoaderExtensions from "../../base/util/LoaderExtensions.js";
import BaseObject from "../base/Object.js";
import Configuration from "./Configuration.js";
import CalendarWeekNumbering from "./date/CalendarWeekNumbering.js";
var rCIgnoreCase = /c/i,
rEIgnoreCase = /e/i,
/*
* With the upgrade of the CLDR to version 41 some unit keys have changed.
* For compatibility reasons this map is used for formatting units.
* It maps a legacy unit key to its renamed key.
*/
mLegacyUnit2CurrentUnit = {
"acceleration-meter-per-second-squared": "acceleration-meter-per-square-second",
"concentr-milligram-per-deciliter": "concentr-milligram-ofglucose-per-deciliter",
"concentr-part-per-million": "concentr-permillion",
"consumption-liter-per-100kilometers": "consumption-liter-per-100-kilometer",
"mass-metric-ton": "mass-tonne",
"pressure-millimeter-of-mercury": "pressure-millimeter-ofhg",
"pressure-pound-per-square-inch": "pressure-pound-force-per-square-inch",
"pressure-inch-hg": "pressure-inch-ofhg",
"torque-pound-foot": "torque-pound-force-foot"
},
rNumberInScientificNotation = /^([+-]?)((\d+)(?:\.(\d+))?)[eE]([+-]?\d+)$/,
rTrailingZeroes = /0+$/;
const rFallbackPatternTextParts = /(.*)?\{[0|1]}(.*)?\{[0|1]}(.*)?/;
const aSupportedWidths = ["narrow", "abbreviated", "wide"];
/**
* Creates an instance of LocaleData for the given locale.
*
* @class Provides access to locale-specific data, such as, date formats, number formats, and currencies.
*
* @param {sap.ui.core.Locale} oLocale the locale
*
* @extends sap.ui.base.Object
* @author SAP SE
* @version 1.120.17
* @public
* @alias sap.ui.core.LocaleData
*/
var LocaleData = BaseObject.extend("sap.ui.core.LocaleData", /** @lends sap.ui.core.LocaleData.prototype */{
constructor: function (oLocale) {
BaseObject.apply(this);
this.oLocale = Locale._getCoreLocale(oLocale);
var oDataLoaded = getData(this.oLocale);
this.mData = oDataLoaded.mData;
this.sCLDRLocaleId = oDataLoaded.sCLDRLocaleId;
},
/**
* @private
* @ui5-restricted UI5 Web Components
*/
_get: function () {
return this._getDeep(this.mData, arguments);
},
/**
* Retrieves merged object if overlay data is available
* @private
* @return {object} merged object
*/
_getMerged: function () {
return this._get.apply(this, arguments);
},
/**
* Get month names in width "narrow", "abbreviated" or "wide". Result may contain alternative month names.
*
* @param {"narrow"|"abbreviated"|"wide"} sWidth
* The required width for the month names
* @param {sap.ui.core.CalendarType} [sCalendarType]
* The type of calendar; defaults to the calendar type either set in configuration or calculated from locale
* @returns {array}
* The array of month names; if no alternative exists the entry for the month is its name as a string; if
* there are alternative month names the entry for the month is an array of strings with the alternative names
* @private
*/
_getMonthsWithAlternatives: function (sWidth, sCalendarType) {
return this._get(getCLDRCalendarName(sCalendarType), "months", "format", sWidth);
},
/**
* Get standalone month names in width "narrow", "abbreviated" or "wide". Result may contain alternative month
* names.
*
* @param {"narrow"|"abbreviated"|"wide"} sWidth
* The required width for the month names
* @param {sap.ui.core.CalendarType} [sCalendarType]
* The type of calendar; defaults to the calendar type either set in configuration or calculated from locale
* @returns {array}
* The array of month names; if no alternative exists the entry for the month is its name as a string; if
* there are alternative month names the entry for the month is an array of strings with the alternative names
* @private
*/
_getMonthsStandAloneWithAlternatives: function (sWidth, sCalendarType) {
return this._get(getCLDRCalendarName(sCalendarType), "months", "stand-alone", sWidth);
},
_getDeep: function (oObject, aPropertyNames) {
var oResult = oObject;
for (var i = 0; i < aPropertyNames.length; i++) {
oResult = oResult[aPropertyNames[i]];
if (oResult === undefined) {
break;
}
}
return oResult;
},
/**
* Get orientation (left-to-right or right-to-left).
*
* @returns {string} character orientation for this locale
* @public
*/
getOrientation: function () {
return this._get("orientation");
},
/**
* Get a display name for the language of the Locale of this LocaleData, using
* the CLDR display names for languages.
*
* The lookup logic works as follows:
* 1. language code and region is checked (e.g. "en-GB")
* 2. If not found: language code and script is checked (e.g. "zh-Hant")
* 3. If not found language code is checked (e.g. "en")
* 4. If it is then still not found <code>undefined</code> is returned.
*
* @returns {string} language name, e.g. "English", "British English", "American English"
* or <code>undefined</code> if language cannot be found
* @private
* @ui5-restricted sap.ushell
*/
getCurrentLanguageName: function () {
return this.getLanguageName(this.oLocale.toString());
},
/**
* Gets the locale-specific language name for the given language tag.
*
* The languages returned by {@link #getLanguages} from the CLDR raw data do not contain the
* language names if they can be derived from the language and the script or the territory.
* If the map of languages contains no entry for the given language tag, derive the language
* name from the used script or region.
*
* @param {string} sLanguageTag
* The language tag, for example "en", "en-US", "en_US", "zh-Hant", or "zh_Hant"
* @returns {string|undefined}
* The language name, or <code>undefined</code> if the name cannot be determined
* @throws {TypeError} When the given language tag isn't valid
*
* @public
*/
getLanguageName: function (sLanguageTag) {
const oLanguageTag = new LanguageTag(sLanguageTag);
let sLanguage = Localization.getModernLanguage(oLanguageTag.language);
let sScript = oLanguageTag.script;
// special case for "sr_Latn" language: "sh" should then be used
if (sLanguage === "sr" && sScript === "Latn") {
sLanguage = "sh";
sScript = null;
}
const sRegion = oLanguageTag.region;
const oLanguages = this._get("languages");
const sLanguageText = oLanguages[sLanguage];
if (!sScript && !sRegion || !sLanguageText) {
return sLanguageText;
}
const sResult = oLanguages[sLanguage + "_" + sRegion] || oLanguages[sLanguage + "_" + sScript];
if (sResult) {
return sResult;
}
if (sScript) {
const sScriptText = this._get("scripts")[sScript];
if (sScriptText) {
return sLanguageText + " (" + sScriptText + ")";
}
}
if (sRegion) {
const sRegionText = this._get("territories")[sRegion];
if (sRegionText) {
return sLanguageText + " (" + sRegionText + ")";
}
}
return sLanguageText;
},
/**
* Gets locale-specific language names, as available in the CLDR raw data.
*
* To avoid redundancies, with CLDR version 43 only language names are contained which cannot be derived from
* the language and the script or the territory. If a language tag is not contained in the map, use
* {@link #getLanguageName} to get the derived locale-specific language name for that language tag.
*
* @returns {Object<string, string>} Maps a language tag to the locale-specific language name
*
* @public
*/
getLanguages: function () {
const oLanguages = this._get("languages");
/** @deprecated As of version 1.120.0 */
["ar_001", "de_AT", "de_CH", "en_AU", "en_CA", "en_GB", "en_US", "es_419", "es_ES", "es_MX", "fa_AF", "fr_CA", "fr_CH", "nds_NL", "nl_BE", "pt_BR", "pt_PT", "ro_MD", "sw_CD", "zh_Hans", "zh_Hant"].forEach(sLanguageTag => {
// for compatibility reasons, ensure that for these language tags the corresponding language names are
// available
if (!oLanguages[sLanguageTag]) {
oLanguages[sLanguageTag] = this.getLanguageName(sLanguageTag);
}
});
return oLanguages;
},
/**
* Gets locale-specific script names, as available in the CLDR raw data.
*
* To avoid redundancies, with CLDR version 43 only scripts are contained for which the language-specific name
* is different from the script key. If a script key is not contained in the map, use the script key as script
* name.
*
* @returns {Object<string, string>} Maps a script key to the locale-specific script name
*
* @public
*/
getScripts: function () {
return this._get("scripts");
},
/**
* Gets locale-specific territory names, as available in the CLDR raw data.
*
* To avoid redundancies, with CLDR version 43 only territories are contained for which the language-specific
* name is different from the territory key.
*
* @returns {Object<string, string>} Maps a territory key to the locale-specific territory name
*
* @public
*/
getTerritories: function () {
return this._get("territories");
},
/**
* Get month names in width "narrow", "abbreviated" or "wide".
*
* @param {"narrow"|"abbreviated"|"wide"} sWidth
* The required width for the month names
* @param {sap.ui.core.CalendarType} [sCalendarType]
* The type of calendar; defaults to the calendar type either set in configuration or calculated from locale
* @returns {string[]}
* The array of month names
* @public
*/
getMonths: function (sWidth, sCalendarType) {
assert(aSupportedWidths.includes(sWidth), "sWidth must be narrow, abbreviated or wide");
return this._get(getCLDRCalendarName(sCalendarType), "months", "format", sWidth).map(vMonthName => {
return Array.isArray(vMonthName) ? vMonthName[0] : vMonthName;
});
},
/**
* Get standalone month names in width "narrow", "abbreviated" or "wide".
*
* @param {"narrow"|"abbreviated"|"wide"} sWidth
* The required width for the month names
* @param {sap.ui.core.CalendarType} [sCalendarType]
* The type of calendar; defaults to the calendar type either set in configuration or calculated from locale
* @returns {string[]}
* The array of standalone month names
* @public
*/
getMonthsStandAlone: function (sWidth, sCalendarType) {
assert(aSupportedWidths.includes(sWidth), "sWidth must be narrow, abbreviated or wide");
return this._get(getCLDRCalendarName(sCalendarType), "months", "stand-alone", sWidth).map(vMonthName => {
return Array.isArray(vMonthName) ? vMonthName[0] : vMonthName;
});
},
/**
* Get day names in width "narrow", "abbreviated" or "wide".
*
* @param {string} sWidth the required width for the day names
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {array} array of day names (starting with Sunday)
* @public
*/
getDays: function (sWidth, sCalendarType) {
assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide" || sWidth == "short", "sWidth must be narrow, abbreviate, wide or short");
return this._get(getCLDRCalendarName(sCalendarType), "days", "format", sWidth);
},
/**
* Get standalone day names in width "narrow", "abbreviated" or "wide".
*
* @param {string} sWidth the required width for the day names
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {array} array of day names (starting with Sunday)
* @public
*/
getDaysStandAlone: function (sWidth, sCalendarType) {
assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide" || sWidth == "short", "sWidth must be narrow, abbreviated, wide or short");
return this._get(getCLDRCalendarName(sCalendarType), "days", "stand-alone", sWidth);
},
/**
* Get quarter names in width "narrow", "abbreviated" or "wide".
*
* @param {string} sWidth the required width for the quarter names
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {array} array of quarters
* @public
*/
getQuarters: function (sWidth, sCalendarType) {
assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide");
return this._get(getCLDRCalendarName(sCalendarType), "quarters", "format", sWidth);
},
/**
* Get standalone quarter names in width "narrow", "abbreviated" or "wide".
*
* @param {string} sWidth the required width for the quarter names
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {array} array of quarters
* @public
*/
getQuartersStandAlone: function (sWidth, sCalendarType) {
assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide");
return this._get(getCLDRCalendarName(sCalendarType), "quarters", "stand-alone", sWidth);
},
/**
* Get day periods in width "narrow", "abbreviated" or "wide".
*
* @param {string} sWidth the required width for the day period names
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {array} array of day periods (AM, PM)
* @public
*/
getDayPeriods: function (sWidth, sCalendarType) {
assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide");
return this._get(getCLDRCalendarName(sCalendarType), "dayPeriods", "format", sWidth);
},
/**
* Get standalone day periods in width "narrow", "abbreviated" or "wide".
*
* @param {string} sWidth the required width for the day period names
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {array} array of day periods (AM, PM)
* @public
*/
getDayPeriodsStandAlone: function (sWidth, sCalendarType) {
assert(sWidth == "narrow" || sWidth == "abbreviated" || sWidth == "wide", "sWidth must be narrow, abbreviated or wide");
return this._get(getCLDRCalendarName(sCalendarType), "dayPeriods", "stand-alone", sWidth);
},
/**
* Get date pattern in format "short", "medium", "long" or "full".
*
* @param {string} sStyle the required style for the date pattern
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} the selected date pattern
* @public
*/
getDatePattern: function (sStyle, sCalendarType) {
assert(sStyle == "short" || sStyle == "medium" || sStyle == "long" || sStyle == "full", "sStyle must be short, medium, long or full");
return this._get(getCLDRCalendarName(sCalendarType), "dateFormats", sStyle);
},
/**
* Get flexible day periods in style format "abbreviated", "narrow" or "wide".
*
* @param {string} sWidth
* The required width for the flexible day period names
* @param {sap.ui.core.CalendarType} [sCalendarType]
* The type of calendar. If it's not set, it falls back to the calendar type either set in
* configuration or calculated from locale.
* @returns {object|undefined}
* Object of flexible day periods or 'undefined' if none can be found
*
* @example <caption>Output</caption>
* {
* "midnight": "midnight",
* "noon": "noon",
* "morning1": "in the morning",
* "afternoon1": "in the afternoon",
* "evening1": "in the evening",
* "night1": "at night"
* }
*
* @private
*/
getFlexibleDayPeriods: function (sWidth, sCalendarType) {
return this._get(getCLDRCalendarName(sCalendarType), "flexibleDayPeriods", "format", sWidth);
},
/**
* Get flexible day periods in style format "abbreviated", "narrow" or "wide" for case
* "stand-alone".
*
* @param {string} sWidth
* The required width for the flexible day period names
* @param {sap.ui.core.CalendarType} [sCalendarType]
* The type of calendar. If it's not set, it falls back to the calendar type either set in
* configuration or calculated from locale.
* @returns {object|undefined}
* Object of flexible day periods or 'undefined' if none can be found
*
* @example <caption>Output</caption>
* {
* "midnight": "midnight",
* "noon": "noon",
* "morning1": "in the morning",
* "afternoon1": "in the afternoon",
* "evening1": "in the evening",
* "night1": "at night"
* }
*
* @private
*/
getFlexibleDayPeriodsStandAlone: function (sWidth, sCalendarType) {
return this._get(getCLDRCalendarName(sCalendarType), "flexibleDayPeriods", "stand-alone", sWidth);
},
/**
* Get flexible day period of time or a point in time
*
* @param {int} iHour Hour
* @param {int} iMinute Minute
* @returns {string} Key of flexible day period of time e.g. <code>afternoon2</code>
*
* @private
*/
getFlexibleDayPeriodOfTime: function (iHour, iMinute) {
var iAbsoluteMinutes, oDayPeriodRules, sPeriodMatch;
iAbsoluteMinutes = (iHour * 60 + iMinute) % 1440;
oDayPeriodRules = this._get("dayPeriodRules");
function parseToAbsoluteMinutes(sValue) {
var aSplit = sValue.split(":"),
sHour = aSplit[0],
sMinute = aSplit[1];
return parseInt(sHour) * 60 + parseInt(sMinute);
}
// unfortunately there are some overlaps:
// e.g. en.json
// "afternoon1": {
// "_before": "18:00",
// "_from": "12:00"
// },
// "noon": {
// "_at": "12:00"
// }
// -> 12:00 can be either "noon" or "afternoon1" because "_from" is inclusive
// therefore first check all exact periods
sPeriodMatch = Object.keys(oDayPeriodRules).find(function (sDayPeriodRule) {
var oDayPeriodRule = oDayPeriodRules[sDayPeriodRule];
return oDayPeriodRule["_at"] && parseToAbsoluteMinutes(oDayPeriodRule["_at"]) === iAbsoluteMinutes;
});
if (sPeriodMatch) {
return sPeriodMatch;
}
return Object.keys(oDayPeriodRules).find(function (sDayPeriodRule) {
var iEndValue,
aIntervals,
iStartValue,
oDayPeriodRule = oDayPeriodRules[sDayPeriodRule];
if (oDayPeriodRule["_at"]) {
return false;
}
iStartValue = parseToAbsoluteMinutes(oDayPeriodRule["_from"]);
iEndValue = parseToAbsoluteMinutes(oDayPeriodRule["_before"]);
// periods which span across days need to be split into individual intervals
// e.g. "22:00 - 03:00" becomes "22:00 - 24:00" and "00:00 - 03:00"
if (iStartValue > iEndValue) {
aIntervals = [{
start: iStartValue,
end: 1440
},
// 24 * 60
{
start: 0,
end: iEndValue
}];
} else {
aIntervals = [{
start: iStartValue,
end: iEndValue
}];
}
return aIntervals.some(function (oInterval) {
return oInterval.start <= iAbsoluteMinutes && oInterval.end > iAbsoluteMinutes;
});
});
},
/**
* Get time pattern in style "short", "medium", "long" or "full".
*
* @param {string} sStyle the required style for the date pattern
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} the selected time pattern
* @public
*/
getTimePattern: function (sStyle, sCalendarType) {
assert(sStyle == "short" || sStyle == "medium" || sStyle == "long" || sStyle == "full", "sStyle must be short, medium, long or full");
return this._get(getCLDRCalendarName(sCalendarType), "timeFormats", sStyle);
},
/**
* Get datetime pattern in style "short", "medium", "long" or "full".
*
* @param {string} sStyle the required style for the datetime pattern
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} the selected datetime pattern
* @public
*/
getDateTimePattern: function (sStyle, sCalendarType) {
assert(sStyle == "short" || sStyle == "medium" || sStyle == "long" || sStyle == "full", "sStyle must be short, medium, long or full");
return this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", sStyle);
},
/**
* Get combined datetime pattern with given date and time style.
*
* @param {string} sDateStyle the required style for the date part
* @param {string} sTimeStyle the required style for the time part
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} the combined datetime pattern
* @public
*/
getCombinedDateTimePattern: function (sDateStyle, sTimeStyle, sCalendarType) {
assert(sDateStyle == "short" || sDateStyle == "medium" || sDateStyle == "long" || sDateStyle == "full", "sStyle must be short, medium, long or full");
assert(sTimeStyle == "short" || sTimeStyle == "medium" || sTimeStyle == "long" || sTimeStyle == "full", "sStyle must be short, medium, long or full");
var sDateTimePattern = this.getDateTimePattern(sDateStyle, sCalendarType),
sDatePattern = this.getDatePattern(sDateStyle, sCalendarType),
sTimePattern = this.getTimePattern(sTimeStyle, sCalendarType);
return sDateTimePattern.replace("{0}", sTimePattern).replace("{1}", sDatePattern);
},
/**
* Get combined pattern with datetime and timezone for the given date and time style.
*
* @example
* // locale de
* oLocaleData.getCombinedDateTimeWithTimezonePattern("long", "long");
* // "d. MMMM y 'um' HH:mm:ss z VV"
*
* // locale en_GB
* oLocaleData.getCombinedDateTimeWithTimezonePattern("long", "long");
* // "d MMMM y 'at' HH:mm:ss z VV"
*
* @param {string} sDateStyle The required style for the date part
* @param {string} sTimeStyle The required style for the time part
* @param {sap.ui.core.CalendarType} [sCalendarType] The type of calendar. If it's not set,
* it falls back to the calendar type either set in the configuration or calculated from
* the locale.
* @returns {string} the combined pattern with datetime and timezone
* @private
* @ui5-restricted sap.ui.core.format.DateFormat
* @since 1.101
*/
getCombinedDateTimeWithTimezonePattern: function (sDateStyle, sTimeStyle, sCalendarType) {
return this.applyTimezonePattern(this.getCombinedDateTimePattern(sDateStyle, sTimeStyle, sCalendarType));
},
/**
* Applies the timezone to the pattern
*
* @param {string} sPattern pattern, e.g. <code>y</code>
* @returns {string} applied timezone, e.g. <code>y VV</code>
* @private
* @ui5-restricted sap.ui.core.format.DateFormat
* @since 1.101
*/
applyTimezonePattern: function (sPattern) {
var aPatterns = [sPattern];
var aMissingTokens = [{
group: "Timezone",
length: 2,
field: "zone",
symbol: "V"
}];
this._appendItems(aPatterns, aMissingTokens);
return aPatterns[0];
},
/**
* Retrieves all timezone translations.
*
* E.g. for locale "en"
* <pre>
* {
* "America/New_York": "Americas, New York"
* ...
* }
* </pre>
*
* @return {Object<string, string>} the mapping, with 'key' being the IANA timezone ID, and
* 'value' being the translation.
* @ui5-restricted sap.ui.core.format.DateFormat, sap.ui.export, sap.ushell
* @private
*/
getTimezoneTranslations: function () {
var sLocale = this.oLocale.toString();
var mTranslations = LocaleData._mTimezoneTranslations[sLocale];
if (!mTranslations) {
LocaleData._mTimezoneTranslations[sLocale] = mTranslations = _resolveTimezoneTranslationStructure(this._get("timezoneNames"));
}
// retrieve a copy such that the original object won't be modified.
return Object.assign({}, mTranslations);
},
/**
* Get custom datetime pattern for a given skeleton format.
*
* The format string does contain pattern symbols (e.g. "yMMMd" or "Hms") and will be converted into the pattern in the used
* locale, which matches the wanted symbols best. The symbols must be in canonical order, that is:
* Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D),
* Hour (h/H/k/K/), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x)
*
* See https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems
*
* @param {string} sSkeleton the wanted skeleton format for the datetime pattern
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} the best matching datetime pattern
* @since 1.34
* @public
*/
getCustomDateTimePattern: function (sSkeleton, sCalendarType) {
var oAvailableFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "availableFormats");
return this._getFormatPattern(sSkeleton, oAvailableFormats, sCalendarType);
},
/**
* Returns the interval format with the given Id (see CLDR documentation for valid Ids)
* or the fallback format if no interval format with that Id is known.
*
* The empty Id ("") might be used to retrieve the interval format fallback.
*
* @param {string} sId Id of the interval format, e.g. "d-d"
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} interval format string with placeholders {0} and {1}
* @public
* @since 1.17.0
*/
getIntervalPattern: function (sId, sCalendarType) {
var oIntervalFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "intervalFormats"),
aIdParts,
sIntervalId,
sDifference,
oInterval,
sPattern;
if (sId) {
aIdParts = sId.split("-");
sIntervalId = aIdParts[0];
sDifference = aIdParts[1];
oInterval = oIntervalFormats[sIntervalId];
if (oInterval) {
sPattern = oInterval[sDifference];
if (sPattern) {
return sPattern;
}
}
}
return oIntervalFormats.intervalFormatFallback;
},
/**
* Get combined interval pattern using a given pattern and the fallback interval pattern.
*
* If a skeleton based pattern is not available or not wanted, this method can be used to create an interval
* pattern based on a given pattern, using the fallback interval pattern.
*
* @param {string} sPattern the single date pattern to use within the interval pattern
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string} the calculated interval pattern
* @since 1.46
* @public
*/
getCombinedIntervalPattern: function (sPattern, sCalendarType) {
const oIntervalFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "intervalFormats");
const [/*sAll*/, sTextBefore, sTextBetween, sTextAfter] = rFallbackPatternTextParts.exec(oIntervalFormats.intervalFormatFallback);
// text part of intervalFormatFallback is not escaped
return LocaleData._escapeIfNeeded(sTextBefore) + sPattern + LocaleData._escapeIfNeeded(sTextBetween) + sPattern + LocaleData._escapeIfNeeded(sTextAfter);
},
/**
* Get interval pattern for a given skeleton format.
*
* The format string does contain pattern symbols (e.g. "yMMMd" or "Hms") and will be converted into the pattern in the used
* locale, which matches the wanted symbols best. The symbols must be in canonical order, that is:
* Era (G), Year (y/Y), Quarter (q/Q), Month (M/L), Week (w/W), Day-Of-Week (E/e/c), Day (d/D),
* Hour (h/H/k/K/), Minute (m), Second (s), Timezone (z/Z/v/V/O/X/x)
*
* See https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems
*
* @param {string} sSkeleton the wanted skeleton format for the datetime pattern
* @param {object|string} vGreatestDiff is either a string which represents the symbol matching the greatest difference in the two dates to format or an object which contains key-value pairs.
* The value is always true. The key is one of the date field symbol groups whose value are different between the two dates. The key can only be set with 'Year', 'Quarter', 'Month', 'Week',
* 'Day', 'DayPeriod', 'Hour', 'Minute', or 'Second'.
* @param {sap.ui.core.CalendarType} [sCalendarType] the type of calendar. If it's not set, it falls back to the calendar type either set in configuration or calculated from locale.
* @returns {string|string[]} the best matching interval pattern if interval difference is given otherwise an array with all possible interval patterns which match the given skeleton format
* @since 1.46
* @public
*/
getCustomIntervalPattern: function (sSkeleton, vGreatestDiff, sCalendarType) {
var oAvailableFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "intervalFormats");
return this._getFormatPattern(sSkeleton, oAvailableFormats, sCalendarType, vGreatestDiff);
},
/* Helper functions for skeleton pattern processing */
_getFormatPattern: function (sSkeleton, oAvailableFormats, sCalendarType, vDiff) {
var vPattern, aPatterns, oIntervalFormats;
if (!vDiff) {
// the call is from getCustomDateTimePattern
vPattern = oAvailableFormats[sSkeleton];
} else if (typeof vDiff === "string") {
// vDiff is given as a symbol
if (vDiff == "j" || vDiff == "J") {
vDiff = this.getPreferredHourSymbol();
}
oIntervalFormats = oAvailableFormats[sSkeleton];
vPattern = oIntervalFormats && oIntervalFormats[vDiff];
}
if (vPattern) {
if (typeof vPattern === "object") {
aPatterns = Object.keys(vPattern).map(function (sKey) {
return vPattern[sKey];
});
} else {
return vPattern;
}
}
if (!aPatterns) {
aPatterns = this._createFormatPattern(sSkeleton, oAvailableFormats, sCalendarType, vDiff);
}
if (aPatterns && aPatterns.length === 1) {
return aPatterns[0];
}
return aPatterns;
},
_createFormatPattern: function (sSkeleton, oAvailableFormats, sCalendarType, vDiff) {
var aTokens = this._parseSkeletonFormat(sSkeleton),
aPatterns,
oBestMatch = this._findBestMatch(aTokens, sSkeleton, oAvailableFormats),
oToken,
oAvailableDateTimeFormats,
oSymbol,
oGroup,
sPattern,
sSinglePattern,
sDiffSymbol,
sDiffGroup,
rMixedSkeleton = /^([GyYqQMLwWEecdD]+)([hHkKjJmszZvVOXx]+)$/,
bSingleDate,
i;
if (vDiff) {
if (typeof vDiff === "string") {
sDiffGroup = mCLDRSymbols[vDiff] ? mCLDRSymbols[vDiff].group : "";
if (sDiffGroup) {
// if the index of interval diff is greater than the index of the last field
// in the sSkeleton, which means the diff unit is smaller than all units in
// the skeleton, return a single date pattern which is generated using the
// given skeleton
bSingleDate = mCLDRSymbolGroups[sDiffGroup].index > aTokens[aTokens.length - 1].index;
}
sDiffSymbol = vDiff;
} else {
bSingleDate = true;
// Special handling of "y" (Year) in case patterns contains also "G" (Era)
if (aTokens[0].symbol === "y" && oBestMatch && oBestMatch.pattern.G) {
oSymbol = mCLDRSymbols["G"];
oGroup = mCLDRSymbolGroups[oSymbol.group];
aTokens.splice(0, 0, {
symbol: "G",
group: oSymbol.group,
match: oSymbol.match,
index: oGroup.index,
field: oGroup.field,
length: 1
});
}
// Check if at least one token's group appears in the interval diff
// If not, a single date pattern is returned
for (i = aTokens.length - 1; i >= 0; i--) {
oToken = aTokens[i];
if (vDiff[oToken.group]) {
bSingleDate = false;
break;
}
}
// select the greatest diff symbol
for (i = 0; i < aTokens.length; i++) {
oToken = aTokens[i];
if (vDiff[oToken.group]) {
sDiffSymbol = oToken.symbol;
break;
}
}
// Special handling of "a" (Dayperiod)
// Find out whether dayperiod is different between the dates
// If yes, set the diff symbol with 'a' Dayperiod symbol
if ((sDiffSymbol == "h" || sDiffSymbol == "K") && vDiff.DayPeriod) {
sDiffSymbol = "a";
}
}
if (bSingleDate) {
return [this.getCustomDateTimePattern(sSkeleton, sCalendarType)];
}
// Only use best match, if there are no missing tokens, as there is no possibility
// to append items on interval formats
if (oBestMatch && oBestMatch.missingTokens.length === 0) {
sPattern = oBestMatch.pattern[sDiffSymbol];
// if there is no exact match, we need to do further processing
if (sPattern && oBestMatch.distance > 0) {
sPattern = this._expandFields(sPattern, oBestMatch.patternTokens, aTokens);
}
}
// If no pattern could be found, get the best availableFormat for the skeleton
// and use the fallbackIntervalFormat to create the pattern
if (!sPattern) {
oAvailableDateTimeFormats = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "availableFormats");
// If it is a mixed skeleton and the greatest interval on time, create a mixed pattern
if (rMixedSkeleton.test(sSkeleton) && "ahHkKjJms".indexOf(sDiffSymbol) >= 0) {
sPattern = this._getMixedFormatPattern(sSkeleton, oAvailableDateTimeFormats, sCalendarType, vDiff);
} else {
sSinglePattern = this._getFormatPattern(sSkeleton, oAvailableDateTimeFormats, sCalendarType);
sPattern = this.getCombinedIntervalPattern(sSinglePattern, sCalendarType);
}
}
aPatterns = [sPattern];
} else if (!oBestMatch) {
sPattern = sSkeleton;
aPatterns = [sPattern];
} else {
if (typeof oBestMatch.pattern === "string") {
aPatterns = [oBestMatch.pattern];
} else if (typeof oBestMatch.pattern === "object") {
aPatterns = [];
for (var sKey in oBestMatch.pattern) {
sPattern = oBestMatch.pattern[sKey];
aPatterns.push(sPattern);
}
}
// if there is no exact match, we need to do further processing
if (oBestMatch.distance > 0) {
if (oBestMatch.missingTokens.length > 0) {
// if tokens are missing create a pattern containing all, otherwise just adjust pattern
if (rMixedSkeleton.test(sSkeleton)) {
aPatterns = [this._getMixedFormatPattern(sSkeleton, oAvailableFormats, sCalendarType)];
} else {
aPatterns = this._expandFields(aPatterns, oBestMatch.patternTokens, aTokens);
aPatterns = this._appendItems(aPatterns, oBestMatch.missingTokens, sCalendarType);
}
} else {
aPatterns = this._expandFields(aPatterns, oBestMatch.patternTokens, aTokens);
}
}
}
// If special input token "J" was used, remove dayperiod from pattern
if (sSkeleton.indexOf("J") >= 0) {
aPatterns.forEach(function (sPattern, iIndex) {
aPatterns[iIndex] = sPattern.replace(/ ?[abB](?=([^']*'[^']*')*[^']*)$/g, "");
});
}
return aPatterns;
},
_parseSkeletonFormat: function (sSkeleton) {
var aTokens = [],
oToken = {
index: -1
},
sSymbol,
oSymbol,
oGroup;
for (var i = 0; i < sSkeleton.length; i++) {
sSymbol = sSkeleton.charAt(i);
// Handle special input symbols
if (sSymbol == "j" || sSymbol == "J") {
sSymbol = this.getPreferredHourSymbol();
}
// if the symbol is the same as current token, increase the length
if (sSymbol == oToken.symbol) {
oToken.length++;
continue;
}
// get symbol group
oSymbol = mCLDRSymbols[sSymbol];
oGroup = mCLDRSymbolGroups[oSymbol.group];
// if group is other, the symbol is not allowed in skeleton tokens
if (oSymbol.group == "Other" || oGroup.diffOnly) {
throw new Error("Symbol '" + sSymbol + "' is not allowed in skeleton format '" + sSkeleton + "'");
}
// if group index the same or lower, format is invalid
if (oGroup.index <= oToken.index) {
throw new Error("Symbol '" + sSymbol + "' at wrong position or duplicate in skeleton format '" + sSkeleton + "'");
}
// create token and add it the token array
oToken = {
symbol: sSymbol,
group: oSymbol.group,
match: oSymbol.match,
index: oGroup.index,
field: oGroup.field,
length: 1
};
aTokens.push(oToken);
}
return aTokens;
},
_findBestMatch: function (aTokens, sSkeleton, oAvailableFormats) {
var aTestTokens,
aMissingTokens,
oToken,
oTestToken,
iTest,
iDistance,
bMatch,
iFirstDiffPos,
oTokenSymbol,
oTestTokenSymbol,
oBestMatch = {
distance: 10000,
firstDiffPos: -1
};
// Loop through all available tokens, find matches and calculate distance
for (var sTestSkeleton in oAvailableFormats) {
// Skip patterns with symbol "B" (which is introduced from CLDR v32.0.0) which isn't supported in DateFormat yet
if (sTestSkeleton === "intervalFormatFallback" || sTestSkeleton.indexOf("B") > -1) {
continue;
}
aTestTokens = this._parseSkeletonFormat(sTestSkeleton);
iDistance = 0;
aMissingTokens = [];
bMatch = true;
// if test format contains more tokens, it cannot be a best match
if (aTokens.length < aTestTokens.length) {
continue;
}
iTest = 0;
iFirstDiffPos = aTokens.length;
for (var i = 0; i < aTokens.length; i++) {
oToken = aTokens[i];
oTestToken = aTestTokens[iTest];
if (iFirstDiffPos === aTokens.length) {
iFirstDiffPos = i;
}
if (oTestToken) {
oTokenSymbol = mCLDRSymbols[oToken.symbol];
oTestTokenSymbol = mCLDRSymbols[oTestToken.symbol];
// if the symbol matches, just add the length difference to the distance
if (oToken.symbol === oTestToken.symbol) {
if (oToken.length === oTestToken.length) {
// both symbol and length match, check the next token
// clear the first difference position
if (iFirstDiffPos === i) {
iFirstDiffPos = aTokens.length;
}
} else {
if (oToken.length < oTokenSymbol.numericCeiling ? oTestToken.length < oTestTokenSymbol.numericCeiling : oTestToken.length >= oTestTokenSymbol.numericCeiling) {
// if the symbols are in the same category (either numeric or text representation), add the length diff
iDistance += Math.abs(oToken.length - oTestToken.length);
} else {
// otherwise add 5 which is bigger than any length difference
iDistance += 5;
}
}
iTest++;
continue;
} else {
// if only the group matches, add some more distance in addition to length difference
if (oToken.match == oTestToken.match) {
iDistance += Math.abs(oToken.length - oTestToken.length) + 10;
iTest++;
continue;
}
}
}
// if neither symbol nor group matched, add it to the missing tokens and add distance
aMissingTokens.push(oToken);
iDistance += 50 - i;
}
// if not all test tokens have been found, the format does not match
if (iTest < aTestTokens.length) {
bMatch = false;
}
// The current pattern is saved as the best pattern when there is a match and
// 1. the distance is smaller than the best distance or
// 2. the distance equals the best distance and the position of the token in the given skeleton which
// isn't the same between the given skeleton and the available skeleton is bigger than the best one's.
if (bMatch && (iDistance < oBestMatch.distance || iDistance === oBestMatch.distance && iFirstDiffPos > oBestMatch.firstDiffPos)) {
oBestMatch.distance = iDistance;
oBestMatch.firstDiffPos = iFirstDiffPos;
oBestMatch.missingTokens = aMissingTokens;
oBestMatch.pattern = oAvailableFormats[sTestSkeleton];
oBestMatch.patternTokens = aTestTokens;
}
}
if (oBestMatch.pattern) {
return oBestMatch;
}
},
_expandFields: function (vPattern, aPatternTokens, aTokens) {
var bSinglePattern = typeof vPattern === "string";
var aPatterns;
if (bSinglePattern) {
aPatterns = [vPattern];
} else {
aPatterns = vPattern;
}
var aResult = aPatterns.map(function (sPattern) {
var mGroups = {},
mPatternGroups = {},
sResultPatterm = "",
bQuoted = false,
i = 0,
iSkeletonLength,
iPatternLength,
iBestLength,
iNewLength,
oSkeletonToken,
oBestToken,
oSymbol,
sChar;
// Create a map of group names to token
aTokens.forEach(function (oToken) {
mGroups[oToken.group] = oToken;
});
// Create a map of group names to token in best pattern
aPatternTokens.forEach(function (oToken) {
mPatternGroups[oToken.group] = oToken;
});
// Loop through pattern and adjust symbol length
while (i < sPattern.length) {
sChar = sPattern.charAt(i);
if (bQuoted) {
sResultPatterm += sChar;
if (sChar == "'") {
bQuoted = false;
}
} else {
oSymbol = mCLDRSymbols[sChar];
// If symbol is a CLDR symbol and is contained in the group, expand length
if (oSymbol && mGroups[oSymbol.group] && mPatternGroups[oSymbol.group]) {
oSkeletonToken = mGroups[oSymbol.group];
oBestToken = mPatternGroups[oSymbol.group];
iSkeletonLength = oSkeletonToken.length;
iBestLength = oBestToken.length;
iPatternLength = 1;
while (sPattern.charAt(i + 1) == sChar) {
i++;
iPatternLength++;
}
// Prevent expanding the length of the field when:
// 1. The length in the best matching skeleton (iBestLength) matches the length of the application provided skeleton (iSkeletonLength) or
// 2. The length of the provided skeleton (iSkeletonLength) and the length of the result pattern (iPatternLength) are not in the same category (numeric or text)
// because switching between numeric to text representation is wrong in all cases
if (iSkeletonLength === iBestLength || (iSkeletonLength < oSymbol.numericCeiling ? iPatternLength >= oSymbol.numericCeiling : iPatternLength < oSymbol.numericCeiling)) {
iNewLength = iPatternLength;
} else {
iNewLength = Math.max(iPatternLength, iSkeletonLength);
}
for (var j = 0; j < iNewLength; j++) {
sResultPatterm += sChar;
}
} else {
sResultPatterm += sChar;
if (sChar == "'") {
bQuoted = true;
}
}
}
i++;
}
return sResultPatterm;
});
return bSinglePattern ? aResult[0] : aResult;
},
_appendItems: function (aPatterns, aMissingTokens, sCalendarType) {
var oAppendItems = this._get(getCLDRCalendarName(sCalendarType), "dateTimeFormats", "appendItems");
aPatterns.forEach(function (sPattern, iIndex) {
var sDisplayName, sAppendPattern, sAppendField;
aMissingTokens.forEach(function (oToken) {
sAppendPattern = oAppendItems[oToken.group];
sDisplayName = "'" + this.getDisplayName(oToken.field) + "'";
sAppendField = "";
for (var i = 0; i < oToken.length; i++) {
sAppendField += oToken.symbol;
}
aPatterns[iIndex] = sAppendPattern.replace(/\{0\}/, sPattern).replace(/\{1\}/, sAppendField).replace(/\{2\}/, sDisplayName);
}.bind(this));
}.bind(this));
return aPatterns;
},
_getMixedFormatPattern: function (sSkeleton, oAvailableFormats, sCalendarType, vDiff) {
var rMixedSkeleton = /^([GyYqQMLwWEecdD]+)([hHkKjJmszZvVOXx]+)$/,
rWideMonth = /MMMM|LLLL/,
rAbbrevMonth = /MMM|LLL/,
rWeekDay = /E|e|c/,
oResult,
sDateSkeleton,
sTimeSkeleton,
sStyle,
sDatePattern,
sTimePattern,
sDateTimePattern,
sResultPattern;
// Split skeleton into date and time part
oResult = rMixedSkeleton.exec(sSkeleton);
sDateSkeleton = oResult[1];
sTimeSkeleton = oResult[2];
// Get patterns for date and time separately
sDatePattern = this._getFormatPattern(sDateSkeleton, oAvailableFormats, sCalendarType);
if (vDiff) {
sTimePattern = this.getCustomIntervalPattern(sTimeSkeleton, vDiff, sCalendarType);
} else {
sTimePattern = this._getFormatPattern(sTimeSkeleton, oAvailableFormats, sCalendarType);
}
// Combine patterns with datetime pattern, dependent on month and weekday
if (rWideMonth.test(sDateSkeleton)) {
sStyle = rWeekDay.test(sDateSkeleton) ? "full" : "long";
} else if (rAbbrevMonth.test(sDateSkeleton)) {
sStyle = "medium";
} else {
sStyle = "short";
}
sDateTimePattern = this.getDateTimePattern(sStyle, sCalendarType);
sResultPattern = sDateTimePattern.replace(/\{1\}/, sDatePattern).replace(/\{0\}/, sTimePattern);
return sResultPattern;
},
/**
* Get number symbol "decimal", "group", "plusSign", "minusSign", "percentSign".
*
* @param {string} sType the required type of symbol
* @returns {string} the selected number symbol
* @public
*/
getNumberSymbol: function (sType) {
assert(sType == "decimal" || sType == "group" || sType == "plusSign" || sType == "minusSign" || sType == "percentSign", "sType must be decimal, group, plusSign, minusSign or percentSign");
return this._get("symbols-latn-" + sType);
},
/**
* Get lenient number symbols for "plusSign" or "minusSign".
*
* @param {string} sType the required type of symbol
* @returns {string} the selected lenient number symbols, e.g. "-‒⁻₋−➖﹣"
* @public
*/
getLenientNumberSymbols: function (sType) {
assert(sType == "plusSign" || sType == "minusSign", "sType must be plusSign or minusSign");
return this._get("lenient-scope-number")[sType];
},
/**
* Get decimal format pattern.
*
* @returns {string} The pattern
* @public
*/
getDecimalPattern: function () {
return this._get("decimalFormat").standard;
},
/**
* Get currency format pattern.
*
* CLDR format pattern:
*
* @example standard with currency symbol in front of the number
* ¤#,##0.00
* $100,000.00
* $-100,000.00
*
* @example accounting with negative number pattern after the semicolon
* ¤#,##0.00;(¤#,##0.00)
* $100,000.00
* ($100,000.00)
*
* @see https://cldr.unicode.org/translation/numbers-currency/number-patterns
*
* @param {string} sContext the context of the currency pattern (standard or accounting)
* @returns {string} The pattern
* @public
*/
getCurrencyPattern: function (sContext) {
// Undocumented contexts for NumberFormat internal use: "sap-standard" and "sap-accounting"
return this._get("currencyFormat")[sContext] || this._get("currencyFormat").standard;
},
getCurrencySpacing: function (sPosition) {
return this._get("currencyFormat", "currencySpacing", sPosition === "after" ? "afterCurrency" : "beforeCurrency");
},
/**
* Get percent format pattern.
*
* @returns {string} The pattern
* @public
*/
getPercentPattern: f