lit 0.3.3 → 0.4.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
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