lit 0.3.3 → 0.4.0.pre.alpha

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -6
  3. data/app/assets/javascripts/lit/lit_frontend.js +20 -12
  4. data/app/assets/stylesheets/lit/application.css +3 -0
  5. data/app/controllers/lit/api/v1/base_controller.rb +1 -1
  6. data/app/controllers/lit/api/v1/locales_controller.rb +1 -1
  7. data/app/controllers/lit/api/v1/localization_keys_controller.rb +12 -4
  8. data/app/controllers/lit/api/v1/localizations_controller.rb +25 -9
  9. data/app/controllers/lit/application_controller.rb +11 -8
  10. data/app/controllers/lit/concerns/request_info_store.rb +1 -0
  11. data/app/controllers/lit/incomming_localizations_controller.rb +22 -15
  12. data/app/controllers/lit/locales_controller.rb +1 -1
  13. data/app/controllers/lit/localization_keys_controller.rb +44 -18
  14. data/app/controllers/lit/localizations_controller.rb +16 -11
  15. data/app/controllers/lit/sources_controller.rb +9 -13
  16. data/app/helpers/lit/frontend_helper.rb +28 -16
  17. data/app/helpers/lit/localizations_helper.rb +2 -1
  18. data/app/jobs/lit/synchronize_source_job.rb +1 -1
  19. data/app/models/lit/incomming_localization.rb +71 -41
  20. data/app/models/lit/locale.rb +11 -13
  21. data/app/models/lit/localization.rb +26 -24
  22. data/app/models/lit/localization_key.rb +46 -55
  23. data/app/models/lit/source.rb +17 -74
  24. data/app/queries/localization_key_search_query.rb +80 -0
  25. data/app/services/remote_interactor_service.rb +45 -0
  26. data/app/services/synchronize_source_service.rb +63 -0
  27. data/app/views/kaminari/lit/_gap.html.erb +1 -1
  28. data/app/views/layouts/lit/_navigation.html.erb +1 -1
  29. data/app/views/lit/dashboard/index.html.erb +2 -2
  30. data/app/views/lit/incomming_localizations/index.html.erb +10 -6
  31. data/app/views/lit/localization_keys/_localization_row.html.erb +1 -1
  32. data/app/views/lit/localization_keys/_localizations_list.html.erb +84 -0
  33. data/app/views/lit/localization_keys/_sidebar.html.erb +66 -0
  34. data/app/views/lit/localization_keys/change_completed.js.erb +2 -0
  35. data/app/views/lit/localization_keys/index.html.erb +3 -121
  36. data/app/views/lit/localization_keys/not_translated.html.erb +9 -0
  37. data/app/views/lit/localization_keys/restore_deleted.js.erb +1 -0
  38. data/app/views/lit/localization_keys/visited_again.html.erb +9 -0
  39. data/app/views/lit/localizations/_previous_versions_rows.html.erb +2 -2
  40. data/app/views/lit/localizations/change_completed.js.erb +2 -0
  41. data/app/views/lit/localizations/update.js.erb +2 -0
  42. data/app/views/lit/sources/_form.html.erb +1 -1
  43. data/app/views/lit/sources/index.html.erb +1 -1
  44. data/config/routes.rb +5 -0
  45. data/db/migrate/20181017123839_lit_add_is_deleted_to_localization_keys.rb +8 -0
  46. data/db/migrate/20181018075955_lit_add_localization_key_is_deleted_to_localization_keys.rb +8 -0
  47. data/db/migrate/20181030111522_lit_add_is_visited_again_to_localization_keys.rb +8 -0
  48. data/lib/generators/lit/install/templates/initializer.rb +5 -1
  49. data/lib/lit.rb +1 -0
  50. data/lib/lit/adapters/redis_storage.rb +1 -1
  51. data/lib/lit/cache.rb +32 -54
  52. data/lib/lit/export.rb +80 -0
  53. data/lib/lit/i18n_backend.rb +12 -9
  54. data/lib/lit/import.rb +179 -0
  55. data/lib/lit/loader.rb +2 -0
  56. data/lib/lit/version.rb +1 -1
  57. data/lib/tasks/lit_tasks.rake +78 -13
  58. metadata +21 -6
@@ -0,0 +1,8 @@
1
+ class LitAddIsDeletedToLocalizationKeys < Rails::VERSION::MAJOR >= 5 ?
2
+ ActiveRecord::Migration[4.2] :
3
+ ActiveRecord::Migration
4
+ def change
5
+ add_column :lit_localization_keys, :is_deleted, :boolean,
6
+ default: false, null: false
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class LitAddLocalizationKeyIsDeletedToLocalizationKeys < Rails::VERSION::MAJOR >= 5 ?
2
+ ActiveRecord::Migration[4.2] :
3
+ ActiveRecord::Migration
4
+ def change
5
+ add_column :lit_incomming_localizations, :localization_key_is_deleted,
6
+ :boolean, null: false, default: false
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ class LitAddIsVisitedAgainToLocalizationKeys < Rails::VERSION::MAJOR >= 5 ?
2
+ ActiveRecord::Migration[4.2] :
3
+ ActiveRecord::Migration
4
+ def change
5
+ add_column :lit_localization_keys, :is_visited_again, :boolean,
6
+ null: false, default: false
7
+ end
8
+ end
@@ -12,6 +12,10 @@ Lit.authentication_verification = <%= @authentication_verification || 'nil' %>
12
12
  # environment
13
13
  Lit.key_value_engine = '<%= @key_value_engine %>'
14
14
 
15
+ # Redis URL to use when key_value_engine is 'redis'
16
+ # When set to `nil`, it uses the REDIS_URL environment variable.
17
+ # Lit.redis_url = 'redis://redis-server:6379/0'
18
+
15
19
  # Pass extra options to key_value_neinge, ie. prefix for redis (only one
16
20
  # supported at the moment)
17
21
  # Lit.storage_options = { prefix: "my_project" }
@@ -43,7 +47,7 @@ Lit.set_last_updated_at_upon_creation = true
43
47
 
44
48
  # Store request info - this will store in cache additional info about request
45
49
  # path that triggered translation key to be displayed / accessed
46
- # For more infor please check README.md
50
+ # For more information please check the README.md
47
51
  Lit.store_request_info = false
48
52
 
49
53
  # Initialize lit
data/lib/lit.rb CHANGED
@@ -5,6 +5,7 @@ module Lit
5
5
  mattr_accessor :authentication_function
6
6
  mattr_accessor :authentication_verification
7
7
  mattr_accessor :key_value_engine
8
+ mattr_accessor :redis_url
8
9
  mattr_accessor :storage_options
9
10
  mattr_accessor :humanize_key
10
11
  mattr_accessor :ignored_keys
@@ -8,7 +8,7 @@ module Lit
8
8
  end
9
9
 
10
10
  def determine_redis_provider
11
- ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
11
+ Lit.redis_url || ENV[ENV['REDIS_PROVIDER'] || 'REDIS_URL']
12
12
  end
13
13
 
14
14
  class RedisStorage
data/lib/lit/cache.rb CHANGED
@@ -64,8 +64,8 @@ module Lit
64
64
  locale_key, key_without_locale = split_key(key)
65
65
  locale = find_locale(locale_key)
66
66
  localization = find_localization(locale, key_without_locale, value: value, force_array: force_array, update_value: true)
67
- return localization.get_value if startup_process && localization.is_changed?
68
- localizations[key] = localization.get_value if localization
67
+ return localization.translation if startup_process && localization.is_changed?
68
+ localizations[key] = localization.translation if localization
69
69
  end
70
70
 
71
71
  def update_cache(key, value)
@@ -82,12 +82,12 @@ module Lit
82
82
  end
83
83
 
84
84
  def load_all_translations
85
- first = Localization.order(id: :asc).first
86
- last = Localization.order(id: :desc).first
85
+ first = Localization.active.order(id: :asc).first
86
+ last = Localization.active.order(id: :desc).first
87
87
  if !first || (!localizations.has_key?(first.full_key) ||
88
88
  !localizations.has_key?(last.full_key))
89
- Localization.includes([:locale, :localization_key]).find_each do |l|
90
- localizations[l.full_key] = l.get_value
89
+ Localization.includes(%i[locale localization_key]).active.find_each do |l|
90
+ localizations[l.full_key] = l.translation
91
91
  end
92
92
  end
93
93
  end
@@ -97,7 +97,7 @@ module Lit
97
97
  locale_key, key_without_locale = split_key(key)
98
98
  locale = find_locale(locale_key)
99
99
  localization = find_localization(locale, key_without_locale, default_fallback: true)
100
- localizations[key] = localization.get_value if localization
100
+ localizations[key] = localization.translation if localization
101
101
  end
102
102
 
103
103
  def delete_key(key)
@@ -126,40 +126,6 @@ module Lit
126
126
  @locale_cache[locale_key]
127
127
  end
128
128
 
129
- # this comes directly from copycopter.
130
- def export
131
- reset
132
- localizations_scope = Lit::Localization
133
- unless ENV['LOCALES'].blank?
134
- locale_keys = ENV['LOCALES'].to_s.split(',') || []
135
- locale_ids = Lit::Locale.where(locale: locale_keys).pluck(:id)
136
- localizations_scope = localizations_scope.where(locale_id: locale_ids) unless locale_ids.empty?
137
- end
138
- db_localizations = {}
139
- localizations_scope.find_each do |l|
140
- db_localizations[l.full_key] = l.get_value
141
- end
142
- exported_keys = nested_string_keys_to_hash(db_localizations)
143
- exported_keys.to_yaml
144
- end
145
-
146
- def nested_string_keys_to_hash(db_localizations)
147
- # http://subtech.g.hatena.ne.jp/cho45/20061122
148
- deep_proc = proc do |_k, s, o|
149
- if s.is_a?(Hash) && o.is_a?(Hash)
150
- next s.merge(o, &deep_proc)
151
- end
152
- next o
153
- end
154
- nested_keys = {}
155
- db_localizations.sort.each do |k, v|
156
- key_parts = k.to_s.split('.')
157
- converted = key_parts.reverse.reduce(v) { |a, n| { n => a } }
158
- nested_keys.merge!(converted, &deep_proc)
159
- end
160
- nested_keys
161
- end
162
-
163
129
  def get_global_hits_counter(key)
164
130
  @hits_counter['global_hits_counter.' + key]
165
131
  end
@@ -190,11 +156,14 @@ module Lit
190
156
  return nil if value.is_a?(Hash)
191
157
  ActiveRecord::Base.transaction do
192
158
  localization_key = find_localization_key(key_without_locale)
193
- localization = Lit::Localization.where(locale_id: locale.id). \
194
- where(localization_key_id: localization_key.id).first_or_initialize
159
+ localization =
160
+ Lit::Localization.active
161
+ .where(locale_id: locale.id)
162
+ .where(localization_key_id: localization_key.id)
163
+ .first_or_initialize
195
164
  if update_value || localization.new_record?
196
165
  if value.is_a?(Array)
197
- value = parse_array_value(value) unless force_array
166
+ value = parse_array_value(value, locale) unless force_array
198
167
  elsif !value.nil?
199
168
  value = parse_value(value, locale)
200
169
  else
@@ -204,7 +173,10 @@ module Lit
204
173
  value = fallback_to_default(localization_key, localization)
205
174
  end
206
175
  end
207
- localization.update_default_value(value)
176
+ # Prevent overwriting existing default value with nil.
177
+ # However, if the localization record is #new_record?, we still need
178
+ # to insert it with an empty default value.
179
+ localization.update_default_value(value) if localization.new_record? || value
208
180
  end
209
181
  return localization
210
182
  end
@@ -236,8 +208,8 @@ module Lit
236
208
  def find_localization_for_delete(locale, key_without_locale)
237
209
  localization_key = find_localization_key_for_delete(key_without_locale)
238
210
  return nil unless localization_key
239
- Lit::Localization.find_by(locale_id: locale.id,
240
- localization_key_id: localization_key.id)
211
+ Lit::Localization.active.find_by(locale_id: locale.id,
212
+ localization_key_id: localization_key.id)
241
213
  end
242
214
 
243
215
  def delete_localization(locale, key_without_locale)
@@ -259,9 +231,9 @@ module Lit
259
231
  when Symbol then
260
232
  lk = Lit::LocalizationKey.where(localization_key: v.to_s).first
261
233
  if lk
262
- loca = Lit::Localization.where(locale_id: locale.id).
234
+ loca = Lit::Localization.active.where(locale_id: locale.id).
263
235
  where(localization_key_id: lk.id).first
264
- new_value = loca.get_value if loca && loca.get_value.present?
236
+ new_value = loca.translation if loca && loca.translation.present?
265
237
  end
266
238
  when String then
267
239
  new_value = v
@@ -275,7 +247,7 @@ module Lit
275
247
  new_value
276
248
  end
277
249
 
278
- def parse_array_value(value)
250
+ def parse_array_value(value, locale)
279
251
  new_value = nil
280
252
  value_clone = value.dup
281
253
  while (v = value_clone.shift) && v.present?
@@ -286,10 +258,12 @@ module Lit
286
258
  end
287
259
 
288
260
  def find_localization_key(key_without_locale)
289
- unless localization_keys.key?(key_without_locale)
290
- find_or_create_localization_key(key_without_locale)
261
+ if localization_keys.key?(key_without_locale)
262
+ Lit::LocalizationKey.find_by(
263
+ id: localization_keys[key_without_locale]
264
+ ) || find_or_create_localization_key(key_without_locale)
291
265
  else
292
- Lit::LocalizationKey.find_by(id: localization_keys[key_without_locale]) || find_or_create_localization_key(key_without_locale)
266
+ find_or_create_localization_key(key_without_locale)
293
267
  end
294
268
  end
295
269
 
@@ -303,7 +277,11 @@ module Lit
303
277
  end
304
278
 
305
279
  def find_or_create_localization_key(key_without_locale)
306
- localization_key = Lit::LocalizationKey.where(localization_key: key_without_locale).first_or_create!
280
+ localization_key = Lit::LocalizationKey.find_or_initialize_by(
281
+ localization_key: key_without_locale
282
+ )
283
+ localization_key.is_visited_again = true if localization_key.is_deleted?
284
+ localization_key.save! if localization_key.changed?
307
285
  localization_keys[key_without_locale] = localization_key.id
308
286
  localization_key
309
287
  end
data/lib/lit/export.rb ADDED
@@ -0,0 +1,80 @@
1
+ require 'csv'
2
+
3
+ module Lit
4
+ class Export
5
+ def self.call(locale_keys:, format:)
6
+ raise ArgumentError, "format must be yaml or csv" if %i[yaml csv].exclude?(format)
7
+ Lit.loader.cache.load_all_translations
8
+ localizations_scope = Lit::Localization.active
9
+ if locale_keys.present?
10
+ locale_ids = Lit::Locale.where(locale: locale_keys).pluck(:id)
11
+ localizations_scope = localizations_scope.where(locale_id: locale_ids) unless locale_ids.empty?
12
+ end
13
+ db_localizations = {}
14
+ localizations_scope.find_each do |l|
15
+ db_localizations[l.full_key] = l.translation
16
+ end
17
+
18
+ case format
19
+ when :yaml
20
+ exported_keys = nested_string_keys_to_hash(db_localizations)
21
+ exported_keys.to_yaml
22
+ when :csv
23
+ relevant_locales = locale_keys.presence || I18n.available_locales.map(&:to_s)
24
+ CSV.generate do |csv|
25
+ csv << ['key', *relevant_locales]
26
+ keys_without_locales = db_localizations.keys.map { |k| k.gsub(/(#{relevant_locales.join('|')})\./, '') }.uniq
27
+ keys_without_locales.each do |key_without_locale|
28
+ # Here, we need to determine if we're dealing with an array or a scalar.
29
+ # In the former case, for simplicity of editing (which is likely the main
30
+ # intent when exporting translations to CSV), let's make the "array" be simulated
31
+ # as a number of consecutive rows that have the same key.
32
+ #
33
+ # For example:
34
+ #
35
+ # key,en
36
+ # date.abbr_month_names, <-- in this case it's empty because that array has nothing at [0]
37
+ # date.abbr_month_names,Jan
38
+ # date.abbr_month_names,Feb
39
+ # date.abbr_month_names,Mar
40
+ # date.abbr_month_names,Apr
41
+ # date.abbr_month_names,May
42
+ # ...
43
+
44
+ key_localizations_per_locale =
45
+ relevant_locales.map { |l| Array.wrap(db_localizations["#{l}.#{key_without_locale}"]) }
46
+ transpose(key_localizations_per_locale).each do |translation_series|
47
+ csv << [key_without_locale, *translation_series]
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ private_class_method def self.nested_string_keys_to_hash(db_localizations)
55
+ # http://subtech.g.hatena.ne.jp/cho45/20061122
56
+ deep_proc = proc do |_k, s, o|
57
+ if s.is_a?(Hash) && o.is_a?(Hash)
58
+ next s.merge(o, &deep_proc)
59
+ end
60
+ next o
61
+ end
62
+ nested_keys = {}
63
+ db_localizations.sort.each do |k, v|
64
+ key_parts = k.to_s.split('.')
65
+ converted = key_parts.reverse.reduce(v) { |a, n| { n => a } }
66
+ nested_keys.merge!(converted, &deep_proc)
67
+ end
68
+ nested_keys
69
+ end
70
+
71
+ # This is like Array#transpose but ignores size differences between inner arrays.
72
+ private_class_method def self.transpose(matrix)
73
+ maxlen = matrix.max { |x| x.length }.length
74
+ matrix.each do |array|
75
+ array[maxlen - 1] = nil if array.length < maxlen
76
+ end
77
+ matrix.transpose
78
+ end
79
+ end
80
+ end
@@ -3,6 +3,7 @@ require 'i18n'
3
3
  module Lit
4
4
  class I18nBackend
5
5
  include I18n::Backend::Simple::Implementation
6
+ include I18n::Backend::Pluralization
6
7
 
7
8
  attr_reader :cache
8
9
 
@@ -10,10 +11,9 @@ module Lit
10
11
  @cache = cache
11
12
  @available_locales_cache = nil
12
13
  @translations = {}
13
- reserved_keys = I18n.const_get :RESERVED_KEYS
14
- reserved_keys << :lit_default_copy
14
+ reserved_keys = I18n.const_get(:RESERVED_KEYS) + %i[lit_default_copy]
15
15
  I18n.send(:remove_const, :RESERVED_KEYS)
16
- I18n.const_set(:RESERVED_KEYS, reserved_keys)
16
+ I18n.const_set(:RESERVED_KEYS, reserved_keys.freeze)
17
17
  end
18
18
 
19
19
  def translate(locale, key, options = {})
@@ -59,7 +59,8 @@ module Lit
59
59
  return true if options[:default].is_a?(String)
60
60
  return true if options[:default].is_a?(Array) && \
61
61
  (options[:default].first.is_a?(String) || \
62
- options[:default].first.is_a?(Symbol))
62
+ options[:default].first.is_a?(Symbol) || \
63
+ options[:default].first.is_a?(Array))
63
64
  false
64
65
  end
65
66
 
@@ -69,11 +70,11 @@ module Lit
69
70
  parts = I18n.normalize_keys(locale, key, scope, options[:separator])
70
71
  key_with_locale = parts.join('.')
71
72
 
72
- ## check in cache or in simple backend
73
+ # check in cache or in simple backend
73
74
  content = @cache[key_with_locale] || super
74
75
  return content if parts.size <= 1
75
76
 
76
- if content.nil? && should_cache?(key_with_locale)
77
+ if content.nil? && should_cache?(key_with_locale, options)
77
78
  new_content = @cache.init_key_with_value(key_with_locale, content)
78
79
  content = new_content if content.nil? # Content can change when Lit.humanize is true for example
79
80
  # so there is no content in cache - it might not be if ie. we're doing
@@ -116,7 +117,7 @@ module Lit
116
117
  end
117
118
  end
118
119
  end
119
- ## return translated content
120
+ # return translated content
120
121
  content
121
122
  end
122
123
 
@@ -180,8 +181,10 @@ module Lit
180
181
  Lit.ignored_keys.any?{ |k| key_without_locale.start_with?(k) }
181
182
  end
182
183
 
183
- def should_cache?(key_with_locale)
184
- return false if @cache.has_key?(key_with_locale)
184
+ def should_cache?(key_with_locale, options)
185
+ if @cache.has_key?(key_with_locale)
186
+ return false unless options[:default]
187
+ end
185
188
 
186
189
  _, key_without_locale = ::Lit::Cache.split_key(key_with_locale)
187
190
  return false if is_ignored_key(key_without_locale)
data/lib/lit/import.rb ADDED
@@ -0,0 +1,179 @@
1
+ require 'csv'
2
+
3
+ module Lit
4
+ class Import
5
+ class << self
6
+ def call(*args)
7
+ new(*args).perform
8
+ end
9
+ end
10
+
11
+ attr_reader :input, :locale_keys, :format, :skip_nil
12
+
13
+ def initialize(input:, locale_keys: [], format:, skip_nil: true, dry_run: false, raw: false)
14
+ raise ArgumentError, 'format must be yaml or csv' if %i[yaml csv].exclude?(format.to_sym)
15
+ @input = input
16
+ @locale_keys = locale_keys.presence || I18n.available_locales
17
+ @format = format
18
+ @skip_nil = skip_nil
19
+ @dry_run = dry_run
20
+ @raw = raw
21
+ end
22
+
23
+ def perform
24
+ send(:"import_#{format}")
25
+ end
26
+
27
+ private
28
+
29
+ def import_yaml
30
+ validate_yaml
31
+ locale_keys.each do |locale|
32
+ I18n.with_locale(locale) do
33
+ yml = parsed_yaml[locale.to_s]
34
+ Hash[*Lit::Cache.flatten_hash(yml)].each do |key, default_translation|
35
+ next if default_translation.nil? && skip_nil
36
+ puts key
37
+ upsert(locale, key, default_translation)
38
+ end
39
+ end
40
+ end
41
+ rescue Psych::SyntaxError => e
42
+ raise ArgumentError, "Invalid YAML file: #{e.message}", cause: e
43
+ end
44
+
45
+ def import_csv
46
+ validate_csv
47
+ processed_csv = preprocess_csv
48
+
49
+ processed_csv.each do |row|
50
+ key = row.first
51
+ row_translations = Hash[locales_in_csv.zip(row.drop(1))]
52
+ row_translations.each do |locale, value|
53
+ next unless locale_keys.blank? || locale_keys.map(&:to_sym).include?(locale.to_sym)
54
+ next if value.nil? && skip_nil
55
+ puts key
56
+ upsert(locale, key, value)
57
+ end
58
+ end
59
+ rescue CSV::MalformedCSVError => e
60
+ raise ArgumentError, "Invalid CSV file: #{e.message}", cause: e
61
+ end
62
+
63
+ def validate_yaml
64
+ errors = []
65
+
66
+ # YAML.load can return false, hence not using #empty?
67
+ errors << :yaml_is_empty if parsed_yaml.blank?
68
+
69
+ if parsed_yaml.present? &&
70
+ (locale_keys.map(&:to_sym) - parsed_yaml.keys.map(&:to_sym)).any?
71
+ errors << :not_all_requested_locales_included_in_header
72
+ end
73
+
74
+ fail ArgumentError, errors.map { |e| e.to_s.humanize }.to_sentence if errors.any?
75
+ end
76
+
77
+ def validate_csv # rubocop:disable Metrics/AbcSize
78
+ errors = []
79
+
80
+ # CSV may not be empty
81
+ errors << :csv_is_empty if parsed_csv.empty?
82
+
83
+ # verify CSV header
84
+ if !parsed_csv.empty? &&
85
+ (locale_keys.map(&:to_s) - parsed_csv[0].drop(1)).any?
86
+ errors << :not_all_requested_locales_included_in_header
87
+ end
88
+
89
+ # any further checks that we at some time think of should fall here
90
+
91
+ fail ArgumentError, errors.map { |e| e.to_s.humanize }.to_sentence if errors.any?
92
+ end
93
+
94
+ # the main task of this routine is to replace blanks with nils (in CSV it cannot be distinguished,
95
+ # so in order for :skip_nil option to work as intended blanks must be treated as nil);
96
+ # as well as that, we need to look for multiple occurrences of certain keys and merge them
97
+ # into arrays
98
+ def preprocess_csv
99
+ concatenate_arrays(replace_blanks(parsed_csv))
100
+ end
101
+
102
+ def parsed_csv
103
+ @parsed_csv ||=
104
+ begin
105
+ CSV.parse(input)
106
+ rescue CSV::MalformedCSVError
107
+ # Some Excel versions tend to save CSVs with columns separated with tabs instead
108
+ # of commas. Let's try that out if needed.
109
+ CSV.parse(input, col_sep: "\t")
110
+ end
111
+ end
112
+
113
+ def parsed_yaml
114
+ @parsed_yaml ||= YAML.load(input)
115
+ end
116
+
117
+ def locales_in_csv
118
+ @locales_in_csv ||= parsed_csv.first.drop(1)
119
+ end
120
+
121
+ # This is mean to insert a value for a key in a given locale
122
+ # using some kind of strategy which depends on the service's options.
123
+ #
124
+ # For instance, when @raw option is true (it's the default),
125
+ # if a key already exists, it overrides the default_value of the
126
+ # existing localization key; otherwise, with @raw set to false,
127
+ # it keeps the default as it is and, no matter if a translated value
128
+ # is there, translated_value is overridden with the imported one
129
+ # and is_changed is set to true.
130
+ def upsert(locale, key, value) # rubocop:disable Metrics/MethodLength
131
+ I18n.with_locale(locale) do
132
+ # when an array has to be inserted with a default value, it needs to
133
+ # be done like:
134
+ # I18n.t('foo', default: [['bar', 'baz']])
135
+ # because without the double array, array items are treated as fallback keys
136
+ # - then, the last array element is the final fallback; so in this case we
137
+ # don't specify fallback keys and only specify the final fallback, which
138
+ # is the array
139
+ val = value.is_a?(Array) ? [value] : value
140
+ I18n.t(key, default: val)
141
+ unless @raw
142
+ # this indicates that this translation already exists
143
+ existing_translation =
144
+ Lit::Localization.joins(:locale, :localization_key)
145
+ .find_by('localization_key = ? and locale = ?',
146
+ key, locale)
147
+ if existing_translation
148
+ existing_translation.update(translated_value: value, is_changed: true)
149
+ lkey = existing_translation.localization_key
150
+ lkey.update(is_deleted: false) if lkey.is_deleted
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ def concatenate_arrays(csv) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/LineLength
157
+ csv.inject([]) do |accu, row|
158
+ if row.first == accu.last&.first # equal keys
159
+ accu.tap do
160
+ accu[-1] = [
161
+ row.first,
162
+ *accu[-1].drop(1)
163
+ .map { |x| Array.wrap(x) }
164
+ .zip(row.drop(1)).map(&:flatten)
165
+ ]
166
+ end
167
+ else
168
+ accu << row
169
+ end
170
+ end
171
+ end
172
+
173
+ def replace_blanks(csv)
174
+ csv.drop(1).each do |row|
175
+ row.replace(row.map(&:presence))
176
+ end
177
+ end
178
+ end
179
+ end