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,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 | }; |