jekyll-chatgpt-translate 0.0.16 → 0.0.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +18 -5
- data/features/cli.feature +5 -4
- data/features/gem_package.feature +1 -1
- data/features/step_definitions/steps.rb +16 -20
- data/jekyll-chatgpt-translate.gemspec +1 -1
- data/lib/jekyll-chatgpt-translate/generator.rb +37 -35
- data/lib/jekyll-chatgpt-translate/plain.rb +2 -0
- data/lib/jekyll-chatgpt-translate/version.rb +1 -1
- data/test/test_generator.rb +47 -25
- data/test/test_plain.rb +4 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f593e5927f89dc9fe25dcbe28cc61ed938b3fdc42aee742d9ec31ebc86417fe
|
4
|
+
data.tar.gz: c5486a9adaf6e838bd016ea01175169786dbdd1807601cf922b0acc065c0a337
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 794664182ebba232506ab97b026d02a7060ee369fb16756308d43362df24aed862703836fb0af142c07240fd1eeeeeb9305e43804ed5b34300ce2dff1c30a03c
|
7
|
+
data.tar.gz: f1db90b466e6ea441519ee35351a2bf62b9182ed772ab020b5cd4692fc8932a22115fb994089b1cb70724c92a4344865810312b7a58fe1fe666ba0e5c308aaeb
|
data/README.md
CHANGED
@@ -7,7 +7,7 @@ If you have a [Jekyll](https://jekyllrb.com/) static site, this plugin may help
|
|
7
7
|
translate its pages to another language, through [ChatGPT](https://chat.openai.com/). See how it
|
8
8
|
works for [my blog](https://github.com/yegor256/ru.yegor256.com),
|
9
9
|
for example [this page](https://ru.yegor256.com/2023-08-13-dictators.html) is translated to
|
10
|
-
[English]().
|
10
|
+
[English](https://ru.yegor256.com/english/2023-08-13-dictators.html).
|
11
11
|
|
12
12
|
Install it first:
|
13
13
|
|
@@ -18,13 +18,16 @@ gem install jekyll-chatgpt-translate
|
|
18
18
|
Then, add this to `_config.yml`:
|
19
19
|
|
20
20
|
```yaml
|
21
|
+
plugins:
|
22
|
+
- ... your other plugins here ...
|
23
|
+
- jekyll-chatgpt-translate
|
21
24
|
chatgpt-translate:
|
22
25
|
model: gpt-3.5-turbo
|
23
26
|
source: en
|
24
27
|
layout: translated
|
25
28
|
targets:
|
26
29
|
-
|
27
|
-
language:
|
30
|
+
language: zh
|
28
31
|
permalink: :year-:month-:day-:slug-chinese.html
|
29
32
|
layout: chinese-translated
|
30
33
|
-
|
@@ -32,7 +35,7 @@ chatgpt-translate:
|
|
32
35
|
permalink: :year-:month-:day-:title-french.html
|
33
36
|
```
|
34
37
|
|
35
|
-
Here, the source language is English (`en`), the target one is Chinese (`
|
38
|
+
Here, the source language is English (`en`), the target one is Chinese (`zh`),
|
36
39
|
the layout is `_layout/translated.html` (you must have this file).
|
37
40
|
|
38
41
|
OpenAI API KEY must be set in `OPENAI_API_KEY` environment variable, otherwise
|
@@ -40,10 +43,15 @@ the plugin will not do any translation and won't generate translated pages.
|
|
40
43
|
You can get your key [here](https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key).
|
41
44
|
|
42
45
|
Inside the original page you can use `{{ page.translated-XX-url }}` in order to render the URL
|
43
|
-
of the translated page, where `XX` is the ISO-
|
46
|
+
of the translated page, where `XX` is the [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
|
47
|
+
code of the target language.
|
44
48
|
Inside the translated page you can use `{{ page.translated-original-url }}` in order
|
45
|
-
to get the URL of the page that was translated.
|
49
|
+
to get the URL of the page that was translated.
|
50
|
+
|
51
|
+
You can also use `{{ page.chatgpt-model }}`
|
46
52
|
inside both the original page and the translated one, to refer to the model of ChatGPT.
|
53
|
+
The presence of this attribute in the `{{ page }}` means that the
|
54
|
+
page was translated or the translated HTML was downloaded and placed into the `_site` directory.
|
47
55
|
|
48
56
|
## Options
|
49
57
|
|
@@ -56,6 +64,11 @@ Full list of options available to specify in `_config.yml`:
|
|
56
64
|
|
57
65
|
* `source` (optional) — is the ISO-839-1 code of the source language.
|
58
66
|
|
67
|
+
* `no_download` (optional) — if this attribute is present, the plugin won't try
|
68
|
+
to find HTML versions of translated pages in the Internet and won't try to
|
69
|
+
download them and place into the `_site` directory. Thus, your entire site
|
70
|
+
will have to be re-translated on every build (might be very ineffective if the site is big!)
|
71
|
+
|
59
72
|
* `layout` (optional) — is name of the file in `_layouts` directory, without the extension.
|
60
73
|
This layout will be specified for the pages generated by this plugin.
|
61
74
|
|
data/features/cli.feature
CHANGED
@@ -13,7 +13,7 @@ Feature: Simple site building
|
|
13
13
|
layout: translated
|
14
14
|
targets:
|
15
15
|
-
|
16
|
-
language:
|
16
|
+
language: zh
|
17
17
|
permalink: :year-:month-:day-:slug-chinese.html
|
18
18
|
layout: chinese-translated
|
19
19
|
-
|
@@ -22,7 +22,7 @@ Feature: Simple site building
|
|
22
22
|
"""
|
23
23
|
And I have a "_layouts/default.html" file with content:
|
24
24
|
"""
|
25
|
-
The Chinese: {{ page.translated-
|
25
|
+
The Chinese: {{ page.translated-zh-url }}
|
26
26
|
The French: {{ page.translated-fr-url }}
|
27
27
|
{{ content }}
|
28
28
|
"""
|
@@ -40,8 +40,9 @@ Feature: Simple site building
|
|
40
40
|
Hello, world!
|
41
41
|
"""
|
42
42
|
Then I build Jekyll site
|
43
|
-
And File "_chatgpt-translated/
|
44
|
-
And File "_chatgpt-translated/
|
43
|
+
And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" exists
|
44
|
+
And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" contains "/2023-01-01-hello-chinese.html"
|
45
|
+
And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" contains "translated-language: \"zh\""
|
45
46
|
And File "_site/2023/01/01/hello.html" exists
|
46
47
|
And File "_site/2023/01/01/hello.html" contains "The Chinese: /2023-01-01-hello-chinese.html"
|
47
48
|
And File "_site/2023-01-01-hello-chinese.html" exists
|
@@ -40,39 +40,35 @@ Given(/^I have a "([^"]*)" file with content:$/) do |file, text|
|
|
40
40
|
File.write(file, text.gsub('\\xFF', 0xFF.chr))
|
41
41
|
end
|
42
42
|
|
43
|
-
When(
|
43
|
+
When('I build Jekyll site') do
|
44
44
|
@stdout = `jekyll build`
|
45
45
|
@exitstatus = $CHILD_STATUS.exitstatus
|
46
46
|
end
|
47
47
|
|
48
|
-
Then(
|
49
|
-
raise "STDOUT doesn't contain '#{
|
48
|
+
Then('Stdout contains {string}') do |string|
|
49
|
+
raise "STDOUT doesn't contain '#{string}':\n#{@stdout}" unless @stdout.include?(string)
|
50
50
|
end
|
51
51
|
|
52
|
-
Then(
|
53
|
-
raise "The file \"#{
|
52
|
+
Then('File {string} exists') do |string|
|
53
|
+
raise "The file \"#{string}\" is absent:\n#{`tree -s`}" unless File.exist?(string)
|
54
54
|
end
|
55
55
|
|
56
|
-
Then(
|
57
|
-
raise "The file \"#{
|
58
|
-
content = File.read(
|
59
|
-
raise "The file \"#{
|
56
|
+
Then('File {string} contains {string}') do |string, string2|
|
57
|
+
raise "The file \"#{string}\" is absent" unless File.exist?(string)
|
58
|
+
content = File.read(string)
|
59
|
+
raise "The file \"#{string}\" doesn't contain \"#{string2}\":\n#{content}" unless content.include?(string2)
|
60
60
|
end
|
61
61
|
|
62
|
-
Then(
|
63
|
-
raise "STDOUT is not empty:\n#{@stdout}" unless @stdout == ''
|
64
|
-
end
|
65
|
-
|
66
|
-
Then(/^Exit code is zero$/) do
|
62
|
+
Then('Exit code is zero') do
|
67
63
|
raise "Non-zero exit #{@exitstatus}:\n#{@stdout}" unless @exitstatus.zero?
|
68
64
|
end
|
69
65
|
|
70
|
-
Then(
|
66
|
+
Then('Exit code is not zero') do
|
71
67
|
raise 'Zero exit code' if @exitstatus.zero?
|
72
68
|
end
|
73
69
|
|
74
|
-
When(
|
75
|
-
@stdout = `#{
|
70
|
+
When('I run bash with {string}') do |string|
|
71
|
+
@stdout = `#{string}`
|
76
72
|
@exitstatus = $CHILD_STATUS.exitstatus
|
77
73
|
end
|
78
74
|
|
@@ -81,14 +77,14 @@ When(/^I run bash with:$/) do |text|
|
|
81
77
|
@exitstatus = $CHILD_STATUS.exitstatus
|
82
78
|
end
|
83
79
|
|
84
|
-
When(
|
80
|
+
When('I copy this gem into temp dir') do
|
85
81
|
FileUtils.copy_entry(@cwd, File.join(@dir, 'jekyll-chatgpt-translate'))
|
86
82
|
end
|
87
83
|
|
88
|
-
Given(
|
84
|
+
Given('It is Unix') do
|
89
85
|
pending if Gem.win_platform?
|
90
86
|
end
|
91
87
|
|
92
|
-
Given(
|
88
|
+
Given('It is Windows') do
|
93
89
|
pending unless Gem.win_platform?
|
94
90
|
end
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
|
28
28
|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
29
29
|
s.required_ruby_version = '>= 2.6'
|
30
30
|
s.name = 'jekyll-chatgpt-translate'
|
31
|
-
s.version = '0.0.
|
31
|
+
s.version = '0.0.18'
|
32
32
|
s.license = 'MIT'
|
33
33
|
s.summary = 'Translate Jekyll Pages Through ChatGPT'
|
34
34
|
s.description = [
|
@@ -54,7 +54,8 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
54
54
|
version = config['version'] || GptTranslate::VERSION
|
55
55
|
threshold = config['threshold'] || 1024
|
56
56
|
start = Time.now
|
57
|
-
|
57
|
+
translated = 0
|
58
|
+
copied = 0
|
58
59
|
model = config['model'] || 'gpt-3.5-turbo'
|
59
60
|
marker = "Translated by ChatGPT #{model}/#{version}"
|
60
61
|
site.posts.docs.shuffle.each do |doc|
|
@@ -63,48 +64,49 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
63
64
|
link = GptTranslate::Permalink.new(doc, target['permalink']).to_path
|
64
65
|
lang = target['language']
|
65
66
|
raise 'Language must be defined for each target' if target.nil?
|
66
|
-
if total >= threshold
|
67
|
-
Jekyll.logger.info("Already generated #{total} pages, that's enough for today")
|
68
|
-
break
|
69
|
-
end
|
70
67
|
path = File.join(home, lang, doc.basename.gsub(/\.md$/, "-#{lang}.md"))
|
71
68
|
FileUtils.mkdir_p(File.dirname(path))
|
72
69
|
File.write(path, '') # in order to surpress warnings in Page ctor
|
73
70
|
dest = Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path)).destination(site.dest)
|
71
|
+
if config['no_download'].nil? && GptTranslate::Ping.new(site, link).found?(dest, version)
|
72
|
+
copied += 1
|
73
|
+
elsif translated >= threshold
|
74
|
+
next
|
75
|
+
else
|
76
|
+
gpt = GptTranslate::ChatGPT.new(
|
77
|
+
key,
|
78
|
+
model,
|
79
|
+
config['source'] || 'en',
|
80
|
+
lang
|
81
|
+
)
|
82
|
+
foreign = gpt.translate(plain)
|
83
|
+
File.write(
|
84
|
+
path,
|
85
|
+
[
|
86
|
+
'---',
|
87
|
+
"layout: #{target['layout'] || layout}",
|
88
|
+
"title: #{doc['title'].to_json}",
|
89
|
+
"description: #{doc['description'].to_json}",
|
90
|
+
"permalink: #{link.to_json}",
|
91
|
+
"translated-original-url: #{doc.url.to_json}",
|
92
|
+
"translated-language: #{lang.to_json}",
|
93
|
+
"chatgpt-model: #{model.to_json}",
|
94
|
+
'---',
|
95
|
+
'',
|
96
|
+
foreign,
|
97
|
+
'',
|
98
|
+
"#{marker} on #{Time.now.strftime('%d/%m/%Y %H:%M')}\n{: .jekyll-chatgpt-translate}"
|
99
|
+
].join("\n")
|
100
|
+
)
|
101
|
+
site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
|
102
|
+
translated += 1
|
103
|
+
Jekyll.logger.info("Translated via ChatGPT: #{path} (#{File.size(path)} bytes)")
|
104
|
+
end
|
74
105
|
doc.data["translated-#{lang}-url"] = link
|
75
106
|
doc.data['chatgpt-model'] = model
|
76
|
-
next if GptTranslate::Ping.new(site, link).found?(dest, version)
|
77
|
-
gpt = GptTranslate::ChatGPT.new(
|
78
|
-
key,
|
79
|
-
model,
|
80
|
-
config['source'] || 'en',
|
81
|
-
lang
|
82
|
-
)
|
83
|
-
translated = gpt.translate(plain)
|
84
|
-
File.write(
|
85
|
-
path,
|
86
|
-
[
|
87
|
-
'---',
|
88
|
-
"layout: #{target['layout'] || layout}",
|
89
|
-
"title: #{doc['title'].to_json}",
|
90
|
-
"description: #{doc['description'].to_json}",
|
91
|
-
"permalink: #{link.to_json}",
|
92
|
-
"translated-original-url: #{doc.url.to_json}",
|
93
|
-
"chatgpt-model: #{model.to_json}",
|
94
|
-
'---',
|
95
|
-
'',
|
96
|
-
translated,
|
97
|
-
'',
|
98
|
-
"#{marker} on #{Time.now.strftime('%d/%m/%Y %H:%M')}\n{: .jekyll-chatgpt-translate}"
|
99
|
-
].join("\n")
|
100
|
-
)
|
101
|
-
site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
|
102
|
-
total += 1
|
103
|
-
Jekyll.logger.info("Translated via ChatGPT: #{path}")
|
104
107
|
end
|
105
|
-
break if total >= threshold
|
106
108
|
end
|
107
|
-
Jekyll.logger.info("#{
|
109
|
+
Jekyll.logger.info("#{translated} pages translated and #{copied} pages copied in #{(Time.now - start).round(2)}s")
|
108
110
|
end
|
109
111
|
|
110
112
|
private
|
data/test/test_generator.rb
CHANGED
@@ -33,19 +33,16 @@ require_relative '../lib/jekyll-chatgpt-translate/generator'
|
|
33
33
|
# License:: MIT
|
34
34
|
class GptTranslate::GeneratorTest < Minitest::Test
|
35
35
|
class FakeSite
|
36
|
-
attr_reader :config
|
36
|
+
attr_reader :config, :pages
|
37
37
|
|
38
|
-
def initialize(config,
|
38
|
+
def initialize(config, docs)
|
39
39
|
@config = config
|
40
|
-
@
|
40
|
+
@docs = docs
|
41
|
+
@pages = []
|
41
42
|
end
|
42
43
|
|
43
44
|
def posts
|
44
|
-
FakePosts.new(@
|
45
|
-
end
|
46
|
-
|
47
|
-
def pages
|
48
|
-
[]
|
45
|
+
FakePosts.new(@docs)
|
49
46
|
end
|
50
47
|
|
51
48
|
def permalink_style
|
@@ -65,7 +62,7 @@ class GptTranslate::GeneratorTest < Minitest::Test
|
|
65
62
|
end
|
66
63
|
|
67
64
|
def dest
|
68
|
-
|
65
|
+
File.dirname(@docs[0])
|
69
66
|
end
|
70
67
|
|
71
68
|
def in_theme_dir(base, _foo = nil, _bar = nil)
|
@@ -78,28 +75,23 @@ class GptTranslate::GeneratorTest < Minitest::Test
|
|
78
75
|
end
|
79
76
|
|
80
77
|
class FakeDocument
|
78
|
+
attr_reader :data
|
79
|
+
|
81
80
|
def initialize(path)
|
82
81
|
@path = path
|
82
|
+
@data = { 'date' => Time.now, 'title' => 'Hello!' }
|
83
83
|
end
|
84
84
|
|
85
85
|
def content
|
86
86
|
'Hello, world!'
|
87
87
|
end
|
88
88
|
|
89
|
-
def
|
90
|
-
|
89
|
+
def []=(key, value)
|
90
|
+
@data[key] = value
|
91
91
|
end
|
92
92
|
|
93
|
-
def []=(key, value); end
|
94
|
-
|
95
93
|
def [](key)
|
96
|
-
|
97
|
-
Time.now
|
98
|
-
elsif key == 'title'
|
99
|
-
'Hello!'
|
100
|
-
else
|
101
|
-
''
|
102
|
-
end
|
94
|
+
@data[key] || ''
|
103
95
|
end
|
104
96
|
|
105
97
|
def relative_path
|
@@ -118,12 +110,12 @@ class GptTranslate::GeneratorTest < Minitest::Test
|
|
118
110
|
class FakePosts
|
119
111
|
attr_reader :config
|
120
112
|
|
121
|
-
def initialize(
|
122
|
-
@
|
113
|
+
def initialize(docs)
|
114
|
+
@docs = docs
|
123
115
|
end
|
124
116
|
|
125
117
|
def docs
|
126
|
-
|
118
|
+
@docs.map { |d| FakeDocument.new(d) }
|
127
119
|
end
|
128
120
|
end
|
129
121
|
|
@@ -137,18 +129,48 @@ class GptTranslate::GeneratorTest < Minitest::Test
|
|
137
129
|
'chatgpt-translate' => {
|
138
130
|
'targets' => [
|
139
131
|
{
|
140
|
-
'language' => '
|
132
|
+
'language' => 'zh',
|
141
133
|
'layout' => 'chinese',
|
142
134
|
'permalink' => ':slug.html'
|
143
135
|
}
|
144
136
|
]
|
145
137
|
}
|
146
138
|
},
|
147
|
-
|
139
|
+
[post]
|
140
|
+
)
|
141
|
+
gen = GptTranslate::Generator.new
|
142
|
+
stub_request(:get, 'https://www.yegor256.com/.html').to_return(body: '')
|
143
|
+
gen.generate(site)
|
144
|
+
assert_equal(1, site.pages.count)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def test_threshold_stops
|
149
|
+
Dir.mktmpdir do |home|
|
150
|
+
post = File.join(home, '2023-01-01-hello.md')
|
151
|
+
File.write(post, "---\ntitle: Hello\n---\n\nHello, world!")
|
152
|
+
site = FakeSite.new(
|
153
|
+
{
|
154
|
+
'chatgpt-translate' => {
|
155
|
+
'threshold' => 1,
|
156
|
+
'targets' => [
|
157
|
+
{
|
158
|
+
'language' => 'zh',
|
159
|
+
'permalink' => ':slug.html'
|
160
|
+
},
|
161
|
+
{
|
162
|
+
'language' => 'fr',
|
163
|
+
'permalink' => ':year/:slug.html'
|
164
|
+
}
|
165
|
+
]
|
166
|
+
}
|
167
|
+
},
|
168
|
+
[post, post]
|
148
169
|
)
|
149
170
|
gen = GptTranslate::Generator.new
|
150
171
|
stub_request(:get, 'https://www.yegor256.com/.html').to_return(body: '')
|
151
172
|
gen.generate(site)
|
173
|
+
assert_equal(1, site.pages.count)
|
152
174
|
end
|
153
175
|
end
|
154
176
|
end
|
data/test/test_plain.rb
CHANGED
@@ -96,6 +96,10 @@ class GptTranslate::PlainTest < Minitest::Test
|
|
96
96
|
"```\nHello\n```",
|
97
97
|
GptTranslate::Plain.new("```\nHello\n```").to_s
|
98
98
|
)
|
99
|
+
assert_equal(
|
100
|
+
"```\nprint('hi!')\n```",
|
101
|
+
GptTranslate::Plain.new("```java\nprint('hi!')\n```").to_s
|
102
|
+
)
|
99
103
|
end
|
100
104
|
|
101
105
|
def test_liquid_tags
|