diff --git a/misc/tutorial/104_i18n.ngdoc b/misc/tutorial/104_i18n.ngdoc index 733297e722..6ebfed9b4f 100644 --- a/misc/tutorial/104_i18n.ngdoc +++ b/misc/tutorial/104_i18n.ngdoc @@ -59,13 +59,20 @@ support. By default ui-grid.base.js will contain just the english language, in o
-
+ +
-

Using attribute:

+

Using attribute:

-
-

Using Filter:

+ +

Using attribute 2:

+

groupPanel.description

+ +

Using Filter that updates with language:

+

{{"groupPanel.description" | t:$ctrl.lang}}

+ +

Using Filter that does not update after load:

{{"groupPanel.description" | t}}

Click the header menu to see language.

diff --git a/src/js/i18n/ui-i18n.js b/src/js/i18n/ui-i18n.js index b071913c04..f6e004ed4a 100644 --- a/src/js/i18n/ui-i18n.js +++ b/src/js/i18n/ui-i18n.js @@ -53,8 +53,17 @@ var langCache = { _langs: {}, current: null, + fallback: i18nConstants.DEFAULT_LANG, get: function (lang) { - return this._langs[lang.toLowerCase()]; + var self = this, + fallbackLang = self.getFallbackLang(); + + if (lang !== self.fallback) { + return angular.merge({}, self._langs[fallbackLang], + self._langs[lang.toLowerCase()]); + } + + return self._langs[lang.toLowerCase()]; }, add: function (lang, strings) { var lower = lang.toLowerCase(); @@ -78,8 +87,14 @@ setCurrent: function (lang) { this.current = lang.toLowerCase(); }, + setFallback: function (lang) { + this.fallback = lang.toLowerCase(); + }, getCurrentLang: function () { return this.current; + }, + getFallbackLang: function () { + return this.fallback.toLowerCase(); } }; @@ -157,11 +172,12 @@ * */ getSafeText: function (path, lang) { - var language = lang || service.getCurrentLang(); - var trans = langCache.get(language); + var language = lang || service.getCurrentLang(), + trans = langCache.get(language), + missing = i18nConstants.MISSING + path; if (!trans) { - return i18nConstants.MISSING; + return missing; } var paths = path.split('.'); @@ -169,14 +185,13 @@ for (var i = 0; i < paths.length; ++i) { if (current[paths[i]] === undefined || current[paths[i]] === null) { - return i18nConstants.MISSING; + return missing; } else { current = current[paths[i]]; } } return current; - }, /** @@ -184,18 +199,36 @@ * @name setCurrentLang * @methodOf ui.grid.i18n.service:i18nService * @description sets the current language to use in the application - * $broadcasts the i18nConstants.UPDATE_EVENT on the $rootScope + * $broadcasts and $emits the i18nConstants.UPDATE_EVENT on the $rootScope * @param {string} lang to set * @example *
          * i18nService.setCurrentLang('fr');
          * 
*/ - setCurrentLang: function (lang) { if (lang) { langCache.setCurrent(lang); $rootScope.$broadcast(i18nConstants.UPDATE_EVENT); + $rootScope.$emit(i18nConstants.UPDATE_EVENT); + } + }, + + /** + * @ngdoc service + * @name setFallbackLang + * @methodOf ui.grid.i18n.service:i18nService + * @description sets the fallback language to use in the application. + * The default fallback language is english. + * @param {string} lang to set + * @example + *
+         * i18nService.setFallbackLang('en');
+         * 
+ */ + setFallbackLang: function (lang) { + if (lang) { + langCache.setFallback(lang); } }, @@ -212,15 +245,23 @@ langCache.setCurrent(lang); } return lang; - } + }, + /** + * @ngdoc service + * @name getFallbackLang + * @methodOf ui.grid.i18n.service:i18nService + * @description returns the fallback language used in the application + */ + getFallbackLang: function () { + return langCache.getFallbackLang(); + } }; return service; - }]); - var localeDirective = function (i18nService, i18nConstants) { + function localeDirective(i18nService, i18nConstants) { return { compile: function () { return { @@ -241,64 +282,67 @@ }; } }; - }; + } module.directive('uiI18n', ['i18nService', 'i18nConstants', localeDirective]); // directive syntax - var uitDirective = function ($parse, i18nService, i18nConstants) { + function uitDirective(i18nService, i18nConstants) { return { restrict: 'EA', compile: function () { return { pre: function ($scope, $elm, $attrs) { - var alias1 = DIRECTIVE_ALIASES[0], - alias2 = DIRECTIVE_ALIASES[1]; - var token = $attrs[alias1] || $attrs[alias2] || $elm.html(); - var missing = i18nConstants.MISSING + token; - var observer; + var listener, observer, prop, + alias1 = DIRECTIVE_ALIASES[0], + alias2 = DIRECTIVE_ALIASES[1], + token = $attrs[alias1] || $attrs[alias2] || $elm.html(); + + function translateToken(property) { + var safeText = i18nService.getSafeText(property); + + $elm.html(safeText); + } + if ($attrs.$$observers) { - var prop = $attrs[alias1] ? alias1 : alias2; + prop = $attrs[alias1] ? alias1 : alias2; observer = $attrs.$observe(prop, function (result) { if (result) { - $elm.html($parse(result)(i18nService.getCurrentLang()) || missing); + translateToken(result); } }); } - var getter = $parse(token); - var listener = $scope.$on(i18nConstants.UPDATE_EVENT, function (evt) { + + listener = $scope.$on(i18nConstants.UPDATE_EVENT, function() { if (observer) { observer($attrs[alias1] || $attrs[alias2]); } else { // set text based on i18n current language - $elm.html(getter(i18nService.get()) || missing); + translateToken(token); } }); $scope.$on('$destroy', listener); - $elm.html(getter(i18nService.get()) || missing); + translateToken(token); } }; } }; - }; + } - angular.forEach( DIRECTIVE_ALIASES, function ( alias ) { - module.directive( alias, ['$parse', 'i18nService', 'i18nConstants', uitDirective] ); - } ); + angular.forEach(DIRECTIVE_ALIASES, function ( alias ) { + module.directive(alias, ['i18nService', 'i18nConstants', uitDirective]); + }); // optional filter syntax - var uitFilter = function ($parse, i18nService, i18nConstants) { - return function (data) { - var getter = $parse(data); + function uitFilter(i18nService) { + return function (data, lang) { // set text based on i18n current language - return getter(i18nService.get()) || i18nConstants.MISSING + data; + return i18nService.getSafeText(data, lang); }; - }; - - angular.forEach( FILTER_ALIASES, function ( alias ) { - module.filter( alias, ['$parse', 'i18nService', 'i18nConstants', uitFilter] ); - } ); - + } + angular.forEach(FILTER_ALIASES, function ( alias ) { + module.filter(alias, ['i18nService', uitFilter]); + }); })(); diff --git a/test/unit/i18n/directives.spec.js b/test/unit/i18n/directives.spec.js index ad684c2d53..b650204ad9 100644 --- a/test/unit/i18n/directives.spec.js +++ b/test/unit/i18n/directives.spec.js @@ -34,35 +34,121 @@ describe('i18n Directives', function() { element = angular.element('

'); recompile(); }); + afterEach(function() { + element.remove(); + }); it('should translate', function() { expect(element.find('p').text()).toBe('Search...'); }); + it('should translate even if token is on the html instead of the attribute', function() { + element = angular.element('

search.placeholder

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should be able to interpolate languages and default to english when the language is not defined', function() { + element = angular.element('

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should be able to interpolate properties', function() { + scope.lang = 'en'; + scope.property = 'search.placeholder'; + element = angular.element('

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should get missing text for missing property', function() { + element = angular.element('

'); + recompile(); + + expect(element.find('p').text()).toBe('[MISSING]search.bad.text'); + }); }); describe('ui-t directive', function() { + afterEach(function() { + element.remove(); + }); it('should translate', function() { element = angular.element('

'); recompile(); expect(element.find('p').text()).toBe('Search...'); }); + it('should translate even if token is on the html instead of the attribute', function() { + element = angular.element('

search.placeholder

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should be able to interpolate languages and default to english when the language is not defined', function() { + element = angular.element('

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should be able to interpolate properties', function() { + scope.lang = 'en'; + scope.property = 'search.placeholder'; + element = angular.element('

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should get missing text for missing property', function() { + element = angular.element('

'); + recompile(); + + expect(element.find('p').text()).toBe('[MISSING]search.bad.text'); + + $rootScope.$broadcast('$uiI18n'); + + expect(element.find('p').text()).toBe('[MISSING]search.bad.text'); + }); }); describe('t filter', function() { + afterEach(function() { + element.remove(); + }); it('should translate', function() { element = angular.element('

{{"search.placeholder" | t}}

'); recompile(); expect(element.find('p').text()).toBe('Search...'); }); + it('should get missing text for missing property', function() { + element = angular.element('

