@@ -15,9 +15,10 @@ const {
1515 createLogger,
1616} = require ( './utils' ) ;
1717
18- function createChangeFeedbackWatcher ( projectRoot ) {
18+ function createChangeFeedbackWatcher ( projectRoot , options = { } ) {
1919 const DUPLICATE_LOG_WINDOW_MS = 2000 ;
2020 const TWIG_DEBOUNCE_MS = 90 ;
21+ const TRANSLATION_DEBOUNCE_MS = 180 ;
2122 const rootPath = path . resolve ( projectRoot ) ;
2223 const storefrontApp = resolveStorefrontApp ( rootPath ) ;
2324 const storefrontRequire = createStorefrontRequire ( rootPath ) ;
@@ -26,7 +27,12 @@ function createChangeFeedbackWatcher(projectRoot) {
2627 const disableJsCompilation = process . env . SHOPWARE_STOREFRONT_DISABLE_JS === '1' ;
2728 const jsCompileFeedbackEnabled = process . env . SHOPWARE_STOREFRONT_JS_COMPILE_FEEDBACK !== '0' ;
2829 const disableTwigWatch = process . env . SHOPWARE_STOREFRONT_DISABLE_TWIG === '1' ;
30+ const disableTranslationWatch = process . env . SHOPWARE_STOREFRONT_DISABLE_TRANSLATION_WATCH === '1' ;
31+ const onTranslationChange = typeof options . onTranslationChange === 'function'
32+ ? options . onTranslationChange
33+ : null ;
2934 const twigLog = createLogger ( 'TWIG' ) ;
35+ const translationLog = createLogger ( 'I18N' ) ;
3036
3137 let watchpack = null ;
3238 const recentlyLogged = new Map ( ) ;
@@ -36,6 +42,14 @@ function createChangeFeedbackWatcher(projectRoot) {
3642 pendingEventType : '' ,
3743 pendingFiles : new Set ( ) ,
3844 } ;
45+ const translationState = {
46+ timer : null ,
47+ inFlight : false ,
48+ queued : false ,
49+ waitLogged : false ,
50+ pendingEventType : '' ,
51+ pendingFiles : new Set ( ) ,
52+ } ;
3953
4054 function logFileEvent ( fileType , eventType , formattedFile , details = '' ) {
4155 const eventColor = eventType === 'remove' ? ANSI . yellow : ANSI . green ;
@@ -142,6 +156,10 @@ function createChangeFeedbackWatcher(projectRoot) {
142156 return 'twig' ;
143157 }
144158
159+ if ( extension === '.json' && isTranslationJsonFile ( filePath ) ) {
160+ return 'translation' ;
161+ }
162+
145163 if ( [ '.js' , '.mjs' , '.cjs' , '.ts' , '.tsx' , '.jsx' ] . includes ( extension ) ) {
146164 return 'js' ;
147165 }
@@ -157,6 +175,15 @@ function createChangeFeedbackWatcher(projectRoot) {
157175 return now - previous < DUPLICATE_LOG_WINDOW_MS ;
158176 }
159177
178+ function isTranslationJsonFile ( filePath ) {
179+ const normalizedPath = String ( filePath || '' ) . replace ( / \\ / g, '/' ) . toLowerCase ( ) ;
180+ if ( normalizedPath === '' || ! normalizedPath . endsWith ( '.json' ) ) {
181+ return false ;
182+ }
183+
184+ return normalizedPath . includes ( '/snippet/' ) || normalizedPath . includes ( '/snippets/' ) ;
185+ }
186+
160187 function rememberTwigPending ( eventType , formattedFile ) {
161188 if ( typeof eventType === 'string' && eventType !== '' ) {
162189 twigState . pendingEventType = eventType ;
@@ -199,6 +226,80 @@ function createChangeFeedbackWatcher(projectRoot) {
199226 } , TWIG_DEBOUNCE_MS ) ;
200227 }
201228
229+ function rememberTranslationPending ( eventType , formattedFile ) {
230+ if ( typeof eventType === 'string' && eventType !== '' ) {
231+ translationState . pendingEventType = eventType ;
232+ }
233+
234+ if ( typeof formattedFile === 'string' && formattedFile !== '' ) {
235+ translationState . pendingFiles . add ( formattedFile ) ;
236+ }
237+ }
238+
239+ async function flushTranslationFeedback ( ) {
240+ const pendingFiles = [ ...translationState . pendingFiles ] ;
241+ const trigger = translationState . pendingEventType || 'change' ;
242+ const fileSummary = summarizeFiles ( pendingFiles ) ;
243+ const reasonLabel = fileSummary ? `${ trigger } : ${ fileSummary } ` : trigger ;
244+
245+ if ( translationState . inFlight ) {
246+ translationState . queued = true ;
247+ if ( ! translationState . waitLogged ) {
248+ translationLog . status ( 'WAIT' , `change queued while cache flush is running${ fileSummary ? ` (${ fileSummary } )` : '' } ` ) ;
249+ translationState . waitLogged = true ;
250+ }
251+ return ;
252+ }
253+
254+ translationState . pendingEventType = '' ;
255+ translationState . pendingFiles . clear ( ) ;
256+ translationState . waitLogged = false ;
257+ translationState . inFlight = true ;
258+ const startedAt = Date . now ( ) ;
259+ translationLog . status ( 'RUN' , `flushing cache (${ reasonLabel } )` ) ;
260+
261+ try {
262+ if ( onTranslationChange ) {
263+ await onTranslationChange ( {
264+ eventType : trigger ,
265+ reasonLabel,
266+ files : pendingFiles ,
267+ } ) ;
268+ }
269+
270+ translationLog . status ( 'OK' , `cache flushed + reload triggered (${ reasonLabel } ) in ${ Date . now ( ) - startedAt } ms` ) ;
271+ } catch ( error ) {
272+ translationLog . status ( 'ERR' , `cache flush failed (${ reasonLabel } ) after ${ Date . now ( ) - startedAt } ms: ${ error ?. message || error } ` , true ) ;
273+ } finally {
274+ translationState . inFlight = false ;
275+
276+ if ( translationState . queued ) {
277+ translationState . queued = false ;
278+ setTimeout ( ( ) => {
279+ void flushTranslationFeedback ( ) ;
280+ } , TRANSLATION_DEBOUNCE_MS ) ;
281+ }
282+ }
283+ }
284+
285+ function scheduleTranslationFeedback ( eventType , formattedFile ) {
286+ rememberTranslationPending ( eventType , formattedFile ) ;
287+
288+ if ( translationState . timer ) {
289+ if ( ! translationState . waitLogged ) {
290+ const queuedFiles = summarizeFiles ( [ ...translationState . pendingFiles ] ) ;
291+ translationLog . status ( 'WAIT' , `change queued while cache flush is running${ queuedFiles ? ` (${ queuedFiles } )` : '' } ` ) ;
292+ translationState . waitLogged = true ;
293+ }
294+ return ;
295+ }
296+
297+ translationState . timer = setTimeout ( ( ) => {
298+ translationState . timer = null ;
299+ void flushTranslationFeedback ( ) ;
300+ } , TRANSLATION_DEBOUNCE_MS ) ;
301+ }
302+
202303 function handleFileEvent ( eventType , absoluteFilePath ) {
203304 const fileType = classifyFile ( absoluteFilePath ) ;
204305 if ( ! fileType ) {
@@ -244,6 +345,24 @@ function createChangeFeedbackWatcher(projectRoot) {
244345 }
245346
246347 scheduleTwigReloadFeedback ( eventType , formattedFile ) ;
348+ return ;
349+ }
350+
351+ if ( fileType === 'translation' ) {
352+ if ( disableTranslationWatch ) {
353+ if ( shouldSkipDuplicate ( eventType , formattedFile ) ) {
354+ return ;
355+ }
356+
357+ logFileEvent ( 'i18n' , eventType , formattedFile , '(skipped: translation watch disabled)' ) ;
358+ return ;
359+ }
360+
361+ if ( shouldSkipDuplicate ( eventType , formattedFile ) ) {
362+ return ;
363+ }
364+
365+ scheduleTranslationFeedback ( eventType , formattedFile ) ;
247366 }
248367 }
249368
@@ -279,6 +398,11 @@ function createChangeFeedbackWatcher(projectRoot) {
279398 twigState . timer = null ;
280399 }
281400
401+ if ( translationState . timer ) {
402+ clearTimeout ( translationState . timer ) ;
403+ translationState . timer = null ;
404+ }
405+
282406 if ( watchpack ) {
283407 watchpack . close ( ) ;
284408 watchpack = null ;
0 commit comments