Class: Fastlane::Helper::TranslateGptHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb

Instance Method Summary collapse

Constructor Details

#initialize(params) ⇒ TranslateGptHelper

Returns a new instance of TranslateGptHelper.



11
12
13
14
15
16
17
18
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 11

def initialize(params)
  @params = params
  @client = OpenAI::Client.new(
    access_token: params[:api_token],
    request_timeout: params[:request_timeout]
  )
  @timeout = params[:request_timeout]
end

Instance Method Details

#check_value_for_translate(string, orignal_string) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 35

def check_value_for_translate(string, orignal_string)
  return true unless string 
  if string.is_a? LocoStrings::LocoString
    return false if orignal_string.value.nil? || orignal_string.value.empty?
    return string.value.empty?
  elsif string.is_a? LocoStrings::LocoVariantions
    orignal_string.strings.each do |key, _|
      return true unless string.strings.has_key?(key)
      return true if string.strings[key].value.empty?
    end
  end
  return false
end

#filter_translated(need_to_skip, base, target) ⇒ Object



334
335
336
337
338
339
340
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 334

def filter_translated(need_to_skip, base, target) 
  if need_to_skip
    return base.reject { |k, v| target[k] }
  else 
    return base
  end
end

#get_context(localization_file, localization_key) ⇒ String

Get the context associated with a localization key

Parameters:

  • localization_file (String)

    The path to the strings file

  • localization_key (String)

    The localization key

Returns:

  • (String)

    The context associated with the localization key



328
329
330
331
332
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 328

def get_context(localization_file, localization_key)
  file = LocoStrings.load(localization_file)
  string = file.read[localization_key]
  return string.comment
end

#get_strings(localization_file) ⇒ Hash

Read the strings file into a hash

Parameters:

  • localization_file (String)

    The path to the strings file

Returns:

  • (Hash)

    The strings file as a hash



319
320
321
322
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 319

def get_strings(localization_file)
  file = LocoStrings.load(localization_file)
  return file.read
end

#log_input(bunch_size) ⇒ Object

Log information about the input strings



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 65

def log_input(bunch_size) 
  @translation_count = @to_translate.size
  number_of_strings = Colorizer::colorize("#{@translation_count}", :blue)
  UI.message "Translating #{number_of_strings} strings..."
  if bunch_size.nil? || bunch_size < 1
    estimated_string = Colorizer::colorize("#{@translation_count * @params[:request_timeout]}", :white)
    UI.message "Estimated time: #{estimated_string} seconds"
  else 
    number_of_bunches = (@translation_count / bunch_size.to_f).ceil
    estimated_string = Colorizer::colorize("#{number_of_bunches * @params[:request_timeout]}", :white)
    UI.message "Estimated time: #{estimated_string} seconds"
  end
end

#prepare_bunch_prompt(strings) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 152

def prepare_bunch_prompt(strings)
  prompt = "I want you to act as a translator for a mobile application strings. " + \
      "Try to keep length of the translated text. " + \
      "You need to response with a JSON only with the translation and nothing else until I say to stop it. "
  if @params[:context] && !@params[:context].empty?
    prompt += "This app is #{@params[:context]}. "
  end
  prompt += "Translate next text from #{@params[:source_language]} to #{@params[:target_language]}:\n"

  json_hash = []
  strings.each do |key, string|
    UI.message "Translating #{key} - #{string}"
    next if string.nil?

    string_hash = {}
    context = string.comment
    string_hash["context"] = context if context && !context.empty?

    key = transform_string(string.key)
    @keys_associations[key] = string.key
    string_hash["key"] = key

    if string.is_a? LocoStrings::LocoString
      next if string.value.nil? || string.value.empty?
      string_hash["string_to_translate"] = string.value
    elsif string.is_a? LocoStrings::LocoVariantions
      variants = {}
      string.strings.each do |key, variant|
        next if variant.nil? || variant.value.nil? || variant.value.empty?
        variants[key] = variant.value
      end
      string_hash["strings_to_translate"] = variants
    else 
      UI.warning "Unknown type of string: #{string.key}"
    end
    json_hash << string_hash
  end
  return '' if json_hash.empty?
  prompt += "'''\n"
  prompt += json_hash.to_json
  prompt += "\n'''"
  return prompt
end

#prepare_hashesObject

Get the strings from a file



56
57
58
59
60
61
62
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 56

def prepare_hashes() 
  if File.extname(@params[:source_file]) == ".xcstrings"
    prepare_xcstrings() 
  else
    prepare_strings() 
  end
end

#prepare_prompt(string) ⇒ Object

Prepare the prompt for the GPT API



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 136

def prepare_prompt(string) 
  prompt = "I want you to act as a translator for a mobile application strings. " + \
      "Try to keep length of the translated text. " + \
      "You need to answer only with the translation and nothing else until I say to stop it.  No commentaries." 
  if @params[:context] && !@params[:context].empty?
    prompt += "This app is #{@params[:context]}. "
  end 
  context = string.comment
  if context && !context.empty?
    prompt += "Additional context is #{context}. "
  end
  prompt += "Translate next text from #{@params[:source_language]} to #{@params[:target_language]}:\n" +
    "#{string.value}"
  return prompt
end

#prepare_stringsObject



49
50
51
52
53
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 49

def prepare_strings() 
  @input_hash = get_strings(@params[:source_file])
  @output_hash = get_strings(@params[:target_file])
  @to_translate = filter_translated(@params[:skip_translated], @input_hash, @output_hash)
end

#prepare_xcstringsObject



20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 20

def prepare_xcstrings() 
  @xcfile = LocoStrings::XCStringsFile.new @params[:source_file]
  @output_hash = {}
  @to_translate = @xcfile.read
  
  if @params[:skip_translated] == true
    @to_translate = @to_translate.reject { |k, original| 
      !check_value_for_translate(
        @xcfile.unit(k, @params[:target_language]),
        original
      )
    }
  end 
end

#request_bunch_translate(strings, prompt, index, number_of_bunches) ⇒ Object



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 231

def request_bunch_translate(strings, prompt, index, number_of_bunches)
  response = @client.chat(
    parameters: {
      model: @params[:model_name],
      messages: [
        { role: "user", content: prompt }
      ],
      temperature: @params[:temperature],
    }
  )
  # extract the translated string from the response
  error = response.dig("error", "message")
  
  #key_log = Colorizer::colorize(key, :blue)
  index_log = Colorizer::colorize("[#{index + 1}/#{number_of_bunches}]", :white)
  if error
    UI.error "#{index_log} Error translating: #{error}"
  else
    target_string = response.dig("choices", 0, "message", "content")
    json_string = target_string[/\[[^\[\]]*\]/m]
    begin
      json_hash = JSON.parse(json_string)
    rescue => error
      UI.error "#{index_log} Error parsing JSON: #{error}"
      UI.error "#{index_log} JSON: \"#{json_string}\""
      return
    end
    keys_to_translate = json_hash.map { |string_hash| string_hash["key"] }
    json_hash.each do |string_hash|
      key = string_hash["key"]
      context = string_hash["context"]
      string_hash.delete("key")
      string_hash.delete("context")
      translated_string = string_hash.values.first
      return unless key && !key.empty? 
      real_key = @keys_associations[key]
      if translated_string.is_a? Hash
        strings = {}
        translated_string.each do |pl_key, value|
          UI.message "#{index_log} Translating #{real_key} > #{pl_key} - #{value}"
          strings[pl_key] = LocoStrings::LocoString.new(pl_key, value, context)
        end
        string = LocoStrings::LocoVariantions.new(real_key, strings, context)
      elsif translated_string && !translated_string.empty?
        UI.message "#{index_log} Translating #{real_key} - #{translated_string}"
        string = LocoStrings::LocoString.new(real_key, translated_string, context)
      end
      @output_hash[real_key] = string
      keys_to_translate.delete(key)
    end

    if keys_to_translate.length > 0
      UI.important "#{index_log} Unable to translate #{keys_to_translate.join(", ")}"
    end
  end
end

#request_translate(key, string, prompt, index) ⇒ Object

Request a translation from the GPT API



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 203

def request_translate(key, string, prompt, index)
  response = @client.chat(
    parameters: {
      model: @params[:model_name], 
      messages: [
        { role: "user", content: prompt }
      ], 
      temperature: @params[:temperature],
    }
  )
  # extract the translated string from the response
  error = response.dig("error", "message")
  key_log = Colorizer::colorize(key, :blue)
  index_log = Colorizer::colorize("[#{index + 1}/#{@translation_count}]", :white)
  if error
    UI.error "#{index_log} Error translating #{key_log}: #{error}"
  else
    target_string = response.dig("choices", 0, "message", "content")
    if target_string && !target_string.empty?
      UI.message "#{index_log} Translating #{key_log} - #{string.value} -> #{target_string}"
      string.value = target_string
      @output_hash[key] = string
    else
      UI.important "#{index_log} Unable to translate #{key_log} - #{string.value}"
    end
  end
