jekyll-uj-powertools 1.2.7 → 1.3.1
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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +17 -4
- data/jekyll-uj-powertools.gemspec +3 -0
- data/lib/_old/generators/translate-pages.rb +223 -0
- data/lib/_old/hooks/translate-pages.rb +253 -0
- data/lib/generators/inject-properties.rb +44 -0
- data/lib/hooks/inject-properties.rb +8 -0
- data/lib/jekyll-uj-powertools/version.rb +1 -1
- data/lib/jekyll-uj-powertools.rb +7 -57
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06406f26c558d459244fad81422d210523148d4a90bd42d004fcdfcaede642e0
|
4
|
+
data.tar.gz: d6e23dd0c11253df4dfef38612049904cb251728beade6139a2d5a74f51f3a19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a40bb126326706aee31507e2ad02d83a0a1e83ff8927bf99d90f0869d4898034f282f73e27032013c5018c3553ed435964f7de170d4b502f2d3be900ef7e1666
|
7
|
+
data.tar.gz: 5c5ff4b05d49cfc03312d7eaa834ef0da25c5a56df0c5ec99fcf7452eda94d40b97809d90edfd1fdfea639008e344c5e7cc22460d00bed30a57fcd48fdd0a05d
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -69,6 +69,13 @@ Convert a string to title case.
|
|
69
69
|
{{ "hello world" | uj_title_case }}
|
70
70
|
```
|
71
71
|
|
72
|
+
### `uj.cache_breaker` Variable
|
73
|
+
Use the `uj.cache_breaker` variable to append a cache-busting query parameter to your assets.
|
74
|
+
|
75
|
+
```liquid
|
76
|
+
<link rel="stylesheet" href="{{ "/assets/css/style.css" | prepend: site.baseurl }}?v={{ uj.cache_breaker }}">
|
77
|
+
```
|
78
|
+
|
72
79
|
These examples show how you can use the features of `jekyll-uj-powertools` in your Jekyll site.
|
73
80
|
|
74
81
|
## 🔧 Development
|
@@ -85,11 +92,17 @@ bundle exec rspec
|
|
85
92
|
|
86
93
|
## 💎 Build + Publish the Gem
|
87
94
|
```shell
|
88
|
-
#
|
89
|
-
|
95
|
+
# Ensure dist folder exists
|
96
|
+
mkdir -p dist
|
97
|
+
|
98
|
+
# Build the gem and push it to RubyGems
|
99
|
+
gem build jekyll-uj-powertools.gemspec -o dist/jekyll-uj-powertools-latest.gem
|
100
|
+
|
101
|
+
# Publish the latest gem
|
102
|
+
gem push dist/jekyll-uj-powertools-latest.gem
|
90
103
|
|
91
|
-
#
|
92
|
-
|
104
|
+
# Clear the files in the dist folder
|
105
|
+
rm -rf dist/*
|
93
106
|
```
|
94
107
|
|
95
108
|
## 🗨️ Contributing
|
@@ -32,6 +32,9 @@ Gem::Specification.new do |spec|
|
|
32
32
|
spec.add_development_dependency "rake"
|
33
33
|
spec.add_development_dependency "rspec"
|
34
34
|
|
35
|
+
# Translation and HTML manipulation requires Nokogiri
|
36
|
+
spec.add_runtime_dependency 'nokogiri', '>= 1.17'
|
37
|
+
|
35
38
|
# Ruby version
|
36
39
|
spec.required_ruby_version = ">= 2.0.0"
|
37
40
|
end
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'digest'
|
6
|
+
|
7
|
+
module Jekyll
|
8
|
+
class TranslatePages < Generator
|
9
|
+
safe true
|
10
|
+
priority :low
|
11
|
+
|
12
|
+
# Variables
|
13
|
+
# Translation path
|
14
|
+
CACHE_DIR = '.temp/translations'
|
15
|
+
# Re-translate pages older than this many days
|
16
|
+
RECHECK_DAYS = 30
|
17
|
+
|
18
|
+
def generate(site)
|
19
|
+
# Get target languages from site config
|
20
|
+
target_langs = site.config.dig('translation', 'languages') || []
|
21
|
+
|
22
|
+
# Log
|
23
|
+
puts "🔁 Starting translation process for supported languages (#{target_langs.length}): #{target_langs.join(', ')}"
|
24
|
+
# puts "📂 Cache directory: #{CACHE_DIR}"
|
25
|
+
# puts "🔍 All environment variables:"
|
26
|
+
# ENV.each { |k, v| puts " #{k}=#{v}" }
|
27
|
+
puts "🔍 UJ_ environment variables:"
|
28
|
+
ENV.select { |k, _| k.start_with?('UJ_') }.each { |k, v| puts " #{k}=#{v}" }
|
29
|
+
|
30
|
+
# Skip if site config translation is disabled
|
31
|
+
unless site.config.dig('translation', 'enabled')
|
32
|
+
puts "🚫 Translation is disabled in _config.yml (translation.enabled: false)"
|
33
|
+
return
|
34
|
+
end
|
35
|
+
|
36
|
+
# Quit if UJ_BUILD_MODE is false
|
37
|
+
if ENV['UJ_BUILD_MODE'] == 'false' && ENV['UJ_TRANSLATION_FORCE'] != 'true'
|
38
|
+
puts "🚫 UJ_BUILD_MODE is set to 'false' (set UJ_TRANSLATION_FORCE=true). Exiting translation process."
|
39
|
+
return
|
40
|
+
end
|
41
|
+
|
42
|
+
# Ensure OpenAI API key is set
|
43
|
+
unless ENV['OPENAI_API_KEY'] && !ENV['OPENAI_API_KEY'].strip.empty?
|
44
|
+
puts "❌ OPENAI_API_KEY not found in environment. Exiting translation process."
|
45
|
+
return
|
46
|
+
end
|
47
|
+
|
48
|
+
# Quit if no languages are configured
|
49
|
+
if target_langs.empty?
|
50
|
+
puts "🚫 No target languages configured in _config.yml (translation.languages). Exiting translation process."
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
# Keep track of skipped files
|
55
|
+
skipped_files = []
|
56
|
+
|
57
|
+
# Loop through all pages in the site
|
58
|
+
site.pages.clone.each do |page|
|
59
|
+
next unless page.output_ext == '.html'
|
60
|
+
original_content = page.content
|
61
|
+
original_hash = Digest::SHA256.hexdigest(original_content)
|
62
|
+
page_path = page.path.sub(/^_?site\//, '')
|
63
|
+
|
64
|
+
# Skip if page.translation.enabled is false
|
65
|
+
if page.data['translation'] && page.data['translation']['enabled'] == false
|
66
|
+
skipped_files << "#{page_path} (translation disabled)"
|
67
|
+
next
|
68
|
+
end
|
69
|
+
|
70
|
+
# Skip if page.redirect.url is set
|
71
|
+
if page.data['redirect'] && page.data['redirect']['url']
|
72
|
+
skipped_files << "#{page_path} (redirect set)"
|
73
|
+
next
|
74
|
+
end
|
75
|
+
|
76
|
+
target_langs.each do |lang|
|
77
|
+
translated_path = File.join(CACHE_DIR, lang, page_path)
|
78
|
+
meta_path = "#{translated_path}.meta.json"
|
79
|
+
|
80
|
+
# @TODO: Remove this
|
81
|
+
# Unless its pages/legal/terms.md, QUIT
|
82
|
+
uj_translation_only = ENV['UJ_TRANSLATION_ONLY']
|
83
|
+
if uj_translation_only && page_path != uj_translation_only
|
84
|
+
skipped_files << "#{page_path} (UJ_TRANSLATION_ONLY is set)"
|
85
|
+
next
|
86
|
+
end
|
87
|
+
|
88
|
+
# Log
|
89
|
+
puts "🌐 Processing page '#{page_path}' for language '#{lang}'"
|
90
|
+
|
91
|
+
# Either read cached translation or generate a new one
|
92
|
+
translated = read_or_translate(original_content, original_hash, lang, translated_path, meta_path)
|
93
|
+
|
94
|
+
next unless translated # skip this lang if translation failed
|
95
|
+
|
96
|
+
# Build new page with translated content
|
97
|
+
new_page = page.dup
|
98
|
+
new_page.data = page.data.dup
|
99
|
+
new_page.data['lang'] = lang
|
100
|
+
new_page.data['permalink'] = "/#{lang}#{page.url}"
|
101
|
+
new_page.content = rewrite_links(translated, lang)
|
102
|
+
|
103
|
+
site.pages << new_page
|
104
|
+
puts "✅ Added translated page: /#{lang}#{page.url}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Log skipped files at the end
|
109
|
+
if skipped_files.any?
|
110
|
+
puts "\n🚫 Skipped files:"
|
111
|
+
skipped_files.each { |f| puts " - #{f}" }
|
112
|
+
end
|
113
|
+
|
114
|
+
puts "🎉 Translation process complete."
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Return cached translation or generate new one via API
|
120
|
+
def read_or_translate(content, hash, lang, path, meta_path)
|
121
|
+
if File.exist?(path) && File.exist?(meta_path)
|
122
|
+
meta = JSON.parse(File.read(meta_path)) rescue {}
|
123
|
+
last_hash = meta['hash']
|
124
|
+
last_time = Time.at(meta['timestamp'].to_i) rescue Time.at(0)
|
125
|
+
|
126
|
+
age_days = ((Time.now - last_time) / (60 * 60 * 24)).round
|
127
|
+
puts "📅 Cache age: #{age_days}/#{RECHECK_DAYS} days"
|
128
|
+
|
129
|
+
# Determine whether we should re-check based on age
|
130
|
+
recheck_due_to_age = RECHECK_DAYS && RECHECK_DAYS > 0 && age_days >= RECHECK_DAYS
|
131
|
+
|
132
|
+
# Re-translate if hash changed or translation is too old (only if RECHECK_DAYS is truthy)
|
133
|
+
if last_hash == hash && !recheck_due_to_age
|
134
|
+
puts "📦 Using cached translation at: #{path}"
|
135
|
+
return File.read(path)
|
136
|
+
else
|
137
|
+
puts "🔁 Cache stale or hash changed, regenerating translation..."
|
138
|
+
end
|
139
|
+
else
|
140
|
+
puts "📭 No cache found, generating translation..."
|
141
|
+
end
|
142
|
+
|
143
|
+
# Log before/after content
|
144
|
+
puts "\n--- BEFORE CONTENT (#{lang}) ---\n#{content[0..500]}..."
|
145
|
+
|
146
|
+
# Translate the content using OpenAI API
|
147
|
+
begin
|
148
|
+
result = translate_with_api(content, lang)
|
149
|
+
rescue => e
|
150
|
+
puts "❌ Skipping translation for '#{lang}' due to error: #{e.message}"
|
151
|
+
return nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# Log the first 500 characters of the result
|
155
|
+
puts "\n--- AFTER TRANSLATION (#{lang}) ---\n#{result[0..500]}..."
|
156
|
+
|
157
|
+
# Save the translation and metadata
|
158
|
+
FileUtils.mkdir_p(File.dirname(path))
|
159
|
+
File.write(path, result)
|
160
|
+
File.write(meta_path, {
|
161
|
+
timestamp: Time.now.to_i,
|
162
|
+
hash: hash
|
163
|
+
}.to_json)
|
164
|
+
|
165
|
+
puts "📝 Cached translation and metadata written to: #{path}"
|
166
|
+
|
167
|
+
result
|
168
|
+
end
|
169
|
+
|
170
|
+
# Perform translation via OpenAI API
|
171
|
+
def translate_with_api(content, lang)
|
172
|
+
system_prompt = "You are a professional translator. Translate the provided HTML content, preserving all original formatting, HTML structure, metadata, and links. Do not explain anything — just return the translated HTML. Translate to #{lang}."
|
173
|
+
user_message = content
|
174
|
+
|
175
|
+
uri = URI('https://api.openai.com/v1/chat/completions')
|
176
|
+
|
177
|
+
res = Net::HTTP.post(
|
178
|
+
uri,
|
179
|
+
{
|
180
|
+
model: 'gpt-4',
|
181
|
+
messages: [
|
182
|
+
{ role: 'system', content: system_prompt },
|
183
|
+
{ role: 'user', content: user_message }
|
184
|
+
],
|
185
|
+
temperature: 0.3
|
186
|
+
}.to_json,
|
187
|
+
{
|
188
|
+
'Authorization' => "Bearer #{ENV['OPENAI_API_KEY']}",
|
189
|
+
'Content-Type' => 'application/json'
|
190
|
+
}
|
191
|
+
)
|
192
|
+
|
193
|
+
if res.code.to_i != 200
|
194
|
+
raise "HTTP #{res.code}: #{res.body}"
|
195
|
+
end
|
196
|
+
|
197
|
+
json = JSON.parse(res.body)
|
198
|
+
# Log json
|
199
|
+
puts "🔍 API response: #{json.inspect}"
|
200
|
+
result = json.dig('choices', 0, 'message', 'content')
|
201
|
+
|
202
|
+
if result.nil? || result.strip.empty?
|
203
|
+
raise "Translation returned empty or invalid content"
|
204
|
+
end
|
205
|
+
|
206
|
+
puts "🔤 Translation complete."
|
207
|
+
result
|
208
|
+
end
|
209
|
+
|
210
|
+
# Rewrite internal links to language-prefixed versions
|
211
|
+
def rewrite_links(html, lang)
|
212
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(html)
|
213
|
+
doc.css('a[href^="/"]').each do |a|
|
214
|
+
href = a['href']
|
215
|
+
next if href.start_with?("/#{lang}") || href.include?('.') || href.start_with?('//')
|
216
|
+
new_href = "/#{lang}#{href}"
|
217
|
+
puts "🔗 Rewriting link: #{href} -> #{new_href}"
|
218
|
+
a['href'] = new_href
|
219
|
+
end
|
220
|
+
doc.to_html
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# Libraries
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'digest'
|
7
|
+
|
8
|
+
# Hook
|
9
|
+
Jekyll::Hooks.register :site, :post_write do |site|
|
10
|
+
# Variables
|
11
|
+
# Translation path
|
12
|
+
CACHE_DIR = '.temp/translations'
|
13
|
+
# Re-translate pages older than this many days
|
14
|
+
RECHECK_DAYS = 30
|
15
|
+
|
16
|
+
# Get target languages from site config
|
17
|
+
target_langs = site.config.dig('translation', 'languages') || []
|
18
|
+
|
19
|
+
# Log
|
20
|
+
puts "🔁 Starting translation process for supported languages (#{target_langs.length}): #{target_langs.join(', ')}"
|
21
|
+
puts "🔍 UJ_ environment variables:"
|
22
|
+
ENV.select { |k, _| k.start_with?('UJ_') }.each { |k, v| puts " #{k}=#{v}" }
|
23
|
+
|
24
|
+
# Skip if site config translation is disabled
|
25
|
+
unless site.config.dig('translation', 'enabled')
|
26
|
+
puts "🚫 Translation is disabled in _config.yml (translation.enabled: false)"
|
27
|
+
next
|
28
|
+
end
|
29
|
+
|
30
|
+
# Quit if UJ_BUILD_MODE is false
|
31
|
+
if ENV['UJ_BUILD_MODE'] == 'false' && ENV['UJ_TRANSLATION_FORCE'] != 'true'
|
32
|
+
puts "🚫 UJ_BUILD_MODE is set to 'false' (set UJ_TRANSLATION_FORCE=true). Exiting translation process."
|
33
|
+
next
|
34
|
+
end
|
35
|
+
|
36
|
+
# Ensure OpenAI API key is set
|
37
|
+
unless ENV['OPENAI_API_KEY'] && !ENV['OPENAI_API_KEY'].strip.empty?
|
38
|
+
puts "❌ OPENAI_API_KEY not found in environment. Exiting translation process."
|
39
|
+
next
|
40
|
+
end
|
41
|
+
|
42
|
+
# Quit if no languages are configured
|
43
|
+
if target_langs.empty?
|
44
|
+
puts "🚫 No target languages configured in _config.yml (translation.languages). Exiting translation process."
|
45
|
+
next
|
46
|
+
end
|
47
|
+
|
48
|
+
# Keep track of skipped files
|
49
|
+
skipped_files = []
|
50
|
+
|
51
|
+
# Loop through all pages in the site
|
52
|
+
site.pages.clone.each do |page|
|
53
|
+
# Quit if its not an HTML page
|
54
|
+
next unless page.output_ext == '.html'
|
55
|
+
|
56
|
+
# Get original content
|
57
|
+
original_content = page.output
|
58
|
+
|
59
|
+
# Extract body content
|
60
|
+
doc = Nokogiri::HTML(original_content)
|
61
|
+
original_content_body = doc.at('body')&.inner_html.to_s
|
62
|
+
|
63
|
+
# Compute original hash
|
64
|
+
original_hash = Digest::SHA256.hexdigest(original_content_body)
|
65
|
+
|
66
|
+
# Get page path and URL
|
67
|
+
page_path = page.path.sub(/^_?site\//, '')
|
68
|
+
page_url = page.url
|
69
|
+
|
70
|
+
# Skip if page.translation.enabled is false
|
71
|
+
if page.data['translation'] && page.data['translation']['enabled'] == false
|
72
|
+
skipped_files << "#{page_path} (translation disabled)"
|
73
|
+
next
|
74
|
+
end
|
75
|
+
|
76
|
+
# Skip if page.redirect.url is set
|
77
|
+
if page.data['redirect'] && page.data['redirect']['url']
|
78
|
+
skipped_files << "#{page_path} (redirect set)"
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
# Loop through target languages
|
83
|
+
target_langs.each do |lang|
|
84
|
+
page_new_url = "/#{lang}#{page.url}"
|
85
|
+
page_new_path = File.join(CACHE_DIR, lang, page_new_url)
|
86
|
+
page_new_meta_path = "#{page_new_path}.meta.json"
|
87
|
+
|
88
|
+
# See if we only want to test a specific page
|
89
|
+
uj_translation_only = ENV['UJ_TRANSLATION_ONLY']
|
90
|
+
if uj_translation_only && page_path != uj_translation_only
|
91
|
+
skipped_files << "#{page_path} (UJ_TRANSLATION_ONLY is set)"
|
92
|
+
next
|
93
|
+
end
|
94
|
+
|
95
|
+
# Log
|
96
|
+
puts "🌐 Processing page '#{page_url}' for language '#{lang}'"
|
97
|
+
|
98
|
+
# LOG new_page.data
|
99
|
+
# Log permalink
|
100
|
+
puts "🔗 New permalink: #{page_new_url}"
|
101
|
+
|
102
|
+
# Either read cached translation or generate a new one
|
103
|
+
translated = read_or_translate(original_content_body, original_hash, lang, page_new_path, page_new_meta_path)
|
104
|
+
|
105
|
+
# Fallback if translation failed
|
106
|
+
if translated.nil?
|
107
|
+
puts "⚠️ Translation failed for #{page_url}, using original content and marking for retry"
|
108
|
+
|
109
|
+
# Force a retry next time by setting bad hash + old timestamp
|
110
|
+
FileUtils.mkdir_p(File.dirname(page_new_meta_path))
|
111
|
+
File.write(page_new_meta_path, {
|
112
|
+
timestamp: 0,
|
113
|
+
hash: '__fail__'
|
114
|
+
}.to_json)
|
115
|
+
|
116
|
+
translated = original_content_body
|
117
|
+
end
|
118
|
+
|
119
|
+
# Rewrite internal links
|
120
|
+
translated_html = rewrite_links(translated, lang)
|
121
|
+
|
122
|
+
# Inject translated content into original HTML structure
|
123
|
+
translated_doc = Nokogiri::HTML(original_content)
|
124
|
+
translated_doc.at('body').inner_html = translated_html
|
125
|
+
final_html = translated_doc.to_html
|
126
|
+
|
127
|
+
# Determine output path
|
128
|
+
output_dir = site.config['destination']
|
129
|
+
translated_output_path = File.join(output_dir, lang, page.url)
|
130
|
+
translated_output_path = File.join(translated_output_path, 'index.html') if translated_output_path.end_with?('/')
|
131
|
+
|
132
|
+
# Write translated page to disk
|
133
|
+
FileUtils.mkdir_p(File.dirname(translated_output_path))
|
134
|
+
File.write(translated_output_path, final_html)
|
135
|
+
puts "✅ Wrote translated file: #{translated_output_path}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Log skipped files at the end
|
140
|
+
if skipped_files.any?
|
141
|
+
puts "\n🚫 Skipped files:"
|
142
|
+
skipped_files.each { |f| puts " - #{f}" }
|
143
|
+
end
|
144
|
+
|
145
|
+
# Log
|
146
|
+
puts "🎉 Translation process complete."
|
147
|
+
end
|
148
|
+
|
149
|
+
def read_or_translate(content, hash, lang, path, page_new_meta_path)
|
150
|
+
if File.exist?(path) && File.exist?(page_new_meta_path)
|
151
|
+
meta = JSON.parse(File.read(page_new_meta_path)) rescue {}
|
152
|
+
last_hash = meta['hash']
|
153
|
+
last_time = Time.at(meta['timestamp'].to_i) rescue Time.at(0)
|
154
|
+
|
155
|
+
age_days = ((Time.now - last_time) / (60 * 60 * 24)).round
|
156
|
+
puts "📅 Cache age: #{age_days}/#{RECHECK_DAYS} days"
|
157
|
+
|
158
|
+
# Determine whether we should re-check based on age
|
159
|
+
recheck_due_to_age = RECHECK_DAYS && RECHECK_DAYS > 0 && age_days >= RECHECK_DAYS
|
160
|
+
|
161
|
+
# Re-translate if hash changed or translation is too old (only if RECHECK_DAYS is truthy)
|
162
|
+
if last_hash == hash && !recheck_due_to_age
|
163
|
+
puts "📦 Using cached translation at: #{path}"
|
164
|
+
return File.read(path)
|
165
|
+
else
|
166
|
+
puts "🔁 Cache stale or hash changed, regenerating translation..."
|
167
|
+
end
|
168
|
+
else
|
169
|
+
puts "📭 No cache found, generating translation..."
|
170
|
+
end
|
171
|
+
|
172
|
+
# Log before/after content
|
173
|
+
puts "\n--- BEFORE CONTENT (#{lang}) ---\n#{content[0..500]}..."
|
174
|
+
|
175
|
+
# Translate the content using OpenAI API
|
176
|
+
begin
|
177
|
+
result = translate_with_api(content, lang)
|
178
|
+
rescue => e
|
179
|
+
puts "❌ Skipping translation for '#{lang}' due to error: #{e.message}"
|
180
|
+
return nil
|
181
|
+
end
|
182
|
+
|
183
|
+
# Log the first 500 characters of the result
|
184
|
+
puts "\n--- AFTER TRANSLATION (#{lang}) ---\n#{result[0..500]}..."
|
185
|
+
|
186
|
+
# Save the translation and metadata
|
187
|
+
FileUtils.mkdir_p(File.dirname(path))
|
188
|
+
File.write(path, result)
|
189
|
+
File.write(page_new_meta_path, {
|
190
|
+
timestamp: Time.now.to_i,
|
191
|
+
hash: hash
|
192
|
+
}.to_json)
|
193
|
+
|
194
|
+
puts "📝 Cached translation and metadata written to: #{path}"
|
195
|
+
|
196
|
+
result
|
197
|
+
end
|
198
|
+
|
199
|
+
def translate_with_api(content, lang)
|
200
|
+
system_prompt = "You are a professional translator. Translate the provided HTML content, preserving all original formatting, HTML structure, metadata, and links. Do not explain anything — just return the translated HTML. Translate to #{lang}."
|
201
|
+
user_message = content
|
202
|
+
|
203
|
+
uri = URI('https://api.openai.com/v1/chat/completions')
|
204
|
+
|
205
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
206
|
+
http.use_ssl = true
|
207
|
+
http.read_timeout = 30 # seconds
|
208
|
+
http.open_timeout = 10 # seconds
|
209
|
+
|
210
|
+
request = Net::HTTP::Post.new(uri.path, {
|
211
|
+
'Authorization' => "Bearer #{ENV['OPENAI_API_KEY']}",
|
212
|
+
'Content-Type' => 'application/json'
|
213
|
+
})
|
214
|
+
|
215
|
+
request.body = {
|
216
|
+
model: 'gpt-4o',
|
217
|
+
messages: [
|
218
|
+
{ role: 'system', content: system_prompt },
|
219
|
+
{ role: 'user', content: user_message }
|
220
|
+
],
|
221
|
+
temperature: 0.3,
|
222
|
+
max_tokens: 4096
|
223
|
+
}.to_json
|
224
|
+
|
225
|
+
response = http.request(request)
|
226
|
+
|
227
|
+
if response.code.to_i != 200
|
228
|
+
raise "HTTP #{response.code}: #{response.body}"
|
229
|
+
end
|
230
|
+
|
231
|
+
json = JSON.parse(response.body)
|
232
|
+
puts "🔍 API response: #{json.inspect}"
|
233
|
+
result = json.dig('choices', 0, 'message', 'content')
|
234
|
+
|
235
|
+
if result.nil? || result.strip.empty?
|
236
|
+
raise "Translation returned empty or invalid content"
|
237
|
+
end
|
238
|
+
|
239
|
+
puts "🔤 Translation complete."
|
240
|
+
result
|
241
|
+
end
|
242
|
+
|
243
|
+
def rewrite_links(html, lang)
|
244
|
+
doc = Nokogiri::HTML::DocumentFragment.parse(html)
|
245
|
+
doc.css('a[href^="/"]').each do |a|
|
246
|
+
href = a['href']
|
247
|
+
next if href.start_with?("/#{lang}") || href.include?('.') || href.start_with?('//')
|
248
|
+
new_href = "/#{lang}#{href}"
|
249
|
+
puts "🔗 Rewriting link: #{href} -> #{new_href}"
|
250
|
+
a['href'] = new_href
|
251
|
+
end
|
252
|
+
doc.to_html
|
253
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Libraries
|
2
|
+
# ...
|
3
|
+
|
4
|
+
module Jekyll
|
5
|
+
class InjectData < Generator
|
6
|
+
safe true
|
7
|
+
priority :low
|
8
|
+
|
9
|
+
def generate(site)
|
10
|
+
# Define a global variable accessible in templates
|
11
|
+
# site.config['cache_breaker'] = Time.now.to_i.to_s
|
12
|
+
|
13
|
+
# Process pages
|
14
|
+
site.pages.each do |page|
|
15
|
+
inject_data(page, site)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Process documents in all collections
|
19
|
+
site.collections.each do |_, collection|
|
20
|
+
collection.docs.each do |document|
|
21
|
+
inject_data(document, site)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def inject_data(item, site)
|
29
|
+
# Inject a random number into the item's data
|
30
|
+
item.data['random_id'] = rand(100) # Random number between 0 and 99
|
31
|
+
|
32
|
+
return unless item.data['layout'] # Skip items without layouts
|
33
|
+
|
34
|
+
# Find the layout file by its name
|
35
|
+
layout_name = item.data['layout']
|
36
|
+
layout = site.layouts[layout_name]
|
37
|
+
|
38
|
+
if layout && layout.data
|
39
|
+
# Merge layout front matter into item's "layout_data"
|
40
|
+
item.data['layout_data'] = layout.data
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/jekyll-uj-powertools.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# Libraries
|
1
2
|
require "jekyll"
|
2
3
|
|
4
|
+
# Module
|
3
5
|
module Jekyll
|
4
6
|
module UJPowertools
|
5
7
|
# Initialize a timestamp that will remain consistent across calls
|
@@ -62,65 +64,13 @@ module Jekyll
|
|
62
64
|
end
|
63
65
|
end
|
64
66
|
|
65
|
-
#
|
66
|
-
|
67
|
-
# safe true
|
68
|
-
# priority :highest
|
67
|
+
# Load Generators
|
68
|
+
require_relative "generators/inject-properties"
|
69
69
|
|
70
|
-
#
|
71
|
-
|
72
|
-
#
|
73
|
-
# end
|
74
|
-
# end
|
75
|
-
|
76
|
-
# Inject data into pages and documents
|
77
|
-
class InjectData < Generator
|
78
|
-
safe true
|
79
|
-
priority :low
|
80
|
-
|
81
|
-
def generate(site)
|
82
|
-
# Define a global variable accessible in templates
|
83
|
-
# site.config['cache_breaker'] = Time.now.to_i.to_s
|
84
|
-
|
85
|
-
# Process pages
|
86
|
-
site.pages.each do |page|
|
87
|
-
inject_data(page, site)
|
88
|
-
end
|
89
|
-
|
90
|
-
# Process documents in all collections
|
91
|
-
site.collections.each do |_, collection|
|
92
|
-
collection.docs.each do |document|
|
93
|
-
inject_data(document, site)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
private
|
99
|
-
|
100
|
-
def inject_data(item, site)
|
101
|
-
# Inject a random number into the item's data
|
102
|
-
item.data['random_id'] = rand(100) # Random number between 0 and 99
|
103
|
-
|
104
|
-
return unless item.data['layout'] # Skip items without layouts
|
105
|
-
|
106
|
-
# Find the layout file by its name
|
107
|
-
layout_name = item.data['layout']
|
108
|
-
layout = site.layouts[layout_name]
|
109
|
-
|
110
|
-
if layout && layout.data
|
111
|
-
# Merge layout front matter into item's "layout_data"
|
112
|
-
item.data['layout_data'] = layout.data
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
70
|
+
# Load Hooks
|
71
|
+
require_relative "hooks/inject-properties"
|
72
|
+
# require_relative "hooks/translate-pages"
|
116
73
|
end
|
117
74
|
|
118
75
|
# Register the filter
|
119
76
|
Liquid::Template.register_filter(Jekyll::UJPowertools)
|
120
|
-
|
121
|
-
# Register hook
|
122
|
-
Jekyll::Hooks.register :site, :pre_render do |site|
|
123
|
-
site.config['uj'] ||= {}
|
124
|
-
site.config['uj']['cacheBreaker'] = Jekyll::UJPowertools.cache_timestamp
|
125
|
-
site.config['uj']['cache_breaker'] = Jekyll::UJPowertools.cache_timestamp
|
126
|
-
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jekyll-uj-powertools
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ITW Creative Works
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-06-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jekyll
|
@@ -72,6 +72,20 @@ dependencies:
|
|
72
72
|
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: nokogiri
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.17'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '1.17'
|
75
89
|
description: jekyll-uj-powertools provides a powerful set of utilities for Jekyll,
|
76
90
|
including functions to remove ads from strings and escape JSON characters.
|
77
91
|
email:
|
@@ -85,6 +99,10 @@ files:
|
|
85
99
|
- README.md
|
86
100
|
- Rakefile
|
87
101
|
- jekyll-uj-powertools.gemspec
|
102
|
+
- lib/_old/generators/translate-pages.rb
|
103
|
+
- lib/_old/hooks/translate-pages.rb
|
104
|
+
- lib/generators/inject-properties.rb
|
105
|
+
- lib/hooks/inject-properties.rb
|
88
106
|
- lib/jekyll-uj-powertools.rb
|
89
107
|
- lib/jekyll-uj-powertools/version.rb
|
90
108
|
- spec/jekyll-uj-powertools_spec.rb
|