diff --git a/packages/common/locales/BUILD.bazel b/packages/common/locales/BUILD.bazel index 3a36a04ad91c4..a1f0bd258fd70 100644 --- a/packages/common/locales/BUILD.bazel +++ b/packages/common/locales/BUILD.bazel @@ -18,6 +18,7 @@ npm_package( # Workaround for `.d.ts`` containing `/// ` # which are generated in TypeScript v2.9, but not before. "/// ": "", + # Workaround for https://github.com/angular/angular/issues/23217 # Webpack will detect that the UMD outputs from TypeScript pass the # `require` function into the module, and cannot accurately track @@ -25,6 +26,12 @@ npm_package( # We don't actually import anything in the locale code so we can # null out the require reference passed into the module. "factory\(require, exports\)": "factory(null, exports)", + + # Attach the locale to the global scope at `ng.common.locale...` if not UMD or CommonJS + "\(function \(factory\) {": "(function (root, factory) {", + "}\)\(function \(require, exports\) {": "})(typeof globalThis !== \"undefined\" && globalThis || typeof global !== \"undefined\" && global || typeof window !== \"undefined\" && window || this, function (require, exports) {", + #11111111111111111111111111111111111111111111111111111111222221111111111111111111111111111111111333333331111111111111111111111111111111111111111111114444411 + "(if \(typeof define === \"function\" && define.amd\) {\n(\s*)define\(\"@angular/common/locales/([^\"]+)\", \[\"require\", \"exports\"], factory\);\n(\s*)})": "$1 else {\n$2if (typeof root.ng === \"undefined\") root.ng = {};if (typeof root.ng.common === \"undefined\") root.ng.common = {};if (typeof root.ng.common.locale === \"undefined\") root.ng.common.locale = {};var container = {};factory(null, container);root.ng.common.locale[\"$3\"] = container.default;\n$4}", }, deps = [":locales"], ) diff --git a/packages/common/test/i18n/format_date_spec.ts b/packages/common/test/i18n/format_date_spec.ts index 085f81375ad7e..a0e78586c2998 100644 --- a/packages/common/test/i18n/format_date_spec.ts +++ b/packages/common/test/i18n/format_date_spec.ts @@ -15,7 +15,7 @@ import localeHu from '@angular/common/locales/hu'; import localeSr from '@angular/common/locales/sr'; import localeTh from '@angular/common/locales/th'; import {isDate, toDate, formatDate} from '@angular/common/src/i18n/format_date'; -import {ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core'; +import {ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵLOCALE_DATA} from '@angular/core'; describe('Format date', () => { describe('toDate', () => { @@ -64,6 +64,13 @@ describe('Format date', () => { registerLocaleData(localeAr); }); + afterAll(() => { + // Clear out the loaded locales + for (const key in ɵLOCALE_DATA) { + delete ɵLOCALE_DATA[key]; + } + }); + beforeEach(() => { date = new Date(2015, 5, 15, 9, 3, 1, 550); }); it('should format each component correctly', () => { diff --git a/packages/common/test/i18n/format_number_spec.ts b/packages/common/test/i18n/format_number_spec.ts index 9a8bd07b99432..8b9bf3a860b32 100644 --- a/packages/common/test/i18n/format_number_spec.ts +++ b/packages/common/test/i18n/format_number_spec.ts @@ -12,7 +12,7 @@ import localeFr from '@angular/common/locales/fr'; import localeAr from '@angular/common/locales/ar'; import {formatCurrency, formatNumber, formatPercent, registerLocaleData} from '@angular/common'; import {describe, expect, it} from '@angular/core/testing/src/testing_internal'; -import {ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID} from '@angular/core'; +import {ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵLOCALE_DATA} from '@angular/core'; describe('Format number', () => { beforeAll(() => { @@ -22,6 +22,13 @@ describe('Format number', () => { registerLocaleData(localeAr); }); + afterAll(() => { + // Clear out the loaded locales + for (const key in ɵLOCALE_DATA) { + delete ɵLOCALE_DATA[key]; + } + }); + describe('Number', () => { describe('transform', () => { it('should return correct value for numbers', () => { diff --git a/packages/common/test/i18n/locale_data_api_spec.ts b/packages/common/test/i18n/locale_data_api_spec.ts index 6babf0bd2080b..178574657d91d 100644 --- a/packages/common/test/i18n/locale_data_api_spec.ts +++ b/packages/common/test/i18n/locale_data_api_spec.ts @@ -6,18 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵfindLocaleData as findLocaleData} from '@angular/core'; +import {ɵLOCALE_DATA, ɵLocaleDataIndex, ɵfindLocaleData as findLocaleData, ɵglobal} from '@angular/core'; import localeCaESVALENCIA from '@angular/common/locales/ca-ES-VALENCIA'; import localeEn from '@angular/common/locales/en'; import localeFr from '@angular/common/locales/fr'; import localeZh from '@angular/common/locales/zh'; import localeFrCA from '@angular/common/locales/fr-CA'; import localeEnAU from '@angular/common/locales/en-AU'; +import localeDe from '@angular/common/locales/de'; +import localeDeCH from '@angular/common/locales/de-CH'; +import localeDeExtra from '@angular/common/locales/extra/de'; import {registerLocaleData} from '../../src/i18n/locale_data'; import {getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrencyDigits} from '../../src/i18n/locale_data_api'; { describe('locale data api', () => { + const fakeGlobalFr: any[] = []; beforeAll(() => { registerLocaleData(localeCaESVALENCIA); registerLocaleData(localeEn); @@ -27,6 +31,19 @@ import {getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrency registerLocaleData(localeFrCA, 'fake_Id2'); registerLocaleData(localeZh); registerLocaleData(localeEnAU); + ɵglobal.ng = {common: {locale: {}}}; + ɵglobal.ng.common.locale['fr'] = fakeGlobalFr; + ɵglobal.ng.common.locale['de'] = localeDe; + ɵglobal.ng.common.locale['de-CH'] = localeDeCH; + ɵglobal.ng.common.locale['extra/de'] = localeDeExtra; + }); + + afterAll(() => { + // Clear out the loaded locales + delete ɵglobal.ng.common.locale; + for (const key in ɵLOCALE_DATA) { + delete ɵLOCALE_DATA[key]; + } }); describe('findLocaleData', () => { @@ -55,6 +72,23 @@ import {getCurrencySymbol, getLocaleDateFormat, FormatWidth, getNumberOfCurrency expect(findLocaleData('fake_iD')).toEqual(localeFr); expect(findLocaleData('fake-id2')).toEqual(localeFrCA); }); + + it('should find the exact LOCALE_DATA if the locale is on the global object', + () => { expect(findLocaleData('de-CH')).toEqual(localeDeCH); }); + + it('should find the parent LOCALE_DATA if the exact locale is not available and the parent locale is on the global object', + () => { expect(findLocaleData('de-BE')).toEqual(localeDe); }); + + it('should add the extra LOCALE_DATA if the locale and the extra locale are on the global object', + () => { + expect(findLocaleData('de')[ɵLocaleDataIndex.ExtraData]).toEqual(localeDeExtra); + }); + + it('should not add the parent extra LOCALE_DATA if the exact extra locale is not the global object', + () => { expect(findLocaleData('de-CH')[ɵLocaleDataIndex.ExtraData]).toBeUndefined(); }); + + it('should find the registered LOCALE_DATA even if the same locale is on the global object', + () => { expect(findLocaleData('fr')).not.toBe(fakeGlobalFr); }); }); describe('getting currency symbol', () => { diff --git a/packages/common/test/i18n/localization_spec.ts b/packages/common/test/i18n/localization_spec.ts index 66d8f7d6b6c4d..1ba0d10ece209 100644 --- a/packages/common/test/i18n/localization_spec.ts +++ b/packages/common/test/i18n/localization_spec.ts @@ -10,7 +10,7 @@ import localeRo from '@angular/common/locales/ro'; import localeSr from '@angular/common/locales/sr'; import localeZgh from '@angular/common/locales/zgh'; import localeFr from '@angular/common/locales/fr'; -import {LOCALE_ID} from '@angular/core'; +import {LOCALE_ID, ɵLOCALE_DATA} from '@angular/core'; import {TestBed, inject} from '@angular/core/testing'; import {NgLocaleLocalization, NgLocalization, getPluralCategory} from '@angular/common/src/i18n/localization'; import {registerLocaleData} from '../../src/i18n/locale_data'; @@ -24,6 +24,13 @@ import {registerLocaleData} from '../../src/i18n/locale_data'; registerLocaleData(localeFr); }); + afterAll(() => { + // Clear out the loaded locales + for (const key in ɵLOCALE_DATA) { + delete ɵLOCALE_DATA[key]; + } + }); + describe('NgLocalization', () => { function roTests() { it('should return plural cases for the provided locale', diff --git a/packages/common/test/pipes/date_pipe_spec.ts b/packages/common/test/pipes/date_pipe_spec.ts index 5b84bb45c67b8..a862ce90984e5 100644 --- a/packages/common/test/pipes/date_pipe_spec.ts +++ b/packages/common/test/pipes/date_pipe_spec.ts @@ -11,6 +11,7 @@ import localeEn from '@angular/common/locales/en'; import localeEnExtra from '@angular/common/locales/extra/en'; import {PipeResolver} from '@angular/compiler/src/pipe_resolver'; import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_reflector'; +import {ɵLOCALE_DATA} from '@angular/core'; { let date: Date; @@ -25,6 +26,13 @@ import {JitReflector} from '@angular/platform-browser-dynamic/src/compiler_refle beforeAll(() => { registerLocaleData(localeEn, localeEnExtra); }); + afterAll(() => { + // Clear out the loaded locales + for (const key in ɵLOCALE_DATA) { + delete ɵLOCALE_DATA[key]; + } + }); + beforeEach(() => { date = new Date(2015, 5, 15, 9, 3, 1, 550); pipe = new DatePipe('en-US'); diff --git a/packages/common/test/pipes/number_pipe_spec.ts b/packages/common/test/pipes/number_pipe_spec.ts index 3dee0a97f146f..2448f86d3f7db 100644 --- a/packages/common/test/pipes/number_pipe_spec.ts +++ b/packages/common/test/pipes/number_pipe_spec.ts @@ -13,6 +13,7 @@ import localeAr from '@angular/common/locales/ar'; import localeDeAt from '@angular/common/locales/de-AT'; import {registerLocaleData, CurrencyPipe, DecimalPipe, PercentPipe, formatNumber} from '@angular/common'; import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testing_internal'; +import {ɵLOCALE_DATA} from '@angular/core'; { describe('Number pipes', () => { @@ -24,6 +25,13 @@ import {beforeEach, describe, expect, it} from '@angular/core/testing/src/testin registerLocaleData(localeDeAt); }); + afterAll(() => { + // Clear out the loaded locales + for (const key in ɵLOCALE_DATA) { + delete ɵLOCALE_DATA[key]; + } + }); + describe('DecimalPipe', () => { describe('transform', () => { let pipe: DecimalPipe; diff --git a/packages/core/src/i18n/locale_data_api.ts b/packages/core/src/i18n/locale_data_api.ts index cc30c203484c2..9f82507681c5b 100644 --- a/packages/core/src/i18n/locale_data_api.ts +++ b/packages/core/src/i18n/locale_data_api.ts @@ -8,6 +8,7 @@ import {LOCALE_DATA, LocaleDataIndex} from './locale_data'; import localeEn from './locale_en'; +import {global} from '../util/global'; /** * Retrieves the plural function used by ICU expressions to determine the plural case to use @@ -30,16 +31,16 @@ export function getLocalePluralCase(locale: string): (value: number) => number { * @see [Internationalization (i18n) Guide](https://angular.io/guide/i18n) */ export function findLocaleData(locale: string): any { - const normalizedLocale = locale.toLowerCase().replace(/_/g, '-'); + const normalizedLocale = normalizeLocale(locale); - let match = LOCALE_DATA[normalizedLocale]; + let match = getLocaleData(normalizedLocale); if (match) { return match; } // let's try to find a parent locale const parentLocale = normalizedLocale.split('-')[0]; - match = LOCALE_DATA[parentLocale]; + match = getLocaleData(parentLocale); if (match) { return match; @@ -51,3 +52,33 @@ export function findLocaleData(locale: string): any { throw new Error(`Missing locale data for the locale "${locale}".`); } + +function getLocaleData(normalizedLocale: string): any { + if (normalizedLocale in LOCALE_DATA) { + return LOCALE_DATA[normalizedLocale]; + } + + if (typeof global.ng === 'undefined') global.ng = {}; + if (typeof global.ng.common === 'undefined') global.ng.common = {}; + if (typeof global.ng.common.locale === 'undefined') global.ng.common.locale = {}; + + // The locale names on the global object are not normalized, so we have to do a search. + // This is only once per requested locale; after that it is cached on LOCALE_DATA. + // Also generally only one or very few locales should be loaded onto the global. + for (const l in global.ng.common.locale) { + if (normalizeLocale(l) === normalizedLocale) { + const localeData = LOCALE_DATA[normalizedLocale] = global.ng.common.locale[l]; + if (localeData !== undefined) { + localeData[LocaleDataIndex.ExtraData] = global.ng.common.locale[`extra/${l}`]; + return localeData; + } + } + } + + return undefined; +} + + +function normalizeLocale(locale: string): string { + return locale.toLowerCase().replace(/_/g, '-'); +} \ No newline at end of file