Skip to content

Commit c503e2b

Browse files
committed
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.
1 parent e0d3fbe commit c503e2b

File tree

5 files changed

+112
-10
lines changed

5 files changed

+112
-10
lines changed

packages/angular/build/src/builders/application/execute-build.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,10 @@ export async function executeBuild(
285285
i18nOptions.hasDefinedSourceLocale ? i18nOptions.sourceLocale : undefined,
286286
);
287287

288-
executionResult.addErrors(result.errors);
289-
executionResult.addWarnings(result.warnings);
288+
// Deduplicate and add errors and warnings
289+
executionResult.addErrors([...new Set(result.errors)]);
290+
executionResult.addWarnings([...new Set(result.warnings)]);
291+
290292
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
291293
executionResult.outputFiles.push(...result.additionalOutputFiles);
292294
executionResult.assetFiles.push(...result.additionalAssets);

packages/angular/build/src/builders/application/i18n.ts

+24
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,30 @@ export async function inlineI18n(
140140
executionResult.assetFiles = updatedAssetFiles;
141141
}
142142

143+
// Inline any template updates if present
144+
if (executionResult.templateUpdates?.size) {
145+
// The development server only allows a single locale but issue a warning if used programmatically (experimental)
146+
// with multiple locales and template HMR.
147+
if (i18nOptions.inlineLocales.size > 1) {
148+
inlineResult.warnings.push(
149+
`Component HMR updates can only be inlined with a single locale. The first locale will be used.`,
150+
);
151+
}
152+
const firstLocale = [...i18nOptions.inlineLocales][0];
153+
154+
for (const [id, content] of executionResult.templateUpdates) {
155+
const templateUpdateResult = await inliner.inlineTemplateUpdate(
156+
firstLocale,
157+
i18nOptions.locales[firstLocale].translation,
158+
content,
159+
id,
160+
);
161+
executionResult.templateUpdates.set(id, templateUpdateResult.code);
162+
inlineResult.errors.push(...templateUpdateResult.errors);
163+
inlineResult.warnings.push(...templateUpdateResult.warnings);
164+
}
165+
}
166+
143167
return inlineResult;
144168
}
145169

packages/angular/build/src/tools/angular/compilation/aot-compilation.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,7 @@ export class AotCompilation extends AngularCompilation {
161161
);
162162
const updateText = angularCompiler.emitHmrUpdateModule(node);
163163
// If compiler cannot generate an update for the component, prevent template updates.
164-
// Also prevent template updates if $localize is directly present which also currently
165-
// prevents a template update at runtime.
166-
// TODO: Support localized template update modules and remove this check.
167-
if (updateText === null || updateText.includes('$localize')) {
164+
if (updateText === null) {
168165
// Build is needed if a template cannot be updated
169166
templateUpdates = undefined;
170167
break;

packages/angular/build/src/tools/esbuild/i18n-inliner-worker.ts

+45-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { loadEsmModule } from '../../utils/load-esm';
1616
/**
1717
* The options passed to the inliner for each file request
1818
*/
19-
interface InlineRequest {
19+
interface InlineFileRequest {
2020
/**
2121
* The filename that should be processed. The data for the file is provided to the Worker
2222
* during Worker initialization.
@@ -34,6 +34,31 @@ interface InlineRequest {
3434
translation?: Record<string, unknown>;
3535
}
3636

37+
/**
38+
* The options passed to the inliner for each code request
39+
*/
40+
interface InlineCodeRequest {
41+
/**
42+
* The code that should be processed.
43+
*/
44+
code: string;
45+
46+
/**
47+
* The filename to use in error and warning messages for the provided code.
48+
*/
49+
filename: string;
50+
51+
/**
52+
* The locale specifier that should be used during the inlining process of the file.
53+
*/
54+
locale: string;
55+
56+
/**
57+
* The translation messages for the locale that should be used during the inlining process of the file.
58+
*/
59+
translation?: Record<string, unknown>;
60+
}
61+
3762
// Extract the application files and common options used for inline requests from the Worker context
3863
// TODO: Evaluate overall performance difference of passing translations here as well
3964
const { files, missingTranslation, shouldOptimize } = (workerData || {}) as {
@@ -47,9 +72,9 @@ const { files, missingTranslation, shouldOptimize } = (workerData || {}) as {
4772
* This function is the main entry for the Worker's action that is called by the worker pool.
4873
*
4974
* @param request An InlineRequest object representing the options for inlining
50-
* @returns An array containing the inlined file and optional map content.
75+
* @returns An object containing the inlined file and optional map content.
5176
*/
52-
export default async function inlineLocale(request: InlineRequest) {
77+
export default async function inlineFile(request: InlineFileRequest) {
5378
const data = files.get(request.filename);
5479

5580
assert(data !== undefined, `Invalid inline request for file '${request.filename}'.`);
@@ -70,6 +95,22 @@ export default async function inlineLocale(request: InlineRequest) {
7095
};
7196
}
7297

98+
/**
99+
* Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
100+
* This function is a secondary entry primarily for use with component HMR update modules.
101+
*
102+
* @param request An InlineRequest object representing the options for inlining
103+
* @returns An object containing the inlined code.
104+
*/
105+
export async function inlineCode(request: InlineCodeRequest) {
106+
const result = await transformWithBabel(request.code, undefined, request);
107+
108+
return {
109+
output: result.code,
110+
messages: result.diagnostics.messages,
111+
};
112+
}
113+
73114
/**
74115
* A Type representing the localize tools module.
75116
*/
@@ -138,7 +179,7 @@ async function createI18nPlugins(locale: string, translation: Record<string, unk
138179
async function transformWithBabel(
139180
code: string,
140181
map: SourceMapInput | undefined,
141-
options: InlineRequest,
182+
options: InlineFileRequest,
142183
) {
143184
let ast;
144185
try {

packages/angular/build/src/tools/esbuild/i18n-inliner.ts

+38
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,44 @@ export class I18nInliner {
209209
};
210210
}
211211

212+
async inlineTemplateUpdate(
213+
locale: string,
214+
translation: Record<string, unknown> | undefined,
215+
templateCode: string,
216+
templateId: string,
217+
): Promise<{ code: string; errors: string[]; warnings: string[] }> {
218+
const hasLocalize = templateCode.includes(LOCALIZE_KEYWORD);
219+
220+
if (!hasLocalize) {
221+
return {
222+
code: templateCode,
223+
errors: [],
224+
warnings: [],
225+
};
226+
}
227+
228+
const { output, messages } = await this.#workerPool.run(
229+
{ code: templateCode, filename: templateId, locale, translation },
230+
{ name: 'inlineCode' },
231+
);
232+
233+
const errors: string[] = [];
234+
const warnings: string[] = [];
235+
for (const message of messages) {
236+
if (message.type === 'error') {
237+
errors.push(message.message);
238+
} else {
239+
warnings.push(message.message);
240+
}
241+
}
242+
243+
return {
244+
code: output,
245+
errors,
246+
warnings,
247+
};
248+
}
249+
212250
/**
213251
* Stops all active transformation tasks and shuts down all workers.
214252
* @returns A void promise that resolves when closing is complete.

0 commit comments

Comments
 (0)