From c503e2b648edb57eab6c7c41868eef2bd7c9b3d8 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 5 May 2025 11:42:47 -0400 Subject: [PATCH] fix(@angular/build): allow component HMR for templates with i18n When using the development server with the `application` build system and HMR has been enabled (default), component templates with i18n are now eligible to be hot reloaded. If translations exist within the template, they will also be translated assuming a matching translation is available. Changing the content of an i18n block may result in a missing translation warning/error. The development server continues to only support a single enabled locale. --- .../src/builders/application/execute-build.ts | 6 ++- .../build/src/builders/application/i18n.ts | 24 +++++++++ .../angular/compilation/aot-compilation.ts | 5 +- .../src/tools/esbuild/i18n-inliner-worker.ts | 49 +++++++++++++++++-- .../build/src/tools/esbuild/i18n-inliner.ts | 38 ++++++++++++++ 5 files changed, 112 insertions(+), 10 deletions(-) diff --git a/packages/angular/build/src/builders/application/execute-build.ts b/packages/angular/build/src/builders/application/execute-build.ts index 72a07d8b8307..0654cd965558 100644 --- a/packages/angular/build/src/builders/application/execute-build.ts +++ b/packages/angular/build/src/builders/application/execute-build.ts @@ -285,8 +285,10 @@ export async function executeBuild( i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined, ); - executionResult.addErrors(result.errors); - executionResult.addWarnings(result.warnings); + // Deduplicate and add errors and warnings + executionResult.addErrors([...new Set(result.errors)]); + executionResult.addWarnings([...new Set(result.warnings)]); + executionResult.addPrerenderedRoutes(result.prerenderedRoutes); executionResult.outputFiles.push(...result.additionalOutputFiles); executionResult.assetFiles.push(...result.additionalAssets); diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index 478a8893ca10..ae37efa674e4 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -140,6 +140,30 @@ export async function inlineI18n( executionResult.assetFiles = updatedAssetFiles; } + // Inline any template updates if present + if (executionResult.templateUpdates?.size) { + // The development server only allows a single locale but issue a warning if used programmatically (experimental) + // with multiple locales and template HMR. + if (i18nOptions.inlineLocales.size > 1) { + inlineResult.warnings.push( + `Component HMR updates can only be inlined with a single locale. The first locale will be used.`, + ); + } + const firstLocale = [...i18nOptions.inlineLocales][0]; + + for (const [id, content] of executionResult.templateUpdates) { + const templateUpdateResult = await inliner.inlineTemplateUpdate( + firstLocale, + i18nOptions.locales[firstLocale].translation, + content, + id, + ); + executionResult.templateUpdates.set(id, templateUpdateResult.code); + inlineResult.errors.push(...templateUpdateResult.errors); + inlineResult.warnings.push(...templateUpdateResult.warnings); + } + } + return inlineResult; } diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index adb8988e875b..a340d602577e 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -161,10 +161,7 @@ export class AotCompilation extends AngularCompilation { ); const updateText = angularCompiler.emitHmrUpdateModule(node); // If compiler cannot generate an update for the component, prevent template updates. - // Also prevent template updates if $localize is directly present which also currently - // prevents a template update at runtime. - // TODO: Support localized template update modules and remove this check. - if (updateText === null || updateText.includes('$localize')) { + if (updateText === null) { // Build is needed if a template cannot be updated templateUpdates = undefined; break; diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts index a453b335032d..9caa2ca5da45 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts @@ -16,7 +16,7 @@ import { loadEsmModule } from '../../utils/load-esm'; /** * The options passed to the inliner for each file request */ -interface InlineRequest { +interface InlineFileRequest { /** * The filename that should be processed. The data for the file is provided to the Worker * during Worker initialization. @@ -34,6 +34,31 @@ interface InlineRequest { translation?: Record; } +/** + * The options passed to the inliner for each code request + */ +interface InlineCodeRequest { + /** + * The code that should be processed. + */ + code: string; + + /** + * The filename to use in error and warning messages for the provided code. + */ + filename: string; + + /** + * The locale specifier that should be used during the inlining process of the file. + */ + locale: string; + + /** + * The translation messages for the locale that should be used during the inlining process of the file. + */ + translation?: Record; +} + // Extract the application files and common options used for inline requests from the Worker context // TODO: Evaluate overall performance difference of passing translations here as well const { files, missingTranslation, shouldOptimize } = (workerData || {}) as { @@ -47,9 +72,9 @@ const { files, missingTranslation, shouldOptimize } = (workerData || {}) as { * This function is the main entry for the Worker's action that is called by the worker pool. * * @param request An InlineRequest object representing the options for inlining - * @returns An array containing the inlined file and optional map content. + * @returns An object containing the inlined file and optional map content. */ -export default async function inlineLocale(request: InlineRequest) { +export default async function inlineFile(request: InlineFileRequest) { const data = files.get(request.filename); assert(data !== undefined, `Invalid inline request for file '${request.filename}'.`); @@ -70,6 +95,22 @@ export default async function inlineLocale(request: InlineRequest) { }; } +/** + * Inlines the provided locale and translation into JavaScript code that contains `$localize` usage. + * This function is a secondary entry primarily for use with component HMR update modules. + * + * @param request An InlineRequest object representing the options for inlining + * @returns An object containing the inlined code. + */ +export async function inlineCode(request: InlineCodeRequest) { + const result = await transformWithBabel(request.code, undefined, request); + + return { + output: result.code, + messages: result.diagnostics.messages, + }; +} + /** * A Type representing the localize tools module. */ @@ -138,7 +179,7 @@ async function createI18nPlugins(locale: string, translation: Record | undefined, + templateCode: string, + templateId: string, + ): Promise<{ code: string; errors: string[]; warnings: string[] }> { + const hasLocalize = templateCode.includes(LOCALIZE_KEYWORD); + + if (!hasLocalize) { + return { + code: templateCode, + errors: [], + warnings: [], + }; + } + + const { output, messages } = await this.#workerPool.run( + { code: templateCode, filename: templateId, locale, translation }, + { name: 'inlineCode' }, + ); + + const errors: string[] = []; + const warnings: string[] = []; + for (const message of messages) { + if (message.type === 'error') { + errors.push(message.message); + } else { + warnings.push(message.message); + } + } + + return { + code: output, + errors, + warnings, + }; + } + /** * Stops all active transformation tasks and shuts down all workers. * @returns A void promise that resolves when closing is complete.