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 | 33 | "angular-translate-handler-log": "^2.10.0", |
34 | 34 | "angular-dynamic-locale": "^0.1.30", |
35 | 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 | 39 | "devDependencies": { |
39 | 40 | "angular-mocks": "~1.5.0" | ... | ... |
gulp/build.js
... | ... | @@ -48,6 +48,7 @@ gulp.task('html', ['inject', 'partials'], function () { |
48 | 48 | .pipe($.replace('/languages/', noosferoThemePrefix + 'languages/')) |
49 | 49 | .pipe($.replace('bower_components/angular-i18n/', noosferoThemePrefix + 'locale/angular-i18n/')) |
50 | 50 | .pipe($.replace('bower_components/moment/', noosferoThemePrefix + 'locale/moment/')) |
51 | + .pipe($.replace('bower_components/messageformat/', noosferoThemePrefix + 'locale/messageformat/')) | |
51 | 52 | .pipe($.sourcemaps.init()) |
52 | 53 | .pipe($.ngAnnotate()) |
53 | 54 | .pipe($.uglify({ preserveComments: $.uglifySaveLicense })).on('error', conf.errorHandler('Uglify')) |
... | ... | @@ -88,6 +89,7 @@ gulp.task('locale', function () { |
88 | 89 | return gulp.src([ |
89 | 90 | path.join("bower_components/angular-i18n", '*.js'), |
90 | 91 | path.join("bower_components/moment/locale", '*.js'), |
92 | + path.join("bower_components/messageformat/locale", '*.js'), | |
91 | 93 | ], {base: 'bower_components/'}) |
92 | 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 | 4 | import {LanguageSelector} from './language-selector.component'; |
5 | 5 | |
6 | 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 | 8 | describe("Components", () => { |
13 | 9 | |
14 | 10 | describe("Language Selector Component", () => { |
15 | 11 | |
16 | 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 | 36 | let component: LanguageSelector = new LanguageSelector( |
41 | 37 | <any>helpers.mocks.$translate, |
42 | 38 | <any>helpers.mocks.tmhDynamicLocale, |
... | ... | @@ -47,11 +43,35 @@ describe("Components", () => { |
47 | 43 | expect(component.availableLanguages).toBeNull(); |
48 | 44 | component.changeLanguage('en'); |
49 | 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 | 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 | 14 | private amMoment: any, |
15 | 15 | private angularLoad: any) { |
16 | 16 | |
17 | + this.configAvailableLanguages(); | |
17 | 18 | this.changeLanguage(tmhDynamicLocale.get() || $translate.use()); |
18 | 19 | } |
19 | 20 | |
... | ... | @@ -24,14 +25,20 @@ export class LanguageSelector { |
24 | 25 | changeLanguage(language: string) { |
25 | 26 | this.changeMomentLocale(language); |
26 | 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 | 42 | private changeMomentLocale(language: string) { |
36 | 43 | let localePromise = Promise.resolve(); |
37 | 44 | if (language != "en") { | ... | ... |
src/app/components/noosfero-activities/activity/new_friendship.html
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | <timeline-heading> |
6 | 6 | <h4 class="timeline-title"> |
7 | 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 | 9 | <span class="comma-separated"> |
10 | 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 | 11 | <strong ng-bind="friend"></strong> | ... | ... |
src/app/index.config.ts
... | ... | @@ -29,6 +29,7 @@ function configTranslation($translateProvider: angular.translate.ITranslateProvi |
29 | 29 | prefix: '/languages/', |
30 | 30 | suffix: '.json' |
31 | 31 | }); |
32 | + $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); | |
32 | 33 | $translateProvider.useMissingTranslationHandlerLog(); |
33 | 34 | $translateProvider.preferredLanguage('en'); |
34 | 35 | $translateProvider.useSanitizeValueStrategy('escape'); | ... | ... |
src/languages/en.json
... | ... | @@ -12,7 +12,7 @@ |
12 | 12 | "profile.wall": "Profile Wall", |
13 | 13 | "activities.create_article.description": "has published on", |
14 | 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 | 16 | "auth.title": "Login", |
17 | 17 | "auth.form.login": "Login / Email address", |
18 | 18 | "auth.form.password": "Password", | ... | ... |
src/languages/pt.json
... | ... | @@ -12,7 +12,7 @@ |
12 | 12 | "profile.wall": "Mural do Perfil", |
13 | 13 | "activities.create_article.description": "publicou em", |
14 | 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 | 16 | "auth.title": "Login", |
17 | 17 | "auth.form.login": "Login / Email", |
18 | 18 | "auth.form.password": "Senha", | ... | ... |
src/spec/mocks.ts
... | ... | @@ -45,9 +45,7 @@ export var mocks = { |
45 | 45 | }, |
46 | 46 | $translate: { |
47 | 47 | use: (lang?: string) => { |
48 | - return { | |
49 | - then: (func?: any) => { if (func) func() } | |
50 | - } | |
48 | + return lang ? Promise.resolve(lang) : "en"; | |
51 | 49 | }, |
52 | 50 | instant: () => { } |
53 | 51 | }, |
... | ... | @@ -60,9 +58,7 @@ export var mocks = { |
60 | 58 | }, |
61 | 59 | angularLoad: { |
62 | 60 | loadScript: (script?: string) => { |
63 | - return { | |
64 | - then: (func?: any) => { if (func) func() } | |
65 | - } | |
61 | + return Promise.resolve(); | |
66 | 62 | } |
67 | 63 | } |
68 | 64 | }; | ... | ... |