better_translate 0.2.0 → 0.3.1

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.
@@ -2,65 +2,75 @@ module BetterTranslate
2
2
  class Translator
3
3
  class << self
4
4
  def work
5
- puts "Avvio della traduzione dei file..."
5
+ message = "Starting file translation..."
6
+ BetterTranslate::Utils.logger(message: message)
6
7
 
7
8
  translations = read_yml_source
8
9
 
9
- # Rimuove le chiavi da escludere (global_exclusions) dalla struttura letta
10
+ # Removes the keys to exclude (global_exclusions) from the read structure
10
11
  global_filtered_translations = remove_exclusions(
11
12
  translations, BetterTranslate.configuration.global_exclusions
12
13
  )
13
14
 
14
- BetterTranslate.configuration.target_languages.each do |target_lang|
15
+ start_time = Time.now
16
+ threads = BetterTranslate.configuration.target_languages.map do |target_lang|
17
+ Thread.new do
15
18
 
16
- # Fase 2: Applica il filtro specifico per la lingua target
19
+ # Phase 2: Apply the target language specific filter
17
20
  lang_exclusions = BetterTranslate.configuration.exclusions_per_language[target_lang[:short_name]] || []
18
21
  filtered_translations = remove_exclusions(
19
22
  global_filtered_translations, lang_exclusions
20
23
  )
21
24
 
22
- puts "Inizio traduzione da #{BetterTranslate.configuration.source_language} a #{target_lang[:short_name]}"
25
+ message = "Starting translation from #{BetterTranslate.configuration.source_language} to #{target_lang[:short_name]}"
26
+ BetterTranslate::Utils.logger(message: message)
23
27
  service = BetterTranslate::Service.new
24
28
  translated_data = translate_with_progress(filtered_translations, service, target_lang[:short_name], target_lang[:name])
25
29
  BetterTranslate::Writer.write_translations(translated_data, target_lang[:short_name])
26
- puts "Traduzione completata da #{BetterTranslate.configuration.source_language} a #{target_lang[:short_name]}"
30
+ end_time = Time.now
31
+ duration = end_time - start_time
32
+ BetterTranslate::Utils.track_metric("translation_duration", duration)
33
+
34
+ message = "Translation completed from #{BetterTranslate.configuration.source_language} to #{target_lang[:short_name]} in #{duration.round(2)} seconds"
35
+ BetterTranslate::Utils.logger(message: message)
36
+ end
27
37
  end
28
38
  end
29
39
 
30
40
  private
31
41
 
32
- # Legge il file YAML in base al percorso fornito.
42
+ # Reads the YAML file based on the provided path.
33
43
  #
34
- # @param file_path [String] percorso del file YAML da leggere
35
- # @return [Hash] struttura dati contenente le traduzioni
36
- # @raise [StandardError] se il file non esiste
44
+ # @param file_path [String] path of the YAML file to read
45
+ # @return [Hash] data structure containing the translations
46
+ # @raise [StandardError] if the file does not exist
37
47
  def read_yml_source
38
48
  file_path = BetterTranslate.configuration.input_file
39
49
  unless File.exist?(file_path)
40
- raise "File non trovato: #{file_path}"
50
+ raise "File not found: #{file_path}"
41
51
  end
42
52
 
43
53
  YAML.load_file(file_path)
44
54
  end
45
55
 
46
- # Rimuove le chiavi globali da escludere dalla struttura dati,
47
- # calcolando i percorsi a partire dal contenuto della lingua di partenza.
56
+ # Removes the global keys to exclude from the data structure,
57
+ # calculating paths starting from the source language content.
48
58
  #
49
- # Ad esempio, se il file YAML è:
59
+ # For example, if the YAML file is:
50
60
  # { "en" => { "sample" => { "valid" => "valid", "excluded" => "Excluded" } } }
51
- # e global_exclusions = ["sample.excluded"],
52
- # il risultato sarà:
61
+ # and global_exclusions = ["sample.excluded"],
62
+ # the result will be:
53
63
  # { "en" => { "sample" => { "valid" => "valid" } } }
54
64
  #
55
- # @param data [Hash, Array, Object] La struttura dati da filtrare.
56
- # @param global_exclusions [Array<String>] Lista dei percorsi (in dot notation) da escludere globalmente.
57
- # @param current_path [Array] Il percorso corrente (usato in maniera ricorsiva, default: []).
58
- # @return [Hash, Array, Object] La struttura dati filtrata.
65
+ # @param data [Hash, Array, Object] The data structure to filter.
66
+ # @param global_exclusions [Array<String>] List of paths (in dot notation) to exclude globally.
67
+ # @param current_path [Array] The current path (used recursively, default: []).
68
+ # @return [Hash, Array, Object] The filtered data structure.
59
69
  def remove_exclusions(data, exclusion_list, current_path = [])
60
70
  if data.is_a?(Hash)
61
71
  data.each_with_object({}) do |(key, value), result|
62
- # Se siamo al livello top-level e la chiave corrisponde alla lingua di partenza,
63
- # resettare il percorso (così da escludere "en" dal percorso finale)
72
+ # If we are at the top-level and the key matches the source language,
73
+ # reset the path (to exclude "en" from the final path)
64
74
  new_path = if current_path.empty? && key == BetterTranslate.configuration.source_language
65
75
  []
66
76
  else
@@ -81,14 +91,14 @@ module BetterTranslate
81
91
  end
82
92
  end
83
93
 
84
- # Metodo ricorsivo che percorre la struttura, traducendo ogni stringa e aggiornando il progresso.
94
+ # Recursive method that traverses the structure, translating each string and updating progress.
85
95
  #
86
- # @param data [Hash, Array, String] La struttura dati da tradurre.
87
- # @param provider [Object] Il provider che risponde al metodo translate.
88
- # @param target_lang_code [String] Codice della lingua target (es. "en").
89
- # @param target_lang_name [String] Nome della lingua target (es. "English").
90
- # @param progress [Hash] Un hash con le chiavi :count e :total per monitorare il progresso.
91
- # @return [Hash, Array, String] La struttura tradotta.
96
+ # @param data [Hash, Array, String] The data structure to translate.
97
+ # @param provider [Object] The provider that responds to the translate method.
98
+ # @param target_lang_code [String] Target language code (e.g. "en").
99
+ # @param target_lang_name [String] Target language name (e.g. "English").
100
+ # @param progress [Hash] A hash with :count and :total keys to monitor progress.
101
+ # @return [Hash, Array, String] The translated structure.
92
102
  def deep_translate_with_progress(data, service, target_lang_code, target_lang_name, progress)
93
103
  if data.is_a?(Hash)
94
104
  data.each_with_object({}) do |(key, value), result|
@@ -106,20 +116,90 @@ module BetterTranslate
106
116
  end
107
117
  end
108
118
 
109
- # Metodo principale per tradurre l'intera struttura dati, con monitoraggio del progresso.
119
+ # Main method to translate the entire data structure, with progress monitoring.
110
120
  #
111
- # @param data [Hash, Array, String] la struttura dati da tradurre
112
- # @param provider [Object] il provider da usare per tradurre (deve implementare translate)
113
- # @param target_lang_code [String] codice della lingua target
114
- # @param target_lang_name [String] nome della lingua target
115
- # @return la struttura tradotta
121
+ # @param data [Hash, Array, String] the data structure to translate
122
+ # @param provider [Object] the provider to use for translation (must implement translate)
123
+ # @param target_lang_code [String] target language code
124
+ # @param target_lang_name [String] target language name
125
+ # @return the translated structure
116
126
  def translate_with_progress(data, service, target_lang_code, target_lang_name)
117
127
  total = count_strings(data)
118
128
  progress = ProgressBar.create(total: total, format: '%a %B %p%% %t')
