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,12 @@
|
|
1
|
+
class CreateI18nAdminWhitelistedResources < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :i18n_admin_whitelisted_resources do |t|
|
4
|
+
t.references :resource, polymorphic: true
|
5
|
+
|
6
|
+
t.timestamps null: false
|
7
|
+
end
|
8
|
+
|
9
|
+
add_index :i18n_admin_whitelisted_resources, [:resource_id, :resource_type],
|
10
|
+
name: 'whitelisted_resources_foreign_key_index'
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
# Copied files come from templates folder
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
# Generator desc
|
7
|
+
desc "I18nAdmin install generator"
|
8
|
+
|
9
|
+
def mount_engine
|
10
|
+
mount_path = ask(
|
11
|
+
"Where would you like to mount I18nAdmin engine ? [/i18n-admin]"
|
12
|
+
).presence || '/i18n-admin'
|
13
|
+
mount_path = mount_path.match(/^\//) ? mount_path : "/#{ mount_path }"
|
14
|
+
|
15
|
+
gsub_file "config/routes.rb", /mount I18nAdmin.*\n/, ''
|
16
|
+
|
17
|
+
route "mount I18nAdmin::Engine => '#{ mount_path }', as: 'i18n_admin'"
|
18
|
+
end
|
19
|
+
|
20
|
+
def copy_initializer
|
21
|
+
copy_file "initializer.rb", "config/initializers/i18n_admin.rb"
|
22
|
+
end
|
23
|
+
|
24
|
+
def install_migrations
|
25
|
+
say "Installing migrations ..."
|
26
|
+
rake 'i18n_admin:install:migrations'
|
27
|
+
say "Migrations installed, don't forget to run `rake db:migrate` to " \
|
28
|
+
"make the translation system work"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
I18nAdmin.config do |config|
|
2
|
+
# User authentication method for the admin panel
|
3
|
+
#
|
4
|
+
# config.authentication_method = :authenticate_user!
|
5
|
+
|
6
|
+
# Method to retrieve the current user
|
7
|
+
#
|
8
|
+
# config.current_user_method = :current_user
|
9
|
+
|
10
|
+
# Define a pattern that will exclude keys from the shown administrable
|
11
|
+
# translations
|
12
|
+
#
|
13
|
+
# For example, to exclude all activerecord translations :
|
14
|
+
# config.excluded_keys_pattern = /^activerecord\./
|
15
|
+
#
|
16
|
+
# config.excluded_keys_pattern = nil
|
17
|
+
|
18
|
+
# When exporting translations, all translated models are exported.
|
19
|
+
# If you want to be able to whitelist exportable models, set the
|
20
|
+
# following option to true
|
21
|
+
#
|
22
|
+
# config.whitelist_models = false
|
23
|
+
|
24
|
+
# If you want imports and exports to be processed asynchronously through
|
25
|
+
# `ActiveJob`, set the following to true.
|
26
|
+
#
|
27
|
+
# config.async_io = false
|
28
|
+
end
|
data/lib/i18n_admin.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'kaminari'
|
2
|
+
require 'request_store'
|
3
|
+
require 'spreadsheet'
|
4
|
+
require 'active_model'
|
5
|
+
require 'turbolinks'
|
6
|
+
require 'nprogress-rails'
|
7
|
+
require 'sucker_punch'
|
8
|
+
require 'sidekiq'
|
9
|
+
require 'sidekiq/job_monitor'
|
10
|
+
require 'paperclip'
|
11
|
+
require 'font-awesome-rails'
|
12
|
+
|
13
|
+
require 'ext/paperclip'
|
14
|
+
|
15
|
+
require "i18n_admin/request_store"
|
16
|
+
require "i18n_admin/hstore_backend"
|
17
|
+
require "i18n_admin/translations"
|
18
|
+
require "i18n_admin/translation_collection"
|
19
|
+
require "i18n_admin/translation"
|
20
|
+
|
21
|
+
require "i18n_admin/errors"
|
22
|
+
require "i18n_admin/export"
|
23
|
+
require "i18n_admin/import"
|
24
|
+
|
25
|
+
require "i18n_admin/engine"
|
26
|
+
|
27
|
+
module I18nAdmin
|
28
|
+
mattr_accessor :root_controller_parent
|
29
|
+
@@root_controller_parent = '::ActionController::Base'
|
30
|
+
|
31
|
+
mattr_accessor :authentication_method
|
32
|
+
@@authentication_method = :authenticate_user!
|
33
|
+
|
34
|
+
mattr_accessor :current_user_method
|
35
|
+
@@current_user_method = :current_user
|
36
|
+
|
37
|
+
mattr_accessor :excluded_keys_pattern
|
38
|
+
@@excluded_keys_pattern = nil
|
39
|
+
|
40
|
+
mattr_accessor :whitelist_models
|
41
|
+
@@whitelist_models = false
|
42
|
+
|
43
|
+
mattr_accessor :async_io
|
44
|
+
@@async_io = false
|
45
|
+
|
46
|
+
def self.config(&block)
|
47
|
+
block_given? ? yield(self) : self
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
Mime::Type.register "application/xls", :xls
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
isolate_namespace I18nAdmin
|
4
|
+
|
5
|
+
initializer 'i18n_admin.configure_i18n_backend' do
|
6
|
+
translations_installed = begin
|
7
|
+
I18nAdmin::TranslationsSet.select('1').inspect
|
8
|
+
true
|
9
|
+
rescue
|
10
|
+
false
|
11
|
+
end
|
12
|
+
|
13
|
+
table_existence = ActiveRecord::Base.connection.table_exists? 'i18n_admin_translations_sets'
|
14
|
+
if translations_installed && table_existence
|
15
|
+
I18n.backend = I18n::Backend::Chain.new(
|
16
|
+
I18nAdmin::HstoreBackend.new,
|
17
|
+
I18n.backend
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
initializer 'i18n_admin.add_assets_to_precompilation', group: :all do |app|
|
23
|
+
app.config.assets.precompile += %w(
|
24
|
+
'i18n_admin/spinner.gif'
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Errors
|
3
|
+
class Base
|
4
|
+
def initialize(attributes = {})
|
5
|
+
assign_attributes(attributes)
|
6
|
+
end
|
7
|
+
|
8
|
+
def assign_attributes(attributes = {})
|
9
|
+
attributes.each do |key, value|
|
10
|
+
public_send(:"#{ key }=", value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def messages
|
15
|
+
@messages ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def type
|
19
|
+
self.class.name.demodulize.underscore.to_sym
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Errors
|
3
|
+
class Collection < Hash
|
4
|
+
def add(error_or_type, options = {})
|
5
|
+
error = error_instance_from(error_or_type, options)
|
6
|
+
|
7
|
+
self[error.type] ||= []
|
8
|
+
self[error.type] << error
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def error_instance_from(error_or_type, options = {})
|
14
|
+
if Symbol === error_or_type
|
15
|
+
camelized = error_or_type.to_s.camelize
|
16
|
+
type = ['I18nAdmin', 'Errors', camelized].join('::').constantize
|
17
|
+
type.new(options)
|
18
|
+
else
|
19
|
+
error_or_type
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Export
|
3
|
+
class Base
|
4
|
+
include Sidekiq::Worker
|
5
|
+
|
6
|
+
attr_reader :locale
|
7
|
+
|
8
|
+
def self.register(type, export)
|
9
|
+
Export.types[type] = export
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def translations
|
15
|
+
@translations ||=
|
16
|
+
translations_for_locale(locale).merge(
|
17
|
+
models_translations_for(locale)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
def original_translations
|
22
|
+
@original_translations ||=
|
23
|
+
I18nAdmin::Translations.for_locale(I18n.default_locale).merge(
|
24
|
+
models_translations_for(I18n.default_locale)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def translations_for_locale(locale)
|
29
|
+
I18nAdmin::Translations.for_locale(locale).select do |key, value|
|
30
|
+
if (pattern = I18nAdmin.excluded_keys_pattern)
|
31
|
+
!key.match(pattern)
|
32
|
+
else
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def models_translations_for(locale)
|
39
|
+
model_translations = {}
|
40
|
+
|
41
|
+
I18n.with_locale(locale) do
|
42
|
+
translated_models.each do |model, attributes|
|
43
|
+
whitelisted_resources_for(model).each do |resource|
|
44
|
+
attributes.each do |attribute|
|
45
|
+
key = model_translation_key_for(resource, attribute)
|
46
|
+
model_translations[key] = resource.send(attribute).to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
model_translations
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_translation_key_for(resource, attribute)
|
56
|
+
['models', resource.class.name, resource.id, attribute].join('-')
|
57
|
+
end
|
58
|
+
|
59
|
+
def translated_models
|
60
|
+
@translated_models ||= begin
|
61
|
+
if defined?(Globalize) || defined?(Para::I18n)
|
62
|
+
fetch_translated_models
|
63
|
+
else
|
64
|
+
[]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def fetch_translated_models
|
70
|
+
model_names.each_with_object({}) do |model_name, models|
|
71
|
+
begin
|
72
|
+
model = model_name.constantize
|
73
|
+
|
74
|
+
if model.respond_to?(:translates?) && model.translates?
|
75
|
+
models[model] = model.translated_attribute_names
|
76
|
+
end
|
77
|
+
rescue LoadError
|
78
|
+
next
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Borrowed from Rails Admin
|
84
|
+
def model_names
|
85
|
+
@model_names ||=
|
86
|
+
([Rails.application] + Rails::Engine.subclasses.map(&:instance)).flat_map do |app|
|
87
|
+
(app.paths['app/models'].to_a + app.config.autoload_paths).map do |load_path|
|
88
|
+
Dir.glob(app.root.join(load_path)).map do |load_dir|
|
89
|
+
Dir.glob(load_dir + '/**/*.rb').map do |filename|
|
90
|
+
next unless filename.match(/\/models\//)
|
91
|
+
# app/models/module/class.rb => module/class.rb => module/class => Module::Class
|
92
|
+
lchomp(filename, "#{app.root.join(load_dir)}/").chomp('.rb').camelize
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end.flatten.compact.uniq
|
97
|
+
end
|
98
|
+
|
99
|
+
def lchomp(base, arg)
|
100
|
+
base.to_s.reverse.chomp(arg.to_s.reverse).reverse
|
101
|
+
end
|
102
|
+
|
103
|
+
# If the `whitelist_models` option is set to true in the initializer,
|
104
|
+
# only fetch the explicitly whitelisted resources for each model
|
105
|
+
#
|
106
|
+
def whitelisted_resources_for(model)
|
107
|
+
resource_id_field = [model.table_name, model.primary_key].join('.')
|
108
|
+
|
109
|
+
whitelisted_resources =
|
110
|
+
if I18nAdmin.whitelist_models
|
111
|
+
model.joins(
|
112
|
+
'INNER JOIN i18n_admin_whitelisted_resources AS whitelist ' \
|
113
|
+
"ON whitelist.resource_id = #{ resource_id_field }"
|
114
|
+
).where(whitelist: { resource_type: model.name })
|
115
|
+
else
|
116
|
+
model.all
|
117
|
+
end
|
118
|
+
|
119
|
+
whitelisted_resources = whitelisted_resources.order("#{ resource_id_field } ASC")
|
120
|
+
|
121
|
+
if defined?(Globalize)
|
122
|
+
whitelisted_resources.includes(:translations)
|
123
|
+
else
|
124
|
+
whitelisted_resources
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module I18nAdmin
|
2
|
+
module Export
|
3
|
+
class XLS < Export::Base
|
4
|
+
register :xls, self
|
5
|
+
|
6
|
+
PAGE_LENGTH = 32_000.0
|
7
|
+
|
8
|
+
attr_reader :spreadsheet, :sheet
|
9
|
+
attr_accessor :export_file
|
10
|
+
|
11
|
+
def perform(locale)
|
12
|
+
@locale = locale
|
13
|
+
@spreadsheet = Spreadsheet::Workbook.new
|
14
|
+
@sheet = spreadsheet.create_worksheet
|
15
|
+
|
16
|
+
add_headers!
|
17
|
+
|
18
|
+
run
|
19
|
+
save
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
default_format = Spreadsheet::Format.new(text_wrap: true)
|
24
|
+
|
25
|
+
index = 0
|
26
|
+
|
27
|
+
translations.each do |key, value|
|
28
|
+
value = value.to_s
|
29
|
+
original_translation = original_translations[key].to_s
|
30
|
+
max_length = [value.length, original_translation.length].max
|
31
|
+
pages = (max_length / PAGE_LENGTH).ceil
|
32
|
+
|
33
|
+
pages.times do |page|
|
34
|
+
index += 1
|
35
|
+
|
36
|
+
translation_key = if pages > 1
|
37
|
+
"#{ key } (#{ page + 1 } / #{ pages })"
|
38
|
+
else
|
39
|
+
key
|
40
|
+
end
|
41
|
+
|
42
|
+
start_offset = page * PAGE_LENGTH
|
43
|
+
|
44
|
+
row = sheet.row(index)
|
45
|
+
row.default_format = default_format
|
46
|
+
row.push(translation_key)
|
47
|
+
row.push(original_translation[start_offset, PAGE_LENGTH])
|
48
|
+
row.push(value[start_offset, PAGE_LENGTH])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def save
|
54
|
+
spreadsheet.write(file_path)
|
55
|
+
|
56
|
+
source = File.open(file_path, 'rb')
|
57
|
+
self.export_file = ExportFile.create!(job_id: job_id, file: source)
|
58
|
+
ensure
|
59
|
+
if defined?(source) && source
|
60
|
+
source.close
|
61
|
+
File.unlink(source.path)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def file_path
|
66
|
+
@file_path ||= [
|
67
|
+
'',
|
68
|
+
'tmp',
|
69
|
+
I18n.t('i18n_admin.exports.file.name', time: Time.now.strftime('%Y_%m_%d-%H_%M'), lang: locale, ext: 'xls')
|
70
|
+
].join('/')
|
71
|
+
end
|
72
|
+
|
73
|
+
def monitoring_data(_, state)
|
74
|
+
{ url: export_file.file.url } if state == 'complete'
|
75
|
+
end
|
76
|
+
|
77
|
+
def export_file
|
78
|
+
@export_file ||= ExportFile.find_by_job_id(job_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def add_headers!
|
84
|
+
sheet.row(0).replace(
|
85
|
+
[
|
86
|
+
I18n.t('i18n_admin.exports.file.columns.key'),
|
87
|
+
I18n.t('i18n_admin.exports.file.columns.original', lang: I18n.default_locale),
|
88
|
+
I18n.t('i18n_admin.exports.file.columns.translated', lang: locale)
|
89
|
+
]
|
90
|
+
)
|
91
|
+
|
92
|
+
format = Spreadsheet::Format.new weight: :bold
|
93
|
+
|
94
|
+
sheet.row(0).default_format = format
|
95
|
+
|
96
|
+
sheet.column(0).width = 30
|
97
|
+
sheet.column(1).width = 100
|
98
|
+
sheet.column(2).width = 100
|
99
|
+
end
|
100
|
+
|
101
|
+
def job_id
|
102
|
+
@job_id ||= jid || randomize_job_id
|
103
|
+
end
|
104
|
+
|
105
|
+
def randomize_job_id
|
106
|
+
['sync', (Time.now.to_f * 1000).round, rand(1000)]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|