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 +1 -0
- package/dist/commands/audit.js +1 -1
- package/dist/commands/translation.js +1 -1
- package/dist/gulp/tasks/audit.js +2 -2
- package/dist/gulp/tasks/defaults.js +1 -1
- package/dist/gulp/tasks/developmentRebuild.js +1 -1
- package/dist/gulp/tasks/distribute.js +1 -1
- package/dist/gulp/tasks/imagemin.js +1 -1
- package/dist/gulp/tasks/jekyll.js +1 -4
- package/dist/gulp/tasks/sass.js +1 -1
- package/dist/gulp/tasks/translation.js +93 -34
- package/dist/gulp/tasks/utils/collectTextNodes.js +21 -9
- package/dist/gulp/tasks/webpack.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/commands/audit.js
CHANGED
|
@@ -11,5 +11,5 @@ module.exports = async function (options) {
|
|
|
11
11
|
// Log
|
|
12
12
|
logger.log(`Starting audit...`);
|
|
13
13
|
|
|
14
|
-
await execute('
|
|
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('
|
|
14
|
+
await execute('UJ_TRANSLATION_FORCE=true bundle exec npm run gulp -- translation', { log: true })
|
|
15
15
|
};
|
package/dist/gulp/tasks/audit.js
CHANGED
|
@@ -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
|
|
39
|
-
if (!Manager.isBuildMode() && process.env.
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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')) {
|
package/dist/gulp/tasks/sass.js
CHANGED
|
@@ -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}
|
|
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
|
-
|
|
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
|
-
//
|
|
65
|
-
|
|
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
|
|
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 (
|
|
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 =
|
|
336
|
+
n.reference.data = adjustedTranslation;
|
|
283
337
|
} else if (n.type === 'text') {
|
|
284
|
-
n.node.text(
|
|
338
|
+
n.node.text(adjustedTranslation);
|
|
285
339
|
} else if (n.type === 'attr') {
|
|
286
|
-
n.node.attr(n.attr,
|
|
340
|
+
n.node.attr(n.attr, adjustedTranslation);
|
|
287
341
|
}
|
|
288
|
-
if (LOUD) logger.log(`${i}: ${n.text} → ${
|
|
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
|
-
|
|
386
|
-
const systemPrompt =
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
74
|
-
.replace(
|
|
75
|
-
|
|
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}
|
|
189
|
+
logger.log(`[watcher] File changed (${path})`);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// Complete
|