Commit ae4699f3bccf859938a7a3a7512d118200ae3b41

Authored by Victor Costa
1 parent 833e19f2
Exists in master and in 1 other branch dev-fixes

Use messageformat to support pluralization in translations

@@ -33,7 +33,8 @@ @@ -33,7 +33,8 @@
33 "angular-translate-handler-log": "^2.10.0", 33 "angular-translate-handler-log": "^2.10.0",
34 "angular-dynamic-locale": "^0.1.30", 34 "angular-dynamic-locale": "^0.1.30",
35 "angular-i18n": "^1.5.0", 35 "angular-i18n": "^1.5.0",
36 - "angular-load": "^0.4.1" 36 + "angular-load": "^0.4.1",
  37 + "angular-translate-interpolation-messageformat": "^2.10.0"
37 }, 38 },
38 "devDependencies": { 39 "devDependencies": {
39 "angular-mocks": "~1.5.0" 40 "angular-mocks": "~1.5.0"
@@ -48,6 +48,7 @@ gulp.task('html', ['inject', 'partials'], function () { @@ -48,6 +48,7 @@ gulp.task('html', ['inject', 'partials'], function () {
48 .pipe($.replace('/languages/', noosferoThemePrefix + 'languages/')) 48 .pipe($.replace('/languages/', noosferoThemePrefix + 'languages/'))
49 .pipe($.replace('bower_components/angular-i18n/', noosferoThemePrefix + 'locale/angular-i18n/')) 49 .pipe($.replace('bower_components/angular-i18n/', noosferoThemePrefix + 'locale/angular-i18n/'))
50 .pipe($.replace('bower_components/moment/', noosferoThemePrefix + 'locale/moment/')) 50 .pipe($.replace('bower_components/moment/', noosferoThemePrefix + 'locale/moment/'))
  51 + .pipe($.replace('bower_components/messageformat/', noosferoThemePrefix + 'locale/messageformat/'))
51 .pipe($.sourcemaps.init()) 52 .pipe($.sourcemaps.init())
52 .pipe($.ngAnnotate()) 53 .pipe($.ngAnnotate())
53 .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) 54 .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify'))
@@ -88,6 +89,7 @@ gulp.task('locale', function () { @@ -88,6 +89,7 @@ gulp.task('locale', function () {
88 return gulp.src([ 89 return gulp.src([
89 path.join("bower_components/angular-i18n", '*.js'), 90 path.join("bower_components/angular-i18n", '*.js'),
90 path.join("bower_components/moment/locale", '*.js'), 91 path.join("bower_components/moment/locale", '*.js'),
  92 + path.join("bower_components/messageformat/locale", '*.js'),
91 ], {base: 'bower_components/'}) 93 ], {base: 'bower_components/'})
92 .pipe(gulp.dest(path.join(conf.paths.dist, '/locale/'))); 94 .pipe(gulp.dest(path.join(conf.paths.dist, '/locale/')));
93 }); 95 });
src/app/components/language-selector/language-selector.component.spec.ts
1 -import {TestComponentBuilder, ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';  
2 -import {Pipe, Input, provide, Component} from 'ng-forward'; 1 +import {ComponentFixture} from 'ng-forward/cjs/testing/test-component-builder';
  2 +import {provide} from 'ng-forward';
3 3
4 import {LanguageSelector} from './language-selector.component'; 4 import {LanguageSelector} from './language-selector.component';
5 5
6 import * as helpers from "../../../spec/helpers"; 6 import * as helpers from "../../../spec/helpers";
7 7
8 -const tcb = new TestComponentBuilder();  
9 -  
10 -const htmlTemplate: string = '<language-selector></language-selector>';  
11 -  
12 describe("Components", () => { 8 describe("Components", () => {
13 9
14 describe("Language Selector Component", () => { 10 describe("Language Selector Component", () => {
15 11
16 beforeEach(angular.mock.module("templates")); 12 beforeEach(angular.mock.module("templates"));
17 13
18 - @Component({  
19 - selector: 'test-container-component',  
20 - template: htmlTemplate,  
21 - directives: [LanguageSelector],  
22 - providers: [  
23 - provide('$translate', {  
24 - useValue: helpers.mocks.$translate  
25 - }),  
26 - provide('tmhDynamicLocale', {  
27 - useValue: helpers.mocks.tmhDynamicLocale  
28 - }),  
29 - provide('amMoment', {  
30 - useValue: helpers.mocks.amMoment  
31 - }),  
32 - provide('angularLoad', {  
33 - useValue: helpers.mocks.angularLoad  
34 - })  
35 - ].concat(helpers.provideFilters("translateFilter"))  
36 - })  
37 - class BlockContainerComponent { }  
38 -  
39 - it("set available languages when change language", () => { 14 + let buildComponent = (): Promise<ComponentFixture> => {
  15 + return helpers.quickCreateComponent({
  16 + template: "<language-selector></language-selector>",
  17 + directives: [LanguageSelector],
  18 + providers: [
  19 + provide('$translate', {
  20 + useValue: helpers.mocks.$translate
  21 + }),
  22 + provide('tmhDynamicLocale', {
  23 + useValue: helpers.mocks.tmhDynamicLocale
  24 + }),
  25 + provide('amMoment', {
  26 + useValue: helpers.mocks.amMoment
  27 + }),
  28 + provide('angularLoad', {
  29 + useValue: helpers.mocks.angularLoad
  30 + })
  31 + ].concat(helpers.provideFilters("translateFilter"))
  32 + });
  33 + }
  34 +
  35 + it("set available languages when change language", (done) => {
40 let component: LanguageSelector = new LanguageSelector( 36 let component: LanguageSelector = new LanguageSelector(
41 <any>helpers.mocks.$translate, 37 <any>helpers.mocks.$translate,
42 <any>helpers.mocks.tmhDynamicLocale, 38 <any>helpers.mocks.tmhDynamicLocale,
@@ -47,11 +43,35 @@ describe(&quot;Components&quot;, () =&gt; { @@ -47,11 +43,35 @@ describe(&quot;Components&quot;, () =&gt; {
47 expect(component.availableLanguages).toBeNull(); 43 expect(component.availableLanguages).toBeNull();
48 component.changeLanguage('en'); 44 component.changeLanguage('en');
49 expect(component.availableLanguages).toBeDefined(); 45 expect(component.availableLanguages).toBeDefined();
  46 + done();
50 }); 47 });
51 48
52 - it("display language options", () => {  
53 - helpers.createComponentFromClass(BlockContainerComponent).then(fixture => { 49 + it("display language options", (done) => {
  50 + buildComponent().then(fixture => {
54 expect(fixture.debugElement.queryAll('li.language').length).toEqual(2); 51 expect(fixture.debugElement.queryAll('li.language').length).toEqual(2);
  52 + done();
  53 + });
  54 + });
  55 +
  56 + it("change the language", (done) => {
  57 + buildComponent().then(fixture => {
  58 + let component: LanguageSelector = fixture.debugElement.componentViewChildren[0].componentInstance;
  59 + let $q = fixture.debugElement.getLocal("$q");
  60 + let loadScripPromise = $q.defer();
  61 + loadScripPromise.resolve();
  62 + component["angularLoad"].loadScript = jasmine.createSpy("loadScript").and.returnValue(loadScripPromise.promise);
  63 + component["tmhDynamicLocale"].set = jasmine.createSpy("set");
  64 + component["tmhDynamicLocale"].get = jasmine.createSpy("get").and.returnValue("en");
  65 + component["$translate"].use = jasmine.createSpy("use");
  66 +
  67 + component.changeLanguage('pt');
  68 + fixture.debugElement.getLocal("$rootScope").$digest();
  69 +
  70 + expect(component["angularLoad"].loadScript).toHaveBeenCalledWith("/bower_components/moment/locale/pt.js");
  71 + expect(component["angularLoad"].loadScript).toHaveBeenCalledWith("/bower_components/messageformat/locale/pt.js");
  72 + expect(component["tmhDynamicLocale"].set).toHaveBeenCalledWith("pt");
  73 + expect(component["$translate"].use).toHaveBeenCalledWith("pt");
  74 + done();
55 }); 75 });
56 }); 76 });
57 77
src/app/components/language-selector/language-selector.component.ts
@@ -14,6 +14,7 @@ export class LanguageSelector { @@ -14,6 +14,7 @@ export class LanguageSelector {
14 private amMoment: any, 14 private amMoment: any,
15 private angularLoad: any) { 15 private angularLoad: any) {
16 16
  17 + this.configAvailableLanguages();
17 this.changeLanguage(tmhDynamicLocale.get() || $translate.use()); 18 this.changeLanguage(tmhDynamicLocale.get() || $translate.use());
18 } 19 }
19 20
@@ -24,14 +25,20 @@ export class LanguageSelector { @@ -24,14 +25,20 @@ export class LanguageSelector {
24 changeLanguage(language: string) { 25 changeLanguage(language: string) {
25 this.changeMomentLocale(language); 26 this.changeMomentLocale(language);
26 this.tmhDynamicLocale.set(language); 27 this.tmhDynamicLocale.set(language);
27 - this.$translate.use(language).then((lang) => {  
28 - this.availableLanguages = {  
29 - "en": this.$translate.instant("language.en"),  
30 - "pt": this.$translate.instant("language.pt")  
31 - }; 28 + this.angularLoad.loadScript(`/bower_components/messageformat/locale/${language}.js`).then(() => {
  29 + return this.$translate.use(language);
  30 + }).then(() => {
  31 + this.configAvailableLanguages();
32 }); 32 });
33 } 33 }
34 34
  35 + private configAvailableLanguages() {
  36 + this.availableLanguages = {
  37 + "en": this.$translate.instant("language.en"),
  38 + "pt": this.$translate.instant("language.pt")
  39 + };
  40 + }
  41 +
35 private changeMomentLocale(language: string) { 42 private changeMomentLocale(language: string) {
36 let localePromise = Promise.resolve(); 43 let localePromise = Promise.resolve();
37 if (language != "en") { 44 if (language != "en") {
src/app/components/noosfero-activities/activity/new_friendship.html
@@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
5 <timeline-heading> 5 <timeline-heading>
6 <h4 class="timeline-title"> 6 <h4 class="timeline-title">
7 <a ui-sref="main.profile.info({profile: ctrl.activity.user.identifier})"><strong ng-bind="ctrl.activity.user.name"></strong></a> 7 <a ui-sref="main.profile.info({profile: ctrl.activity.user.identifier})"><strong ng-bind="ctrl.activity.user.name"></strong></a>
8 - <span> {{"activities.new_friendship.description" | translate:{friends: ctrl.activity.params.friend_name.length} }} </span> 8 + <span> {{"activities.new_friendship.description" | translate:{friends: ctrl.activity.params.friend_name.length}:"messageformat" }} </span>
9 <span class="comma-separated"> 9 <span class="comma-separated">
10 <a class="separated-item" ui-sref="main.profile.info({profile: ctrl.activity.params.friend_url[$index].profile})" ng-repeat="friend in ctrl.activity.params.friend_name"> 10 <a class="separated-item" ui-sref="main.profile.info({profile: ctrl.activity.params.friend_url[$index].profile})" ng-repeat="friend in ctrl.activity.params.friend_name">
11 <strong ng-bind="friend"></strong> 11 <strong ng-bind="friend"></strong>
src/app/index.config.ts
@@ -29,6 +29,7 @@ function configTranslation($translateProvider: angular.translate.ITranslateProvi @@ -29,6 +29,7 @@ function configTranslation($translateProvider: angular.translate.ITranslateProvi
29 prefix: '/languages/', 29 prefix: '/languages/',
30 suffix: '.json' 30 suffix: '.json'
31 }); 31 });
  32 + $translateProvider.addInterpolation('$translateMessageFormatInterpolation');
32 $translateProvider.useMissingTranslationHandlerLog(); 33 $translateProvider.useMissingTranslationHandlerLog();
33 $translateProvider.preferredLanguage('en'); 34 $translateProvider.preferredLanguage('en');
34 $translateProvider.useSanitizeValueStrategy('escape'); 35 $translateProvider.useSanitizeValueStrategy('escape');
src/languages/en.json
@@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
12 "profile.wall": "Profile Wall", 12 "profile.wall": "Profile Wall",
13 "activities.create_article.description": "has published on", 13 "activities.create_article.description": "has published on",
14 "activities.add_member_in_community.description": "has joined the community", 14 "activities.add_member_in_community.description": "has joined the community",
15 - "activities.new_friendship.description": "has made {{friends}} new friend(s):", 15 + "activities.new_friendship.description": "has made {friends, plural, one{one new friend} other{# new friends}}:",
16 "auth.title": "Login", 16 "auth.title": "Login",
17 "auth.form.login": "Login / Email address", 17 "auth.form.login": "Login / Email address",
18 "auth.form.password": "Password", 18 "auth.form.password": "Password",
src/languages/pt.json
@@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
12 "profile.wall": "Mural do Perfil", 12 "profile.wall": "Mural do Perfil",
13 "activities.create_article.description": "publicou em", 13 "activities.create_article.description": "publicou em",
14 "activities.add_member_in_community.description": "entrou na comunidade", 14 "activities.add_member_in_community.description": "entrou na comunidade",
15 - "activities.new_friendship.description": "fez {{friends}} novo(s) amigo(s):", 15 + "activities.new_friendship.description": "fez {friends, plural, one{um novo amigo} other{# novos amigos}}:",
16 "auth.title": "Login", 16 "auth.title": "Login",
17 "auth.form.login": "Login / Email", 17 "auth.form.login": "Login / Email",
18 "auth.form.password": "Senha", 18 "auth.form.password": "Senha",
src/spec/mocks.ts
@@ -45,9 +45,7 @@ export var mocks = { @@ -45,9 +45,7 @@ export var mocks = {
45 }, 45 },
46 $translate: { 46 $translate: {
47 use: (lang?: string) => { 47 use: (lang?: string) => {
48 - return {  
49 - then: (func?: any) => { if (func) func() }  
50 - } 48 + return lang ? Promise.resolve(lang) : "en";
51 }, 49 },
52 instant: () => { } 50 instant: () => { }
53 }, 51 },
@@ -60,9 +58,7 @@ export var mocks = { @@ -60,9 +58,7 @@ export var mocks = {
60 }, 58 },
61 angularLoad: { 59 angularLoad: {
62 loadScript: (script?: string) => { 60 loadScript: (script?: string) => {
63 - return {  
64 - then: (func?: any) => { if (func) func() }  
65 - } 61 + return Promise.resolve();
66 } 62 }
67 } 63 }
68 }; 64 };