Commit ae4699f3bccf859938a7a3a7512d118200ae3b41
1 parent
833e19f2
Exists in
master
and in
1 other branch
Use messageformat to support pluralization in translations
Showing
9 changed files
with
72 additions
and
45 deletions
Show diff stats
bower.json
@@ -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" |
gulp/build.js
@@ -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("Components", () => { | @@ -47,11 +43,35 @@ describe("Components", () => { | ||
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 | }; |