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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 02ae7e131829c4fa8de58cc8442a66c4469849ef
4
+ data.tar.gz: 00483d502876735d4062205d0c671255cbb2216c
5
+ SHA512:
6
+ metadata.gz: f6f60e39ca4eb851df17d9a38fcac50df910c9c1b071739a29a65ae067abc1b026ceeb123124c7157d82642433ddc1ca9fa9843f405f9a604aec9b7a99d7c056
7
+ data.tar.gz: 52c1b20386afbcd82178981210e088ea07a4b08476f935f063a9c082fdc929915c5bbc05847be3778249fca79164a9d2458715403ef842d12a54920747e7bbd6
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # I18nAdmin
2
+
3
+ Admin panel to translate Rails apps with `I18n::Backend::KeyValue` stores.
4
+
5
+ **Warning : Only PostgreSQL is supported due to HSTORE field type usage to store translations**
6
+
7
+ It works by creating a I18n backend chain and plugging a custom backend before
8
+ the original YAML backend.
9
+
10
+ When you translate a key, it is stored in a `HSTORE` field, with one database
11
+ row per locale. This allows for fast translations fetching on request start.
12
+
13
+ The translations are then merged with the ones existing in the YAML, and cached
14
+ for the request duration using the
15
+ [RequestStore](https://github.com/steveklabnik/request_store) gem.
16
+
17
+ ## Install
18
+
19
+ Add to your Gemfile and bundle :
20
+
21
+ ```ruby
22
+ gem 'i18n_admin', github: 'glyph-fr/i18n_admin'
23
+ ```
24
+
25
+ Run the install generator which will mount the engine and create an initializer
26
+ template to configure the gem, and create necessary migration :
27
+
28
+ ```bash
29
+ rails generate i18n_admin:install
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ I18nAdmin gives you an admin panel to translate your static contents.
35
+ It's accessible at the Engine mount point that you choosed when running
36
+ the install generator.
37
+
38
+ The default URL is `/i18n-admin`
39
+
40
+ ### Authentication
41
+
42
+ To plug the engine with your authentication system, you can pass a method
43
+ name to execute in a `before_action` filter in the I18nAdmin controllers.
44
+
45
+ This can be done in the initializer file generated at
46
+ `config/initializers/i18n_admin.rb` :
47
+
48
+ ```ruby
49
+ I18nAdmin.config do |config|
50
+ config.authentication_method = :authenticate_user!
51
+ end
52
+ ```
53
+
54
+ ### Excluding keys from the admin
55
+
56
+ To avoid some keys to be displayed in the translation admin, you can use a
57
+ Regex in the initializer file. The regex is matched against the whole key path
58
+ (e.g. "time.formats.short")
59
+
60
+ ```ruby
61
+ I18nAdmin.config do |config|
62
+ # Exclude all date and time formats from translations
63
+ config.excluded_keys_pattern = /^(date|time)/
64
+ end
65
+ ```
66
+
67
+ ## UI locale
68
+
69
+ The locale is defined from the rails' `I18n.locale`, which is automatically
70
+ added in the `<html>` tag `lang` attribute.
71
+
72
+ The JS app then taps into this locale to set its default locale and allow
73
+ displaying side-by-side the content with the default locale and the current
74
+ locale's translation for the key
75
+
76
+ ## Licence
77
+
78
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'I18nAdmin'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
18
+ load 'rails/tasks/engine.rake'
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
@@ -0,0 +1,21 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // compiled file.
9
+ //
10
+ // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
11
+ // about supported directives.
12
+ //
13
+ //= require jquery
14
+ //= require jquery_ujs
15
+ //= require jquery.autoGrow
16
+ //= require turbolinks
17
+ //= require bootstrap
18
+ //= require nprogress
19
+ //= require nprogress-turbolinks
20
+ //= require sidekiq-job_monitor
21
+ //= require_tree ./src
@@ -0,0 +1,37 @@
1
+ class I18nAdminJobMonitor
2
+ constructor: (@$el) ->
3
+ @initializeSidekiqJobMonitor()
4
+
5
+ initializeSidekiqJobMonitor: ->
6
+ @$el.sidekiqJobMonitor(onStart: @onMonitoringStart)
7
+
8
+ onMonitoringStart: (monitor) =>
9
+ @monitor = monitor
10
+ @$modal = monitor.$el
11
+ @$modal.appendTo('body').modal()
12
+ @$modal.on('complete', @onMonitoringComplete)
13
+ @$modal.on('hide.bs.modal', @onModalHide)
14
+ @$modal.on('failed', @hideModal)
15
+ @$modal.on('click', '[data-cancel="job"]', @cancelClicked)
16
+
17
+ onMonitoringComplete: (e, monitor, data) =>
18
+ @$modal.modal('hide')
19
+ window.location.href = data.url
20
+
21
+ onModalHide: (e) =>
22
+ @cancel()
23
+
24
+ cancelClicked: =>
25
+ @hideModal()
26
+
27
+ hideModal: =>
28
+ @$modal?.modal('hide')
29
+
30
+ cancel: ->
31
+ @monitor.stopMonitoring()
32
+ @monitor.cancelJob()
33
+
34
+
35
+ $(document).on 'page:change', ->
36
+ $('[data-export-link][data-remote]').each (i, el) ->
37
+ new I18nAdminJobMonitor($(el))
@@ -0,0 +1,23 @@
1
+ class ImportProgress
2
+ constructor: (@$el) ->
3
+ @checkURL = @$el.data('check-url')
4
+ @checkProgress()
5
+
6
+ checkProgress: ->
7
+ $.getJSON(@checkURL).then (job) =>
8
+ switch job.state
9
+ when 'pending' then setTimeout($.proxy(@checkProgress, this), 1000)
10
+ when 'success' then @handleSuccess(job)
11
+ when 'error' then @handleError(job)
12
+
13
+ handleSuccess: (job) ->
14
+ $('[data-status="pending"]').addClass('hidden')
15
+ $('[data-status="success"]').removeClass('hidden')
16
+
17
+ handleError: (job) ->
18
+ $('[data-status="pending"]').addClass('hidden')
19
+ $('[data-status="error"]').removeClass('hidden')
20
+
21
+ $(document).on 'page:change', ->
22
+ if ($importProgress = $('[data-processing-import]')).length
23
+ new ImportProgress($importProgress)
@@ -0,0 +1,45 @@
1
+ class Translation
2
+ constructor: (@$el) ->
3
+ @initialize()
4
+
5
+ initialize: ->
6
+ @$form = @$el.find('[data-translation-form]')
7
+ @$field = @$el.find('[data-translation-field]')
8
+ @currentValue = @$field.val()
9
+
10
+ @$field.on('blur', $.proxy(@updateTranslation, this))
11
+ @$form.on('ajax:beforeSend', $.proxy(@onBeforeSend, this))
12
+ @$form.on('ajax:complete', $.proxy(@onSaveComplete, this))
13
+ @$form.on('ajax:success', $.proxy(@translationSaved, this))
14
+
15
+ @$field.autoGrow()
16
+
17
+ updateTranslation: ->
18
+ if (newValue = @$field.val()) != @currentValue
19
+ @currentValue = newValue
20
+ @$form.submit()
21
+
22
+ translationSaved: (e, response) ->
23
+ @onSaveComplete()
24
+
25
+ $newMarkup = $(response)
26
+ @$el.html($newMarkup.html())
27
+
28
+ @initialize()
29
+ @showSuccess()
30
+
31
+ onBeforeSend: ->
32
+ NProgress.start()
33
+ @$el.addClass('loading')
34
+
35
+ onSaveComplete: ->
36
+ NProgress.done()
37
+ @$el.removeClass('loading')
38
+
39
+ showSuccess: ->
40
+ @$field.addClass('success')
41
+ setTimeout (=> @$field.removeClass('success')), 1000
42
+
43
+ $(document).on 'page:change', ->
44
+ if ($translations = $('[data-translation]')).length
45
+ $translations.each (i, el) -> new Translation($(el))
@@ -0,0 +1,88 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any styles
10
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
11
+ * file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require nprogress
15
+ *= require nprogress-bootstrap
16
+ *= require_self
17
+ */
18
+
19
+ @import "bootstrap";
20
+
21
+ body {
22
+ overflow: scroll;
23
+ }
24
+
25
+ .pagination {
26
+ margin-top: 0;
27
+ }
28
+
29
+ .panel-title {
30
+ font-size: 2em;
31
+ }
32
+
33
+ .panel-heading h2 {
34
+ font-size: 1em;
35
+ margin-top: 0;
36
+ }
37
+
38
+ .panel-default > .panel-heading.panel-subheading,
39
+ .panel-default > .panel-footer {
40
+ background: #fcfcfc;
41
+ }
42
+
43
+ .panel-subheading .pagination-container .text-center {
44
+ text-align: left;
45
+ }
46
+
47
+ .pagination-container .pagination {
48
+ margin-bottom: 0px;
49
+ }
50
+
51
+ .pagination-column {
52
+ border-right: 1px solid #ccc;
53
+ }
54
+
55
+ .table > tbody > tr > td.input-cell {
56
+ padding: 0;
57
+ }
58
+
59
+ .translation-input {
60
+ min-width: 400px;
61
+
62
+ &.success {
63
+ background: #DFF0D8;
64
+ }
65
+ }
66
+
67
+ td.input-cell textarea {
68
+ border-radius: 0;
69
+ border: 0;
70
+ box-shadow: 1px 2px 5px #f0f0f0 inset;
71
+ }
72
+
73
+ td.input-cell textarea:focus {
74
+ box-shadow: 1px 2px 5px #ccc inset;
75
+ }
76
+
77
+ .translation-row {
78
+ &.loading {
79
+ opacity: 0.5;
80
+ }
81
+ }
82
+
83
+ .translation-key-cell {
84
+ max-width: 100px;
85
+ text-overflow: ellipsis;
86
+ overflow: hidden;
87
+ }
88
+
@@ -0,0 +1,21 @@
1
+ module I18nAdmin
2
+ class ApplicationController < I18nAdmin.root_controller_parent.constantize
3
+ include FontAwesome::Rails::IconHelper
4
+
5
+ helper_method :current_locale
6
+
7
+ if (authentication_method = I18nAdmin.authentication_method)
8
+ before_filter authentication_method
9
+ end
10
+
11
+ def current_user
12
+ if (current_user_method = I18nAdmin.current_user_method)
13
+ send(current_user_method)
14
+ end
15
+ end
16
+
17
+ def current_locale
18
+ @current_locale ||= (params[:locale] || I18n.locale).to_sym
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ module I18nAdmin
2
+ class ExportsController < I18nAdmin::ApplicationController
3
+ def show
4
+ respond_to do |format|
5
+ format.html do
6
+ @job_id = Export::XLS.perform_async(current_locale.to_s)
7
+ render 'queued', layout: false
8
+ end
9
+
10
+ format.xls do
11
+ job = Export::XLS.new
12
+ job.perform(current_locale.to_s)
13
+
14
+ redirect_to job.export_file.file.url
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module I18nAdmin
2
+ class ImportsController < I18nAdmin::ApplicationController
3
+ def show
4
+ respond_to do |format|
5
+ format.json do
6
+ job = ImportJob.find(params[:id])
7
+ render json: job.as_json(only: [:id, :filename, :state])
8
+ end
9
+ end
10
+ end
11
+
12
+ def new
13
+ end
14
+
15
+ def create
16
+ unless params[:file].present?
17
+ flash[:error] = t('i18n_admin.imports.please_choose_file')
18
+ render 'new'
19
+ return
20
+ end
21
+
22
+ @job = ImportJob.create!(locale: current_locale, filename: params[:file].original_filename)
23
+ Import::Job.perform_async(current_locale, params[:file], @job.id)
24
+
25
+ render 'processing'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module I18nAdmin
2
+ class TranslationsController < I18nAdmin::ApplicationController
3
+ before_filter :fetch_translations
4
+
5
+ def index
6
+ @translations = @translations.search(params[:q]) if params[:q]
7
+ @translations = @translations.page(params[:page]).per(60)
8
+
9
+ @locales = I18n.available_locales
10
+ end
11
+
12
+ def update
13
+ translation = @translations.find(params[:id])
14
+ translation.value = translation_params[:value]
15
+
16
+ I18nAdmin::Translations.update(translation)
17
+ render partial: 'translation', locals: { translation: translation }
18
+ end
19
+
20
+ private
21
+
22
+ def translation_params
23
+ params.require(:translation).permit(:value)
24
+ end
25
+
26
+ def fetch_translations
27
+ @translations = I18nAdmin::Translations.translations_for(current_locale)
28
+ end
29
+ end
30
+ end