jekyll-uj-powertools 1.3.0 → 1.3.2
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/README.md +10 -4
- data/lib/{generators → _old/generators}/translate-pages.rb +22 -8
- data/lib/_old/hooks/translate-pages.rb +253 -0
- data/lib/generators/{inject-data.rb → inject-properties.rb} +10 -1
- data/lib/hooks/inject-properties.rb +8 -0
- data/lib/jekyll-uj-powertools/version.rb +1 -1
- data/lib/jekyll-uj-powertools.rb +12 -8
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f91b80879708adc5fb116e4ce3722237453ddd4ba20c9c1ebf5f798446f45c64
|
4
|
+
data.tar.gz: e35de7eebf30b3f535a64abb5f09f06c7c0ea2b75e0c304ccc05571927dca464
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ed5f204a4864131bbdbd54fa8838a058e385fa60826274d2208cfee36e3c23b57978df8fcdcd3b972a9629a3825c55618a5b437e012be88768ccb1d4fd09ab6
|
7
|
+
data.tar.gz: f4a1853571d6c486aa054e09ed725eb2af9035ada859fac30446a3356943c9b63a665a0d673aba75695471df0f30a1b49c308a6f62060feba91c27207e61c9ee
|
data/README.md
CHANGED
@@ -92,11 +92,17 @@ bundle exec rspec
|
|
92
92
|
|
93
93
|
## 💎 Build + Publish the Gem
|
94
94
|
```shell
|
95
|
-
#
|
96
|
-
|
95
|
+
# Ensure dist folder exists
|
96
|
+
mkdir -p dist
|
97
97
|
|
98
|
-
#
|
99
|
-
gem
|
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
|
103
|
+
|
104
|
+
# Clear the files in the dist folder
|
105
|
+
rm -rf dist/*
|
100
106
|
```
|
101
107
|
|
102
108
|
## 🗨️ Contributing
|
@@ -16,11 +16,12 @@ module Jekyll
|
|
16
16
|
RECHECK_DAYS = 30
|
17
17
|
|
18
18
|
def generate(site)
|
19
|
+
# Get target languages from site config
|
19
20
|
target_langs = site.config.dig('translation', 'languages') || []
|
20
21
|
|
21
22
|
# Log
|
22
|
-
puts "🔁 Starting translation process for supported languages: #{target_langs.join(', ')}"
|
23
|
-
puts "📂 Cache directory: #{CACHE_DIR}"
|
23
|
+
puts "🔁 Starting translation process for supported languages (#{target_langs.length}): #{target_langs.join(', ')}"
|
24
|
+
# puts "📂 Cache directory: #{CACHE_DIR}"
|
24
25
|
# puts "🔍 All environment variables:"
|
25
26
|
# ENV.each { |k, v| puts " #{k}=#{v}" }
|
26
27
|
puts "🔍 UJ_ environment variables:"
|
@@ -32,6 +33,12 @@ module Jekyll
|
|
32
33
|
return
|
33
34
|
end
|
34
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
|
+
|
35
42
|
# Ensure OpenAI API key is set
|
36
43
|
unless ENV['OPENAI_API_KEY'] && !ENV['OPENAI_API_KEY'].strip.empty?
|
37
44
|
puts "❌ OPENAI_API_KEY not found in environment. Exiting translation process."
|
@@ -72,8 +79,9 @@ module Jekyll
|
|
72
79
|
|
73
80
|
# @TODO: Remove this
|
74
81
|
# Unless its pages/legal/terms.md, QUIT
|
75
|
-
|
76
|
-
|
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)"
|
77
85
|
next
|
78
86
|
end
|
79
87
|
|
@@ -132,6 +140,10 @@ module Jekyll
|
|
132
140
|
puts "📭 No cache found, generating translation..."
|
133
141
|
end
|
134
142
|
|
143
|
+
# Log before/after content
|
144
|
+
puts "\n--- BEFORE CONTENT (#{lang}) ---\n#{content[0..500]}..."
|
145
|
+
|
146
|
+
# Translate the content using OpenAI API
|
135
147
|
begin
|
136
148
|
result = translate_with_api(content, lang)
|
137
149
|
rescue => e
|
@@ -139,6 +151,10 @@ module Jekyll
|
|
139
151
|
return nil
|
140
152
|
end
|
141
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
|
142
158
|
FileUtils.mkdir_p(File.dirname(path))
|
143
159
|
File.write(path, result)
|
144
160
|
File.write(meta_path, {
|
@@ -148,10 +164,6 @@ module Jekyll
|
|
148
164
|
|
149
165
|
puts "📝 Cached translation and metadata written to: #{path}"
|
150
166
|
|
151
|
-
# Log before/after content
|
152
|
-
puts "\n--- BEFORE CONTENT (#{lang}) ---\n#{content[0..500]}..."
|
153
|
-
puts "\n--- AFTER TRANSLATION (#{lang}) ---\n#{result[0..500]}..."
|
154
|
-
|
155
167
|
result
|
156
168
|
end
|
157
169
|
|
@@ -183,6 +195,8 @@ module Jekyll
|
|
183
195
|
end
|
184
196
|
|
185
197
|
json = JSON.parse(res.body)
|
198
|
+
# Log json
|
199
|
+
puts "🔍 API response: #{json.inspect}"
|
186
200
|
result = json.dig('choices', 0, 'message', 'content')
|
187
201
|
|
188
202
|
if result.nil? || result.strip.empty?
|
@@ -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
|
@@ -1,3 +1,6 @@
|
|
1
|
+
# Libraries
|
2
|
+
# ...
|
3
|
+
|
1
4
|
module Jekyll
|
2
5
|
class InjectData < Generator
|
3
6
|
safe true
|
@@ -26,7 +29,13 @@ module Jekyll
|
|
26
29
|
# Inject a random number into the item's data
|
27
30
|
item.data['random_id'] = rand(100) # Random number between 0 and 99
|
28
31
|
|
29
|
-
|
32
|
+
# Inject the file extension into the item's data
|
33
|
+
if item.respond_to?(:path)
|
34
|
+
item.data['extension'] = File.extname(item.path)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Skip items without layouts
|
38
|
+
return unless item.data['layout']
|
30
39
|
|
31
40
|
# Find the layout file by its name
|
32
41
|
layout_name = item.data['layout']
|
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
|
@@ -60,18 +62,20 @@ module Jekyll
|
|
60
62
|
def self.cache_timestamp
|
61
63
|
@cache_timestamp
|
62
64
|
end
|
65
|
+
|
66
|
+
# Check if a string ends with a specific suffix
|
67
|
+
# def uj_ends_with(input, suffix)
|
68
|
+
# input.end_with?(suffix)
|
69
|
+
# end
|
63
70
|
end
|
64
71
|
|
65
72
|
# Load Generators
|
66
|
-
require_relative "generators/inject-
|
67
|
-
|
73
|
+
require_relative "generators/inject-properties"
|
74
|
+
|
75
|
+
# Load Hooks
|
76
|
+
require_relative "hooks/inject-properties"
|
77
|
+
# require_relative "hooks/translate-pages"
|
68
78
|
end
|
69
79
|
|
70
80
|
# Register the filter
|
71
81
|
Liquid::Template.register_filter(Jekyll::UJPowertools)
|
72
|
-
|
73
|
-
# Register hook
|
74
|
-
Jekyll::Hooks.register :site, :pre_render do |site|
|
75
|
-
site.config['uj'] ||= {}
|
76
|
-
site.config['uj']['cache_breaker'] = Jekyll::UJPowertools.cache_timestamp
|
77
|
-
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.3.
|
4
|
+
version: 1.3.2
|
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-06-
|
11
|
+
date: 2025-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: jekyll
|
@@ -99,8 +99,10 @@ files:
|
|
99
99
|
- README.md
|
100
100
|
- Rakefile
|
101
101
|
- jekyll-uj-powertools.gemspec
|
102
|
-
- lib/generators/
|
103
|
-
- lib/
|
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
|
104
106
|
- lib/jekyll-uj-powertools.rb
|
105
107
|
- lib/jekyll-uj-powertools/version.rb
|
106
108
|
- spec/jekyll-uj-powertools_spec.rb
|