Skip to content

feat(i18n): inline the source locale and refresh integrity hashes after translation - #173

Merged
Aukevanoost merged 1 commit into
native-federation:mainfrom
JohnYoungers:feat/i18n-inline-source-locale
Oct 28, 2026
Merged

Aukevanoost merged 1 commit into
native-federation:mainfrom
JohnYoungers:feat/i18n-inline-source-locale

Conversation

@JohnYoungers

@JohnYoungers JohnYoungers commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #170.

Problem

1. A source-only run leaves the raw $localize calls. With localize: true, Angular inlines $localize in everything it builds, including the source locale, and removes the @angular/localize/init polyfill because nothing needs the runtime any more (this is deliberate in @angular/build). The federation output (shared packages, mappings, exposed modules and their chunks) is built outside Angular, so translateFederationArtifacts translates it itself.

localize-translate writes an inlined copy of the source locale whenever it is given -l, and that already happens when another locale is translated. But when the source locale is the only locale to process (localize lists just the source locale, or no other locale is configured), the function returned early. The source locale folder then keeps the raw calls, and nothing defines the global $localize at runtime unless the app brings its own copy of the polyfill. The app fails with $localize is not a function in the source locale only.

The builder had the same limitation: it only called translateFederationArtifacts when i18n.locales was non-empty, so i18n: { sourceLocale: 'en' } with localize: true never reached it.

2. Stale integrity hashes (#170). remoteEntry.json is copied to the locale folders before localize-translate runs. Translation changes the bundles, so with features.integrityHashes on, the hashes in each locale's remoteEntry.json no longer match the files. The same hashes are written to importmap.json, which had the same problem.

Change

  • Do not return early when the source locale is requested (localize: true, or an array that contains the source locale code). The same single localize-translate run inlines it, so there is no second pass and no extra translation file: -t is passed without values and --target-locales is left out when there is no other locale.
  • The builder no longer requires a non-empty i18n.locales to call translateFederationArtifacts, and I18nConfig.locales is optional, so a config with only a sourceLocale is handled.
  • After translation, recompute the integrity entries in remoteEntry.json and importmap.json for every locale folder that was written, the source locale included. The existing algorithm prefix of each entry is kept, entries for files that are not in that folder are left alone, and a file is only rewritten when it has an integrity map.

Apps that do not use localize are unaffected (this code only runs when translation does).

Tests

  • Unit tests for: the source locale in the same run, the source-only run (no --target-locales, no remoteEntry.json copy), no configured locales, an i18n config without locales, and the integrity refresh (all written locales, remoteEntry.json and importmap.json, algorithm kept, missing file kept, no integrity map left untouched).
  • Two tests in i18n.integration.spec.ts that run the real localize-translate CLI: one checks that both the source and a translated locale have no $localize left and matching integrity hashes, the other covers the source-only run.
  • On a Windows machine 3 of the existing i18n.spec.ts tests fail with and without this change because they assert POSIX paths.

Open questions

  • With localize: ['en'] (source locale only), watch/serve rebuilds now run localize-translate over the shared bundles, which they skipped before. That is the same cost a single translated locale such as ['de'] already has on each rebuild. Is that acceptable, or should source-only runs be skipped or reduced on rebuilds?
  • Watch rebuilds run this step again without cleaning the output folder. Angular's own output from the previous cycle can already be in the source locale folder; it has no $localize calls left, so translating it again does not change it. I did not test multi-locale watch mode.

@JohnYoungers
JohnYoungers force-pushed the feat/i18n-inline-source-locale branch from bc2a5e9 to f82b3c6 Compare October 22, 2026 19:38
@JohnYoungers
JohnYoungers marked this pull request as ready for review October 22, 2026 19:39

@Aukevanoost Aukevanoost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! One thing outside the diff: builder.ts only calls translateFederationArtifacts when i18n.locales is non-empty, so the source-locale-only case (i18n: { sourceLocale: 'en' }, no locales, localize: true) is never reached from the builder. The federation bundles still keep raw $localize calls there. The hasLocales guard needs adjusting too.

expect(infoSpy).not.toHaveBeenCalled();
});

describe('source locale', () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since #176, callers pass base/browser and localePath no longer adds browser. These dist/browser/<locale> expectations (and the ones in the integration spec) will fail after a rebase on main.

}

// Translating changes the bundles, so the integrity hashes in the copied remoteEntry.json are stale
refreshIntegrity(translatedPaths);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core also writes these hashes to importmap.json next to remoteEntry.json. That file keeps the old hashes, so a host that uses it gets an SRI mismatch on the shared bundles.

Comment thread src/builders/build/i18n.ts Outdated
const inlineSourceLocale =
localize === true || (Array.isArray(localize) && localize.includes(sourceLocale));

if (locales.length === 0 && !inlineSourceLocale) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With localize: ['en'] this used to return early. Now every watch/serve rebuild starts localize-translate and re-translates all shared bundles, which adds noticeable time to each rebuild.

Comment thread src/builders/build/i18n.ts Outdated
const locales = Object.keys(i18n.locales).filter(locale => neededLocales.includes(locale));

if (locales.length === 0) {
// Angular inlines $localize in the source locale whenever it builds it, but the federation output is

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could this be one line? Also, "a run of its own" reads like a second run, but it's the same run without --target-locales.

Comment thread src/builders/build/i18n.ts Outdated
}
}

