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