diff --git a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts index 4286f33a1b41..778ca55bb814 100644 --- a/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner-worker.ts @@ -11,6 +11,7 @@ import { type ɵParsedTranslation, ɵisMissingTranslationError, ɵmakeTemplateObject, + ɵparseMessage, ɵtranslate, } from '@angular/localize'; import { MagicString } from 'magic-string'; @@ -335,11 +336,20 @@ type DiagnosticMessage = { type: 'error' | 'warning'; message: string }; */ function translateMessage( diagnostics: DiagnosticMessage[], - translations: Record, + translations: Record | undefined, messageParts: TemplateStringsArray, substitutions: readonly number[], missingTranslation: 'error' | 'warning' | 'ignore', ): [TemplateStringsArray, readonly number[]] { + // Fast path: untranslated locale (e.g. source locale without a translation dictionary). + // Directly parse the message parts to strip metadata and placeholder markers without + // throwing and catching a MissingTranslationError exception. + if (translations === undefined) { + const message = ɵparseMessage(messageParts, substitutions); + + return [ɵmakeTemplateObject(message.messageParts, message.messageParts), substitutions]; + } + try { return ɵtranslate(translations, messageParts, substitutions) as [ TemplateStringsArray, @@ -512,7 +522,7 @@ async function inlineLocalize( for (const callSite of metadata.callSites) { const [translatedParts, translatedSubstitutions] = translateMessage( diagnostics, - translation || {}, + translation, callSite.messageParts, callSite.expressionIndexes, translation === undefined ? 'ignore' : missingTranslation, diff --git a/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts b/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts index bbc827b94c7f..2fbb6e02ce31 100644 --- a/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts +++ b/packages/angular/build/src/tools/i18n/i18n-inliner_spec.ts @@ -136,6 +136,20 @@ describe('I18nInliner', () => { expect(findFile(outputFiles, 'main.js').text).toContain('"Hello"'); }); + it('retains the original messages with placeholders for a locale without translations', async () => { + const source = 'export const greeting = $localize`:@@greeting:Hello ${name}:NAME:!`;\n'; + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', source)], + 'en-US', + undefined, + ); + + expect(errors).toEqual([]); + expect(warnings).toEqual([]); + expect(findFile(outputFiles, 'main.js').text).toContain('`Hello ${name}!`'); + expect(findFile(outputFiles, 'main.js').text).not.toContain('$localize'); + }); + it('warns and retains the original message when a locale is missing a translation', async () => { const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( [browserFile('main.js', GREETING_SOURCE)], diff --git a/scripts/benchmarks/i18n/scenarios.mts b/scripts/benchmarks/i18n/scenarios.mts index aaebdfd47cf7..5cb16f654d51 100644 --- a/scripts/benchmarks/i18n/scenarios.mts +++ b/scripts/benchmarks/i18n/scenarios.mts @@ -541,6 +541,113 @@ export function createLargeEnterpriseScenario( }; } +/** + * 7. Massive Multichunk Scenario: + * 800 chunks total (~150 MB JS): 1 main bundle (6 MB) + 250 small chunks (40 KB) + 549 feature chunks (245 KB), + * 8 locales, 5,000 translations, sourcemaps ON. + * Stresses worker thread pool queue capacity, dispatch throughput, IPC messaging, and memory reclamation + * across hundreds of parallel files. + */ +export function createMassiveMultichunkScenario( + options: ScenarioFactoryOptions = {}, +): BenchmarkScenario { + let workload: GeneratedWorkload | undefined; + + return { + name: 'massive-multichunk', + description: 'Massive Multichunk: 800 chunks (~150 MB JS), 8 locales, sourcemaps ON', + get inputSizeBytes() { + return workload?.totalInputSizeBytes ?? 0; + }, + get localeCount() { + return DEFAULT_LOCALES_8.length; + }, + async setup() { + await initializeFixtures(); + const files: BuildOutputFile[] = []; + const totalMessages = 5000; + let messageOffset = 0; + + // 1. Dominant entry bundle: 6 MB, 1,000 messages + const mainMessages = 1000; + const { codeFile: mainCode, mapFile: mainMap } = generateSyntheticBundle({ + filename: 'main.js', + targetByteSize: 6 * 1024 * 1024, + messageCount: mainMessages, + withSourceMap: true, + messageIdOffset: messageOffset, + }); + files.push(mainCode); + if (mainMap) { + files.push(mainMap); + } + messageOffset += mainMessages; + + // 2. Small route/dialog chunks (< 100 KB): 250 chunks x 40 KB = 10 MB, 1,000 messages (4 msgs/chunk) + const smallChunkCount = 250; + const smallChunkSizeBytes = 40 * 1024; + const smallChunkMessages = 4; + for (let i = 0; i < smallChunkCount; i++) { + const { codeFile, mapFile } = generateSyntheticBundle({ + filename: `small_chunk_${i}.js`, + targetByteSize: smallChunkSizeBytes, + messageCount: smallChunkMessages, + withSourceMap: true, + messageIdOffset: messageOffset + i * smallChunkMessages, + }); + files.push(codeFile); + if (mapFile) { + files.push(mapFile); + } + } + messageOffset += smallChunkCount * smallChunkMessages; + + // 3. Medium/large feature chunks: 549 chunks x 245 KB = ~134.5 MB, 3,000 messages + const featureChunkCount = 549; + const featureChunkSizeBytes = 245 * 1024; + const remainingMessages = totalMessages - messageOffset; + const messagesPerFeatureChunk = Math.max( + 1, + Math.floor(remainingMessages / featureChunkCount), + ); + for (let i = 0; i < featureChunkCount; i++) { + const { codeFile, mapFile } = generateSyntheticBundle({ + filename: `feature_chunk_${i}.js`, + targetByteSize: featureChunkSizeBytes, + messageCount: messagesPerFeatureChunk, + withSourceMap: true, + messageIdOffset: messageOffset + i * messagesPerFeatureChunk, + }); + files.push(codeFile); + if (mapFile) { + files.push(mapFile); + } + } + + const locales = generateTranslations(DEFAULT_LOCALES_8, totalMessages); + workload = { + files, + locales, + totalInputSizeBytes: calculateInputSizeBytes(files), + }; + }, + async run() { + if (!workload) { + return; + } + const inliner = new I18nInliner({ + missingTranslation: 'warning', + maxConcurrency: options.concurrency, + }); + try { + await inliner.inlineAll(workload.files, workload.locales); + } finally { + await inliner.close(); + } + }, + }; +} + export function getAllScenarios(options: ScenarioFactoryOptions = {}): BenchmarkScenario[] { return [ createStandardAppScenario(options), @@ -549,6 +656,7 @@ export function getAllScenarios(options: ScenarioFactoryOptions = {}): Benchmark createLargeEnterpriseScenario(options), createMonolithicScenario(options), createMultiDominantScenario(options), + createMassiveMultichunkScenario(options), createPersistentCacheWarmScenario(options), ]; }