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', + })); }, }; }