// remoteEntry.json lists a subresource integrity hash per file when the integrityHashes feature is on

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this comment only restates what the field is, so it can go.

@JohnYoungers

Copy link
Copy Markdown
Contributor Author

@Aukevanoost - thanks again for the review; I've rebased on main and pushed these updates:

  • Paths after fix(i18n): honor a custom outputPath.browser folder when translating federation output #176: the new unit and integration tests now pass dist/browser and expect dist/browser/<locale>.
  • Builder guard: hasLocales is gone, so the builder calls translateFederationArtifacts whenever there is an i18n config and a locale filter. I18nConfig.locales is now optional, so i18n: { sourceLocale: 'en' } with localize: true reaches the source-locale inlining. Added a unit test for it.
  • importmap.json: its integrity entries are now refreshed along with remoteEntry.json in each written locale folder. Added a unit test.
  • Nits: the comment on the source-locale check is one line and no longer says "a run of its own", and I removed the comment above refreshIntegrity.

One question on the rebuild cost: with localize: ['en'], each watch/serve rebuild now runs localize-translate over the shared bundles. That is the same cost localize: ['de'] already has today, and skipping it would leave raw $localize in the source locale during dev. Is that acceptable, or would you rather skip or reduce the source-only run on rebuilds? I can add that if so.

@Aukevanoost

Copy link
Copy Markdown
Contributor

Thanks for the updates! #194 was merged just after your push, and it changes the API this PR builds on, so could you rebase on main again?

  • translateFederationArtifacts now takes a list of file names (federationFiles: string[]) instead of the FederationInfo result, so the new tests need e.g. ['cmp.js'] instead of a federationResult object.
  • i18n.integration.spec.ts conflicts with main.

On the rebuild cost: let's keep it as is. Skipping the source-only run would bring back the $localize is not a function error after the first edit, since Angular drops the polyfill. It's about 0.3 s per rebuild, less than a ['de'] run already costs. Two small things:

  • Could you rename locales to targetLocales? Next to inlineSourceLocale it's easy to misread.
  • Please add an integration test that runs the ['en'] translation twice and checks the output and integrity hashes don't change.

@JohnYoungers
JohnYoungers force-pushed the feat/i18n-inline-source-locale branch from 60879d4 to 69e61fa Compare October 28, 2026 12:53
… integrity hashes after translation

localize-translate already writes an inlined source locale copy when it is given
-l, which Angular's own build does too. That only happened when another locale was
being translated, though: with localize listing just the source locale (or no other
locale configured) translateFederationArtifacts returned early and the federation
output kept its raw $localize calls. Run it for that case as well; no translation
file is needed, a bare -t is accepted.

remoteEntry.json was copied to the locale folders before translating, so its
integrity hashes no longer matched the translated files (nor the inlined source
locale). Recompute them after translation, keeping the existing algorithm.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@JohnYoungers
JohnYoungers force-pushed the feat/i18n-inline-source-locale branch from 69e61fa to 270fad6 Compare October 28, 2026 12:54
@JohnYoungers

Copy link
Copy Markdown
Contributor Author

I've pulled in the latest changes:

  • translateFederationArtifacts takes federationFiles in the tests now, and the i18n.integration.spec.ts conflict is resolved.
  • Renamed locales to targetLocales inside the function.
  • Added an integration test that runs the ['en'] translation twice with the real CLI and checks that the output and the integrity hashes don't change.

@Aukevanoost Aukevanoost left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the contribution!

@Aukevanoost
Aukevanoost merged commit 40744cc into native-federation:main Oct 28, 2026
1 check passed
Aukevanoost added a commit that referenced this pull request Oct 28, 2026
Backports 22.2.3..22.2.4 onto the Angular 21 layout.

Incoming:
- #167/#169: the dev server keeps federation externals out of the
  stylesheet build; Angular's ssr/prerender corrections reach serveWithVite.
- #166/#168: context tsconfigs pass `types` resolved instead of widening
  typeRoots.
- #173/#176/#186/#200: i18n translates every federation output, honours
  outputPath.browser, stays under the Windows command line limit, inlines
  the source locale only when requested and refreshes integrity hashes.
- #187/#190: optional-peer entry points are skipped only when the peer is
  missing; @angular/localize is an optional peer.
- #189/#195/#196: federation build results carry their entry; a path
  written twice is reported once.
- #194: @softarc/native-federation ~4.9.0, orchestrator ^4.9.0.

Angular 21 specific adaptations:
- Checked the #167/#169 assumptions against @angular/build 21.2.24:
  serveWithVite reads externalDependencies and ssr back from the options it
  passed, and the stylesheet bundler uses externalDependencies, as on 22.
- No // upstream: tags change (still angular-cli v21.2.19, 1a728258d6).
- Skipped #188/#198 (remote builder): this branch has no remote builder.
- Kept the internal builder without instrumentForCoverage; dropped it from
  the incoming spec.
- Moved output-entries and the i18n integration spec into src/utils.
- Skipped the `types: ["*"]` spec below TypeScript 6 (branch is on 5.9).
- @angular/localize optional peer/devDependency at >=21.2.0/^21.2.0.
- Kept the -v4 name, 21.2.16 version, ng21 pins and this branch's global
  pnpm overrides; main's vite 8 override doesn't apply to 21.2's vite 7.
- pnpm-lock.yaml regenerated rather than merged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stale SRI hashes after translation

2 participants