From 1e1a0cbd14d3cb6bc0062d68f46400492b13936d Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 31 Oct 2019 09:38:59 +0000 Subject: [PATCH 1/2] build(common): add global UMD export to locale files In order to support adding locales during compile-time inlining of translations (i.e. after the TS build has completed), we need to be able to attach the locale to the global scope. This commit modifies the UMD wrapper of the final locale files that appear in the `@angular/common` package to export the locale onto the global object if not in AMD or CommonJS. The locale (e.g 'en-UK') is adding to the `ng.common.locale['en-UK']` global. --- packages/common/locales/BUILD.bazel | 7 +++++++ 1 file changed, 7 insertions(+) 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"], ) From c67541855bb16d6983a70dcaa6c3aa77303a83ba Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 31 Oct 2019 12:55:54 +0000 Subject: [PATCH 2/2] feat(common): support loading locales from a global To support compile time localization, we need to be able to provide the locales via a well known global property `ng.common.locale`. This commit changes `findLocaleData()` so that it will attempt to read the local from the global if the locale has not already been registered. --- packages/common/test/i18n/format_date_spec.ts | 9 ++++- .../common/test/i18n/format_number_spec.ts | 9 ++++- .../common/test/i18n/locale_data_api_spec.ts | 36 +++++++++++++++++- .../common/test/i18n/localization_spec.ts | 9 ++++- packages/common/test/pipes/date_pipe_spec.ts | 8 ++++ .../common/test/pipes/number_pipe_spec.ts | 8 ++++ packages/core/src/i18n/locale_data_api.ts | 37 +++++++++++++++++-- 7 files changed, 109 insertions(+), 7 deletions(-) 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