Skip to content

Self-injected i18n locale-data import is classified as an explicit external and breaks apps that externalize @angular/common #33512

Description

@ValentinBossi

Command

serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

No response

Description

externalDependencies excludes packages from the bundle so the user can
provide them at runtime (e.g. via an import map — the documented contract,
used among others by module-federation tooling). Bare imports of the
externalized packages in main.js are therefore expected.

However, when a non-English i18n.sourceLocale is configured, the builder
itself injects a locale-data import into the polyfills bundle
(application-code-bundle.tsangular:locale/data:<code>
@angular/common/locales/global/<code>). If @angular/common is
externalized, this builder-injected import inherits the external
classification through the prefix rule (--external:@foo/bar implies
--external:@foo/bar/*) and ends up as an unresolvable bare specifier in
the polyfills bundle — in ng build output as well as in the dev server.

Unlike the user-authored imports in main.js, this import cannot reasonably
be satisfied by the user: the locale-data file is not emitted or served
under any stable URL, and the polyfills script typically executes before a
runtime-provided import map exists. The builder should treat its own
locale-data injection as internal and always make it resolvable (bundle it,
or let Vite prebundle it in the dev server), regardless of the externals
configuration.

Minimal Reproduction

ng new repro --defaults && cd repro

angular.json: add to the project "i18n": { "sourceLocale": "de-DE" } and to
the build target options "externalDependencies": ["@angular/common"].

ng build
grep -o 'import"@angular/common[^"]*"' dist/repro/browser/polyfills*.js
# → import"@angular/common/locales/global/de-DE"   (bare, unresolvable)

Comparison of the polyfills bundle (ng build):

config polyfills output
de-DE, no externalDependencies locale data bundled inline (ng.common.locales…)
de-DE + externalDependencies bare import"@angular/common/locales/global/de-DE"
en-US + externalDependencies no locale reference at all (framework built-in)

The same happens with the dev server: ng serve, then
curl http://localhost:4200/polyfills.js shows
import "@angular/common/locales/global/de"; while Vite prebundles and
rewrites all other polyfill imports. (Side observation: the dev server falls
back from de-DE to de via the locale plugin's require.resolve path,
while ng build keeps de-DE because the external option matches before
resolution — two different code paths produce different specifiers.)

Note: the browser console additionally shows a failing bare
"@angular/common" from main.js — that one is expected (the user opted
into providing it at runtime). The defect is solely the builder-injected
@angular/common/locales/global/<code> import in the polyfills bundle.

Proposed fix

Exempt the builder's own locale-data imports from the explicit-external
classification, so the dev server prebundles them exactly as in a default
setup:

// packages/angular/build/src/builders/application/execute-build.ts
import { LOCALE_DATA_BASE_MODULE } from '../../tools/esbuild/i18n-locale-plugin';

const isExplicitExternal = (dep: string): boolean => {
  // Locale-data imports are injected by the builder itself (see
  // i18n-locale-plugin) and must stay resolvable via Vite prebundling
  // even when the containing package is externalized by the user.
  if (dep.startsWith(`${LOCALE_DATA_BASE_MODULE}/`)) {
    return false;
  }
  if (exclusions.has(dep)) {
    return true;
  }
  for (const prefix of exclusionsPrefixes) {
    if (dep.startsWith(prefix)) {
      return true;
    }
  }
  return false;
};

Exception or Error

Uncaught TypeError: Failed to resolve module specifier
"@angular/common/locales/global/de". Relative references must start with
either "/", "./", or "../".

Your Environment

Angular CLI: 21.2.x
@angular/build: 21.2.x
Node: 24.x

Anything else relevant?

  • Downstream context: module-federation tooling
    (@angular-architects/native-federation) externalizes @angular/common via
    build plugins and hits exactly this classification; it currently works
    around it by forcing the polyfills bundle into packages: 'bundle' mode
    (adding a local polyfill entry), which inlines the locale data like a
    production build does.
  • User-level workaround: add any local file (e.g. an empty
    src/polyfills-switch.ts) to the polyfills array — this disables
    external packages for the polyfills bundle
    (getEsBuildCommonPolyfillsOptions) and bundles the locale data inline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions