fastlane-plugin-translate_gpt 0.1.3 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 162ac503b51db5c9395059702dfeedca75cbcd3bb26f68702059f45eb4540ffc
4
- data.tar.gz: d73722901b8e2d68c3287187687761930962e4392e7016847bfc9946d57c3748
3
+ metadata.gz: c370098458b2bc0d453de5101fa0a1f3273947cd41bc953bb94cf74f5ad58d6f
4
+ data.tar.gz: 96cccf7814b8796119d08340cca72089dea6c48a177df111b52bb2b1196ee756
5
5
  SHA512:
6
- metadata.gz: b260653373ca72b56e753aad327338559eeb330e64e602f905720b12d8cb6e6c35d0c5750e8be8199169d41cc7549dc19cd4f30a19caf652465ce3a6d10f54b1
7
- data.tar.gz: 21df8623e7bce11bf1c3658917dd498bd95929e77236dc32509ca8f3245c8491450cecee1996f2b40147e834aa2a76582facea074dcaa17b8e916a652e964193
6
+ metadata.gz: 5d07f6a7c6a9351b633781d53ddd40e1a432a62bd270d9df8223d40d33b8ae5034123b430db52498a97c462baf5841f7d828df582a9e3cfd4767d83e064ef7ff
7
+ data.tar.gz: e5e90093831fa83665f98b302ca433f5e04e5765f74c6a0a0fd5972fcad3dbaf690657e0136acf7ab0f49f253117a5b4deb6ab23b5e209507871b1376d59de2f
data/README.md CHANGED
@@ -53,6 +53,9 @@ The following options are available for `translate-gpt`:
53
53
  | `source_file` | The path to the `Localizable.strings` or `strings.xml` file to be translated. | `GPT_SOURCE_FILE` |
54
54
  | `target_file` | The path to the output file for the translated strings. | `GPT_TARGET_FILE` |
55
55
  | `context` | Common context for the translation | `GPT_COMMON_CONTEXT` |
56
+ | `bunch_size` | Number of strings to translate in a single request.| `GPT_BUNCH_SIZE` |
57
+
58
+ **Note:** __I advise using `bunch_size`. It will reduce the number of API requests and translations will be more accurate.__
56
59
 
57
60
  ## Providing context
58
61
 
@@ -7,73 +7,16 @@ module Fastlane
7
7
  module Actions
8
8
  class TranslateGptAction < Action
9
9
  def self.run(params)
10
- client = OpenAI::Client.new(
11
- access_token: params[:api_token],
12
- request_timeout: params[:request_timeout]
13
- )
14
-
15
- input_hash = Helper::TranslateGptHelper.get_strings(params[:source_file])
16
- output_hash = Helper::TranslateGptHelper.get_strings(params[:target_file])
17
-
18
- if params[:skip_translated]
19
- to_translate = input_hash.reject { |k, v| output_hash[k] }
10
+ helper = Helper::TranslateGptHelper.new(params)
11
+ helper.prepare_hashes()
12
+ bunch_size = params[:bunch_size]
13
+ helper.log_input(bunch_size)
14
+ if bunch_size.nil? || bunch_size < 1
15
+ helper.translate_strings()
20
16
  else
21
- to_translate = input_hash
22
- end
23
-
24
- UI.message "Translating #{to_translate.size} strings..."
25
-
26
- to_translate.each_with_index do |(key, string), index|
27
- prompt = "I want you to act as a translator for a mobile application strings. " + \
28
- "You need to answer only with translation and nothing else. No commentaries. " + \
29
- "Try to keep length of the translated text. " + \
30
- "I will send you a text and you translate it from #{params[:source_language]} to #{params[:target_language]}. "
31
- if params[:context] && !params[:context].empty?
32
- prompt += "This app is #{params[:context]}. "
33
- end
34
- context = string.comment
35
- if context && !context.empty?
36
- prompt += "Additional context is #{context}. "
37
- end
38
- prompt += "Source text:\n#{string.value}"
39
-
40
- # translate the source string to the target language
41
- response = client.chat(
42
- parameters: {
43
- model: params[:model_name],
44
- messages: [
45
- { role: "user", content: prompt }
46
- ],
47
- temperature: params[:temperature],
48
- }
49
- )
50
- # extract the translated string from the response
51
- error = response.dig("error", "message")
52
- if error
53
- UI.error "Error translating #{key}: #{error}"
54
- else
55
- target_string = response.dig("choices", 0, "message", "content")
56
- if target_string && !target_string.empty?
57
- UI.message "Translating #{key} - #{string.value} -> #{target_string}"
58
- string.value = target_string
59
- output_hash[key] = string
60
- else
61
- UI.warning "Unable to translate #{key} - #{string.value}"
62
- end
63
- end
64
- if index < to_translate.size - 1
65
- Helper::TranslateGptHelper.timeout params[:request_timeout]
66
- end
17
+ helper.translate_bunch_of_strings(bunch_size)
67
18
  end
