From 59629a0801fcfa1e61410d3dee6717c208b66e7f Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Fri, 26 Feb 2016 10:02:52 -0800 Subject: [PATCH] feat(i18n): added i18nPlural and i18nSelect pipes Closes #7268 --- modules/angular2/src/common/pipes.ts | 32 +--------- .../angular2/src/common/pipes/common_pipes.ts | 6 +- .../src/common/pipes/i18n_plural_pipe.ts | 62 +++++++++++++++++++ .../src/common/pipes/i18n_select_pipe.ts | 47 ++++++++++++++ .../common/pipes/i18n_plural_pipe_spec.ts | 59 ++++++++++++++++++ .../common/pipes/i18n_select_pipe_spec.ts | 52 ++++++++++++++++ modules/angular2/test/public_api_spec.ts | 4 ++ tools/public_api_guard/public_api_spec.ts | 4 ++ 8 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 modules/angular2/src/common/pipes/i18n_plural_pipe.ts create mode 100644 modules/angular2/src/common/pipes/i18n_select_pipe.ts create mode 100644 modules/angular2/test/common/pipes/i18n_plural_pipe_spec.ts create mode 100644 modules/angular2/test/common/pipes/i18n_select_pipe_spec.ts diff --git a/modules/angular2/src/common/pipes.ts b/modules/angular2/src/common/pipes.ts index 0f7f9a9952..b829810dfb 100644 --- a/modules/angular2/src/common/pipes.ts +++ b/modules/angular2/src/common/pipes.ts @@ -3,15 +3,6 @@ * @description * This module provides a set of common Pipes. */ -import {AsyncPipe} from './pipes/async_pipe'; -import {UpperCasePipe} from './pipes/uppercase_pipe'; -import {LowerCasePipe} from './pipes/lowercase_pipe'; -import {JsonPipe} from './pipes/json_pipe'; -import {SlicePipe} from './pipes/slice_pipe'; -import {DatePipe} from './pipes/date_pipe'; -import {DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe'; -import {ReplacePipe} from './pipes/replace_pipe'; -import {CONST_EXPR} from 'angular2/src/facade/lang'; export {AsyncPipe} from './pipes/async_pipe'; export {DatePipe} from './pipes/date_pipe'; @@ -21,23 +12,6 @@ export {LowerCasePipe} from './pipes/lowercase_pipe'; export {NumberPipe, DecimalPipe, PercentPipe, CurrencyPipe} from './pipes/number_pipe'; export {UpperCasePipe} from './pipes/uppercase_pipe'; export {ReplacePipe} from './pipes/replace_pipe'; - -/** - * A collection of Angular core pipes that are likely to be used in each and every - * application. - * - * This collection can be used to quickly enumerate all the built-in pipes in the `pipes` - * property of the `@Component` or `@View` decorators. - */ -export const COMMON_PIPES = CONST_EXPR([ - AsyncPipe, - UpperCasePipe, - LowerCasePipe, - JsonPipe, - SlicePipe, - DecimalPipe, - PercentPipe, - CurrencyPipe, - DatePipe, - ReplacePipe -]); +export {I18nPluralPipe} from './pipes/i18n_plural_pipe'; +export {I18nSelectPipe} from './pipes/i18n_select_pipe'; +export {COMMON_PIPES} from './pipes/common_pipes'; diff --git a/modules/angular2/src/common/pipes/common_pipes.ts b/modules/angular2/src/common/pipes/common_pipes.ts index 2bc6eeed07..810b1e0eb4 100644 --- a/modules/angular2/src/common/pipes/common_pipes.ts +++ b/modules/angular2/src/common/pipes/common_pipes.ts @@ -11,6 +11,8 @@ import {SlicePipe} from './slice_pipe'; import {DatePipe} from './date_pipe'; import {DecimalPipe, PercentPipe, CurrencyPipe} from './number_pipe'; import {ReplacePipe} from './replace_pipe'; +import {I18nPluralPipe} from './i18n_plural_pipe'; +import {I18nSelectPipe} from './i18n_select_pipe'; import {CONST_EXPR} from 'angular2/src/facade/lang'; /** @@ -30,5 +32,7 @@ export const COMMON_PIPES = CONST_EXPR([ PercentPipe, CurrencyPipe, DatePipe, - ReplacePipe + ReplacePipe, + I18nPluralPipe, + I18nSelectPipe ]); diff --git a/modules/angular2/src/common/pipes/i18n_plural_pipe.ts b/modules/angular2/src/common/pipes/i18n_plural_pipe.ts new file mode 100644 index 0000000000..8cf45e82dc --- /dev/null +++ b/modules/angular2/src/common/pipes/i18n_plural_pipe.ts @@ -0,0 +1,62 @@ +import { + CONST, + isStringMap, + StringWrapper, + isPresent, + RegExpWrapper +} from 'angular2/src/facade/lang'; +import {Injectable, PipeTransform, Pipe} from 'angular2/core'; +import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception'; + +var interpolationExp: RegExp = RegExpWrapper.create('#'); + +/** + * + * Maps a value to a string that pluralizes the value properly. + * + * ## Usage + * + * expression | i18nPlural:mapping + * + * where `expression` is a number and `mapping` is an object that indicates the proper text for + * when the `expression` evaluates to 0, 1, or some other number. You can interpolate the actual + * value into the text using the `#` sign. + * + * ## Example + * + * ``` + *
+ * {{ messages.length | i18nPlural: messageMapping }} + *
+ * + * class MyApp { + * messages: any[]; + * messageMapping: any = { + * '=0': 'No messages.', + * '=1': 'One message.', + * 'other': '# messages.' + * } + * ... + * } + * ``` + * + */ +@CONST() +@Pipe({name: 'i18nPlural', pure: true}) +@Injectable() +export class I18nPluralPipe implements PipeTransform { + transform(value: number, args: any[] = null): string { + var key: string; + var valueStr: string; + var pluralMap: {[count: string]: string} = args[0]; + + if (!isStringMap(pluralMap)) { + throw new InvalidPipeArgumentException(I18nPluralPipe, pluralMap); + } + + key = value === 0 || value === 1 ? `=${value}` : 'other'; + valueStr = isPresent(value) ? value.toString() : ''; + + return StringWrapper.replaceAll(pluralMap[key], interpolationExp, valueStr); + } +} diff --git a/modules/angular2/src/common/pipes/i18n_select_pipe.ts b/modules/angular2/src/common/pipes/i18n_select_pipe.ts new file mode 100644 index 0000000000..619b38c9dc --- /dev/null +++ b/modules/angular2/src/common/pipes/i18n_select_pipe.ts @@ -0,0 +1,47 @@ +import {CONST, isStringMap} from 'angular2/src/facade/lang'; +import {StringMapWrapper} from 'angular2/src/facade/collection'; +import {Injectable, PipeTransform, Pipe} from 'angular2/core'; +import {InvalidPipeArgumentException} from './invalid_pipe_argument_exception'; + +/** + * + * Generic selector that displays the string that matches the current value. + * + * ## Usage + * + * expression | i18nSelect:mapping + * + * where `mapping` is an object that indicates the text that should be displayed + * for different values of the provided `expression`. + * + * ## Example + * + * ``` + *
+ * {{ gender | i18nSelect: inviteMap }} + *
+ * + * class MyApp { + * gender: string = 'male'; + * inviteMap: any = { + * 'male': 'Invite her.', + * 'female': 'Invite him.', + * 'other': 'Invite them.' + * } + * ... + * } + * ``` + */ +@CONST() +@Pipe({name: 'i18nSelect', pure: true}) +@Injectable() +export class I18nSelectPipe implements PipeTransform { + transform(value: string, args: any[] = null): string { + var mapping: {[key: string]: string} = args[0]; + if (!isStringMap(mapping)) { + throw new InvalidPipeArgumentException(I18nSelectPipe, mapping); + } + + return StringMapWrapper.contains(mapping, value) ? mapping[value] : mapping['other']; + } +} diff --git a/modules/angular2/test/common/pipes/i18n_plural_pipe_spec.ts b/modules/angular2/test/common/pipes/i18n_plural_pipe_spec.ts new file mode 100644 index 0000000000..6a220afd1e --- /dev/null +++ b/modules/angular2/test/common/pipes/i18n_plural_pipe_spec.ts @@ -0,0 +1,59 @@ +import { + ddescribe, + describe, + it, + iit, + xit, + expect, + beforeEach, + afterEach +} from 'angular2/testing_internal'; + +import {I18nPluralPipe} from 'angular2/common'; +import {PipeResolver} from 'angular2/src/core/linker/pipe_resolver'; + +export function main() { + describe("I18nPluralPipe", () => { + var pipe; + var mapping = {'=0': 'No messages.', '=1': 'One message.', 'other': 'There are some messages.'}; + var interpolatedMapping = + {'=0': 'No messages.', '=1': 'One message.', 'other': 'There are # messages, that is #.'}; + + beforeEach(() => { pipe = new I18nPluralPipe(); }); + + it('should be marked as pure', + () => { expect(new PipeResolver().resolve(I18nPluralPipe).pure).toEqual(true); }); + + describe("transform", () => { + it("should return 0 text if value is 0", () => { + var val = pipe.transform(0, [mapping]); + expect(val).toEqual('No messages.'); + }); + + it("should return 1 text if value is 1", () => { + var val = pipe.transform(1, [mapping]); + expect(val).toEqual('One message.'); + }); + + it("should return other text if value is anything other than 0 or 1", () => { + var val = pipe.transform(6, [mapping]); + expect(val).toEqual('There are some messages.'); + }); + + it("should interpolate the value into the text where indicated", () => { + var val = pipe.transform(6, [interpolatedMapping]); + expect(val).toEqual('There are 6 messages, that is 6.'); + }); + + it("should use 'other' if value is undefined", () => { + var messageLength; + var val = pipe.transform(messageLength, [interpolatedMapping]); + expect(val).toEqual('There are messages, that is .'); + }); + + it("should not support bad arguments", + () => { expect(() => pipe.transform(0, ['hey'])).toThrowError(); }); + }); + + }); +} diff --git a/modules/angular2/test/common/pipes/i18n_select_pipe_spec.ts b/modules/angular2/test/common/pipes/i18n_select_pipe_spec.ts new file mode 100644 index 0000000000..7a9e7f21cd --- /dev/null +++ b/modules/angular2/test/common/pipes/i18n_select_pipe_spec.ts @@ -0,0 +1,52 @@ +import { + ddescribe, + describe, + it, + iit, + xit, + expect, + beforeEach, + afterEach +} from 'angular2/testing_internal'; + +import {I18nSelectPipe} from 'angular2/common'; +import {PipeResolver} from 'angular2/src/core/linker/pipe_resolver'; + +export function main() { + describe("I18nSelectPipe", () => { + var pipe; + var mapping = {'male': 'Invite him.', 'female': 'Invite her.', 'other': 'Invite them.'}; + + beforeEach(() => { pipe = new I18nSelectPipe(); }); + + it('should be marked as pure', + () => { expect(new PipeResolver().resolve(I18nSelectPipe).pure).toEqual(true); }); + + describe("transform", () => { + it("should return male text if value is male", () => { + var val = pipe.transform('male', [mapping]); + expect(val).toEqual('Invite him.'); + }); + + it("should return female text if value is female", () => { + var val = pipe.transform('female', [mapping]); + expect(val).toEqual('Invite her.'); + }); + + it("should return other text if value is anything other than male or female", () => { + var val = pipe.transform('Anything else', [mapping]); + expect(val).toEqual('Invite them.'); + }); + + it("should use 'other' if value is undefined", () => { + var gender; + var val = pipe.transform(gender, [mapping]); + expect(val).toEqual('Invite them.'); + }); + + it("should not support bad arguments", + () => { expect(() => pipe.transform('male', ['hey'])).toThrowError(); }); + }); + + }); +} diff --git a/modules/angular2/test/public_api_spec.ts b/modules/angular2/test/public_api_spec.ts index 7fb93690b0..29ba7129cc 100644 --- a/modules/angular2/test/public_api_spec.ts +++ b/modules/angular2/test/public_api_spec.ts @@ -208,6 +208,10 @@ var NG_COMMON = [ 'FormBuilder.array()', 'FormBuilder.control()', 'FormBuilder.group()', + 'I18nPluralPipe', + 'I18nPluralPipe.transform()', + 'I18nSelectPipe', + 'I18nSelectPipe.transform()', 'JsonPipe', 'JsonPipe.transform()', 'LowerCasePipe', diff --git a/tools/public_api_guard/public_api_spec.ts b/tools/public_api_guard/public_api_spec.ts index 09a0e044c6..a725b7c2df 100644 --- a/tools/public_api_guard/public_api_spec.ts +++ b/tools/public_api_guard/public_api_spec.ts @@ -641,6 +641,10 @@ const COMMON = [ 'FormBuilder.array(controlsConfig:any[], validator:Function, asyncValidator:Function):ControlArray', 'FormBuilder.control(value:Object, validator:Function, asyncValidator:Function):Control', 'FormBuilder.group(controlsConfig:{[key:string]:any}, extra:{[key:string]:any}):ControlGroup', + 'I18nPluralPipe', + 'I18nPluralPipe.transform(value:number, args:any[]):string', + 'I18nSelectPipe', + 'I18nSelectPipe.transform(value:string, args:any[]):string', 'JsonPipe', 'JsonPipe.transform(value:any, args:any[]):string', 'LowerCasePipe',