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,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
|