Skip to content

Commit 0e47f10

Browse files
Portugal, Marcelomportuga
Portugal, Marcelo
authored andcommittedJun 19, 2018
feat(i18n): Add fallback lang support
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
1 parent 5ea8e54 commit 0e47f10

File tree

4 files changed

+232
-46
lines changed

4 files changed

+232
-46
lines changed
 

‎misc/tutorial/104_i18n.ngdoc

+11-4
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,20 @@ support. By default ui-grid.base.js will contain just the english language, in o
5959
</file>
6060
<file name="index.html">
6161
<div ng-controller="MainCtrl as $ctrl">
62-
<select id="langDropdown" ng-model="$ctrl.lang" ng-options="l for l in $ctrl.langs"></select><br>
62+
<select id="langDropdown" ng-model="$ctrl.lang" ng-options="l for l in $ctrl.langs"></select>
63+
<br />
6364

6465
<div ui-i18n="{{$ctrl.lang}}">
65-
<p>Using attribute:</p>
66+
<h2>Using attribute:</h2>
6667
<p ui-t="groupPanel.description"></p>
67-
<br/>
68-
<p>Using Filter:</p>
68+
69+
<h2>Using attribute 2:</h2>
70+
<p ui-translate>groupPanel.description</p>
71+
72+
<h2>Using Filter that updates with language:</h2>
73+
<p>{{"groupPanel.description" | t:$ctrl.lang}}</p>
74+
75+
<h2>Using Filter that does not update after load:</h2>
6976
<p>{{"groupPanel.description" | t}}</p>
7077

7178
<p>Click the header menu to see language.</p>

‎src/js/i18n/ui-i18n.js

+82-38
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,17 @@
5353
var langCache = {
5454
_langs: {},
5555
current: null,
56+
fallback: i18nConstants.DEFAULT_LANG,
5657
get: function (lang) {
57-
return this._langs[lang.toLowerCase()];
58+
var self = this,
59+
fallbackLang = self.getFallbackLang();
60+
61+
if (lang !== self.fallback) {
62+
return angular.merge({}, self._langs[fallbackLang],
63+
self._langs[lang.toLowerCase()]);
64+
}
65+
66+
return self._langs[lang.toLowerCase()];
5867
},
5968
add: function (lang, strings) {
6069
var lower = lang.toLowerCase();
@@ -78,8 +87,14 @@
7887
setCurrent: function (lang) {
7988
this.current = lang.toLowerCase();
8089
},
90+
setFallback: function (lang) {
91+
this.fallback = lang.toLowerCase();
92+
},
8193
getCurrentLang: function () {
8294
return this.current;
95+
},
96+
getFallbackLang: function () {
97+
return this.fallback.toLowerCase();
8398
}
8499
};
85100

@@ -157,45 +172,63 @@
157172
* </pre>
158173
*/
159174
getSafeText: function (path, lang) {
160-
var language = lang || service.getCurrentLang();
161-
var trans = langCache.get(language);
175+
var language = lang || service.getCurrentLang(),
176+
trans = langCache.get(language),
177+
missing = i18nConstants.MISSING + path;
162178

163179
if (!trans) {
164-
return i18nConstants.MISSING;
180+
return missing;
165181
}
166182

167183
var paths = path.split('.');
168184
var current = trans;
169185

170186
for (var i = 0; i < paths.length; ++i) {
171187
if (current[paths[i]] === undefined || current[paths[i]] === null) {
172-
return i18nConstants.MISSING;
188+
return missing;
173189
} else {
174190
current = current[paths[i]];
175191
}
176192
}
177193

178194
return current;
179-
180195
},
181196

182197
/**
183198
* @ngdoc service
184199
* @name setCurrentLang
185200
* @methodOf ui.grid.i18n.service:i18nService
186201
* @description sets the current language to use in the application
187-
* $broadcasts the i18nConstants.UPDATE_EVENT on the $rootScope
202+
* $broadcasts and $emits the i18nConstants.UPDATE_EVENT on the $rootScope
188203
* @param {string} lang to set
189204
* @example
190205
* <pre>
191206
* i18nService.setCurrentLang('fr');
192207
* </pre>
193208
*/
194-
195209
setCurrentLang: function (lang) {
196210
if (lang) {
197211
langCache.setCurrent(lang);
198212
$rootScope.$broadcast(i18nConstants.UPDATE_EVENT);
213+
$rootScope.$emit(i18nConstants.UPDATE_EVENT);
214+
}
215+
},
216+
217+
/**
218+
* @ngdoc service
219+
* @name setFallbackLang
220+
* @methodOf ui.grid.i18n.service:i18nService
221+
* @description sets the fallback language to use in the application.
222+
* The default fallback language is english.
223+
* @param {string} lang to set
224+
* @example
225+
* <pre>
226+
* i18nService.setFallbackLang('en');
227+
* </pre>
228+
*/
229+
setFallbackLang: function (lang) {
230+
if (lang) {
231+
langCache.setFallback(lang);
199232
}
200233
},
201234

@@ -212,15 +245,23 @@
212245
langCache.setCurrent(lang);
213246
}
214247
return lang;
215-
}
248+
},
216249

