Skip to content

Commit

Permalink
feat(i18n): Add fallback lang support
Browse files Browse the repository at this point in the history
Adding the ability to have a fallback language in order to improve
usability for languages that have not been fully translated.

BREAKING CHANGE: getSafeText() will now return [MISSING] + the path to
the missing property or the fallback value if the property is available
on the fallback language.

fix #6396
  • Loading branch information
Portugal, Marcelo authored and mportuga committed Jun 19, 2018
1 parent 5ea8e54 commit 0e47f10
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 46 deletions.
15 changes: 11 additions & 4 deletions misc/tutorial/104_i18n.ngdoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,20 @@ support. By default ui-grid.base.js will contain just the english language, in o
</file>
<file name="index.html">
<div ng-controller="MainCtrl as $ctrl">
<select id="langDropdown" ng-model="$ctrl.lang" ng-options="l for l in $ctrl.langs"></select><br>
<select id="langDropdown" ng-model="$ctrl.lang" ng-options="l for l in $ctrl.langs"></select>
<br />

<div ui-i18n="{{$ctrl.lang}}">
<p>Using attribute:</p>
<h2>Using attribute:</h2>
<p ui-t="groupPanel.description"></p>
<br/>
<p>Using Filter:</p>

<h2>Using attribute 2:</h2>
<p ui-translate>groupPanel.description</p>

<h2>Using Filter that updates with language:</h2>
<p>{{"groupPanel.description" | t:$ctrl.lang}}</p>

<h2>Using Filter that does not update after load:</h2>
<p>{{"groupPanel.description" | t}}</p>

<p>Click the header menu to see language.</p>
Expand Down
120 changes: 82 additions & 38 deletions src/js/i18n/ui-i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
};

Expand Down Expand Up @@ -157,45 +172,63 @@
* </pre>
*/
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('.');
var current = trans;

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;

},

/**
* @ngdoc service
* @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
* <pre>
* i18nService.setCurrentLang('fr');
* </pre>
*/

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
* <pre>
* i18nService.setFallbackLang('en');
* </pre>
*/
setFallbackLang: function (lang) {
if (lang) {
langCache.setFallback(lang);
}
},

Expand All @@ -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 {
Expand All @@ -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]);
});
})();
86 changes: 86 additions & 0 deletions test/unit/i18n/directives.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,121 @@ describe('i18n Directives', function() {
element = angular.element('<div ui-i18n="lang"><p ui-translate="search.placeholder"></p></div>');
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('<div ui-i18n="en"><p ui-translate>search.placeholder</p></div>');
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('<div ui-i18n="{{lang}}"><p ui-translate="search.placeholder"></p></div>');
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('<div ui-i18n="{{lang}}"><p ui-translate="{{property}}"></p></div>');
recompile();

expect(element.find('p').text()).toBe('Search...');
});
it('should get missing text for missing property', function() {
element = angular.element('<div ui-i18n="en"><p ui-translate="search.bad.text"></p></div>');
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('<div ui-i18n="en"><p ui-t="search.placeholder"></p></div>');
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('<div ui-i18n="en"><p ui-t>search.placeholder</p></div>');
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('<div ui-i18n="{{lang}}"><p ui-t="search.placeholder"></p></div>');
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('<div ui-i18n="{{lang}}"><p ui-t="{{property}}"></p></div>');
recompile();

expect(element.find('p').text()).toBe('Search...');
});
it('should get missing text for missing property', function() {
element = angular.element('<div ui-i18n="en"><p ui-t="search.bad.text"></p></div>');
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('<div ui-i18n="en"><p>{{"search.placeholder" | t}}</p></div>');
recompile();

expect(element.find('p').text()).toBe('Search...');
});
it('should get missing text for missing property', function() {
element = angular.element('<div ui-i18n="en"><p>{{"search.bad.text" | t}}</p></div>');
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('<div ui-i18n="en"><p>{{"search.placeholder" | uiTranslate}}</p></div>');
recompile();

expect(element.find('p').text()).toBe('Search...');
});
it('should translate even without the ui-i18n directive', function() {
element = angular.element('<div><p>{{"search.placeholder" | uiTranslate:"en"}}</p></div>');
recompile();

expect(element.find('p').text()).toBe('Search...');
});
it('should get missing text for missing property', function() {
element = angular.element('<div ui-i18n="en"><p>{{"search.bad.text" | uiTranslate}}</p></div>');
recompile();

expect(element.find('p').text()).toBe('[MISSING]search.bad.text');
});
});
});
Loading

0 comments on commit 0e47f10

Please sign in to comment.