ultimate-jekyll-manager 0.0.39 → 0.0.40

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.
package/README.md CHANGED
@@ -76,6 +76,7 @@ npx uj audit
76
76
  ```bash
77
77
  GH_TOKEN=XXX \
78
78
  GITHUB_REPOSITORY=XXX \
79
+ UJ_TRANSLATION_CACHE=true \
79
80
  npx uj translation
80
81
  ```
81
82
  <!-- Developing -->
@@ -11,5 +11,5 @@ module.exports = async function (options) {
11
11
  // Log
12
12
  logger.log(`Starting audit...`);
13
13
 
14
- await execute('UJ_FORCE_AUDIT=true bundle exec npm run gulp -- audit', { log: true })
14
+ await execute('UJ_AUDIT_FORCE=true bundle exec npm run gulp -- audit', { log: true })
15
15
  };
@@ -11,5 +11,5 @@ module.exports = async function (options) {
11
11
  // Log
12
12
  logger.log(`Starting translation...`);
13
13
 
14
- await execute('UJ_FORCE_TRANSLATION=true bundle exec npm run gulp -- translation', { log: true })
14
+ await execute('UJ_TRANSLATION_FORCE=true bundle exec npm run gulp -- translation', { log: true })
15
15
  };
@@ -35,8 +35,8 @@ async function audit(complete) {
35
35
  // Log
36
36
  logger.log('Starting...');
37
37
 
38
- // Quit if NOT in build mode and UJ_FORCE_AUDIT is not true
39
- if (!Manager.isBuildMode() && process.env.UJ_FORCE_AUDIT !== 'true') {
38
+ // Quit if NOT in build mode and UJ_AUDIT_FORCE is not true
39
+ if (!Manager.isBuildMode() && process.env.UJ_AUDIT_FORCE !== 'true') {
40
40
  logger.log('Skipping audit in development mode');
41
41
  return complete();
42
42
  }
@@ -232,7 +232,7 @@ function defaultsWatcher(complete) {
232
232
  // Watch for changes
233
233
  watch(input, { delay: delay, dot: true }, defaults)
234
234
  .on('change', (path) => {
235
- logger.log(`[watcher] File ${path} was changed`);
235
+ logger.log(`[watcher] File changed (${path})`);
236
236
  });
237
237
 
238
238
  // Complete
@@ -91,7 +91,7 @@ function developmentRebuildWatcher(complete) {
91
91
  // Watch for changes
92
92
  watch(input, { delay: delay, dot: true }, developmentRebuild)
93
93
  .on('change', (path) => {
94
- logger.log(`[watcher] File ${path} was changed`);
94
+ logger.log(`[watcher] File changed (${path})`);
95
95
  });
96
96
 
97
97
  // Complete
@@ -102,7 +102,7 @@ function distributeWatcher(complete) {
102
102
  // Watch for changes
103
103
  watch(input, { delay: delay, dot: true }, distribute)
104
104
  .on('change', (path) => {
105
- logger.log(`[watcher] File ${path} was changed`);
105
+ logger.log(`[watcher] File changed (${path})`);
106
106
  });
107
107
 
108
108
  // Complete
@@ -130,7 +130,7 @@ function imageminWatcher(complete) {
130
130
  // Watch for changes
131
131
  watch(input, { delay: delay, dot: true }, imagemin)
132
132
  .on('change', (path) => {
133
- logger.log(`[watcher] File ${path} was changed`);
133
+ logger.log(`[watcher] File changed (${path})`);
134
134
  });
135
135
 
136
136
  // Complete
@@ -79,9 +79,6 @@ async function jekyll(complete) {
79
79
  // Run buildpost hook
80
80
  await hook('build:post', index);
81
81
 
82
- // QUIT
83
- // QUIT
84
-
85
82
  // Log
86
83
  logger.log('Finished!');
87
84
 
@@ -113,7 +110,7 @@ function jekyllWatcher(complete) {
113
110
  // Watch for changes
114
111
  watch(input, { delay: delay, dot: true }, jekyll)
115
112
  .on('change', (path) => {
116
- logger.log(`[watcher] File ${path} was changed`);
113
+ logger.log(`[watcher] File changed (${path})`);
117
114
 
118
115
  // Check if changed file is a .rb file
119
116
  if (path.endsWith('.rb')) {
@@ -113,7 +113,7 @@ function sassWatcher(complete) {
113
113
  // Watch for changes
114
114
  watch(input, { delay: delay, dot: true }, sass)
115
115
  .on('change', (path) => {
116
- logger.log(`[watcher] File ${path} was changed`);
116
+ logger.log(`[watcher] File changed (${path})`);
117
117
  });
118
118
 
119
119
  // Complete
@@ -1,7 +1,7 @@
1
1
  // Libraries
2
2
  const Manager = new (require('../../build.js'));
3
3
  const logger = Manager.logger('translation');
4
- const { series } = require('gulp');
4
+ const { series, watch } = require('gulp');
5
5
  const glob = require('glob').globSync;
6
6
  const path = require('path');
7
7
  const fetch = require('wonderful-fetch');
@@ -9,7 +9,7 @@ const jetpack = require('fs-jetpack');
9
9
  const cheerio = require('cheerio');
10
10
  const crypto = require('crypto');
11
11
  const yaml = require('js-yaml');
12
- const { execute, wait } = require('node-powertools');
12
+ const { execute, wait, template } = require('node-powertools');
13
13
  const { Octokit } = require('@octokit/rest')
14
14
  const AdmZip = require('adm-zip') // npm install adm-zip
15
15
 
@@ -38,13 +38,25 @@ const RECHECK_DAYS = 0;
38
38
  // const AI_MODEL = 'gpt-4.1-nano';
39
39
  const AI_MODEL = 'gpt-4.1-mini';
40
40
  const TRANSLATION_BRANCH = 'uj-translations';
41
- const LOUD = false;
42
- // const LOUD = true;
41
+ // const LOUD = false;
42
+ const LOUD = process.env.UJ_LOUD_LOGS === 'true';
43
43
 
44
44
  const TRANSLATION_DELAY_MS = 500; // wait between each translation
45
45
  const TRANSLATION_BATCH_SIZE = 25; // wait longer every N translations
46
46
  const TRANSLATION_BATCH_DELAY_MS = 10000; // longer wait after batch
47
47
 
48
+ // Prompt
49
+ const SYSTEM_PROMPT = `
50
+ You are a professional translator.
51
+ Translate the provided content, preserving all original formatting, HTML structure, metadata, and links.
52
+ Do not explain anything — just return the translated content.
53
+ The content is TAGGED with [0]...[/0], etc. to mark the text. You MUST KEEP THESE TAGS IN PLACE IN YOUR RESPONSE and OPEN ([0]) and CLOSE ([/0]) them PROPERLY.
54
+ DO NOT translate URLs or other non-text elements.
55
+ DO NOT translate the brand.
56
+ Brand: {brand}
57
+ Translate to {lang}
58
+ `;
59
+
48
60
  // Variables
49
61
  let octokit;
50
62
 
@@ -61,11 +73,8 @@ async function translation(complete) {
61
73
  // Log
62
74
  logger.log('Starting...');
63
75
 
64
- // Get ignored pages
65
- const ignoredPages = getIgnoredPages();
66
-
67
- // Quit if NOT in build mode and UJ_FORCE_TRANSLATION is not true
68
- if (!Manager.isBuildMode() && process.env.UJ_FORCE_TRANSLATION !== 'true') {
76
+ // Quit if NOT in build mode and UJ_TRANSLATION_FORCE is not true
77
+ if (!Manager.isBuildMode() && process.env.UJ_TRANSLATION_FORCE !== 'true') {
69
78
  logger.log('Skipping translation in development mode');
70
79
  return complete();
71
80
  }
@@ -103,6 +112,34 @@ async function translation(complete) {
103
112
  return complete();
104
113
  };
105
114
 
115
+ // TODO: Currently this does not work because it will run an infinite loop
116
+ function translationWatcher(complete) {
117
+ // Quit if in build mode
118
+ if (Manager.isBuildMode()) {
119
+ logger.log('[watcher] Skipping watcher in build mode');
120
+ return complete();
121
+ }
122
+
123
+ // Log
124
+ logger.log('[watcher] Watching for changes...');
125
+
126
+ // Get ignored pages
127
+ const ignoredPages = getIgnoredPages();
128
+ const ignore = [
129
+ ...ignoredPages.files.map(key => `_site/${key}.html`),
130
+ ...ignoredPages.folders.map(folder => `_site/${folder}/**/*`)
131
+ ]
132
+
133
+ // Watch for changes
134
+ watch(input, { delay: delay, ...getGlobOptions(), }, translation)
135
+ .on('change', (path) => {
136
+ logger.log(`[watcher] File changed (${path})`);
137
+ });
138
+
139
+ // Complete
140
+ return complete();
141
+ }
142
+
106
143
  // Default Task
107
144
  module.exports = series(translation);
108
145
 
@@ -110,7 +147,6 @@ module.exports = series(translation);
110
147
  async function processTranslation() {
111
148
  const enabled = config?.translation?.enabled !== false;
112
149
  const languages = config?.translation?.languages || [];
113
- const ignoredPages = getIgnoredPages();
114
150
  const updatedFiles = new Set();
115
151
 
116
152
  // Quit if translation is disabled or no languages are configured
@@ -135,13 +171,7 @@ async function processTranslation() {
135
171
  // }
136
172
 
137
173
  // Get files
138
- const allFiles = glob(input, {
139
- nodir: true,
140
- ignore: [
141
- ...ignoredPages.files.map(key => `_site/${key}.html`),
142
- ...ignoredPages.folders.map(folder => `_site/${folder}/**/*`)
143
- ]
144
- });
174
+ const allFiles = glob(input, getGlobOptions());
145
175
 
146
176
  // Log
147
177
  logger.log(`Translating ${allFiles.length} files for ${languages.length} supported languages: ${languages.join(', ')}`);
@@ -153,6 +183,8 @@ async function processTranslation() {
153
183
  skipped: new Set(),
154
184
  }
155
185
  };
186
+ const promptHash = crypto.createHash('sha256').update(SYSTEM_PROMPT).digest('hex');
187
+
156
188
  for (const lang of languages) {
157
189
  const metaPath = path.join(CACHE_DIR, lang, 'meta.json');
158
190
  let meta = {};
@@ -163,6 +195,15 @@ async function processTranslation() {
163
195
  logger.warn(`⚠️ Failed to parse meta for [${lang}], starting fresh`);
164
196
  }
165
197
  }
198
+
199
+ // Check if the promptHash matches; if not, invalidate the cache
200
+ if (meta.prompt?.hash !== promptHash) {
201
+ logger.warn(`⚠️ Prompt hash mismatch for [${lang}]. Invalidating cache.`);
202
+ meta = {};
203
+ }
204
+
205
+ // Store the current promptHash in the meta file
206
+ meta.prompt = { hash: promptHash };
166
207
  metas[lang] = { meta, path: metaPath, skipped: new Set() };
167
208
  }
168
209
 
@@ -217,11 +258,16 @@ async function processTranslation() {
217
258
  const age = entry?.timestamp
218
259
  ? (Date.now() - new Date(entry.timestamp).getTime()) / (1000 * 60 * 60 * 24)
219
260
  : Infinity;
220
- const useCached = entry && entry.hash === hash && (RECHECK_DAYS === 0 || age < RECHECK_DAYS);
261
+ const useCached = entry
262
+ && entry.hash === hash
263
+ && (RECHECK_DAYS === 0 || age < RECHECK_DAYS);
221
264
  const startTime = Date.now();
222
265
 
223
266
  // Check if we can use cached translation
224
- if (useCached && jetpack.exists(cachePath)) {
267
+ if (
268
+ (useCached || process.env.UJ_TRANSLATION_CACHE === 'true')
269
+ && jetpack.exists(cachePath)
270
+ ) {
225
271
  translated = jetpack.read(cachePath);
226
272
  logger.log(`📦 Using cached translation for ${relativePath} [${lang}]`);
227
273
  } else {
@@ -278,14 +324,22 @@ async function processTranslation() {
278
324
  return logger.warn(`⚠️ Could not find translated tag for index ${i}`);
279
325
  }
280
326
 
327
+ // Extract original leading and trailing whitespace
328
+ const originalText = n.text;
329
+ const leadingWhitespace = originalText.match(/^\s*/)?.[0] || '';
330
+ const trailingWhitespace = originalText.match(/\s*$/)?.[0] || '';
331
+
332
+ // Reapply the original whitespace to the translation
333
+ const adjustedTranslation = `${leadingWhitespace}${translation.trim()}${trailingWhitespace}`;
334
+
281
335
  if (n.type === 'data') {
282
- n.reference.data = translation;
336
+ n.reference.data = adjustedTranslation;
283
337
  } else if (n.type === 'text') {
284
- n.node.text(translation);
338
+ n.node.text(adjustedTranslation);
285
339
  } else if (n.type === 'attr') {
286
- n.node.attr(n.attr, translation);
340
+ n.node.attr(n.attr, adjustedTranslation);
287
341
  }
288
- if (LOUD) logger.log(`${i}: ${n.text} → ${translation}`);
342
+ if (LOUD) logger.log(`${i}: "${n.text.trim()}""${adjustedTranslation.trim()}"`);
289
343
  });
290
344
 
291
345
  // Rewrite links
@@ -382,14 +436,11 @@ async function processTranslation() {
382
436
  }
383
437
 
384
438
  async function translateWithAPI(openAIKey, content, lang) {
385
- // Prompt
386
- const systemPrompt = `
387
- You are a professional translator.
388
- Translate the provided content, preserving all original formatting, HTML structure, metadata, and links.
389
- Do not explain anything — just return the translated content.
390
- The content is TAGGED with [0]...[/0], etc. to mark the text. You MUST KEEP THESE TAGS IN PLACE IN YOUR RESPONSE and OPEN ([0]) and CLOSE ([/0]) them PROPERLY.
391
- Translate to ${lang}.
392
- `;
439
+ const brand = config?.brand?.name || 'Unknown Brand';
440
+ const systemPrompt = template(SYSTEM_PROMPT, {
441
+ lang,
442
+ brand
443
+ });
393
444
 
394
445
  // Request
395
446
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
@@ -550,9 +601,6 @@ async function insertLanguageTags($, languages, relativePath, filePath) {
550
601
  const format = isHtml ? 'html' : 'xml';
551
602
  const formatted = await formatDocument($.html(), format);
552
603
 
553
- console.log('---SAVING filePath', filePath);
554
- console.log('---SAVING formatted.error', formatted.error);
555
-
556
604
  // Write the formatted content back to the file
557
605
  jetpack.write(filePath, formatted.content);
558
606
  }
@@ -628,6 +676,17 @@ function getIgnoredPages() {
628
676
  };
629
677
  }
630
678
 
679
+ function getGlobOptions() {
680
+ const ignoredPages = getIgnoredPages();
681
+ return {
682
+ nodir: true,
683
+ ignore: [
684
+ ...ignoredPages.files.map(key => `_site/${key}.html`),
685
+ ...ignoredPages.folders.map(folder => `_site/${folder}/**/*`)
686
+ ]
687
+ }
688
+ }
689
+
631
690
  // Git Sync: Pull
632
691
  async function fetchTranslationsBranch() {
633
692
  const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
@@ -21,14 +21,14 @@ const collectTextNodes = ($, options) => {
21
21
  const i = textNodes.length;
22
22
  const text = node.text().trim();
23
23
  if (text) {
24
+
25
+ // Push
24
26
  textNodes.push({
25
27
  node,
26
28
  type: 'text',
27
29
  attr: null,
28
30
  text,
29
31
  tagged: `[${i}]${text}[/${i}]`,
30
- line: el.startIndex || 0, // Add line information
31
- column: 0 // Column is not directly available, default to 0
32
32
  });
33
33
  }
34
34
  return;
@@ -51,14 +51,14 @@ const collectTextNodes = ($, options) => {
51
51
  const text = node.attr('content')?.trim();
52
52
  if (text) {
53
53
  const i = textNodes.length;
54
+
55
+ // Push
54
56
  textNodes.push({
55
57
  node,
56
58
  type: 'attr',
57
59
  attr: 'content',
58
60
  text,
59
61
  tagged: `[${i}]${text}[/${i}]`,
60
- line: el.startIndex || 0, // Add line information
61
- column: 0 // Column is not directly available, default to 0
62
62
  });
63
63
  }
64
64
  }
@@ -70,10 +70,15 @@ const collectTextNodes = ($, options) => {
70
70
  if (child.type === 'text' && child.data?.trim()) {
71
71
  const i = textNodes.length;
72
72
  const text = child.data
73
- .replace(/^\s+/, '')
74
- .replace(/\s+$/, '')
75
- .replace(/\s+/g, ' ');
73
+ // Preserve a single leading whitespace if it exists
74
+ .replace(/^\s*(\s)\s*/, '$1')
75
+ // Preserve a single trailing whitespace if it exists
76
+ .replace(/\s*(\s)\s*$/, '$1')
77
+ // Normalize internal whitespace
78
+ .replace(/\s+/g, ' ')
79
+ // const text = trimPreserveOneCleanInner(child.data);
76
80
 
81
+ // Push
77
82
  textNodes.push({
78
83
  node,
79
84
  type: 'data',
@@ -81,8 +86,6 @@ const collectTextNodes = ($, options) => {
81
86
  reference: child,
82
87
  text,
83
88
  tagged: `[${i}]${text}[/${i}]`,
84
- line: el.startIndex || 0, // Add line information
85
- column: 0 // Column is not directly available, default to 0
86
89
  });
87
90
  }
88
91
  });
@@ -92,3 +95,12 @@ const collectTextNodes = ($, options) => {
92
95
  };
93
96
 
94
97
  module.exports = collectTextNodes;
98
+
99
+
100
+ function trimPreserveOneCleanInner(str) {
101
+ const match = str.match(/^(\s*)(.*?)(\s*)$/);
102
+ const leading = match[1] ? match[1][0] || '' : '';
103
+ const content = match[2].replace(/\s+/g, ' ');
104
+ const trailing = match[3] ? match[3][0] || '' : '';
105
+ return leading + content + trailing;
106
+ }
@@ -186,7 +186,7 @@ function webpackWatcher(complete) {
186
186
  watch(input, { delay: delay, dot: true }, webpack)
187
187
  .on('change', (path) => {
188
188
  // Log
189
- logger.log(`[watcher] File ${path} was changed`);
189
+ logger.log(`[watcher] File changed (${path})`);
190
190
  });
191
191
 
192
192
  // Complete
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "0.0.39",
3
+ "version": "0.0.40",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {