From aa9e7c615529cb9dd6dccd862674cadac0372f08 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 3 Dec 2023 11:41:06 -0500 Subject: [PATCH] fix(@angular-devkit/build-angular): normalize locale tags with Intl API when resolving in application builder All specified locales in the i18n configuration for an application use the `application` or `browser-esbuild` builders will now be normalized using the `Intl` API. This ensures that the provided locale tags are both well-formed and correctly cased. This also more easily allowed an optimization for the default locale which is already embedded into the framework and will now no longer be injected by the build process if active. (cherry picked from commit f0c032dc587286de678f83e8ecc4225a30f9f5ed) --- .../src/tools/esbuild/i18n-locale-plugin.ts | 64 ++++++++++++++----- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/i18n-locale-plugin.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/i18n-locale-plugin.ts index 751f648cf736..ddfcb50fdc75 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/i18n-locale-plugin.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/i18n-locale-plugin.ts @@ -8,6 +8,11 @@ import type { Plugin } from 'esbuild'; +/** + * The internal namespace used by generated locale import statements and Angular locale data plugin. + */ +export const LOCALE_DATA_NAMESPACE = 'angular:locale/data'; + /** * The base module location used to search for locale specific data. */ @@ -35,15 +40,39 @@ export function createAngularLocaleDataPlugin(): Plugin { build.onResolve({ filter: /^angular:locale\/data:/ }, async ({ path }) => { // Extract the locale from the path - const originalLocale = path.split(':', 3)[2]; + const rawLocaleTag = path.split(':', 3)[2]; - // Remove any private subtags since these will never match - let partialLocale = originalLocale.replace(/-x(-[a-zA-Z0-9]{1,8})+$/, ''); + // Extract and normalize the base name of the raw locale tag + let partialLocaleTag: string; + try { + const locale = new Intl.Locale(rawLocaleTag); + partialLocaleTag = locale.baseName; + } catch { + return { + path: rawLocaleTag, + namespace: LOCALE_DATA_NAMESPACE, + errors: [ + { + text: `Invalid or unsupported locale provided in configuration: "${rawLocaleTag}"`, + }, + ], + }; + } let exact = true; - while (partialLocale) { - const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocale}`; + while (partialLocaleTag) { + // Angular embeds the `en`/`en-US` locale into the framework and it does not need to be included again here. + // The onLoad hook below for the locale data namespace has an `empty` loader that will prevent inclusion. + // Angular does not contain exact locale data for `en-US` but `en` is equivalent. + if (partialLocaleTag === 'en' || partialLocaleTag === 'en-US') { + return { + path: rawLocaleTag, + namespace: LOCALE_DATA_NAMESPACE, + }; + } + // Attempt to resolve the locale tag data within the Angular base module location + const potentialPath = `${LOCALE_DATA_BASE_MODULE}/${partialLocaleTag}`; const result = await build.resolve(potentialPath, { kind: 'import-statement', resolveDir: build.initialOptions.absWorkingDir, @@ -58,39 +87,40 @@ export function createAngularLocaleDataPlugin(): Plugin { ...result.warnings, { location: null, - text: `Locale data for '${originalLocale}' cannot be found. Using locale data for '${partialLocale}'.`, + text: `Locale data for '${rawLocaleTag}' cannot be found. Using locale data for '${partialLocaleTag}'.`, }, ], }; } } - // Remove the last subtag and try again with a less specific locale - const parts = partialLocale.split('-'); - partialLocale = parts.slice(0, -1).join('-'); + // Remove the last subtag and try again with a less specific locale. + // Usually the match is exact so the string splitting here is not done until actually needed after the exact + // match fails to resolve. + const parts = partialLocaleTag.split('-'); + partialLocaleTag = parts.slice(0, -1).join('-'); exact = false; - // The locales "en" and "en-US" are considered exact to retain existing behavior - if (originalLocale === 'en-US' && partialLocale === 'en') { - exact = true; - } } // Not found so issue a warning and use an empty loader. Framework built-in `en-US` data will be used. // This retains existing behavior as in the Webpack-based builder. return { - path: originalLocale, - namespace: 'angular:locale/data', + path: rawLocaleTag, + namespace: LOCALE_DATA_NAMESPACE, warnings: [ { location: null, - text: `Locale data for '${originalLocale}' cannot be found. No locale data will be included for this locale.`, + text: `Locale data for '${rawLocaleTag}' cannot be found. No locale data will be included for this locale.`, }, ], }; }); // Locales that cannot be found will be loaded as empty content with a warning from the resolve step - build.onLoad({ filter: /./, namespace: 'angular:locale/data' }, () => ({ loader: 'empty' })); + build.onLoad({ filter: /./, namespace: LOCALE_DATA_NAMESPACE }, () => ({ + contents: '', + loader: 'empty', + })); }, }; }