ultimate-jekyll-manager 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,7 +42,6 @@ const AI = {
42
42
  }
43
43
  const CACHE_DIR = '.temp/cache/translation';
44
44
  const CACHE_BRANCH = 'cache-uj-translation';
45
- const RECHECK_DAYS = 0;
46
45
  // const LOUD = false;
47
46
  const LOUD = process.env.UJ_LOUD_LOGS === 'true';
48
47
  const CONTROL = 'UJ-TRANSLATION-CONTROL';
@@ -171,8 +170,6 @@ module.exports = series(
171
170
  async function processTranslation() {
172
171
  const enabled = config?.translation?.enabled !== false;
173
172
  const languages = config?.translation?.languages || [];
174
- const updatedFiles = new Set();
175
-
176
173
  // Track timing
177
174
  const startTime = Date.now();
178
175
 
@@ -199,14 +196,12 @@ async function processTranslation() {
199
196
  logger.log(`Translating ${allFiles.length} files for ${languages.length} supported languages: ${languages.join(', ')}`);
200
197
  // logger.log(allFiles);
201
198
 
202
- // Prepare meta caches per language
203
- const metas = {
204
- global: {
205
- skipped: new Set(),
206
- }
207
- };
199
+ // Prepare prompt hash for cache invalidation
208
200
  const promptHash = crypto.createHash('sha256').update(SYSTEM_PROMPT).digest('hex');
201
+ const skippedFiles = new Set();
209
202
 
203
+ // Load per-language meta (prompt hash only)
204
+ const metas = {};
210
205
  for (const lang of languages) {
211
206
  const metaPath = path.join(CACHE_DIR, lang, 'meta.json');
212
207
  let meta = {};
@@ -218,30 +213,30 @@ async function processTranslation() {
218
213
  }
219
214
  }
220
215
 
221
- // Check if the promptHash matches; if not, invalidate the cache
216
+ // Check if the promptHash matches; if not, wipe all page caches for this language
222
217
  if (meta.prompt?.hash !== promptHash) {
223
- logger.warn(`⚠️ Prompt cache MISS [${lang}]: hash mismatch — invalidating ${Object.keys(meta).length - 1} cached entries.`);
224
- meta = {};
218
+ const pagesDir = path.join(CACHE_DIR, lang, 'pages');
219
+ const existing = jetpack.exists(pagesDir) ? jetpack.find(pagesDir, { matching: '**/*', files: true }).length : 0;
220
+ logger.warn(`⚠️ Prompt cache MISS [${lang}]: hash mismatch — clearing ${existing} cached page files.`);
221
+ jetpack.remove(pagesDir);
225
222
  } else {
226
- const entries = Object.keys(meta).filter(k => k !== 'prompt').length;
227
- logger.log(`✅ Prompt cache HIT [${lang}]: ${entries} cached entries available.`);
223
+ const pagesDir = path.join(CACHE_DIR, lang, 'pages');
224
+ const existing = jetpack.exists(pagesDir) ? jetpack.find(pagesDir, { matching: '**/*', files: true }).length : 0;
225
+ logger.log(`✅ Prompt cache HIT [${lang}]: ${existing} cached page files available.`);
228
226
  }
229
227
 
230
- // Store the current promptHash in the meta file
231
228
  meta.prompt = { hash: promptHash };
232
- metas[lang] = { meta, path: metaPath, skipped: new Set() };
229
+ metas[lang] = { meta, path: metaPath };
233
230
  }
234
231
 
235
232
  // Track token usage and statistics
236
233
  const tokens = { input: 0, output: 0 };
237
234
  const tasks = [];
238
235
  const stats = {
239
- fromCache: 0,
240
- newlyProcessed: 0,
241
- totalProcessed: 0,
242
- failedFiles: [],
243
- cachedFiles: [],
244
- processedFiles: []
236
+ totalPages: 0,
237
+ cachedStrings: 0,
238
+ newStrings: 0,
239
+ failedPages: [],
245
240
  };
246
241
 
247
242
  // Calculate total tasks for progress tracking
@@ -264,29 +259,24 @@ async function processTranslation() {
264
259
  // Collect text nodes
265
260
  const textNodes = collectTextNodes($, { tag: false });
266
261
 
267
- // Build strings array for translation
262
+ // Build strings array and per-string hashes
268
263
  const strings = textNodes.map(n => n.text);
269
-
270
- // Compute hash from the strings
271
- const hash = crypto.createHash('sha256').update(JSON.stringify(strings)).digest('hex');
264
+ const stringHashes = strings.map(s => crypto.createHash('sha256').update(s).digest('hex').slice(0, 12));
272
265
 
273
266
  // Skip all except the specified HTML file
274
267
  if (ujOnly && relativePath !== ujOnly) {
275
- // Update to work with the new SET protocol
276
- metas.global.skipped.add(`${relativePath} (UJ_TRANSLATION_ONLY set)`);
268
+ skippedFiles.add(`${relativePath} (UJ_TRANSLATION_ONLY set)`);
277
269
  continue;
278
270
  }
279
271
 
280
272
  // Log the page being processed
281
- logger.log(`🔍 Processing: ${relativePath} (${strings.length} strings, hash: ${hash})`);
273
+ logger.log(`🔍 Processing: ${relativePath} (${strings.length} strings)`);
282
274
  if (LOUD) logger.log(`🔍 Strings: \n${JSON.stringify(strings, null, 2)}`)
283
275
 
284
- // Translate this file for all languages in parallel
276
+ // Translate this file for all languages
285
277
  for (const lang of languages) {
286
278
  const task = async () => {
287
- const meta = metas[lang].meta;
288
- const cachePath = path.join(CACHE_DIR, lang, 'pages', relativePath);
289
- // const outPath = path.join('_site', lang, relativePath);
279
+ const cachePath = path.join(CACHE_DIR, lang, 'pages', `${relativePath}.json`);
290
280
  const isHomepage = relativePath === 'index.html';
291
281
  const outPath = isHomepage
292
282
  ? path.join('_site', `${lang}.html`)
@@ -301,75 +291,75 @@ async function processTranslation() {
301
291
  // Log
302
292
  logger.log(`🌐 Started [${progress} - ${percentage}%]: ${logTag}`);
303
293
 
304
- // Skip if the file is not in the meta or if it has no text nodes
305
- let translated = null;
306
- const entry = meta[relativePath];
307
- const age = entry?.timestamp
308
- ? (Date.now() - new Date(entry.timestamp).getTime()) / (1000 * 60 * 60 * 24)
309
- : Infinity;
310
- const useCached = entry
311
- && entry.hash === hash
312
- && (RECHECK_DAYS === 0 || age < RECHECK_DAYS);
313
294
  const startTime = Date.now();
314
295
 
315
- function setResult(success) {
316
- if (success) {
317
- meta[relativePath] = {
318
- timestamp: new Date().toISOString(),
319
- hash,
320
- };
296
+ // Load existing per-string cache for this page
297
+ let pageCache = {};
298
+ if (jetpack.exists(cachePath)) {
299
+ try {
300
+ pageCache = jetpack.read(cachePath, 'json') || {};
301
+ } catch (e) {
302
+ pageCache = {};
303
+ }
304
+ }
305
+
306
+ // Separate cached vs uncached strings
307
+ const translated = new Array(strings.length);
308
+ const uncachedIndices = [];
309
+
310
+ for (let i = 0; i < strings.length; i++) {
311
+ const cached = pageCache[stringHashes[i]];
312
+ if (cached !== undefined) {
313
+ translated[i] = cached;
314
+ stats.cachedStrings++;
321
315
  } else {
322
- meta[relativePath] = {
323
- timestamp: 0,
324
- hash: '__fail__',
325
- };
316
+ uncachedIndices.push(i);
326
317
  }
327
318
  }
328
319
 
329
- // Check if we can use cached translation
330
- if (
331
- (useCached || process.env.UJ_TRANSLATION_CACHE === 'true')
332
- && jetpack.exists(cachePath)
333
- ) {
334
- translated = jetpack.read(cachePath, 'json');
335
- logger.log(`📦 Success [${progress} - ${percentage}%]: ${logTag} - Using cache`);
336
- stats.fromCache++;
337
- stats.cachedFiles.push(logTag);
320
+ const cachedCount = strings.length - uncachedIndices.length;
321
+
322
+ // If everything is cached, skip API call
323
+ if (uncachedIndices.length === 0) {
324
+ const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
325
+ logger.log(`📦 Success [${progress} - ${percentage}%]: ${logTag} — All ${strings.length} strings from cache (${elapsedTime}s)`);
338
326
  } else {
339
- try {
340
- const { result, usage } = await translateWithAPI(openAIKey, strings, lang, logTag);
327
+ // Translate only the uncached strings
328
+ const uncachedStrings = uncachedIndices.map(i => strings[i]);
341
329
 
342
- // Log
343
- const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
344
- logger.log(`✅ Success [${progress} - ${percentage}%]: ${logTag} - Translated (Elapsed time: ${elapsedTime}s)`);
330
+ try {
331
+ const { result, usage } = await translateWithAPI(openAIKey, uncachedStrings, lang, logTag);
345
332
 
346
- // Set translated result
347
- translated = result;
333
+ // Place translations back at their original indices and update cache
334
+ for (let j = 0; j < uncachedIndices.length; j++) {
335
+ const origIndex = uncachedIndices[j];
336
+ translated[origIndex] = result[j];
337
+ pageCache[stringHashes[origIndex]] = result[j];
338
+ }
348
339
 
349
340
  // Update token totals
350
341
  tokens.input += usage.input_tokens || 0;
351
342
  tokens.output += usage.output_tokens || 0;
343
+ stats.newStrings += uncachedIndices.length;
352
344
 
353
- // Save to cache (JSON array)
354
- jetpack.write(cachePath, translated);
355
-
356
- // Set result
357
- setResult(true);
358
- stats.newlyProcessed++;
359
- stats.processedFiles.push(logTag);
345
+ const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
346
+ logger.log(`✅ Success [${progress} - ${percentage}%]: ${logTag} — ${uncachedIndices.length} new + ${cachedCount} cached (${elapsedTime}s)`);
360
347
  } catch (e) {
361
348
  const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(2);
362
- logger.error(`❌ Failed [${progress} - ${percentage}%]: ${logTag} — ${e.message} (Elapsed time: ${elapsedTime}s)`);
349
+ logger.error(`❌ Failed [${progress} - ${percentage}%]: ${logTag} — ${e.message} (${elapsedTime}s)`);
363
350
 
364
- // Fallback to original strings
365
- translated = strings;
351
+ // Fill uncached slots with originals
352
+ for (const i of uncachedIndices) {
353
+ translated[i] = strings[i];
354
+ }
366
355
 
367
- // Save failure to cache
368
- setResult(false);
369
- stats.failedFiles.push(logTag);
356
+ stats.failedPages.push(logTag);
370
357
  }
371
358
  }
372
359
 
360
+ // Save updated page cache
361
+ jetpack.write(cachePath, pageCache);
362
+
373
363
  // Reset the DOM to avoid conflicts between languages
374
364
  const $ = cheerio.load(originalHtml);
375
365
  const textNodes = collectTextNodes($, { tag: false });
@@ -387,8 +377,7 @@ async function processTranslation() {
387
377
  translation.includes(CONTROL)
388
378
  && n.node.attr('id') !== CONTROL
389
379
  ) {
390
- logger.error(`❌ Failed: ${logTag} — Control tag mismatch at index ${i}`);
391
- return setResult(false);
380
+ return logger.error(`❌ Failed: ${logTag} — Control tag mismatch at index ${i}`);
392
381
  }
393
382
 
394
383
  // Preserve original leading/trailing whitespace
@@ -419,12 +408,9 @@ async function processTranslation() {
419
408
  || controlTag.text() !== CONTROL
420
409
  ) {
421
410
  logger.error(`❌ Failed: ${logTag} — Control tag mismatch or missing`);
422
- return setResult(false);
411
+ return;
423
412
  }
424
413
 
425
- // Delete the control tag
426
- // controlTag.remove();
427
-
428
414
  // Set the lang attribute on the <html> tag
429
415
  $('html').attr('lang', lang);
430
416
 
@@ -453,25 +439,7 @@ async function processTranslation() {
453
439
  const sitemapXml = jetpack.read(sitemapPath);
454
440
  await insertLanguageTags(cheerio.load(sitemapXml, { xmlMode: true }), languages, relativePath, sitemapPath);
455
441
 
456
- // Save output
457
- // const formatted = await formatDocument($.html(), 'html');
458
-
459
- // console.log('----relativePath', relativePath);
460
- // console.log('----filePath', filePath);
461
- // console.log('----outPath', outPath);
462
- // console.log('----FORMATTED.ERROR', formatted.error);
463
-
464
- // Write the translated file
465
- // jetpack.write(outPath, formatted.content);
466
- // logger.log(`✅ Wrote: ${outPath}`);
467
-
468
- // Track updated files only if it's new or updated
469
- // if (!useCached || !entry || entry.hash !== hash) {
470
- // }
471
- // Track updated files
472
- updatedFiles.add(cachePath);
473
- updatedFiles.add(metas[lang].path);
474
- stats.totalProcessed++;
442
+ stats.totalPages++;
475
443
  };
476
444
 
477
445
  // Add to tasks
@@ -484,17 +452,9 @@ async function processTranslation() {
484
452
  await Promise.all(tasks.map(task => q.add(task)));
485
453
 
486
454
  // Log skipped files
487
- logger.warn('🚫 Skipped files:');
488
- let totalSkipped = 0;
489
- for (const [lang, meta] of Object.entries(metas)) {
490
- if (meta.skipped.size > 0) {
491
- logger.warn(` [${lang}] ${meta.skipped.size} skipped files:`);
492
- meta.skipped.forEach(f => logger.warn(` ${f}`));
493
- totalSkipped += meta.skipped.size;
494
- }
495
- }
496
- if (totalSkipped === 0) {
497
- logger.warn(' NONE');
455
+ if (skippedFiles.size > 0) {
456
+ logger.warn(`🚫 Skipped ${skippedFiles.size} files:`);
457
+ skippedFiles.forEach(f => logger.warn(` ${f}`));
498
458
  }
499
459
 
500
460
  // Save all updated meta files
@@ -526,13 +486,15 @@ async function processTranslation() {
526
486
  logger.log(` End time: ${new Date(endTime).toLocaleTimeString()}`);
527
487
  logger.log(` Total elapsed: ${elapsedFormatted}`);
528
488
 
529
- // File processing stats
530
- logger.log('\n📁 File Processing:');
531
- logger.log(` Total processed: ${stats.totalProcessed}`);
532
- logger.log(` From cache: ${stats.fromCache} (${((stats.fromCache / stats.totalProcessed) * 100).toFixed(1)}%)`);
533
- logger.log(` Newly translated: ${stats.newlyProcessed} (${((stats.newlyProcessed / stats.totalProcessed) * 100).toFixed(1)}%)`);
534
- if (stats.failedFiles.length > 0) {
535
- logger.log(` Failed: ${stats.failedFiles.length}`);
489
+ // Processing stats
490
+ const totalStrings = stats.cachedStrings + stats.newStrings;
491
+ logger.log('\n📁 Processing:');
492
+ logger.log(` Pages processed: ${stats.totalPages}`);
493
+ logger.log(` Strings total: ${totalStrings.toLocaleString()}`);
494
+ logger.log(` From cache: ${stats.cachedStrings.toLocaleString()} (${totalStrings ? ((stats.cachedStrings / totalStrings) * 100).toFixed(1) : 0}%)`);
495
+ logger.log(` Newly translated: ${stats.newStrings.toLocaleString()} (${totalStrings ? ((stats.newStrings / totalStrings) * 100).toFixed(1) : 0}%)`);
496
+ if (stats.failedPages.length > 0) {
497
+ logger.log(` Failed pages: ${stats.failedPages.length}`);
536
498
  }
537
499
 
538
500
  // Token usage
@@ -563,40 +525,15 @@ async function processTranslation() {
563
525
  forceRecreate: true, // ALWAYS create a fresh branch - no history needed
564
526
  stats: {
565
527
  timestamp: new Date().toISOString(),
566
- sourceCount: allFiles.length,
567
- cachedCount: allCacheFiles.length,
568
- processedNow: stats.totalProcessed,
569
- fromCache: stats.fromCache,
570
- newlyProcessed: stats.newlyProcessed,
571
- timing: {
572
- startTime,
573
- endTime,
574
- elapsedMs
575
- },
576
- tokenUsage: tokens.input > 0 || tokens.output > 0 ? {
577
- inputTokens: tokens.input,
578
- outputTokens: tokens.output,
579
- totalTokens: tokens.input + tokens.output,
580
- inputCost,
581
- outputCost,
582
- totalCost
583
- } : undefined,
584
- languageBreakdown: languages.map(lang => ({
585
- language: lang,
586
- total: stats.totalProcessed / languages.length,
587
- fromCache: stats.cachedFiles.filter(f => f.startsWith(`[${lang}]`)).length,
588
- newlyTranslated: stats.processedFiles.filter(f => f.startsWith(`[${lang}]`)).length,
589
- failed: stats.failedFiles.filter(f => f.startsWith(`[${lang}]`)).length
590
- })),
591
- details: `Translated ${allFiles.length} pages to ${languages.length} languages (${languages.join(', ')})\n\n### Language Breakdown:\n${languages.map(lang => {
592
- const langStats = {
593
- total: stats.totalProcessed / languages.length,
594
- fromCache: stats.cachedFiles.filter(f => f.startsWith(`[${lang}]`)).length,
595
- newlyTranslated: stats.processedFiles.filter(f => f.startsWith(`[${lang}]`)).length,
596
- failed: stats.failedFiles.filter(f => f.startsWith(`[${lang}]`)).length
597
- };
598
- return `**${lang.toUpperCase()}:** ${langStats.total} total (${langStats.fromCache} cached, ${langStats.newlyTranslated} new${langStats.failed > 0 ? `, ${langStats.failed} failed` : ''})`;
599
- }).join('\n')}\n\n### Files from cache:\n${stats.cachedFiles.length > 0 ? stats.cachedFiles.slice(0, 10).map(f => `- ${f}`).join('\n') + (stats.cachedFiles.length > 10 ? `\n- ... and ${stats.cachedFiles.length - 10} more` : '') : 'None'}\n\n### Newly translated files:\n${stats.processedFiles.length > 0 ? stats.processedFiles.slice(0, 10).map(f => `- ${f}`).join('\n') + (stats.processedFiles.length > 10 ? `\n- ... and ${stats.processedFiles.length - 10} more` : '') : 'None'}${stats.failedFiles.length > 0 ? `\n\n### Failed translations:\n${stats.failedFiles.slice(0, 10).map(f => `- ${f}`).join('\n') + (stats.failedFiles.length > 10 ? `\n- ... and ${stats.failedFiles.length - 10} more` : '')}` : ''}`
528
+ pages: stats.totalPages,
529
+ strings: { cached: stats.cachedStrings, new: stats.newStrings, total: totalStrings },
530
+ failed: stats.failedPages.length,
531
+ languages: languages.join(', '),
532
+ timing: { startTime, endTime, elapsedMs },
533
+ tokenUsage: tokens.input > 0 || tokens.output > 0
534
+ ? { input: tokens.input, output: tokens.output, total: tokens.input + tokens.output, cost: totalCost }
535
+ : undefined,
536
+ details: `Translated ${stats.totalPages} pages to ${languages.length} languages (${languages.join(', ')}): ${stats.cachedStrings} cached + ${stats.newStrings} new strings${stats.failedPages.length > 0 ? `, ${stats.failedPages.length} failed pages` : ''}`
600
537
  }
601
538
  });
602
539
  }
@@ -145,6 +145,27 @@ class Manager {
145
145
  // Initialize messaging
146
146
  this.libraries.messaging = firebase.messaging();
147
147
 
148
+ // Handle background push messages (when the page is not focused)
149
+ this.libraries.messaging.onBackgroundMessage((payload) => {
150
+ console.log('Background message received:', payload);
151
+
152
+ const notification = payload.notification || {};
153
+ const data = payload.data || {};
154
+
155
+ const title = notification.title || 'New notification';
156
+ const options = {
157
+ body: notification.body || '',
158
+ icon: notification.icon || notification.image || '/assets/images/favicon/favicon-192x192.png',
159
+ data: payload,
160
+ };
161
+
162
+ if (data.click_action) {
163
+ options.data.click_action = data.click_action;
164
+ }
165
+
166
+ serviceWorker.registration.showNotification(title, options);
167
+ });
168
+
148
169
  // Attach firebase to SWManager
149
170
  this.libraries.firebase = firebase;
150
171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -108,7 +108,7 @@
108
108
  "prettier": "^3.8.3",
109
109
  "sass": "^1.100.0",
110
110
  "spellchecker": "^3.7.1",
111
- "web-manager": "^4.2.0",
111
+ "web-manager": "^4.3.0",
112
112
  "webpack": "^5.107.2",
113
113
  "wonderful-fetch": "^2.0.5",
114
114
  "wonderful-version": "^1.3.2",