Problem
The @fundamental-ngx/i18n package bundles all 36 languages into a single FESM module (~1,284 KB). Any app that imports from @fundamental-ngx/core pulls in the entire i18n package as a transitive dependency — even if the app only needs English.
Impact: 1.2 MB of unused translation data in the initial bundle for single-language apps. In our case, this was 59% of a 2.7 MB initial bundle.
Root causes
- No secondary entry points — there is no way to import
@fundamental-ngx/i18n/en or @fundamental-ngx/i18n/languages/english. The only entry point is the root, which re-exports everything.
- Barrel export re-exports all languages —
src/lib/languages/index.ts does export * for all 36 language files, preventing tree-shaking.
- No
exports field in package.json — modern bundlers cannot resolve subpaths.
- Core components depend on the full i18n package — e.g.,
@fundamental-ngx/core/busy-indicator, toolbar, and message-strip import from @fundamental-ngx/i18n, which pulls all languages into the initial chunk.
Bundle analysis evidence
node_modules/@fundamental-ngx/i18n/fesm2022/fundamental-ngx-i18n.mjs — 1,284 KB (in initial chunk)
Each individual language is ~30-35 KB, and the core tokens/pipes/resolver are ~5 KB. The remaining ~1,100 KB is 35 languages that aren't used.
Proposed solution
1. Add secondary entry points per language
@fundamental-ngx/i18n → core only (tokens, pipes, resolver, types — ~5 KB)
@fundamental-ngx/i18n/en → English translations
@fundamental-ngx/i18n/de → German translations
@fundamental-ngx/i18n/fr → French translations
...
2. Add exports field to package.json
{
"exports": {
".": { "default": "./fesm2022/fundamental-ngx-i18n.mjs" },
"./en": { "default": "./fesm2022/fundamental-ngx-i18n-en.mjs" },
"./de": { "default": "./fesm2022/fundamental-ngx-i18n-de.mjs" }
}
}
3. Decouple core component imports from language data
Core components should only import the injection tokens and types from @fundamental-ngx/i18n — not the language constants. The FD_LANGUAGE_SIGNAL factory should use a minimal English fallback (or empty object) without importing the full FD_LANGUAGE_ENGLISH constant from the barrel.
4. Consider dynamic language loading by default
The FD_LANGUAGE_SIGNAL factory could detect LOCALE_ID at runtime and import() the matching language chunk lazily, making all language data lazy-loaded by default:
// Instead of eagerly importing FD_LANGUAGE_ENGLISH
factory: () => {
const locale = inject(LOCALE_ID);
const lang = signal(MINIMAL_FALLBACK);
import(`@fundamental-ngx/i18n/${locale}`).then(m => lang.set(m.default));
return lang;
}
Current workaround
We replaced the FESM in node_modules with an English-only shim (~31 KB) via a postinstall script. This reduced the initial bundle from 2.70 MB to 1.37 MB. The shim exports the same public API surface but only includes English translations.
Expected bundle impact of the fix
| Scenario |
Current |
With fix |
| Single-language app (en) |
1,284 KB |
~35 KB |
| Two languages (en + de) |
1,284 KB |
~70 KB |
| All languages |
1,284 KB |
1,284 KB (no regression) |
Additional context
- Angular 21 +
@angular/build:application (Vite + esbuild)
@fundamental-ngx/i18n version 0.62.x
- tsconfig
paths aliases do NOT work as a workaround because esbuild resolves node_modules before checking paths for packages that exist on disk
Problem
The
@fundamental-ngx/i18npackage bundles all 36 languages into a single FESM module (~1,284 KB). Any app that imports from@fundamental-ngx/corepulls in the entire i18n package as a transitive dependency — even if the app only needs English.Impact: 1.2 MB of unused translation data in the initial bundle for single-language apps. In our case, this was 59% of a 2.7 MB initial bundle.
Root causes
@fundamental-ngx/i18n/enor@fundamental-ngx/i18n/languages/english. The only entry point is the root, which re-exports everything.src/lib/languages/index.tsdoesexport *for all 36 language files, preventing tree-shaking.exportsfield in package.json — modern bundlers cannot resolve subpaths.@fundamental-ngx/core/busy-indicator,toolbar, andmessage-stripimport from@fundamental-ngx/i18n, which pulls all languages into the initial chunk.Bundle analysis evidence
Each individual language is ~30-35 KB, and the core tokens/pipes/resolver are ~5 KB. The remaining ~1,100 KB is 35 languages that aren't used.
Proposed solution
1. Add secondary entry points per language
2. Add
exportsfield to package.json{ "exports": { ".": { "default": "./fesm2022/fundamental-ngx-i18n.mjs" }, "./en": { "default": "./fesm2022/fundamental-ngx-i18n-en.mjs" }, "./de": { "default": "./fesm2022/fundamental-ngx-i18n-de.mjs" } } }3. Decouple core component imports from language data
Core components should only import the injection tokens and types from
@fundamental-ngx/i18n— not the language constants. TheFD_LANGUAGE_SIGNALfactory should use a minimal English fallback (or empty object) without importing the fullFD_LANGUAGE_ENGLISHconstant from the barrel.4. Consider dynamic language loading by default
The
FD_LANGUAGE_SIGNALfactory could detectLOCALE_IDat runtime andimport()the matching language chunk lazily, making all language data lazy-loaded by default:Current workaround
We replaced the FESM in
node_moduleswith an English-only shim (~31 KB) via a postinstall script. This reduced the initial bundle from 2.70 MB to 1.37 MB. The shim exports the same public API surface but only includes English translations.Expected bundle impact of the fix
Additional context
@angular/build:application(Vite + esbuild)@fundamental-ngx/i18nversion 0.62.xpathsaliases do NOT work as a workaround because esbuild resolvesnode_modulesbefore checking paths for packages that exist on disk