jekyll-uj-powertools 1.3.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2db2b27f78f8588283c589c8d6c06e342ba6a11a1b1b55669ea81b722b5fe64b
4
- data.tar.gz: 47b62725a184ea99d3f25daec3620e57539dbd8fb42159bdd81bb1f68176e5f9
3
+ metadata.gz: 06406f26c558d459244fad81422d210523148d4a90bd42d004fcdfcaede642e0
4
+ data.tar.gz: d6e23dd0c11253df4dfef38612049904cb251728beade6139a2d5a74f51f3a19
5
5
  SHA512:
6
- metadata.gz: 81e5f82be027417b008c9a6d3cbc2bb86bc5f992a60447fb42dc94011da6abb4cb5374756bca80141c36cad263e92e16c2a7ee18a94c4c5122173e9c1464a5ae
7
- data.tar.gz: 6a3ea9d448ba3a543cf70623e6eb67ab72dc25eecbd761d9959e66cf6a9db92342271307713a95ea6bb268b761e2f23e0565d107c3c4fa8f6528543b3118f036
6
+ metadata.gz: a40bb126326706aee31507e2ad02d83a0a1e83ff8927bf99d90f0869d4898034f282f73e27032013c5018c3553ed435964f7de170d4b502f2d3be900ef7e1666
7
+ data.tar.gz: 5c5ff4b05d49cfc03312d7eaa834ef0da25c5a56df0c5ec99fcf7452eda94d40b97809d90edfd1fdfea639008e344c5e7cc22460d00bed30a57fcd48fdd0a05d
data/README.md CHANGED
@@ -92,11 +92,17 @@ bundle exec rspec
92
92
 
93
93
  ## 💎 Build + Publish the Gem
94
94
  ```shell
95
- # Build the gem
96
- gem build jekyll-uj-powertools.gemspec
95
+ # Ensure dist folder exists
96
+ mkdir -p dist
97
97
 
98
- # Publish the gem where X.X.X is the version number
99
- gem push jekyll-uj-powertools-X.X.X.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
- if page_path != 'pages/legal/terms.md'
76
- skipped_files << "#{page_path} (only 'pages/legal/terms.md' is processed)"
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
@@ -0,0 +1,8 @@
1
+ # Libraries
2
+ # ...
3
+
4
+ # Hook
5
+ Jekyll::Hooks.register :site, :pre_render do |site|
6
+ site.config['uj'] ||= {}
7
+ site.config['uj']['cache_breaker'] = Jekyll::UJPowertools.cache_timestamp
8
+ end
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module UJPowertools
3
- VERSION = "1.3.0"
3
+ VERSION = "1.3.1"
4
4
  end
5
5
  end
@@ -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
@@ -63,15 +65,12 @@ module Jekyll
63
65
  end
64
66
 
65
67
  # Load Generators
66
- require_relative "generators/inject-data"
67
- require_relative "generators/translate-pages"
68
+ require_relative "generators/inject-properties"
69
+
70
+ # Load Hooks
71
+ require_relative "hooks/inject-properties"
72
+ # require_relative "hooks/translate-pages"
68
73
  end
69
74
 
70
75
  # Register the filter
71
76
  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.0
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-06-16 00:00:00.000000000 Z
11
+ date: 2025-06-17 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/inject-data.rb
103
- - lib/generators/translate-pages.rb
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