119
- deep_translate_with_progress(data, service, target_lang_code, target_lang_name, progress)
129
+
130
+ start_time = Time.now
131
+ result = if total > 50 # Usa il batch processing per dataset grandi
132
+ batch_translate_with_progress(data, service, target_lang_code, target_lang_name, progress)
133
+ else
134
+ deep_translate_with_progress(data, service, target_lang_code, target_lang_name, progress)
135
+ end
136
+
137
+ duration = Time.now - start_time
138
+ BetterTranslate::Utils.track_metric("translation_method_duration", {
139
+ method: total > 50 ? 'batch' : 'deep',
140
+ duration: duration,
141
+ total_strings: total
142
+ })
143
+
144
+ result
145
+ end
146
+
147
+ def batch_translate_with_progress(data, service, target_lang_code, target_lang_name, progress)
148
+ texts = extract_translatable_texts(data)
149
+ translations = {}
150
+
151
+ texts.each_slice(10).each_with_index do |batch, index|
152
+ batch_start = Time.now
153
+
154
+ batch_translations = batch.map do |text|
155
+ translated = service.translate(text, target_lang_code, target_lang_name)
156
+ progress.increment
157
+ [text, translated]
158
+ end.to_h
159
+
160
+ translations.merge!(batch_translations)
161
+
162
+ batch_duration = Time.now - batch_start
163
+ BetterTranslate::Utils.track_metric("batch_translation_duration", {
164
+ batch_number: index + 1,
165
+ size: batch.size,
166
+ duration: batch_duration
167
+ })
168
+ end
169
+
170
+ replace_translations(data, translations)
171
+ end
172
+
173
+ def extract_translatable_texts(data)
174
+ texts = Set.new
175
+ traverse_structure(data) do |value|
176
+ texts.add(value) if value.is_a?(String) && !value.strip.empty?
177
+ end
178
+ texts.to_a
179
+ end
180
+
181
+ def replace_translations(data, translations)
182
+ traverse_structure(data) do |value|
183
+ if value.is_a?(String) && !value.strip.empty? && translations.key?(value)
184
+ translations[value]
185
+ else
186
+ value
187
+ end
188
+ end
189
+ end
190
+
191
+ def traverse_structure(data, &block)
192
+ case data
193
+ when Hash
194
+ data.transform_values { |v| traverse_structure(v, &block) }
195
+ when Array
196
+ data.map { |v| traverse_structure(v, &block) }
197
+ else
198
+ yield data
199
+ end
120
200
  end
121
201
 
122
- # Conta ricorsivamente il numero di stringhe traducibili nella struttura dati.
202
+ # Recursively counts the number of translatable strings in the data structure.
123
203
  def count_strings(data)
124
204
  if data.is_a?(Hash)
125
205
  data.values.sum { |v| count_strings(v) }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/better_seeder/utils.rb
4
+ #
5
+ # = BetterSeeder::Utils
6
+ #
7
+ # This module provides utility methods for seed management. In particular,
8
+ # allows transforming class names into snake_case format with the "_structure.rb" suffix,
9
+ # manage log messages and configure the logger level for ActiveRecord.
10
+
11
+ module BetterTranslate
12
+ class Utils
13
+ class << self
14
+ ##
15
+ # Logs a message using the Rails logger if available, otherwise prints it to standard output.
16
+ #
17
+ # ==== Parametri
18
+ # * +message+ - The message to log (can be a string or nil).
19
+ #
20
+ # ==== Ritorno
21
+ # Does not return a significant value.
22
+ #
23
+ def logger(message: nil)
24
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
25
+ Rails.logger.info message
26
+ else
27
+ puts message
28
+ end
29
+ end
30
+
31
+ def track_metric(name, value)
32
+ @metrics ||= {}
33
+ @metrics[name] ||= []
34
+ @metrics[name] << { value: value, timestamp: Time.now }
35
+
36
+ # Mantieni solo le ultime 1000 metriche per tipo
37
+ @metrics[name] = @metrics[name].last(1000) if @metrics[name].size > 1000
38
+ end
39
+
40
+ def get_metrics
41
+ @metrics || {}
42
+ end
43
+
44
+ def clear_metrics
45
+ @metrics = {}
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BetterTranslate
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -1,19 +1,19 @@
1
1
  module BetterTranslate