250+
/**
251+
* @ngdoc service
252+
* @name getFallbackLang
253+
* @methodOf ui.grid.i18n.service:i18nService
254+
* @description returns the fallback language used in the application
255+
*/
256+
getFallbackLang: function () {
257+
return langCache.getFallbackLang();
258+
}
217259
};
218260

219261
return service;
220-
221262
}]);
222263

223-
var localeDirective = function (i18nService, i18nConstants) {
264+
function localeDirective(i18nService, i18nConstants) {
224265
return {
225266
compile: function () {
226267
return {
@@ -241,64 +282,67 @@
241282
};
242283
}
243284
};
244-
};
285+
}
245286

246287
module.directive('uiI18n', ['i18nService', 'i18nConstants', localeDirective]);
247288

248289
// directive syntax
249-
var uitDirective = function ($parse, i18nService, i18nConstants) {
290+
function uitDirective(i18nService, i18nConstants) {
250291
return {
251292
restrict: 'EA',
252293
compile: function () {
253294
return {
254295
pre: function ($scope, $elm, $attrs) {
255-
var alias1 = DIRECTIVE_ALIASES[0],
256-
alias2 = DIRECTIVE_ALIASES[1];
257-
var token = $attrs[alias1] || $attrs[alias2] || $elm.html();
258-
var missing = i18nConstants.MISSING + token;
259-
var observer;
296+
var listener, observer, prop,
297+
alias1 = DIRECTIVE_ALIASES[0],
298+
alias2 = DIRECTIVE_ALIASES[1],
299+
token = $attrs[alias1] || $attrs[alias2] || $elm.html();
300+
301+
function translateToken(property) {
302+
var safeText = i18nService.getSafeText(property);
303+
304+
$elm.html(safeText);
305+
}
306+
260307
if ($attrs.$$observers) {
261-
var prop = $attrs[alias1] ? alias1 : alias2;
308+
prop = $attrs[alias1] ? alias1 : alias2;
262309
observer = $attrs.$observe(prop, function (result) {
263310
if (result) {
264-
$elm.html($parse(result)(i18nService.getCurrentLang()) || missing);
311+
translateToken(result);
265312
}
266313
});
267314
}
268-
var getter = $parse(token);
269-
var listener = $scope.$on(i18nConstants.UPDATE_EVENT, function (evt) {
315+
316+
listener = $scope.$on(i18nConstants.UPDATE_EVENT, function() {
270317
if (observer) {
271318
observer($attrs[alias1] || $attrs[alias2]);
272319
} else {
273320
// set text based on i18n current language
274-
$elm.html(getter(i18nService.get()) || missing);
321+
translateToken(token);
275322
}
276323
});
277324
$scope.$on('$destroy', listener);
278325

279-
$elm.html(getter(i18nService.get()) || missing);
326+
translateToken(token);
280327
}
281328
};
282329
}
283330
};
284-
};
331+
}
285332

286-
angular.forEach( DIRECTIVE_ALIASES, function ( alias ) {
287-
module.directive( alias, ['$parse', 'i18nService', 'i18nConstants', uitDirective] );
288-
} );
333+
angular.forEach(DIRECTIVE_ALIASES, function ( alias ) {
334+
module.directive(alias, ['i18nService', 'i18nConstants', uitDirective]);
335+
});
289336

290337
// optional filter syntax
291-
var uitFilter = function ($parse, i18nService, i18nConstants) {
292-
return function (data) {
293-
var getter = $parse(data);
338+
function uitFilter(i18nService) {
339+
return function (data, lang) {
294340
// set text based on i18n current language
295-
return getter(i18nService.get()) || i18nConstants.MISSING + data;
341+
return i18nService.getSafeText(data, lang);
296342
};
297-
};
298-
299-
angular.forEach( FILTER_ALIASES, function ( alias ) {
300-
module.filter( alias, ['$parse', 'i18nService', 'i18nConstants', uitFilter] );
301-
} );
302-
343+
}
303344

345+
angular.forEach(FILTER_ALIASES, function ( alias ) {
346+
module.filter(alias, ['i18nService', uitFilter]);
347+
});
304348
})();

‎test/unit/i18n/directives.spec.js

+86
Original file line numberDiff line numberDiff line change
@@ -34,35 +34,121 @@ describe('i18n Directives', function() {
3434
element = angular.element('<div ui-i18n="lang"><p ui-translate="search.placeholder"></p></div>');
3535
recompile();
3636
});
37+
afterEach(function() {
38+
element.remove();
39+
});
3740
it('should translate', function() {
3841
expect(element.find('p').text()).toBe('Search...');
3942
});
43+
it('should translate even if token is on the html instead of the attribute', function() {
44+
element = angular.element('<div ui-i18n="en"><p ui-translate>search.placeholder</p></div>');
45+
recompile();
46+
47+
expect(element.find('p').text()).toBe('Search...');
48+
});
49+
it('should be able to interpolate languages and default to english when the language is not defined', function() {
50+
element = angular.element('<div ui-i18n="{{lang}}"><p ui-translate="search.placeholder"></p></div>');
51+
recompile();
52+
53+
expect(element.find('p').text()).toBe('Search...');
54+
});
55+
it('should be able to interpolate properties', function() {
56+
scope.lang = 'en';
57+
scope.property = 'search.placeholder';
58+
element = angular.element('<div ui-i18n="{{lang}}"><p ui-translate="{{property}}"></p></div>');
59+
recompile();
60+
61+
expect(element.find('p').text()).toBe('Search...');
62+
});
63+
it('should get missing text for missing property', function() {
64+
element = angular.element('<div ui-i18n="en"><p ui-translate="search.bad.text"></p></div>');
65+
recompile();
66+
67+
expect(element.find('p').text()).toBe('[MISSING]search.bad.text');
68+
});
4069
});
4170