68
-
69
- UI.message "Writing #{output_hash.size} strings to #{params[:target_file]}..."
70
-
71
- file = LocoStrings.load(params[:target_file])
72
- file.read
73
- output_hash.each do |key, value|
74
- file.update(key, value.value, value.comment)
75
- end
76
- file.write
19
+ helper.write_output()
77
20
  end
78
21
 
79
22
  #####################################################
@@ -159,7 +102,14 @@ module Fastlane
159
102
  description: "Common context for the translation",
160
103
  optional: true,
161
104
  type: String
162
- )
105
+ ),
106
+ FastlaneCore::ConfigItem.new(
107
+ key: :bunch_size,
108
+ env_name: "GPT_BUNCH_SIZE",
109
+ description: "Number of strings to translate in a single request",
110
+ optional: true,
111
+ type: Integer
112
+ ),
163
113
  ]
164
114
  end
165
115
 
@@ -1,15 +1,226 @@
1
1
  require 'fastlane_core/ui/ui'
2
2
  require 'loco_strings'
3
+ require 'json'
3
4
 
4
5
  module Fastlane
5
6
  UI = FastlaneCore::UI unless Fastlane.const_defined?("UI")
6
7
 
7
8
  module Helper
8
9
  class TranslateGptHelper
10
+ def initialize(params)
11
+ @params = params
12
+ @client = OpenAI::Client.new(
13
+ access_token: params[:api_token],
14
+ request_timeout: params[:request_timeout]
15
+ )
16
+ @timeout = params[:request_timeout]
17
+ end
18
+
19
+ # Get the strings from a file
20
+ def prepare_hashes()
21
+ @input_hash = get_strings(@params[:source_file])
22
+ @output_hash = get_strings(@params[:target_file])
23
+ @to_translate = filter_translated(@params[:skip_translated], @input_hash, @output_hash)
24
+ end
25
+
26
+ # Log information about the input strings
27
+ def log_input(bunch_size)
28
+ @translation_count = @to_translate.size
29
+ number_of_strings = Colorizer::colorize("#{@translation_count}", :blue)
30
+ UI.message "Translating #{number_of_strings} strings..."
31
+ if bunch_size.nil? || bunch_size < 1
32
+ estimated_string = Colorizer::colorize("#{@translation_count * @params[:request_timeout]}", :white)
33
+ UI.message "Estimated time: #{estimated_string} seconds"
34
+ else
35
+ number_of_bunches = (@translation_count / bunch_size.to_f).ceil
36
+ estimated_string = Colorizer::colorize("#{number_of_bunches * @params[:request_timeout]}", :white)
37
+ UI.message "Estimated time: #{estimated_string} seconds"
38
+ end
39
+ end
40
+
41
+ # Cycle through the input strings and translate them
42
+ def translate_strings()
43
+ @to_translate.each_with_index do |(key, string), index|
44
+ prompt = prepare_prompt string
45
+
46
+ max_retries = 10
47
+ times_retried = 0
48
+
49
+ # translate the source string to the target language
50
+ begin
51
+ request_translate(key, string, prompt, index)
52
+ rescue Net::ReadTimeout => error
53
+ if times_retried < max_retries
54
+ times_retried += 1
55
+ UI.important "Failed to request translation, retry #{times_retried}/#{max_retries}"
56
+ wait 1
57
+ retry
58
+ else
59
+ UI.error "Can't translate #{key}: #{error}"
60
+ end
61
+ end
62
+ if index < @translation_count - 1 then wait end
63
+ end
64
+ end
65
+
66
+ def translate_bunch_of_strings(bunch_size)
67
+ bunch_index = 0
68
+ number_of_bunches = (@translation_count / bunch_size.to_f).ceil
69
+ @to_translate.each_slice(bunch_size) do |bunch|
70
+ prompt = prepare_bunch_prompt bunch
71
+ max_retries = 10
72
+ times_retried = 0
73
+
74
+ # translate the source string to the target language
75
+ begin
76
+ request_bunch_translate(bunch, prompt, bunch_index, number_of_bunches)
77
+ bunch_index += 1
78
+ rescue Net::ReadTimeout => error
79
+ if times_retried < max_retries
80
+ times_retried += 1
81
+ UI.important "Failed to request translation, retry #{times_retried}/#{max_retries}"
82
+ wait 1
83
+ retry
84
+ else
85
+ UI.error "Can't translate #{key}: #{error}"
86
+ end
87
+ end
88
+ if bunch_index < number_of_bunches - 1 then wait end
89
+ end
90
+ end
91
+
92
+ # Prepare the prompt for the GPT API
93
+ def prepare_prompt(string)
94
+ prompt = "I want you to act as a translator for a mobile application strings. " + \
95
+ "Try to keep length of the translated text. " + \
96
+ "You need to answer only with the translation and nothing else until I say to stop it. No commentaries."
97
+ if @params[:context] && !@params[:context].empty?
98
+ prompt += "This app is #{@params[:context]}. "
99
+ end
100
+ context = string.comment
101
+ if context && !context.empty?
102
+ prompt += "Additional context is #{context}. "
103
+ end
104
+ prompt += "Translate next text from #{@params[:source_language]} to #{@params[:target_language]}:\n" +
105
+ "#{string.value}"
106
+ return prompt
107
+ end
108
+
109
+ def prepare_bunch_prompt(strings)
110
+ prompt = "I want you to act as a translator for a mobile application strings. " + \
111
+ "Try to keep length of the translated text. " + \
112
+ "You need to answer only with the translation and nothing else until I say to stop it."
113
+ if @params[:context] && !@params[:context].empty?
114
+ prompt += "This app is #{@params[:context]}. "
115
+ end
116
+ prompt += "Translate next text from #{@params[:source_language]} to #{@params[:target_language]}:\n"
117
+
118
+ json_hash = []
119
+ strings.each do |key, string|
120
+ string_hash = {}
121
+ context = string.comment
122
+ if context && !context.empty?
123
+ string_hash["context"] = context
124
+ end
125
+ string_hash["key"] = string.key
126
+ string_hash["string_to_translate"] = string.value
127
+ json_hash << string_hash
128
+ end
129
+ prompt += "'''\n"
130
+ prompt += json_hash.to_json
131
+ prompt += "\n'''"
132
+ return prompt
133
+ end
134
+
135
+ # Request a translation from the GPT API
136
+ def request_translate(key, string, prompt, index)
137
+ response = @client.chat(
138
+ parameters: {
139
+ model: @params[:model_name],
140
+ messages: [
141
+ { role: "user", content: prompt }
142
+ ],
143
+ temperature: @params[:temperature],
144
+ }
145
+ )
146
+ # extract the translated string from the response
147
+ error = response.dig("error", "message")
148
+ key_log = Colorizer::colorize(key, :blue)
149
+ index_log = Colorizer::colorize("[#{index + 1}/#{@translation_count}]", :white)
150
+ if error
151
+ UI.error "#{index_log} Error translating #{key_log}: #{error}"
152
+ else
153
+ target_string = response.dig("choices", 0, "message", "content")
154
+ if target_string && !target_string.empty?
155
+ UI.message "#{index_log} Translating #{key_log} - #{string.value} -> #{target_string}"
156
+ string.value = target_string
157
+ @output_hash[key] = string
158
+ else
159
+ UI.important "#{index_log} Unable to translate #{key_log} - #{string.value}"
160
+ end
161
+ end
162
+ end
163
+
164
+ def request_bunch_translate(strings, prompt, index, number_of_bunches)
165
+ response = @client.chat(
166
+ parameters: {
167
+ model: @params[:model_name],
168
+ messages: [
169
+ { role: "user", content: prompt }
170
+ ],
171
+ temperature: @params[:temperature],
172
+ }
173
+ )
174
+ # extract the translated string from the response
175
+ error = response.dig("error", "message")
176
+
177
+ #key_log = Colorizer::colorize(key, :blue)
178
+ index_log = Colorizer::colorize("[#{index + 1}/#{number_of_bunches}]", :white)
179
+ if error
180
+ UI.error "#{index_log} Error translating: #{error}"
181
+ else
182
+ target_string = response.dig("choices", 0, "message", "content")
183
+ json_string = target_string[/\[[^\[\]]*\]/m]
184
+ json_hash = JSON.parse(json_string)
185
+ keys_to_translate = json_hash.map { |string_hash| string_hash["key"] }
186
+ json_hash.each do |string_hash|
187
+ key = string_hash["key"]
188
+ context = string_hash["context"]
189
+ string_hash.delete("key")
190
+ string_hash.delete("context")
191
+ translated_string = string_hash.values.first
192
+ if key && !key.empty? && translated_string && !translated_string.empty?
193
+ UI.message "#{index_log} Translating #{key} - #{translated_string}"
194
+ string = LocoStrings::LocoString.new(key, translated_string, context)
195
+ @output_hash[key] = string
196
+ keys_to_translate.delete(key)
197
+ end
198
+ end
199
+
200
+ if keys_to_translate.length > 0
201
+ UI.important "#{index_log} Unable to translate #{keys_to_translate.join(", ")}"
202
+ end
203
+ end
204
+ end
205
+
206
+ # Write the translated strings to the target file
207
+ def write_output()
208
+ number_of_strings = Colorizer::colorize("#{@output_hash.size}", :blue)
209
+ target_string = Colorizer::colorize(@params[:target_file], :white)
210
+ UI.message "Writing #{number_of_strings} strings to #{target_string}..."
211
+
212
+ file = LocoStrings.load(@params[:target_file])
213
+ file.read
214
+ @output_hash.each do |key, value|
215
+ file.update(key, value.value, value.comment)
216
+ end
217
+ file.write
218
+ end
219
+
9
220
  # Read the strings file into a hash
10
221
  # @param localization_file [String] The path to the strings file
11
222
  # @return [Hash] The strings file as a hash
12
- def self.get_strings(localization_file)
223
+ def get_strings(localization_file)
13
224
  file = LocoStrings.load(localization_file)
14
225
  return file.read
15
226
  end
@@ -18,25 +229,36 @@ module Fastlane
18
229
  # @param localization_file [String] The path to the strings file
19
230
  # @param localization_key [String] The localization key
20
231
  # @return [String] The context associated with the localization key
21
- def self.get_context(localization_file, localization_key)
232
+ def get_context(localization_file, localization_key)
22
233
  file = LocoStrings.load(localization_file)
23
234
  string = file.read[localization_key]
24
235
  return string.comment
25
236
  end
26
237
 
238
+ def filter_translated(need_to_skip, base, target)
239
+ if need_to_skip
240
+ return base.reject { |k, v| target[k] }
241
+ else
242
+ return base
243
+ end
244
+ end
245
+
27
246
  # Sleep for a specified number of seconds, displaying a progress bar
28
247
  # @param seconds [Integer] The number of seconds to sleep
29
- def self.timeout(total)
248
+ def wait(seconds = @timeout)
30
249
  sleep_time = 0
31
- while sleep_time < total
32
- percent_complete = (sleep_time.to_f / total.to_f) * 100.0
250
+ while sleep_time < seconds
251
+ percent_complete = (sleep_time.to_f / seconds.to_f) * 100.0
33
252
  progress_bar_width = 20
