From 161a4dd15f6966d93167ce9b8e1d4aa6ab76e7b3 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Fri, 12 Aug 2016 14:46:06 -0700 Subject: [PATCH] feat(i18n): Add NgLocaleLocalization which returns plural cases given a locale (#10744) --- modules/@angular/common/index.ts | 2 +- modules/@angular/common/src/localization.ts | 399 ++++++++++++++++++ .../@angular/common/test/localization_spec.ts | 117 +++++ npm-shrinkwrap.clean.json | 50 ++- npm-shrinkwrap.json | 84 +++- package.json | 1 + scripts/cldr/gen_plural_rules.js | 174 ++++++++ tools/public_api_guard/common/index.d.ts | 19 + 8 files changed, 841 insertions(+), 5 deletions(-) create mode 100644 modules/@angular/common/test/localization_spec.ts create mode 100644 scripts/cldr/gen_plural_rules.js diff --git a/modules/@angular/common/index.ts b/modules/@angular/common/index.ts index cc38f9192a..3ed795c92f 100644 --- a/modules/@angular/common/index.ts +++ b/modules/@angular/common/index.ts @@ -14,7 +14,7 @@ export * from './src/pipes'; export * from './src/directives'; export * from './src/common_directives'; export * from './src/location'; -export {NgLocalization} from './src/localization'; +export {NgLocalization, NgLocaleLocalization, Plural, getPluralCase} from './src/localization'; // Note: This does not contain the location providers, // as they need some platform specific implementations to work. diff --git a/modules/@angular/common/src/localization.ts b/modules/@angular/common/src/localization.ts index 471724b3ce..b235a4b1de 100644 --- a/modules/@angular/common/src/localization.ts +++ b/modules/@angular/common/src/localization.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {Injectable} from '@angular/core'; /** * @experimental */ export abstract class NgLocalization { abstract getPluralCategory(value: any): string; } + /** * Returns the plural category for a given value. * - "=value" when the case exists, @@ -24,3 +26,400 @@ export function getPluralCategory( return cases.indexOf(nbCase) > -1 ? nbCase : ngLocalization.getPluralCategory(value); } + +/** + * Returns the plural case based on the locale + * + * @experimental + */ +@Injectable() +export class NgLocaleLocalization extends NgLocalization { + constructor(private _locale: string) { super(); } + + getPluralCategory(value: any): string { + const plural = getPluralCase(this._locale, value); + + switch (plural) { + case Plural.Zero: + return 'zero'; + case Plural.One: + return 'one'; + case Plural.Two: + return 'two'; + case Plural.Few: + return 'few'; + case Plural.Many: + return 'many'; + default: + return 'other'; + } + } +} + +// This is generated code DO NOT MODIFY +// see angular2/script/cldr/gen_plural_rules.js + +/** @experimental */ +export enum Plural { + Zero, + One, + Two, + Few, + Many, + Other +} + +/** + * Returns the plural case based on the locale + * + * @experimental + */ +export function getPluralCase(locale: string, nLike: number | string): Plural { + // TODO(vicb): lazy compute + if (typeof nLike === 'string') { + nLike = parseInt(nLike, 10); + } + const n: number = nLike as number; + const nDecimal = n.toString().replace(/^[^.]*\.?/, ''); + const i = Math.floor(Math.abs(n)); + const v = nDecimal.length; + const f = parseInt(nDecimal, 10); + const t = parseInt(n.toString().replace(/^[^.]*\.?|0+$/g, ''), 10) || 0; + + const lang = locale.split('_')[0].toLowerCase(); + + switch (lang) { + case 'af': + case 'asa': + case 'az': + case 'bem': + case 'bez': + case 'bg': + case 'brx': + case 'ce': + case 'cgg': + case 'chr': + case 'ckb': + case 'ee': + case 'el': + case 'eo': + case 'es': + case 'eu': + case 'fo': + case 'fur': + case 'gsw': + case 'ha': + case 'haw': + case 'hu': + case 'jgo': + case 'jmc': + case 'ka': + case 'kk': + case 'kkj': + case 'kl': + case 'ks': + case 'ksb': + case 'ky': + case 'lb': + case 'lg': + case 'mas': + case 'mgo': + case 'ml': + case 'mn': + case 'nb': + case 'nd': + case 'ne': + case 'nn': + case 'nnh': + case 'nyn': + case 'om': + case 'or': + case 'os': + case 'ps': + case 'rm': + case 'rof': + case 'rwk': + case 'saq': + case 'seh': + case 'sn': + case 'so': + case 'sq': + case 'ta': + case 'te': + case 'teo': + case 'tk': + case 'tr': + case 'ug': + case 'uz': + case 'vo': + case 'vun': + case 'wae': + case 'xog': + if (n === 1) return Plural.One; + return Plural.Other; + case 'agq': + case 'bas': + case 'cu': + case 'dav': + case 'dje': + case 'dua': + case 'dyo': + case 'ebu': + case 'ewo': + case 'guz': + case 'kam': + case 'khq': + case 'ki': + case 'kln': + case 'kok': + case 'ksf': + case 'lrc': + case 'lu': + case 'luo': + case 'luy': + case 'mer': + case 'mfe': + case 'mgh': + case 'mua': + case 'mzn': + case 'nmg': + case 'nus': + case 'qu': + case 'rn': + case 'rw': + case 'sbp': + case 'twq': + case 'vai': + case 'yav': + case 'yue': + case 'zgh': + case 'ak': + case 'ln': + case 'mg': + case 'pa': + case 'ti': + if (n === Math.floor(n) && n >= 0 && n <= 1) return Plural.One; + return Plural.Other; + case 'am': + case 'as': + case 'bn': + case 'fa': + case 'gu': + case 'hi': + case 'kn': + case 'mr': + case 'zu': + if (i === 0 || n === 1) return Plural.One; + return Plural.Other; + case 'ar': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 3 && n % 100 <= 10) return Plural.Few; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 99) return Plural.Many; + return Plural.Other; + case 'ast': + case 'ca': + case 'de': + case 'en': + case 'et': + case 'fi': + case 'fy': + case 'gl': + case 'it': + case 'nl': + case 'sv': + case 'sw': + case 'ur': + case 'yi': + if (i === 1 && v === 0) return Plural.One; + return Plural.Other; + case 'be': + if (n % 10 === 1 && !(n % 100 === 11)) return Plural.One; + if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 4 && + !(n % 100 >= 12 && n % 100 <= 14)) + return Plural.Few; + if (n % 10 === 0 || n % 10 === Math.floor(n % 10) && n % 10 >= 5 && n % 10 <= 9 || + n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 14) + return Plural.Many; + return Plural.Other; + case 'br': + if (n % 10 === 1 && !(n % 100 === 11 || n % 100 === 71 || n % 100 === 91)) return Plural.One; + if (n % 10 === 2 && !(n % 100 === 12 || n % 100 === 72 || n % 100 === 92)) return Plural.Two; + if (n % 10 === Math.floor(n % 10) && (n % 10 >= 3 && n % 10 <= 4 || n % 10 === 9) && + !(n % 100 >= 10 && n % 100 <= 19 || n % 100 >= 70 && n % 100 <= 79 || + n % 100 >= 90 && n % 100 <= 99)) + return Plural.Few; + if (!(n === 0) && n % 1e6 === 0) return Plural.Many; + return Plural.Other; + case 'bs': + case 'hr': + case 'sr': + // https://github.com/papandreou/node-cldr/issues/31 + if (v === 0 && i % 10 === 1 && (!(i % 100 === 11)) || f % 10 === 1 && !(f % 100 === 11)) + return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + (!(i % 100 >= 12 && i % 100 <= 14)) || + f % 10 === Math.floor(f % 10) && f % 10 >= 2 && f % 10 <= 4 && + !(f % 100 >= 12 && f % 100 <= 14)) + return Plural.Few; + return Plural.Other; + case 'cs': + case 'sk': + if (i === 1 && v === 0) return Plural.One; + if (i === Math.floor(i) && i >= 2 && i <= 4 && v === 0) return Plural.Few; + if (!(v === 0)) return Plural.Many; + return Plural.Other; + case 'cy': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n === 3) return Plural.Few; + if (n === 6) return Plural.Many; + return Plural.Other; + case 'da': + if (n === 1 || !(t === 0) && (i === 0 || i === 1)) return Plural.One; + return Plural.Other; + case 'dsb': + case 'hsb': + if (v === 0 && (i % 100 === 1 || f % 100 === 1)) return Plural.One; + if (v === 0 && (i % 100 === 2 || f % 100 === 2)) return Plural.Two; + if (v === 0 && (i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || + f % 100 === Math.floor(f % 100) && f % 100 >= 3 && f % 100 <= 4)) + return Plural.Few; + return Plural.Other; + case 'ff': + case 'fr': + case 'hy': + case 'kab': + if (i === 0 || i === 1) return Plural.One; + return Plural.Other; + case 'fil': + if (v === 0 && (i === 1 || i === 2 || i === 3 || + v === 0 && (!(i % 10 === 4 || i % 10 === 6 || i % 10 === 9) || + !(v === 0) && !(f % 10 === 4 || f % 10 === 6 || f % 10 === 9)))) + return Plural.One; + return Plural.Other; + case 'ga': + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + if (n === Math.floor(n) && n >= 3 && n <= 6) return Plural.Few; + if (n === Math.floor(n) && n >= 7 && n <= 10) return Plural.Many; + return Plural.Other; + case 'gd': + if (n === 1 || n === 11) return Plural.One; + if (n === 2 || n === 12) return Plural.Two; + if (n === Math.floor(n) && (n >= 3 && n <= 10 || n >= 13 && n <= 19)) return Plural.Few; + return Plural.Other; + case 'gv': + if (v === 0 && i % 10 === 1) return Plural.One; + if (v === 0 && i % 10 === 2) return Plural.Two; + if (v === 0 && + (i % 100 === 0 || i % 100 === 20 || i % 100 === 40 || i % 100 === 60 || i % 100 === 80)) + return Plural.Few; + if (!(v === 0)) return Plural.Many; + return Plural.Other; + case 'he': + if (i === 1 && v === 0) return Plural.One; + if (i === 2 && v === 0) return Plural.Two; + if (v === 0 && !(n >= 0 && n <= 10) && n % 10 === 0) return Plural.Many; + return Plural.Other; + case 'is': + if (t === 0 && i % 10 === 1 && (!(i % 100 === 11) || !(t === 0))) return Plural.One; + return Plural.Other; + case 'ksh': + if (n === 0) return Plural.Zero; + if (n === 1) return Plural.One; + return Plural.Other; + case 'kw': + case 'naq': + case 'se': + case 'smn': + if (n === 1) return Plural.One; + if (n === 2) return Plural.Two; + return Plural.Other; + case 'lag': + if (n === 0) return Plural.Zero; + if ((i === 0 || i === 1) && !(n === 0)) return Plural.One; + return Plural.Other; + case 'lt': + if (n % 10 === 1 && !(n % 100 >= 11 && n % 100 <= 19)) return Plural.One; + if (n % 10 === Math.floor(n % 10) && n % 10 >= 2 && n % 10 <= 9 && + !(n % 100 >= 11 && n % 100 <= 19)) + return Plural.Few; + if (!(f === 0)) return Plural.Many; + return Plural.Other; + case 'lv': + case 'prg': + if (n % 10 === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19 || + v === 2 && f % 100 === Math.floor(f % 100) && f % 100 >= 11 && f % 100 <= 19) + return Plural.Zero; + if (n % 10 === 1 && + (!(n % 100 === 11) || + v === 2 && f % 10 === 1 && (!(f % 100 === 11) || !(v === 2) && f % 10 === 1))) + return Plural.One; + return Plural.Other; + case 'mk': + if (v === 0 && (i % 10 === 1 || f % 10 === 1)) return Plural.One; + return Plural.Other; + case 'mt': + if (n === 1) return Plural.One; + if (n === 0 || n % 100 === Math.floor(n % 100) && n % 100 >= 2 && n % 100 <= 10) + return Plural.Few; + if (n % 100 === Math.floor(n % 100) && n % 100 >= 11 && n % 100 <= 19) return Plural.Many; + return Plural.Other; + case 'pl': + if (i === 1 && v === 0) return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14)) + return Plural.Few; + if (v === 0 && !(i === 1) && + (i % 10 === Math.floor(i % 10) && i % 10 >= 0 && i % 10 <= 1 || + v === 0 && + (i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || + v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 12 && i % 100 <= 14))) + return Plural.Many; + return Plural.Other; + case 'pt': + if (n === Math.floor(n) && n >= 0 && n <= 2 && !(n === 2)) return Plural.One; + return Plural.Other; + case 'ro': + if (i === 1 && v === 0) return Plural.One; + if (!(v === 0) || n === 0 || + !(n === 1) && n % 100 === Math.floor(n % 100) && n % 100 >= 1 && n % 100 <= 19) + return Plural.Few; + return Plural.Other; + case 'ru': + case 'uk': + if (v === 0 && i % 10 === 1 && !(i % 100 === 11)) return Plural.One; + if (v === 0 && i % 10 === Math.floor(i % 10) && i % 10 >= 2 && i % 10 <= 4 && + !(i % 100 >= 12 && i % 100 <= 14)) + return Plural.Few; + if (v === 0 && (i % 10 === 0 || + v === 0 && (i % 10 === Math.floor(i % 10) && i % 10 >= 5 && i % 10 <= 9 || + v === 0 && i % 100 === Math.floor(i % 100) && i % 100 >= 11 && + i % 100 <= 14))) + return Plural.Many; + return Plural.Other; + case 'shi': + if (i === 0 || n === 1) return Plural.One; + if (n === Math.floor(n) && n >= 2 && n <= 10) return Plural.Few; + return Plural.Other; + case 'si': + if (n === 0 || n === 1 || i === 0 && f === 1) return Plural.One; + return Plural.Other; + case 'sl': + if (v === 0 && i % 100 === 1) return Plural.One; + if (v === 0 && i % 100 === 2) return Plural.Two; + if (v === 0 && + (i % 100 === Math.floor(i % 100) && i % 100 >= 3 && i % 100 <= 4 || !(v === 0))) + return Plural.Few; + return Plural.Other; + case 'tzm': + if (n === Math.floor(n) && n >= 0 && n <= 1 || n === Math.floor(n) && n >= 11 && n <= 99) + return Plural.One; + return Plural.Other; + default: + return Plural.Other; + } +} \ No newline at end of file diff --git a/modules/@angular/common/test/localization_spec.ts b/modules/@angular/common/test/localization_spec.ts new file mode 100644 index 0000000000..e3ea80bbcd --- /dev/null +++ b/modules/@angular/common/test/localization_spec.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AsyncTestCompleter, afterEach, beforeEach, ddescribe, describe, iit, inject, it, xit} from '@angular/core/testing/testing_internal'; +import {expect} from '@angular/platform-browser/testing/matchers'; + +import {NgLocaleLocalization, NgLocalization, getPluralCategory} from '../src/localization'; + + +export function main() { + describe('localization', () => { + describe('NgLocaleLocalization', () => { + it('should return the correct values for the "en" locale', () => { + const localization = new NgLocaleLocalization('en_US'); + + expect(localization.getPluralCategory(0)).toEqual('other'); + expect(localization.getPluralCategory(1)).toEqual('one'); + expect(localization.getPluralCategory(2)).toEqual('other'); + }); + + it('should return the correct values for the "ro" locale', () => { + const localization = new NgLocaleLocalization('ro'); + + expect(localization.getPluralCategory(0)).toEqual('few'); + expect(localization.getPluralCategory(1)).toEqual('one'); + expect(localization.getPluralCategory(2)).toEqual('few'); + expect(localization.getPluralCategory(12)).toEqual('few'); + expect(localization.getPluralCategory(23)).toEqual('other'); + expect(localization.getPluralCategory(1212)).toEqual('few'); + expect(localization.getPluralCategory(1223)).toEqual('other'); + }); + + it('should return the correct values for the "sr" locale', () => { + const localization = new NgLocaleLocalization('sr'); + + expect(localization.getPluralCategory(1)).toEqual('one'); + expect(localization.getPluralCategory(31)).toEqual('one'); + expect(localization.getPluralCategory(0.1)).toEqual('one'); + expect(localization.getPluralCategory(1.1)).toEqual('one'); + expect(localization.getPluralCategory(2.1)).toEqual('one'); + + expect(localization.getPluralCategory(3)).toEqual('few'); + expect(localization.getPluralCategory(33)).toEqual('few'); + expect(localization.getPluralCategory(0.2)).toEqual('few'); + expect(localization.getPluralCategory(0.3)).toEqual('few'); + expect(localization.getPluralCategory(0.4)).toEqual('few'); + expect(localization.getPluralCategory(2.2)).toEqual('few'); + + expect(localization.getPluralCategory(2.11)).toEqual('other'); + expect(localization.getPluralCategory(2.12)).toEqual('other'); + expect(localization.getPluralCategory(2.13)).toEqual('other'); + expect(localization.getPluralCategory(2.14)).toEqual('other'); + expect(localization.getPluralCategory(2.15)).toEqual('other'); + + expect(localization.getPluralCategory(0)).toEqual('other'); + expect(localization.getPluralCategory(5)).toEqual('other'); + expect(localization.getPluralCategory(10)).toEqual('other'); + expect(localization.getPluralCategory(35)).toEqual('other'); + expect(localization.getPluralCategory(37)).toEqual('other'); + expect(localization.getPluralCategory(40)).toEqual('other'); + expect(localization.getPluralCategory(0.0)).toEqual('other'); + expect(localization.getPluralCategory(0.5)).toEqual('other'); + expect(localization.getPluralCategory(0.6)).toEqual('other'); + + expect(localization.getPluralCategory(2)).toEqual('few'); + expect(localization.getPluralCategory(2.1)).toEqual('one'); + expect(localization.getPluralCategory(2.2)).toEqual('few'); + expect(localization.getPluralCategory(2.3)).toEqual('few'); + expect(localization.getPluralCategory(2.4)).toEqual('few'); + expect(localization.getPluralCategory(2.5)).toEqual('other'); + + expect(localization.getPluralCategory(20)).toEqual('other'); + expect(localization.getPluralCategory(21)).toEqual('one'); + expect(localization.getPluralCategory(22)).toEqual('few'); + expect(localization.getPluralCategory(23)).toEqual('few'); + expect(localization.getPluralCategory(24)).toEqual('few'); + expect(localization.getPluralCategory(25)).toEqual('other'); + }); + }); + + describe('getPluralCategory', () => { + it('should return plural category', () => { + const localization = new FrLocalization(); + + expect(getPluralCategory(0, ['one', 'other'], localization)).toEqual('one'); + expect(getPluralCategory(1, ['one', 'other'], localization)).toEqual('one'); + expect(getPluralCategory(5, ['one', 'other'], localization)).toEqual('other'); + }); + + it('should return discrete cases', () => { + const localization = new FrLocalization(); + + expect(getPluralCategory(0, ['one', 'other', '=0'], localization)).toEqual('=0'); + expect(getPluralCategory(1, ['one', 'other'], localization)).toEqual('one'); + expect(getPluralCategory(5, ['one', 'other', '=5'], localization)).toEqual('=5'); + expect(getPluralCategory(6, ['one', 'other', '=5'], localization)).toEqual('other'); + }); + }); + }); +} + +class FrLocalization extends NgLocalization { + getPluralCategory(value: number): string { + switch (value) { + case 0: + case 1: + return 'one'; + default: + return 'other'; + } + } +} \ No newline at end of file diff --git a/npm-shrinkwrap.clean.json b/npm-shrinkwrap.clean.json index 0ddcaa19df..5ffc11de5f 100644 --- a/npm-shrinkwrap.clean.json +++ b/npm-shrinkwrap.clean.json @@ -1660,6 +1660,17 @@ } } }, + "cldr": { + "version": "3.5.0", + "dependencies": { + "uglify-js": { + "version": "1.3.3" + }, + "underscore": { + "version": "1.3.3" + } + } + }, "cli-color": { "version": "1.1.0", "dependencies": { @@ -3283,6 +3294,9 @@ "hash.js": { "version": "1.0.3" }, + "hashish": { + "version": "0.0.4" + }, "hawk": { "version": "3.1.2" }, @@ -3990,6 +4004,17 @@ "media-typer": { "version": "0.3.0" }, + "memoizeasync": { + "version": "0.8.0", + "dependencies": { + "lru-cache": { + "version": "2.5.0" + }, + "passerror": { + "version": "0.0.2" + } + } + }, "memoizee": { "version": "0.3.10" }, @@ -4341,6 +4366,9 @@ "parseurl": { "version": "1.3.0" }, + "passerror": { + "version": "0.0.1" + }, "path-array": { "version": "1.0.0" }, @@ -4379,6 +4407,9 @@ "pbkdf2-compat": { "version": "2.0.1" }, + "pegjs": { + "version": "0.7.0" + }, "pify": { "version": "2.3.0" }, @@ -4844,6 +4875,14 @@ "send": { "version": "0.13.0" }, + "seq": { + "version": "0.3.5", + "dependencies": { + "chainsaw": { + "version": "0.0.9" + } + } + }, "sequencify": { "version": "0.0.7" }, @@ -5420,6 +5459,9 @@ "underscore.string": { "version": "2.3.3" }, + "unicoderegexp": { + "version": "0.4.1" + }, "unique-stream": { "version": "1.0.0" }, @@ -5696,9 +5738,15 @@ } } }, + "xmldom": { + "version": "0.1.19" + }, "xmlhttprequest-ssl": { "version": "1.5.1" }, + "xpath": { + "version": "0.0.7" + }, "xtend": { "version": "4.0.1" }, @@ -5730,5 +5778,5 @@ } }, "name": "angular-srcs", - "version": "2.0.0-rc.4" + "version": "2.0.0-rc.5" } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 5110f549b0..cb26470c28 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "angular-srcs", - "version": "2.0.0-rc.4", + "version": "2.0.0-rc.5", "dependencies": { "@types/angularjs": { "version": "1.5.13-alpha", @@ -2598,6 +2598,23 @@ } } }, + "cldr": { + "version": "3.5.0", + "from": "cldr@latest", + "resolved": "https://registry.npmjs.org/cldr/-/cldr-3.5.0.tgz", + "dependencies": { + "uglify-js": { + "version": "1.3.3", + "from": "uglify-js@1.3.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-1.3.3.tgz" + }, + "underscore": { + "version": "1.3.3", + "from": "underscore@1.3.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.3.3.tgz" + } + } + }, "cli-color": { "version": "1.1.0", "from": "cli-color@>=1.0.0 <2.0.0", @@ -2878,7 +2895,8 @@ }, "through2": { "version": "2.0.1", - "from": "through2@>=2.0.0 <3.0.0" + "from": "through2@2.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.1.tgz" } } }, @@ -2899,7 +2917,8 @@ }, "lodash": { "version": "4.14.2", - "from": "lodash@>=4.2.1 <5.0.0" + "from": "lodash@4.14.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.14.2.tgz" }, "readable-stream": { "version": "2.0.6", @@ -5204,6 +5223,11 @@ "from": "hash.js@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz" }, + "hashish": { + "version": "0.0.4", + "from": "hashish@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz" + }, "hawk": { "version": "3.1.2", "from": "hawk@>=3.1.0 <3.2.0", @@ -6341,6 +6365,23 @@ "from": "media-typer@0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" }, + "memoizeasync": { + "version": "0.8.0", + "from": "memoizeasync@0.8.0", + "resolved": "https://registry.npmjs.org/memoizeasync/-/memoizeasync-0.8.0.tgz", + "dependencies": { + "lru-cache": { + "version": "2.5.0", + "from": "lru-cache@2.5.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.5.0.tgz" + }, + "passerror": { + "version": "0.0.2", + "from": "passerror@0.0.2", + "resolved": "https://registry.npmjs.org/passerror/-/passerror-0.0.2.tgz" + } + } + }, "memoizee": { "version": "0.3.10", "from": "memoizee@>=0.3.9 <0.4.0", @@ -6910,6 +6951,11 @@ "from": "parseurl@>=1.3.0 <1.4.0", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz" }, + "passerror": { + "version": "0.0.1", + "from": "passerror@0.0.1", + "resolved": "https://registry.npmjs.org/passerror/-/passerror-0.0.1.tgz" + }, "path-array": { "version": "1.0.0", "from": "path-array@>=1.0.0 <2.0.0", @@ -6972,6 +7018,11 @@ "from": "pbkdf2-compat@2.0.1", "resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz" }, + "pegjs": { + "version": "0.7.0", + "from": "pegjs@0.7.0", + "resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.7.0.tgz" + }, "pify": { "version": "2.3.0", "from": "pify@>=2.0.0 <3.0.0", @@ -7727,6 +7778,18 @@ "from": "send@0.13.0", "resolved": "https://registry.npmjs.org/send/-/send-0.13.0.tgz" }, + "seq": { + "version": "0.3.5", + "from": "seq@0.3.5", + "resolved": "https://registry.npmjs.org/seq/-/seq-0.3.5.tgz", + "dependencies": { + "chainsaw": { + "version": "0.0.9", + "from": "chainsaw@>=0.0.7 <0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.0.9.tgz" + } + } + }, "sequencify": { "version": "0.0.7", "from": "sequencify@>=0.0.7 <0.1.0", @@ -8643,6 +8706,11 @@ "from": "underscore.string@>=2.3.3 <2.4.0", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz" }, + "unicoderegexp": { + "version": "0.4.1", + "from": "unicoderegexp@0.4.1", + "resolved": "https://registry.npmjs.org/unicoderegexp/-/unicoderegexp-0.4.1.tgz" + }, "unique-stream": { "version": "1.0.0", "from": "unique-stream@>=1.0.0 <2.0.0", @@ -9087,11 +9155,21 @@ } } }, + "xmldom": { + "version": "0.1.19", + "from": "xmldom@0.1.19", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz" + }, "xmlhttprequest-ssl": { "version": "1.5.1", "from": "xmlhttprequest-ssl@1.5.1", "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.1.tgz" }, + "xpath": { + "version": "0.0.7", + "from": "xpath@0.0.7", + "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.7.tgz" + }, "xtend": { "version": "4.0.1", "from": "xtend@>=4.0.0 <5.0.0", diff --git a/package.json b/package.json index 21229d8f84..44a94d020b 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "canonical-path": "0.0.2", "chokidar": "^1.1.0", "clang-format": "^1.0.32", + "cldr": "^3.5.0", "conventional-changelog": "^1.1.0", "cors": "^2.7.1", "firefox-profile": "^0.3.4", diff --git a/scripts/cldr/gen_plural_rules.js b/scripts/cldr/gen_plural_rules.js new file mode 100644 index 0000000000..183d74b1dc --- /dev/null +++ b/scripts/cldr/gen_plural_rules.js @@ -0,0 +1,174 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const cldr = require('cldr'); +// locale list +const locales = cldr.localeIds; +const langToRule = {}; +const ruleToLang = {}; +const variants = []; +const localeToVariant = {}; +const DEFAULT_RULE = `function anonymous(n\n/**/) {\nreturn"other"\n}`; + +locales.forEach(locale => { + const rule = normalizeRule(cldr.extractPluralRuleFunction(locale).toString()); + const lang = getVariantLang(locale, rule); + + if (!lang || !rule) { + return; + } + + if (!ruleToLang[rule]) { + ruleToLang[rule] = []; + } else if (ruleToLang[rule].indexOf(lang) > -1) { + return; + } + + ruleToLang[rule].push(lang); +}); + +var nextVariantCode = 'a'.charCodeAt(0); + +variants.forEach(locale => { + const rule = normalizeRule(cldr.extractPluralRuleFunction(locale).toString()); + + if (!rule) { + return; + } + + var mapTo = null; + + if (ruleToLang[rule]) { + mapTo = ruleToLang[rule][0]; + localeToVariant[locale] = mapTo; + return; + } + + if (!mapTo) { + mapTo = '_' + String.fromCharCode(nextVariantCode++); + + langToRule[mapTo] = rule; + ruleToLang[rule] = [mapTo]; + localeToVariant[locale] = mapTo; + } +}); + +console.log(generateCode()); + +function generateCode() { + checkMapping(); + + return ` +// This is generated code DO NOT MODIFY +// see angular2/script/cldr/gen_plural_rules.js + +enum Plural { + Zero, + One, + Two, + Few, + Many, + Other +} + +function getPluralCase(locale: string, n: number|string): Plural { +` + generateVars() + + generateRules() + ` +}`; +} + + +function generateRules() { + const codeParts = [` +const lang = locale.split('_')[0].toLowerCase(); + +switch (lang) {`]; + + Object.keys(ruleToLang).forEach(rule => { + const langs = ruleToLang[rule]; + codeParts.push(...langs.map(l => ` case '${l}': `)); + codeParts.push(` ${rule}`); + }); + + codeParts.push(` default: + return Plural.Other; +}`); + + return codeParts.join('\n'); +} + +function generateVars(){ + return ` +function getPluralCase(locale: string, nLike: number | string): Plural { +// TODO(vicb): lazy compute +if (typeof nLike === 'string') { + nLike = parseInt(nLike, 10); +} +const n: number = nLike as number; +const nDecimal = n.toString().replace(/^[^.]*\\.?/, ""); +const i = Math.floor(Math.abs(n)); +const v = nDecimal.length; +const f = parseInt(nDecimal, 10); +const t = parseInt(n.toString().replace(/^[^.]*\\.?|0+$/g,""), 10) || 0; +`; +} + +function checkMapping() { + if (localeToVariant.length) { + console.log(`Mapping required:`); + console.log(localeToVariant); + throw new Error('not implemented'); + } +} + + +/** + * If the language rule do not match an existing language rule, flag it as variant and handle it at the end + */ +function getVariantLang(locale, rule) { + var lang = locale.split('_')[0]; + + if (!langToRule[lang]) { + langToRule[lang] = rule; + return lang; + } + + if (langToRule[lang] === rule) { + return lang; + } + + variants.push(locale); + return null; +} + +function normalizeRule(fn) { + if (fn === DEFAULT_RULE) return; + + return fn + .replace(toRegExp('function anonymous(n\n/**/) {\n'), '') + .replace(toRegExp('var'), 'let') + .replace(toRegExp('"zero"'), ' Plural.Zero') + .replace(toRegExp('"one"'), ' Plural.One') + .replace(toRegExp('"two"'), ' Plural.Two') + .replace(toRegExp('"few"'), ' Plural.Few') + .replace(toRegExp('"many"'), ' Plural.Many') + .replace(toRegExp('"other"'), ' Plural.Other') + .replace(toRegExp('\n}'), '') + .replace(toRegExp('let'), '') + .replace(toRegExp('if(typeof n==="string")n=parseInt(n,10);'), '') + .replace(toRegExp('i=Math.floor(Math.abs(n))'), '') + .replace(/v=n.toString.*?.length/g, '') + .replace(/f=parseInt.*?\|\|0/g, '') + .replace(/t=parseInt.*?\|\|0/g, '') + .replace(/^[ ,;]*/, '') + + ';'; +} + +function toRegExp(s) { + return new RegExp(s.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'), 'g'); +} \ No newline at end of file diff --git a/tools/public_api_guard/common/index.d.ts b/tools/public_api_guard/common/index.d.ts index d9e7a12785..93aafd9479 100644 --- a/tools/public_api_guard/common/index.d.ts +++ b/tools/public_api_guard/common/index.d.ts @@ -36,6 +36,9 @@ export declare class DecimalPipe implements PipeTransform { transform(value: any, digits?: string): string; } +/** @experimental */ +export declare function getPluralCase(locale: string, nLike: number | string): Plural; + /** @stable */ export declare class HashLocationStrategy extends LocationStrategy { constructor(_platformLocation: PlatformLocation, _baseHref?: string); @@ -129,6 +132,12 @@ export declare class NgIf { constructor(_viewContainer: ViewContainerRef, _templateRef: TemplateRef); } +/** @experimental */ +export declare class NgLocaleLocalization extends NgLocalization { + constructor(_locale: string); + getPluralCategory(value: any): string; +} + /** @experimental */ export declare abstract class NgLocalization { abstract getPluralCategory(value: any): string; @@ -213,6 +222,16 @@ export declare abstract class PlatformLocation { abstract replaceState(state: any, title: string, url: string): void; } +/** @experimental */ +export declare enum Plural { + Zero = 0, + One = 1, + Two = 2, + Few = 3, + Many = 4, + Other = 5, +} + /** @deprecated */ export declare class ReplacePipe implements PipeTransform { transform(value: any, pattern: string | RegExp, replacement: Function | string): any;