4271
describe('ui-t directive', function() {
72+
afterEach(function() {
73+
element.remove();
74+
});
4375
it('should translate', function() {
4476
element = angular.element('<div ui-i18n="en"><p ui-t="search.placeholder"></p></div>');
4577
recompile();
4678

4779
expect(element.find('p').text()).toBe('Search...');
4880
});
81+
it('should translate even if token is on the html instead of the attribute', function() {
82+
element = angular.element('<div ui-i18n="en"><p ui-t>search.placeholder</p></div>');
83+
recompile();
84+
85+
expect(element.find('p').text()).toBe('Search...');
86+
});
87+
it('should be able to interpolate languages and default to english when the language is not defined', function() {
88+
element = angular.element('<div ui-i18n="{{lang}}"><p ui-t="search.placeholder"></p></div>');
89+
recompile();
90+
91+
expect(element.find('p').text()).toBe('Search...');
92+
});
93+
it('should be able to interpolate properties', function() {
94+
scope.lang = 'en';
95+
scope.property = 'search.placeholder';
96+
element = angular.element('<div ui-i18n="{{lang}}"><p ui-t="{{property}}"></p></div>');
97+
recompile();
98+
99+
expect(element.find('p').text()).toBe('Search...');
100+
});
101+
it('should get missing text for missing property', function() {
102+
element = angular.element('<div ui-i18n="en"><p ui-t="search.bad.text"></p></div>');
103+
recompile();
104+
105+
expect(element.find('p').text()).toBe('[MISSING]search.bad.text');
106+
107+
$rootScope.$broadcast('$uiI18n');
108+
109+
expect(element.find('p').text()).toBe('[MISSING]search.bad.text');
110+
});
49111
});
50112

51113
describe('t filter', function() {
114+
afterEach(function() {
115+
element.remove();
116+
});
52117
it('should translate', function() {
53118
element = angular.element('<div ui-i18n="en"><p>{{"search.placeholder" | t}}</p></div>');
54119
recompile();
55120

56121
expect(element.find('p').text()).toBe('Search...');
57122
});
123+
it('should get missing text for missing property', function() {
124+
element = angular.element('<div ui-i18n="en"><p>{{"search.bad.text" | t}}</p></div>');
125+
recompile();
126+
127+
expect(element.find('p').text()).toBe('[MISSING]search.bad.text');
128+
});
58129
});
59130

