i18n_admin 0.3.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +78 -0
- data/Rakefile +23 -0
- data/app/assets/images/i18n_admin/spinner.gif +0 -0
- data/app/assets/javascripts/i18n_admin/application.js +21 -0
- data/app/assets/javascripts/i18n_admin/src/i18n-admin-job-monitor.coffee +37 -0
- data/app/assets/javascripts/i18n_admin/src/import.coffee +23 -0
- data/app/assets/javascripts/i18n_admin/src/translation.coffee +45 -0
- data/app/assets/stylesheets/i18n_admin/application.css.scss +88 -0
- data/app/controllers/i18n_admin/application_controller.rb +21 -0
- data/app/controllers/i18n_admin/exports_controller.rb +19 -0
- data/app/controllers/i18n_admin/imports_controller.rb +28 -0
- data/app/controllers/i18n_admin/translations_controller.rb +30 -0
- data/app/helpers/i18n_admin/application_helper.rb +40 -0
- data/app/models/i18n_admin/export_file.rb +4 -0
- data/app/models/i18n_admin/import_file.rb +4 -0
- data/app/models/i18n_admin/import_job.rb +4 -0
- data/app/models/i18n_admin/resource_file.rb +8 -0
- data/app/models/i18n_admin/translations_set.rb +5 -0
- data/app/models/i18n_admin/whitelisted_resource.rb +9 -0
- data/app/views/i18n_admin/exports/queued.html.haml +18 -0
- data/app/views/i18n_admin/imports/errors/_resource_invalid.html.erb +0 -0
- data/app/views/i18n_admin/imports/errors/_resource_not_found.html.erb +0 -0
- data/app/views/i18n_admin/imports/new.html.erb +61 -0
- data/app/views/i18n_admin/imports/processing.html.erb +33 -0
- data/app/views/i18n_admin/imports/queued.html.haml +0 -0
- data/app/views/i18n_admin/translations/_pagination.html.erb +3 -0
- data/app/views/i18n_admin/translations/_translation.html.erb +9 -0
- data/app/views/i18n_admin/translations/index.html.erb +81 -0
- data/app/views/layouts/i18n_admin/application.html.haml +11 -0
- data/config/locales/i18n_admin.en.yml +38 -0
- data/config/locales/i18n_admin.fr.yml +52 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20140829094415_create_i18n_admin_translations_sets.rb +12 -0
- data/db/migrate/20150825133437_create_i18n_admin_import_jobs.rb +11 -0
- data/db/migrate/20150828074645_create_i18n_admin_whitelisted_resources.rb +12 -0
- data/db/migrate/20160404161658_create_i18n_admin_resource_files.rb +11 -0
- data/lib/ext/paperclip.rb +5 -0
- data/lib/generators/i18n_admin/install/install_generator.rb +31 -0
- data/lib/generators/i18n_admin/install/templates/initializer.rb +28 -0
- data/lib/i18n_admin.rb +51 -0
- data/lib/i18n_admin/engine.rb +28 -0
- data/lib/i18n_admin/errors.rb +10 -0
- data/lib/i18n_admin/errors/base.rb +23 -0
- data/lib/i18n_admin/errors/collection.rb +24 -0
- data/lib/i18n_admin/errors/resource_invalid.rb +7 -0
- data/lib/i18n_admin/errors/resource_not_found.rb +7 -0
- data/lib/i18n_admin/export.rb +14 -0
- data/lib/i18n_admin/export/base.rb +129 -0
- data/lib/i18n_admin/export/xls.rb +110 -0
- data/lib/i18n_admin/hstore_backend.rb +68 -0
- data/lib/i18n_admin/import.rb +16 -0
- data/lib/i18n_admin/import/base.rb +64 -0
- data/lib/i18n_admin/import/job.rb +17 -0
- data/lib/i18n_admin/import/xls.rb +58 -0
- data/lib/i18n_admin/request_store.rb +11 -0
- data/lib/i18n_admin/translation.rb +27 -0
- data/lib/i18n_admin/translation_collection.rb +34 -0
- data/lib/i18n_admin/translations.rb +96 -0
- data/lib/i18n_admin/version.rb +3 -0
- data/lib/tasks/i18n_admin_tasks.rake +35 -0
- metadata +307 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'i18n/backend/key_value'
|
2
|
+
|
3
|
+
module I18nAdmin
|
4
|
+
class HstoreBackend < I18n::Backend::KeyValue
|
5
|
+
def available_locales
|
6
|
+
@available_locales ||= I18nAdmin::TranslationsSet.pluck(:locale)
|
7
|
+
end
|
8
|
+
|
9
|
+
class Store
|
10
|
+
include I18nAdmin::RequestStore
|
11
|
+
|
12
|
+
def [](path)
|
13
|
+
locale, key = locale_and_key_from(path)
|
14
|
+
cached_translations_for(locale).translations[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def store_translations(locale, key, value)
|
18
|
+
translations_set = translations_set_for(locale)
|
19
|
+
translations_set.translations[key] = value
|
20
|
+
translations_set.translations_will_change!
|
21
|
+
translations_set.save
|
22
|
+
value
|
23
|
+
end
|
24
|
+
|
25
|
+
def translations_for(locale)
|
26
|
+
translations_set_for(locale).translations
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def locale_and_key_from(path)
|
32
|
+
path.split('.', 2)
|
33
|
+
end
|
34
|
+
|
35
|
+
def translations_set_for(locale)
|
36
|
+
model.where(locale: locale).first_or_initialize do |set|
|
37
|
+
set.translations ||= {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def cached_translations_for(locale)
|
42
|
+
store_key = store_key_for(locale, :set)
|
43
|
+
request_store.store[store_key] ||= translations_set_for(locale)
|
44
|
+
end
|
45
|
+
|
46
|
+
def model
|
47
|
+
@model ||= I18nAdmin::TranslationsSet
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@store = HstoreBackend::Store.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def store_translations(locale, data, options = {})
|
56
|
+
data.each do |key, value|
|
57
|
+
store.store_translations(locale, key, value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
|
63
|
+
def lookup(locale, key, scope = [], options = {})
|
64
|
+
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
65
|
+
store["#{locale}.#{key}"]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Import
|
3
|
+
class Base
|
4
|
+
attr_reader :locale
|
5
|
+
|
6
|
+
def self.register(type, import)
|
7
|
+
Import.types[type] = import
|
8
|
+
end
|
9
|
+
|
10
|
+
def errors
|
11
|
+
@errors ||= I18nAdmin::Errors::Collection.new
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def save_updated_models
|
17
|
+
# Save all updated model translations
|
18
|
+
ActiveRecord::Base.transaction do
|
19
|
+
updated_models.each do |key, resource|
|
20
|
+
unless resource.save
|
21
|
+
errors.add(:resource_invalid, key: key, resource: resource)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_translation(key, value)
|
28
|
+
case key
|
29
|
+
when /^models\-/ then update_model_translation(key, value)
|
30
|
+
else update_static_translation(key, value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def update_model_translation(key, value)
|
35
|
+
_, model_name, id, field = key.split('-')
|
36
|
+
update_cache_key = [model_name, id].join('-')
|
37
|
+
|
38
|
+
# Find resource from update cache, or in database
|
39
|
+
unless (resource = updated_models[update_cache_key])
|
40
|
+
model = model_name.constantize
|
41
|
+
resource = model.where(id: id).first
|
42
|
+
end
|
43
|
+
|
44
|
+
# Only update found resources
|
45
|
+
if resource
|
46
|
+
resource.send(:"#{ field }=", value)
|
47
|
+
updated_models[update_cache_key] ||= resource
|
48
|
+
else
|
49
|
+
errors.add(:resource_not_found, {
|
50
|
+
key: update_cache_key, model_name: model_name, id: id
|
51
|
+
})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_static_translation(key, value)
|
56
|
+
I18n.backend.store_translations(locale, key => value)
|
57
|
+
end
|
58
|
+
|
59
|
+
def updated_models
|
60
|
+
@updated_models ||= {}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Import
|
3
|
+
class Job
|
4
|
+
include SuckerPunch::Job
|
5
|
+
|
6
|
+
def perform(locale, file_path, job_id)
|
7
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
8
|
+
import = Import::XLS.new(locale, file_path)
|
9
|
+
job = I18nAdmin::ImportJob.find(job_id)
|
10
|
+
|
11
|
+
state = import.run ? 'success' : 'error'
|
12
|
+
job.update(state: state)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Import
|
3
|
+
class XLS < Import::Base
|
4
|
+
# Used to split cells with large content
|
5
|
+
PAGINATION_PATTERN = /\(\d+ \/ (\d+)\)$/
|
6
|
+
|
7
|
+
register :xls, self
|
8
|
+
|
9
|
+
attr_reader :file, :spreadsheet, :sheet
|
10
|
+
|
11
|
+
def self.import(locale, file)
|
12
|
+
import = new(locale, file)
|
13
|
+
import.run
|
14
|
+
ensure
|
15
|
+
file.close
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(locale, file)
|
19
|
+
@locale = locale
|
20
|
+
@spreadsheet = Spreadsheet.open(file.path)
|
21
|
+
@sheet = spreadsheet.worksheet(0)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
I18n.with_locale(locale) do
|
26
|
+
index = 0
|
27
|
+
|
28
|
+
while (index += 1) < sheet.row_count
|
29
|
+
key, value, index = extract_translation_at(index)
|
30
|
+
update_translation(key, value)
|
31
|
+
end
|
32
|
+
|
33
|
+
save_updated_models
|
34
|
+
end
|
35
|
+
|
36
|
+
errors.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def extract_translation_at(index)
|
42
|
+
key, _, value = sheet.row(index)
|
43
|
+
|
44
|
+
if (pagination = key.match(PAGINATION_PATTERN))
|
45
|
+
pages = pagination[1].to_i
|
46
|
+
key = key.gsub(PAGINATION_PATTERN, '').strip
|
47
|
+
|
48
|
+
(pages - 1).times do |page|
|
49
|
+
index += 1
|
50
|
+
value += sheet.row(index).pop.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
[key, value, index]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module I18nAdmin
|
4
|
+
class Translation
|
5
|
+
include ActiveModel::Model
|
6
|
+
|
7
|
+
attr_accessor :key, :original, :value, :locale
|
8
|
+
|
9
|
+
def persisted?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_param
|
14
|
+
Base64.strict_encode64(key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def matches?(search)
|
18
|
+
[:key, :original, :value].any? do |key|
|
19
|
+
send(key).to_s.match(search)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.key_from(encoded_key)
|
24
|
+
Base64.decode64(encoded_key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
class TranslationCollection
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_writer :translations
|
6
|
+
|
7
|
+
delegate :<<, :each, :[], :[]=, :length, to: :translations
|
8
|
+
|
9
|
+
def translations
|
10
|
+
@translations ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def page(page_index)
|
14
|
+
Kaminari.paginate_array(self).page(page_index)
|
15
|
+
end
|
16
|
+
|
17
|
+
def search(query)
|
18
|
+
regex = /#{ query.split(' ').join('|') }/i
|
19
|
+
|
20
|
+
# Duplicate and filter translations
|
21
|
+
updated = dup
|
22
|
+
updated.translations.select! { |translation| translation.matches?(regex) }
|
23
|
+
updated
|
24
|
+
end
|
25
|
+
|
26
|
+
def find(encoded_key)
|
27
|
+
key = I18nAdmin::Translation.key_from(encoded_key)
|
28
|
+
|
29
|
+
translations.find do |translation|
|
30
|
+
translation.key == key
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
class Translations
|
3
|
+
include I18nAdmin::RequestStore
|
4
|
+
|
5
|
+
def self.translations_for(locale)
|
6
|
+
translations = TranslationCollection.new
|
7
|
+
|
8
|
+
for_locale(locale).each do |key, value|
|
9
|
+
translations << Translation.new(
|
10
|
+
key: key,
|
11
|
+
original: for_locale(I18n.default_locale)[key],
|
12
|
+
value: value,
|
13
|
+
locale: locale
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
translations
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.for_locale(locale)
|
21
|
+
new.for_locale(locale.to_sym)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Save a translation object
|
25
|
+
def self.update(translation)
|
26
|
+
I18n.backend.store_translations(
|
27
|
+
translation.locale,
|
28
|
+
translation.key => translation.value
|
29
|
+
)
|
30
|
+
|
31
|
+
if translation.locale == I18n.default_locale
|
32
|
+
translation.original = translation.value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def for_locale(locale = I18n.locale)
|
37
|
+
translations = with_empty_keys_for(locale, all_translations_for(locale))
|
38
|
+
|
39
|
+
translations.keys.sort.each_with_object({}) do |key, hash|
|
40
|
+
hash[key] = translations[key]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def all_translations_for(locale)
|
45
|
+
request_store.store[store_key_for(locale, :hash)] ||= backends.map do |backend|
|
46
|
+
translations_for(locale, backend)
|
47
|
+
end.reduce(&:reverse_merge)
|
48
|
+
end
|
49
|
+
|
50
|
+
def backends
|
51
|
+
@backends ||= I18n.backend.backends
|
52
|
+
end
|
53
|
+
|
54
|
+
def translations_for(locale, backend)
|
55
|
+
translations = if backend.protected_methods.include?(:translations)
|
56
|
+
parse_from_deep_hash(locale, backend.send(:translations))
|
57
|
+
elsif backend.respond_to?(:store)
|
58
|
+
backend.store.translations_for(locale)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_from_deep_hash(locale, translations)
|
63
|
+
flatten_translation_hash(translations[locale] || {})
|
64
|
+
end
|
65
|
+
|
66
|
+
def flatten_translation_hash(hash, scope = nil, translations = {})
|
67
|
+
hash.each_with_object(translations) do |(key, value), buffer|
|
68
|
+
scoped_key = [scope, key.to_s].compact.join('.')
|
69
|
+
|
70
|
+
if value.is_a?(Hash)
|
71
|
+
flatten_translation_hash(value, scoped_key, buffer)
|
72
|
+
elsif elegible_key?(scoped_key)
|
73
|
+
buffer[scoped_key] = value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def elegible_key?(key)
|
79
|
+
if (pattern = I18nAdmin.excluded_keys_pattern)
|
80
|
+
!key.match(pattern)
|
81
|
+
else
|
82
|
+
true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def with_empty_keys_for(locale, hash)
|
87
|
+
return hash if I18n.default_locale == locale
|
88
|
+
|
89
|
+
all_translations_for(I18n.default_locale).keys.each do |key|
|
90
|
+
hash[key] = "" unless hash.key?(key)
|
91
|
+
end
|
92
|
+
|
93
|
+
hash
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# desc "Explaining what the task does"
|
2
|
+
# task :i18n_admin do
|
3
|
+
# # Task goes here
|
4
|
+
# end
|
5
|
+
|
6
|
+
namespace :i18n do
|
7
|
+
namespace :translations do
|
8
|
+
task export: :environment do
|
9
|
+
format = (ENV['format'] || 'xls').to_sym
|
10
|
+
locale = (ENV['locale'] || I18n.default_locale).to_sym
|
11
|
+
|
12
|
+
data = I18nAdmin::Export.for(format).export(locale)
|
13
|
+
|
14
|
+
file_name = ['translations', locale, format].join('.')
|
15
|
+
file_path = Rails.root.join('tmp', file_name)
|
16
|
+
|
17
|
+
File.open(file_path, 'wb') do |file|
|
18
|
+
file.write(data)
|
19
|
+
end
|
20
|
+
|
21
|
+
puts "Exported translations to : #{ file_path }"
|
22
|
+
end
|
23
|
+
|
24
|
+
task import: :environment do
|
25
|
+
format = (ENV['format'] || 'xls').to_sym
|
26
|
+
locale = (ENV['locale'] || I18n.default_locale).to_sym
|
27
|
+
file_path = ENV['file']
|
28
|
+
file = File.open(file_path, 'rb')
|
29
|
+
|
30
|
+
data = I18nAdmin::Import.for(format).import(locale, file)
|
31
|
+
|
32
|
+
puts "Imported translations from : #{ file_path }"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|