actiontext_translate 0.1.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 +7 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +333 -0
- data/lib/actiontext_translate/configuration.rb +31 -0
- data/lib/actiontext_translate/html_processor.rb +159 -0
- data/lib/actiontext_translate/translator.rb +61 -0
- data/lib/actiontext_translate/translators/base.rb +62 -0
- data/lib/actiontext_translate/translators/chatgpt.rb +128 -0
- data/lib/actiontext_translate/version.rb +5 -0
- data/lib/actiontext_translate.rb +42 -0
- metadata +143 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0de2c393d8b1ce42d10f5f3de9da605d055f3eacfa71a0a38338d3a8ef588b40
|
|
4
|
+
data.tar.gz: bd701532905304c0e8d23fd93e77a42e77781d0f0225d19644807d1793bff420
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2a32f00eeb6517cda823e6facb7cfcc8e21672b562ad79dc58bed7828501263e2b680bddf7efc77dde0652464e3bf2130d8eca4c82a9dace71fbe6be94c435bc
|
|
7
|
+
data.tar.gz: 8fda68dee1d2f4884f718f10f0a0ca24f8003b2da9375706dd0ed9d7cafd8ecfcd37456d9c5dc580d27ce10cb17c41ca104e993abecfcdd2f55a66a4868f76f5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2024-12-18
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release
|
|
12
|
+
- ChatGPT (OpenAI) translator with HTML preservation
|
|
13
|
+
- Link protection to prevent URL corruption during translation
|
|
14
|
+
- ActionText rich text support
|
|
15
|
+
- HTML tag preservation
|
|
16
|
+
- Extensible translator interface for multiple providers
|
|
17
|
+
- Configuration system
|
|
18
|
+
- Comprehensive test suite
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mykyta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# ActionText Translate
|
|
2
|
+
|
|
3
|
+
A Ruby gem for translating ActionText rich text content using ChatGPT (OpenAI) while perfectly preserving HTML structure, tags, and links.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **High-Quality Translation**: Uses ChatGPT for natural, contextual translations
|
|
8
|
+
- **HTML Preservation**: Maintains all HTML tags, attributes, and structure
|
|
9
|
+
- **Smart Link Protection**: Prevents URL corruption during translation
|
|
10
|
+
- **ActionText Support**: Seamless integration with Rails ActionText
|
|
11
|
+
- **Extensible Architecture**: Easy to add additional translation providers
|
|
12
|
+
- **Well Tested**: Comprehensive test suite with 33+ test cases
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add this line to your application's Gemfile:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'actiontext_translate'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
And then execute:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install it yourself as:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install actiontext_translate
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
### Basic Configuration
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
ActiontextTranslate.configure do |config|
|
|
40
|
+
config.provider = :chatgpt
|
|
41
|
+
config.api_key = ENV['OPENAI_API_KEY']
|
|
42
|
+
config.model = 'gpt-4o-mini' # Fast and cost-effective
|
|
43
|
+
config.temperature = 0.3 # Lower temperature for consistent translations
|
|
44
|
+
config.timeout = 30 # API timeout in seconds
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Rails Configuration
|
|
49
|
+
|
|
50
|
+
Create an initializer `config/initializers/actiontext_translate.rb`:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# config/initializers/actiontext_translate.rb
|
|
54
|
+
ActiontextTranslate.configure do |config|
|
|
55
|
+
config.provider = :chatgpt
|
|
56
|
+
config.api_key = ENV['OPENAI_API_KEY']
|
|
57
|
+
config.model = 'gpt-4o-mini'
|
|
58
|
+
config.temperature = 0.3
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Get Your OpenAI API Key
|
|
63
|
+
|
|
64
|
+
1. Visit https://platform.openai.com/api-keys
|
|
65
|
+
2. Create a new API key
|
|
66
|
+
3. Add it to your environment variables
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
### Simple Text Translation
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
# Basic translation
|
|
74
|
+
ActiontextTranslate.translate("Привіт Світ", from: "uk", to: "en")
|
|
75
|
+
# => "Hello World"
|
|
76
|
+
|
|
77
|
+
# With custom translator instance
|
|
78
|
+
translator = ActiontextTranslate::Translator.new
|
|
79
|
+
translator.translate("Bonjour le monde", from: "fr", to: "en")
|
|
80
|
+
# => "Hello world"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### HTML Translation
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
html = '<p>Check <a href="https://example.com">this link</a> for more info.</p>'
|
|
87
|
+
|
|
88
|
+
result = ActiontextTranslate.translate_html(html, from: "uk", to: "en")
|
|
89
|
+
# => '<p>Check <a href="https://example.com">this link</a> for more info.</p>'
|
|
90
|
+
# All HTML structure and link attributes are perfectly preserved!
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### ActionText Translation
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# Translate ActionText rich text content
|
|
97
|
+
article = Article.find(1)
|
|
98
|
+
translated_html = ActiontextTranslate.translate_action_text(
|
|
99
|
+
article.body,
|
|
100
|
+
from: "uk",
|
|
101
|
+
to: "en"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Set the translated content
|
|
105
|
+
article.body_en = translated_html
|
|
106
|
+
article.save
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Rails Integration
|
|
110
|
+
|
|
111
|
+
### Background Job Example
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# app/jobs/translate_content_job.rb
|
|
115
|
+
class TranslateContentJob < ApplicationJob
|
|
116
|
+
queue_as :default
|
|
117
|
+
|
|
118
|
+
def perform(model_class, model_id, from_locale, to_locale)
|
|
119
|
+
model = model_class.constantize.find_by(id: model_id)
|
|
120
|
+
return unless model
|
|
121
|
+
|
|
122
|
+
translator = ActiontextTranslate::Translator.new
|
|
123
|
+
|
|
124
|
+
# Translate ActionText body field
|
|
125
|
+
if model.respond_to?(:body) && model.body.present?
|
|
126
|
+
translated_html = translator.translate_action_text(
|
|
127
|
+
model.body,
|
|
128
|
+
from: from_locale,
|
|
129
|
+
to: to_locale
|
|
130
|
+
)
|
|
131
|
+
model.public_send("body_#{to_locale}=", translated_html)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Translate regular text fields
|
|
135
|
+
if model.respond_to?(:title) && model.title.present?
|
|
136
|
+
translated_title = translator.translate(
|
|
137
|
+
model.title,
|
|
138
|
+
from: from_locale,
|
|
139
|
+
to: to_locale
|
|
140
|
+
)
|
|
141
|
+
model.public_send("title_#{to_locale}=", translated_title)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
model.save!
|
|
145
|
+
rescue ActiontextTranslate::Translators::TranslationError => e
|
|
146
|
+
Rails.logger.error("Translation failed: #{e.message}")
|
|
147
|
+
raise
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Rake Task Example
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
# lib/tasks/translations.rake
|
|
156
|
+
namespace :translations do
|
|
157
|
+
desc 'Translate content from Ukrainian to English'
|
|
158
|
+
task translate_articles: :environment do
|
|
159
|
+
Article.where(translated: false).find_each do |article|
|
|
160
|
+
puts "Translating Article ##{article.id}..."
|
|
161
|
+
TranslateContentJob.perform_later('Article', article.id, 'uk', 'en')
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Model Integration
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# app/models/article.rb
|
|
171
|
+
class Article < ApplicationRecord
|
|
172
|
+
has_rich_text :body
|
|
173
|
+
has_rich_text :body_en
|
|
174
|
+
|
|
175
|
+
after_create :enqueue_translation
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def enqueue_translation
|
|
180
|
+
TranslateContentJob.perform_later(self.class.name, id, 'uk', 'en')
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Advanced Features
|
|
186
|
+
|
|
187
|
+
### Link Protection
|
|
188
|
+
|
|
189
|
+
The gem automatically protects links during translation:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
html = '<p>Visit <a href="https://example.com">our website</a> or <a href="https://docs.com">https://docs.com</a></p>'
|
|
193
|
+
|
|
194
|
+
result = ActiontextTranslate.translate_html(html, from: "uk", to: "en")
|
|
195
|
+
# - Links with descriptive text: text gets translated, URL preserved
|
|
196
|
+
# - Links where text = URL: kept unchanged
|
|
197
|
+
# - All attributes (href, rel, target, class) are preserved
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### HTML Structure Preservation
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
html = <<~HTML
|
|
204
|
+
<div class="content">
|
|
205
|
+
<h1>Title</h1>
|
|
206
|
+
<p>Paragraph with <strong>bold</strong> and <em>italic</em> text.</p>
|
|
207
|
+
<ul>
|
|
208
|
+
<li>Item 1</li>
|
|
209
|
+
<li>Item 2</li>
|
|
210
|
+
</ul>
|
|
211
|
+
</div>
|
|
212
|
+
HTML
|
|
213
|
+
|
|
214
|
+
result = ActiontextTranslate.translate_html(html, from: "uk", to: "en")
|
|
215
|
+
# All tags, classes, and structure are perfectly preserved
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Error Handling
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
begin
|
|
222
|
+
translator = ActiontextTranslate::Translator.new
|
|
223
|
+
result = translator.translate("Text", from: "uk", to: "en")
|
|
224
|
+
rescue ActiontextTranslate::Translators::TranslationError => e
|
|
225
|
+
Rails.logger.error("Translation failed: #{e.message}")
|
|
226
|
+
# Handle error (retry, fallback, notify, etc.)
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Supported Languages
|
|
231
|
+
|
|
232
|
+
The gem supports **all languages** supported by OpenAI's GPT models. Simply use the appropriate language code:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
# Ukrainian to English
|
|
236
|
+
ActiontextTranslate.translate("Привіт", from: "uk", to: "en")
|
|
237
|
+
|
|
238
|
+
# French to German
|
|
239
|
+
ActiontextTranslate.translate("Bonjour", from: "fr", to: "de")
|
|
240
|
+
|
|
241
|
+
# Any language combination
|
|
242
|
+
ActiontextTranslate.translate(text, from: "source_lang", to: "target_lang")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Cost Considerations
|
|
246
|
+
|
|
247
|
+
Using ChatGPT (GPT-4o-mini) is very cost-effective:
|
|
248
|
+
|
|
249
|
+
- **Input**: ~$0.15 per 1M tokens
|
|
250
|
+
- **Output**: ~$0.60 per 1M tokens
|
|
251
|
+
- **Average translation**: A typical blog post (1000 words) costs < $0.01
|
|
252
|
+
|
|
253
|
+
Example monthly costs for 1000 translations:
|
|
254
|
+
- ~$5-10/month for typical usage
|
|
255
|
+
- Pay only for what you use
|
|
256
|
+
|
|
257
|
+
## Configuration Options
|
|
258
|
+
|
|
259
|
+
| Option | Default | Description |
|
|
260
|
+
|--------|---------|-------------|
|
|
261
|
+
| `provider` | `:chatgpt` | Translation provider |
|
|
262
|
+
| `api_key` | `nil` | OpenAI API key (required) |
|
|
263
|
+
| `model` | `'gpt-4o-mini'` | OpenAI model to use |
|
|
264
|
+
| `temperature` | `0.3` | Response randomness (0.0-2.0) |
|
|
265
|
+
| `timeout` | `30` | API request timeout in seconds |
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
After checking out the repo, run:
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
bundle install
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Run tests:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
bundle exec rspec
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Check code coverage (requires 90% minimum):
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
bundle exec rspec
|
|
285
|
+
open coverage/index.html
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Run RuboCop:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
bundle exec rubocop
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Code Coverage
|
|
295
|
+
|
|
296
|
+
The gem maintains **96%+ code coverage** with comprehensive tests:
|
|
297
|
+
- 48 test cases covering all major features
|
|
298
|
+
- Integration tests with realistic Trix/ActionText content
|
|
299
|
+
- Edge case coverage for HTML preservation
|
|
300
|
+
- Minimum coverage threshold: 90%
|
|
301
|
+
|
|
302
|
+
View coverage report after running tests:
|
|
303
|
+
```bash
|
|
304
|
+
open coverage/index.html
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Contributing
|
|
308
|
+
|
|
309
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mykbren/actiontext_translate.
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
314
|
+
|
|
315
|
+
## Roadmap
|
|
316
|
+
|
|
317
|
+
Future enhancements planned:
|
|
318
|
+
|
|
319
|
+
- [ ] Additional translation providers (Google Translate, DeepL, Azure)
|
|
320
|
+
- [ ] Batch translation support for improved performance
|
|
321
|
+
- [ ] Translation memory/caching to reduce costs
|
|
322
|
+
- [ ] Automatic language detection
|
|
323
|
+
- [ ] Rails generators for easy setup
|
|
324
|
+
- [ ] Translation quality scoring
|
|
325
|
+
- [ ] Custom prompt templates
|
|
326
|
+
- [ ] Streaming support for large documents
|
|
327
|
+
|
|
328
|
+
## Support
|
|
329
|
+
|
|
330
|
+
For questions, issues, or feature requests:
|
|
331
|
+
- Open an issue on GitHub
|
|
332
|
+
- Review the test suite for examples
|
|
333
|
+
- Check the documentation
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiontextTranslate
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :provider, :api_key, :timeout, :model, :temperature
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@provider = :chatgpt
|
|
9
|
+
@api_key = nil
|
|
10
|
+
@timeout = 30
|
|
11
|
+
@model = 'gpt-4o-mini' # Fast and cost-effective for translations
|
|
12
|
+
@temperature = 0.3 # Lower temperature for more consistent translations
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
attr_writer :configuration
|
|
18
|
+
|
|
19
|
+
def configuration
|
|
20
|
+
@configuration ||= Configuration.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def configure
|
|
24
|
+
yield(configuration)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset_configuration!
|
|
28
|
+
@configuration = Configuration.new
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'nokogiri'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'cgi'
|
|
6
|
+
|
|
7
|
+
module ActiontextTranslate
|
|
8
|
+
# Handles HTML processing for translation, including link protection
|
|
9
|
+
class HtmlProcessor
|
|
10
|
+
# Protect links by replacing them with simple placeholders
|
|
11
|
+
# @param html [String] The HTML content to process
|
|
12
|
+
# @return [Array<String, Hash>] Protected HTML and link map
|
|
13
|
+
def self.protect_links(html)
|
|
14
|
+
doc = Nokogiri::HTML.fragment(html)
|
|
15
|
+
link_map = {}
|
|
16
|
+
counter = 0
|
|
17
|
+
|
|
18
|
+
doc.css('a').each do |link|
|
|
19
|
+
counter += 1
|
|
20
|
+
start_placeholder = "XLINKSTARTX#{counter}X"
|
|
21
|
+
end_placeholder = "XLINKENDX#{counter}X"
|
|
22
|
+
|
|
23
|
+
text = link.text.strip
|
|
24
|
+
href = link['href'].to_s
|
|
25
|
+
|
|
26
|
+
# Determine if link is translatable
|
|
27
|
+
# Links are not translatable if:
|
|
28
|
+
# 1. Text exactly matches URL
|
|
29
|
+
# 2. Text contains any URL (to avoid translator breaking URLs)
|
|
30
|
+
uri_regex = URI::DEFAULT_PARSER.make_regexp(%w[http https])
|
|
31
|
+
translatable = !(text == href || text.match?(uri_regex))
|
|
32
|
+
|
|
33
|
+
link_map[start_placeholder] = {
|
|
34
|
+
end_placeholder: end_placeholder,
|
|
35
|
+
attributes: link.attributes.transform_values(&:value),
|
|
36
|
+
text: text,
|
|
37
|
+
translatable: translatable
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Replace link with placeholders
|
|
41
|
+
protected_text = translatable ? text : ''
|
|
42
|
+
link.replace("#{start_placeholder}#{protected_text}#{end_placeholder}")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
[doc.to_html, link_map]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Restore links after translation
|
|
49
|
+
# @param html [String] The translated HTML with placeholders
|
|
50
|
+
# @param link_map [Hash] The link map from protect_links
|
|
51
|
+
# @return [String] HTML with restored links
|
|
52
|
+
def self.restore_links(html, link_map)
|
|
53
|
+
html = html.dup
|
|
54
|
+
|
|
55
|
+
link_map.each do |start_placeholder, data|
|
|
56
|
+
end_placeholder = data[:end_placeholder]
|
|
57
|
+
|
|
58
|
+
# Extract the inner text
|
|
59
|
+
inner_text = html[/#{Regexp.escape(start_placeholder)}(.*?)#{Regexp.escape(end_placeholder)}/m, 1].to_s.strip
|
|
60
|
+
|
|
61
|
+
# Use original text for non-translatable links
|
|
62
|
+
inner_text = data[:text] unless data[:translatable]
|
|
63
|
+
|
|
64
|
+
# Rebuild <a> tag with exact attributes
|
|
65
|
+
attrs = data[:attributes].map { |k, v| %(#{k}="#{CGI.escapeHTML(v)}") }.join(' ')
|
|
66
|
+
link_html = "<a #{attrs}>#{inner_text}</a>"
|
|
67
|
+
|
|
68
|
+
html.gsub!(/#{Regexp.escape(start_placeholder)}.*?#{Regexp.escape(end_placeholder)}/m, link_html)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
html
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Protect ActionText attachments (images with captions) by replacing with placeholders
|
|
75
|
+
# @param html [String] The HTML content to process
|
|
76
|
+
# @return [Array<String, Hash>] Protected HTML and attachment map
|
|
77
|
+
def self.protect_attachments(html)
|
|
78
|
+
doc = Nokogiri::HTML.fragment(html)
|
|
79
|
+
attachment_map = {}
|
|
80
|
+
counter = 0
|
|
81
|
+
|
|
82
|
+
doc.css('action-text-attachment').each do |attachment|
|
|
83
|
+
counter += 1
|
|
84
|
+
start_placeholder = "XATTACHSTARTX#{counter}X"
|
|
85
|
+
end_placeholder = "XATTACHENDX#{counter}X"
|
|
86
|
+
|
|
87
|
+
# Extract all attributes
|
|
88
|
+
attributes = attachment.attributes.transform_values(&:value)
|
|
89
|
+
|
|
90
|
+
# Get inner HTML
|
|
91
|
+
inner_html = attachment.inner_html
|
|
92
|
+
|
|
93
|
+
# Check if there's translatable content (figcaption, img alt)
|
|
94
|
+
inner_doc = Nokogiri::HTML.fragment(inner_html)
|
|
95
|
+
figcaption = inner_doc.at_css('figcaption')
|
|
96
|
+
img = inner_doc.at_css('img')
|
|
97
|
+
|
|
98
|
+
caption_text = figcaption&.text&.strip
|
|
99
|
+
has_translatable_content = caption_text&.length.to_i.positive?
|
|
100
|
+
translatable_alt = img&.[]('alt')&.strip
|
|
101
|
+
|
|
102
|
+
# Keep alt text as-is in inner_html for translation
|
|
103
|
+
# We'll extract the translated alt text after translation
|
|
104
|
+
|
|
105
|
+
# Store attachment data
|
|
106
|
+
attachment_map[start_placeholder] = {
|
|
107
|
+
end_placeholder: end_placeholder,
|
|
108
|
+
tag_name: 'action-text-attachment',
|
|
109
|
+
attributes: attributes,
|
|
110
|
+
inner_html: inner_html,
|
|
111
|
+
translatable_alt: translatable_alt,
|
|
112
|
+
has_translatable_content: has_translatable_content
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# Replace attachment with placeholder + inner content (so captions get translated)
|
|
116
|
+
attachment.replace("#{start_placeholder}#{inner_html}#{end_placeholder}")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
[doc.to_html, attachment_map]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Restore ActionText attachments after translation
|
|
123
|
+
# @param html [String] The translated HTML with placeholders
|
|
124
|
+
# @param attachment_map [Hash] The attachment map from protect_attachments
|
|
125
|
+
# @return [String] HTML with restored attachments
|
|
126
|
+
def self.restore_attachments(html, attachment_map)
|
|
127
|
+
html = html.dup
|
|
128
|
+
|
|
129
|
+
attachment_map.each do |start_placeholder, data|
|
|
130
|
+
end_placeholder = data[:end_placeholder]
|
|
131
|
+
|
|
132
|
+
# Extract the translated inner HTML
|
|
133
|
+
inner_html = html[/#{Regexp.escape(start_placeholder)}(.*?)#{Regexp.escape(end_placeholder)}/m, 1].to_s
|
|
134
|
+
|
|
135
|
+
# Alt text was kept in the HTML and should have been translated naturally
|
|
136
|
+
# No additional processing needed for alt text
|
|
137
|
+
|
|
138
|
+
# Rebuild action-text-attachment tag
|
|
139
|
+
attrs = data[:attributes].map { |k, v| %(#{k}="#{CGI.escapeHTML(v)}") }.join(' ')
|
|
140
|
+
attachment_html = "<#{data[:tag_name]} #{attrs}>#{inner_html}</#{data[:tag_name]}>"
|
|
141
|
+
|
|
142
|
+
# Replace placeholder with full attachment
|
|
143
|
+
html.gsub!(/#{Regexp.escape(start_placeholder)}.*?#{Regexp.escape(end_placeholder)}/m, attachment_html)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
html
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Sanitize HTML by removing potentially problematic elements
|
|
150
|
+
# @param html [String] The HTML content
|
|
151
|
+
# @return [String] Sanitized HTML
|
|
152
|
+
def self.sanitize_for_translation(html)
|
|
153
|
+
# Remove script and style tags
|
|
154
|
+
doc = Nokogiri::HTML.fragment(html)
|
|
155
|
+
doc.css('script, style').remove
|
|
156
|
+
doc.to_html
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiontextTranslate
|
|
4
|
+
# Main translator class that delegates to provider-specific translators
|
|
5
|
+
class Translator
|
|
6
|
+
attr_reader :provider
|
|
7
|
+
|
|
8
|
+
def initialize(provider = nil)
|
|
9
|
+
@provider = create_provider(provider || ActiontextTranslate.configuration.provider)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Translate text
|
|
13
|
+
# @param text [String] Text to translate
|
|
14
|
+
# @param from [String] Source language code
|
|
15
|
+
# @param to [String] Target language code
|
|
16
|
+
# @param html_mode [Boolean] Whether to preserve HTML tags
|
|
17
|
+
# @return [String] Translated text
|
|
18
|
+
def translate(text, from:, to:, html_mode: false)
|
|
19
|
+
provider.translate(text, from: from, to: to, html_mode: html_mode)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Translate HTML content with full link and tag preservation
|
|
23
|
+
# @param html [String] HTML content to translate
|
|
24
|
+
# @param from [String] Source language code
|
|
25
|
+
# @param to [String] Target language code
|
|
26
|
+
# @return [String] Translated HTML
|
|
27
|
+
def translate_html(html, from:, to:)
|
|
28
|
+
provider.translate_html(html, from: from, to: to)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Translate ActionText rich text content
|
|
32
|
+
# @param rich_text [ActionText::RichText, String] Rich text to translate
|
|
33
|
+
# @param from [String] Source language code
|
|
34
|
+
# @param to [String] Target language code
|
|
35
|
+
# @return [String] Translated HTML suitable for ActionText
|
|
36
|
+
def translate_action_text(rich_text, from:, to:)
|
|
37
|
+
html = extract_html_from_rich_text(rich_text)
|
|
38
|
+
translate_html(html, from: from, to: to)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def create_provider(provider_name)
|
|
44
|
+
case provider_name.to_sym
|
|
45
|
+
when :chatgpt, :openai
|
|
46
|
+
require_relative 'translators/chatgpt'
|
|
47
|
+
Translators::Chatgpt.new
|
|
48
|
+
else
|
|
49
|
+
raise ArgumentError, "Unknown translation provider: #{provider_name}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_html_from_rich_text(rich_text)
|
|
54
|
+
if rich_text.respond_to?(:body)
|
|
55
|
+
rich_text.body.to_s
|
|
56
|
+
else
|
|
57
|
+
rich_text.to_s
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiontextTranslate
|
|
4
|
+
module Translators
|
|
5
|
+
# Base translator interface
|
|
6
|
+
class Base
|
|
7
|
+
attr_reader :config
|
|
8
|
+
|
|
9
|
+
def initialize(config = nil)
|
|
10
|
+
@config = config || ActiontextTranslate.configuration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Translate text from source language to target language
|
|
14
|
+
# @param text [String] The text to translate
|
|
15
|
+
# @param from [String] Source language code (e.g., 'uk', 'en')
|
|
16
|
+
# @param to [String] Target language code (e.g., 'en', 'uk')
|
|
17
|
+
# @param html_mode [Boolean] Whether to preserve HTML tags
|
|
18
|
+
# @return [String] Translated text
|
|
19
|
+
def translate(text, from:, to:, html_mode: false)
|
|
20
|
+
raise NotImplementedError, "#{self.class} must implement #translate"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Translate with HTML preservation using the HTML processor
|
|
24
|
+
# @param html [String] The HTML content to translate
|
|
25
|
+
# @param from [String] Source language code
|
|
26
|
+
# @param to [String] Target language code
|
|
27
|
+
# @return [String] Translated HTML with preserved structure
|
|
28
|
+
def translate_html(html, from:, to:)
|
|
29
|
+
return html if blank?(html)
|
|
30
|
+
|
|
31
|
+
# Protect ActionText attachments first (images with captions)
|
|
32
|
+
protected_html, attachment_map = HtmlProcessor.protect_attachments(html)
|
|
33
|
+
|
|
34
|
+
# Protect links (in the exposed inner HTML of attachments)
|
|
35
|
+
protected_html, link_map = HtmlProcessor.protect_links(protected_html)
|
|
36
|
+
|
|
37
|
+
# Translate
|
|
38
|
+
translated_html = translate(protected_html, from: from, to: to, html_mode: true)
|
|
39
|
+
|
|
40
|
+
# Restore in reverse order: links first, then attachments
|
|
41
|
+
translated_html = HtmlProcessor.restore_links(translated_html, link_map)
|
|
42
|
+
HtmlProcessor.restore_attachments(translated_html, attachment_map)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if value is blank (nil, empty, or whitespace)
|
|
46
|
+
# @param value [Object] Value to check
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def blank?(value)
|
|
49
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?) || (value.is_a?(String) && value.strip.empty?)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
protected
|
|
53
|
+
|
|
54
|
+
# Normalize language code to a standard format
|
|
55
|
+
# @param code [String, Symbol] Language code
|
|
56
|
+
# @return [String] Normalized language code
|
|
57
|
+
def normalize_language_code(code)
|
|
58
|
+
code.to_s.downcase.split(/[-_]/).first
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
module ActiontextTranslate
|
|
8
|
+
module Translators
|
|
9
|
+
# ChatGPT (OpenAI) translator implementation
|
|
10
|
+
class Chatgpt < Base
|
|
11
|
+
API_ENDPOINT = 'https://api.openai.com/v1/chat/completions'
|
|
12
|
+
|
|
13
|
+
# Language code to name mapping for clearer ChatGPT prompts
|
|
14
|
+
# Only includes languages where the code doesn't obviously map to the name
|
|
15
|
+
# For unlisted languages, the code is capitalized (e.g., 'fr' -> 'Fr')
|
|
16
|
+
LANGUAGE_NAMES = {
|
|
17
|
+
'en' => 'English',
|
|
18
|
+
'uk' => 'Ukrainian',
|
|
19
|
+
'de' => 'German',
|
|
20
|
+
'fr' => 'French',
|
|
21
|
+
'es' => 'Spanish',
|
|
22
|
+
'it' => 'Italian',
|
|
23
|
+
'pl' => 'Polish',
|
|
24
|
+
'pt' => 'Portuguese'
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def initialize(config = nil)
|
|
28
|
+
super
|
|
29
|
+
@api_key = config&.api_key || ActiontextTranslate.configuration.api_key
|
|
30
|
+
raise ArgumentError, 'API key is required for ChatGPT translator' if @api_key.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Translate text using ChatGPT
|
|
34
|
+
def translate(text, from:, to:, html_mode: false)
|
|
35
|
+
return text if blank?(text)
|
|
36
|
+
|
|
37
|
+
source_lang = language_name(from)
|
|
38
|
+
target_lang = language_name(to)
|
|
39
|
+
|
|
40
|
+
prompt = build_prompt(text, source_lang, target_lang, html_mode)
|
|
41
|
+
|
|
42
|
+
response = call_openai_api(prompt)
|
|
43
|
+
extract_translation(response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_prompt(text, source_lang, target_lang, html_mode)
|
|
49
|
+
if html_mode
|
|
50
|
+
<<~PROMPT
|
|
51
|
+
Translate the following HTML content from #{source_lang} to #{target_lang}.
|
|
52
|
+
|
|
53
|
+
CRITICAL RULES:
|
|
54
|
+
1. Preserve ALL HTML tags, attributes, and structure EXACTLY as they are
|
|
55
|
+
2. Only translate the text content between tags
|
|
56
|
+
3. Do NOT translate URLs, link hrefs, or placeholder text like "XLINKSTARTX1X"
|
|
57
|
+
4. Do NOT add or remove any HTML tags
|
|
58
|
+
5. Maintain proper spacing and formatting
|
|
59
|
+
6. Return ONLY the translated HTML, no explanations
|
|
60
|
+
|
|
61
|
+
Content to translate:
|
|
62
|
+
#{text}
|
|
63
|
+
PROMPT
|
|
64
|
+
else
|
|
65
|
+
<<~PROMPT
|
|
66
|
+
Translate the following text from #{source_lang} to #{target_lang}.
|
|
67
|
+
Return ONLY the translation, no explanations or additional text.
|
|
68
|
+
|
|
69
|
+
Text to translate:
|
|
70
|
+
#{text}
|
|
71
|
+
PROMPT
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def call_openai_api(prompt)
|
|
76
|
+
uri = URI(API_ENDPOINT)
|
|
77
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
78
|
+
http.use_ssl = true
|
|
79
|
+
http.read_timeout = config.timeout
|
|
80
|
+
|
|
81
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
82
|
+
request['Authorization'] = "Bearer #{@api_key}"
|
|
83
|
+
request['Content-Type'] = 'application/json'
|
|
84
|
+
request.body = {
|
|
85
|
+
model: config.model,
|
|
86
|
+
messages: [
|
|
87
|
+
{
|
|
88
|
+
role: 'system',
|
|
89
|
+
content: 'You are a professional translator. Translate accurately and naturally while ' \
|
|
90
|
+
'preserving any HTML structure.'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
role: 'user',
|
|
94
|
+
content: prompt
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
temperature: config.temperature
|
|
98
|
+
}.to_json
|
|
99
|
+
|
|
100
|
+
response = http.request(request)
|
|
101
|
+
|
|
102
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
103
|
+
raise TranslationError, "OpenAI API error: #{response.code} - #{response.body}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
JSON.parse(response.body)
|
|
107
|
+
rescue JSON::ParserError => e
|
|
108
|
+
raise TranslationError, "Failed to parse OpenAI response: #{e.message}"
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
raise TranslationError, "OpenAI API request failed: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def extract_translation(response)
|
|
114
|
+
response.dig('choices', 0, 'message', 'content')&.strip || ''
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
raise TranslationError, "Failed to extract translation from response: #{e.message}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def language_name(code)
|
|
120
|
+
normalized = normalize_language_code(code)
|
|
121
|
+
LANGUAGE_NAMES[normalized] || normalized.capitalize
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Custom error class for translation errors
|
|
126
|
+
class TranslationError < StandardError; end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'actiontext_translate/version'
|
|
4
|
+
require_relative 'actiontext_translate/configuration'
|
|
5
|
+
require_relative 'actiontext_translate/html_processor'
|
|
6
|
+
require_relative 'actiontext_translate/translators/base'
|
|
7
|
+
require_relative 'actiontext_translate/translator'
|
|
8
|
+
|
|
9
|
+
module ActiontextTranslate
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
# Quick access method for translation
|
|
13
|
+
# @param text [String] Text to translate
|
|
14
|
+
# @param from [String] Source language code
|
|
15
|
+
# @param to [String] Target language code
|
|
16
|
+
# @param html_mode [Boolean] Whether to preserve HTML
|
|
17
|
+
# @return [String] Translated text
|
|
18
|
+
def self.translate(text, from:, to:, html_mode: false)
|
|
19
|
+
translator = Translator.new
|
|
20
|
+
translator.translate(text, from: from, to: to, html_mode: html_mode)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Quick access method for HTML translation
|
|
24
|
+
# @param html [String] HTML to translate
|
|
25
|
+
# @param from [String] Source language code
|
|
26
|
+
# @param to [String] Target language code
|
|
27
|
+
# @return [String] Translated HTML
|
|
28
|
+
def self.translate_html(html, from:, to:)
|
|
29
|
+
translator = Translator.new
|
|
30
|
+
translator.translate_html(html, from: from, to: to)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Quick access method for ActionText translation
|
|
34
|
+
# @param rich_text [ActionText::RichText, String] Rich text to translate
|
|
35
|
+
# @param from [String] Source language code
|
|
36
|
+
# @param to [String] Target language code
|
|
37
|
+
# @return [String] Translated HTML
|
|
38
|
+
def self.translate_action_text(rich_text, from:, to:)
|
|
39
|
+
translator = Translator.new
|
|
40
|
+
translator.translate_action_text(rich_text, from: from, to: to)
|
|
41
|
+
end
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: actiontext_translate
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- mykbren
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-12-18 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: nokogiri
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.13'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.13'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rubocop
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.21'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.21'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: simplecov
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.22'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.22'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: webmock
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.18'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.18'
|
|
97
|
+
description: A Ruby gem for translating ActionText rich text content using ChatGPT
|
|
98
|
+
while perfectly preserving HTML structure, tags, and links. Extensible architecture
|
|
99
|
+
allows adding additional translation providers.
|
|
100
|
+
email:
|
|
101
|
+
- myk.bren@gmail.com
|
|
102
|
+
executables: []
|
|
103
|
+
extensions: []
|
|
104
|
+
extra_rdoc_files: []
|
|
105
|
+
files:
|
|
106
|
+
- CHANGELOG.md
|
|
107
|
+
- LICENSE.txt
|
|
108
|
+
- README.md
|
|
109
|
+
- lib/actiontext_translate.rb
|
|
110
|
+
- lib/actiontext_translate/configuration.rb
|
|
111
|
+
- lib/actiontext_translate/html_processor.rb
|
|
112
|
+
- lib/actiontext_translate/translator.rb
|
|
113
|
+
- lib/actiontext_translate/translators/base.rb
|
|
114
|
+
- lib/actiontext_translate/translators/chatgpt.rb
|
|
115
|
+
- lib/actiontext_translate/version.rb
|
|
116
|
+
homepage: https://github.com/mykbren/actiontext_translate
|
|
117
|
+
licenses:
|
|
118
|
+
- MIT
|
|
119
|
+
metadata:
|
|
120
|
+
homepage_uri: https://github.com/mykbren/actiontext_translate
|
|
121
|
+
source_code_uri: https://github.com/mykbren/actiontext_translate
|
|
122
|
+
changelog_uri: https://github.com/mykbren/actiontext_translate/blob/main/CHANGELOG.md
|
|
123
|
+
rubygems_mfa_required: 'true'
|
|
124
|
+
post_install_message:
|
|
125
|
+
rdoc_options: []
|
|
126
|
+
require_paths:
|
|
127
|
+
- lib
|
|
128
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
129
|
+
requirements:
|
|
130
|
+
- - ">="
|
|
131
|
+
- !ruby/object:Gem::Version
|
|
132
|
+
version: 2.7.0
|
|
133
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '0'
|
|
138
|
+
requirements: []
|
|
139
|
+
rubygems_version: 3.4.6
|
|
140
|
+
signing_key:
|
|
141
|
+
specification_version: 4
|
|
142
|
+
summary: Translate ActionText content while preserving HTML tags using ChatGPT
|
|
143
|
+
test_files: []
|