From 297904462d9ff9b81f2e9017895d299447d1ee8d Mon Sep 17 00:00:00 2001 From: gconsidine Date: Wed, 27 Sep 2017 15:04:09 -0400 Subject: [PATCH 1/2] Implement manual initialzation for Angular app Manual initialization allows for some asynchronous work to finish ahead of Angular's startup. The initial motivation is to be able to guarantee translation files have been fetched before rendering content that needs translation. If a locale isn't supported or if the request to get a json file fails, the i18n service falls back to en. Signed-off-by: gconsidine --- awx/ui/.eslintignore | 1 + awx/ui/.eslintrc.js | 3 +- awx/ui/build/webpack.base.js | 2 +- awx/ui/build/webpack.development.js | 2 - awx/ui/build/webpack.test.js | 5 +- awx/ui/build/webpack.watch.js | 4 +- awx/ui/client/features/credentials/index.js | 42 +++--- awx/ui/client/features/index.js | 17 ++- awx/ui/client/index.template.ejs | 2 +- awx/ui/client/installing.template.ejs | 2 +- awx/ui/client/lib/components/index.js | 10 +- awx/ui/client/lib/models/index.js | 10 +- awx/ui/client/lib/services/index.js | 12 +- awx/ui/client/src/app.js | 138 ++++++++++---------- awx/ui/client/src/app.start.js | 72 ++++++++++ awx/ui/client/src/i18n.js | 76 +++++------ awx/ui/karma.conf.js | 14 +- 17 files changed, 253 insertions(+), 159 deletions(-) create mode 100644 awx/ui/client/src/app.start.js diff --git a/awx/ui/.eslintignore b/awx/ui/.eslintignore index 1de8506d67..f290f33893 100644 --- a/awx/ui/.eslintignore +++ b/awx/ui/.eslintignore @@ -17,3 +17,4 @@ test !client/lib/models/**/*.js !client/lib/services/**/*.js !client/features/**/*.js +!client/src/app.start.js diff --git a/awx/ui/.eslintrc.js b/awx/ui/.eslintrc.js index 5cb4504be4..c624bf962a 100644 --- a/awx/ui/.eslintrc.js +++ b/awx/ui/.eslintrc.js @@ -20,7 +20,7 @@ module.exports = { node: true }, globals: { - angular: true, + angular: true, d3: true, $: true, _: true, @@ -43,6 +43,7 @@ module.exports = { 'no-param-reassign': 'off', 'no-plusplus': 'off', 'no-underscore-dangle': 'off', + 'no-use-before-define': 'off', 'object-curly-newline': 'off', 'space-before-function-paren': ['error', 'always'] } diff --git a/awx/ui/build/webpack.base.js b/awx/ui/build/webpack.base.js index e0b6998974..ae4c3a856d 100644 --- a/awx/ui/build/webpack.base.js +++ b/awx/ui/build/webpack.base.js @@ -109,7 +109,7 @@ const base = { jsonlint: 'codemirror.jsonlint' }), new ExtractTextPlugin('css/[name].[chunkhash].css'), - new CleanWebpackPlugin([STATIC_PATH, COVERAGE_PATH, LANGUAGES_PATH], { + new CleanWebpackPlugin([STATIC_PATH, COVERAGE_PATH], { root: UI_PATH, verbose: false }), diff --git a/awx/ui/build/webpack.development.js b/awx/ui/build/webpack.development.js index 5e936c0a7e..a9b2db20d8 100644 --- a/awx/ui/build/webpack.development.js +++ b/awx/ui/build/webpack.development.js @@ -1,5 +1,3 @@ -const path = require('path'); - const _ = require('lodash'); const base = require('./webpack.base'); diff --git a/awx/ui/build/webpack.test.js b/awx/ui/build/webpack.test.js index 0ebcac286b..8fb4e4e1c7 100644 --- a/awx/ui/build/webpack.test.js +++ b/awx/ui/build/webpack.test.js @@ -1,13 +1,12 @@ -const path = require('path'); - const _ = require('lodash'); const webpack = require('webpack'); const STATIC_URL = '/static/'; -const development = require('./webpack.development'); +const development = require('./webpack.base'); const test = { + devtool: 'cheap-source-map', plugins: [ new webpack.DefinePlugin({ $basePath: STATIC_URL diff --git a/awx/ui/build/webpack.watch.js b/awx/ui/build/webpack.watch.js index 1c2f9e5270..5030bdf4d4 100644 --- a/awx/ui/build/webpack.watch.js +++ b/awx/ui/build/webpack.watch.js @@ -2,8 +2,8 @@ const path = require('path'); const _ = require('lodash'); const webpack = require('webpack'); -const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); +const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); const TARGET_PORT = _.get(process.env, 'npm_package_config_django_port', 8043); const TARGET_HOST = _.get(process.env, 'npm_package_config_django_host', 'https://localhost'); @@ -29,6 +29,7 @@ const watch = { ] }, plugins: [ + new HtmlWebpackHarddiskPlugin(), new HardSourceWebpackPlugin({ cacheDirectory: 'node_modules/.cache/hard-source/[confighash]', recordsPath: 'node_modules/.cache/hard-source/[confighash]/records.json', @@ -41,7 +42,6 @@ const watch = { files: ['package.json'] } }), - new HtmlWebpackHarddiskPlugin(), new webpack.HotModuleReplacementPlugin() ], devServer: { diff --git a/awx/ui/client/features/credentials/index.js b/awx/ui/client/features/credentials/index.js index 5800793287..5a20d290aa 100644 --- a/awx/ui/client/features/credentials/index.js +++ b/awx/ui/client/features/credentials/index.js @@ -3,6 +3,8 @@ import AddController from './add-credentials.controller'; import EditController from './edit-credentials.controller'; import CredentialsStrings from './credentials.strings'; +const MODULE_NAME = 'at.features.credentials'; + const addEditTemplate = require('~features/credentials/add-edit-credentials.view.html'); function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) { @@ -51,12 +53,8 @@ CredentialsResolve.$inject = [ 'OrganizationModel' ]; -function CredentialsConfig ($stateExtenderProvider, legacyProvider, stringProvider) { - const stateExtender = $stateExtenderProvider.$get(); - const legacy = legacyProvider.$get(); - const strings = stringProvider.$get(); - - stateExtender.addState({ +function CredentialsRun ($stateExtender, legacy, strings) { + $stateExtender.addState({ name: 'credentials.add', route: '/add', ncyBreadcrumb: { @@ -78,7 +76,7 @@ function CredentialsConfig ($stateExtenderProvider, legacyProvider, stringProvid } }); - stateExtender.addState({ + $stateExtender.addState({ name: 'credentials.edit', route: '/:credential_id', ncyBreadcrumb: { @@ -101,25 +99,27 @@ function CredentialsConfig ($stateExtenderProvider, legacyProvider, stringProvid } }); - stateExtender.addState(legacy.getStateConfiguration('list')); - stateExtender.addState(legacy.getStateConfiguration('edit-permissions')); - stateExtender.addState(legacy.getStateConfiguration('add-permissions')); - stateExtender.addState(legacy.getStateConfiguration('add-organization')); - stateExtender.addState(legacy.getStateConfiguration('edit-organization')); - stateExtender.addState(legacy.getStateConfiguration('add-credential-type')); - stateExtender.addState(legacy.getStateConfiguration('edit-credential-type')); + $stateExtender.addState(legacy.getStateConfiguration('list')); + $stateExtender.addState(legacy.getStateConfiguration('edit-permissions')); + $stateExtender.addState(legacy.getStateConfiguration('add-permissions')); + $stateExtender.addState(legacy.getStateConfiguration('add-organization')); + $stateExtender.addState(legacy.getStateConfiguration('edit-organization')); + $stateExtender.addState(legacy.getStateConfiguration('add-credential-type')); + $stateExtender.addState(legacy.getStateConfiguration('edit-credential-type')); } -CredentialsConfig.$inject = [ - '$stateExtenderProvider', - 'LegacyCredentialsServiceProvider', - 'CredentialsStringsProvider' +CredentialsRun.$inject = [ + '$stateExtender', + 'LegacyCredentialsService', + 'CredentialsStrings' ]; angular - .module('at.features.credentials', []) - .config(CredentialsConfig) + .module(MODULE_NAME, []) .controller('AddController', AddController) .controller('EditController', EditController) .service('LegacyCredentialsService', LegacyCredentials) - .service('CredentialsStrings', CredentialsStrings); + .service('CredentialsStrings', CredentialsStrings) + .run(CredentialsRun); + +export default MODULE_NAME; diff --git a/awx/ui/client/features/index.js b/awx/ui/client/features/index.js index f5756de731..60598ed192 100644 --- a/awx/ui/client/features/index.js +++ b/awx/ui/client/features/index.js @@ -1,5 +1,16 @@ -import '~features/credentials'; +import atLibServices from '~services'; +import atLibComponents from '~components'; +import atLibModels from '~models'; -angular.module('at.features', [ - 'at.features.credentials' +import atFeaturesCredentials from '~features/credentials'; + +const MODULE_NAME = 'at.features'; + +angular.module(MODULE_NAME, [ + atLibServices, + atLibComponents, + atLibModels, + atFeaturesCredentials ]); + +export default MODULE_NAME; diff --git a/awx/ui/client/index.template.ejs b/awx/ui/client/index.template.ejs index 7d7973088e..4e47d3c8cb 100644 --- a/awx/ui/client/index.template.ejs +++ b/awx/ui/client/index.template.ejs @@ -1,5 +1,5 @@ - + diff --git a/awx/ui/client/installing.template.ejs b/awx/ui/client/installing.template.ejs index f9005def42..42aa1ffc07 100644 --- a/awx/ui/client/installing.template.ejs +++ b/awx/ui/client/installing.template.ejs @@ -1,5 +1,5 @@ - + {% load staticfiles %} diff --git a/awx/ui/client/lib/components/index.js b/awx/ui/client/lib/components/index.js index 9d6a2a1ac2..426b9244cd 100644 --- a/awx/ui/client/lib/components/index.js +++ b/awx/ui/client/lib/components/index.js @@ -1,3 +1,5 @@ +import atLibServices from '~services'; + import actionGroup from '~components/action/action-group.directive'; import divider from '~components/utility/divider.directive'; import form from '~components/form/form.directive'; @@ -28,8 +30,12 @@ import truncate from '~components/truncate/truncate.directive'; import BaseInputController from '~components/input/base.controller'; import ComponentsStrings from '~components/components.strings'; +const MODULE_NAME = 'at.lib.components'; + angular - .module('at.lib.components', []) + .module(MODULE_NAME, [ + atLibServices + ]) .directive('atActionGroup', actionGroup) .directive('atDivider', divider) .directive('atForm', form) @@ -58,3 +64,5 @@ angular .directive('atTruncate', truncate) .service('BaseInputController', BaseInputController) .service('ComponentsStrings', ComponentsStrings); + +export default MODULE_NAME; diff --git a/awx/ui/client/lib/models/index.js b/awx/ui/client/lib/models/index.js index e56782441a..1213463e57 100644 --- a/awx/ui/client/lib/models/index.js +++ b/awx/ui/client/lib/models/index.js @@ -1,3 +1,5 @@ +import atLibServices from '~services'; + import Base from '~models/Base'; import Config from '~models/Config'; import Credential from '~models/Credential'; @@ -5,11 +7,17 @@ import CredentialType from '~models/CredentialType'; import Me from '~models/Me'; import Organization from '~models/Organization'; +const MODULE_NAME = 'at.lib.models'; + angular - .module('at.lib.models', []) + .module(MODULE_NAME, [ + atLibServices + ]) .service('BaseModel', Base) .service('ConfigModel', Config) .service('CredentialModel', Credential) .service('CredentialTypeModel', CredentialType) .service('MeModel', Me) .service('OrganizationModel', Organization); + +export default MODULE_NAME; diff --git a/awx/ui/client/lib/services/index.js b/awx/ui/client/lib/services/index.js index 89b5f1d73b..39ed1d8299 100644 --- a/awx/ui/client/lib/services/index.js +++ b/awx/ui/client/lib/services/index.js @@ -1,11 +1,17 @@ +import AppStrings from '~services/app.strings'; +import BaseStringService from '~services/base-string.service'; import CacheService from '~services/cache.service'; import EventService from '~services/event.service'; -import BaseStringService from '~services/base-string.service'; -import AppStrings from '~services/app.strings'; + +const MODULE_NAME = 'at.lib.services'; angular - .module('at.lib.services', []) + .module(MODULE_NAME, [ + 'I18N' + ]) .service('AppStrings', AppStrings) .service('BaseStringService', BaseStringService) .service('CacheService', CacheService) .service('EventService', EventService); + +export default MODULE_NAME; diff --git a/awx/ui/client/src/app.js b/awx/ui/client/src/app.js index fb5e4a1f75..1a068de835 100644 --- a/awx/ui/client/src/app.js +++ b/awx/ui/client/src/app.js @@ -1,7 +1,7 @@ // Configuration dependencies global.$AnsibleConfig = null; // Provided via Webpack DefinePlugin in webpack.config.js -global.$ENV = {} ; +global.$ENV = {}; // ui-router debugging if ($ENV['route-debug']){ let trace = angular.module('ui.router').trace; @@ -14,7 +14,8 @@ if ($basePath) { urlPrefix = `${$basePath}`; } -// Modules +import start from './app.start'; + import portalMode from './portal-mode/main'; import systemTracking from './system-tracking/main'; import inventoriesHosts from './inventories-hosts/main'; @@ -46,71 +47,76 @@ import access from './access/main'; import scheduler from './scheduler/main'; import instanceGroups from './instance-groups/main'; -import '../lib/components'; -import '../lib/models'; -import '../lib/services'; -import '../features'; +import atFeatures from '~features'; +import atLibComponents from '~components'; +import atLibModels from '~models'; +import atLibServices from '~services'; -angular.module('awApp', [ - 'I18N', - 'AngularCodeMirrorModule', - 'angular-duration-format', - 'angularMoment', - 'AngularScheduler', - 'angular-md5', - 'dndLists', - 'ncy-angular-breadcrumb', - 'ngSanitize', - 'ngCookies', - 'ngToast', - 'gettext', - 'Timezones', - 'ui.router', - 'ui.router.state.events', - 'lrInfiniteScroll', +start.bootstrap(() => { + angular.bootstrap(document.body, ['awApp']); +}); - about.name, - access.name, - license.name, - RestServices.name, - browserData.name, - configuration.name, - systemTracking.name, - inventoriesHosts.name, - inventoryScripts.name, - credentials.name, - credentialTypes.name, - organizations.name, - managementJobs.name, - breadCrumb.name, - home.name, - login.name, - activityStream.name, - workflowResults.name, - jobResults.name, - jobSubmission.name, - notifications.name, - standardOut.name, - Templates.name, - portalMode.name, - jobs.name, - teams.name, - users.name, - projects.name, - scheduler.name, - instanceGroups.name, +angular + .module('awApp', [ + 'I18N', + 'AngularCodeMirrorModule', + 'angular-duration-format', + 'angularMoment', + 'AngularScheduler', + 'angular-md5', + 'dndLists', + 'ncy-angular-breadcrumb', + 'ngSanitize', + 'ngCookies', + 'ngToast', + 'gettext', + 'Timezones', + 'ui.router', + 'ui.router.state.events', + 'lrInfiniteScroll', - 'Utilities', - 'templates', - 'PromptDialog', - 'AWDirectives', - 'features', + about.name, + access.name, + license.name, + RestServices.name, + browserData.name, + configuration.name, + systemTracking.name, + inventoriesHosts.name, + inventoryScripts.name, + credentials.name, + credentialTypes.name, + organizations.name, + managementJobs.name, + breadCrumb.name, + home.name, + login.name, + activityStream.name, + workflowResults.name, + jobResults.name, + jobSubmission.name, + notifications.name, + standardOut.name, + Templates.name, + portalMode.name, + jobs.name, + teams.name, + users.name, + projects.name, + scheduler.name, + instanceGroups.name, - 'at.lib.components', - 'at.lib.models', - 'at.lib.services', - 'at.features', -]) + 'Utilities', + 'templates', + 'PromptDialog', + 'AWDirectives', + 'features', + + atFeatures, + atLibComponents, + atLibModels, + atLibServices + ]) .constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/') .constant('AngularScheduler.useTimezone', true) .constant('AngularScheduler.showUTCField', true) @@ -171,13 +177,13 @@ angular.module('awApp', [ 'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer', 'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest', 'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService', - 'FeaturesService', '$filter', 'SocketService', 'AppStrings', 'I18NInit', + 'FeaturesService', '$filter', 'SocketService', 'AppStrings', function($stateExtender, $q, $compile, $cookies, $rootScope, $log, $stateParams, CheckLicense, $location, Authorization, LoadBasePaths, Timer, LoadConfig, Store, pendoService, Prompt, Rest, Wait, ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService, - $filter, SocketService, AppStrings, I18NInit) { - I18NInit(); + $filter, SocketService, AppStrings) { + $rootScope.$state = $state; $rootScope.$state.matches = function(stateName) { return $state.current.name.search(stateName) > 0; diff --git a/awx/ui/client/src/app.start.js b/awx/ui/client/src/app.start.js new file mode 100644 index 0000000000..f715f14082 --- /dev/null +++ b/awx/ui/client/src/app.start.js @@ -0,0 +1,72 @@ +const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'ja', 'nl']; +const DEFAULT_LOCALE = 'en'; +const BASE_PATH = global.$basePath ? `${global.$basePath}languages/` : '/static/languages/'; + +/** + * The Angular app is manually initialized in order to complete some + * asynchronous work up front. This function returns a callback so app.js can + * call `angular.bootstrap` when the work is complete. + * + * @argument {function} - Callback. + */ +function bootstrap (callback) { + fetchLocaleStrings((locale) => { + if (locale) { + angular.module('I18N').constant('LOCALE', locale); + } + + angular.element(document).ready(() => callback()); + }); +} + +/** + * GET the localized JSON strings file or fall back to the default language + * if the locale isn't supported or if the request fails. + * + * @argument {function} - Callback. + * + * @returns {object=} - Locale data if it exists. + */ +function fetchLocaleStrings (callback) { + const code = normalizeLocaleCode(navigator.language || navigator.userLanguage); + + if (isDefaultLocale(code) || !isSupportedLocale(code)) { + callback({ code }); + + return; + } + + const request = $.ajax(`${BASE_PATH}${code}.json`); + + request.done(res => { + if (res[code]) { + callback({ code, strings: res[code] }); + } else { + callback({ code: DEFAULT_LOCALE }); + } + }); + + request.fail(() => callback({ code: DEFAULT_LOCALE })); +} + +function normalizeLocaleCode (code) { + try { + code = code.split('-')[0].toLowerCase(); + } catch (error) { + code = DEFAULT_LOCALE; + } + + return code; +} + +function isSupportedLocale (code) { + return SUPPORTED_LOCALES.includes(code); +} + +function isDefaultLocale (code) { + return code === DEFAULT_LOCALE; +} + +export default { + bootstrap +}; diff --git a/awx/ui/client/src/i18n.js b/awx/ui/client/src/i18n.js index 685c7e77df..04ee4ebcf7 100644 --- a/awx/ui/client/src/i18n.js +++ b/awx/ui/client/src/i18n.js @@ -1,52 +1,36 @@ -/* jshint ignore:start */ +import { sprintf } from 'sprintf-js'; -var sprintf = require('sprintf-js').sprintf; -let defaultLanguage = 'en_US'; +function I18n (gettextCatalog) { + return { + N_, + sprintf, + _: s => gettextCatalog.getString(s), + translate: (singular, context) => gettextCatalog.getString(singular, context), + translatePlural: (count, singular, plural, context) => { + return gettextCatalog.getPlural(count, singular, plural, context); + }, + hasTranslation: () => gettextCatalog.strings[gettextCatalog.currentLanguage] !== undefined + }; +} + +I18n.$inject = ['gettextCatalog']; + +function run (LOCALE, gettextCatalog) { + if (LOCALE.code && LOCALE.strings) { + gettextCatalog.setCurrentLanguage(LOCALE.code); + gettextCatalog.setStrings(LOCALE.code, LOCALE.strings); + } +} + +run.$inject = ['LOCALE', 'gettextCatalog']; -/** - * @ngdoc method - * @name function:i18n#N_ - * @methodOf function:N_ - * @description this function marks the translatable string with N_ - * for 'grunt nggettext_extract' - * -*/ export function N_(s) { return s; } -export default - angular.module('I18N', []) - .factory('I18NInit', ['$window', 'gettextCatalog', - function ($window, gettextCatalog) { - return function() { - var langInfo = ($window.navigator.languages || [])[0] || - $window.navigator.language || - $window.navigator.userLanguage || - ''; - var langUrl = langInfo.replace('-', '_'); - - if (langUrl === defaultLanguage) { - return; - } - - // gettextCatalog.debug = true; - gettextCatalog.setCurrentLanguage(langInfo); - gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json'); - }; - }]) - .factory('i18n', ['gettextCatalog', - function (gettextCatalog) { - return { - _: function (s) { return gettextCatalog.getString (s); }, - N_: N_, - translate: (singular, context) => gettextCatalog.getString(singular, context), - translatePlural: (count, singular, plural, context) => { - return gettextCatalog.getPlural(count, singular, plural, context); - }, - sprintf: sprintf, - hasTranslation: function () { - return gettextCatalog.strings[gettextCatalog.currentLanguage] !== undefined; - } - }; - }]); +export default angular + .module('I18N', [ + 'gettext' + ]) + .factory('i18n', I18n) + .run(run); diff --git a/awx/ui/karma.conf.js b/awx/ui/karma.conf.js index f35dea0200..165ca318d2 100644 --- a/awx/ui/karma.conf.js +++ b/awx/ui/karma.conf.js @@ -16,16 +16,16 @@ module.exports = function(config) { reporters: ['progress', 'coverage', 'junit'], files:[ './client/src/vendor.js', - './client/src/app.js', './node_modules/angular-mocks/angular-mocks.js', - { pattern: './tests/**/*-test.js' }, - 'client/src/**/*.html' + './client/src/app.js', + './tests/**/*-test.js', + './client/src/**/*.html' ], preprocessors: { - './client/src/vendor.js': ['webpack', 'sourcemap'], - './client/src/app.js': ['webpack', 'sourcemap'], - './tests/**/*-test.js': ['webpack', 'sourcemap'], - 'client/src/**/*.html': ['html2js'] + './client/src/vendor.js': ['webpack'], + './client/src/app.js': ['webpack'], + './tests/**/*-test.js': ['webpack'], + './client/src/**/*.html': ['html2js'] }, webpack: webpackTestConfig, webpackMiddleware: { From d9e360e5755746582bcf58137cfaa882a0323ae7 Mon Sep 17 00:00:00 2001 From: gconsidine Date: Thu, 28 Sep 2017 14:46:56 -0400 Subject: [PATCH 2/2] Add check if navigator.languages exists to use --- awx/ui/client/src/app.start.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/app.start.js b/awx/ui/client/src/app.start.js index f715f14082..44f8113567 100644 --- a/awx/ui/client/src/app.start.js +++ b/awx/ui/client/src/app.start.js @@ -28,7 +28,7 @@ function bootstrap (callback) { * @returns {object=} - Locale data if it exists. */ function fetchLocaleStrings (callback) { - const code = normalizeLocaleCode(navigator.language || navigator.userLanguage); + const code = getNormalizedLocaleCode(); if (isDefaultLocale(code) || !isSupportedLocale(code)) { callback({ code }); @@ -49,14 +49,29 @@ function fetchLocaleStrings (callback) { request.fail(() => callback({ code: DEFAULT_LOCALE })); } -function normalizeLocaleCode (code) { +/** + * Grabs the language off of navigator for browser compatibility. + * If the language isn't set, then it falls back to the DEFAULT_LOCALE. The + * locale code is normalized to be lowercase and 2 characters in length. + */ +function getNormalizedLocaleCode () { + let code; + + if (navigator.languages && navigator.languages[0]) { + [code] = navigator.languages; + } else if (navigator.language) { + code = navigator.language; + } else { + code = navigator.userLanguage; + } + try { code = code.split('-')[0].toLowerCase(); } catch (error) { code = DEFAULT_LOCALE; } - return code; + return code.substring(0, 2); } function isSupportedLocale (code) {