{{"search.bad.text" | t}}

'); + recompile(); + + expect(element.find('p').text()).toBe('[MISSING]search.bad.text'); + }); }); describe('uiTranslate filter', function() { + afterEach(function() { + element.remove(); + }); it('should translate', function() { element = angular.element('

{{"search.placeholder" | uiTranslate}}

'); recompile(); expect(element.find('p').text()).toBe('Search...'); }); + it('should translate even without the ui-i18n directive', function() { + element = angular.element('

{{"search.placeholder" | uiTranslate:"en"}}

'); + recompile(); + + expect(element.find('p').text()).toBe('Search...'); + }); + it('should get missing text for missing property', function() { + element = angular.element('

{{"search.bad.text" | uiTranslate}}

'); + recompile(); + + expect(element.find('p').text()).toBe('[MISSING]search.bad.text'); + }); }); }); diff --git a/test/unit/i18n/i18nService.spec.js b/test/unit/i18n/i18nService.spec.js index 78a19597b6..a258d37b3c 100644 --- a/test/unit/i18n/i18nService.spec.js +++ b/test/unit/i18n/i18nService.spec.js @@ -19,6 +19,15 @@ describe('i18nService', function () { expect(i18nService.getCurrentLang()).toBe('fr'); expect(i18nService.get().search.placeholder).toBe('Recherche...'); }); + describe('get', function() { + it('should return the language passed to it if a language is passed', function() { + expect(i18nService.get('fr').search.placeholder).toBe('Recherche...'); + }); + it('should the current language no language is passed to it', function() { + i18nService.setCurrentLang('en'); + expect(i18nService.get().search.placeholder).toBe('Search...'); + }); + }); describe('add', function() { it('should add a language when langs is a string', function() { i18nService.add('tst',{test:'testlang'}); @@ -40,18 +49,58 @@ describe('i18nService', function () { }); it('should return all langs', function() { var langs = i18nService.getAllLangs(); + expect(langs.length).toBeGreaterThan(8); }); + describe('fallback lang', function() { + it('getFallbackLang should default to english when a fallback language is not set', function() { + expect(i18nService.getFallbackLang()).toEqual(i18nConstants.DEFAULT_LANG); + }); + it('getFallbackLang should return the user set language when the user calls setFallbackLang', function() { + var fallback = 'es'; + i18nService.setFallbackLang(fallback); + expect(i18nService.getFallbackLang()).toEqual(fallback); + }); + it('getFallbackLang should return english when the user calls setFallbackLang with an empty string', function() { + var fallback = ''; + + i18nService.setFallbackLang(fallback); + expect(i18nService.getFallbackLang()).toEqual(i18nConstants.DEFAULT_LANG); + }); + }); describe('getSafeText', function() { + beforeEach(function() { + i18nService.setCurrentLang('en'); + i18nService.setFallbackLang('en'); + }); it('should get safe text when text is defined', function() { expect(i18nService.getSafeText('search.placeholder')).toBe('Search...'); }); - it('should get safe text for missing property', function() { - expect(i18nService.getSafeText('search.bad.text')).toBe('[MISSING]'); + it('should get missing text for missing property', function() { + var badText = 'search.bad.text'; + + expect(i18nService.getSafeText(badText)).toBe(i18nConstants.MISSING + badText); + }); + it('should get fallback text when language is missing or nonexistent', function() { + expect(i18nService.getSafeText('search.placeholder', 'valerian')).toBe('Search...'); + }); + it('should get missing text when language is missing or nonexistent and there is no fallback', function() { + var badText = 'bad.text'; + + expect(i18nService.getSafeText(badText, 'valerian')).toBe(i18nConstants.MISSING + badText); + }); + it('should get missing text when language is missing or nonexistent and the fallback language is the same', function() { + var missingProperty = 'search.placeholder'; + + i18nService.setFallbackLang('valerian'); + expect(i18nService.getSafeText(missingProperty, 'valerian')).toBe(i18nConstants.MISSING + missingProperty); }); - it('should get missing text when language is missing or nonexistent', function() { - expect(i18nService.getSafeText('search.placeholder', 'valerian')).toBe(i18nConstants.MISSING); + it('should get missing text when language is missing or nonexistent and the fallback language is also missing it', function() { + var missingProperty = 'search.placeholder'; + + i18nService.setFallbackLang('orcish'); + expect(i18nService.getSafeText(missingProperty, 'valerian')).toBe(i18nConstants.MISSING + missingProperty); }); }); });