jekyll-uj-powertools 1.2.7 → 1.3.0

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: e1eb2312be53f2c304f73706fa54b5c46ad7dc7d3f5ec430da54c41068923501
4
- data.tar.gz: 3f5529e3c48c5dd86ec556927e5eaa8eabf80d496437df3ca2f1416bfff0e841
3
+ metadata.gz: 2db2b27f78f8588283c589c8d6c06e342ba6a11a1b1b55669ea81b722b5fe64b
4
+ data.tar.gz: 47b62725a184ea99d3f25daec3620e57539dbd8fb42159bdd81bb1f68176e5f9
5
5
  SHA512:
6
- metadata.gz: 42c17aed83e227c2ace9e65c73079ca439e49cae48183a5bdef771e556c043f0cf67f166af174a0505f174f6a36f89aed4bf186b9741f23dad3fe03fbc599bf1
7
- data.tar.gz: d60994e315edf47d64e04e4d5718380293f0aa9f6719b9c3e714f034f451c2b277f3e06f5e6207d291302e70ceb200aba5bc9029e45c8967bda23b152a46bd76
6
+ metadata.gz: 81e5f82be027417b008c9a6d3cbc2bb86bc5f992a60447fb42dc94011da6abb4cb5374756bca80141c36cad263e92e16c2a7ee18a94c4c5122173e9c1464a5ae
7
+ data.tar.gz: 6a3ea9d448ba3a543cf70623e6eb67ab72dc25eecbd761d9959e66cf6a9db92342271307713a95ea6bb268b761e2f23e0565d107c3c4fa8f6528543b3118f036
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in jekyll-uj-powertools.gemspec
6
6
  gemspec
7
7
 
8
+ # Require Jekyll version if specified in the environment variable
8
9
  if ENV["JEKYLL_VERSION"]
9
10
  gem "jekyll", "~> #{ENV["JEKYLL_VERSION"]}"
