better_translate 0.5.0 → 1.0.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/.env.example +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +125 -114
- data/CLAUDE.md +385 -0
- data/README.md +629 -244
- data/Rakefile +7 -1
- data/Steepfile +29 -0
- data/docs/implementation/00-overview.md +220 -0
- data/docs/implementation/01-setup_dependencies.md +668 -0
- data/docs/implementation/02-error_handling.md +65 -0
- data/docs/implementation/03-core_components.md +457 -0
- data/docs/implementation/03.5-variable_preservation.md +509 -0
- data/docs/implementation/04-provider_architecture.md +571 -0
- data/docs/implementation/05-translation_logic.md +1065 -0
- data/docs/implementation/06-main_module_api.md +122 -0
- data/docs/implementation/07-direct_translation_helpers.md +582 -0
- data/docs/implementation/08-rails_integration.md +323 -0
- data/docs/implementation/09-testing_suite.md +228 -0
- data/docs/implementation/10-documentation_examples.md +150 -0
- data/docs/implementation/11-quality_security.md +65 -0
- data/docs/implementation/12-cli_standalone.md +698 -0
- data/exe/better_translate +9 -0
- data/lib/better_translate/cache.rb +125 -0
- data/lib/better_translate/cli.rb +304 -0
- data/lib/better_translate/configuration.rb +201 -0
- data/lib/better_translate/direct_translator.rb +131 -0
- data/lib/better_translate/errors.rb +101 -0
- data/lib/better_translate/progress_tracker.rb +157 -0
- data/lib/better_translate/provider_factory.rb +45 -0
- data/lib/better_translate/providers/anthropic_provider.rb +154 -0
- data/lib/better_translate/providers/base_http_provider.rb +239 -0
- data/lib/better_translate/providers/chatgpt_provider.rb +138 -44
- data/lib/better_translate/providers/gemini_provider.rb +123 -61
- data/lib/better_translate/railtie.rb +18 -0
- data/lib/better_translate/rate_limiter.rb +90 -0
- data/lib/better_translate/strategies/base_strategy.rb +58 -0
- data/lib/better_translate/strategies/batch_strategy.rb +56 -0
- data/lib/better_translate/strategies/deep_strategy.rb +45 -0
- data/lib/better_translate/strategies/strategy_selector.rb +43 -0
- data/lib/better_translate/translator.rb +115 -284
- data/lib/better_translate/utils/hash_flattener.rb +104 -0
- data/lib/better_translate/validator.rb +105 -0
- data/lib/better_translate/variable_extractor.rb +259 -0
- data/lib/better_translate/version.rb +2 -9
- data/lib/better_translate/yaml_handler.rb +168 -0
- data/lib/better_translate.rb +97 -73
- data/lib/generators/better_translate/analyze/USAGE +12 -0
- data/lib/generators/better_translate/analyze/analyze_generator.rb +94 -0
- data/lib/generators/better_translate/install/USAGE +13 -0
- data/lib/generators/better_translate/install/install_generator.rb +71 -0
- data/lib/generators/better_translate/install/templates/README +20 -0
- data/lib/generators/better_translate/install/templates/initializer.rb.tt +47 -0
- data/lib/generators/better_translate/translate/USAGE +13 -0
- data/lib/generators/better_translate/translate/translate_generator.rb +114 -0
- data/lib/tasks/better_translate.rake +136 -0
- data/sig/better_translate/cache.rbs +28 -0
- data/sig/better_translate/cli.rbs +24 -0
- data/sig/better_translate/configuration.rbs +78 -0
- data/sig/better_translate/direct_translator.rbs +18 -0
- data/sig/better_translate/errors.rbs +46 -0
- data/sig/better_translate/progress_tracker.rbs +29 -0
- data/sig/better_translate/provider_factory.rbs +8 -0
- data/sig/better_translate/providers/anthropic_provider.rbs +27 -0
- data/sig/better_translate/providers/base_http_provider.rbs +44 -0
- data/sig/better_translate/providers/chatgpt_provider.rbs +25 -0
- data/sig/better_translate/providers/gemini_provider.rbs +22 -0
- data/sig/better_translate/railtie.rbs +7 -0
- data/sig/better_translate/rate_limiter.rbs +20 -0
- data/sig/better_translate/strategies/base_strategy.rbs +19 -0
- data/sig/better_translate/strategies/batch_strategy.rbs +13 -0
- data/sig/better_translate/strategies/deep_strategy.rbs +11 -0
- data/sig/better_translate/strategies/strategy_selector.rbs +10 -0
- data/sig/better_translate/translator.rbs +24 -0
- data/sig/better_translate/utils/hash_flattener.rbs +14 -0
- data/sig/better_translate/validator.rbs +14 -0
- data/sig/better_translate/variable_extractor.rbs +40 -0
- data/sig/better_translate/version.rbs +4 -0
- data/sig/better_translate/yaml_handler.rbs +29 -0
- data/sig/better_translate.rbs +32 -2
- data/sig/faraday.rbs +22 -0
- data/sig/generators/better_translate/analyze/analyze_generator.rbs +18 -0
- data/sig/generators/better_translate/install/install_generator.rbs +14 -0
- data/sig/generators/better_translate/translate/translate_generator.rbs +10 -0
- data/sig/optparse.rbs +9 -0
- data/sig/psych.rbs +5 -0
- data/sig/rails.rbs +34 -0
- metadata +89 -203
- data/lib/better_translate/helper.rb +0 -83
- data/lib/better_translate/providers/base_provider.rb +0 -102
- data/lib/better_translate/service.rb +0 -144
- data/lib/better_translate/similarity_analyzer.rb +0 -218
- data/lib/better_translate/utils.rb +0 -55
- data/lib/better_translate/writer.rb +0 -75
- data/lib/generators/better_translate/analyze_generator.rb +0 -57
- data/lib/generators/better_translate/install_generator.rb +0 -14
- data/lib/generators/better_translate/templates/better_translate.rb +0 -56
- data/lib/generators/better_translate/translate_generator.rb +0 -84
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module BetterTranslate
|
|
6
|
+
module Generators
|
|
7
|
+
# Rails generator for analyzing YAML locale files
|
|
8
|
+
#
|
|
9
|
+
# Provides statistics about translation files: string count, structure, etc.
|
|
10
|
+
#
|
|
11
|
+
# @example Analyze a file
|
|
12
|
+
# rails generate better_translate:analyze config/locales/en.yml
|
|
13
|
+
#
|
|
14
|
+
class AnalyzeGenerator < Rails::Generators::Base
|
|
15
|
+
source_root File.expand_path("templates", __dir__)
|
|
16
|
+
|
|
17
|
+
desc "Analyze YAML locale file structure and statistics"
|
|
18
|
+
|
|
19
|
+
argument :file_path,
|
|
20
|
+
type: :string,
|
|
21
|
+
required: false,
|
|
22
|
+
default: "config/locales/en.yml",
|
|
23
|
+
desc: "Path to YAML file to analyze"
|
|
24
|
+
|
|
25
|
+
# Analyze YAML file
|
|
26
|
+
#
|
|
27
|
+
# @return [void]
|
|
28
|
+
#
|
|
29
|
+
def analyze_file
|
|
30
|
+
full_path = Rails.root.join(file_path)
|
|
31
|
+
|
|
32
|
+
unless File.exist?(full_path)
|
|
33
|
+
say "File not found: #{full_path}", :red
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
say "Analyzing: #{file_path}", :green
|
|
38
|
+
say "=" * 60
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
data = YAML.load_file(full_path)
|
|
42
|
+
|
|
43
|
+
# Flatten to count strings
|
|
44
|
+
flattened = BetterTranslate::Utils::HashFlattener.flatten(data)
|
|
45
|
+
|
|
46
|
+
say ""
|
|
47
|
+
say "Total strings: #{flattened.size}", :cyan
|
|
48
|
+
say ""
|
|
49
|
+
|
|
50
|
+
# Show structure
|
|
51
|
+
say "Structure:", :yellow
|
|
52
|
+
show_structure(data, 0)
|
|
53
|
+
|
|
54
|
+
say ""
|
|
55
|
+
say "=" * 60
|
|
56
|
+
say "Analysis complete", :green
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
say "Error analyzing file: #{e.message}", :red
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Show nested structure
|
|
65
|
+
#
|
|
66
|
+
# @param hash [Hash] Hash to show
|
|
67
|
+
# @param level [Integer] Nesting level
|
|
68
|
+
# @return [void]
|
|
69
|
+
# @api private
|
|
70
|
+
#
|
|
71
|
+
def show_structure(hash, level)
|
|
72
|
+
hash.each do |key, value|
|
|
73
|
+
indent = " " * level
|
|
74
|
+
if value.is_a?(Hash)
|
|
75
|
+
say "#{indent}#{key}/ (#{count_strings(value)} strings)", :white
|
|
76
|
+
show_structure(value, level + 1)
|
|
77
|
+
else
|
|
78
|
+
say "#{indent}#{key}: #{value.to_s[0..50]}#{value.to_s.length > 50 ? "..." : ""}", :white
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Count strings in nested hash
|
|
84
|
+
#
|
|
85
|
+
# @param hash [Hash] Hash to count
|
|
86
|
+
# @return [Integer] String count
|
|
87
|
+
# @api private
|
|
88
|
+
#
|
|
89
|
+
def count_strings(hash)
|
|
90
|
+
BetterTranslate::Utils::HashFlattener.flatten(hash).size
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Installs BetterTranslate configuration files for Rails application.
|
|
3
|
+
|
|
4
|
+
This will create:
|
|
5
|
+
- config/initializers/better_translate.rb (Ruby configuration)
|
|
6
|
+
- config/better_translate.yml (YAML configuration)
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
bin/rails generate better_translate:install
|
|
10
|
+
|
|
11
|
+
This will create:
|
|
12
|
+
config/initializers/better_translate.rb
|
|
13
|
+
config/better_translate.yml
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module BetterTranslate
|
|
6
|
+
module Generators
|
|
7
|
+
# Rails generator for installing BetterTranslate
|
|
8
|
+
#
|
|
9
|
+
# Creates initializer and config files for Rails applications.
|
|
10
|
+
#
|
|
11
|
+
# @example Run generator
|
|
12
|
+
# rails generate better_translate:install
|
|
13
|
+
#
|
|
14
|
+
class InstallGenerator < Rails::Generators::Base
|
|
15
|
+
source_root File.expand_path("templates", __dir__)
|
|
16
|
+
|
|
17
|
+
desc "Creates BetterTranslate initializer and config files"
|
|
18
|
+
|
|
19
|
+
# Create initializer file
|
|
20
|
+
#
|
|
21
|
+
# @return [void]
|
|
22
|
+
#
|
|
23
|
+
def create_initializer_file
|
|
24
|
+
template "initializer.rb.tt", "config/initializers/better_translate.rb"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Create YAML config file
|
|
28
|
+
#
|
|
29
|
+
# @return [void]
|
|
30
|
+
#
|
|
31
|
+
def create_config_file
|
|
32
|
+
sample_config = {
|
|
33
|
+
"provider" => "chatgpt",
|
|
34
|
+
"openai_key" => "YOUR_OPENAI_API_KEY",
|
|
35
|
+
"gemini_key" => "YOUR_GEMINI_API_KEY",
|
|
36
|
+
"anthropic_key" => "YOUR_ANTHROPIC_API_KEY",
|
|
37
|
+
"source_language" => "en",
|
|
38
|
+
"target_languages" => [
|
|
39
|
+
{ "short_name" => "it", "name" => "Italian" },
|
|
40
|
+
{ "short_name" => "es", "name" => "Spanish" },
|
|
41
|
+
{ "short_name" => "fr", "name" => "French" }
|
|
42
|
+
],
|
|
43
|
+
"input_file" => "config/locales/en.yml",
|
|
44
|
+
"output_folder" => "config/locales",
|
|
45
|
+
"verbose" => true,
|
|
46
|
+
"dry_run" => false,
|
|
47
|
+
"translation_mode" => "override",
|
|
48
|
+
"preserve_variables" => true,
|
|
49
|
+
"global_exclusions" => Array.new,
|
|
50
|
+
"exclusions_per_language" => Hash.new,
|
|
51
|
+
"model" => nil,
|
|
52
|
+
"temperature" => 0.3,
|
|
53
|
+
"max_tokens" => 2000,
|
|
54
|
+
"timeout" => 30,
|
|
55
|
+
"max_retries" => 3,
|
|
56
|
+
"rate_limit" => 10
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
create_file "config/better_translate.yml", sample_config.to_yaml
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Show post-install message
|
|
63
|
+
#
|
|
64
|
+
# @return [void]
|
|
65
|
+
#
|
|
66
|
+
def show_readme
|
|
67
|
+
readme "README" if behavior == :invoke
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
BetterTranslate has been installed!
|
|
4
|
+
|
|
5
|
+
Created files:
|
|
6
|
+
• config/initializers/better_translate.rb
|
|
7
|
+
• config/better_translate.yml
|
|
8
|
+
|
|
9
|
+
Next steps:
|
|
10
|
+
|
|
11
|
+
1. Edit config/better_translate.yml and add your API keys
|
|
12
|
+
2. Customize translation settings in config/initializers/better_translate.rb
|
|
13
|
+
3. Run translation:
|
|
14
|
+
|
|
15
|
+
rake better_translate:translate
|
|
16
|
+
|
|
17
|
+
For more information:
|
|
18
|
+
• https://github.com/alessiobussolari/better_translate
|
|
19
|
+
|
|
20
|
+
===============================================================================
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# BetterTranslate Configuration
|
|
4
|
+
#
|
|
5
|
+
# For more configuration options, see config/better_translate.yml
|
|
6
|
+
|
|
7
|
+
BetterTranslate.configure do |config|
|
|
8
|
+
# Provider: :chatgpt, :gemini, or :anthropic
|
|
9
|
+
config.provider = :chatgpt
|
|
10
|
+
|
|
11
|
+
# API Keys (alternatively set via ENV variables)
|
|
12
|
+
config.openai_key = ENV["OPENAI_API_KEY"]
|
|
13
|
+
config.gemini_key = ENV["GEMINI_API_KEY"]
|
|
14
|
+
config.anthropic_key = ENV["ANTHROPIC_API_KEY"]
|
|
15
|
+
|
|
16
|
+
# Source and target languages
|
|
17
|
+
config.source_language = "en"
|
|
18
|
+
config.target_languages = [
|
|
19
|
+
{ short_name: "it", name: "Italian" },
|
|
20
|
+
{ short_name: "es", name: "Spanish" },
|
|
21
|
+
{ short_name: "fr", name: "French" }
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# File paths
|
|
25
|
+
config.input_file = Rails.root.join("config", "locales", "en.yml").to_s
|
|
26
|
+
config.output_folder = Rails.root.join("config", "locales").to_s
|
|
27
|
+
|
|
28
|
+
# Options
|
|
29
|
+
config.verbose = true
|
|
30
|
+
config.dry_run = false # Set to true for testing
|
|
31
|
+
config.translation_mode = :override # or :incremental
|
|
32
|
+
config.preserve_variables = true
|
|
33
|
+
|
|
34
|
+
# Optional: Provider-specific settings
|
|
35
|
+
# config.model = "gpt-5-nano" # ChatGPT model
|
|
36
|
+
# config.temperature = 0.3
|
|
37
|
+
# config.max_tokens = 2000
|
|
38
|
+
# config.timeout = 30
|
|
39
|
+
# config.max_retries = 3
|
|
40
|
+
# config.rate_limit = 10
|
|
41
|
+
|
|
42
|
+
# Optional: Exclusions
|
|
43
|
+
# config.global_exclusions = ["excluded_key", "another_key"]
|
|
44
|
+
# config.exclusions_per_language = {
|
|
45
|
+
# "it" => ["italy_specific_exclusion"]
|
|
46
|
+
# }
|
|
47
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Runs BetterTranslate translation based on existing configuration.
|
|
3
|
+
|
|
4
|
+
This will translate your YAML locale files using the configuration
|
|
5
|
+
in config/better_translate.yml.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
bin/rails generate better_translate:translate
|
|
9
|
+
|
|
10
|
+
This will execute the translation task.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--dry-run Run in dry-run mode (no files will be written)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module BetterTranslate
|
|
6
|
+
module Generators
|
|
7
|
+
# Rails generator for running translations
|
|
8
|
+
#
|
|
9
|
+
# Executes translation based on existing configuration.
|
|
10
|
+
#
|
|
11
|
+
# @example Run generator
|
|
12
|
+
# rails generate better_translate:translate
|
|
13
|
+
#
|
|
14
|
+
# @example Run with dry-run
|
|
15
|
+
# rails generate better_translate:translate --dry-run
|
|
16
|
+
#
|
|
17
|
+
class TranslateGenerator < Rails::Generators::Base
|
|
18
|
+
source_root File.expand_path("templates", __dir__)
|
|
19
|
+
|
|
20
|
+
desc "Run BetterTranslate translation task"
|
|
21
|
+
|
|
22
|
+
class_option :dry_run,
|
|
23
|
+
type: :boolean,
|
|
24
|
+
default: false,
|
|
25
|
+
desc: "Run in dry-run mode (no files written)"
|
|
26
|
+
|
|
27
|
+
# Run translation
|
|
28
|
+
#
|
|
29
|
+
# @return [void]
|
|
30
|
+
#
|
|
31
|
+
def run_translation
|
|
32
|
+
config_file = Rails.root.join("config", "better_translate.yml")
|
|
33
|
+
|
|
34
|
+
unless File.exist?(config_file)
|
|
35
|
+
say "Config file not found at #{config_file}", :red
|
|
36
|
+
say "Run 'rails generate better_translate:install' first"
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Load configuration from YAML
|
|
41
|
+
yaml_config = YAML.load_file(config_file)
|
|
42
|
+
|
|
43
|
+
# Configure BetterTranslate
|
|
44
|
+
BetterTranslate.configure do |config|
|
|
45
|
+
config.provider = yaml_config["provider"]&.to_sym
|
|
46
|
+
config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
|
|
47
|
+
config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
|
|
48
|
+
config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
|
|
49
|
+
|
|
50
|
+
config.source_language = yaml_config["source_language"]
|
|
51
|
+
config.target_languages = yaml_config["target_languages"]&.map do |lang|
|
|
52
|
+
if lang.is_a?(Hash)
|
|
53
|
+
{ short_name: lang["short_name"], name: lang["name"] }
|
|
54
|
+
else
|
|
55
|
+
lang
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
config.input_file = Rails.root.join(yaml_config["input_file"]).to_s
|
|
60
|
+
config.output_folder = Rails.root.join(yaml_config["output_folder"]).to_s
|
|
61
|
+
config.verbose = yaml_config.fetch("verbose", true)
|
|
62
|
+
config.dry_run = options[:dry_run] || yaml_config.fetch("dry_run", false)
|
|
63
|
+
|
|
64
|
+
# Map "full" to :override for backward compatibility
|
|
65
|
+
translation_mode = yaml_config.fetch("translation_mode", "override")
|
|
66
|
+
translation_mode = "override" if translation_mode == "full"
|
|
67
|
+
config.translation_mode = translation_mode.to_sym
|
|
68
|
+
|
|
69
|
+
config.preserve_variables = yaml_config.fetch("preserve_variables", true)
|
|
70
|
+
|
|
71
|
+
# Exclusions
|
|
72
|
+
config.global_exclusions = yaml_config["global_exclusions"] || []
|
|
73
|
+
config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
|
|
74
|
+
|
|
75
|
+
# Provider options
|
|
76
|
+
config.model = yaml_config["model"] if yaml_config["model"]
|
|
77
|
+
config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
|
|
78
|
+
config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
|
|
79
|
+
config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
|
|
80
|
+
config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
|
|
81
|
+
config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Perform translation
|
|
85
|
+
say "Starting translation...", :green
|
|
86
|
+
say "Provider: #{BetterTranslate.configuration.provider}"
|
|
87
|
+
say "Source: #{BetterTranslate.configuration.source_language}"
|
|
88
|
+
say "Targets: #{BetterTranslate.configuration.target_languages.map { |l| l[:name] }.join(", ")}"
|
|
89
|
+
|
|
90
|
+
say "DRY RUN MODE - No files will be written", :yellow if options[:dry_run]
|
|
91
|
+
|
|
92
|
+
say ""
|
|
93
|
+
|
|
94
|
+
results = BetterTranslate.translate_files
|
|
95
|
+
|
|
96
|
+
# Report results
|
|
97
|
+
say ""
|
|
98
|
+
say "=" * 60
|
|
99
|
+
say "Translation Complete", :green
|
|
100
|
+
say "=" * 60
|
|
101
|
+
say "Success: #{results[:success_count]}", :green
|
|
102
|
+
say "Failure: #{results[:failure_count]}", results[:failure_count].zero? ? :green : :red
|
|
103
|
+
|
|
104
|
+
return unless results[:errors].any?
|
|
105
|
+
|
|
106
|
+
say ""
|
|
107
|
+
say "Errors:", :red
|
|
108
|
+
results[:errors].each do |error|
|
|
109
|
+
say " - #{error[:language]}: #{error[:error]}", :red
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
# Define :environment task as no-op if it doesn't exist (for non-Rails usage)
|
|
6
|
+
unless Rake::Task.task_defined?(:environment)
|
|
7
|
+
task :environment do
|
|
8
|
+
# No-op for standalone usage
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
namespace :better_translate do
|
|
13
|
+
desc "Translate YAML locale files using AI providers"
|
|
14
|
+
task translate: :environment do
|
|
15
|
+
config_file = Rails.root.join("config", "better_translate.yml")
|
|
16
|
+
|
|
17
|
+
unless File.exist?(config_file)
|
|
18
|
+
puts "Error: Configuration file not found at #{config_file}"
|
|
19
|
+
puts "Run 'rake better_translate:config:generate' to create one."
|
|
20
|
+
exit 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Load configuration from YAML
|
|
24
|
+
yaml_config = YAML.load_file(config_file)
|
|
25
|
+
|
|
26
|
+
# Configure BetterTranslate
|
|
27
|
+
BetterTranslate.configure do |config|
|
|
28
|
+
config.provider = yaml_config["provider"]&.to_sym
|
|
29
|
+
config.openai_key = yaml_config["openai_key"] || ENV["OPENAI_API_KEY"]
|
|
30
|
+
config.gemini_key = yaml_config["gemini_key"] || ENV["GEMINI_API_KEY"]
|
|
31
|
+
config.anthropic_key = yaml_config["anthropic_key"] || ENV["ANTHROPIC_API_KEY"]
|
|
32
|
+
|
|
33
|
+
config.source_language = yaml_config["source_language"]
|
|
34
|
+
config.target_languages = yaml_config["target_languages"]&.map do |lang|
|
|
35
|
+
if lang.is_a?(Hash)
|
|
36
|
+
{ short_name: lang["short_name"], name: lang["name"] }
|
|
37
|
+
else
|
|
38
|
+
lang
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
config.input_file = yaml_config["input_file"]
|
|
43
|
+
config.output_folder = yaml_config["output_folder"]
|
|
44
|
+
config.verbose = yaml_config.fetch("verbose", true)
|
|
45
|
+
config.dry_run = yaml_config.fetch("dry_run", false)
|
|
46
|
+
|
|
47
|
+
# Map "full" to :override for backward compatibility
|
|
48
|
+
translation_mode = yaml_config.fetch("translation_mode", "override")
|
|
49
|
+
translation_mode = "override" if translation_mode == "full"
|
|
50
|
+
config.translation_mode = translation_mode.to_sym
|
|
51
|
+
|
|
52
|
+
config.preserve_variables = yaml_config.fetch("preserve_variables", true)
|
|
53
|
+
|
|
54
|
+
# Exclusions
|
|
55
|
+
config.global_exclusions = yaml_config["global_exclusions"] || []
|
|
56
|
+
config.exclusions_per_language = yaml_config["exclusions_per_language"] || {}
|
|
57
|
+
|
|
58
|
+
# Provider options
|
|
59
|
+
config.model = yaml_config["model"] if yaml_config["model"]
|
|
60
|
+
config.temperature = yaml_config["temperature"] if yaml_config["temperature"]
|
|
61
|
+
config.max_tokens = yaml_config["max_tokens"] if yaml_config["max_tokens"]
|
|
62
|
+
config.timeout = yaml_config["timeout"] if yaml_config["timeout"]
|
|
63
|
+
config.max_retries = yaml_config["max_retries"] if yaml_config["max_retries"]
|
|
64
|
+
config.rate_limit = yaml_config["rate_limit"] if yaml_config["rate_limit"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Perform translation
|
|
68
|
+
puts "Starting translation..."
|
|
69
|
+
puts "Provider: #{BetterTranslate.configuration.provider}"
|
|
70
|
+
puts "Source: #{BetterTranslate.configuration.source_language}"
|
|
71
|
+
puts "Targets: #{BetterTranslate.configuration.target_languages.map { |l| l[:name] }.join(", ")}"
|
|
72
|
+
puts
|
|
73
|
+
|
|
74
|
+
results = BetterTranslate.translate_files
|
|
75
|
+
|
|
76
|
+
# Report results
|
|
77
|
+
puts
|
|
78
|
+
puts "=" * 60
|
|
79
|
+
puts "Translation Complete"
|
|
80
|
+
puts "=" * 60
|
|
81
|
+
puts "Success: #{results[:success_count]}"
|
|
82
|
+
puts "Failure: #{results[:failure_count]}"
|
|
83
|
+
|
|
84
|
+
if results[:errors].any?
|
|
85
|
+
puts
|
|
86
|
+
puts "Errors:"
|
|
87
|
+
results[:errors].each do |error|
|
|
88
|
+
puts " - #{error[:language]}: #{error[:error]}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
namespace :config do
|
|
94
|
+
desc "Generate sample configuration file"
|
|
95
|
+
task generate: :environment do
|
|
96
|
+
config_file = Rails.root.join("config", "better_translate.yml")
|
|
97
|
+
|
|
98
|
+
if File.exist?(config_file)
|
|
99
|
+
puts "Configuration file already exists at #{config_file}"
|
|
100
|
+
puts "Delete it first if you want to regenerate."
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sample_config = {
|
|
105
|
+
"provider" => "chatgpt",
|
|
106
|
+
"openai_key" => "YOUR_OPENAI_API_KEY",
|
|
107
|
+
"gemini_key" => "YOUR_GEMINI_API_KEY",
|
|
108
|
+
"anthropic_key" => "YOUR_ANTHROPIC_API_KEY",
|
|
109
|
+
"source_language" => "en",
|
|
110
|
+
"target_languages" => [
|
|
111
|
+
{ "short_name" => "it", "name" => "Italian" },
|
|
112
|
+
{ "short_name" => "es", "name" => "Spanish" },
|
|
113
|
+
{ "short_name" => "fr", "name" => "French" }
|
|
114
|
+
],
|
|
115
|
+
"input_file" => "config/locales/en.yml",
|
|
116
|
+
"output_folder" => "config/locales",
|
|
117
|
+
"verbose" => true,
|
|
118
|
+
"dry_run" => false,
|
|
119
|
+
"translation_mode" => "full",
|
|
120
|
+
"preserve_variables" => true,
|
|
121
|
+
"global_exclusions" => [],
|
|
122
|
+
"exclusions_per_language" => {},
|
|
123
|
+
"model" => nil,
|
|
124
|
+
"temperature" => 0.3,
|
|
125
|
+
"max_tokens" => 2000,
|
|
126
|
+
"timeout" => 30,
|
|
127
|
+
"max_retries" => 3,
|
|
128
|
+
"rate_limit" => 10
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
File.write(config_file, sample_config.to_yaml)
|
|
132
|
+
puts "Generated configuration file at #{config_file}"
|
|
133
|
+
puts "Please edit it with your API keys and preferences."
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module BetterTranslate
|
|
2
|
+
# LRU (Least Recently Used) Cache implementation
|
|
3
|
+
#
|
|
4
|
+
# Thread-safe cache with configurable capacity and optional TTL.
|
|
5
|
+
class Cache
|
|
6
|
+
type cache_entry = { value: String, timestamp: Time }
|
|
7
|
+
|
|
8
|
+
@capacity: Integer
|
|
9
|
+
@ttl: Integer?
|
|
10
|
+
@cache: Hash[String, cache_entry]
|
|
11
|
+
@mutex: Thread::Mutex
|
|
12
|
+
|
|
13
|
+
attr_reader capacity: Integer
|
|
14
|
+
attr_reader ttl: Integer?
|
|
15
|
+
|
|
16
|
+
def initialize: (?capacity: Integer, ?ttl: Integer?) -> void
|
|
17
|
+
|
|
18
|
+
def get: (String key) -> String?
|
|
19
|
+
|
|
20
|
+
def set: (String key, String value) -> String
|
|
21
|
+
|
|
22
|
+
def clear: () -> void
|
|
23
|
+
|
|
24
|
+
def size: () -> Integer
|
|
25
|
+
|
|
26
|
+
def key?: (String key) -> bool
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module BetterTranslate
|
|
2
|
+
# Command-line interface for BetterTranslate
|
|
3
|
+
#
|
|
4
|
+
# Provides standalone CLI commands for translation without Rails.
|
|
5
|
+
class CLI
|
|
6
|
+
@args: Array[String]
|
|
7
|
+
|
|
8
|
+
attr_reader args: Array[String]
|
|
9
|
+
|
|
10
|
+
def initialize: (Array[String] args) -> void
|
|
11
|
+
|
|
12
|
+
def run: () -> void
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def show_help: () -> void
|
|
17
|
+
|
|
18
|
+
def run_translate: () -> void
|
|
19
|
+
|
|
20
|
+
def run_generate: () -> void
|
|
21
|
+
|
|
22
|
+
def run_direct: () -> void
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module BetterTranslate
|
|
2
|
+
# Configuration class for BetterTranslate
|
|
3
|
+
#
|
|
4
|
+
# Manages all configuration options with type safety and validation.
|
|
5
|
+
class Configuration
|
|
6
|
+
type target_language = { short_name: String, name: String }
|
|
7
|
+
|
|
8
|
+
@provider: Symbol?
|
|
9
|
+
@openai_key: String?
|
|
10
|
+
@google_gemini_key: String?
|
|
11
|
+
@anthropic_key: String?
|
|
12
|
+
@source_language: String?
|
|
13
|
+
@target_languages: Array[target_language]
|
|
14
|
+
@input_file: String?
|
|
15
|
+
@output_folder: String?
|
|
16
|
+
@translation_mode: Symbol
|
|
17
|
+
@translation_context: String?
|
|
18
|
+
@max_concurrent_requests: Integer
|
|
19
|
+
@request_timeout: Integer
|
|
20
|
+
@max_retries: Integer
|
|
21
|
+
@retry_delay: Float
|
|
22
|
+
@cache_enabled: bool
|
|
23
|
+
@cache_size: Integer
|
|
24
|
+
@cache_ttl: Integer?
|
|
25
|
+
@verbose: bool
|
|
26
|
+
@dry_run: bool
|
|
27
|
+
@global_exclusions: Array[String]
|
|
28
|
+
@exclusions_per_language: Hash[String, Array[String]]
|
|
29
|
+
@preserve_variables: bool
|
|
30
|
+
|
|
31
|
+
attr_accessor provider: (Symbol | nil)
|
|
32
|
+
attr_accessor openai_key: String?
|
|
33
|
+
attr_accessor google_gemini_key: String?
|
|
34
|
+
attr_accessor gemini_key: String?
|
|
35
|
+
attr_accessor anthropic_key: String?
|
|
36
|
+
attr_accessor source_language: String?
|
|
37
|
+
attr_accessor target_languages: Array[target_language]
|
|
38
|
+
attr_accessor input_file: String?
|
|
39
|
+
attr_accessor output_folder: String?
|
|
40
|
+
attr_accessor translation_mode: Symbol
|
|
41
|
+
attr_accessor translation_context: String?
|
|
42
|
+
attr_accessor max_concurrent_requests: Integer
|
|
43
|
+
attr_accessor request_timeout: Integer
|
|
44
|
+
attr_accessor max_retries: Integer
|
|
45
|
+
attr_accessor retry_delay: Float
|
|
46
|
+
attr_accessor cache_enabled: bool
|
|
47
|
+
attr_accessor cache_size: Integer
|
|
48
|
+
attr_accessor cache_ttl: Integer?
|
|
49
|
+
attr_accessor verbose: bool
|
|
50
|
+
attr_accessor dry_run: bool
|
|
51
|
+
attr_accessor global_exclusions: Array[String]
|
|
52
|
+
attr_accessor exclusions_per_language: Hash[String, Array[String]]
|
|
53
|
+
attr_accessor preserve_variables: bool
|
|
54
|
+
|
|
55
|
+
# Provider-specific options
|
|
56
|
+
attr_accessor model: String?
|
|
57
|
+
attr_accessor temperature: Float?
|
|
58
|
+
attr_accessor max_tokens: Integer?
|
|
59
|
+
attr_accessor timeout: Integer?
|
|
60
|
+
attr_accessor rate_limit: (Integer | Float)?
|
|
61
|
+
|
|
62
|
+
def initialize: () -> void
|
|
63
|
+
|
|
64
|
+
def validate!: () -> true
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def validate_provider!: () -> void
|
|
69
|
+
|
|
70
|
+
def validate_api_keys!: () -> void
|
|
71
|
+
|
|
72
|
+
def validate_languages!: () -> void
|
|
73
|
+
|
|
74
|
+
def validate_files!: () -> void
|
|
75
|
+
|
|
76
|
+
def validate_optional_settings!: () -> void
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module BetterTranslate
|
|
2
|
+
# Direct text translator for non-YAML workflows
|
|
3
|
+
#
|
|
4
|
+
# Provides convenience methods for translating individual strings or batches
|
|
5
|
+
# without requiring YAML files. Useful for runtime translation needs.
|
|
6
|
+
class DirectTranslator
|
|
7
|
+
@config: Configuration
|
|
8
|
+
@provider: Providers::BaseHttpProvider
|
|
9
|
+
|
|
10
|
+
attr_reader config: Configuration
|
|
11
|
+
|
|
12
|
+
def initialize: (Configuration config) -> void
|
|
13
|
+
|
|
14
|
+
def translate: (String text, to: (String | Symbol), language_name: String) -> String
|
|
15
|
+
|
|
16
|
+
def translate_batch: (Array[String] texts, to: (String | Symbol), language_name: String, ?skip_errors: bool) -> Array[String?]
|
|
17
|
+
end
|
|
18
|
+
end
|