Commit ae4699f3bccf859938a7a3a7512d118200ae3b41
1 parent
833e19f2
Exists in
master
and in
32 other branches
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 | }; | ... | ... |