10
11
  end
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
@@ -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,41 @@
1
+ module Jekyll
2
+ class InjectData < Generator
3
+ safe true
4
+ priority :low
5
+
6
+ def generate(site)
7
+ # Define a global variable accessible in templates
8
+ # site.config['cache_breaker'] = Time.now.to_i.to_s
9
+
10
+ # Process pages
11
+ site.pages.each do |page|
12
+ inject_data(page, site)
13
+ end
14
+
15
+ # Process documents in all collections
16
+ site.collections.each do |_, collection|
17
+ collection.docs.each do |document|
18
+ inject_data(document, site)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def inject_data(item, site)
26
+ # Inject a random number into the item's data
27
+ item.data['random_id'] = rand(100) # Random number between 0 and 99
28
+
29
+ return unless item.data['layout'] # Skip items without layouts
30
+
31
+ # Find the layout file by its name
32
+ layout_name = item.data['layout']
33
+ layout = site.layouts[layout_name]
34
+
35
+ if layout && layout.data
36
+ # Merge layout front matter into item's "layout_data"
37
+ item.data['layout_data'] = layout.data
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,209 @@
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
+ target_langs = site.config.dig('translation', 'languages') || []
20
+
21
+ # Log
22
+ puts "🔁 Starting translation process for supported languages: #{target_langs.join(', ')}"
23
+ puts "📂 Cache directory: #{CACHE_DIR}"
24
+ # puts "🔍 All environment variables:"
25
+ # ENV.each { |k, v| puts " #{k}=#{v}" }
26
+ puts "🔍 UJ_ environment variables:"
27
+ ENV.select { |k, _| k.start_with?('UJ_') }.each { |k, v| puts " #{k}=#{v}" }
28
+
29
+ # Skip if site config translation is disabled
30
+ unless site.config.dig('translation', 'enabled')
31
+ puts "🚫 Translation is disabled in _config.yml (translation.enabled: false)"
32
+ return
33
+ end
34
+
35
+ # Ensure OpenAI API key is set
36
+ unless ENV['OPENAI_API_KEY'] && !ENV['OPENAI_API_KEY'].strip.empty?
37
+ puts "❌ OPENAI_API_KEY not found in environment. Exiting translation process."
38
+ return
39
+ end
40
+
41
+ # Quit if no languages are configured
42
+ if target_langs.empty?
43
+ puts "🚫 No target languages configured in _config.yml (translation.languages). Exiting translation process."
44
+ return
45
+ end
46
+
47
+ # Keep track of skipped files
48
+ skipped_files = []
49
+
50
+ # Loop through all pages in the site
51
+ site.pages.clone.each do |page|
52
+ next unless page.output_ext == '.html'
53
+ original_content = page.content
54
+ original_hash = Digest::SHA256.hexdigest(original_content)
55
+ page_path = page.path.sub(/^_?site\//, '')
56
+
57
+ # Skip if page.translation.enabled is false
58
+ if page.data['translation'] && page.data['translation']['enabled'] == false
59
+ skipped_files << "#{page_path} (translation disabled)"
60
+ next
61
+ end
62
+
63
+ # Skip if page.redirect.url is set
64
+ if page.data['redirect'] && page.data['redirect']['url']
65
+ skipped_files << "#{page_path} (redirect set)"
66
+ next
67
+ end
68
+
69
+ target_langs.each do |lang|
70
+ translated_path = File.join(CACHE_DIR, lang, page_path)
71
+ meta_path = "#{translated_path}.meta.json"
72
+
73
+ # @TODO: Remove this
74
+ # 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)"
77
+ next
78
+ end
79
+
80
+ # Log
81
+ puts "🌐 Processing page '#{page_path}' for language '#{lang}'"
82
+
83
+ # Either read cached translation or generate a new one
84
+ translated = read_or_translate(original_content, original_hash, lang, translated_path, meta_path)
85
+
86
+ next unless translated # skip this lang if translation failed
87
+
88
+ # Build new page with translated content
89
+ new_page = page.dup
90
+ new_page.data = page.data.dup
91
+ new_page.data['lang'] = lang
92
+ new_page.data['permalink'] = "/#{lang}#{page.url}"
93
+ new_page.content = rewrite_links(translated, lang)
94
+
95
+ site.pages << new_page
96
+ puts "✅ Added translated page: /#{lang}#{page.url}"
97
+ end
98
+ end
99
+
100
+ # Log skipped files at the end
101
+ if skipped_files.any?
102
+ puts "\n🚫 Skipped files:"
103
+ skipped_files.each { |f| puts " - #{f}" }
104
+ end
105
+
106
+ puts "🎉 Translation process complete."
107
+ end
108
+
109
+ private
110
+
111
+ # Return cached translation or generate new one via API
112
+ def read_or_translate(content, hash, lang, path, meta_path)
113
+ if File.exist?(path) && File.exist?(meta_path)
114
+ meta = JSON.parse(File.read(meta_path)) rescue {}
115
+ last_hash = meta['hash']
116
+ last_time = Time.at(meta['timestamp'].to_i) rescue Time.at(0)
117
+
118
+ age_days = ((Time.now - last_time) / (60 * 60 * 24)).round
119
+ puts "📅 Cache age: #{age_days}/#{RECHECK_DAYS} days"
120
+
121
+ # Determine whether we should re-check based on age
122
+ recheck_due_to_age = RECHECK_DAYS && RECHECK_DAYS > 0 && age_days >= RECHECK_DAYS
123
+
124
+ # Re-translate if hash changed or translation is too old (only if RECHECK_DAYS is truthy)
125
+ if last_hash == hash && !recheck_due_to_age
126
+ puts "📦 Using cached translation at: #{path}"
127
+ return File.read(path)
128
+ else
129
+ puts "🔁 Cache stale or hash changed, regenerating translation..."
130
+ end
131
+ else
132
+ puts "📭 No cache found, generating translation..."
133
+ end
134
+
135
+ begin
136
+ result = translate_with_api(content, lang)
137
+ rescue => e
138
+ puts "❌ Skipping translation for '#{lang}' due to error: #{e.message}"
139
+ return nil
140
+ end
141
+
142
+ FileUtils.mkdir_p(File.dirname(path))
143
+ File.write(path, result)
144
+ File.write(meta_path, {
145
+ timestamp: Time.now.to_i,
146
+ hash: hash
147
+ }.to_json)
148
+
149
+ puts "📝 Cached translation and metadata written to: #{path}"
150
+
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
+ result
156
+ end
157
+
158
+ # Perform translation via OpenAI API
159
+ def translate_with_api(content, lang)
160
+ 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}."
161
+ user_message = content
162
+
163
+ uri = URI('https://api.openai.com/v1/chat/completions')
164
+
165
+ res = Net::HTTP.post(
166
+ uri,
167
+ {
168
+ model: 'gpt-4',
169
+ messages: [
170
+ { role: 'system', content: system_prompt },
171
+ { role: 'user', content: user_message }
172
+ ],
173
+ temperature: 0.3
174
+ }.to_json,
175
+ {
176
+ 'Authorization' => "Bearer #{ENV['OPENAI_API_KEY']}",
177
+ 'Content-Type' => 'application/json'
178
+ }
179
+ )
180
+
181
+ if res.code.to_i != 200
182
+ raise "HTTP #{res.code}: #{res.body}"
183
+ end
184
+
185
+ json = JSON.parse(res.body)
186
+ result = json.dig('choices', 0, 'message', 'content')
187
+
188
+ if result.nil? || result.strip.empty?
189
+ raise "Translation returned empty or invalid content"
190
+ end
191
+
192
+ puts "🔤 Translation complete."
193
+ result
194
+ end
195
+
196
+ # Rewrite internal links to language-prefixed versions
197
+ def rewrite_links(html, lang)
198
+ doc = Nokogiri::HTML::DocumentFragment.parse(html)
199
+ doc.css('a[href^="/"]').each do |a|
200
+ href = a['href']
201
+ next if href.start_with?("/#{lang}") || href.include?('.') || href.start_with?('//')
202
+ new_href = "/#{lang}#{href}"
203
+ puts "🔗 Rewriting link: #{href} -> #{new_href}"
204
+ a['href'] = new_href
205
+ end
206
+ doc.to_html
207
+ end
208
+ end
209
+ end
@@ -1,5 +1,5 @@
1
1
  module Jekyll
