immosquare-yaml 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aef05b6cde6a978367813b253fe4dcc378674aa2b6e0192efe967caaf5babd21
4
- data.tar.gz: 1bea9842a643d06132b023cf10c8e1f3624c94d83fdff9d62509017fc6873064
3
+ metadata.gz: 7ace65cc1c3b599449f0c7af622e242507a010262e536484c1424bb19eeb9ba3
4
+ data.tar.gz: b10ce8e8239464b58ff87be76512db3fd2731c38aabf256d4f917d26e5cc8ede
5
5
  SHA512:
6
- metadata.gz: b067dc205dd3e76680c7b76cc851a7676244fcbfb224865f8fdd7368ea185b7fc7612e282c621144b44c7edde5edb13e0dd7db43baa4d6a62ec9a5aa58bfbd1d
7
- data.tar.gz: 226772ab6581607c3d41ba8e3fe0bdb360d12e900a4440eba6554d0eb646d7b518f91794dae055f6ff234a798f74b1f0678e5e908dd36e41170441e28f43884a
6
+ metadata.gz: '048cdac2661a8ab810c12547f5e398612b5a6f94788340055cf8e46b004f3a357b9c1269f210bf29e75fa329248d86f4a92108ad097aabcf39fe91b722af5d12'
7
+ data.tar.gz: f4eddc1f73762f31a84b6f2b30e5dbc2bcc7c63ce8f5cb83a8ad5668dbde11d71d3a7aed5b37d0c7bf7c8fef02e0e74743bb0f992ccc9ae304b27adf37ff5da8
@@ -0,0 +1,12 @@
1
+ module ImmosquareYaml
2
+ class Configuration
3
+
4
+ attr_accessor :openai_api_key, :openai_model
5
+
6
+ def initialize
7
+ @openai_api_key = nil
8
+ @openai_model = nil
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module ImmosquareYaml
2
+ class Railtie < Rails::Railtie
3
+
4
+ rake_tasks do
5
+ load "tasks/immosquare-yaml.rake"
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,45 @@
1
+ module ImmosquareYaml
2
+ module SharedMethods
3
+ INDENT_SIZE = 2
4
+ NOTHING = "".freeze
5
+ SPACE = " ".freeze
6
+ NEWLINE = "\n".freeze
7
+ SIMPLE_QUOTE = "'".freeze
8
+ DOUBLE_QUOTE = '"'.freeze
9
+ DOUBLE_SIMPLE_QUOTE = "''".freeze
10
+ WEIRD_QUOTES_REGEX = /‘|’|“|”|‛|‚|„|‟|#{Regexp.quote(DOUBLE_SIMPLE_QUOTE)}/.freeze
11
+ YML_SPECIAL_CHARS = ["-", "`", "{", "}", "|", "[", "]", ">", ":", "\"", "'", "*", "=", "%", ",", "!", "?", "&", "#", "@"].freeze
12
+ RESERVED_KEYS = [
13
+ "yes", "no", "on", "off", "true", "false",
14
+ "Yes", "No", "On", "Off", "True", "False",
15
+ "YES", "NO", "ON", "OFF", "TRUE", "FALSE"
16
+ ].freeze
17
+
18
+
19
+ ##============================================================##
20
+ ## Deep transform values resursively
21
+ ##============================================================##
22
+ def deep_transform_values(hash, &block)
23
+ hash.transform_values do |value|
24
+ if value.is_a?(Hash)
25
+ deep_transform_values(value, &block)
26
+ else
27
+ block.call(value)
28
+ end
29
+ end
30
+ end
31
+
32
+ ##============================================================##
33
+ ## sort_by_key Function
34
+ ## Purpose: Sort a hash by its keys, optionally recursively, with
35
+ ## case-insensitive comparison and stripping of double quotes.
36
+ ## ============================================================ #
37
+ def sort_by_key(hash, recursive = false, &block)
38
+ block ||= proc {|a, b| a.to_s.downcase.gsub("\"", "") <=> b.to_s.downcase.gsub("\"", "") }
39
+ hash.keys.sort(&block).each_with_object({}) do |key, seed|
40
+ seed[key] = hash[key]
41
+ seed[key] = sort_by_key(seed[key], true, &block) if recursive && seed[key].is_a?(Hash)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,331 @@
1
+ require "iso-639"
2
+ require "httparty"
3
+
4
+
5
+ module ImmosquareYaml
6
+
7
+ module Translate
8
+ extend SharedMethods
9
+
10
+ class << self
11
+
12
+ def translate(file_path, locale_to, options = {})
13
+ begin
14
+ ##=============================================================##
15
+ ## options
16
+ ##=============================================================##
17
+ options = {
18
+ :reset_translations => false
19
+ }.merge(options)
20
+ options[:reset_translations] = false if ![true, false].include?(options[:reset_translations])
21
+
22
+
23
+ ##=============================================================##
24
+ ## Load config keys from config_dev.yml
25
+ ##=============================================================##
26
+ raise("Error: openai_api_key not found in config_dev.yml") if ImmosquareYaml.configuration.openai_api_key.nil?
27
+ raise("Error: File #{file_path} not found") if !File.exist?(file_path)
28
+ raise("Error: locale is not a locale") if !locale_to.is_a?(String) || locale_to.size != 2
29
+
30
+ ##============================================================##
31
+ ## We clean the file before translation
32
+ ##============================================================##
33
+ ImmosquareYaml.clean(file_path)
34
+
35
+ ##============================================================##
36
+ ## We parse the clean input file
37
+ ##============================================================##
38
+ hash_from = ImmosquareYaml.parse(file_path)
39
+ raise("#{file_path} is not a correct yml translation file") if !hash_from.is_a?(Hash) && hash_from.keys.size > 1
40
+
41
+ ##============================================================##
42
+ ## Check if the locale is present in the file
43
+ ##============================================================##
44
+ locale_from = hash_from.keys.first.to_s
45
+ raise("Error: The destination file (#{locale_to}) is the same as the source file (#{locale_from}).") if locale_from == locale_to
46
+ raise("Error: Expected the source file (#{file_path}) to end with '#{locale_from}.yml' but it didn't.") if !file_path.end_with?("#{locale_from}.yml")
47
+
48
+
49
+ ##============================================================##
50
+ ## Prepare the output file
51
+ ##============================================================##
52
+ file_basename = File.basename(file_path)
53
+ file_dirname = File.dirname(file_path)
54
+ translated_file_path = "#{file_dirname}/#{file_basename.gsub("#{locale_from}.yml", "#{locale_to}.yml")}"
55
+
56
+ ##============================================================##
57
+ ## We create a hash with all keys from the source file
58
+ ##============================================================##
59
+ hash_to = {locale_to => hash_from.delete(locale_from)}
60
+
61
+ ##============================================================##
62
+ ## We create a array with all keys from the source file
63
+ ##============================================================##
64
+ array_to = translatable_array(hash_to)
65
+ array_to = array_to.map {|k, v| [k, v, nil] }
66
+
67
+ ##============================================================##
68
+ ## If we already have a translation file for the language
69
+ ## we get the values in it and put it in our
70
+ ## file... You have to do well with !nil?
71
+ ## to retrieve the values "" and " "...
72
+ ##============================================================##
73
+ if File.exist?(translated_file_path) && options[:reset_translations] == false
74
+ temp_hash = ImmosquareYaml.parse(translated_file_path)
75
+ raise("#{translated_file_path} is not a correct yml translation file") if !temp_hash.is_a?(Hash) && temp_hash.keys.size > 1
76
+
77
+ ##============================================================##
78
+ ## t can be nil if the key is not present in the source file
79
+ ##============================================================##
80
+ translatable_array(temp_hash).each do |key, value|
81
+ t = array_to.find {|k, _v| k == key }
82
+ t[2] = value if !t.nil? && !value.nil?
83
+ end
84
+ end
85
+
86
+ ##============================================================##
87
+ ## Here we have to do all the translation logic...
88
+ ## For the moment we use the OPENAI API, but we can imagine
89
+ ## using other translation APIs in the future.
90
+ ##============================================================##
91
+ translated_array = translate_with_open_ai(array_to, locale_from, locale_to)
92
+
93
+ ##============================================================##
94
+ ## Then we have to reformat the output yml file
95
+ ##============================================================##
96
+ final_array = translated_array.map {|k, _from, to| [k, to] }
97
+ final_hash = translatable_hash(final_array)
98
+
99
+ ##============================================================##
100
+ ## We write the output file and clean it
101
+ ##============================================================##
102
+ File.write(translated_file_path, ImmosquareYaml.dump(final_hash))
103
+ ImmosquareYaml.clean(translated_file_path)
104
+ rescue StandardError => e
105
+ puts(e.message)
106
+ puts(e.backtrace)
107
+ false
108
+ end
109
+ end
110
+
111
+
112
+ private
113
+
114
+ ##============================================================##
115
+ ## To translatable hash to array
116
+ ## opitons are :
117
+ ## :format => "string" or "array"
118
+ ## :keys_only => true or false
119
+ ## {:fr=>{"demo1"=>"demo1", "demo2"=>{"demo2-1"=>"demo2-1"}}}
120
+ ## format = "string" and keys_only = false => [["fr.demo1", "demo1"], ["fr.demo2.demo2-1", "demo2-1"]]
121
+ ## format = "string" and keys_only = true => ["fr.demo1", "fr.demo2.demo2-1"]
122
+ ## format = "array" and keys_only = false => [[["fr", "demo1"], "demo1"], [["fr", "demo2", "demo2-1"], "demo2-1"]]
123
+ ## format = "array" and keys_only = true => [["fr", "demo1"], ["fr", "demo2", "demo2-1"]]
124
+ ## ============================================================
125
+ def translatable_array(hash, key = nil, result = [], **options)
126
+ options = {
127
+ :format => "string",
128
+ :keys_only => false
129
+ }.merge(options)
130
+ options[:keys_only] = false if ![true, false].include?(options[:keys_only])
131
+ options[:format] = "string" if !["string", "array"].include?(options[:format])
132
+
133
+
134
+ if hash.is_a?(Hash)
135
+ hash.each_key do |k|
136
+ translatable_array(hash[k], "#{key}#{":" if !key.nil?}#{k}", result, **options)
137
+ end
138
+ else
139
+ r2 = options[:format] == "string" ? key.split(":").join(".") : key.split(":")
140
+ result << (options[:keys_only] ? r2 : [r2, hash])
141
+ end
142
+ result
143
+
144
+ end
145
+
146
+ ##============================================================##
147
+ ## We can do the inverse of the previous function
148
+ ##============================================================##
149
+ def translatable_hash(array)
150
+ data_hash = array.to_h
151
+ final = {}
152
+ data_hash.each do |key, value|
153
+ key_parts = key.split(".")
154
+ leaf = key_parts.pop
155
+ parent = key_parts.inject(final) {|h, k| h[k] ||= {} }
156
+ parent[leaf] = value
157
+ end
158
+ final
159
+ end
160
+
161
+ ##============================================================##
162
+ ## Translate with OpenAI
163
+ ##
164
+ ## [
165
+ ## ["en.mlsconnect.contact_us", "Nous contacter", "Contact us"],
166
+ ## ["en.mlsconnect.description", "Description", nil],
167
+ ## ...
168
+ ## ]
169
+ ##============================================================##
170
+ def translate_with_open_ai(array, from, to)
171
+ ##============================================================##
172
+ ## https://platform.openai.com/docs/models/
173
+ ## No all models are available for all users.
174
+ ## The model `gpt-4-32k` does not exist or you do not have access to it.
175
+ ## Learn more: https://help.openai.com/en/articles/7102672-how-can-i-access-gpt-4.
176
+ ##============================================================##
177
+ model_name = ImmosquareYaml.configuration.openai_model
178
+ models = [
179
+ {:name => "gpt-3.5-turbo", :tokens => 4097, :input => 0.0015, :output => 0.002, :group_size => 75},
180
+ {:name => "gpt-3.5-turbo-16k", :tokens => 16_385, :input => 0.0030, :output => 0.004, :group_size => 300},
181
+ {:name => "gpt-4", :tokens => 8192, :input => 0.0300, :output => 0.060, :group_size => 150},
182
+ {:name => "gpt-4-32k", :tokens => 32_769, :input => 0.0600, :output => 0.120, :group_size => 600}
183
+ ]
184
+ model = models.find {|m| m[:name] == model_name }
185
+ model = models.find {|m| m[:name] == "gpt-3.5-turbo-16k" } if model.nil?
186
+
187
+ ##============================================================##
188
+ ## Manage blank values
189
+ ##============================================================##
190
+ blank_values = [NOTHING, SPACE, "\"\"", "\"#{SPACE}\""]
191
+ cant_be_translated = "CANNOT-BE-TRANSLATED"
192
+ array = array.map do |key, from, to|
193
+ [key, from, blank_values.include?(from) ? from : to]
194
+ end
195
+
196
+
197
+ ##============================================================##
198
+ ## we want to send as little data as possible to openAI because
199
+ ## we pay for the volume of data sent. So we're going to send. We put
200
+ ## a number rather than a string for the translations to be made.
201
+ ## We take the 16k model to have 16,000k tokens per request
202
+ ## (around 16,000/4 = 4000 characters).
203
+ ## ==
204
+ ## Remove the translations that have already been made
205
+ ##============================================================##
206
+ data_open_ai = array.clone
207
+ data_open_ai = data_open_ai.map.with_index {|(_k, from, to), index| [index, from, to] }
208
+ data_open_ai = data_open_ai.select {|_index, from, to| !from.nil? && to.nil? }
209
+
210
+ ##============================================================##
211
+ ## Remove quotes surrounding the value if they are present.
212
+ ## and remove to to avoid error in translation
213
+ ##============================================================##
214
+ data_open_ai = data_open_ai.map do |index, from, _to|
215
+ from = from.to_s
216
+ from = from[1..-2] while (from.start_with?(DOUBLE_QUOTE) && from.end_with?(DOUBLE_QUOTE)) || (from.start_with?(SIMPLE_QUOTE) && from.end_with?(SIMPLE_QUOTE))
217
+ [index, from]
218
+ end
219
+
220
+ return array if data_open_ai.empty?
221
+
222
+ ##============================================================##
223
+ ## Call OpenAI API
224
+ ##============================================================##
225
+ index = 0
226
+ group_size = model[:group_size]
227
+ from_iso = ISO_639.find_by_code(from).english_name.split(";").first
228
+ to_iso = ISO_639.find_by_code(to).english_name.split(";").first
229
+ ai_resuslts = []
230
+ prompt_system = "You are a translation tool from #{from_iso} to #{to_iso}\n" \
231
+ "The input is an array of pairs, where each pair contains an index and a string to translate, formatted as [index, string_to_translate]\n" \
232
+ "Your task is to create an output array where each element is a pair consisting of the index and the translated string, formatted as [index, 'string_translated']\n" \
233
+ "\nRules to respect:\n" \
234
+ "- Do not escape apostrophes in translated strings; leave them as they are.\n" \
235
+ "- Special characters, except apostrophes, that need to be escaped in translated strings should be escaped using a single backslash (\\), not double (\\\\).\n" \
236
+ "- If a string cannot be translated use the string '#{cant_be_translated}' translated as the translation value witouth quote (simple or double) quote, just the string\n" \
237
+ "- If you dont know the correct translatation but the original word seems to make sense in the original language use it for the translated field otherwise use the #{cant_be_translated} strategy of the preceding point\n" \
238
+ "- Use only doubles quotes (\") to enclose translated strings and avoid using single quotes (').\n" \
239
+ "- Your output must ONLY be an array with the same number of pairs as the input, without any additional text or explanation.\n" \
240
+ "- You need to check that the globle array is correctly closed at the end of the response. (the response must therefore end with ]] to to be consistent)"
241
+ prompt_init = "Please proceed with translating the following array:"
242
+ headers = {
243
+ "Content-Type" => "application/json",
244
+ "Authorization" => "Bearer #{ImmosquareYaml.configuration.openai_api_key}"
245
+ }
246
+
247
+
248
+ ##============================================================##
249
+ ## Loop
250
+ ##============================================================##
251
+ puts("fields to translate : #{data_open_ai.size}#{" by group of #{group_size}" if data_open_ai.size > group_size}")
252
+ while index < data_open_ai.size
253
+ data_group = data_open_ai[index, group_size]
254
+
255
+
256
+ begin
257
+ puts("call OPENAI Api (with model #{model[:name]}) #{" for #{data_group.size} fields" if data_open_ai.size > group_size}")
258
+ prompt = "#{prompt_init}:\n\n#{data_group.inspect}\n\n"
259
+ body = {
260
+ :model => model[:name],
261
+ :messages => [
262
+ {:role => "system", :content => prompt_system},
263
+ {:role => "user", :content => prompt}
264
+ ],
265
+ :temperature => 0.0
266
+ }
267
+ t0 = Time.now
268
+ call = HTTParty.post("https://api.openai.com/v1/chat/completions", :body => body.to_json, :headers => headers, :timeout => 240)
269
+
270
+ puts("responded in #{(Time.now - t0).round(2)} seconds")
271
+ raise(call["error"]["message"]) if call.code != 200
272
+
273
+
274
+ ##============================================================##
275
+ ## We check that the result is complete
276
+ ##============================================================##
277
+ response = JSON.parse(call.body)
278
+ choice = response["choices"][0]
279
+ raise("Result is not complete") if choice["finish_reason"] != "stop"
280
+
281
+
282
+ ##============================================================##
283
+ ## We calculate the estimate price of the call
284
+ ##============================================================##
285
+ input_price = (response["usage"]["prompt_tokens"] / 1000.0) * model[:input]
286
+ output_price = (response["usage"]["completion_tokens"] / 1000.0) * model[:output]
287
+ price = input_price + output_price
288
+ puts("Estimate price => #{input_price.round(3)} + #{output_price.round(3)} = #{price.round(3)} USD")
289
+
290
+ ##============================================================##
291
+ ## We check that the result is an array
292
+ ##============================================================##
293
+ content = eval(choice["message"]["content"])
294
+ raise("Is not an array") if !content.is_a?(Array)
295
+
296
+ ##============================================================##
297
+ ## We save the result
298
+ ##============================================================##
299
+ content.each do |index, translation|
300
+ ai_resuslts << [index, translation == cant_be_translated ? nil : translation]
301
+ end
302
+ rescue StandardError => e
303
+ puts("error OPEN AI API => #{e.message}")
304
+ puts(e.message)
305
+ puts(e.backtrace)
306
+ end
307
+ index += group_size
308
+ end
309
+
310
+
311
+ ##============================================================##
312
+ ## We put the translations in the original array
313
+ ##============================================================##
314
+ ai_resuslts.each do |index, translation|
315
+ array[index][2] = translation
316
+ end
317
+
318
+ ##============================================================##
319
+ ## We return the modified array
320
+ ##============================================================##
321
+ array.map.with_index do |(k, from, to), index|
322
+ from = from.to_s
323
+ to = "#{DOUBLE_QUOTE}#{to}#{DOUBLE_QUOTE}" if ai_resuslts.find {|i, _t| i == index } && ((from.start_with?(DOUBLE_QUOTE) && from.end_with?(DOUBLE_QUOTE)) || (from.start_with?(SIMPLE_QUOTE) && from.end_with?(SIMPLE_QUOTE)))
324
+ [k, from, to]
325
+ end
326
+ end
327
+
328
+
329
+ end
330
+ end
331
+ end
@@ -0,0 +1,3 @@
1
+ module ImmosquareYaml
2
+ VERSION = "0.1.2".freeze
3
+ end
@@ -1,19 +1,29 @@
1
+ require_relative "immosquare-yaml/configuration"
2
+ require_relative "immosquare-yaml/shared_methods"
3
+ require_relative "immosquare-yaml/translate"
4
+ require_relative "immosquare-yaml/railtie" if defined?(Rails)
5
+
6
+
1
7
  module ImmosquareYaml
8
+ extend SharedMethods
9
+
2
10
  class << self
3
11
 
4
- INDENT_SIZE = 2
5
- SPACE = " ".freeze
6
- NEWLINE = "\n".freeze
7
- SIMPLE_QUOTE = "'".freeze
8
- DOUBLE_QUOTE = '"'.freeze
9
- DOUBLE_SIMPLE_QUOTE = "''".freeze
10
- WEIRD_QUOTES_REGEX = /‘|’|“|”|‛|‚|„|‟|#{Regexp.quote(DOUBLE_SIMPLE_QUOTE)}/.freeze
11
- YML_SPECIAL_CHARS = ["-", "`", "{", "}", "|", "[", "]", ">", ":", "\"", "'", "*", "=", "%", ",", "!", "?", "&", "#", "@"].freeze
12
- RESERVED_KEYS = [
13
- "yes", "no", "on", "off", "true", "false",
14
- "Yes", "No", "On", "Off", "True", "False",
15
- "YES", "NO", "ON", "OFF", "TRUE", "FALSE"
16
- ].freeze
12
+
13
+
14
+
15
+ ##===========================================================================##
16
+ ## Gem configuration
17
+ ##===========================================================================##
18
+ attr_writer :configuration
19
+
20
+ def configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ def config
25
+ yield(configuration)
26
+ end
17
27
 
18
28
  ##===========================================================================##
19
29
  ## This method cleans a specified YAML file by processing it line by line.
@@ -167,7 +177,8 @@ module ImmosquareYaml
167
177
  ## Remove quotes surrounding the value if they are present.
168
178
  ## They are not necessary in this case after | or |-
169
179
  ##============================================================##
170
- value = value[1..-2] if (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
180
+ value = value[1..-2] while (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
181
+
171
182
 
172
183
  ##=============================================================##
173
184
  ## We parse on the 2 types of line breaks
@@ -189,8 +200,8 @@ module ImmosquareYaml
189
200
  ## Finalizing the construction by adding a newline at the end and
190
201
  ## removing whitespace from empty lines.
191
202
  ##===========================================================================##
192
- lines += [""]
193
- lines = lines.map {|l| l.strip.empty? ? "" : l }
203
+ lines += [NOTHING]
204
+ lines = lines.map {|l| l.strip.empty? ? NOTHING : l }
194
205
  lines.join("\n")
195
206
  end
196
207
 
@@ -280,7 +291,7 @@ module ImmosquareYaml
280
291
  ## Detecting blank lines to specially handle the last line within a block;
281
292
  ## if we are inside a block or it's the last line, we avoid skipping
282
293
  ##===================================================================================#
283
- blank_line = current_line.gsub(NEWLINE, "").empty?
294
+ blank_line = current_line.gsub(NEWLINE, NOTHING).empty?
284
295
  next if !(last_line || inblock || !blank_line)
285
296
 
286
297
  ##============================================================##
@@ -391,14 +402,14 @@ module ImmosquareYaml
391
402
  ## If the line is commented out, we keep and we remove newlines
392
403
  ##============================================================##
393
404
  if current_line.lstrip.start_with?("#")
394
- lines << current_line.gsub(NEWLINE, "")
405
+ lines << current_line.gsub(NEWLINE, NOTHING)
395
406
  ##================================================= ============##
396
407
  ## If is in a block (multiline > | or |-), we clean
397
408
  ## the line because it can start with spaces tabs etc.
398
409
  ## and put it with the block indenter
399
410
  ##================================================= ============##
400
411
  elsif inblock == true
401
- current_line = current_line.gsub(NEWLINE, "").strip
412
+ current_line = current_line.gsub(NEWLINE, NOTHING).strip
402
413
  lines << "#{SPACE * (inblock_indent + INDENT_SIZE)}#{current_line}"
403
414
  ##================================================= ============##
404
415
  ## if the line ends with a multi-line character and we have a key.
@@ -413,7 +424,7 @@ module ImmosquareYaml
413
424
  ## $ : Matches the end of the line/string.
414
425
  ##================================================= ============##
415
426
  elsif current_line.rstrip.match?(/\S+: [>|](\d*)[-+]?$/)
416
- lines << current_line.gsub(NEWLINE, "")
427
+ lines << current_line.gsub(NEWLINE, NOTHING)
417
428
  inblock_indent = indent_level
418
429
  inblock = true
419
430
  ##============================================================##
@@ -427,7 +438,7 @@ module ImmosquareYaml
427
438
  ## my key: line1 line2 line3
428
439
  ##============================================================##
429
440
  elsif split.size < 2
430
- lines[-1] = (lines[-1] + " #{current_line.lstrip}").gsub(NEWLINE, "")
441
+ lines[-1] = (lines[-1] + " #{current_line.lstrip}").gsub(NEWLINE, NOTHING)
431
442
  ##============================================================##
432
443
  ## Otherwise we are in the case of a classic line
433
444
  ## key: value or key: without value
@@ -470,8 +481,8 @@ module ImmosquareYaml
470
481
  ## spaces on "empty" lines + double spaces
471
482
  ## with the same technique as above
472
483
  ##============================================================##
473
- lines += [""]
474
- lines = lines.map {|l| (l.strip.empty? ? "" : l).to_s.gsub(/(?<=\S)\s+/, SPACE) }
484
+ lines += [NOTHING]
485
+ lines = lines.map {|l| (l.strip.empty? ? NOTHING : l).to_s.gsub(/(?<=\S)\s+/, SPACE) }
475
486
  File.write(file_path, lines.join(NEWLINE))
476
487
  end
477
488
 
@@ -481,35 +492,32 @@ module ImmosquareYaml
481
492
  ##============================================================##
482
493
  ## Strategy:
483
494
  ## 1. Forcefully convert the key to a string to handle gsub operations, especially if it's an integer.
484
- ## 2. Check if the key is an integer.
485
- ## 3. Remove quotes if they are present.
495
+ ## 2. Remove quotes if they are present.
496
+ ## 3. Check if the key is an integer.
486
497
  ## 4. Re-add quotes if the key is a reserved word or an integer.
487
- #
488
- ## Regular Expression Explanation:
489
- ## /\A(['“‘”’"])(.*)\1\z/
490
- ## \A: Matches the start of the string, ensuring our pattern begins at the very start of the string.
491
- ## (['“‘”’"]): Captures a single quote character. It matches any of the characters specified within the brackets.
492
- ## This includes various types of single and double quotes.
493
- ## (.*) : Captures zero or more of any character. It "captures" the entirety of the string between the quotes.
494
- ## \1: Refers back to the first captured group, ensuring the same type of quote character is found at the end.
495
- ## \z: Matches the end of the string, ensuring our pattern matches up to the very end.
496
- #
497
- ## In the second argument of gsub, we use '\2' to refer back to the content captured by the second capture group.
498
+ ##
498
499
  ## This allows us to fetch the string without the surrounding quotes.
499
500
  ##============================================================##
500
501
  def clean_key(key)
501
502
  ##============================================================##
502
503
  ## Convert key to string to avoid issues with gsub operations
503
- ## + Check if the key is an integer
504
504
  ##============================================================##
505
- key = key.to_s
506
- is_int = key =~ /\A[-+]?\d+\z/
505
+ key = key.to_s
507
506
 
508
507
  ##============================================================##
509
508
  ## Remove surrounding quotes from the key
509
+ ##============================================================##
510
+ key = key[1..-2] if (key.start_with?(DOUBLE_QUOTE) && key.end_with?(DOUBLE_QUOTE)) || (key.start_with?(SIMPLE_QUOTE) && key.end_with?(SIMPLE_QUOTE))
511
+
512
+ ##============================================================##
513
+ ## Check if the key is an integer
514
+ ##============================================================##
515
+ is_int = key =~ /\A[-+]?\d+\z/
516
+
517
+ ##============================================================##
518
+ ##
510
519
  ## Re-add quotes if the key is in the list of reserved keys or is an integer
511
520
  ##============================================================##
512
- key = key.gsub(/\A(['“”‘’"]?)(.*)\1\z/, '\2')
513
521
  key = "\"#{key}\"" if RESERVED_KEYS.include?(key) || is_int
514
522
  key
515
523
  end
@@ -543,7 +551,7 @@ module ImmosquareYaml
543
551
  ## \v: corresponds to a vertical tab
544
552
  ## We keep the \n
545
553
  ##============================================================##
546
- value = value.gsub(/[\t\r\f\v]+/, "")
554
+ value = value.gsub(/[\t\r\f\v]+/, NOTHING)
547
555
 
548
556
  ##============================================================##
549
557
  ## Replace multiple spaces with a single space.
@@ -561,10 +569,11 @@ module ImmosquareYaml
561
569
  value = value.gsub(WEIRD_QUOTES_REGEX, SIMPLE_QUOTE)
562
570
 
563
571
  ##============================================================##
564
- ## Remove quotes surrounding the value if they are present.
572
+ ## Remove all quotes surrounding the value if they are present.
565
573
  ## They will be re-added later if necessary.
574
+ ## """"value"""" => value
566
575
  ##============================================================##
567
- value = value[1..-2] if (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
576
+ value = value[1..-2] while (value.start_with?(DOUBLE_QUOTE) && value.end_with?(DOUBLE_QUOTE)) || (value.start_with?(SIMPLE_QUOTE) && value.end_with?(SIMPLE_QUOTE))
568
577
 
569
578
  ##============================================================##
570
579
  ## Convert emoji representations such as \U0001F600 to their respective emojis.
@@ -603,32 +612,6 @@ module ImmosquareYaml
603
612
  value
604
613
  end
605
614
 
606
- ##============================================================##
607
- ## Deep transform values resursively
608
- ##============================================================##
609
- def deep_transform_values(hash, &block)
610
- hash.transform_values do |value|
611
- if value.is_a?(Hash)
612
- deep_transform_values(value, &block)
613
- else
614
- block.call(value)
615
- end
616
- end
617
- end
618
-
619
- ##============================================================##
620
- ## sort_by_key Function
621
- ## Purpose: Sort a hash by its keys, optionally recursively, with
622
- ## case-insensitive comparison and stripping of double quotes.
623
- ## ============================================================ #
624
- def sort_by_key(hash, recursive = false, &block)
625
- block ||= proc {|a, b| a.to_s.downcase.gsub(DOUBLE_QUOTE, "") <=> b.to_s.downcase.gsub(DOUBLE_QUOTE, "") }
626
- hash.keys.sort(&block).each_with_object({}) do |key, seed|
627
- seed[key] = hash[key]
628
- seed[key] = sort_by_key(seed[key], true, &block) if recursive && seed[key].is_a?(Hash)
629
- end
630
- end
631
-
632
615
  ##============================================================##
633
616
  ## parse_xml Function
634
617
  ## Purpose: Parse an XML file into a nested hash representation.
@@ -657,7 +640,7 @@ module ImmosquareYaml
657
640
  ##============================================================##
658
641
  ## Check for blank lines (which can be present within multi-line blocks)
659
642
  ##============================================================##
660
- blank_line = line.gsub(NEWLINE, "").empty?
643
+ blank_line = line.gsub(NEWLINE, NOTHING).empty?
661
644
 
662
645
  ##============================================================##
663
646
  ## Split the line into key and value.
@@ -685,9 +668,9 @@ module ImmosquareYaml
685
668
  ## We no longer have the >
686
669
  ## because it is transformed in the clean_xml into |
687
670
  ##============================================================##
688
- elsif line.gsub("#{key}:", "").strip.start_with?("|")
671
+ elsif line.gsub("#{key}:", NOTHING).strip.start_with?("|")
689
672
  inblock = indent_level
690
- block_type = line.gsub("#{key}:", "").strip
673
+ block_type = line.gsub("#{key}:", NOTHING).strip
691
674
  result = last_keys.reduce(nested_hash) {|hash, k| hash[k] }
692
675
  result[key] = [block_type, []]
693
676
  last_keys << key
@@ -0,0 +1,27 @@
1
+ namespace :immosquare_yaml do
2
+
3
+ ##============================================================##
4
+ ## Function to translate translation files in rails app
5
+ ##============================================================##
6
+ desc "Translate translation files in rails app"
7
+ task :translate => :environment do
8
+ source_locale = "fr"
9
+ locales = I18n.available_locales.map(&:to_s).reject {|l| l == source_locale }
10
+ Dir.glob("#{Rails.root}/config/locales/**/*#{source_locale}.yml").each do |file|
11
+ locales.each do |locale|
12
+ ImmosquareYaml::Translate.translate(file, locale)
13
+ end
14
+ end
15
+ end
16
+
17
+ ##============================================================##
18
+ ## Function to clean translation files in rails app
19
+ ##============================================================##
20
+ desc "Clean translation files in rails app"
21
+ task :clean => :environment do
22
+ Dir.glob("#{Rails.root}/config/locales/**/*.yml").each do |file|
23
+ ImmosquareYaml.clean(file)
24
+ end
25
+ end
26
+
27
+ end
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: immosquare-yaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - IMMO SQUARE
8
8
  autorequire:
9
- bindir: exe
9
+ bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-21 00:00:00.000000000 Z
11
+ date: 2023-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: iso-639
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">"
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.5
19
+ version: 0.3.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">"
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.5
26
+ version: 0.3.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.21.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.21.0
27
41
  description: IMMOSQUARE-YAML is a specialized Ruby gem tailored primarily for parsing
28
42
  and dumping YML translation files, addressing challenges faced with other parsers
29
43
  like interpreting translation keys as booleans, multi-line strings, and more.
@@ -34,7 +48,12 @@ extensions: []
34
48
  extra_rdoc_files: []
35
49
  files:
36
50
  - lib/immosquare-yaml.rb
37
- - lib/version.rb
51
+ - lib/immosquare-yaml/configuration.rb
52
+ - lib/immosquare-yaml/railtie.rb
53
+ - lib/immosquare-yaml/shared_methods.rb
54
+ - lib/immosquare-yaml/translate.rb
55
+ - lib/immosquare-yaml/version.rb
56
+ - lib/tasks/immosquare-yaml.rake
38
57
  homepage: https://github.com/IMMOSQUARE/immosquare-yaml
39
58
  licenses:
40
59
  - MIT
@@ -47,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
47
66
  requirements:
48
67
  - - ">="
49
68
  - !ruby/object:Gem::Version
50
- version: 2.6.0
69
+ version: 2.7.2
51
70
  required_rubygems_version: !ruby/object:Gem::Requirement
52
71
  requirements:
53
72
  - - ">="
data/lib/version.rb DELETED
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ImmosquareYaml
4
- VERSION = "0.1.1"
5
- end