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