@@ -41,18 +41,17 @@ export interface InlineCodeRequest {
4141 */
4242 translation ?: Blob | SharedArrayBuffer ;
4343
44+ translationKey ?: string ;
45+
4446 /**
4547 * How to handle missing translations.
4648 */
4749 missingTranslation ?: 'error' | 'warning' | 'ignore' ;
4850}
4951
50- /**
51- * The response returned from a code request.
52- */
53- export interface InlineCodeResult {
54- output : string ;
55- messages : { type : 'error' | 'warning' ; message : string } [ ] ;
52+ export interface InlineFileBatchLocaleEntry {
53+ translation ?: Blob | SharedArrayBuffer ;
54+ translationKey ?: string ;
5655}
5756
5857/**
@@ -77,7 +76,7 @@ export interface InlineFileBatchRequest {
7776 /**
7877 * The locale specifiers and optional translations to use during the inlining process of the file.
7978 */
80- locales : ReadonlyMap < string , Blob | SharedArrayBuffer | undefined > ;
79+ locales : ReadonlyMap < string , InlineFileBatchLocaleEntry | Blob | SharedArrayBuffer | undefined > ;
8180
8281 /**
8382 * How to handle missing translations.
@@ -101,6 +100,38 @@ export interface InlineFileBatchRequest {
101100 * all long-term worker caches are cleared.
102101 */
103102 generation ?: number ;
103+
104+ /**
105+ * Optional file contents Blob when dispatched via the shared worker pool.
106+ */
107+ fileBlob ?: Blob ;
108+
109+ /**
110+ * Optional cache key uniquely identifying the file content and AST metadata.
111+ */
112+ fileKey ?: string ;
113+
114+ /**
115+ * Optional sourcemap Blob for the file when dispatched via the shared worker pool.
116+ */
117+ mapBlob ?: Blob ;
118+ }
119+
120+ export interface InlineDiagnosticMessage {
121+ type : 'error' | 'warning' ;
122+ message : string ;
123+ }
124+
125+ export interface InlineFileResult {
126+ file : string ;
127+ code : string ;
128+ map ?: string ;
129+ messages : InlineDiagnosticMessage [ ] ;
130+ }
131+
132+ export interface InlineCodeResult {
133+ output : string ;
134+ messages : InlineDiagnosticMessage [ ] ;
104135}
105136
106137/**
@@ -110,7 +141,7 @@ export interface InlineLocaleResult {
110141 locale : string ;
111142 code ?: string ;
112143 map ?: string ;
113- messages : { type : 'error' | 'warning' ; message : string } [ ] ;
144+ messages : InlineDiagnosticMessage [ ] ;
114145}
115146
116147/**
@@ -128,6 +159,17 @@ export type InlineFileBatchResult =
128159 results : InlineLocaleResult [ ] ;
129160 } ;
130161
162+ /**
163+ * Maximum number of AST metadata structures cached in memory per worker isolate.
164+ * Bounding capacity prevents unbounded memory growth across watch rebuilds.
165+ */
166+ const MAX_CACHED_FILES = 256 ;
167+
168+ /**
169+ * Maximum number of deserialized translation dictionaries cached in memory per worker isolate.
170+ */
171+ const MAX_CACHED_TRANSLATIONS = 32 ;
172+
131173/**
132174 * Cached file data including code and extracted localization metadata.
133175 */
@@ -137,12 +179,12 @@ interface CachedFileData {
137179}
138180
139181/**
140- * Cache of file data promises keyed by filename.
182+ * Cache of file data promises keyed by `${filename}\0${hash}` or filename.
141183 */
142184const fileDataCache = new Map < string , Promise < CachedFileData > > ( ) ;
143185
144186/**
145- * Cache of deserialized translation messages keyed by locale.
187+ * Deserialized translation message dictionary cache keyed by `${locale}\0${translationKey}` or locale.
146188 */
147189const deserializedTranslations = new Map < string , Promise < Record < string , ɵParsedTranslation > > > ( ) ;
148190
@@ -152,72 +194,106 @@ const deserializedTranslations = new Map<string, Promise<Record<string, ɵParsed
152194let currentGeneration : number | undefined ;
153195
154196/**
155- * Retrieves the file data for a filename, loading and extracting localization metadata.
156- * If `cache` is true, the result is cached in `fileDataCache` across requests in this Worker .
157- * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`, allowing it
158- * to be garbage-collected once the batch request finishes.
197+ * Retrieves the code and extracted localization metadata for a file .
198+ * Caches the metadata promise in memory to avoid reparsing the AST across locales .
199+ * If `cache` is false (ephemeral), the result is not retained in `fileDataCache`,
200+ * allowing it to be garbage-collected once the batch request finishes.
159201 *
160- * @param filename The name of the file to load .
202+ * @param filename The name of the file.
161203 * @param codeBlob The source code file as a Blob.
204+ * @param fileKey Optional cache key uniquely identifying the file content.
162205 * @param cache Whether to cache the loaded file data in the Worker's long-term cache.
163- * @returns The cached or newly extracted code and localization metadata .
206+ * @returns The cached file data .
164207 */
165- function loadFileData ( filename : string , codeBlob : Blob , cache = true ) : Promise < CachedFileData > {
166- const existing = fileDataCache . get ( filename ) ;
167- if ( existing ) {
168- if ( ! cache ) {
169- fileDataCache . delete ( filename ) ;
170- }
171-
172- return existing ;
173- }
208+ function getFileData (
209+ filename : string ,
210+ codeBlob : Blob ,
211+ fileKey ?: string ,
212+ cache = true ,
213+ ) : Promise < CachedFileData > {
214+ const cacheKey = fileKey ?? filename ;
215+ let dataPromise = fileDataCache . get ( cacheKey ) ;
216+ if ( ! dataPromise ) {
217+ dataPromise = ( async ( ) => {
218+ const code = await codeBlob . text ( ) ;
174219
175- const fileDataPromise = ( async ( ) => {
176- const code = await codeBlob . text ( ) ;
177- const metadata = extractLocalizeMetadata ( filename , code ) ;
220+ return {
221+ code,
222+ metadata : extractLocalizeMetadata ( filename , code ) ,
223+ } ;
224+ } ) ( ) . catch ( ( error ) => {
225+ if ( fileDataCache . get ( cacheKey ) === dataPromise ) {
226+ fileDataCache . delete ( cacheKey ) ;
227+ }
228+ throw error ;
229+ } ) ;
178230
179- return { code, metadata } ;
180- } ) ( ) ;
231+ if ( cache ) {
232+ if ( fileDataCache . size >= MAX_CACHED_FILES ) {
233+ const oldestKey = fileDataCache . keys ( ) . next ( ) . value ;
234+ if ( oldestKey !== undefined ) {
235+ fileDataCache . delete ( oldestKey ) ;
236+ }
237+ }
181238
182- if ( cache ) {
183- fileDataPromise . catch ( ( ) => {
184- fileDataCache . delete ( filename ) ;
185- } ) ;
186- fileDataCache . set ( filename , fileDataPromise ) ;
239+ fileDataCache . set ( cacheKey , dataPromise ) ;
240+ }
241+ } else if ( cache ) {
242+ fileDataCache . delete ( cacheKey ) ;
243+ fileDataCache . set ( cacheKey , dataPromise ) ;
244+ } else {
245+ fileDataCache . delete ( cacheKey ) ;
187246 }
188247
189- return fileDataPromise ;
248+ return dataPromise ;
190249}
191250
192251/**
193252 * Deserializes or wraps the translation messages for a locale, reusing the result for any
194- * subsequent request that targets the same locale.
195- * @param locale The locale identifier.
196- * @param translation Optional serialized translation messages (SharedArrayBuffer or Blob) .
197- * @returns The translation messages, or undefined if the locale has no translations .
253+ * subsequent request that targets the same locale and translation payload .
254+ *
255+ * @param request The translation request object containing locale, translation payload, and optional key .
256+ * @param explicitTranslation Optional fallback translation payload if request is a string .
198257 */
199258function loadTranslation (
200259 locale : string ,
201260 translation ?: Blob | SharedArrayBuffer ,
261+ translationKey ?: string ,
202262) : Promise < Record < string , ɵParsedTranslation > > | undefined {
203263 if ( ! translation ) {
204264 return undefined ;
205265 }
206266
207- let messagesPromise = deserializedTranslations . get ( locale ) ;
267+ const cacheKey = translationKey ? `${ locale } \0${ translationKey } ` : undefined ;
268+ let messagesPromise = cacheKey ? deserializedTranslations . get ( cacheKey ) : undefined ;
208269 if ( ! messagesPromise ) {
209270 if ( translation instanceof Blob ) {
210271 messagesPromise = translation
211272 . arrayBuffer ( )
212273 . then ( ( buffer ) => deserialize ( new Uint8Array ( buffer ) ) as Record < string , ɵParsedTranslation > )
213274 . catch ( ( error ) => {
214- deserializedTranslations . delete ( locale ) ;
275+ if ( cacheKey && deserializedTranslations . get ( cacheKey ) === messagesPromise ) {
276+ deserializedTranslations . delete ( cacheKey ) ;
277+ }
215278 throw error ;
216279 } ) ;
217280 } else {
218281 messagesPromise = Promise . resolve ( createSharedTranslationProxy ( translation ) ) ;
219282 }
220- deserializedTranslations . set ( locale , messagesPromise ) ;
283+
284+ if ( cacheKey ) {
285+ if ( deserializedTranslations . size >= MAX_CACHED_TRANSLATIONS ) {
286+ const oldestKey = deserializedTranslations . keys ( ) . next ( ) . value ;
287+ if ( oldestKey !== undefined ) {
288+ deserializedTranslations . delete ( oldestKey ) ;
289+ }
290+ }
291+
292+ deserializedTranslations . set ( cacheKey , messagesPromise ) ;
293+ }
294+ } else if ( cacheKey ) {
295+ deserializedTranslations . delete ( cacheKey ) ;
296+ deserializedTranslations . set ( cacheKey , messagesPromise ) ;
221297 }
222298
223299 return messagesPromise ;
@@ -240,14 +316,25 @@ export async function inlineFileBatch(
240316
241317 if ( request . activeLocales ) {
242318 const activeSet = new Set ( request . activeLocales ) ;
243- for ( const locale of deserializedTranslations . keys ( ) ) {
244- if ( ! activeSet . has ( locale ) ) {
245- deserializedTranslations . delete ( locale ) ;
319+ for ( const key of deserializedTranslations . keys ( ) ) {
320+ const keyLocale = key . includes ( '\0' ) ? key . split ( '\0' , 1 ) [ 0 ] : key ;
321+ if ( ! activeSet . has ( keyLocale ) ) {
322+ deserializedTranslations . delete ( key ) ;
246323 }
247324 }
248325 }
249326
250- const { code, metadata } = await loadFileData ( request . filename , request . code , ! request . ephemeral ) ;
327+ const codeBlob = request . code ?? request . fileBlob ;
328+ if ( ! codeBlob ) {
329+ throw new Error ( `File content not provided for: ${ request . filename } ` ) ;
330+ }
331+
332+ const { code, metadata } = await getFileData (
333+ request . filename ,
334+ codeBlob ,
335+ request . fileKey ,
336+ ! request . ephemeral ,
337+ ) ;
251338
252339 // Fast path: file has no $localize call sites or locale insert sites
253340 if ( metadata . callSites . length === 0 && metadata . localeInsertSites . length === 0 ) {
@@ -263,20 +350,31 @@ export async function inlineFileBatch(
263350
264351 // Parse the sourcemap once for the entire batch if provided.
265352 // It will naturally be garbage-collected after this batch action returns.
353+ const rawMapBlob = request . map ?? request . mapBlob ;
266354 let map : SourceMapInput | undefined ;
267- if ( request . map ) {
268- const rawMap = await request . map . text ( ) ;
355+ let rawMap : string | undefined ;
356+ if ( rawMapBlob ) {
357+ rawMap = await rawMapBlob . text ( ) ;
269358 map = rawMap ? ( JSON . parse ( rawMap ) as SourceMapInput ) : undefined ;
270359 }
271360
272361 const results = await Promise . all (
273- Array . from ( request . locales , async ( [ locale , translation ] ) => {
362+ Array . from ( request . locales , async ( [ locale , entry ] ) => {
363+ const translation =
364+ entry && typeof entry === 'object' && 'translation' in entry
365+ ? entry . translation
366+ : ( entry as Blob | SharedArrayBuffer | undefined ) ;
367+ const translationKey =
368+ entry && typeof entry === 'object' && 'translationKey' in entry
369+ ? entry . translationKey
370+ : undefined ;
371+
274372 const result = await inlineLocalize (
275373 code ,
276374 map ,
277375 metadata ,
278376 locale ,
279- await loadTranslation ( locale , translation ) ,
377+ await loadTranslation ( locale , translation , translationKey ) ,
280378 request . filename ,
281379 request . missingTranslation ,
282380 ) ;
@@ -300,7 +398,7 @@ export async function inlineFileBatch(
300398 * Inlines the provided locale and translation into JavaScript code that contains `$localize` usage.
301399 * This function is a secondary entry primarily for use with component HMR update modules.
302400 *
303- * @param request An InlineRequest object representing the options for inlining
401+ * @param request An InlineCodeRequest object representing the options for inlining
304402 * @returns An object containing the inlined code.
305403 */
306404export async function inlineCode ( request : InlineCodeRequest ) : Promise < InlineCodeResult > {
@@ -310,7 +408,7 @@ export async function inlineCode(request: InlineCodeRequest): Promise<InlineCode
310408 undefined ,
311409 metadata ,
312410 request . locale ,
313- await loadTranslation ( request . locale , request . translation ) ,
411+ await loadTranslation ( request . locale , request . translation , request . translationKey ) ,
314412 request . filename ,
315413 request . missingTranslation ,
316414 ) ;
@@ -532,7 +630,7 @@ async function inlineLocalize(
532630 }
533631
534632 const outputCode = magicString . toString ( ) ;
535- let outputMap ;
633+ let outputMap : string | undefined ;
536634 if ( map ) {
537635 // A decoded map is generated here rather than an encoded one because remapping decodes its
538636 // inputs. Encoding the mappings only for remapping to immediately decode them again doubles
@@ -542,12 +640,14 @@ async function inlineLocalize(
542640 includeContent : true ,
543641 hires : 'boundary' ,
544642 } ) ;
545- outputMap = remapping ( [ { ...rawMap , version : 3 } satisfies DecodedSourceMap , map ] , ( ) => null ) ;
643+ outputMap = JSON . stringify (
644+ remapping ( [ { ...rawMap , version : 3 } satisfies DecodedSourceMap , map ] , ( ) => null ) ,
645+ ) ;
546646 }
547647
548648 return {
549649 code : outputCode ,
550- map : outputMap && JSON . stringify ( outputMap ) ,
650+ map : outputMap ,
551651 diagnostics,
552652 } ;
553653}
0 commit comments