60131
describe('uiTranslate filter', function() {
132+
afterEach(function() {
133+
element.remove();
134+
});
61135
it('should translate', function() {
62136
element = angular.element('<div ui-i18n="en"><p>{{"search.placeholder" | uiTranslate}}</p></div>');
63137
recompile();
64138

65139
expect(element.find('p').text()).toBe('Search...');
66140
});
141+
it('should translate even without the ui-i18n directive', function() {
142+
element = angular.element('<div><p>{{"search.placeholder" | uiTranslate:"en"}}</p></div>');
143+
recompile();
144+
145+
expect(element.find('p').text()).toBe('Search...');
146+
});
147+
it('should get missing text for missing property', function() {
148+
element = angular.element('<div ui-i18n="en"><p>{{"search.bad.text" | uiTranslate}}</p></div>');
149+
recompile();
150+
151+
expect(element.find('p').text()).toBe('[MISSING]search.bad.text');
152+
});
67153
});
68154
});

‎test/unit/i18n/i18nService.spec.js

+53-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ describe('i18nService', function () {
1919
expect(i18nService.getCurrentLang()).toBe('fr');
2020
expect(i18nService.get().search.placeholder).toBe('Recherche...');
2121
});
22+
describe('get', function() {
23+
it('should return the language passed to it if a language is passed', function() {
24+
expect(i18nService.get('fr').search.placeholder).toBe('Recherche...');
25+
});
26+
it('should the current language no language is passed to it', function() {
27+
i18nService.setCurrentLang('en');
28+
expect(i18nService.get().search.placeholder).toBe('Search...');
29+
});
30+
});
2231
describe('add', function() {
2332
it('should add a language when langs is a string', function() {
2433
i18nService.add('tst',{test:'testlang'});
@@ -40,18 +49,58 @@ describe('i18nService', function () {
4049
});
4150
it('should return all langs', function() {
4251
var langs = i18nService.getAllLangs();
52+
4353
expect(langs.length).toBeGreaterThan(8);
4454
});
55+
describe('fallback lang', function() {
56+
it('getFallbackLang should default to english when a fallback language is not set', function() {
57+
expect(i18nService.getFallbackLang()).toEqual(i18nConstants.DEFAULT_LANG);
58+
});
59+
it('getFallbackLang should return the user set language when the user calls setFallbackLang', function() {
60+
var fallback = 'es';
4561

62+
i18nService.setFallbackLang(fallback);
63+
expect(i18nService.getFallbackLang()).toEqual(fallback);
64+
});
65+
it('getFallbackLang should return english when the user calls setFallbackLang with an empty string', function() {
66+
var fallback = '';
67+
68+
i18nService.setFallbackLang(fallback);
69+
expect(i18nService.getFallbackLang()).toEqual(i18nConstants.DEFAULT_LANG);
70+
});
71+
});
4672
describe('getSafeText', function() {
73+
beforeEach(function() {
74+
i18nService.setCurrentLang('en');
75+
i18nService.setFallbackLang('en');
76+
});
4777
it('should get safe text when text is defined', function() {
4878
expect(i18nService.getSafeText('search.placeholder')).toBe('Search...');
4979
});
50-
it('should get safe text for missing property', function() {
51-
expect(i18nService.getSafeText('search.bad.text')).toBe('[MISSING]');
80+
it('should get missing text for missing property', function() {
81+
var badText = 'search.bad.text';
82+
83+
expect(i18nService.getSafeText(badText)).toBe(i18nConstants.MISSING + badText);
84+
});
85+
it('should get fallback text when language is missing or nonexistent', function() {
86+
expect(i18nService.getSafeText('search.placeholder', 'valerian')).toBe('Search...');
87+
});
88+
it('should get missing text when language is missing or nonexistent and there is no fallback', function() {
89+
var badText = 'bad.text';
90+
91+
expect(i18nService.getSafeText(badText, 'valerian')).toBe(i18nConstants.MISSING + badText);
92+
});
93+
it('should get missing text when language is missing or nonexistent and the fallback language is the same', function() {
94+
var missingProperty = 'search.placeholder';
95+
96+
i18nService.setFallbackLang('valerian');
97+
expect(i18nService.getSafeText(missingProperty, 'valerian')).toBe(i18nConstants.MISSING + missingProperty);
5298
});
53-
it('should get missing text when language is missing or nonexistent', function() {
54-
expect(i18nService.getSafeText('search.placeholder', 'valerian')).toBe(i18nConstants.MISSING);
99+
it('should get missing text when language is missing or nonexistent and the fallback language is also missing it', function() {
100+
var missingProperty = 'search.placeholder';
101+
102+
i18nService.setFallbackLang('orcish');
103+
expect(i18nService.getSafeText(missingProperty, 'valerian')).toBe(i18nConstants.MISSING + missingProperty);
55104
});
56105
});
57106
});

0 commit comments

Comments
 (0)
Please sign in to comment.