Skip to content

Commit 6acadf2

Browse files
committed
refactor(@angular/build): parallelize multi-locale inlining with 2D task sharding
The new `inlineFileBatch` worker action processes multiple locales per file in memory while parsing sourcemaps and extracting AST metadata once per batch. `I18nInliner` now provides `inlineAll` with adaptive 2D task partitioning across files and locales to maximize worker thread utilization across any project topology. The application builder's `inlineI18n` step now dispatches all locales in parallel prior to executing per-locale post-bundle actions.
1 parent 204eafc commit 6acadf2

4 files changed

Lines changed: 570 additions & 104 deletions

File tree

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { BuilderContext } from '@angular-devkit/architect';
1010
import type { Metafile } from 'esbuild';
11+
import assert from 'node:assert';
1112
import { readFile } from 'node:fs/promises';
1213
import { join } from 'node:path';
1314
import {
@@ -74,7 +75,7 @@ export async function inlineI18n(
7475
);
7576

7677
try {
77-
for (const locale of i18nOptions.inlineLocales) {
78+
const localesToInline = Array.from(i18nOptions.inlineLocales, (locale) => {
7879
const localeDescription = i18nOptions.locales[locale];
7980
let translationIntegrity: string | undefined = '';
8081
for (const file of localeDescription.files) {
@@ -85,12 +86,18 @@ export async function inlineI18n(
8586
translationIntegrity += (translationIntegrity ? '|' : '') + file.integrity;
8687
}
8788

88-
// A locale specific set of files is returned from the inliner.
89-
const localeInlineResult = await inliner.inlineForLocale(
89+
return {
9090
locale,
91-
localeDescription.translation,
91+
translation: localeDescription.translation,
9292
translationIntegrity,
93-
);
93+
};
94+
});
95+
96+
const inlinedLocales = await inliner.inlineAll(localesToInline);
97+
98+
for (const locale of i18nOptions.inlineLocales) {
99+
const localeInlineResult = inlinedLocales.get(locale);
100+
assert(localeInlineResult !== undefined, 'Inlined result must exist for locale: ' + locale);
94101
const localeOutputFiles = localeInlineResult.outputFiles;
95102
inlineResult.errors.push(...localeInlineResult.errors);
96103
inlineResult.warnings.push(...localeInlineResult.warnings);

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

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,45 @@ interface InlineCodeRequest {
6464
translation?: Blob;
6565
}
6666

67+
/**
68+
* The options passed to the inliner for a batch file request
69+
*/
70+
interface InlineFileBatchRequest {
71+
/**
72+
* The filename that should be processed. The data for the file is provided to the Worker
73+
* during Worker initialization.
74+
*/
75+
filename: string;
76+
77+
/**
78+
* The locale specifiers or locale objects that should be used during the inlining process of the file.
79+
*/
80+
locales: (string | { locale: string; translation?: Blob })[];
81+
}
82+
83+
/**
84+
* The result for a single locale within a batch file request.
85+
*/
86+
interface InlineLocaleResult {
87+
locale: string;
88+
code: string;
89+
map?: string;
90+
messages: { type: 'error' | 'warning'; message: string }[];
91+
}
92+
93+
/**
94+
* The response returned from a batch file request.
95+
*/
96+
interface InlineFileBatchResult {
97+
file: string;
98+
results: InlineLocaleResult[];
99+
}
100+
67101
// Extract the application files and common options used for inline requests from the Worker context
68-
const { files, missingTranslation } = (workerData || {}) as {
102+
const { files, missingTranslation, translations } = (workerData || {}) as {
69103
files: ReadonlyMap<string, Blob>;
70104
missingTranslation: 'error' | 'warning' | 'ignore';
105+
translations?: ReadonlyMap<string, Blob>;
71106
};
72107

73108
/**
@@ -113,24 +148,30 @@ function getFileData(filename: string): Promise<CachedFileData> {
113148
}
114149

115150
/**
116-
* Deserializes the translation messages for an inline request, reusing the result for any
151+
* Deserializes the translation messages for a locale, reusing the result for any
117152
* subsequent request that targets the same locale.
118-
* @param request An inline request containing the locale and its serialized messages.
153+
* @param locale The locale identifier.
154+
* @param translation Optional serialized translation messages. If omitted, workerData.translations is used.
119155
* @returns The translation messages, or undefined if the locale has no translations.
120156
*/
121157
function loadTranslation(
122-
request: InlineFileRequest | InlineCodeRequest,
158+
locale: string,
159+
translation?: Blob,
123160
): Promise<Record<string, unknown>> | undefined {
124-
const { locale, translation } = request;
125-
if (!translation) {
161+
const translationBlob = translation ?? translations?.get(locale);
162+
if (!translationBlob) {
126163
return undefined;
127164
}
128165

129166
let messagesPromise = deserializedTranslations.get(locale);
130167
if (!messagesPromise) {
131-
messagesPromise = translation
168+
messagesPromise = translationBlob
132169
.arrayBuffer()
133-
.then((buffer) => deserialize(new Uint8Array(buffer)) as Record<string, unknown>);
170+
.then((buffer) => deserialize(new Uint8Array(buffer)) as Record<string, unknown>)
171+
.catch((error) => {
172+
deserializedTranslations.delete(locale);
173+
throw error;
174+
});
134175
deserializedTranslations.set(locale, messagesPromise);
135176
}
136177

@@ -149,8 +190,6 @@ export default async function inlineFile(request: InlineFileRequest) {
149190

150191
// Sourcemaps are parsed on demand per request rather than cached long-term to prevent
151192
// monotonic memory growth as a worker processes multiple files across the build.
152-
// When multi-locale batching is implemented, the sourcemap can be parsed once per batch and released
153-
// upon batch completion.
154193
const rawMap = await files.get(request.filename + '.map')?.text();
155194
const map = rawMap ? (JSON.parse(rawMap) as SourceMapInput) : undefined;
156195

@@ -159,7 +198,7 @@ export default async function inlineFile(request: InlineFileRequest) {
159198
map,
160199
metadata,
161200
request.locale,
162-
await loadTranslation(request),
201+
await loadTranslation(request.locale, request.translation),
163202
request.filename,
164203
);
165204

@@ -171,6 +210,50 @@ export default async function inlineFile(request: InlineFileRequest) {
171210
};
172211
}
173212

213+
/**
214+
* Inlines multiple locales and translations into a JavaScript file that contains `$localize` usage.
215+
*
216+
* @param request An InlineFileBatchRequest object representing the options for inlining.
217+
* @returns An object containing the inlined results for each requested locale.
218+
*/
219+
export async function inlineFileBatch(
220+
request: InlineFileBatchRequest,
221+
): Promise<InlineFileBatchResult> {
222+
const { code, metadata } = await getFileData(request.filename);
223+
224+
// Parse the sourcemap once for the entire batch.
225+
// It will naturally be garbage-collected after this batch action returns.
226+
const rawMap = await files.get(request.filename + '.map')?.text();
227+
const map = rawMap ? (JSON.parse(rawMap) as SourceMapInput) : undefined;
228+
229+
const results = await Promise.all(
230+
request.locales.map(async (entry) => {
231+
const locale = typeof entry === 'string' ? entry : entry.locale;
232+
const translation = typeof entry === 'string' ? undefined : entry.translation;
233+
const result = await inlineLocalize(
234+
code,
235+
map,
236+
metadata,
237+
locale,
238+
await loadTranslation(locale, translation),
239+
request.filename,
240+
);
241+
242+
return {
243+
locale,
244+
code: result.code,
245+
map: result.map,
246+
messages: result.diagnostics.messages,
247+
};
248+
}),
249+
);
250+
251+
return {
252+
file: request.filename,
253+
results,
254+
};
255+
}
256+
174257
/**
175258
* Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
176259
* This function is a secondary entry primarily for use with component HMR update modules.
@@ -185,7 +268,7 @@ export async function inlineCode(request: InlineCodeRequest) {
185268
undefined,
186269
metadata,
187270
request.locale,
188-
await loadTranslation(request),
271+
await loadTranslation(request.locale, request.translation),
189272
request.filename,
190273
);
191274

0 commit comments

Comments
 (0)