2
2
  module UJPowertools
3
- VERSION = "1.2.7"
3
+ VERSION = "1.3.0"
4
4
  end
5
5
  end
@@ -62,57 +62,9 @@ module Jekyll
62
62
  end
63
63
  end
64
64
 
65
- # Set a global cache buster timestamp
66
- # class CacheBreakerGenerator < Jekyll::Generator
67
- # safe true
68
- # priority :highest
69
-
70
- # def generate(site)
71
- # # Define a global variable accessible in templates
72
- # site.config['cache_breaker2'] = Time.now.to_i.to_s
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
65
+ # Load Generators
66
+ require_relative "generators/inject-data"
67
+ require_relative "generators/translate-pages"
116
68
  end
117
69
 
118
70
  # Register the filter
@@ -121,6 +73,5 @@ Liquid::Template.register_filter(Jekyll::UJPowertools)
121
73
  # Register hook
122
74
  Jekyll::Hooks.register :site, :pre_render do |site|
123
75
  site.config['uj'] ||= {}
124
- site.config['uj']['cacheBreaker'] = Jekyll::UJPowertools.cache_timestamp
125
76
  site.config['uj']['cache_breaker'] = Jekyll::UJPowertools.cache_timestamp
126
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.2.7
4
+ version: 1.3.0
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-05-01 00:00:00.000000000 Z
11
+ date: 2025-06-16 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,8 @@ files:
85
99
  - README.md
86
100
  - Rakefile
87
101
  - jekyll-uj-powertools.gemspec
102
+ - lib/generators/inject-data.rb
103
+ - lib/generators/translate-pages.rb
88
104
  - lib/jekyll-uj-powertools.rb
89
105
  - lib/jekyll-uj-powertools/version.rb
90
106
  - spec/jekyll-uj-powertools_spec.rb