From 6128ed8449a43c6c75b0086e9eebee0e98c26b09 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:16:09 -0400 Subject: [PATCH] refactor(@angular/build): pass files directly to inlineAll in i18n inliner Pass build files directly into inlineAll and inlineForLocale via the files parameter instead of requiring them in the I18nInliner constructor options. Because file data is passed on-demand to workers per batch request via Blobs, the WorkerPool has no dependency on build files during initialization. Passing files directly to inlineAll decouples the inliner and worker pool lifecycle from individual build runs, allowing inliner and worker pool reuse across watch mode rebuilds. It also eliminates retention of previous build file buffers on the I18nInliner instance and removes the need to pass empty outputFiles arrays when only inlineTemplateUpdate is called. --- .../build/src/builders/application/i18n.ts | 5 +- .../build/src/tools/esbuild/i18n-inliner.ts | 88 ++--- .../src/tools/esbuild/i18n-inliner_spec.ts | 312 ++++++++++-------- 3 files changed, 226 insertions(+), 179 deletions(-) diff --git a/packages/angular/build/src/builders/application/i18n.ts b/packages/angular/build/src/builders/application/i18n.ts index 5d7efd7548f9..d2db9b6babd9 100644 --- a/packages/angular/build/src/builders/application/i18n.ts +++ b/packages/angular/build/src/builders/application/i18n.ts @@ -44,11 +44,10 @@ export async function inlineI18n( }> { const { i18nOptions, baseHref, cacheOptions } = options; - // Create the multi-threaded inliner with common options and the files generated from the build. + // Create the multi-threaded inliner with common options. const inliner = new I18nInliner( { missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning', - outputFiles: executionResult.outputFiles, persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined, localizeVersion: i18nOptions.localizeVersion, }, @@ -92,7 +91,7 @@ export async function inlineI18n( }; }); - const inlinedLocales = await inliner.inlineAll(localesToInline); + const inlinedLocales = await inliner.inlineAll(executionResult.outputFiles, localesToInline); for (const locale of i18nOptions.inlineLocales) { const localeInlineResult = inlinedLocales.get(locale); diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts index 4350112d0cf9..84d302428342 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner.ts @@ -99,7 +99,6 @@ async function serializeTranslation( */ export interface I18nInlinerOptions { missingTranslation: 'error' | 'warning' | 'ignore'; - outputFiles: BuildOutputFile[]; persistentCachePath?: string; localizeVersion?: string; } @@ -165,19 +164,36 @@ export class I18nInliner { #transformedFileCache: Cache | undefined; #translationCache: Cache | undefined; #generation = 0; - readonly #localizeFiles: ReadonlyMap; - readonly #unmodifiedFiles: Array; constructor( private readonly options: I18nInlinerOptions, maxThreads?: number, ) { - this.#unmodifiedFiles = []; - const { outputFiles, missingTranslation } = options; - const files = new Map(); + const { missingTranslation } = options; + + this.#workerPool = new WorkerPool({ + filename: require.resolve('./i18n-inliner-worker'), + maxThreads, + // Extract options to ensure only the named options are serialized and sent to the worker + workerData: { + missingTranslation, + }, + }); + } - const pendingMaps = []; - for (const file of outputFiles) { + #partitionFiles(files: Iterable): { + filenames: string[]; + localizeFiles: Map; + localizeMaps: Map; + unmodifiedFiles: BuildOutputFile[]; + } { + const filenames: string[] = []; + const localizeFiles = new Map(); + const localizeMaps = new Map(); + const unmodifiedFiles: BuildOutputFile[] = []; + + const pendingMaps: BuildOutputFile[] = []; + for (const file of files) { if (file.type === BuildOutputFileType.Root || file.type === BuildOutputFileType.ServerRoot) { // Skip also the server entry-point. // Skip stats and similar files. @@ -193,7 +209,8 @@ export class I18nInliner { const hasLocalize = contentBuffer.includes(LOCALIZE_KEYWORD); if (hasLocalize) { - files.set(file.path, file); + localizeFiles.set(file.path, file); + filenames.push(file.path); continue; } @@ -204,28 +221,20 @@ export class I18nInliner { continue; } - this.#unmodifiedFiles.push(file); + unmodifiedFiles.push(file); } // Check if any pending map files should be processed by checking if the parent JS file is present for (const file of pendingMaps) { - if (files.has(file.path.slice(0, -4))) { - files.set(file.path, file); + const jsPath = file.path.slice(0, -4); + if (localizeFiles.has(jsPath)) { + localizeMaps.set(jsPath, file); } else { - this.#unmodifiedFiles.push(file); + unmodifiedFiles.push(file); } } - this.#localizeFiles = files; - - this.#workerPool = new WorkerPool({ - filename: require.resolve('./i18n-inliner-worker'), - maxThreads, - // Extract options to ensure only the named options are serialized and sent to the worker - workerData: { - missingTranslation, - }, - }); + return { filenames, localizeFiles, localizeMaps, unmodifiedFiles }; } /** @@ -234,10 +243,12 @@ export class I18nInliner { * An adaptive 2D task-partitioning algorithm distributes (files x locales) work units * across all worker threads while caching AST metadata and sourcemaps in worker memory. * + * @param files The build output files to transform. * @param locales The locales and translations to inline. * @returns A map of locale names to their inlined output files and diagnostics. */ async inlineAll( + files: Iterable, locales: Iterable, ): Promise> { await this.initCache(); @@ -250,16 +261,14 @@ export class I18nInliner { return new Map(); } + const { filenames, localizeFiles, localizeMaps, unmodifiedFiles } = this.#partitionFiles(files); + const fileResultsByLocale = new Map>(); for (const { locale } of localeList) { assert(!fileResultsByLocale.has(locale), 'Duplicate locale provided to inliner: ' + locale); fileResultsByLocale.set(locale, new Map()); } - const filenames = Array.from(this.#localizeFiles.keys()).filter( - (name) => !name.endsWith('.map'), - ); - // Process locales in sliding windows to cap peak worker memory. // Ensure the window has at least enough locales to saturate all available workers on high-core machines. const windowSize = Math.max(DEFAULT_LOCALE_WINDOW_SIZE, this.#workerPool.maxThreads || 1); @@ -304,7 +313,7 @@ export class I18nInliner { const cacheChecks: Promise[] = []; for (const filename of filenames) { - const file = this.#localizeFiles.get(filename); + const file = localizeFiles.get(filename); assert(file !== undefined, 'Localize file must exist: ' + filename); const fileEntriesPromises = windowLocales.map( @@ -361,6 +370,8 @@ export class I18nInliner { // Adaptive 2D Sharding for uncached tasks in this window if (uncachedByFile.size > 0) { await this.#processUncachedBatches( + localizeFiles, + localizeMaps, uncachedByFile, fileResultsByLocale, activeLocales, @@ -381,7 +392,7 @@ export class I18nInliner { if (fileResults) { for (const filename of filenames) { - const originalFile = this.#localizeFiles.get(filename); + const originalFile = localizeFiles.get(filename); assert(originalFile !== undefined, 'Localize file must exist: ' + filename); const fileResult = fileResults.get(filename); @@ -396,7 +407,7 @@ export class I18nInliner { outputFiles.push(originalFile.clone()); } - const originalMap = this.#localizeFiles.get(filename + '.map'); + const originalMap = localizeMaps.get(filename); if (fileResult.map !== undefined) { outputFiles.push(createOutputFile(filename + '.map', fileResult.map, type)); } else if (originalMap !== undefined) { @@ -414,7 +425,7 @@ export class I18nInliner { } // Include cloned unmodified files for every locale - outputFiles.push(...this.#unmodifiedFiles.map((file) => file.clone())); + outputFiles.push(...unmodifiedFiles.map((file) => file.clone())); resultsByLocale.set(locale, { outputFiles, @@ -427,6 +438,8 @@ export class I18nInliner { } async #processUncachedBatches( + localizeFiles: Map, + localizeMaps: Map, uncachedByFile: Map, fileResultsByLocale: Map>, activeLocales?: string[], @@ -438,7 +451,7 @@ export class I18nInliner { // Extract file data and identify the heaviest file size in a single pass let maxFileSize = 0; const sortedFiles = Array.from(uncachedByFile, ([filename, entries]) => { - const codeFile = this.#localizeFiles.get(filename); + const codeFile = localizeFiles.get(filename); assert(codeFile !== undefined, 'Localize file must exist: ' + filename); const fileSize = codeFile.contents.byteLength; if (fileSize > maxFileSize) { @@ -456,7 +469,7 @@ export class I18nInliner { const workerTasks: Promise[] = []; for (const { filename, entries, codeFile, fileSize } of sortedFiles) { - const mapFile = this.#localizeFiles.get(filename + '.map'); + const mapFile = localizeMaps.get(filename); const codeBlob = new Blob([codeFile.contents]); const mapBlob = mapFile ? new Blob([mapFile.contents]) : undefined; @@ -548,20 +561,21 @@ export class I18nInliner { } /** - * Performs inlining of translations for the provided locale and translations. The files that - * are processed originate from the files passed to the class constructor and filter by presence - * of the localize function keyword. + * Performs inlining of translations for the provided locale and translations. + * + * @param files The build output files to transform. * @param locale The string representing the locale to inline. * @param translation The translation messages to use when inlining. * @param translationIntegrity An optional integrity value for the translation messages to use for caching. * @returns A promise that resolves to an array of OutputFiles representing a translated result. */ async inlineForLocale( + files: Iterable, locale: string, translation: Record | undefined, translationIntegrity?: string, ): Promise { - const results = await this.inlineAll([{ locale, translation, translationIntegrity }]); + const results = await this.inlineAll(files, [{ locale, translation, translationIntegrity }]); const result = results.get(locale); assert(result !== undefined, `Result for locale '${locale}' should be present.`); diff --git a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts b/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts index 9cc560d038a1..8059270145bb 100644 --- a/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts +++ b/packages/angular/build/src/tools/esbuild/i18n-inliner_spec.ts @@ -14,7 +14,7 @@ import path from 'node:path'; import { initializeHash } from '../../utils/hash'; import { type BuildOutputFile, BuildOutputFileType, createOutputFile } from './bundler-files'; import { createPersistentCacheStore } from './cache'; -import { I18nInliner } from './i18n-inliner'; +import { I18nInliner, type I18nInlinerOptions } from './i18n-inliner'; /** * A module that uses a `$localize` message with an explicit message identifier so that the @@ -63,25 +63,23 @@ describe('I18nInliner', () => { // A single thread is used throughout so that every file of every locale is inlined by the same // Worker. Any translation state that a Worker retains between requests is then observable. - function createInliner(outputFiles: BuildOutputFile[]): I18nInliner { - inliner = new I18nInliner({ missingTranslation: 'warning', outputFiles }, 1); + function createInliner(options?: Partial, maxThreads = 1): I18nInliner { + inliner = new I18nInliner({ missingTranslation: 'warning', ...options }, maxThreads); return inliner; } - beforeAll(async () => { - await initializeHash(); - }); - afterEach(async () => { await inliner?.close(); inliner = undefined; }); it('inlines the translations of a locale', async () => { - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', GREETING_SOURCE)], + 'fr', + { greeting: translationFor('Bonjour') }, + ); expect(errors).toEqual([]); expect(warnings).toEqual([]); @@ -90,14 +88,17 @@ describe('I18nInliner', () => { }); it('inlines the translations of each locale when several are inlined in sequence', async () => { - const localeInliner = createInliner([browserFile('main.js', GREETING_SOURCE)]); + const files = [browserFile('main.js', GREETING_SOURCE)]; + const localeInliner = createInliner(); - const french = await localeInliner.inlineForLocale('fr', { + const french = await localeInliner.inlineForLocale(files, 'fr', { greeting: translationFor('Bonjour'), }); - const german = await localeInliner.inlineForLocale('de', { greeting: translationFor('Hallo') }); + const german = await localeInliner.inlineForLocale(files, 'de', { + greeting: translationFor('Hallo'), + }); // Repeats the first locale to cover a locale being inlined again after another has been. - const frenchAgain = await localeInliner.inlineForLocale('fr', { + const frenchAgain = await localeInliner.inlineForLocale(files, 'fr', { greeting: translationFor('Bonjour'), }); @@ -107,19 +108,22 @@ describe('I18nInliner', () => { }); it('inlines the translations of a locale into every file that uses them', async () => { - const { outputFiles } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - browserFile('chunk.js', GREETING_SOURCE), - ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }); + const { outputFiles } = await createInliner().inlineForLocale( + [browserFile('main.js', GREETING_SOURCE), browserFile('chunk.js', GREETING_SOURCE)], + 'fr', + { greeting: translationFor('Bonjour') }, + ); expect(findFile(outputFiles, 'main.js').text).toContain('"Bonjour"'); expect(findFile(outputFiles, 'chunk.js').text).toContain('"Bonjour"'); }); it('retains the original messages for a locale without translations', async () => { - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - ]).inlineForLocale('en-US', undefined); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', GREETING_SOURCE)], + 'en-US', + undefined, + ); // A locale without translations is the source locale, so its messages are not missing. expect(errors).toEqual([]); @@ -128,9 +132,11 @@ describe('I18nInliner', () => { }); it('warns and retains the original message when a locale is missing a translation', async () => { - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - ]).inlineForLocale('fr', { unrelated: translationFor('Sans rapport') }); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', GREETING_SOURCE)], + 'fr', + { unrelated: translationFor('Sans rapport') }, + ); expect(errors).toEqual([]); expect(warnings.length).toBe(1); @@ -141,9 +147,16 @@ describe('I18nInliner', () => { it('replaces the locale placeholder with the locale being inlined', async () => { // The placeholder is only inlined for files that use `$localize`, which is where the build // inserts it, so the message is present alongside it here. - const { outputFiles } = await createInliner([ - browserFile('main.js', `export const locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`), - ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }); + const { outputFiles } = await createInliner().inlineForLocale( + [ + browserFile( + 'main.js', + `export const locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`, + ), + ], + 'fr', + { greeting: translationFor('Bonjour') }, + ); expect(findFile(outputFiles, 'main.js').text).toContain('"fr"'); expect(findFile(outputFiles, 'main.js').text).not.toContain('___NG_LOCALE_INSERT___'); @@ -158,10 +171,11 @@ describe('I18nInliner', () => { sourcemap: 'external', }); - const { outputFiles } = await createInliner([ - browserFile('main.js', code), - browserFile('main.js.map', map), - ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }); + const { outputFiles } = await createInliner().inlineForLocale( + [browserFile('main.js', code), browserFile('main.js.map', map)], + 'fr', + { greeting: translationFor('Bonjour') }, + ); const outputMap = JSON.parse(findFile(outputFiles, 'main.js.map').text) as { version: number; @@ -177,7 +191,7 @@ describe('I18nInliner', () => { describe('inlineTemplateUpdate', () => { it('inlines the translations of a locale into a template update', async () => { - const { code, errors, warnings } = await createInliner([]).inlineTemplateUpdate( + const { code, errors, warnings } = await createInliner().inlineTemplateUpdate( 'fr', { greeting: translationFor('Bonjour') }, GREETING_SOURCE, @@ -191,7 +205,7 @@ describe('I18nInliner', () => { }); it('retains the original messages for a locale without translations', async () => { - const { code, errors, warnings } = await createInliner([]).inlineTemplateUpdate( + const { code, errors, warnings } = await createInliner().inlineTemplateUpdate( 'en-US', undefined, GREETING_SOURCE, @@ -205,7 +219,7 @@ describe('I18nInliner', () => { it('returns the code untouched when it has no localize calls', async () => { const source = 'export const answer = 42;\n'; - const { code } = await createInliner([]).inlineTemplateUpdate( + const { code } = await createInliner().inlineTemplateUpdate( 'fr', { greeting: translationFor('Bonjour') }, source, @@ -221,7 +235,6 @@ describe('I18nInliner', () => { try { const inliner = new I18nInliner({ missingTranslation: 'error', - outputFiles: [], persistentCachePath: cacheDir, }); @@ -245,11 +258,13 @@ describe('I18nInliner', () => { }); it('safely inlines translations containing special characters, quotes, and newlines', async () => { - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - ]).inlineForLocale('fr', { - greeting: translationFor('Bonjour "mon ami" \\ \' \n '), - }); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', GREETING_SOURCE)], + 'fr', + { + greeting: translationFor('Bonjour "mon ami" \\ \' \n '), + }, + ); expect(errors).toEqual([]); expect(warnings).toEqual([]); @@ -260,11 +275,13 @@ describe('I18nInliner', () => { it('inlines translations containing placeholders', async () => { const source = 'export const welcome = (name) => $localize`:@@welcome:Hello ${name}!`;\n'; - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', source), - ]).inlineForLocale('fr', { - welcome: parsedTranslation(['Bonjour ', ' !'], ['PH'], 'Bonjour {$PH} !'), - }); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', source)], + 'fr', + { + welcome: parsedTranslation(['Bonjour ', ' !'], ['PH'], 'Bonjour {$PH} !'), + }, + ); expect(errors).toEqual([]); expect(warnings).toEqual([]); @@ -276,12 +293,14 @@ describe('I18nInliner', () => { it('inlines multiple localize calls within the same file', async () => { const source = 'export const a = $localize`:@@greeting:Hello`;\nexport const b = $localize`:@@farewell:Goodbye`;\n'; - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', source), - ]).inlineForLocale('fr', { - greeting: translationFor('Bonjour'), - farewell: translationFor('Au revoir'), - }); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', source)], + 'fr', + { + greeting: translationFor('Bonjour'), + farewell: translationFor('Au revoir'), + }, + ); expect(errors).toEqual([]); expect(warnings).toEqual([]); @@ -291,20 +310,20 @@ describe('I18nInliner', () => { }); it('inlines translations across multiple files using multiple worker threads in parallel', async () => { + const files = [ + browserFile('main.js', GREETING_SOURCE), + browserFile('chunk1.js', GREETING_SOURCE), + browserFile('chunk2.js', GREETING_SOURCE), + browserFile('chunk3.js', GREETING_SOURCE), + ]; inliner = new I18nInliner( { missingTranslation: 'warning', - outputFiles: [ - browserFile('main.js', GREETING_SOURCE), - browserFile('chunk1.js', GREETING_SOURCE), - browserFile('chunk2.js', GREETING_SOURCE), - browserFile('chunk3.js', GREETING_SOURCE), - ], }, 4, ); - const { outputFiles, errors, warnings } = await inliner.inlineForLocale('fr', { + const { outputFiles, errors, warnings } = await inliner.inlineForLocale(files, 'fr', { greeting: translationFor('Bonjour'), }); @@ -316,10 +335,14 @@ describe('I18nInliner', () => { }); it('leaves files without localize calls unmodified', async () => { - const { outputFiles } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - browserFile('other.js', 'export const answer = 42;\n'), - ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }); + const { outputFiles } = await createInliner().inlineForLocale( + [ + browserFile('main.js', GREETING_SOURCE), + browserFile('other.js', 'export const answer = 42;\n'), + ], + 'fr', + { greeting: translationFor('Bonjour') }, + ); expect(findFile(outputFiles, 'other.js').text).toBe('export const answer = 42;\n'); }); @@ -327,16 +350,18 @@ describe('I18nInliner', () => { it('inlines nested $localize calls in post-order', async () => { const source = 'export const msg = $localize`:@@outer:You selected ${$localize`:@@inner:Apple`} for delivery.`;\n'; - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', source), - ]).inlineForLocale('fr', { - inner: translationFor('Pomme'), - outer: parsedTranslation( - ['Vous avez sélectionné ', ' pour la livraison.'], - ['PH'], - 'Vous avez sélectionné {$PH} pour la livraison.', - ), - }); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', source)], + 'fr', + { + inner: translationFor('Pomme'), + outer: parsedTranslation( + ['Vous avez sélectionné ', ' pour la livraison.'], + ['PH'], + 'Vous avez sélectionné {$PH} pour la livraison.', + ), + }, + ); expect(errors).toEqual([]); expect(warnings).toEqual([]); @@ -347,7 +372,8 @@ describe('I18nInliner', () => { it('reports an error diagnostic when a $localize template has a malformed escape sequence', async () => { const source = 'export const msg = $localize`:@@id:\\unicode:`;\n'; - const { errors } = await createInliner([browserFile('main.js', source)]).inlineForLocale( + const { errors } = await createInliner().inlineForLocale( + [browserFile('main.js', source)], 'fr', {}, ); @@ -358,9 +384,12 @@ describe('I18nInliner', () => { }); it('inlines the translations of a locale when translationIntegrity is provided', async () => { - const { outputFiles, errors, warnings } = await createInliner([ - browserFile('main.js', GREETING_SOURCE), - ]).inlineForLocale('fr', { greeting: translationFor('Bonjour') }, 'sha256-test-integrity'); + const { outputFiles, errors, warnings } = await createInliner().inlineForLocale( + [browserFile('main.js', GREETING_SOURCE)], + 'fr', + { greeting: translationFor('Bonjour') }, + 'sha256-test-integrity', + ); expect(errors).toEqual([]); expect(warnings).toEqual([]); @@ -372,13 +401,13 @@ describe('I18nInliner', () => { inliner = new I18nInliner( { missingTranslation: 'warning', - outputFiles: [browserFile('main.js', GREETING_SOURCE)], localizeVersion: '20.2.0', }, 1, ); const { outputFiles, errors, warnings } = await inliner.inlineForLocale( + [browserFile('main.js', GREETING_SOURCE)], 'fr', { greeting: translationFor('Bonjour') }, 'sha256-test-integrity', @@ -391,13 +420,14 @@ describe('I18nInliner', () => { }); it('inlines multiple locales in parallel via inlineAll', async () => { - const localeInliner = createInliner([ + const files = [ browserFile('main.js', GREETING_SOURCE), browserFile('chunk.js', GREETING_SOURCE), browserFile('other.js', 'export const answer = 42;\n'), - ]); + ]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, { locale: 'de', translation: { greeting: translationFor('Hallo') } }, { locale: 'es', translation: { greeting: translationFor('Hola') } }, @@ -437,13 +467,14 @@ describe('I18nInliner', () => { sourcemap: 'external', }); - const localeInliner = createInliner([ + const files = [ browserFile('main.js', code), browserFile('main.js.map', map), browserFile('other.js', 'export const answer = 42;\n'), - ]); + ]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, { locale: 'de', translation: { greeting: translationFor('Hallo') } }, { locale: 'en-US', translation: undefined }, @@ -483,13 +514,13 @@ describe('I18nInliner', () => { const cacheDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18n-cache-test-')); try { + const files = [ + browserFile('main.js', GREETING_SOURCE), + browserFile('other.js', 'export const answer = 42;\n'), + ]; const initialInliner = new I18nInliner( { missingTranslation: 'warning', - outputFiles: [ - browserFile('main.js', GREETING_SOURCE), - browserFile('other.js', 'export const answer = 42;\n'), - ], persistentCachePath: cacheDir, }, 2, @@ -497,6 +528,7 @@ describe('I18nInliner', () => { // Pre-populate cache for 'fr' await initialInliner.inlineForLocale( + files, 'fr', { greeting: translationFor('Bonjour') }, 'integrity-fr-1', @@ -507,16 +539,12 @@ describe('I18nInliner', () => { inliner = new I18nInliner( { missingTranslation: 'warning', - outputFiles: [ - browserFile('main.js', GREETING_SOURCE), - browserFile('other.js', 'export const answer = 42;\n'), - ], persistentCachePath: cacheDir, }, 2, ); - const results = await inliner.inlineAll([ + const results = await inliner.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') }, @@ -564,15 +592,15 @@ describe('I18nInliner', () => { translation: { greeting: translationFor(`Hello ${i}`) }, })); - const inliner = new I18nInliner( + const files = [browserFile('main.js', GREETING_SOURCE)]; + inliner = new I18nInliner( { missingTranslation: 'warning', - outputFiles: [browserFile('main.js', GREETING_SOURCE)], }, 2, ); - const results = await inliner.inlineAll(locales); + const results = await inliner.inlineAll(files, locales); expect(results.size).toBe(20); for (let i = 0; i < 20; i++) { @@ -584,9 +612,10 @@ describe('I18nInliner', () => { it('injects locale data alongside ___NG_LOCALE_INSERT___ for non-English locales', async () => { const source = `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`; - const localeInliner = createInliner([browserFile('polyfills.js', source)]); + const files = [browserFile('polyfills.js', source)]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, { locale: 'en-US', translation: undefined }, ]); @@ -606,9 +635,10 @@ describe('I18nInliner', () => { it('warns and uses parent locale data when a subtag locale is not directly available', async () => { const source = `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`; - const localeInliner = createInliner([browserFile('polyfills.js', source)]); + const files = [browserFile('polyfills.js', source)]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'fr-ZZ', translation: { greeting: translationFor('Bonjour') } }, ]); @@ -624,9 +654,10 @@ describe('I18nInliner', () => { it('warns and includes no locale data when locale data cannot be found', async () => { const source = `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`; - const localeInliner = createInliner([browserFile('polyfills.js', source)]); + const files = [browserFile('polyfills.js', source)]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'xx-YY', translation: { greeting: translationFor('Test') } }, ]); @@ -642,9 +673,10 @@ describe('I18nInliner', () => { it('reports an error diagnostic when an invalid or unsupported locale is provided', async () => { const source = `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`; - const localeInliner = createInliner([browserFile('polyfills.js', source)]); + const files = [browserFile('polyfills.js', source)]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'invalid locale tag', translation: { greeting: translationFor('Test') } }, ]); @@ -658,9 +690,10 @@ describe('I18nInliner', () => { const source = `(globalThis.$localize ??= {}).locale = "___NG_LOCALE_INSERT___";\n` + `const secondary = "___NG_LOCALE_INSERT___";\n${GREETING_SOURCE}`; - const localeInliner = createInliner([browserFile('polyfills.js', source)]); + const files = [browserFile('polyfills.js', source)]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, ]); @@ -682,12 +715,10 @@ describe('I18nInliner', () => { sourcemap: 'external', }); - const localeInliner = createInliner([ - browserFile('main.js', code), - browserFile('main.js.map', map), - ]); + const files = [browserFile('main.js', code), browserFile('main.js.map', map)]; + const localeInliner = createInliner(); - const results = await localeInliner.inlineAll([ + const results = await localeInliner.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, { locale: 'de', translation: { greeting: translationFor('Hallo') } }, ]); @@ -713,13 +744,13 @@ describe('I18nInliner', () => { const source = '// $localize comment only\nexport const value = 123;\n'; try { + const files = [browserFile('main.js', source)]; const inliner1 = new I18nInliner({ missingTranslation: 'error', - outputFiles: [browserFile('main.js', source)], persistentCachePath: cacheDir, }); - const results1 = await inliner1.inlineAll([ + const results1 = await inliner1.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') }, @@ -733,11 +764,10 @@ describe('I18nInliner', () => { const inliner2 = new I18nInliner({ missingTranslation: 'error', - outputFiles: [browserFile('main.js', source)], persistentCachePath: cacheDir, }); - const results2 = await inliner2.inlineAll([ + const results2 = await inliner2.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') }, @@ -757,14 +787,14 @@ describe('I18nInliner', () => { const cacheDir = await fs.mkdtemp(path.join(os.tmpdir(), 'i18n-inliner-trans-cache-test-')); try { + const files = [browserFile('main.js', GREETING_SOURCE)]; const inliner1 = new I18nInliner({ missingTranslation: 'error', - outputFiles: [browserFile('main.js', GREETING_SOURCE)], persistentCachePath: cacheDir, }); const translationIntegrity = 'hash-test-integrity-12345'; - const results1 = await inliner1.inlineAll([ + const results1 = await inliner1.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') }, @@ -791,11 +821,10 @@ describe('I18nInliner', () => { const inliner2 = new I18nInliner({ missingTranslation: 'error', - outputFiles: [browserFile('main.js', GREETING_SOURCE)], persistentCachePath: cacheDir, }); - const results2 = await inliner2.inlineAll([ + const results2 = await inliner2.inlineAll(files, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') }, @@ -814,13 +843,16 @@ describe('I18nInliner', () => { }); it('throws an error when duplicate locales are provided to inlineAll', async () => { - const localeInliner = createInliner([browserFile('main.js', GREETING_SOURCE)]); + const localeInliner = createInliner(); await expectAsync( - localeInliner.inlineAll([ - { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, - { locale: 'fr', translation: { greeting: translationFor('Salut') } }, - ]), + localeInliner.inlineAll( + [browserFile('main.js', GREETING_SOURCE)], + [ + { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, + { locale: 'fr', translation: { greeting: translationFor('Salut') } }, + ], + ), ).toBeRejectedWithError(/Duplicate locale provided to inliner: fr/); }); @@ -828,14 +860,14 @@ describe('I18nInliner', () => { const localeInliner = new I18nInliner( { missingTranslation: 'warning', - outputFiles: [browserFile('main.js', GREETING_SOURCE)], }, 2, ); try { + const files1 = [browserFile('main.js', GREETING_SOURCE)]; // First generation - const results1 = await localeInliner.inlineAll([ + const results1 = await localeInliner.inlineAll(files1, [ { locale: 'fr', translation: { greeting: translationFor('Bonjour') } }, ]); expect(findFile(results1.get('fr')?.outputFiles ?? [], 'main.js').text).toContain( @@ -843,7 +875,8 @@ describe('I18nInliner', () => { ); // Second generation (e.g. watch mode rebuild with updated translation) - const results2 = await localeInliner.inlineAll([ + const files2 = [browserFile('main.js', GREETING_SOURCE)]; + const results2 = await localeInliner.inlineAll(files2, [ { locale: 'fr', translation: { greeting: translationFor('Salut') } }, { locale: 'de', translation: { greeting: translationFor('Hallo') } }, ]); @@ -856,16 +889,19 @@ describe('I18nInliner', () => { it('correctly escapes backticks, double quotes, and expression delimiters in translated template literals', async () => { const source = 'export const msg = $localize`:@@msg:Hello ${name}:name:!`;\n'; - const inliner = createInliner([browserFile('main.js', source)]); + const inliner = createInliner(); - const results = await inliner.inlineAll([ - { - locale: 'fr', - translation: { - msg: parsedTranslation(['Bonjour "', '` with ${injected} and \\backslash!'], ['name']), + const results = await inliner.inlineAll( + [browserFile('main.js', source)], + [ + { + locale: 'fr', + translation: { + msg: parsedTranslation(['Bonjour "', '` with ${injected} and \\backslash!'], ['name']), + }, }, - }, - ]); + ], + ); const outputText = findFile(results.get('fr')?.outputFiles ?? [], 'main.js').text; expect(outputText).toContain('`Bonjour "${name}\\` with \\${injected} and \\\\backslash!`'); @@ -879,13 +915,11 @@ describe('I18nInliner', () => { const largeFile = browserFile('main.js', largeSource); const smallFile = browserFile('chunk.js', smallSource); + const files = [largeFile, smallFile]; - inliner = new I18nInliner( - { missingTranslation: 'error', outputFiles: [largeFile, smallFile] }, - 2, - ); + inliner = new I18nInliner({ missingTranslation: 'error' }, 2); - const results = await inliner.inlineAll([ + const results = await inliner.inlineAll(files, [ { locale: 'fr', translation: {