jekyll-uj-powertools 1.2.6 → 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 +4 -4
- data/Gemfile +1 -0
- data/README.md +7 -0
- data/jekyll-uj-powertools.gemspec +3 -0
- data/lib/generators/inject-data.rb +41 -0
- data/lib/generators/translate-pages.rb +209 -0
- data/lib/jekyll-uj-powertools/version.rb +1 -1
- data/lib/jekyll-uj-powertools.rb +4 -52
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2db2b27f78f8588283c589c8d6c06e342ba6a11a1b1b55669ea81b722b5fe64b
|
4
|
+
data.tar.gz: 47b62725a184ea99d3f25daec3620e57539dbd8fb42159bdd81bb1f68176e5f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 81e5f82be027417b008c9a6d3cbc2bb86bc5f992a60447fb42dc94011da6abb4cb5374756bca80141c36cad263e92e16c2a7ee18a94c4c5122173e9c1464a5ae
|
7
|
+
data.tar.gz: 6a3ea9d448ba3a543cf70623e6eb67ab72dc25eecbd761d9959e66cf6a9db92342271307713a95ea6bb268b761e2f23e0565d107c3c4fa8f6528543b3118f036
|
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
|
@@ -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
|
data/lib/jekyll-uj-powertools.rb
CHANGED
@@ -62,57 +62,9 @@ module Jekyll
|
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
#
|
66
|
-
|
67
|
-
|
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,5 +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']['
|
76
|
+
site.config['uj']['cache_breaker'] = Jekyll::UJPowertools.cache_timestamp
|
125
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.
|
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-
|
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
|