honyaku 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c4deb596d588ecdc949ec04c2c139bdd0c8d886c365aa9e5f980f21ffd0b986a
4
+ data.tar.gz: 42749082904b0c7cfd8ce08b038c776a7e3c57cf22b37878a1e3145cf5878c7a
5
+ SHA512:
6
+ metadata.gz: 0bab4819f367a8144255b85f6b1c5aa0b23b132d4dc02fe6cb2c7bad6bfbdc09f6904c833ee37849ad739275d4c03f6ee65fa7917de1e95c5c249ef562d82d28
7
+ data.tar.gz: 8178c8117039fcb339476be232b109695d787a64346af4d64eb8bf3375c735494e563f1a44871acd01b534822de3cf9111b93b79a014100d9eab1033dc0ffba0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Andrew Culver
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # Honyaku 翻訳
2
+
3
+ A Ruby gem for quickly, reliably, and accurately translating your Rails application using OpenAI. Created because it replaced a $34K/year SaaS contract and streamlined our deploy process.
4
+
5
+ Honyaku was built using [Cursor Composer](https://docs.cursor.com/composer) with [claude-3.5-sonnet](https://www.anthropic.com/news/claude-35-sonnet), prompted by [Andrew Culver](https://x.com/andrewculver) at [ClickFunnels](https://www.clickfunnels.com).
6
+
7
+ ## Features
8
+
9
+ - Uses GPT-4 for high-quality translations (GPT-3.5-turbo optional for faster processing)
10
+ - Preserves YAML structure, references, and interpolation variables
11
+ - Supports translation rules via `.honyakurules` files
12
+ - Handles large files through automatic chunking
13
+ - Automatically fixes YAML formatting issues caused by the GPT
14
+ - Supports backup creation before modifications
15
+ - Smart file skipping to avoid unnecessary retranslation
16
+
17
+ # Example Output
18
+
19
+ ```
20
+ $ honyaku translate ja --path config/locales/en/affiliates
21
+ πŸ“‹ Found 2 translation rule file(s):
22
+ πŸ“ /Users/andrewculver/Sites/admin/.honyakurules
23
+ 🌐 /Users/andrewculver/Sites/admin/.honyakurules.ja
24
+ 🌏 Translating from en to ja...
25
+ πŸ“‚ Processing files in config/locales/en/affiliates...
26
+ πŸ“ Processing config/locales/en/affiliates/active_referrals_report.en.yml...
27
+ πŸ“¦ Splitting file into 3 chunks...
28
+ πŸ”„ Translating chunk 1 of 3...
29
+ πŸ”„ Translating chunk 2 of 3...
30
+ πŸ”„ Translating chunk 3 of 3...
31
+ ✨ Created config/locales/ja/affiliates/active_referrals_report.ja.yml
32
+ πŸ”§ Checking for YAML issues...
33
+ βœ… No more YAML errors found
34
+ πŸ“ Processing config/locales/en/affiliates/add_tag_actions.en.yml...
35
+ ✨ Created config/locales/ja/affiliates/add_tag_actions.ja.yml
36
+ πŸ”§ Checking for YAML issues...
37
+ πŸ”§ Found YAML error on line 5: (<unknown>): found character that cannot start any token while scanning for the next token at line 5 column 13
38
+ zero: %{count}をフィγƒͺγ‚¨γ‚€γƒˆγ«γ‚³γƒŸγƒƒγ‚·γƒ§γƒ³γƒ—γƒ©γƒ³γ‚’γ‚―γ‚·γƒ§γƒ³γ‚’θΏ½εŠ γ™γ‚‹
39
+ πŸ”§ Found YAML error on line 6: (<unknown>): found character that cannot start any token while scanning for the next token at line 6 column 12
40
+ one: %{count}をフィγƒͺγ‚¨γ‚€γƒˆγ«γ‚³γƒŸγƒƒγ‚·γƒ§γƒ³γƒ—γƒ©γƒ³γ‚’γ‚―γ‚·γƒ§γƒ³γ‚’θΏ½εŠ γ™γ‚‹
41
+ πŸ”§ Found YAML error on line 7: (<unknown>): found character that cannot start any token while scanning for the next token at line 7 column 14
42
+ other: %{count}をフィγƒͺγ‚¨γ‚€γƒˆγ«γ‚³γƒŸγƒƒγ‚·γƒ§γƒ³γƒ—γƒ©γƒ³γ‚’γ‚―γ‚·γƒ§γƒ³γ‚’θΏ½εŠ γ™γ‚‹
43
+ βœ… No more YAML errors found
44
+ ✨ Fixed YAML formatting issues
45
+ ⏭️ Skipping config/locales/en/affiliates/applied_tags.en.yml - translation is up to date
46
+ ⏭️ Skipping config/locales/en/affiliates/approve_actions.en.yml - translation is up to date
47
+ ...
48
+ ```
49
+
50
+ ## Installation
51
+
52
+ Add to your Gemfile:
53
+ ```ruby
54
+ gem 'honyaku'
55
+ ```
56
+
57
+ Or install directly:
58
+ ```bash
59
+ gem install honyaku
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ Set your OpenAI API key:
65
+ ```bash
66
+ export OPENAI_API_KEY=your-api-key
67
+ ```
68
+
69
+ Or if you've already got that configured for another purpose and you want to specify a different key for Honyaku, you can set this and we'll use it instead:
70
+ ```bash
71
+ export HONYAKU_OPENAI_API_KEY=your-api-key
72
+ ```
73
+
74
+ ## Usage
75
+
76
+ ### Basic Translation
77
+
78
+ ```bash
79
+ # Translate a file
80
+ honyaku translate ja --path config/locales/en.yml
81
+
82
+ # Translate a directory
83
+ honyaku translate es --path config/locales
84
+
85
+ # Create backups before modifying
86
+ honyaku translate ja --backup --path config/locales/en.yml
87
+
88
+ # Use GPT-3.5-turbo for faster processing
89
+ honyaku translate fr --model gpt-3.5-turbo --path config/locales/en.yml
90
+
91
+ # Force retranslation of files even if they're up to date
92
+ honyaku translate ja --force --path config/locales/en.yml
93
+ ```
94
+
95
+ ### Smart File Skipping
96
+
97
+ Honyaku tracks file modification times to avoid unnecessary retranslation:
98
+
99
+ - Checks both git history and filesystem timestamps
100
+ - Uses the newer of the two dates for comparison
101
+ - Skips translation if target file is newer than source
102
+ - Shows "⏭️ Skipping" message for up-to-date files
103
+
104
+ You can override this behavior with `--force` to retranslate all files regardless of their timestamps.
105
+
106
+ ### Translation Rules
107
+
108
+ Honyaku supports two types of rule files:
109
+ - `.honyakurules` - General rules for all translations
110
+ - `.honyakurules.{locale}` - Language-specific rules (e.g., `.honyakurules.ja`)
111
+
112
+ Example `.honyakurules`:
113
+ ```yaml
114
+ Don't translate the term "ClickFunnels", that's our brand name.
115
+ ```
116
+
117
+ Example `.honyakurules.ja`:
118
+ ```yaml
119
+ When translating to Japanese, do not insert a space between particles like `%{site_name} に`... that should be `%{site_name}に`
120
+ ```
121
+
122
+ Rules can be used for:
123
+ - Preserving brand names
124
+ - Enforcing locale-specific formatting
125
+ - Maintaining consistent terminology
126
+
127
+ ### YAML Fixing
128
+
129
+ Fix formatting issues in translated files:
130
+ ```bash
131
+ # Fix a single file
132
+ honyaku fix config/locales/ja/application.ja.yml
133
+
134
+ # Fix all files in a directory
135
+ honyaku fix config/locales/ja --backup
136
+ ```
137
+
138
+ ## Technical Details
139
+
140
+ ### Large File Handling
141
+
142
+ Files over 250 lines are automatically split into chunks for translation. Each chunk maintains proper YAML structure to ensure accurate translations.
143
+
144
+ ### Error Recovery
145
+
146
+ When invalid YAML is detected:
147
+ 1. Automatic formatting fixes are attempted
148
+ 2. Translation is retried if necessary
149
+ 3. Original file is preserved if fixes fail
150
+
151
+ ### Model Selection
152
+
153
+ - Default: GPT-4 (higher quality, slower)
154
+ - Alternative: GPT-3.5-turbo (faster, less accurate)
155
+
156
+ ## Development
157
+
158
+ After checking out the repo:
159
+ 1. Run `bin/setup` to install dependencies
160
+ 2. Run `rake test` to run the tests
161
+ 3. Run `bin/console` for an interactive prompt
162
+
163
+ ## Contributing
164
+
165
+ Bug reports and pull requests are welcome on GitHub at https://github.com/andrewculver/honyaku.
166
+
167
+ ## License
168
+
169
+ Released under the MIT License. See [LICENSE](LICENSE.txt) for details.
data/exe/honyaku ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "honyaku"
5
+ Honyaku::CLI.start(ARGV)
@@ -0,0 +1,323 @@
1
+ require "thor"
2
+ require "yaml"
3
+ require "honyaku/translator"
4
+
5
+ module Honyaku
6
+ class CLI < Thor
7
+ desc "translate LOCALE", "Translate your application into the specified locale"
8
+ long_desc <<-LONGDESC
9
+ Translates YAML files from one locale to another using OpenAI.
10
+
11
+ Examples:
12
+ # Translate a specific file from English to Japanese
13
+ $ honyaku translate ja --path config/locales/en.yml
14
+
15
+ # Translate all files in a directory from English to Spanish
16
+ $ honyaku translate es --path config/locales
17
+
18
+ # Translate using GPT-4 for higher accuracy
19
+ $ honyaku translate de --model gpt-4 --path config/locales/en.yml
20
+ LONGDESC
21
+ method_option :from, aliases: "-f", desc: "Source locale (defaults to en)"
22
+ method_option :path, aliases: "-p", desc: "Path to YAML file or directory (defaults to config/locales)"
23
+ method_option :model,
24
+ aliases: "-m",
25
+ desc: "Specify which AI model to use (defaults to gpt-4, use gpt-3.5-turbo for faster but less accurate translations)"
26
+ method_option :backup, aliases: "-b", type: :boolean, desc: "Create .bak files before modifying"
27
+ method_option :force, type: :boolean, desc: "Retranslate files even if target is newer than source"
28
+ def translate(locale)
29
+ api_key = ENV["HONYAKU_OPENAI_API_KEY"] || ENV["OPENAI_API_KEY"]
30
+ unless api_key
31
+ puts "❌ Please set either HONYAKU_OPENAI_API_KEY or OPENAI_API_KEY environment variable"
32
+ exit 1
33
+ end
34
+
35
+ source_locale = options[:from] || "en"
36
+ path = options[:path] || "config/locales"
37
+ model = options[:model] || "gpt-4"
38
+
39
+ # Check if the source path exists
40
+ unless File.exist?(path)
41
+ puts "❌ Source path not found: #{path}"
42
+ puts " Please check that the file or directory exists"
43
+ exit 1
44
+ end
45
+
46
+ # Find all .honyakurules files from root to current path
47
+ rules = find_translation_rules(path, locale)
48
+ if rules.any?
49
+ puts "πŸ“‹ Found #{rules.length} translation rule file(s):"
50
+ rules.each do |rule|
51
+ prefix = rule[:locale_specific] ? "🌐" : "πŸ“"
52
+ puts " #{prefix} #{rule[:path]}"
53
+ end
54
+ end
55
+
56
+ puts "🌏 Translating from #{source_locale} to #{locale}..."
57
+ puts "πŸ“‚ Processing files in #{path}..."
58
+
59
+ translator = Translator.new(model: model, translation_rules: rules)
60
+
61
+ if File.file?(path)
62
+ process_file(path, translator, source_locale, locale)
63
+ else
64
+ files = Dir.glob("#{path}/**/*.yml")
65
+ if files.empty?
66
+ puts "❌ No YAML files found in: #{path}"
67
+ puts " Make sure your path contains .yml files"
68
+ exit 1
69
+ end
70
+ files.each do |file|
71
+ process_file(file, translator, source_locale, locale)
72
+ end
73
+ end
74
+
75
+ puts "βœ… Translation complete!"
76
+ end
77
+
78
+ desc "fix PATH", "Fix YAML formatting issues in translated files"
79
+ long_desc <<-LONGDESC
80
+ Fixes common YAML formatting issues in translated files, such as:
81
+ - Adding quotes around values that start with %{variable}
82
+ - Fixing spacing in interpolation variables
83
+ - Preserving YAML references and anchors
84
+ - Maintaining proper indentation
85
+
86
+ Examples:
87
+ # Fix a specific file
88
+ $ honyaku fix config/locales/ja/courses.ja.yml
89
+
90
+ # Fix all YAML files in a directory
91
+ $ honyaku fix config/locales/ja
92
+ LONGDESC
93
+ method_option :model, aliases: "-m", desc: "Specify which AI model to use (defaults to gpt-3.5-turbo)"
94
+ method_option :backup, aliases: "-b", type: :boolean, desc: "Create .bak files before modifying"
95
+ def fix(path)
96
+ api_key = ENV["HONYAKU_OPENAI_API_KEY"] || ENV["OPENAI_API_KEY"]
97
+ unless api_key
98
+ puts "❌ Please set either HONYAKU_OPENAI_API_KEY or OPENAI_API_KEY environment variable"
99
+ exit 1
100
+ end
101
+
102
+ model = options[:model] || "gpt-3.5-turbo"
103
+
104
+ puts "πŸ”§ Fixing YAML formatting issues..."
105
+ puts "πŸ“‚ Processing files in #{path}..."
106
+
107
+ fixer = Translator.new(model: model)
108
+
109
+ if File.file?(path)
110
+ fix_file(path, fixer)
111
+ else
112
+ Dir.glob("#{path}/**/*.yml").each do |file|
113
+ fix_file(file, fixer)
114
+ end
115
+ end
116
+
117
+ puts "βœ… Fixes complete!"
118
+ end
119
+
120
+ private
121
+
122
+ def find_translation_rules(start_path, target_locale = nil)
123
+ rules = []
124
+
125
+ # Start from the directory containing the YAML file/directory
126
+ current_path = File.expand_path(start_path)
127
+
128
+ # First check the current working directory
129
+ if File.exist?('.honyakurules')
130
+ rules << {
131
+ path: File.expand_path('.honyakurules'),
132
+ content: File.read('.honyakurules').strip
133
+ }
134
+ end
135
+
136
+ # Check for locale-specific rules in current directory
137
+ if target_locale && File.exist?(".honyakurules.#{target_locale}")
138
+ rules << {
139
+ path: File.expand_path(".honyakurules.#{target_locale}"),
140
+ content: File.read(".honyakurules.#{target_locale}").strip,
141
+ locale_specific: true
142
+ }
143
+ end
144
+
145
+ # Walk up the directory tree from the YAML path
146
+ while current_path != '/' && current_path != Dir.pwd
147
+ # Check for general rules
148
+ rules_file = File.join(current_path, '.honyakurules')
149
+ if File.exist?(rules_file)
150
+ rules << {
151
+ path: rules_file,
152
+ content: File.read(rules_file).strip
153
+ }
154
+ end
155
+
156
+ # Check for locale-specific rules
157
+ if target_locale
158
+ locale_rules_file = File.join(current_path, ".honyakurules.#{target_locale}")
159
+ if File.exist?(locale_rules_file)
160
+ rules << {
161
+ path: locale_rules_file,
162
+ content: File.read(locale_rules_file).strip,
163
+ locale_specific: true
164
+ }
165
+ end
166
+ end
167
+
168
+ current_path = File.dirname(current_path)
169
+ end
170
+
171
+ # Reverse to maintain root-to-local order, but ensure locale-specific rules come after general rules
172
+ rules.reverse.partition { |r| !r[:locale_specific] }.flatten
173
+ end
174
+
175
+ def process_file(file_path, translator, source_locale, target_locale)
176
+ # Check if this is a source locale file we should translate
177
+ source_pattern = /#{source_locale}(\/|\.yml)/
178
+ return unless file_path =~ source_pattern
179
+
180
+ # Generate the target filename
181
+ target_file = file_path.gsub(source_pattern, "#{target_locale}\\1")
182
+
183
+ # Only skip if target exists AND is newer (unless --force is used)
184
+ if File.exist?(target_file) && !options[:force]
185
+ source_time = get_last_modified_time(file_path)
186
+ target_time = get_last_modified_time(target_file)
187
+
188
+ if target_time && source_time && target_time > source_time
189
+ puts "⏭️ Skipping #{file_path} - translation is up to date"
190
+ return
191
+ end
192
+ end
193
+
194
+ puts "πŸ“ Processing #{file_path}..."
195
+
196
+ begin
197
+ attempts = 0
198
+ max_attempts = 3
199
+
200
+ loop do
201
+ attempts += 1
202
+ begin
203
+ translated_content = translator.translate_hash(file_path, source_locale, target_locale)
204
+ rescue => e
205
+ puts "❌ Translation failed: #{e.message}"
206
+ break
207
+ end
208
+
209
+ # Don't proceed if translation failed
210
+ if !translated_content || translated_content.strip.empty?
211
+ puts "❌ Translation failed - no content generated"
212
+ break
213
+ end
214
+
215
+ # Create directory and write file only if we have valid content
216
+ FileUtils.mkdir_p(File.dirname(target_file))
217
+
218
+ # Backup if requested
219
+ if options[:backup] && File.exist?(target_file)
220
+ backup_path = "#{target_file}.bak"
221
+ FileUtils.cp(target_file, backup_path)
222
+ end
223
+
224
+ # Write the translated content
225
+ File.write(target_file, translated_content)
226
+ puts "✨ Created #{target_file}"
227
+
228
+ # Automatically fix any YAML issues
229
+ puts "πŸ”§ Checking for YAML issues..."
230
+ begin
231
+ fixed_content = translator.fix_yaml(target_file)
232
+ if fixed_content != translated_content
233
+ if options[:backup] && !File.exist?("#{target_file}.bak")
234
+ FileUtils.cp(target_file, "#{target_file}.bak")
235
+ end
236
+
237
+ File.write(target_file, fixed_content)
238
+ puts "✨ Fixed YAML formatting issues"
239
+ end
240
+ break # Success! Exit the loop
241
+ rescue => e
242
+ if e.message.include?("needs retranslation") && attempts < max_attempts
243
+ puts "⚠️ Translation attempt #{attempts} produced invalid YAML, retrying..."
244
+ # Clean up the file before retrying
245
+ File.unlink(target_file) if File.exist?(target_file)
246
+ next
247
+ else
248
+ # Clean up and re-raise
249
+ File.unlink(target_file) if File.exist?(target_file)
250
+ raise e
251
+ end
252
+ end
253
+ end
254
+ rescue => e
255
+ puts "❌ Error processing #{file_path}: #{e.message}"
256
+ # Ensure file is cleaned up if it was created
257
+ File.unlink(target_file) if File.exist?(target_file)
258
+ end
259
+ end
260
+
261
+ def fix_file(file_path, fixer)
262
+ puts "πŸ”§ Fixing #{file_path}..."
263
+
264
+ begin
265
+ # Backup if requested
266
+ if options[:backup]
267
+ backup_path = "#{file_path}.bak"
268
+ FileUtils.cp(file_path, backup_path)
269
+ puts "πŸ“‘ Created backup at #{backup_path}"
270
+ end
271
+
272
+ fixed_content = fixer.fix_yaml(file_path)
273
+ File.write(file_path, fixed_content)
274
+ puts "✨ Fixed #{file_path}"
275
+ rescue => e
276
+ puts "❌ Error fixing #{file_path}: #{e.message}"
277
+ end
278
+ end
279
+
280
+ def get_last_modified_time(file_path)
281
+ times = []
282
+
283
+ # Get git timestamp if available
284
+ if git_time = get_git_modified_time(file_path)
285
+ times << git_time
286
+ end
287
+
288
+ # Get filesystem timestamp
289
+ if File.exist?(file_path)
290
+ times << File.mtime(file_path)
291
+ end
292
+
293
+ # Return the newest timestamp (or nil if no timestamps found)
294
+ times.max
295
+ end
296
+
297
+ def get_git_modified_time(file_path)
298
+ return nil unless system("git rev-parse --is-inside-work-tree > /dev/null 2>&1")
299
+
300
+ time_str = `git log -1 --format=%cd --date=iso -- #{file_path} 2>/dev/null`.strip
301
+ return nil if time_str.empty?
302
+
303
+ Time.parse(time_str)
304
+ rescue
305
+ nil
306
+ end
307
+
308
+ desc "status", "Show translation status for all locales"
309
+ def status
310
+ puts "πŸ“Š Translation Status:"
311
+ # Status reporting logic will go here
312
+ end
313
+
314
+ desc "version", "Show Honyaku version"
315
+ def version
316
+ puts "Honyaku v#{Honyaku::VERSION}"
317
+ end
318
+
319
+ def self.exit_on_failure?
320
+ true
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,241 @@
1
+ require "openai"
2
+ require "yaml"
3
+
4
+ module Honyaku
5
+ class Translator
6
+ LINES_PER_CHUNK = 250
7
+
8
+ def initialize(api_key: nil, model: "gpt-4", translation_rules: [])
9
+ api_key ||= ENV["HONYAKU_OPENAI_API_KEY"] || ENV["OPENAI_API_KEY"]
10
+ @client = OpenAI::Client.new(access_token: api_key)
11
+ @model = model
12
+ @translation_rules = translation_rules
13
+ end
14
+
15
+ def translate_hash(file_path, from_locale, to_locale)
16
+ yaml_content = File.read(file_path)
17
+ lines = yaml_content.lines
18
+
19
+ # If the file is small enough, translate it all at once
20
+ if lines.size <= LINES_PER_CHUNK
21
+ result = translate_chunk(yaml_content, from_locale, to_locale)
22
+ raise "Translation failed" unless result
23
+ return result
24
+ end
25
+
26
+ # Otherwise, split into chunks and translate each
27
+ chunks = split_into_chunks(lines)
28
+ puts "πŸ“¦ Splitting file into #{chunks.size} chunks..."
29
+
30
+ translated_chunks = []
31
+
32
+ chunks.each_with_index do |chunk, i|
33
+ puts "πŸ”„ Translating chunk #{i + 1} of #{chunks.size}..."
34
+ result = translate_chunk(chunk, from_locale, to_locale)
35
+
36
+ # If any chunk fails, abort the whole translation
37
+ raise "Translation failed for chunk #{i + 1}" unless result
38
+ translated_chunks << result
39
+ end
40
+
41
+ translated_chunks.join("\n")
42
+ end
43
+
44
+ def fix_yaml(file_path)
45
+ content = File.read(file_path)
46
+ fixed_any = false
47
+
48
+ loop do
49
+ begin
50
+ YAML.load(content)
51
+ puts "βœ… No more YAML errors found"
52
+ return content
53
+ rescue Psych::SyntaxError => e
54
+ # If OpenAI returned invalid YAML structure, signal that we need to retranslate
55
+ if e.message.include?("did not find expected key while parsing a block mapping")
56
+ raise "Translation resulted in invalid YAML structure - needs retranslation"
57
+ end
58
+
59
+ lines = content.lines
60
+ line_number = e.line - 1 # YAML errors are 1-based
61
+ problematic_line = lines[line_number]
62
+
63
+ puts "πŸ”§ Found YAML error on line #{e.line}: #{e.message}"
64
+ puts " #{problematic_line.strip}"
65
+
66
+ # Only try to fix common syntax issues
67
+ if e.message.include?("cannot start any token")
68
+ fixed = false
69
+
70
+ # Fix case 1: Values starting with %{var} need quotes
71
+ if problematic_line.include?("%{") && problematic_line =~ /^(\s*[^:]+:\s*)(?:(&\w+)\s+)?(%\{.+)$/
72
+ prefix, reference, value = $1, $2, $3
73
+ fixed_line = if reference
74
+ "#{prefix}#{reference} \"#{value}\""
75
+ else
76
+ "#{prefix}\"#{value}\""
77
+ end
78
+ fixed = true
79
+ # Fix case 2: Fix incorrect spacing in %{ var }
80
+ elsif problematic_line.include?("% {")
81
+ fixed_line = problematic_line.gsub("% {", "%{")
82
+ fixed = true
83
+ end
84
+
85
+ if fixed
86
+ # Update the line
87
+ lines[line_number] = "#{fixed_line}\n"
88
+ content = lines.join
89
+ fixed_any = true
90
+ next # Continue to the next iteration to find more errors
91
+ end
92
+ end
93
+
94
+ # If we get here, we couldn't fix this error
95
+ if fixed_any
96
+ puts "❌ Unable to fix remaining YAML errors"
97
+ else
98
+ puts "❌ Unable to fix any YAML errors"
99
+ end
100
+ return content
101
+ end
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def split_into_chunks(lines)
108
+ chunks = []
109
+ current_chunk = []
110
+ current_indent = 0
111
+ line_count = 0
112
+
113
+ lines.each do |line|
114
+ # Calculate the indentation level of the current line
115
+ indent = line[/\A */].length
116
+
117
+ # Start a new chunk if we hit the line limit and we're at the root level
118
+ if line_count >= LINES_PER_CHUNK && indent <= current_indent
119
+ chunks << current_chunk.join
120
+ current_chunk = []
121
+ line_count = 0
122
+ end
123
+
124
+ current_chunk << line
125
+ line_count += 1
126
+ current_indent = indent
127
+ end
128
+
129
+ # Add the last chunk if there's anything left
130
+ chunks << current_chunk.join if current_chunk.any?
131
+
132
+ chunks
133
+ end
134
+
135
+ def translate_chunk(content, from_locale, to_locale)
136
+ max_retries = 3
137
+ attempts = 0
138
+
139
+ begin
140
+ attempts += 1
141
+ response = @client.chat(
142
+ parameters: {
143
+ model: @model,
144
+ messages: [
145
+ {
146
+ role: "system",
147
+ content: build_system_prompt
148
+ },
149
+ {
150
+ role: "user",
151
+ content: "Translate this YAML content from #{from_locale} to #{to_locale}. Keep all structure and special characters exactly the same:\n\n#{content}"
152
+ }
153
+ ],
154
+ temperature: 0.7
155
+ }
156
+ )
157
+
158
+ # Clean up any markdown code block markers
159
+ response_text = response.dig("choices", 0, "message", "content")
160
+ response_text.gsub(/^```ya?ml\s*\n/, '').gsub(/\n```\s*$/, '')
161
+ rescue => e
162
+ # Don't retry if it's a billing/credits issue
163
+ if e.message.include?("insufficient_quota") || e.message.include?("billing")
164
+ puts "❌ OpenAI API error: #{e.message}"
165
+ raise e
166
+ end
167
+
168
+ if attempts < max_retries
169
+ puts "⚠️ OpenAI API error, retrying (attempt #{attempts}/#{max_retries}): #{e.message}"
170
+ sleep(attempts) # Exponential backoff
171
+ retry
172
+ else
173
+ puts "❌ OpenAI API error after #{max_retries} attempts: #{e.message}"
174
+ raise e
175
+ end
176
+ end
177
+ end
178
+
179
+ def fix_line(line, error)
180
+ response = @client.chat(
181
+ parameters: {
182
+ model: @model,
183
+ messages: [
184
+ {
185
+ role: "system",
186
+ content: "You are a YAML expert. Fix the provided line to be valid YAML. Common issues include:
187
+ - Values starting with % need to be quoted
188
+ - Proper escaping of special characters
189
+ Return only the fixed line, no explanation needed."
190
+ },
191
+ {
192
+ role: "user",
193
+ content: "Fix this YAML line that generated this error: #{error}\n\nLine: #{line}"
194
+ }
195
+ ],
196
+ temperature: 0.3
197
+ }
198
+ )
199
+
200
+ response.dig("choices", 0, "message", "content").strip
201
+ rescue => e
202
+ puts "⚠️ Error getting fix suggestion: #{e.message}"
203
+ line
204
+ end
205
+
206
+ def build_system_prompt
207
+ base_prompt = <<~PROMPT
208
+ You are a professional translator. You will be translating YAML files.
209
+
210
+ CRITICAL REQUIREMENTS:
211
+ 1. Only translate text values after the colon (:)
212
+ 2. Never modify, translate, or remove:
213
+ - YAML keys (text before the colon)
214
+ - Interpolation variables (like %{name})
215
+ - YAML references and anchors
216
+ - Comments
217
+ - Empty lines
218
+ 3. Keep all special characters exactly as they appear
219
+ 4. Maintain the exact same line count
220
+ 5. Never add or remove lines
221
+ 6. Never change the structure of the file
222
+ PROMPT
223
+
224
+ if @translation_rules.any?
225
+ general_rules, locale_rules = @translation_rules.partition { |r| !r[:locale_specific] }
226
+
227
+ if general_rules.any?
228
+ base_prompt += "\n\nGeneral translation rules:\n" +
229
+ general_rules.map { |rule| rule[:content] }.join("\n\n")
230
+ end
231
+
232
+ if locale_rules.any?
233
+ base_prompt += "\n\nTarget language specific rules:\n" +
234
+ locale_rules.map { |rule| rule[:content] }.join("\n\n")
235
+ end
236
+ end
237
+
238
+ base_prompt
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Honyaku
4
+ VERSION = "0.1.0"
5
+ end
data/lib/honyaku.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "honyaku/version"
4
+ require "honyaku/cli"
5
+ require "honyaku/translator"
6
+
7
+ module Honyaku
8
+ class Error < StandardError; end
9
+ # Your code goes here...
10
+ end
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: honyaku
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew Culver
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-02-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-openai
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: yaml
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.3.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.3.0
55
+ description:
56
+ email:
57
+ - andrew.culver@gmail.com
58
+ executables:
59
+ - honyaku
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - LICENSE.txt
64
+ - README.md
65
+ - exe/honyaku
66
+ - lib/honyaku.rb
67
+ - lib/honyaku/cli.rb
68
+ - lib/honyaku/translator.rb
69
+ - lib/honyaku/version.rb
70
+ homepage: https://github.com/andrewculver/honyaku
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/andrewculver/honyaku
75
+ source_code_uri: https://github.com/andrewculver/honyaku
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: 3.0.0
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubygems_version: 3.2.33
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: Translate your Rails application using OpenAI
95
+ test_files: []