/**
* Copyright IBM Corp. 2020, 2024
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import axios from 'axios';
import { DDOAPI } from '../DDO';
import ipcinfoCookie from '../../internal/vendor/@carbon/ibmdotcom-utilities/utilities/ipcinfoCookie/ipcinfoCookie';
import root from 'window-or-global';
/**
* @typedef {object} Locale
* @property {string} cc The country code.
* @property {string} lc The language code.
*/
/**
* @constant {string | string} Host for the Locale API call.
* @private
*/
const _host =
(process &&
(process.env.REACT_APP_TRANSLATION_HOST || process.env.TRANSLATION_HOST)) ||
'https://1.www.s81c.com';
/**
* Sets the default location if nothing is returned.
*
* @type {Locale}
* @private
*/
const _localeDefault = {
lc: 'en',
cc: 'us',
};
/**
* Default display name for lang combination.
*
* @type {string}
* @private
*/
const _localeNameDefault = 'United States — English';
/**
* Locale API endpoint.
*
* @type {string}
* @private
*/
const _endpoint = `${_host}/common/js/dynamicnav/www/countrylist/jsononly`;
/**
* Configuration for axios.
*
* @type {{headers: {'Content-Type': string}}}
* @private
*/
const _axiosConfig = {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
};
/**
* Session Storage key for country list.
*
* @type {string}
* @private
*/
const _sessionListKey = 'cds-countrylist';
/**
* Two hours in milliseconds to compare session timestamp.
*
* @type {number}
* @private
*/
const _twoHours = 60 * 60 * 2000;
/**
* The cache for in-flight or resolved requests for the country list, keyed by
* the initiating locale.
*
* @type {object}
* @private
*/
const _requestsList = {};
/**
* Retrieves the default locale.
*
* @returns {Locale} The default locale.
*/
const _getLocaleDefault = () => _localeDefault;
/**
* Use the <html> lang attr to determine a return locale object, or "false"
* when it's not available so the consumer can decide what to do next.
*
* @type {(Locale | boolean)}
* @private
*/
function _getLocaleFromLangAttr() {
if (root.document?.documentElement?.lang) {
const lang = root.document.documentElement.lang.toLowerCase();
const locale = {};
if (lang.indexOf('-') === -1) {
locale.lc = lang;
} else {
const codes = lang.split('-');
locale.cc = codes[1];
locale.lc = codes[0];
}
return locale;
}
return false;
}
/**
* Gets the locale from the cookie and returns it if both 'cc' and 'lc' values
* are present.
*
* @async
* @returns {Promise<Locale|boolean>} The cookie object if 'cc' and 'lc' values are present, otherwise false.
*/
const _getLocaleFromCookie = async () => {
const cookie = ipcinfoCookie.get();
if (cookie && cookie.cc && cookie.lc) {
await LocaleAPI.getList(cookie);
return cookie;
}
return false;
};
/**
* Get the locale from the user's browser.
*
* @async
* @returns {Promise<Locale|boolean>} The verified locale or false if not found.
*/
const _getLocaleFromBrowser = async () => {
try {
const cc = await DDOAPI.getLocation();
// Language preference from browser can return in either 'en-US' format or
// 'en' so will need to extract language only.
const lang = root.navigator.language;
const lc = lang.split('-')[0];
if (cc && lc) {
const list = await LocaleAPI.getList({ cc, lc });
const verifiedCodes = LocaleAPI.verifyLocale(cc, lc, list);
// Set the ipcInfo cookie.
ipcinfoCookie.set(verifiedCodes);
return verifiedCodes;
}
} catch (e) {
// Intentionally throw away the exception in favor of returning false.
}
return false;
};
/**
* Return a locale object based on the DDO API, or "false" so the consumer can
* decide what to do next.
*
* @returns {(Locale | boolean)} Locale from the DDO, or "false" if not present.
* @private
*/
function _getLocaleFromDDO() {
const ddoLocal = Object.assign({}, root.digitalData || {});
if (ddoLocal.page?.pageInfo?.language) {
const lang = {};
// if DDO language contains both lc & cc (ie. en-US)
if (
ddoLocal.page?.pageInfo?.language.includes('-') &&
ddoLocal.page?.pageInfo?.ibm?.country
) {
// Set proper LC for us to use.
lang.lc = ddoLocal.page.pageInfo.language.substring(0, 2).toLowerCase();
lang.cc = ddoLocal.page.pageInfo.ibm.country.toLowerCase().trim();
// If there are multiple countries use just the first one for the CC value
if (lang.cc.indexOf(',') > -1) {
lang.cc = lang.cc.substring(0, lang.cc.indexOf(',')).trim();
}
// Gb will be uk elsewhere
if (lang.cc === 'gb') {
lang.cc = 'uk';
}
// Map worldwide (ZZ) pages to US
if (lang.cc === 'zz') {
lang.cc = 'us';
}
} else {
// set lc with just the language code
lang.lc = ddoLocal.page.pageInfo.language.substring(0, 2).toLowerCase();
}
return lang;
}
return false;
}
/**
* Locale API class with method of fetching user's locale for ibm.com.
*/
class LocaleAPI {
/**
* Clears the cache.
*/
static clearCache() {
if (typeof sessionStorage !== 'undefined') {
Object.keys(_requestsList).forEach((key) => delete _requestsList[key]);
for (let i = 0; i < sessionStorage.length; ++i) {
const key = sessionStorage.key(i);
if (key.indexOf(_sessionListKey) === 0) {
sessionStorage.removeItem(key);
}
}
}
}
/**
* Gets the user's locale.
*
* Grab the locale from the available information on the page in the following
* order:
*
* 1. DDO
* 2. HTML lang attribute
* 3. ipcInfo cookie
* 4. Browser (navigator.language)
* 5. Default (us-EN)
*
* @returns {Promise<Locale>} Locale object.
* @example
* import { LocaleAPI } from '@carbon/ibmdotcom-services';
*
* async function getLocale() {
* const locale = await LocaleAPI.getLocale();
* return locale;
* }
*/
static async getLocale() {
const localeGetters = [
_getLocaleFromDDO,
_getLocaleFromLangAttr,
_getLocaleFromCookie,
_getLocaleFromBrowser,
];
for (const getter of localeGetters) {
const locale = await getter();
if (locale) {
return locale;
}
}
return _getLocaleDefault();
}
/**
* Gets the user's locale.
*
* @returns {Promise<Locale>} Locale object.
* @example
* import { LocaleAPI } from '@carbon/ibmdotcom-services';
*
* function async getLocale() {
* const locale = await LocaleAPI.getLang();
* }
*
* @deprecated in favor of LocalAPI.getLocale.
*/
static async getLang() {
return this.getLocale();
}
/**
* This fetches the language display name based on locale.
*
* @param {(Locale | boolean)} locale (optional) If not given, uses LocaleAPI.getLocale logic.
* @returns {Promise<string>} Display name of locale/language.
*/
static async getLangDisplay(locale) {
const lang = locale ? locale : await this.getLocale();
const list = await this.getList(lang);
// combines the countryList arrays
let countries = [];
list.regionList.forEach((region) => {
countries = countries.concat(region.countryList);
});
// get match for countries with multiple languages
const location = countries.filter((country) => {
let htmlLang = country.locale.findIndex(
(loc) => loc[0] === `${lang.lc}-${lang.cc}`
);
if (htmlLang !== -1) {
let localeMatch = country.locale.filter((l) =>
l.includes(`${lang.lc}-${lang.cc}`)
);
country.locale.splice(0, country.locale.length, ...localeMatch);
return country;
}
});
if (location.length) {
return `${location[0].name} — ${location[0].locale[0][1]}`;
} else {
return _localeNameDefault;
}
}
/**
* Get the country list of all supported countries and their languages
* if it is not already stored in session storage.
*
* @param {Locale} locale Locale object.
* @param {string} locale.cc Country code.
* @param {string} locale.lc Language code.
* @returns {Promise<any>} Promise object.
* @example
* import { LocaleAPI } from '@carbon/ibmdotcom-services';
*
* function async getLocale() {
* const list = await LocaleAPI.getList({ cc: 'us', lc: 'en' });
* return list;
* }
*/
static async getList({ cc, lc }) {
return new Promise((resolve, reject) => {
this.fetchList(cc, lc, resolve, reject);
});
}
/**
* Fetches the list data based on cc/lc combination.
*
* @param {string} cc Country code.
* @param {string} lc Language code.
* @param {Function} resolve Resolves the Promise.
* @param {Function} reject Rejects the promise.
*/
static fetchList(cc, lc, resolve, reject) {
const key = cc !== 'undefined' ? `${lc}-${cc}` : `${lc}`;
const itemKey = `${_sessionListKey}-${key}`;
const sessionList = this.getSessionCache(itemKey);
if (sessionList) {
resolve(sessionList);
} else {
if (!_requestsList[key]) {
const url = `${_endpoint}/${
cc !== 'undefined' ? `${cc}${lc}` : `${lc}`
}-utf8.json`;
_requestsList[key] = axios.get(url, _axiosConfig).then((response) => {
const { data } = response;
data['timestamp'] = Date.now();
sessionStorage.setItem(
`${_sessionListKey}-${key}`,
JSON.stringify(data)
);
return data;
});
}
_requestsList[key].then(resolve, (error) => {
if (cc === _localeDefault.cc && lc === _localeDefault.lc) {
reject(error);
} else {
this.fetchList(_localeDefault.cc, _localeDefault.lc, resolve, reject);
}
});
}
}
/**
* Verify that the cc and lc combo is in the list of supported cc-lc combos.
*
* @param {string} cc Country code.
* @param {string} lc Language code.
* @param {object} list Country list.
* @returns {object} Object with lc and cc.
* @example
* import { LocaleAPI } from '@carbon/ibmdotcom-services';
*
* async function getLocale() {
* const locale = await LocaleAPI.verifyLocale(cc, lc, data);
* return locale;
* }
*/
static verifyLocale(cc, lc, list) {
let priorityLC;
let locale;
const language =
list &&
list.regionList.forEach((region) =>
region.countryList.forEach((country) => {
const code = country.locale[0][0].split('-');
const languageCode = code[0];
const countryCode = code[1];
if (countryCode === cc && languageCode === lc) {
locale = { cc, lc };
}
// save the priority language associated with the user's country code
else if (countryCode === cc && !priorityLC) {
priorityLC = languageCode;
}
})
);
if (!language && priorityLC) {
locale = { cc, lc: priorityLC };
}
return locale;
}
/**
* Retrieves session cache and checks if cache needs to be refreshed
*
* @param {string} key Session storage key.
*/
static getSessionCache(key) {
const session =
typeof sessionStorage === 'undefined'
? undefined
: JSON.parse(sessionStorage.getItem(key));
if (!session || !session.timestamp) {
return;
}
const currentTime = Date.now(),
timeDiff = currentTime - session.timestamp;
if (timeDiff > _twoHours) {
sessionStorage.removeItem(key);
return;
}
return session;
}
}
export default LocaleAPI;