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.
- checksums.yaml +4 -4
- data/README.md +40 -6
- data/app/assets/javascripts/lit/lit_frontend.js +20 -12
- data/app/assets/stylesheets/lit/application.css +3 -0
- data/app/controllers/lit/api/v1/base_controller.rb +1 -1
- data/app/controllers/lit/api/v1/locales_controller.rb +1 -1
- data/app/controllers/lit/api/v1/localization_keys_controller.rb +12 -4
- data/app/controllers/lit/api/v1/localizations_controller.rb +25 -9
- data/app/controllers/lit/application_controller.rb +11 -8
- data/app/controllers/lit/concerns/request_info_store.rb +1 -0
- data/app/controllers/lit/incomming_localizations_controller.rb +22 -15
- data/app/controllers/lit/locales_controller.rb +1 -1
- data/app/controllers/lit/localization_keys_controller.rb +44 -18
- data/app/controllers/lit/localizations_controller.rb +16 -11
- data/app/controllers/lit/sources_controller.rb +9 -13
- data/app/helpers/lit/frontend_helper.rb +28 -16
- data/app/helpers/lit/localizations_helper.rb +2 -1
- data/app/jobs/lit/synchronize_source_job.rb +1 -1
- data/app/models/lit/incomming_localization.rb +71 -41
- data/app/models/lit/locale.rb +11 -13
- data/app/models/lit/localization.rb +26 -24
- data/app/models/lit/localization_key.rb +46 -55
- data/app/models/lit/source.rb +17 -74
- data/app/queries/localization_key_search_query.rb +80 -0
- data/app/services/remote_interactor_service.rb +45 -0
- data/app/services/synchronize_source_service.rb +63 -0
- data/app/views/kaminari/lit/_gap.html.erb +1 -1
- data/app/views/layouts/lit/_navigation.html.erb +1 -1
- data/app/views/lit/dashboard/index.html.erb +2 -2
- data/app/views/lit/incomming_localizations/index.html.erb +10 -6
- data/app/views/lit/localization_keys/_localization_row.html.erb +1 -1
- data/app/views/lit/localization_keys/_localizations_list.html.erb +84 -0
- data/app/views/lit/localization_keys/_sidebar.html.erb +66 -0
- data/app/views/lit/localization_keys/change_completed.js.erb +2 -0
- data/app/views/lit/localization_keys/index.html.erb +3 -121
- data/app/views/lit/localization_keys/not_translated.html.erb +9 -0
- data/app/views/lit/localization_keys/restore_deleted.js.erb +1 -0
- data/app/views/lit/localization_keys/visited_again.html.erb +9 -0
- data/app/views/lit/localizations/_previous_versions_rows.html.erb +2 -2
- data/app/views/lit/localizations/change_completed.js.erb +2 -0
- data/app/views/lit/localizations/update.js.erb +2 -0
- data/app/views/lit/sources/_form.html.erb +1 -1
- data/app/views/lit/sources/index.html.erb +1 -1
- data/config/routes.rb +5 -0
- data/db/migrate/20181017123839_lit_add_is_deleted_to_localization_keys.rb +8 -0
- data/db/migrate/20181018075955_lit_add_localization_key_is_deleted_to_localization_keys.rb +8 -0
- data/db/migrate/20181030111522_lit_add_is_visited_again_to_localization_keys.rb +8 -0
- data/lib/generators/lit/install/templates/initializer.rb +5 -1
- data/lib/lit.rb +1 -0
- data/lib/lit/adapters/redis_storage.rb +1 -1
- data/lib/lit/cache.rb +32 -54
- data/lib/lit/export.rb +80 -0
- data/lib/lit/i18n_backend.rb +12 -9
- data/lib/lit/import.rb +179 -0
- data/lib/lit/loader.rb +2 -0
- data/lib/lit/version.rb +1 -1
- data/lib/tasks/lit_tasks.rake +78 -13
- metadata +21 -6
@@ -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
|
@@ -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
|
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
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.
|
68
|
-
localizations[key] = 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([
|
90
|
-
localizations[l.full_key] = l.
|
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.
|
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 =
|
194
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
290
|
-
|
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
|
-
|
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.
|
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
|
data/lib/lit/i18n_backend.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|