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:
- *
- * - n for number
- * - d for decimal digits
- * - p for percentage
- * - c for currency
- *
- *
- * 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
- *
- *
- *
- * Format |
- * Meaning |
- * "en-US" |
- *
- *
- * f |
- * Long Date, Short Time |
- * dddd, MMMM dd, yyyy h:mm tt |
- *
- *
- * F |
- * Long Date, Long Time |
- * dddd, MMMM dd, yyyy h:mm:ss tt |
- *
- *
- * t |
- * Short Time |
- * h:mm tt |
- *
- *
- * T |
- * Long Time |
- * h:mm:ss tt |
- *
- *
- * d |
- * Short Date |
- * M/d/yyyy |
- *
- *
- * D |
- * Long Date |
- * dddd, MMMM dd, yyyy |
- *
- *
- * Y |
- * Month/Year |
- * MMMM, yyyy |
- *
- *
- * M |
- * Month/Day |
- * MMMM 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:
- *
- *
- * Token |
- * Meaning |
- * Example |
- *
- *
- * d |
- * Day of month (no leading zero) |
- * 5 |
- *
- *
- * dd |
- * Day of month (leading zero) |
- * 05 |
- *
- *
- * ddd |
- * Day name (abbreviated) |
- * Sat |
- *
- *
- * dddd |
- * Day name (full) |
- * Saturday |
- *
- *
- * M |
- * Month of year (no leading zero) |
- * 9 |
- *
- *
- * MM |
- * Month of year (leading zero) |
- * 09 |
- *
- *
- * MMM |
- * Month name (abbreviated) |
- * Sep |
- *
- *
- * MMMM |
- * Month name (full) |
- * September |
- *
- *
- * yy |
- * Year (two digits) |
- * 55 |
- *
- *
- * yyyy |
- * Year (four digits) |
- * 1955 |
- *
- *
- * 'literal' |
- * Literal Text |
- * 'of the clock' |
- *
- *
- * \' |
- * Single Quote |
- * 'o'\''clock' |
- *
- *
- * m |
- * Minutes (no leading zero) |
- * 9 |
- *
- *
- * mm |
- * Minutes (leading zero) |
- * 09 |
- *
- *
- * h |
- * Hours (12 hour time, no leading zero) |
- * 6 |
- *
- *
- * hh |
- * Hours (12 hour time, leading zero) |
- * 06 |
- *
- *
- * H |
- * Hours (24 hour time, no leading zero) |
- * 5 (5am) 15 (3pm) |
- *
- *
- * HH |
- * Hours (24 hour time, leading zero) |
- * 05 (5am) 15 (3pm) |
- *
- *
- * s |
- * Seconds (no leading zero) |
- * 9 |
- *
- *
- * ss |
- * Seconds (leading zero) |
- * 09 |
- *
- *
- * f |
- * Deciseconds |
- * 1 |
- *
- *
- * ff |
- * Centiseconds |
- * 11 |
- *
- *
- * fff |
- * Milliseconds |
- * 111 |
- *
- *
- * t |
- * AM/PM indicator (first letter) |
- * A or P |
- *
- *
- * tt |
- * AM/PM indicator (full) |
- * AM or PM |
- *
- *
- * z |
- * Timezone offset (hours only, no leading zero) |
- * -8 |
- *
- *
- * zz |
- * Timezone offset (hours only, leading zero) |
- * -08 |
- *
- *
- * zzz |
- * Timezone offset (full hours/minutes) |
- * -08:00 |
- *
- *
- * g or gg |
- * Era name |
- * A.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:
+ *
+ * - n for number
+ * - d for decimal digits
+ * - p for percentage
+ * - c for currency
+ *
+ *
+ * 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
+ *
+ *
+ *
+ * Format |
+ * Meaning |
+ * "en-US" |
+ *
+ *
+ * f |
+ * Long Date, Short Time |
+ * dddd, MMMM dd, yyyy h:mm tt |
+ *
+ *
+ * F |
+ * Long Date, Long Time |
+ * dddd, MMMM dd, yyyy h:mm:ss tt |
+ *
+ *
+ * t |
+ * Short Time |
+ * h:mm tt |
+ *
+ *
+ * T |
+ * Long Time |
+ * h:mm:ss tt |
+ *
+ *
+ * d |
+ * Short Date |
+ * M/d/yyyy |
+ *
+ *
+ * D |
+ * Long Date |
+ * dddd, MMMM dd, yyyy |
+ *
+ *
+ * Y |
+ * Month/Year |
+ * MMMM, yyyy |
+ *
+ *
+ * M |
+ * Month/Day |
+ * MMMM 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:
+ *
+ *
+ * Token |
+ * Meaning |
+ * Example |
+ *
+ *
+ * d |
+ * Day of month (no leading zero) |
+ * 5 |
+ *
+ *
+ * dd |
+ * Day of month (leading zero) |
+ * 05 |
+ *
+ *
+ * ddd |
+ * Day name (abbreviated) |
+ * Sat |
+ *
+ *
+ * dddd |
+ * Day name (full) |
+ * Saturday |
+ *
+ *
+ * M |
+ * Month of year (no leading zero) |
+ * 9 |
+ *
+ *
+ * MM |
+ * Month of year (leading zero) |
+ * 09 |
+ *
+ *
+ * MMM |
+ * Month name (abbreviated) |
+ * Sep |
+ *
+ *
+ * MMMM |
+ * Month name (full) |
+ * September |
+ *
+ *
+ * yy |
+ * Year (two digits) |
+ * 55 |
+ *
+ *
+ * yyyy |
+ * Year (four digits) |
+ * 1955 |
+ *
+ *
+ * 'literal' |
+ * Literal Text |
+ * 'of the clock' |
+ *
+ *
+ * \' |
+ * Single Quote |
+ * 'o'\''clock' |
+ *
+ *
+ * m |
+ * Minutes (no leading zero) |
+ * 9 |
+ *
+ *
+ * mm |
+ * Minutes (leading zero) |
+ * 09 |
+ *
+ *
+ * h |
+ * Hours (12 hour time, no leading zero) |
+ * 6 |
+ *
+ *
+ * hh |
+ * Hours (12 hour time, leading zero) |
+ * 06 |
+ *
+ *
+ * H |
+ * Hours (24 hour time, no leading zero) |
+ * 5 (5am) 15 (3pm) |
+ *
+ *
+ * HH |
+ * Hours (24 hour time, leading zero) |
+ * 05 (5am) 15 (3pm) |
+ *
+ *
+ * s |
+ * Seconds (no leading zero) |
+ * 9 |
+ *
+ *
+ * ss |
+ * Seconds (leading zero) |
+ * 09 |
+ *
+ *
+ * f |
+ * Deciseconds |
+ * 1 |
+ *
+ *
+ * ff |
+ * Centiseconds |
+ * 11 |
+ *
+ *
+ * fff |
+ * Milliseconds |
+ * 111 |
+ *
+ *
+ * t |
+ * AM/PM indicator (first letter) |
+ * A or P |
+ *
+ *
+ * tt |
+ * AM/PM indicator (full) |
+ * AM or PM |
+ *
+ *
+ * z |
+ * Timezone offset (hours only, no leading zero) |
+ * -8 |
+ *
+ *
+ * zz |
+ * Timezone offset (hours only, leading zero) |
+ * -08 |
+ *
+ *
+ * zzz |
+ * Timezone offset (full hours/minutes) |
+ * -08:00 |
+ *
+ *
+ * g or gg |
+ * Era name |
+ * A.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']