i18n_admin 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +78 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/images/i18n_admin/spinner.gif +0 -0
  6. data/app/assets/javascripts/i18n_admin/application.js +21 -0
  7. data/app/assets/javascripts/i18n_admin/src/i18n-admin-job-monitor.coffee +37 -0
  8. data/app/assets/javascripts/i18n_admin/src/import.coffee +23 -0
  9. data/app/assets/javascripts/i18n_admin/src/translation.coffee +45 -0
  10. data/app/assets/stylesheets/i18n_admin/application.css.scss +88 -0
  11. data/app/controllers/i18n_admin/application_controller.rb +21 -0
  12. data/app/controllers/i18n_admin/exports_controller.rb +19 -0
  13. data/app/controllers/i18n_admin/imports_controller.rb +28 -0
  14. data/app/controllers/i18n_admin/translations_controller.rb +30 -0
  15. data/app/helpers/i18n_admin/application_helper.rb +40 -0
  16. data/app/models/i18n_admin/export_file.rb +4 -0
  17. data/app/models/i18n_admin/import_file.rb +4 -0
  18. data/app/models/i18n_admin/import_job.rb +4 -0
  19. data/app/models/i18n_admin/resource_file.rb +8 -0
  20. data/app/models/i18n_admin/translations_set.rb +5 -0
  21. data/app/models/i18n_admin/whitelisted_resource.rb +9 -0
  22. data/app/views/i18n_admin/exports/queued.html.haml +18 -0
  23. data/app/views/i18n_admin/imports/errors/_resource_invalid.html.erb +0 -0
  24. data/app/views/i18n_admin/imports/errors/_resource_not_found.html.erb +0 -0
  25. data/app/views/i18n_admin/imports/new.html.erb +61 -0
  26. data/app/views/i18n_admin/imports/processing.html.erb +33 -0
  27. data/app/views/i18n_admin/imports/queued.html.haml +0 -0
  28. data/app/views/i18n_admin/translations/_pagination.html.erb +3 -0
  29. data/app/views/i18n_admin/translations/_translation.html.erb +9 -0
  30. data/app/views/i18n_admin/translations/index.html.erb +81 -0
  31. data/app/views/layouts/i18n_admin/application.html.haml +11 -0
  32. data/config/locales/i18n_admin.en.yml +38 -0
  33. data/config/locales/i18n_admin.fr.yml +52 -0
  34. data/config/routes.rb +6 -0
  35. data/db/migrate/20140829094415_create_i18n_admin_translations_sets.rb +12 -0
  36. data/db/migrate/20150825133437_create_i18n_admin_import_jobs.rb +11 -0
  37. data/db/migrate/20150828074645_create_i18n_admin_whitelisted_resources.rb +12 -0
  38. data/db/migrate/20160404161658_create_i18n_admin_resource_files.rb +11 -0
  39. data/lib/ext/paperclip.rb +5 -0
  40. data/lib/generators/i18n_admin/install/install_generator.rb +31 -0
  41. data/lib/generators/i18n_admin/install/templates/initializer.rb +28 -0
  42. data/lib/i18n_admin.rb +51 -0
  43. data/lib/i18n_admin/engine.rb +28 -0
  44. data/lib/i18n_admin/errors.rb +10 -0
  45. data/lib/i18n_admin/errors/base.rb +23 -0
  46. data/lib/i18n_admin/errors/collection.rb +24 -0
  47. data/lib/i18n_admin/errors/resource_invalid.rb +7 -0
  48. data/lib/i18n_admin/errors/resource_not_found.rb +7 -0
  49. data/lib/i18n_admin/export.rb +14 -0
  50. data/lib/i18n_admin/export/base.rb +129 -0
  51. data/lib/i18n_admin/export/xls.rb +110 -0
  52. data/lib/i18n_admin/hstore_backend.rb +68 -0
  53. data/lib/i18n_admin/import.rb +16 -0
  54. data/lib/i18n_admin/import/base.rb +64 -0
  55. data/lib/i18n_admin/import/job.rb +17 -0
  56. data/lib/i18n_admin/import/xls.rb +58 -0
  57. data/lib/i18n_admin/request_store.rb +11 -0
  58. data/lib/i18n_admin/translation.rb +27 -0
  59. data/lib/i18n_admin/translation_collection.rb +34 -0
  60. data/lib/i18n_admin/translations.rb +96 -0
  61. data/lib/i18n_admin/version.rb +3 -0
  62. data/lib/tasks/i18n_admin_tasks.rake +35 -0
  63. 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,11 @@
1
+ class CreateI18nAdminResourceFiles < ActiveRecord::Migration
2
+ def change
3
+ create_table :i18n_admin_resource_files do |t|
4
+ t.string :type
5
+ t.string :job_id
6
+ t.attachment :file
7
+
8
+ t.timestamps null: false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # Fixes excel uploading
2
+ #
3
+ if Paperclip.options[:content_type_mappings]
4
+ Paperclip.options[:content_type_mappings][:xls] = "CDF V2 Document, No summary info"
5
+ 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,10 @@
1
+ module I18nAdmin
2
+ module Errors
3
+ end
4
+ end
5
+
6
+ require 'i18n_admin/errors/base'
7
+ require 'i18n_admin/errors/collection'
8
+
9
+ require 'i18n_admin/errors/resource_not_found'
10
+ require 'i18n_admin/errors/resource_invalid'
@@ -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,7 @@
1
+ module I18nAdmin
2
+ module Errors
3
+ class ResourceInvalid < Errors::Base
4
+ attr_accessor :key, :resource
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module I18nAdmin
2
+ module Errors
3
+ class ResourceNotFound < Errors::Base
4
+ attr_accessor :key, :model_name, :id
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ module I18nAdmin
2
+ module Export
3
+ def self.types
4
+ @types ||= {}
5
+ end
6
+
7
+ def self.for(format)
8
+ types[format]
9
+ end
10
+ end
11
+ end
12
+
13
+ require 'i18n_admin/export/base'
14
+ require 'i18n_admin/export/xls'
@@ -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