2
2
  class Writer
3
3
  class << self
4
- # Scrive il file di traduzione per una lingua target.
5
- # Se il metodo di traduzione è :override, il file viene riscritto da zero;
6
- # se è :incremental, il file esistente viene aggiornato inserendo le nuove traduzioni nelle posizioni corrette.
4
+ # Writes the translation file for a target language.
5
+ # If the translation method is :override, the file is rewritten from scratch;
6
+ # if it's :incremental, the existing file is updated by inserting new translations in the correct positions.
7
7
  #
8
- # @param translated_data [Hash] La struttura dati tradotta (ad es. { "sample" => { "valid" => "tradotto", "new_key" => "nuova traduzione" } }).
9
- # @param target_lang_code [String] Codice della lingua target (es. "ru").
8
+ # @param translated_data [Hash] The translated data structure (e.g. { "sample" => { "valid" => "translated", "new_key" => "new translation" } }).
9
+ # @param target_lang_code [String] Target language code (e.g. "ru").
10
10
  def write_translations(translated_data, target_lang_code)
11
11
  output_folder = BetterTranslate.configuration.output_folder
12
12
  output_file = File.join(output_folder, "#{target_lang_code}.yml")
13
13
 
14
- # Riformatta la struttura per utilizzare il codice target come chiave principale.
14
+ # Reformats the structure to use the target code as the main key.
15
15
  output_content = if translated_data.is_a?(Hash) && translated_data.key?(BetterTranslate.configuration.source_language)
16
- # Sostituisce la chiave della lingua di partenza con quella target.
16
+ # Replaces the source language key with the target one.
17
17
  { target_lang_code => translated_data[BetterTranslate.configuration.source_language] }
18
18
  else
19
19
  { target_lang_code => translated_data }
@@ -23,11 +23,13 @@ module BetterTranslate
23
23
  existing_data = YAML.load_file(output_file)
24
24
  merged = deep_merge(existing_data, output_content)
25
25
  File.write(output_file, merged.to_yaml(line_width: -1))
26
- puts "File aggiornato in modalità incremental: #{output_file}"
26
+ message = "File updated in incremental mode: #{output_file}"
27
+ BetterTranslate::Utils.logger(message: message)
27
28
  else
28
29
  FileUtils.mkdir_p(output_folder) unless Dir.exist?(output_folder)
29
30
  File.write(output_file, output_content.to_yaml(line_width: -1))
30
- puts "File riscritto in modalità override: #{output_file}"
31
+ message = "File rewritten in override mode: #{output_file}"
32
+ BetterTranslate::Utils.logger(message: message)
31
33
  end
32
34
  end
33
35
 
@@ -35,12 +37,12 @@ module BetterTranslate
35
37
 
36
38
  # Metodo di deep merge: unisce in modo ricorsivo i due hash.
37
39
  # Se una chiave esiste in entrambi gli hash e i valori sono hash, li unisce ricorsivamente.
38
- # Altrimenti, se la chiave esiste già nell'hash esistente, la mantiene e non la sovrascrive.
40
+ # Otherwise, if the key already exists in the existing hash, it keeps it and doesn't overwrite it.
39
41
  # Se la chiave non esiste, la aggiunge.
40
42
  #
41
- # @param existing [Hash] hash esistente (file corrente)
42
- # @param new_data [Hash] nuovo hash con le traduzioni da unire
43
- # @return [Hash] hash unito
43
+ # @param existing [Hash] existing hash (current file)
44
+ # @param new_data [Hash] new hash with translations to merge
45
+ # @return [Hash] merged hash
44
46
  def deep_merge(existing, new_data)
