From 057e283cec810ba1924fb6df460fa76872f27144 Mon Sep 17 00:00:00 2001 From: Adrien LAUER Date: Fri, 15 Apr 2016 14:27:47 +0200 Subject: [PATCH] Refine translation fallback behavior, add basic auth doc --- CHANGELOG.md | 11 +- modules/application.js | 33 +- modules/culture.js | 1412 ++++++++++++++++++++-------------------- modules/env.js | 26 +- modules/security.js | 78 ++- specs/culture.spec.js | 9 +- test-main.js | 3 +- 7 files changed, 807 insertions(+), 765 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9020bb..611ffa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # Version 2.3.0 (?) -* [new] Add support for HTML5 mode (pretty urls) +* [new] Add support for HTML5 mode (pretty urls). * [new] Add the ability to specify an `optional` attribute on any fragment configuration, allowing application to load anyway. -* [new] Add the ability to specify an `ignore` attribute on any fragment configuration to avoid loading it. -* [new] Add the ability to specify a `translationFallback` boolean in the `culture` module configuration. If true, missing translations will fallback to their value in the default language. Note: the default culture bundle will always be loaded if the option is activated. +* [new] Add the ability to specify an `ignore` attribute on any fragment configuration to avoid loading it (useful for development). +* [new] Implement best-effort credentials cleanup for basic authentication (forcing the browser to forget credentials). +* [chg] Fallback to default culture when a translation is missing in active culture is no longer active by default. Sets `translationFallback` to `true` on the `culture` module configuration to force the fallback behavior. +* [fix] When translation fallback is active, always load default translations even when another culture is stored in preferences (#66). +* [fix] Fix translation of "Close all" notification dismiss link (#67). +* [fix] Catch JSON parsing error when persisted state is corrupted and fallback to default value (#68). +* [fix] Do not prevent `redirectAfterLogin` page to be shown after manual logout (#69). * [brk] Remove `text` module which has been moved to `w20-extras` add-on. # Version 2.2.2 (2016-02-15) diff --git a/modules/application.js b/modules/application.js index be8a758..1e48b01 100644 --- a/modules/application.js +++ b/modules/application.js @@ -84,8 +84,9 @@ define([ * ... * } */ - var w20CoreApplication = angular.module('w20CoreApplication', [ 'w20CoreEnv', 'ngRoute', 'ngSanitize' ]), + var w20CoreApplication = angular.module('w20CoreApplication', ['w20CoreEnv', 'ngRoute', 'ngSanitize']), config = module && module.config() || {}, + appId = config.id || 'w20app', allRoutes = {}, allRouteHandlers = {}, sceUrlWhiteList = [], @@ -100,7 +101,7 @@ define([ }; // Routes configuration - w20CoreApplication.config([ '$routeProvider', '$locationProvider', '$sceDelegateProvider', function ($routeProvider, $locationProvider, $sceDelegateProvider) { + w20CoreApplication.config(['$routeProvider', '$locationProvider', '$sceDelegateProvider', function ($routeProvider, $locationProvider, $sceDelegateProvider) { $locationProvider.hashPrefix('!'); if (config.prettyUrls) { $locationProvider.html5Mode(true); @@ -133,7 +134,7 @@ define([ return deferred !== null ? deferred.then(checkSecurity, checkSecurity) : checkSecurity(); }], - routeCheck: [ '$q', '$injector', function ($q, $injector) { + routeCheck: ['$q', '$injector', function ($q, $injector) { if (typeof route.check === 'undefined') { return $q.defer().resolve(); } @@ -154,17 +155,17 @@ define([ // Home route var homeRoute; if (typeof allRoutes[config.home] !== 'undefined') { - homeRoute = _.extend(_.extend({}, allRoutes[config.home]), { path: '', hidden: true }); + homeRoute = _.extend(_.extend({}, allRoutes[config.home]), {path: '', hidden: true}); } - $routeProvider.when('/', homeRoute || { template: '' }); + $routeProvider.when('/', homeRoute || {template: ''}); // Fallback route var fallbackRoute; if (typeof allRoutes[config.notFound] !== 'undefined') { - fallbackRoute = _.extend(_.extend({}, allRoutes[config.notFound]), { path: undefined, hidden: true }); + fallbackRoute = _.extend(_.extend({}, allRoutes[config.notFound]), {path: undefined, hidden: true}); $routeProvider.otherwise(fallbackRoute); } - } ]); + }]); // Cache busting w20CoreApplication.config(['$provide', function ($provide) { @@ -211,7 +212,7 @@ define([ * * This id can be used to disambiguate between multiple W20 applications when necessary. */ - applicationId: config.id || 'w20app', + applicationId: appId, /** * @ngdoc function @@ -266,7 +267,7 @@ define([ return first.stack === second.stack; } - this.$get = [ '$log', '$injector', function ($log, $injector) { + this.$get = ['$log', '$injector', function ($log, $injector) { return function (exception, cause) { try { $injector.invoke(['$timeout', 'EventService', function ($timeout, eventService) { @@ -321,7 +322,7 @@ define([ }]; }); - w20CoreApplication.run([ 'EventService', '$location', '$rootScope', function (eventService, $location, $rootScope) { + w20CoreApplication.run(['EventService', '$location', '$rootScope', function (eventService, $location, $rootScope) { if (typeof config.redirectAfterRouteError === 'string') { eventService.on('$routeChangeError', function () { $location.path(config.redirectAfterRouteError); @@ -331,7 +332,7 @@ define([ eventService.on('$routeChangeSuccess', function (current) { $rootScope.currentRoute = current && current.$$route; }); - } ]); + }]); function registerRouteHandler(type, handlerFn) { allRouteHandlers[type] = handlerFn; @@ -360,13 +361,13 @@ define([ registerRouteHandler('sandbox', function (route) { var sandboxPermissions = route.sandboxPermissions || config.defaultSandboxPermissions; route.template = '
' + - '' + - '
'; + '' + + ''; return route; }); return { - angularModules: [ 'w20CoreApplication' ], + angularModules: ['w20CoreApplication'], lifecycle: { pre: function (modules, fragments, callback) { @@ -428,6 +429,8 @@ define([ } }, - registerRouteHandler: registerRouteHandler + registerRouteHandler: registerRouteHandler, + + id: appId }; }); diff --git a/modules/culture.js b/modules/culture.js index 3fdd113..2cfa844 100644 --- a/modules/culture.js +++ b/modules/culture.js @@ -15,163 +15,209 @@ define([ '{lodash}/lodash', '{angular}/angular', '{globalize}/globalize', - + '{w20-core}/modules/application', '{w20-core}/modules/env' -], function (module, require, w20, $, _, angular, globalize) { +], function (module, require, w20, $, _, angular, globalize, application) { 'use strict'; + // Config var config = module && module.config() || {}, - availableCultures = [], + translationFallback = config.translationFallback || false; + + // Global state + var availableCultures = [], availableCultureObjects = [], - allBundles = {}, - loadedCultures = [], - defaultCulture = globalize.findClosestCulture('default'), - activeCulture = defaultCulture, - persistedCulture; - - function loadCultureBundles(culture) { - var deferred = $.Deferred(); - - // Load culture bundles - if ($.inArray(culture.name, loadedCultures) === -1) { - loadedCultures.push(culture.name); - - // Build the language dependencies to load - var modulesToLoad = (allBundles[culture.language] || []).map(function (elt) { - return '[text]!' + elt; - }); + defaultCulture = globalize.cultures.default, + activeCulture = defaultCulture; + + // Utility functions + var addBundles, + loadCultureBundles, + switchCulture, + buildAngularLocale, + buildDate, + savePreferredCulture, + getPreferredCulture; + + (function () { + var allBundles = {}, + loadedCultures = []; + + addBundles = function (language, newBundles) { + if (typeof allBundles[language] === 'undefined') { + allBundles[language] = []; + } + allBundles[language] = allBundles[language].concat(newBundles); + }; - // Add the culture specific dependencies to load - modulesToLoad = modulesToLoad.concat((allBundles[culture.name] || []).map(function (elt) { - return '[text]!' + elt; - })); + loadCultureBundles = function (culture) { + var deferred = $.Deferred(); - // Add the multi-language dependencies (with an empty key) - if (typeof allBundles[''] !== 'undefined' && allBundles[''].length > 0) { - modulesToLoad = modulesToLoad.concat(allBundles[''].map(function (elt) { - return '[text]!' + elt.replace(':language', culture.language).replace(':culture', culture.name); + // Load culture bundles + if ($.inArray(culture.name, loadedCultures) === -1) { + loadedCultures.push(culture.name); + + // Build the language dependencies to load + var modulesToLoad = (allBundles[culture.language] || []).map(function (elt) { + return '[text]!' + elt; + }); + + // Add the culture specific dependencies to load + modulesToLoad = modulesToLoad.concat((allBundles[culture.name] || []).map(function (elt) { + return '[text]!' + elt; })); - } - if (modulesToLoad.length > 0) { - w20.console.log('Loading i18n bundles: ' + modulesToLoad); - - require(modulesToLoad, function () { - var bundlesLoaded = Array.prototype.slice.call(arguments, 0); - for (var i = 0; i < bundlesLoaded.length; i++) { - try { - var messages = angular.fromJson(bundlesLoaded[i]); - globalize.addCultureInfo(culture.name, { - messages: messages - }); - } catch (e) { - deferred.reject(new Error('Error loading i18n bundle ' + bundlesLoaded[i], e)); + // Add the multi-language dependencies (with an empty key) + if (typeof allBundles[''] !== 'undefined' && allBundles[''].length > 0) { + modulesToLoad = modulesToLoad.concat(allBundles[''].map(function (elt) { + return '[text]!' + elt.replace(':language', culture.language).replace(':culture', culture.name); + })); + } + + if (modulesToLoad.length > 0) { + w20.console.log('Loading i18n bundles: ' + modulesToLoad); + + require(modulesToLoad, function () { + var bundlesLoaded = Array.prototype.slice.call(arguments, 0); + for (var i = 0; i < bundlesLoaded.length; i++) { + try { + var messages = angular.fromJson(bundlesLoaded[i]); + globalize.addCultureInfo(culture.name, { + messages: messages + }); + } catch (e) { + deferred.reject(new Error('Error loading i18n bundle ' + bundlesLoaded[i], e)); + } } - } + deferred.resolve(culture); + }); + } else { deferred.resolve(culture); - }); + } } else { deferred.resolve(culture); } - } else { - deferred.resolve(culture); - } - - return deferred.promise(); - } - - function switchCulture(selector) { - var culture = (typeof selector === 'string' ? globalize.findClosestCulture(selector) : selector); - - return loadCultureBundles(culture).then(function (cultureObject) { - activeCulture = globalize.culture(cultureObject.name); - w20.console.info('Culture has been set to ' + activeCulture.name); - return cultureObject; - }, function (e) { - throw new Error('Could not switch to culture ' + culture.name, e); - }); - } - - function buildAngularLocale(culture) { - var standardCalendar = culture.calendars.standard, - currency = culture.numberFormat.currency, - number = culture.numberFormat; - - function buildNumberPattern(patterns) { + + return deferred.promise(); + }; + + switchCulture = function (selector) { + var culture = (typeof selector === 'string' ? globalize.findClosestCulture(selector) : selector); + + return loadCultureBundles(culture).then(function (cultureObject) { + activeCulture = globalize.culture(cultureObject.name); + w20.console.info('Culture has been set to ' + activeCulture.name); + return cultureObject; + }, function (e) { + throw new Error('Could not switch to culture ' + culture.name, e); + }); + }; + + savePreferredCulture = function (cultureName) { + localStorage.setItem('w20.' + application.id + '.preferred-culture', cultureName); + }; + + getPreferredCulture = function () { + var persistedCulture = localStorage.getItem('w20.' + application.id + '.preferred-culture'); + if (persistedCulture) { + return globalize.findClosestCulture(persistedCulture); + } else { + return defaultCulture; + } + }; + + buildAngularLocale = function (culture) { + var standardCalendar = culture.calendars.standard, + currency = culture.numberFormat.currency, + number = culture.numberFormat; + + function buildNumberPattern(patterns) { + return { + negPre: patterns[0].substring(0, patterns[0].indexOf('n') - 1), + negSuf: patterns[0].substring(patterns[0].indexOf('n')), + posPre: patterns[1] ? patterns[1].substring(0, patterns[0].indexOf('n') - 1).replace('$', '\u00a4') : '', + posSuf: patterns[1] ? patterns[1].substring(patterns[0].indexOf('n')).replace('$', '\u00a4') : '' + }; + } + + function buildDateTimePattern(pattern) { + var result = pattern; + + // day name + result = result.replace(/dddd/g, 'EEEE'); + result = result.replace(/ddd/g, 'EEE'); + + // milliseconds + result = result.replace(/fff/g, '.sss'); + + // AM/PM + result = result.replace(/tt/g, 'a'); + + // Timezone + result = result.replace(/zzz/g, 'Z'); + + return result; + } + return { - negPre: patterns[0].substring(0, patterns[0].indexOf('n') - 1), - negSuf: patterns[0].substring(patterns[0].indexOf('n')), - posPre: patterns[1] ? patterns[1].substring(0, patterns[0].indexOf('n') - 1).replace('$', '\u00a4') : '', - posSuf: patterns[1] ? patterns[1].substring(patterns[0].indexOf('n')).replace('$', '\u00a4') : '' + 'DATETIME_FORMATS': { + 'AMPMS': [ + standardCalendar.AM && standardCalendar.AM[0] || 'AM', + standardCalendar.PM && standardCalendar.PM[0] || 'PM' + ], + 'DAY': standardCalendar.days.names, + 'MONTH': standardCalendar.months.names, + 'SHORTDAY': standardCalendar.days.namesAbbr, + 'SHORTMONTH': standardCalendar.months.namesAbbr, + 'fullDate': buildDateTimePattern(standardCalendar.patterns.D), + 'longDate': buildDateTimePattern(standardCalendar.patterns.d), + 'medium': buildDateTimePattern(standardCalendar.patterns.F), + 'mediumDate': buildDateTimePattern(standardCalendar.patterns.d), + 'mediumTime': buildDateTimePattern(standardCalendar.patterns.T), + 'short': buildDateTimePattern(standardCalendar.patterns.f), + 'shortDate': buildDateTimePattern(standardCalendar.patterns.d), + 'shortTime': buildDateTimePattern(standardCalendar.patterns.t) + }, + 'NUMBER_FORMATS': { + 'CURRENCY_SYM': currency.symbol, + 'DECIMAL_SEP': number['.'], + 'GROUP_SEP': number[','], + 'PATTERNS': [ + _.merge({ + 'gSize': number.groupSizes[0], + 'lgSize': number.groupSizes[0], + 'maxFrac': number.decimals, + 'minFrac': 0, + 'minInt': 1 + }, buildNumberPattern(number.pattern)), + _.merge({ + 'gSize': currency.groupSizes[0], + 'lgSize': currency.groupSizes[0], + 'maxFrac': currency.decimals, + 'minFrac': currency.decimals, + 'minInt': 1 + }, buildNumberPattern(currency.pattern)) + ] + }, + 'id': culture.name.toLowerCase(), + 'pluralCat': function () { + return 'other'; + } }; - } - - function buildDateTimePattern(pattern) { - var result = pattern; - - // day name - result = result.replace(/dddd/g, 'EEEE'); - result = result.replace(/ddd/g, 'EEE'); - - // milliseconds - result = result.replace(/fff/g, '.sss'); - - // AM/PM - result = result.replace(/tt/g, 'a'); - - // Timezone - result = result.replace(/zzz/g, 'Z'); - - return result; - } - - return { - 'DATETIME_FORMATS': { - 'AMPMS': [ - standardCalendar.AM && standardCalendar.AM[0] || 'AM', - standardCalendar.PM && standardCalendar.PM[0] || 'PM' - ], - 'DAY': standardCalendar.days.names, - 'MONTH': standardCalendar.months.names, - 'SHORTDAY': standardCalendar.days.namesAbbr, - 'SHORTMONTH': standardCalendar.months.namesAbbr, - 'fullDate': buildDateTimePattern(standardCalendar.patterns.D), - 'longDate': buildDateTimePattern(standardCalendar.patterns.d), - 'medium': buildDateTimePattern(standardCalendar.patterns.F), - 'mediumDate': buildDateTimePattern(standardCalendar.patterns.d), - 'mediumTime': buildDateTimePattern(standardCalendar.patterns.T), - 'short': buildDateTimePattern(standardCalendar.patterns.f), - 'shortDate': buildDateTimePattern(standardCalendar.patterns.d), - 'shortTime': buildDateTimePattern(standardCalendar.patterns.t) - }, - 'NUMBER_FORMATS': { - 'CURRENCY_SYM': currency.symbol, - 'DECIMAL_SEP': number['.'], - 'GROUP_SEP': number[','], - 'PATTERNS': [ - _.merge({ - 'gSize': number.groupSizes[0], - 'lgSize': number.groupSizes[0], - 'maxFrac': number.decimals, - 'minFrac': 0, - 'minInt': 1 - }, buildNumberPattern(number.pattern)), - _.merge({ - 'gSize': currency.groupSizes[0], - 'lgSize': currency.groupSizes[0], - 'maxFrac': currency.decimals, - 'minFrac': currency.decimals, - 'minInt': 1 - }, buildNumberPattern(currency.pattern)) - ] - }, - 'id': culture.name.toLowerCase(), - 'pluralCat': function () { - return 'other'; - } }; - } + buildDate = function (input) { + if (input instanceof Date) { + return input; + } + else if (typeof input === 'number' || typeof input === 'string') { + return new Date(input); + } + else { + return undefined; + } + }; + })(); /** * @ngdoc object @@ -189,8 +235,14 @@ define([ * // Array of available cultures in the application * "available" : [ "ietf-code-1", "ietf-code-2", ... ], * - * // Default culture of the application when no user preference is overriding it - * "default" : "ietf-code" + * // Default culture of the application when no user preference is remembered + * "default" : "ietf-code", + * + * // If true, translations that are missing in the active culture fallback to the default culture. + * // If false, the raw keys are displayed instead. + * // Note that setting this to true will always load default culture translation bundles, potentially in + * // addition to active culture translations. + * "translationFallback" : true|false * } * * # Fragment definition @@ -424,19 +476,6 @@ define([ * } */ var w20CoreCulture = angular.module('w20CoreCulture', ['w20CoreEnv', 'ngResource']); - var placeholderRegexp = new RegExp('{-?[0-9]+}', 'g'); - - var buildDate = function (input) { - if (input instanceof Date) { - return input; - } - else if (typeof input === 'number' || typeof input === 'string') { - return new Date(input); - } - else { - return undefined; - } - }; /** * @ngdoc service @@ -448,102 +487,98 @@ define([ * you need to use it. */ w20CoreCulture.factory('CultureService', ['EventService', 'StateService', '$rootScope', '$locale', '$window', function (eventService, stateService, $rootScope, $locale, $window) { + var placeholderRegexp = new RegExp('{-?[0-9]+}', 'g'), + service = { + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#defaultCulture + * @methodOf w20CoreCulture.service:CultureService + * @returns {Object} The default culture object + * + * @description + * + * Returns the default culture of the application (not necessarily the active one). + */ + defaultCulture: function () { + return defaultCulture; + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#culture + * @methodOf w20CoreCulture.service:CultureService + * @param {String} [selector] The selector of the culture to switch to. Examples : "fr", "fr-FR", ["en-US", "fr-FR"], "fr;q=0.4, es;q=0.5, he". + * @returns {Object} The current culture object if no selector was specified, undefined if new culture selector was specified. + * + * @description + * + * Getter/setter of the currently active culture in the application. Return the current culture if the + * selector parameter is undefined, change it otherwise. + * + * Automatically load i18n string bundles configured when switching for the first time to a new culture. + * The w20.culture.culture-changed event is fired when the culture has switched. + */ + culture: function (selector) { + if (typeof selector === 'undefined') { + return globalize.culture(); + } - var cultureState = stateService.state('culture', 'first', defaultCulture); - - var service = { - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#defaultCulture - * @methodOf w20CoreCulture.service:CultureService - * @returns {Object} The default culture object - * - * @description - * - * Returns the default culture of the application (not necessarily the active one). - */ - defaultCulture: function () { - return defaultCulture; - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#culture - * @methodOf w20CoreCulture.service:CultureService - * @param {String} [selector] The selector of the culture to switch to. Examples : "fr", "fr-FR", ["en-US", "fr-FR"], "fr;q=0.4, es;q=0.5, he". - * @returns {Object} The current culture object if no selector was specified, undefined if new culture selector was specified. - * - * @description - * - * Getter/setter of the currently active culture in the application. Return the current culture if the - * selector parameter is undefined, change it otherwise. - * - * Automatically load i18n string bundles configured when switching for the first time to a new culture. - * The w20.culture.culture-changed event is fired when the culture has switched. - */ - culture: function (selector) { - if (typeof selector === 'undefined') { - return globalize.culture(); - } - - switchCulture(selector).done(function (newCulture) { - // Override $locale values with new ones - _.merge($locale, buildAngularLocale(newCulture)); + switchCulture(selector).done(function (newCulture) { + // Override $locale values with new ones + _.merge($locale, buildAngularLocale(newCulture)); - // persist culture preference - if ($window.localStorage && newCulture && newCulture.name) { - cultureState.value(newCulture.name); - persistedCulture = cultureState.value(); - } + // persist culture preference + savePreferredCulture(newCulture.name); - /** - * This event is emitted after the culture has changed successfully. - * - * @name w20.culture.culture-changed - * @w20doc event - * @memberOf w20CoreCulture - * @argument {Object} The new culture definition. - */ - eventService.emit('w20.culture.culture-changed', newCulture); + /** + * This event is emitted after the culture has changed successfully. + * + * @name w20.culture.culture-changed + * @w20doc event + * @memberOf w20CoreCulture + * @argument {Object} The new culture definition. + */ + eventService.emit('w20.culture.culture-changed', newCulture); - $rootScope.$safeApply(); - }); - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#availableCultures - * @methodOf w20CoreCulture.service:CultureService - * @returns {Object} The array of available cultures (fully detailed culture description). - * - * @description - * - * Return an array of available cultures in the application. - */ - availableCultures: function () { - return availableCultureObjects; - }, - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#localize - * @methodOf w20CoreCulture.service:CultureService - * @param {String} key The i18n key to localize. - * @param {Array} [values] The localized string placeholder values. - * @param {String} [defaultValue] A default value to be returned if no localization exists in the language. - * @param {String} [culture] If specified this culture selector will be used to do the localization. - * @returns {String} The translated string, with placeholders replaced by their respective values. - * - * @description - * - * Localize an i18n key in the current culture or the explicitely specified one. An localized string - * can contain placeholders like {0}, {1}, {2}, ... {n} which will be replaced by the corresponding - * element in the values array. - */ - localize: function (key, values, defaultValue, culture) { - var result = globalize.localize(key, culture || activeCulture.name); - if (typeof result === 'undefined') { - result = globalize.localize(key, defaultCulture.name); + $rootScope.$safeApply(); + }); + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#availableCultures + * @methodOf w20CoreCulture.service:CultureService + * @returns {Object} The array of available cultures (fully detailed culture description). + * + * @description + * + * Return an array of available cultures in the application. + */ + availableCultures: function () { + return availableCultureObjects; + }, + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#localize + * @methodOf w20CoreCulture.service:CultureService + * @param {String} key The i18n key to localize. + * @param {Array} [values] The localized string placeholder values. + * @param {String} [defaultValue] A default value to be returned if no localization exists in the language. + * @param {String} [culture] If specified this culture selector will be used to do the localization. + * @returns {String} The translated string, with placeholders replaced by their respective values. + * + * @description + * + * Localize an i18n key in the current culture or the explicitely specified one. An localized string + * can contain placeholders like {0}, {1}, {2}, ... {n} which will be replaced by the corresponding + * element in the values array. + */ + localize: function (key, values, defaultValue, culture) { + var result = globalize.localize(key, culture || activeCulture.name); + if (typeof result === 'undefined' && translationFallback) { + result = globalize.localize(key, defaultCulture.name); + } if (typeof result === 'undefined') { if (typeof defaultValue === 'undefined') { return '[' + key + ']'; @@ -552,435 +587,434 @@ define([ return defaultValue; } } - } - var typeOfValues = typeof values; - if (typeOfValues !== 'undefined' && (typeOfValues === 'string' || typeOfValues === 'int' || typeOfValues === 'float')) { - values = [values]; - } + var typeOfValues = typeof values; + if (typeOfValues !== 'undefined' && (typeOfValues === 'string' || typeOfValues === 'int' || typeOfValues === 'float')) { + values = [values]; + } - return result.replace(placeholderRegexp, function (item) { - return values[parseInt(item.substring(1, item.length - 1))] || ''; - }); - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#format - * @methodOf w20CoreCulture.service:CultureService - * @param {*} value The value to format. - * @param {String} format Pattern used for the formatting. - * @param {String} [culture] If specified this culture selector will be used to do the formatting. - * @returns {String} The value formatted. - * - * @description - * - * Format any value according to the format parameter. The current culture is used for formatting rules, - * except if a selector is specified as the culture parameter. - * - * ##### Number formatting - * - * When formatting a number with format(), the main purpose is to convert the - * number into a human readable string using the culture's standard grouping and - * decimal rules. The rules between cultures can vary a lot. For example, in some - * cultures, the grouping of numbers is done unevenly. In the "te-IN" culture - * (Telugu in India), groups have 3 digits and then 2 digits. The number 1000000 - * (one million) is written as "10,00,000". Some cultures do not group numbers at - * all. - - * There are four main types of number formatting: - * - * - * Even within the same culture, the formatting rules can vary between these four - * types of numbers. For example, the expected number of decimal places may differ - * from the number format to the currency format. Each format token may also be - * followed by a number. The number determines how many decimal places to display - * for all the format types except decimal, for which it means the minimum number - * of digits to display, zero padding it if necessary. Also note that the way - * negative numbers are represented in each culture can vary, such as what the - * negative sign is, and whether the negative sign appears before or after the - * number. This is especially apparent with currency formatting, where many - * cultures use parentheses instead of a negative sign. - * - * // just for example - will vary by culture - * CultureService.format( 123.45, "n" ); // 123.45 - * CultureService.format( 123.45, "n0" ); // 123 - * CultureService.format( 123.45, "n1" ); // 123.5 - * - * CultureService.format( 123.45, "d" ); // 123 - * CultureService.format( 12, "d3" ); // 012 - * - * CultureService.format( 123.45, "c" ); // $123.45 - * CultureService.format( 123.45, "c0" ); // $123 - * CultureService.format( 123.45, "c1" ); // $123.5 - * CultureService.format( -123.45, "c" ); // ($123.45) - * - * CultureService.format( 0.12345, "p" ); // 12.35 % - * CultureService.format( 0.12345, "p0" ); // 12 % - * CultureService.format( 0.12345, "p4" ); // 12.3450 % - * - * Parsing with parseInt and parseFloat also accepts any of these formats. - * - * ##### Date formatting - * - * Date formatting varies wildly by culture, not just in the spelling of month and - * day names, and the date separator, but by the expected order of the various - * date components, whether to use a 12 or 24 hour clock, and how months and days - * are abbreviated. Many cultures even include "genitive" month names, which are - * different from the typical names and are used only in certain cases. - * - * Also, each culture has a set of "standard" or "typical" formats. For example, - * in "en-US", when displaying a date in its fullest form, it looks like - * "Saturday, November 05, 1955". Note the non-abbreviated day and month name, the - * zero padded date, and four digit year. So, the culture service expects a certain set - * of "standard" formatting strings for dates in the "patterns" property of the - * "standard" calendar of each culture, that describe specific formats for the - * culture. The third column shows example values in the neutral English culture - * "en-US"; see the second table for the meaning tokens used in date formats. - * - * // just for example - will vary by culture - * cultureService.format( new Date(2012, 1, 20), 'd' ); // 2/20/2012 - * cultureService.format( new Date(2012, 1, 20), 'D' ); // Monday, February 20, 2012 - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
FormatMeaning"en-US"
fLong Date, Short Timedddd, MMMM dd, yyyy h:mm tt
FLong Date, Long Timedddd, MMMM dd, yyyy h:mm:ss tt
tShort Timeh:mm tt
TLong Timeh:mm:ss tt
dShort DateM/d/yyyy
DLong Datedddd, MMMM dd, yyyy
YMonth/YearMMMM, yyyy
MMonth/DayMMMM dd
- * - * In addition to these standard formats, there is the "S" format. This is a - * sortable format that is identical in every culture: - * "yyyy'-'MM'-'dd'T'HH':'mm':'ss". - * - * When more specific control is needed over the formatting, you may use any - * format you wish by specifying the following custom tokens: - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
TokenMeaningExample
dDay of month (no leading zero)5
ddDay of month (leading zero)05
dddDay name (abbreviated)Sat
ddddDay name (full)Saturday
MMonth of year (no leading zero)9
MMMonth of year (leading zero)09
MMMMonth name (abbreviated)Sep
MMMMMonth name (full)September
yyYear (two digits)55
yyyyYear (four digits)1955
'literal'Literal Text'of the clock'
\'Single Quote'o'\''clock'
mMinutes (no leading zero)9
mmMinutes (leading zero)09
hHours (12 hour time, no leading zero)6
hhHours (12 hour time, leading zero)06
HHours (24 hour time, no leading zero)5 (5am) 15 (3pm)
HHHours (24 hour time, leading zero)05 (5am) 15 (3pm)
sSeconds (no leading zero)9
ssSeconds (leading zero)09
fDeciseconds1
ffCentiseconds11
fffMilliseconds111
tAM/PM indicator (first letter)A or P
ttAM/PM indicator (full)AM or PM
zTimezone offset (hours only, no leading zero)-8
zzTimezone offset (hours only, leading zero)-08
zzzTimezone offset (full hours/minutes)-08:00
g or ggEra nameA.D.
- */ - format: function (value, format, culture) { - return globalize.format(value, format, culture || activeCulture); - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#parseInt - * @methodOf w20CoreCulture.service:CultureService - * @param {String} value The value to parse as an integer. - * @param {int} [radix] The radix used for the conversion (10 by default). - * @param {String} [culture] If specified this culture selector will be used to do the parsing. - * @returns {int} The integer parsed. - * - * @description - * - * Parses a string representing a whole number in the given radix (10 by default), - * taking into account any formatting rules followed by the given culture (or the - * current culture, if not specified). - * - * // assuming a culture where "," is the group separator - * // and "." is the decimal separator - * CultureService.parseInt( "1,234.56" ); // 1234 - * // assuming a culture where "." is the group separator - * // and "," is the decimal separator - * CultureService.parseInt( "1.234,56" ); // 1234 - */ - parseInt: function (value, radix, culture) { - return globalize.parseInt(value, radix, culture || activeCulture); - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#parseFloat - * @methodOf w20CoreCulture.service:CultureService - * @param {String} value The value to parse as a float. - * @param {int} [radix] The radix used for the conversion (10 by default). - * @param {String} [culture] If specified this culture selector will be used to do the parsing. - * @returns {Number} The float parsed. - * - * @description - * - * Parses a string representing a floating point number in the given radix (10 by - * default), taking into account any formatting rules followed by the given - * culture (or the current culture, if not specified). - * - * // assuming a culture where "," is the group separator - * // and "." is the decimal separator - * CultureService.parseFloat( "1,234.56" ); // 1234.56 - * // assuming a culture where "." is the group separator - * // and "," is the decimal separator - * CultureService.parseFloat( "1.234,56" ); // 1234.56 - */ - parseFloat: function (value, radix, culture) { - return globalize.parseFloat(value, radix, culture || activeCulture); - }, - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#culture - * @methodOf w20CoreCulture.service:CultureService - * @param {String} value The value to parse as a date. - * @param {String} [formats] The formats used to do the parsing. - * @param {String} [culture] If specified this culture selector will be used to do the parsing. - * @returns {Date} The date object parsed. - * - * @description - * - * Parses a string representing a date into a JavaScript Date object, taking into - * account the given possible formats (or the given culture's set of default - * formats if not given). As before, the current culture is used if one is not - * specified. - * - * cultureService.culture( "en" ); - * cultureService.parseDate( "1/2/2003" ); // Thu Jan 02 2003 - * cultureService.culture( "fr" ); - * cultureService.parseDate( "1/2/2003" ); // Sat Feb 01 2003 - */ - parseDate: function (value, formats, culture) { - return globalize.parseDate(value, formats, culture || activeCulture); - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#addCultureInfo - * @methodOf w20CoreCulture.service:CultureService - * @param {String} [cultureName] If supplied it will create a culture with this name. - * @param {String} [baseCultureName] If supplied it will extend this culture to create the new one. - * @param {String} info The culture information to add, according to the culture object format. - * - * @description - * - * This method allows you to create a new culture based on an existing culture or add to existing culture info. - * If the optional argument `baseCultureName` is not supplied, it will extend the existing culture if it - * exists or create a new culture based on the default culture if it doesn't exist. If `cultureName` is not - * supplied, it will add the supplied info to the current culture. - */ - addCultureInfo: function (cultureName, baseCultureName, info) { - return globalize.addCultureInfo(cultureName, baseCultureName, info); - }, - - /** - * @ngdoc function - * @name w20CoreCulture.service:CultureService#displayName - * @methodOf w20CoreCulture.service:CultureService - * @param {String} object The object to compute the display name of. - * @param {String} [values] The values used to localize the i18n key if any. - * @returns {String} The display name of the object. - * - * @description - * - * Compute the display name of an object by following these steps: - * - * * Return its `label` attribute if it exists, or, - * * Return the localized form of its `i18n` attribute if it exists, or, - * * Return an empty string. - */ - displayName: function (object, values) { - if (typeof object.label !== 'undefined') { - return object.label; - } - else if (typeof object.i18n !== 'undefined') { - return service.localize(object.i18n, values); - } - else { - return ''; + return result.replace(placeholderRegexp, function (item) { + return values[parseInt(item.substring(1, item.length - 1))] || ''; + }); + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#format + * @methodOf w20CoreCulture.service:CultureService + * @param {*} value The value to format. + * @param {String} format Pattern used for the formatting. + * @param {String} [culture] If specified this culture selector will be used to do the formatting. + * @returns {String} The value formatted. + * + * @description + * + * Format any value according to the format parameter. The current culture is used for formatting rules, + * except if a selector is specified as the culture parameter. + * + * ##### Number formatting + * + * When formatting a number with format(), the main purpose is to convert the + * number into a human readable string using the culture's standard grouping and + * decimal rules. The rules between cultures can vary a lot. For example, in some + * cultures, the grouping of numbers is done unevenly. In the "te-IN" culture + * (Telugu in India), groups have 3 digits and then 2 digits. The number 1000000 + * (one million) is written as "10,00,000". Some cultures do not group numbers at + * all. + + * There are four main types of number formatting: + * + * + * Even within the same culture, the formatting rules can vary between these four + * types of numbers. For example, the expected number of decimal places may differ + * from the number format to the currency format. Each format token may also be + * followed by a number. The number determines how many decimal places to display + * for all the format types except decimal, for which it means the minimum number + * of digits to display, zero padding it if necessary. Also note that the way + * negative numbers are represented in each culture can vary, such as what the + * negative sign is, and whether the negative sign appears before or after the + * number. This is especially apparent with currency formatting, where many + * cultures use parentheses instead of a negative sign. + * + * // just for example - will vary by culture + * CultureService.format( 123.45, "n" ); // 123.45 + * CultureService.format( 123.45, "n0" ); // 123 + * CultureService.format( 123.45, "n1" ); // 123.5 + * + * CultureService.format( 123.45, "d" ); // 123 + * CultureService.format( 12, "d3" ); // 012 + * + * CultureService.format( 123.45, "c" ); // $123.45 + * CultureService.format( 123.45, "c0" ); // $123 + * CultureService.format( 123.45, "c1" ); // $123.5 + * CultureService.format( -123.45, "c" ); // ($123.45) + * + * CultureService.format( 0.12345, "p" ); // 12.35 % + * CultureService.format( 0.12345, "p0" ); // 12 % + * CultureService.format( 0.12345, "p4" ); // 12.3450 % + * + * Parsing with parseInt and parseFloat also accepts any of these formats. + * + * ##### Date formatting + * + * Date formatting varies wildly by culture, not just in the spelling of month and + * day names, and the date separator, but by the expected order of the various + * date components, whether to use a 12 or 24 hour clock, and how months and days + * are abbreviated. Many cultures even include "genitive" month names, which are + * different from the typical names and are used only in certain cases. + * + * Also, each culture has a set of "standard" or "typical" formats. For example, + * in "en-US", when displaying a date in its fullest form, it looks like + * "Saturday, November 05, 1955". Note the non-abbreviated day and month name, the + * zero padded date, and four digit year. So, the culture service expects a certain set + * of "standard" formatting strings for dates in the "patterns" property of the + * "standard" calendar of each culture, that describe specific formats for the + * culture. The third column shows example values in the neutral English culture + * "en-US"; see the second table for the meaning tokens used in date formats. + * + * // just for example - will vary by culture + * cultureService.format( new Date(2012, 1, 20), 'd' ); // 2/20/2012 + * cultureService.format( new Date(2012, 1, 20), 'D' ); // Monday, February 20, 2012 + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
FormatMeaning"en-US"
fLong Date, Short Timedddd, MMMM dd, yyyy h:mm tt
FLong Date, Long Timedddd, MMMM dd, yyyy h:mm:ss tt
tShort Timeh:mm tt
TLong Timeh:mm:ss tt
dShort DateM/d/yyyy
DLong Datedddd, MMMM dd, yyyy
YMonth/YearMMMM, yyyy
MMonth/DayMMMM dd
+ * + * In addition to these standard formats, there is the "S" format. This is a + * sortable format that is identical in every culture: + * "yyyy'-'MM'-'dd'T'HH':'mm':'ss". + * + * When more specific control is needed over the formatting, you may use any + * format you wish by specifying the following custom tokens: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
TokenMeaningExample
dDay of month (no leading zero)5
ddDay of month (leading zero)05
dddDay name (abbreviated)Sat
ddddDay name (full)Saturday
MMonth of year (no leading zero)9
MMMonth of year (leading zero)09
MMMMonth name (abbreviated)Sep
MMMMMonth name (full)September
yyYear (two digits)55
yyyyYear (four digits)1955
'literal'Literal Text'of the clock'
\'Single Quote'o'\''clock'
mMinutes (no leading zero)9
mmMinutes (leading zero)09
hHours (12 hour time, no leading zero)6
hhHours (12 hour time, leading zero)06
HHours (24 hour time, no leading zero)5 (5am) 15 (3pm)
HHHours (24 hour time, leading zero)05 (5am) 15 (3pm)
sSeconds (no leading zero)9
ssSeconds (leading zero)09
fDeciseconds1
ffCentiseconds11
fffMilliseconds111
tAM/PM indicator (first letter)A or P
ttAM/PM indicator (full)AM or PM
zTimezone offset (hours only, no leading zero)-8
zzTimezone offset (hours only, leading zero)-08
zzzTimezone offset (full hours/minutes)-08:00
g or ggEra nameA.D.
+ */ + format: function (value, format, culture) { + return globalize.format(value, format, culture || activeCulture); + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#parseInt + * @methodOf w20CoreCulture.service:CultureService + * @param {String} value The value to parse as an integer. + * @param {int} [radix] The radix used for the conversion (10 by default). + * @param {String} [culture] If specified this culture selector will be used to do the parsing. + * @returns {int} The integer parsed. + * + * @description + * + * Parses a string representing a whole number in the given radix (10 by default), + * taking into account any formatting rules followed by the given culture (or the + * current culture, if not specified). + * + * // assuming a culture where "," is the group separator + * // and "." is the decimal separator + * CultureService.parseInt( "1,234.56" ); // 1234 + * // assuming a culture where "." is the group separator + * // and "," is the decimal separator + * CultureService.parseInt( "1.234,56" ); // 1234 + */ + parseInt: function (value, radix, culture) { + return globalize.parseInt(value, radix, culture || activeCulture); + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#parseFloat + * @methodOf w20CoreCulture.service:CultureService + * @param {String} value The value to parse as a float. + * @param {int} [radix] The radix used for the conversion (10 by default). + * @param {String} [culture] If specified this culture selector will be used to do the parsing. + * @returns {Number} The float parsed. + * + * @description + * + * Parses a string representing a floating point number in the given radix (10 by + * default), taking into account any formatting rules followed by the given + * culture (or the current culture, if not specified). + * + * // assuming a culture where "," is the group separator + * // and "." is the decimal separator + * CultureService.parseFloat( "1,234.56" ); // 1234.56 + * // assuming a culture where "." is the group separator + * // and "," is the decimal separator + * CultureService.parseFloat( "1.234,56" ); // 1234.56 + */ + parseFloat: function (value, radix, culture) { + return globalize.parseFloat(value, radix, culture || activeCulture); + }, + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#culture + * @methodOf w20CoreCulture.service:CultureService + * @param {String} value The value to parse as a date. + * @param {String} [formats] The formats used to do the parsing. + * @param {String} [culture] If specified this culture selector will be used to do the parsing. + * @returns {Date} The date object parsed. + * + * @description + * + * Parses a string representing a date into a JavaScript Date object, taking into + * account the given possible formats (or the given culture's set of default + * formats if not given). As before, the current culture is used if one is not + * specified. + * + * cultureService.culture( "en" ); + * cultureService.parseDate( "1/2/2003" ); // Thu Jan 02 2003 + * cultureService.culture( "fr" ); + * cultureService.parseDate( "1/2/2003" ); // Sat Feb 01 2003 + */ + parseDate: function (value, formats, culture) { + return globalize.parseDate(value, formats, culture || activeCulture); + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#addCultureInfo + * @methodOf w20CoreCulture.service:CultureService + * @param {String} [cultureName] If supplied it will create a culture with this name. + * @param {String} [baseCultureName] If supplied it will extend this culture to create the new one. + * @param {String} info The culture information to add, according to the culture object format. + * + * @description + * + * This method allows you to create a new culture based on an existing culture or add to existing culture info. + * If the optional argument `baseCultureName` is not supplied, it will extend the existing culture if it + * exists or create a new culture based on the default culture if it doesn't exist. If `cultureName` is not + * supplied, it will add the supplied info to the current culture. + */ + addCultureInfo: function (cultureName, baseCultureName, info) { + return globalize.addCultureInfo(cultureName, baseCultureName, info); + }, + + /** + * @ngdoc function + * @name w20CoreCulture.service:CultureService#displayName + * @methodOf w20CoreCulture.service:CultureService + * @param {String} object The object to compute the display name of. + * @param {String} [values] The values used to localize the i18n key if any. + * @returns {String} The display name of the object. + * + * @description + * + * Compute the display name of an object by following these steps: + * + * * Return its `label` attribute if it exists, or, + * * Return the localized form of its `i18n` attribute if it exists, or, + * * Return an empty string. + */ + displayName: function (object, values) { + if (typeof object.label !== 'undefined') { + return object.label; + } + else if (typeof object.i18n !== 'undefined') { + return service.localize(object.i18n, values); + } + else { + return ''; + } } - } - }; + }; return service; }]); @@ -1247,19 +1281,6 @@ define([ angularModules: ['w20CoreCulture'], lifecycle: { pre: function (modules, fragments, callback) { - allBundles = {}; - availableCultures = []; - availableCultureObjects = []; - loadedCultures = []; - - function addBundles(language, newBundles) { - if (typeof allBundles[language] === 'undefined') { - allBundles[language] = []; - } - - allBundles[language] = allBundles[language].concat(newBundles); - } - // gather all fragments bundles and add them to the configuration _.each(fragments || {}, function (fragment) { if (typeof fragment.definition.i18n === 'string') { @@ -1279,7 +1300,6 @@ define([ }), function (elt) { return '{globalize}/cultures/globalize.culture.' + elt; }), function () { - availableCultures = _.pluck(_.filter(globalize.cultures, function (elt, key) { return key !== 'default' && (key !== 'en' || _.contains(config.available, 'en')); }), 'name'); @@ -1288,29 +1308,17 @@ define([ return globalize.cultures[name]; }); - if (window.localStorage) { - persistedCulture = localStorage.getItem('w20.state.' + w20.fragments['w20-core'].configuration.modules.application.id + '.culture'); - if (persistedCulture) { - persistedCulture = globalize.findClosestCulture(JSON.parse(persistedCulture).first); - persistedCulture = _.contains(availableCultureObjects, persistedCulture) ? persistedCulture : undefined; - } - } - if (typeof config['default'] !== 'undefined') { defaultCulture = globalize.findClosestCulture(config['default']) || defaultCulture; - } - else { + } else { defaultCulture = globalize.findClosestCulture(window.navigator.language || window.navigator.userLanguage) || defaultCulture; } - w20.console.log('Available cultures: ' + availableCultures); - - var culturesToLoad = [persistedCulture || defaultCulture]; - if (config.translationFallback === true) { - culturesToLoad.push(defaultCulture); + var bundlesToLoad = [getPreferredCulture()]; + if (translationFallback === true) { + bundlesToLoad.push(defaultCulture); } - - $.when.apply(undefined, _.uniq(culturesToLoad, false, 'name').map(loadCultureBundles)).then(function (persistedCulture, defaultCulture) { + $.when.apply(undefined, _.uniq(bundlesToLoad, false, 'name').map(loadCultureBundles)).then(function (persistedCulture, defaultCulture) { switchCulture(persistedCulture || defaultCulture).done(function (culture) { w20CoreCulture.config(['$provide', function ($provide) { // define $locale values based on default culture diff --git a/modules/env.js b/modules/env.js index 4835c9f..3639963 100644 --- a/modules/env.js +++ b/modules/env.js @@ -43,7 +43,7 @@ define([ * * */ - var w20CoreEnv = angular.module('w20CoreEnv', [ 'w20CoreSecurity', 'w20CoreApplication' ]), + var w20CoreEnv = angular.module('w20CoreEnv', ['w20CoreSecurity', 'w20CoreApplication']), config = module && module.config() || {}; /** @@ -81,7 +81,7 @@ define([ * The StateService provides key/value storage for data that needs to be persisted across sessions. * */ - w20CoreEnv.factory('StateService', [ '$rootScope', '$log', '$window', 'ApplicationService', function ($rootScope, $log, $window, applicationService) { + w20CoreEnv.factory('StateService', ['$rootScope', '$log', '$window', 'ApplicationService', function ($rootScope, $log, $window, applicationService) { var states = {}; return { @@ -114,7 +114,7 @@ define([ throw new Error('Key argument is required for using a state, got undefined'); } - var prefix = 'w20.state.' + applicationService.applicationId + '.' + namespace, + var prefix = 'w20.' + applicationService.applicationId + '.state.' + namespace, storage = session ? $window.sessionStorage : $window.localStorage; if (!storage) { @@ -172,7 +172,7 @@ define([ } if (typeof states[namespace] === 'undefined') { - states[namespace] = $window.localStorage.getItem('w20.state.' + applicationService.applicationId + '.' + namespace) || {}; + states[namespace] = $window.localStorage.getItem('w20.' + applicationService.applicationId + '.state.' + namespace) || {}; } return _.keys(states[namespace]); @@ -190,7 +190,7 @@ define([ * The PreferencesService provides an abstraction over the StateService dedicated to application preference storage. * */ - w20CoreEnv.factory('PreferencesService', [ 'StateService', + w20CoreEnv.factory('PreferencesService', ['StateService', function (stateService) { var preferences = {}, icons = {}, meta = {}; @@ -323,7 +323,7 @@ define([ * uses AngularJS events on the root scope. * */ - w20CoreEnv.factory('EventService', [ '$rootScope', '$injector', function ($rootScope, $injector) { + w20CoreEnv.factory('EventService', ['$rootScope', '$injector', function ($rootScope, $injector) { var viewListeners = []; $rootScope.$on('$routeChangeSuccess', function () { @@ -407,7 +407,7 @@ define([ return deregisterFn; } }; - } ]); + }]); /** * @ngdoc service @@ -422,7 +422,7 @@ define([ * * unknown : the application connectivity state is unknown. * */ - w20CoreEnv.factory('ConnectivityService', [ '$window', '$log', 'EventService', function ($window, $log, eventService) { + w20CoreEnv.factory('ConnectivityService', ['$window', '$log', 'EventService', function ($window, $log, eventService) { var beforeSendTime, lastState = { httpStatus: undefined, online: undefined, @@ -516,15 +516,15 @@ define([ return lastState; } }; - } ]); + }]); - w20CoreEnv.run([ 'ConnectivityService', '$window', '$injector', '$rootScope', function (connectivityService, $window, $injector, $rootScope) { + w20CoreEnv.run(['ConnectivityService', '$window', '$injector', '$rootScope', function (connectivityService, $window, $injector, $rootScope) { w20.injector = $injector; Object.getPrototypeOf($rootScope).$safeApply = function (fn) { fn = fn || function () { - }; + }; if (this.$$phase) { fn(); } @@ -541,9 +541,9 @@ define([ }); connectivityService.check(); - } ]); + }]); return { - angularModules: [ 'w20CoreEnv' ] + angularModules: ['w20CoreEnv'] }; }); diff --git a/modules/security.js b/modules/security.js index e089276..29e8987 100644 --- a/modules/security.js +++ b/modules/security.js @@ -31,8 +31,6 @@ define([ * * Authentication is handled through authentication providers. You can register additional providers with the API. * - * BasicAuthentication provider is built-in and handles http basic authentication. - * * Configuration * ------------- * @@ -61,53 +59,68 @@ define([ * } * } * + * Basic authentication + * -------------------- + * + * A provider handling basic authentication is built-in and available under the name `BasicAuthentication`. Its + * configuration in the fragment definition is as follows: + * + * "security" : { + * "provider" : "BasicAuthentication", + * "config" : { + * "authentication": "url/of/the/authentication/resource", + * "authorizations" : "url/of/the/authorizations/resource", + * "clearCredentials": true|false + * } + * } + * + * The authentication resource is where the basic authentication challenge must take place: + * + * * The first request issued to this resource is a GET without credentials, + * * The resource triggers the credentials input dialog of the browser by returning a 401 status code, + * * The browser issues a second GET request with the entered credentials to the resource which will return either + * a success status code (200 or 204) if the credentials are valid, or return a 401 status code again if credentials + * are invalid. + * + * Upon logout a DELETE request is made to the authentication resource which must invalidate the server-side security + * session for the authenticated subject. The resource must return a success status code (200 or 204) upon successful + * logout. A best-effort try is made to force the browser to forget the credentials but keep in mind that this is not + * standardized across browsers and may not work under some circumstances. To disable this attempt, specify `false` + * in the `clearCredentials` option. + * + * The authorizations resource is requested after successful authentication to return the profile of the authenticated + * subject along with its authorizations. */ var w20CoreSecurity = angular.module('w20CoreSecurity', ['w20CoreEnv', 'ngResource']), config = module && module.config() || {}, allProviders = {}, allRealms = {}; - var BasicAuthenticationProvider = ['$resource', '$document', '$q', function ($resource, $document, $q) { + var BasicAuthenticationProvider = ['$resource', '$window', '$q', function ($resource, $window, $q) { var AuthenticationResource, AuthorizationsResource, realm, authenticationUrl, clearCredentials; - function doCleanCredentials(loginUrl) { - var done; - - // For IE/Edge browsers - if ($document.execCommand) { - done = $document.execCommand('ClearAuthenticationCache', 'false'); - } - - // Others - if (!done) { - $.ajax({ - type: 'GET', - url: loginUrl, - async: true, - username: 'logmeout', - password: '123456', - headers: { Authorization: 'Basic xxx' } - }); + function randomString(length) { + var chars = []; + var possible = 'abcdefghijklmnopqrstuvwxyz0123456789'; + for (var i = 0; i < length; i++) { + chars[i] = possible.charAt(Math.floor(Math.random() * possible.length)); } + return chars.join(''); } return { setConfig: function (providerConfig) { - - clearCredentials = providerConfig.clearCredentials; + clearCredentials = providerConfig.clearCredentials || true; if (typeof providerConfig.authentication === 'undefined') { throw new Error('Authentication URL is required for BasicAuthentication provider, got undefined'); } - authenticationUrl = require.toUrl(providerConfig.authentication).replace(/:(?!\/\/)/, '\\:'); - AuthenticationResource = $resource(authenticationUrl); if (typeof providerConfig.authorizations === 'undefined') { throw new Error('Authorizations URL is required for BasicAuthentication provider, got undefined'); } - AuthorizationsResource = $resource(require.toUrl(providerConfig.authorizations).replace(/:(?!\/\/)/, '\\:')); }, @@ -141,7 +154,18 @@ define([ var deferred = $q.defer(); AuthenticationResource.remove(function () { if (clearCredentials) { - doCleanCredentials(authenticationUrl); + if (!$window.document.execCommand('ClearAuthenticationCache', 'false')) { + $.ajax({ + type: 'GET', + url: authenticationUrl, + async: true, + username: randomString(8), + password: randomString(8), + headers: { + Authorization: 'Basic ' + randomString(20) + } + }); + } } deferred.resolve(realm); }, function () { diff --git a/specs/culture.spec.js b/specs/culture.spec.js index 7efedb6..725a068 100644 --- a/specs/culture.spec.js +++ b/specs/culture.spec.js @@ -10,8 +10,9 @@ define([ '{angular}/angular', 'w20', '{w20-core}/modules/culture', + '{w20-core}/modules/application', '{angular-mocks}/angular-mocks' -], function (angular, w20, culture) { +], function (angular, w20, culture, application) { 'use strict'; describe('The Culture Service', function () { @@ -68,13 +69,13 @@ define([ expect($rootScope.$emit).toHaveBeenCalledWith('w20.culture.culture-changed', cultureService.culture()); }); - it('should persist the culture name to local storage when switching culture', function(done) { + it('should persist the culture name to local storage when switching culture', function (done) { expect(cultureService.culture().name).toBe('en-GB'); - expect(localStorage.getItem('w20.state.' + w20.fragments['w20-core'].configuration.modules.application.id + '.culture')).toBeNull(); + expect(localStorage.getItem('w20.' + application.id + '.preferred-culture')).toBeNull(); var unregister = $rootScope.$on('w20.culture.culture-changed', function () { expect(cultureService.culture().name).toBe('fr-FR'); - expect(JSON.parse(localStorage.getItem('w20.state.' + w20.fragments['w20-core'].configuration.modules.application.id + '.culture'))).toEqual({first:cultureService.culture().name}); + expect(localStorage.getItem('w20.' + application.id + '.preferred-culture')).toEqual(cultureService.culture().name); unregister(); done(); }); diff --git a/test-main.js b/test-main.js index 9699c9c..1df9054 100644 --- a/test-main.js +++ b/test-main.js @@ -27,7 +27,8 @@ window.w20 = { 'en-GB', 'fr-FR' ], - default: 'en-GB' + default: 'en-GB', + translationFallback: true }, ui: { 'expandedRouteCategories': ['category1.category11']