end

#transform_string(input_string) ⇒ Object



196
197
198
199
200
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 196

def transform_string(input_string)
  uppercased_string = input_string.upcase
  escaped_string = uppercased_string.gsub(/[^0-9a-zA-Z]+/, '_')
  return escaped_string
end

#translate_bunch_of_strings(bunch_size) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 104

def translate_bunch_of_strings(bunch_size)
  bunch_index = 0
  number_of_bunches = (@translation_count / bunch_size.to_f).ceil
  @keys_associations = {}
  @to_translate.each_slice(bunch_size) do |bunch|
    prompt = prepare_bunch_prompt bunch
    if prompt.empty?
      UI.important "Empty prompt, skipping bunch"
      next
    end
    max_retries = 10
    times_retried = 0

    # translate the source string to the target language
    begin
      request_bunch_translate(bunch, prompt, bunch_index, number_of_bunches)
      bunch_index += 1
    rescue Net::ReadTimeout => error
      if times_retried < max_retries
        times_retried += 1
        UI.important "Failed to request translation, retry #{times_retried}/#{max_retries}"
        wait 1
        retry
      else
        UI.error "Can't translate the bunch: #{error}"
      end
    end
    if bunch_index < number_of_bunches - 1 then wait end
  end
end

#translate_stringsObject

Cycle through the input strings and translate them



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 80

def translate_strings()
  @to_translate.each_with_index do |(key, string), index|
    prompt = prepare_prompt string

    max_retries = 10
    times_retried = 0

    # translate the source string to the target language
    begin
      request_translate(key, string, prompt, index)
    rescue Net::ReadTimeout => error
      if times_retried < max_retries
        times_retried += 1
        UI.important "Failed to request translation, retry #{times_retried}/#{max_retries}"
        wait 1
        retry
      else
        UI.error "Can't translate #{key}: #{error}"
      end
    end
    if index < @translation_count - 1 then wait end
  end
end

#wait(seconds = @timeout) ⇒ Object

Sleep for a specified number of seconds, displaying a progress bar

Parameters:

  • seconds (Integer) (defaults to: @timeout)

    The number of seconds to sleep



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 344

def wait(seconds = @timeout)
  sleep_time = 0
  while sleep_time < seconds
    percent_complete = (sleep_time.to_f / seconds.to_f) * 100.0
    progress_bar_width = 20
    completed_width = (progress_bar_width * percent_complete / 100.0).round
    remaining_width = progress_bar_width - completed_width
    print "\rTimeout [" 
    print Colorizer::code(:green)
    print "=" * completed_width
    print " " * remaining_width
    print Colorizer::code(:reset)
    print "]"
    print " %.2f%%" % percent_complete
    $stdout.flush
    sleep(1)
    sleep_time += 1
  end
  print "\r"
  $stdout.flush
end

#write_outputObject

Write the translated strings to the target file



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/fastlane/plugin/translate_gpt/helper/translate_gpt_helper.rb', line 289

def write_output()
  number_of_strings = Colorizer::colorize("#{@output_hash.size}", :blue)  
  target_string = Colorizer::colorize(@params[:target_file], :white)
  UI.message "Writing #{number_of_strings} strings to #{target_string}..."

  if @xcfile.nil?
    file = LocoStrings.load(@params[:target_file])
    file.read
    @output_hash.each do |key, value|
      file.update(key, value.value, value.comment)
    end
    file.write
  else
    @xcfile.update_file_path(@params[:target_file])
    @output_hash.each do |key, value|
      if value.is_a? LocoStrings::LocoString
        @xcfile.update(key, value.value, value.comment, "translated", @params[:target_language])
      elsif value.is_a? LocoStrings::LocoVariantions
        value.strings.each do |pl_key, variant|
          @xcfile.update_variation(key, pl_key, variant.value, variant.comment, "translated", @params[:target_language])
        end
      end
    end
    @xcfile.write
  end
end