45
47
  merged = existing.dup
46
48
  new_data.each do |key, value|
@@ -48,8 +50,8 @@ module BetterTranslate
48
50
  if merged[key].is_a?(Hash) && value.is_a?(Hash)
49
51
  merged[key] = deep_merge(merged[key], value)
50
52
  else
51
- # Se la chiave esiste già, non sovrascrivo il valore (modalità incrementale)
52
- # oppure potresti decidere di aggiornare comunque, a seconda delle esigenze.
53
+ # If the key already exists, don't overwrite the value (incremental mode)
54
+ # or you might decide to update anyway, depending on the requirements.
53
55
  merged[key] = merged[key]
54
56
  end
55
57
  else
@@ -2,49 +2,58 @@
2
2
  require "ruby-progressbar"
3
3
 
4
4
  require "better_translate/version"
5
+ require "better_translate/utils"
5
6
  require "better_translate/translator"
6
7
  require "better_translate/service"
7
8
  require "better_translate/writer"
9
+ require "better_translate/helper"
8
10
 
9
11
  require 'better_translate/providers/base_provider'
10
12
  require 'better_translate/providers/chatgpt_provider'
11
13
  require 'better_translate/providers/gemini_provider'
12
14
 
15
+ require 'ostruct'
16
+
13
17
  module BetterTranslate
14
18
  class << self
15
19
  attr_accessor :configuration
16
20
 
17
- # Metodo per configurare la gemma
21
+ # Method to configure the gem
18
22
  def configure
19
23
  self.configuration ||= OpenStruct.new
20
24
  yield(configuration) if block_given?
21
25
  end
22
26
 
23
- # Metodo install per generare il file di configurazione (initializer)
27
+ # Install method to generate the configuration file (initializer)
24
28
  def install
25
29
  unless defined?(Rails) && Rails.respond_to?(:root)
26
- puts "Il metodo install è disponibile solo in un'applicazione Rails."
30
+ message = "The install method is only available in a Rails application."
31
+ BetterTranslate::Utils.logger(message: message)
27
32
  return
28
33
  end
29
34
 
30
- # Costruisce il percorso della cartella template all'interno della gemma
35
+ # Builds the path to the template folder inside the gem
31
36
  source = File.expand_path("../generators/better_translate/templates/better_translate.rb", __dir__)
32
37
  destination = File.join(Rails.root, "config", "initializers", "better_translate.rb")
33
38
 
34
39
  if File.exist?(destination)
35
- puts "Il file initializer esiste già: #{destination}"
40
+ message = "The initializer file already exists: #{destination}"
41
+ BetterTranslate::Utils.logger(message: message)
36
42
  else
37
43
  FileUtils.mkdir_p(File.dirname(destination))
38
44
  FileUtils.cp(source, destination)
39
- puts "Initializer creato in: #{destination}"
45
+ message = "The initializer file already exists: #{destination}"
46
+ BetterTranslate::Utils.logger(message: message)
40
47
  end
41
48
  end
42
49
 
43
50
  def magic
44
- puts "Metodo magic invocato: eseguirò la traduzione dei file..."
51
+ message = "Magic method invoked: Translation will begin..."
52
+ BetterTranslate::Utils.logger(message: message)
45
53
 
46
54
  BetterTranslate::Translator.work
47
- puts "Metodo magic invocato: Traduzione completata con successo!"
55
+ message = "Magic method invoked: Translation completed successfully!"
56
+ BetterTranslate::Utils.logger(message: message)
48
57
  end
49
58
 
50
59
  end
@@ -4,7 +4,7 @@ module BetterTranslate
4
4
  module Generators
5
5
  class InstallGenerator < Rails::Generators::Base
6
6
  source_root File.expand_path('templates', __dir__)
7
- desc "Genera il file config/initializers/better_translate.rb con la configurazione di default"
7
+ desc "Generates the config/initializers/better_translate.rb file with default configuration"
8
8
 
9
9
  def copy_initializer
10
10
  template "better_translate.rb", "config/initializers/better_translate.rb"
@@ -1,17 +1,17 @@
1
1
  BetterTranslate.configure do |config|
2
- # Scegli il provider da utilizzare: :chatgpt oppure :gemini
2
+ # Choose the provider to use: :chatgpt or :gemini
3
3
  config.provider = :chatgpt
4
4
 
5
- # API key per OpenAI
5
+ # API key for OpenAI
6
6
  config.openai_key = ENV.fetch("OPENAI_API_KEY") { "YOUR_OPENAI_API_KEY" }
7
7
 
8
- # API key per Google Gemini
8
+ # API key for Google Gemini
9
9
  config.google_gemini_key = ENV.fetch("GOOGLE_GEMINI_KEY") { "YOUR_GOOGLE_GEMINI_KEY" }
10
10
 
11
- # Lingua sorgente (ad esempio "en" se il file sorgente è in inglese)
11
+ # Source language (for example "en" if the source file is in English)
12
12
  config.source_language = "en"
13
13
 
14
- # Lista delle lingue target (short_name e name)
14
+ # List of target languages (short_name and name)
15
15
  config.target_languages = [
16
16
  # { short_name: 'es', name: 'spagnolo' },
17
17
  # { short_name: 'it', name: 'italiano' },
@@ -21,12 +21,12 @@ BetterTranslate.configure do |config|
21
21
  { short_name: "ru", name: "russian" }
22
22
  ]
23
23
 
24
- # Esclusioni globali (percorsi in dot notation) da escludere per tutte le lingue
24
+ # Global exclusions (paths in dot notation) to exclude for all languages
25
25
  config.global_exclusions = [
26
26
  "key.value"
27
27
  ]
28
28
 
29
- # Esclusioni specifiche per lingua
29
+ # Language-specific exclusions
30
30
  config.exclusions_per_language = {
31
31
  "es" => [],
32
32
  "it" => [],
@@ -36,14 +36,14 @@ BetterTranslate.configure do |config|
36
36
  "ru" => []
37
37
  }
38
38
 
39
- # Percorso del file di input (ad es. en.yml)
39
+ # Input file path (e.g. en.yml)
40
40
  config.input_file = Rails.root.join("config", "locales", "en.yml").to_s
41
41
 
42
- # Cartella di output dove verranno salvati i file tradotti
42
+ # Output folder where translated files will be saved
43
43
  config.output_folder = Rails.root.join("config", "locales", "translated").to_s
44
44
 
45
- # Metodo di traduzione:
46
- # - :override => rigenera tutte le traduzioni
47
- # - :incremental => aggiorna solo le chiavi mancanti (o quelle che hanno subito modifiche)
45
+ # Translation method:
46
+ # - :override => regenerates all translations
47
+ # - :incremental => updates only missing keys (or those that have been modified)
48
48
  config.translation_method = :override
49
49
  end
@@ -3,12 +3,14 @@ require 'rails/generators'
3
3
  module BetterTranslate
4
4
  module Generators
5
5
  class TranslateGenerator < Rails::Generators::Base
6
- desc "Lancia il processo di traduzione configurato in BetterTranslate"
6
+ desc "Starts the translation process configured in BetterTranslate"
7
7
 
8
8
  def run_translation
9
- say_status("Starting", "Esecuzione della traduzione con BetterTranslate...", :green)
9
+ message = "Starting translation with BetterTranslate..."
10
+ BetterTranslate::Utils.logger(message: message)
10
11
  BetterTranslate.magic
11
- say_status("Done", "Traduzione completata.", :green)
12
+ message = "Translation completed."
13
+ BetterTranslate::Utils.logger(message: message)
12
14
  end
13
15
  end
14
16
  end
@@ -0,0 +1,4 @@
1
+ module BetterTranslate
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end