@okta-dfuhriman/okta-auth-js
Version:
The Okta Auth SDK
238 lines (208 loc) • 7.49 kB
text/typescript
/*!
* Copyright (c) 2015-2016, Okta, Inc. and/or its affiliates. All rights reserved.
* The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
*
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and limitations under the License.
*/
import config from 'config/config.json';
import fetch from 'cross-fetch';
import country from 'nls/country.json';
import login from 'nls/login.json';
import Q from 'q';
import { _ } from 'okta';
import BrowserFeatures from 'util/BrowserFeatures';
import Logger from 'util/Logger';
const STORAGE_KEY = 'osw.languages';
declare type i18nOptions = {
[localeOrCountry: string]: {
[i18nKey: string]: string;
}
}
declare type Assets = {
baseUrl: string;
rewrite(origPath: string): string;
}
/**
* Converts options to our internal format, which distinguishes between
* login and country bundles.
*
* Example options.i18n passed in by the developer:
* {
* 'en': {
* 'needhelp': 'need help override',
* 'primaryauth.title': 'new sign in text',
* 'country.JP' = 'Japan, Japan'
* }
* }
*
* Parsed:
* {
* 'en': {
* 'login': {
* 'needhelp': 'need help override',
* 'primaryauth.title': 'new sign in text',
* },
* 'country': {
* 'JP': 'Japan, Japan'
* }
* }
* }
*/
function parseOverrides(i18n) {
if (!i18n) {
return {};
}
const i18nWithLowerCaseKeys = {};
_.each(_.keys(i18n), function(key) {
i18nWithLowerCaseKeys[key.toLowerCase()] = i18n[key];
});
return _.mapObject(i18nWithLowerCaseKeys, function(props) {
const mapped = { login: {}, country: {} };
if (!_.isObject(props)) {
throw new Error('Invalid format for "i18n"');
}
_.each(props, function(val, key) {
const split = key.split(/^country\./);
if (split.length > 1) {
mapped.country[split[1]] = val;
} else {
mapped.login[split[0]] = val;
}
});
return mapped;
});
}
// Caching: We only bundle English by default in the Sign-In Widget. Other
// languages are loaded on demand and cached in localStorage. These languages
// are tied to the version of the widget - when it bumps, we reset the cache.
function getCachedLanguages() {
let storage = JSON.parse(localStorage.getItem(STORAGE_KEY));
if (!storage || storage.version !== config.version) {
storage = {
version: config.version,
};
}
return storage;
}
function addLanguageToCache(language, loginJson, countryJson) {
const current = getCachedLanguages();
current[language] = {
login: loginJson,
country: countryJson,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(current));
}
// By default, the assets.bundleUrl is tied to the Okta CDN.
//
// There are two overrides available for modifying where we load the asset
// bundles from:
//
// 1. assets.baseUrl
//
// This is the base path the OSW pulls assets from, which in this case is
// the Okta CDN. Override this config option if you want to host the
// files on your own domain, or if you're using a new version of the
// widget whose language files haven't been published to the CDN yet.
//
// 2. assets.rewrite
//
// This is a function that can be used to modify the path + fileName of
// the bundle we're loading, relative to the baseUrl. When called, it
// will pass the current path, and expect the new path to be returned.
// This is useful, for example, if your build process has an extra
// cachebusting step, i.e:
//
// function rewrite(file) {
// // file: /labels/json/login_ja.json
// return file.replace('.json', '.' + md5file(file) + '.json');
// }
//
// Note: Most developers will not need to use these overrides - the default
// is to use the Okta CDN and to use the same path + file structure the
// widget module publishes by default.
function fetchJson(bundle, language, assets) {
// Our bundles use _ to separate country and region, i.e:
// zh-CN -> zh_CN
const languageCode = language.replace('-', '_');
const path = assets.rewrite(encodeURI(`/labels/json/${bundle}_${languageCode}.json`));
const headers = {
Accept: 'application/json',
'Content-Type': 'text/plain',
};
const mode = 'no-cors';
return fetch(assets.baseUrl + path, {
method: 'GET',
headers,
mode,
})
.then(res => res.text())
.then(txt => JSON.parse(txt));
}
function getBundles(language, assets, supportedLanguages) {
// Two special cases:
// 1. Default language is already bundled with the widget
// 2. If the language is not in our config file, it means that they've
// probably defined it on their own.
if (language === config.defaultLanguage || !_.contains(supportedLanguages, language)) {
return Q({});
}
//local storage does not work correctly with android web views (embeded browers)
//so skip the caching and just make the request to get the local info
const localStorageIsSupported = !BrowserFeatures.localStorageIsNotSupported();
if (localStorageIsSupported) {
const cached = getCachedLanguages();
if (cached[language]) {
return Q(cached[language]);
}
}
return Q.all([fetchJson('login', language, assets), fetchJson('country', language, assets)])
.spread(function(loginJson, countryJson) {
if (localStorageIsSupported) {
addLanguageToCache(language, loginJson, countryJson);
}
return { login: loginJson, country: countryJson };
})
.catch(function() {
// If there is an error, this will default to the bundled language and
// we will no longer try to load the language this session.
Logger.warn('Unable to load language: ' + language);
return {};
});
}
export default {
login: login,
country: country,
// Courage components within the sign in widget point to courage bundle to look
// up i18nkeys. Since we dont have courage.properties inside the sign in widget
// we are pointing courage bundle to login.
courage: login,
currentLanguage: null,
isLoaded: function(language: string): boolean {
return this.currentLanguage === language;
},
remove: function(): void {
this.currentLanguage = null;
},
loadLanguage: function(language: string, overrides: i18nOptions, assets: Assets, supportedLanguages: string[]): Q.Promise<void> {
const parsedOverrides = parseOverrides(overrides);
const lowerCaseLanguage = language.toLowerCase();
return getBundles(language, assets, supportedLanguages).then(bundles => {
// Always extend from the built in defaults in the event that some
// properties are not translated
this.login = _.extend({}, login, bundles.login);
this.country = _.extend({}, country, bundles.country);
this.courage = _.extend({}, login, bundles.login);
if (parsedOverrides[lowerCaseLanguage]) {
_.extend(this.login, parsedOverrides[lowerCaseLanguage]['login']);
_.extend(this.country, parsedOverrides[lowerCaseLanguage]['country']);
_.extend(this.courage, parsedOverrides[lowerCaseLanguage]['login']);
}
this.currentLanguage = language;
});
},
};