34
253
  completed_width = (progress_bar_width * percent_complete / 100.0).round
35
254
  remaining_width = progress_bar_width - completed_width
36
- print "\rTimeout ["
255
+ print "\rTimeout ["
256
+ print Colorizer::code(:green)
37
257
  print "=" * completed_width
38
258
  print " " * remaining_width
39
- print "] %.2f%%" % percent_complete
259
+ print Colorizer::code(:reset)
260
+ print "]"
261
+ print " %.2f%%" % percent_complete
40
262
  $stdout.flush
41
263
  sleep(1)
42
264
  sleep_time += 1
@@ -45,5 +267,28 @@ module Fastlane
45
267
  $stdout.flush
46
268
  end
47
269
  end
270
+
271
+ # Helper class for bash colors
272
+ class Colorizer
273
+ COLORS = {
274
+ black: 30,
275
+ red: 31,
276
+ green: 32,
277
+ yellow: 33,
278
+ blue: 34,
279
+ magenta: 35,
280
+ cyan: 36,
281
+ white: 37,
282
+ reset: 0,
283
+ }
284
+
285
+ def self.colorize(text, color)
286
+ color_code = COLORS[color.to_sym]
287
+ "\e[#{color_code}m#{text}\e[0m"
288
+ end
289
+ def self.code(color)
290
+ "\e[#{COLORS[color.to_sym]}m"
291
+ end
292
+ end
48
293
  end
49
294
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module TranslateGpt
3
- VERSION = "0.1.3"
3
+ VERSION = "0.1.5"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-translate_gpt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksei Cherepanov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-20 00:00:00.000000000 Z
11
+ date: 2023-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-openai
@@ -193,7 +193,10 @@ files:
193
193
  homepage: https://github.com/ftp27/fastlane-plugin-translate_gpt
194
194
  licenses:
195
195
  - MIT
196
- metadata: {}
196
+ metadata:
197
+ homepage_uri: https://github.com/ftp27/fastlane-plugin-translate_gpt
198
+ source_code_uri: https://github.com/ftp27/fastlane-plugin-translate_gpt
199
+ github_repo: https://github.com/ftp27/fastlane-plugin-translate_gpt
197
200
  post_install_message:
198
201
  rdoc_options: []
199
202
  require_paths: