rails_i18n_manager 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +7 -0
  3. data/README.md +177 -0
  4. data/Rakefile +18 -0
  5. data/app/controllers/rails_i18n_manager/application_controller.rb +5 -0
  6. data/app/controllers/rails_i18n_manager/translation_apps_controller.rb +71 -0
  7. data/app/controllers/rails_i18n_manager/translations_controller.rb +248 -0
  8. data/app/helpers/rails_i18n_manager/application_helper.rb +74 -0
  9. data/app/helpers/rails_i18n_manager/custom_form_builder.rb +223 -0
  10. data/app/jobs/rails_i18n_manager/application_job.rb +5 -0
  11. data/app/jobs/rails_i18n_manager/translations_import_job.rb +70 -0
  12. data/app/lib/rails_i18n_manager/forms/base.rb +25 -0
  13. data/app/lib/rails_i18n_manager/forms/translation_file_form.rb +55 -0
  14. data/app/lib/rails_i18n_manager/google_translate.rb +72 -0
  15. data/app/models/rails_i18n_manager/application_record.rb +20 -0
  16. data/app/models/rails_i18n_manager/translation_app.rb +108 -0
  17. data/app/models/rails_i18n_manager/translation_key.rb +161 -0
  18. data/app/models/rails_i18n_manager/translation_value.rb +11 -0
  19. data/app/views/layouts/rails_i18n_manager/application.html.slim +55 -0
  20. data/app/views/layouts/rails_i18n_manager/application.js.erb +5 -0
  21. data/app/views/rails_i18n_manager/form_builder/_basic_field.html.erb +108 -0
  22. data/app/views/rails_i18n_manager/form_builder/_error_notification.html.erb +7 -0
  23. data/app/views/rails_i18n_manager/shared/_flash.html.slim +6 -0
  24. data/app/views/rails_i18n_manager/translation_apps/_breadcrumbs.html.slim +12 -0
  25. data/app/views/rails_i18n_manager/translation_apps/_filter_bar.html.slim +9 -0
  26. data/app/views/rails_i18n_manager/translation_apps/form.html.slim +30 -0
  27. data/app/views/rails_i18n_manager/translation_apps/index.html.slim +27 -0
  28. data/app/views/rails_i18n_manager/translations/_breadcrumbs.html.slim +17 -0
  29. data/app/views/rails_i18n_manager/translations/_filter_bar.html.slim +17 -0
  30. data/app/views/rails_i18n_manager/translations/_form.html.slim +29 -0
  31. data/app/views/rails_i18n_manager/translations/_sub_nav.html.slim +7 -0
  32. data/app/views/rails_i18n_manager/translations/_translation_value_fields.html.slim +10 -0
  33. data/app/views/rails_i18n_manager/translations/edit.html.slim +6 -0
  34. data/app/views/rails_i18n_manager/translations/import.html.slim +26 -0
  35. data/app/views/rails_i18n_manager/translations/index.html.slim +50 -0
  36. data/config/locales/en.yml +5 -0
  37. data/config/routes.rb +22 -0
  38. data/db/migrate/20221001001344_add_rails_i18n_manager_tables.rb +26 -0
  39. data/lib/rails_i18n_manager/config.rb +20 -0
  40. data/lib/rails_i18n_manager/engine.rb +42 -0
  41. data/lib/rails_i18n_manager/version.rb +3 -0
  42. data/lib/rails_i18n_manager.rb +60 -0
  43. data/public/rails_i18n_manager/application.css +67 -0
  44. data/public/rails_i18n_manager/application.js +37 -0
  45. data/public/rails_i18n_manager/favicon.ico +0 -0
  46. data/public/rails_i18n_manager/utility.css +99 -0
  47. metadata +292 -0
@@ -0,0 +1,161 @@
1
+ module RailsI18nManager
2
+ class TranslationKey < ApplicationRecord
3
+
4
+ belongs_to :translation_app, class_name: "RailsI18nManager::TranslationApp"
5
+ has_many :translation_values, class_name: "RailsI18nManager::TranslationValue", dependent: :destroy
6
+ accepts_nested_attributes_for :translation_values, reject_if: ->(x){ x["id"].nil? && x["translation"].blank? }
7
+
8
+ validates :translation_app, presence: true
9
+ validates :key, presence: true, uniqueness: {case_sensitive: false, scope: [:translation_app_id]}
10
+ validate :validate_translation_values_includes_default_locale
11
+
12
+ def validate_translation_values_includes_default_locale
13
+ return if new_record?
14
+ if translation_values.empty? || translation_values.none?{|x| x.locale == translation_app.default_locale }
15
+ errors.add(:base, "Translation for default locale is required")
16
+ end
17
+ end
18
+
19
+ scope :search, ->(str){
20
+ fields = [
21
+ "#{table_name}.key",
22
+ "#{TranslationApp.table_name}.name",
23
+ "#{TranslationValue.table_name}.locale",
24
+ "#{TranslationValue.table_name}.translation",
25
+ ]
26
+
27
+ like = connection.adapter_name.downcase.to_s == "postgres" ? "ILIKE" : "LIKE"
28
+
29
+ sql_conditions = []
30
+
31
+ fields.each do |col|
32
+ sql_conditions << "(#{col} #{like} :search)"
33
+ end
34
+
35
+ self.left_joins(:translation_values, :translation_app)
36
+ .where(sql_conditions.join(" OR "), search: "%#{str}%")
37
+ }
38
+
39
+ def default_translation
40
+ return @default_translation if defined?(@default_translation)
41
+ @default_translation = self.translation_values.detect{|x| x.locale == translation_app.default_locale.to_s }&.translation
42
+ end
43
+
44
+ def any_missing_translations?
45
+ self.translation_app.all_locales.any? do |locale|
46
+ val_record = translation_values.detect{|x| x.locale == locale.to_s}
47
+
48
+ next val_record.nil? || val_record.translation.blank?
49
+ end
50
+ end
51
+
52
+ def self.to_csv
53
+ CSV.generate do |csv|
54
+ csv << ["App Name", "Key", "Locale", "Translation", "Updated At"]
55
+
56
+ self.all.order(key: :asc).includes(:translation_app, :translation_values).each do |key_record|
57
+ value_records = {}
58
+
59
+ key_record.translation_values.each do |value_record|
60
+ value_records[value_record.locale] = value_record
61
+ end
62
+
63
+ key_record.translation_app.all_locales.each do |locale|
64
+ value_record = value_records[locale]
65
+ csv << [key_record.translation_app.name, key_record.key, value_record&.locale, value_record&.translation, value_record&.updated_at&.to_s]
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def self.export_to(app_name: nil, zip: false, format: :yaml)
72
+ format = format.to_sym
73
+
74
+ if format == :yaml
75
+ format = "yml"
76
+ elsif [:yaml, :json].exclude?(format)
77
+ raise ArgumentError.new("Invalid format provided")
78
+ end
79
+
80
+ base_export_path = Rails.root.join("tmp/export/translations/")
81
+
82
+ files_to_delete = Dir.glob("#{base_export_path}/*").each do |f|
83
+ if File.ctime(f) > 1.minutes.ago
84
+ `rm -rf #{f}`
85
+ #File.delete(f)
86
+ end
87
+ end
88
+
89
+ base_folder_path = File.join(base_export_path, "#{Time.now.to_i}/")
90
+
91
+ FileUtils.mkdir_p(base_folder_path)
92
+
93
+ if app_name.nil?
94
+ translation_apps = TranslationApp.order(name: :asc)
95
+ else
96
+ translation_apps = [TranslationApp.find_by!(name: app_name)]
97
+ end
98
+
99
+ if translation_apps.empty?
100
+ return nil
101
+ end
102
+
103
+ translation_apps.each do |app_record|
104
+ current_app_name = app_record.name
105
+
106
+ key_records = app_record.translation_keys.order(key: :asc).includes(:translation_values)
107
+
108
+ app_record.all_locales.each do |locale|
109
+ tree = {}
110
+
111
+ key_records.each do |key_record|
112
+ val_record = key_record.translation_values.detect{|x| x.locale == locale.to_s}
113
+
114
+ split_keys = [locale.to_s] + key_record.key.split(".")
115
+
116
+ RailsI18nManager.hash_deep_set(tree, split_keys, val_record.try!(:translation))
117
+ end
118
+
119
+ filename = File.join(base_folder_path, current_app_name, "#{locale}.#{format}")
120
+
121
+ FileUtils.mkdir_p(File.dirname(filename))
122
+
123
+ File.open(filename, "wb") do |io|
124
+ if format == :json
125
+ str = tree.to_json
126
+ else
127
+ str = tree.to_yaml(line_width: -1).sub("---\n", "")
128
+ end
129
+
130
+ io.write(str)
131
+ end
132
+ end
133
+ end
134
+
135
+ if zip
136
+ temp_file = Tempfile.new([Time.now.to_i.to_s, ".zip"], binmode: true)
137
+
138
+ files_to_write = Dir.glob("#{base_folder_path}/**/**")
139
+
140
+ if files_to_write.empty?
141
+ return nil
142
+ end
143
+
144
+ zip_file = Zip::File.open(temp_file, create: !File.exist?(temp_file)) do |zipfile|
145
+ files_to_write.each do |file|
146
+ zipfile.add(file.sub(base_folder_path, "translations/"), file)
147
+ end
148
+ end
149
+
150
+ output_path = temp_file.path
151
+ elsif app_name
152
+ output_path = File.join(base_folder_path, app_name)
153
+ else
154
+ output_path = base_folder_path
155
+ end
156
+
157
+ return output_path
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,11 @@
1
+ module RailsI18nManager
2
+ class TranslationValue < ApplicationRecord
3
+
4
+ belongs_to :translation_key, class_name: "RailsI18nManager::TranslationKey"
5
+
6
+ validates :translation_key, presence: true
7
+ validates :locale, presence: true, uniqueness: {scope: :translation_key_id}
8
+ validates :translation, presence: {if: ->(){ locale == translation_key.translation_app.default_locale.to_s } }
9
+
10
+ end
11
+ end
@@ -0,0 +1,55 @@
1
+ - @title = "Translations Manager"
2
+
3
+ doctype html
4
+ html
5
+ head
6
+ title = @title
7
+
8
+ = csrf_meta_tags
9
+ meta name="viewport" content="width=device-width, height=device-height, initial-scale=1.0"
10
+
11
+ link rel="icon" type="image/x-icon" href=custom_asset_path("/rails_i18n_manager/favicon.ico")
12
+
13
+ link rel="stylesheet" href=custom_asset_path("/rails_i18n_manager/utility.css")
14
+
15
+ script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" referrerpolicy="no-referrer"
16
+
17
+ link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous"
18
+ script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3" crossorigin="anonymous"
19
+
20
+ link rel="stylesheet" href=custom_asset_path("/rails_i18n_manager/application.css")
21
+
22
+ script src="https://cdn.jsdelivr.net/npm/@rails/ujs@7.0.4-3/lib/assets/compiled/rails-ujs.min.js"
23
+
24
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/slim-select/2.4.5/slimselect.min.css" integrity="sha512-GvqWM4KWH8mbgWIyvwdH8HgjUbyZTXrCq0sjGij9fDNiXz3vJoy3jCcAaWNekH2rJe4hXVWCJKN+bEW8V7AAEQ==" crossorigin="anonymous" referrerpolicy="no-referrer"
25
+ script src="https://cdnjs.cloudflare.com/ajax/libs/slim-select/2.4.5/slimselect.global.min.js" integrity="sha512-r2ujllVbPV4gVNZyqAB6LS3cnpEenEl18yFYoowmutUX5zVXQi5mp13lMWv3FQpsn96eFJTcd5VqBkZuatGtWQ==" crossorigin="anonymous" referrerpolicy="no-referrer"
26
+
27
+ script src="https://cdnjs.cloudflare.com/ajax/libs/autosize.js/3.0.20/autosize.min.js" integrity="sha512-EAEoidLzhKrfVg7qX8xZFEAebhmBMsXrIcI0h7VPx2CyAyFHuDvOAUs9CEATB2Ou2/kuWEDtluEVrQcjXBy9yw==" crossorigin="anonymous" referrerpolicy="no-referrer"
28
+
29
+ script src=custom_asset_path("/rails_i18n_manager/application.js")
30
+
31
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/3.2.1/css/font-awesome.min.css" integrity="sha512-IJ+BZHGlT4K43sqBGUzJ90pcxfkREDVZPZxeexRigVL8rzdw/gyJIflDahMdNzBww4k0WxpyaWpC2PLQUWmMUQ==" crossorigin="anonymous" referrerpolicy="no-referrer"
32
+
33
+ link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==" crossorigin="anonymous" referrerpolicy="no-referrer"
34
+
35
+ body
36
+ nav.navbar.navbar-expand-lg.navbar-light.bg-success.fixed-top
37
+ .container-fluid
38
+ h1.hidden = @title
39
+
40
+ a.navbar-brand = "#{@title}"
41
+
42
+ button.navbar-toggler type="button" data-bs-toggle="collapse" data-bs-target="#navbar-list" aria-controls="navbar-list" aria-expanded="false" aria-label="Toggle navigation"
43
+ span.navbar-toggler-icon
44
+
45
+ #navbar-list.collapse.navbar-collapse
46
+ ul.navbar-nav.me-auto.mb-2.mb-lg-0
47
+ li.nav-item
48
+ a.nav-link class=("active" if params[:controller].split("/").last == "translations") href=translations_path Translations
49
+ li.nav-item
50
+ a.nav-link class=("active" if params[:controller].split("/").last == "translation_apps") href=translation_apps_path = RailsI18nManager::TranslationApp::NAME.pluralize
51
+
52
+ .container-fluid
53
+ = render "rails_i18n_manager/shared/flash"
54
+
55
+ = yield
@@ -0,0 +1,5 @@
1
+ $("#flash-container").html("<%= j render partial: "rails_i18n_manager/shared/flash" %>");
2
+
3
+ <%= yield %>
4
+
5
+ window.init();
@@ -0,0 +1,108 @@
1
+ <%
2
+ if options[:field_layout].to_s == "horizontal"
3
+ options[:field_wrapper_html][:class] ||= ""
4
+ options[:field_wrapper_html][:class].concat(" row").strip!
5
+
6
+ options[:label_wrapper_html][:class] ||= ""
7
+ options[:label_wrapper_html][:class].concat(" col-md-1 col-form-label text-md-end").strip!
8
+
9
+ options[:input_wrapper_html][:class] ||= ""
10
+ options[:input_wrapper_html][:class].concat(" col-auto g-md-0").strip!
11
+ end
12
+
13
+ if type == :text_area
14
+ options[:rows] = options[:rows] || 1
15
+ end
16
+
17
+ case type
18
+ when :select
19
+ if options.has_key?(:prompt)
20
+ if !options[:prompt].is_a?(String)
21
+ options[:prompt] = !!options[:prompt]
22
+ end
23
+ elsif options[:selected].blank? && !options[:include_blank]
24
+ options[:prompt] = true
25
+ end
26
+
27
+ if options[:prompt] == true
28
+ options[:prompt] = "Select..."
29
+ end
30
+
31
+ options[:input_html][:class] ||= ""
32
+ options[:input_html][:class].concat(" form-select").strip!
33
+ when :checkbox
34
+ checkbox_label = options.delete(:label)
35
+
36
+ options[:label_html][:class] ||= ""
37
+ options[:label_html][:class].concat(" form-check-label").strip!
38
+
39
+ options[:input_html][:class] ||= ""
40
+ options[:input_html][:class].concat(" form-check-input").strip!
41
+ else
42
+ options[:input_html][:class] ||= ""
43
+ if options[:input_html][:class].exclude?("form-control-plaintext")
44
+ options[:input_html][:class].concat(" form-control").strip!
45
+ end
46
+ end
47
+ %>
48
+
49
+
50
+ <% field = capture do %>
51
+ <% if type == :view %>
52
+ <%= text_field_tag nil, options[:input_html][:value], options[:input_html] %>
53
+ <% elsif type == :select %>
54
+ <%=
55
+ f.select(
56
+ method,
57
+ options[:collection],
58
+ {
59
+ include_blank: options[:include_blank],
60
+ selected: options[:selected],
61
+ prompt: options[:prompt],
62
+ disabled: options[:disabled]
63
+ },
64
+ options[:input_html],
65
+ )
66
+ %>
67
+ <% elsif type == :checkbox %>
68
+ <%= f.check_box method, **options[:input_html] %>
69
+ <%= f.label method, options[:label], **options[:label_html] do %>
70
+ <%= checkbox_label %>
71
+ <% end %>
72
+ <% elsif type == :textarea %>
73
+ <%= f.text_area method, options[:input_html] %>
74
+ <% else %>
75
+ <%= f.text_field method, options[:input_html] %>
76
+ <% end %>
77
+ <% end %>
78
+
79
+ <%= content_tag :div, **options[:field_wrapper_html] do %>
80
+ <%= content_tag :div, **options[:label_wrapper_html] do %>
81
+ <% if options[:label] %>
82
+ <%= f.label method, options[:label], **options[:label_html] do %>
83
+ <%= options[:label] %>
84
+ <% if options[:required] %>
85
+ <span><%= options[:required_text] %></span>
86
+ <% end %>
87
+ <% end %>
88
+ <% end %>
89
+ <% end %>
90
+
91
+ <%= content_tag :div, **options[:input_wrapper_html] do %>
92
+ <%= field %>
93
+
94
+ <% if options[:help_text] %>
95
+ <small style="display:block;" class="text-muted">
96
+ <%= options[:help_text].html_safe %>
97
+ </small>
98
+ <% end %>
99
+
100
+ <% if options[:errors].present? %>
101
+ <div class="invalid-feedback">
102
+ <% options[:errors].each do |error| %>
103
+ <div><%= error %></div>
104
+ <% end %>
105
+ </div>
106
+ <% end %>
107
+ <% end %>
108
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <% if f.object.errors[:base].present? %>
2
+ <div class="alert alert-danger">
3
+ <% f.object.errors[:base].each do |error| %>
4
+ <%= error %>
5
+ <% end %>
6
+ </div>
7
+ <% end %>
@@ -0,0 +1,6 @@
1
+ #flash-container
2
+ - flash.each do |name, msg|
3
+ - if msg.is_a?(String)
4
+ .alert.alert-dismissible class="alert-#{name.to_s == "notice" ? "success" : "danger"}"
5
+ button.btn-close type="button" data-bs-dismiss="alert"
6
+ span id="flash_#{name}" = msg
@@ -0,0 +1,12 @@
1
+ nav style="--bs-breadcrumb-divider: '/';"
2
+ .breadcrumb
3
+ = breadcrumb_item RailsI18nManager::TranslationApp::NAME.pluralize, translation_apps_path
4
+
5
+ - if @translation_app
6
+ - if @translation_app.new_record?
7
+ = breadcrumb_item "New", new_translation_app_path
8
+ - else
9
+ = breadcrumb_item @translation_app.name, translation_app_path(@translation_app)
10
+
11
+ - if ["edit", "update"].include?(action_name)
12
+ = breadcrumb_item "Edit", edit_translation_app_path(@translation_app)
@@ -0,0 +1,9 @@
1
+ form
2
+ .row.align-items-center.g-1
3
+ .col-auto
4
+ = text_field_tag :search, params[:search], placeholder: "Search", class: "form-control", style: "max-width: 200px; width: 100%;"
5
+
6
+ .col-auto
7
+ button.btn.btn-primary.btn-sm type="submit" Filter
8
+ - if params[:search].present?
9
+ = link_to "Clear", nil, class: "btn btn-sm space-left"
@@ -0,0 +1,30 @@
1
+ = render "breadcrumbs"
2
+
3
+ h2
4
+ - if @translation_app.new_record?
5
+ | New Translation App
6
+ - else
7
+ | Edit Translation App
8
+
9
+ - url = @translation_app.new_record? ? translation_apps_path : translation_app_path(@translation_app)
10
+ - method = @translation_app.new_record? ? :post : :patch
11
+
12
+ - view_mode = params[:action] == "show"
13
+
14
+ = custom_form_for @translation_app, url: url, method: method, defaults: {view_mode: view_mode}, html: {class: "form-horizontal"} do |f|
15
+ = f.error_notification
16
+
17
+ = f.field :name, type: :text
18
+
19
+ = f.field :default_locale, type: :select, collection: RailsI18nManager.config.valid_locales.dup, selected: @translation_app.default_locale, include_blank: f.object.default_locale.nil?, input_html: {style: "width:120px;"}
20
+
21
+ = f.field :additional_locales, type: :select, collection: RailsI18nManager.config.valid_locales.dup, selected: @translation_app.additional_locales_array, include_blank: false, input_html: {multiple: true}, help_text: "Warning: Removing any locale will result in its translations being deleted."
22
+
23
+ - if !view_mode
24
+ .form-group
25
+ button.btn.btn-primary type="submit" Save
26
+
27
+ = link_to "Cancel", {action: :index}, class: 'btn btn-secondary space-left2'
28
+
29
+ - if !@translation_app.new_record?
30
+ = link_to "Delete", {action: :destroy, id: @translation_app.id}, method: :delete, data: { confirm: "WARNING: All the associated translations will be deleted.\n\nAre you sure you want to delete this translation app?" }, class: 'btn btn-danger space-left2'
@@ -0,0 +1,27 @@
1
+ .well.alert.alert-dark.permanent.pull-right style=("max-width: 920px;")
2
+ | Once you have created a #{RailsI18nManager::TranslationApp::NAME} then you can use the Import functionality on #{link_to "Translations Import page", import_translations_path}
3
+
4
+ .row.align-items-center.g-3
5
+ .col-auto
6
+ h2.page-title = RailsI18nManager::TranslationApp::NAME.pluralize
7
+ .col-auto
8
+ = link_to "New App", new_translation_app_path, class: 'btn btn-primary btn-sm'
9
+
10
+ .space-above4
11
+ = render "filter_bar"
12
+
13
+ table.table.table-striped.table-hover.space-above3.list-table
14
+ thead
15
+ tr
16
+ th = sort_link(:name)
17
+ th Default Locale
18
+ th Additional Locales
19
+ th Actions
20
+ tbody
21
+ - @translation_apps.each do |x|
22
+ tr
23
+ td = x.name
24
+ td = x.default_locale
25
+ td = x.additional_locales_array.join(", ")
26
+ td
27
+ = link_to "Edit", {action: :edit, id: x.id}
@@ -0,0 +1,17 @@
1
+ nav style="--bs-breadcrumb-divider: '/';"
2
+ .breadcrumb
3
+ = breadcrumb_item "Translations", translations_path
4
+
5
+ - if action_name == "import"
6
+ = breadcrumb_item "Import", import_translations_path
7
+
8
+ - elsif @translation_key
9
+ - if @translation_key.new_record?
10
+ = breadcrumb_item "New", new_translation_path(@translation_key)
11
+ - else
12
+ = breadcrumb_item @translation_key.translation_app.name, translations_path(app_name: @translation_key.translation_app.name)
13
+
14
+ = breadcrumb_item @translation_key.key, translation_path(@translation_key)
15
+
16
+ - if ["edit", "update"].include?(action_name)
17
+ = breadcrumb_item "Edit", edit_translation_path(@translation_key)
@@ -0,0 +1,17 @@
1
+ form
2
+ .row.align-items-center.g-1
3
+ .col-auto
4
+ = select_tag :app_name, options_for_select(RailsI18nManager::TranslationApp.order(name: :asc).pluck(:name), params[:app_name]), prompt: "All Apps", class: "form-select", style: "min-width: 220px"
5
+
6
+ .col-auto
7
+ = select_tag :status, options_for_select([["All Active", nil], "Missing", "Inactive", "All"], params[:status]), class: 'form-select', style: "min-width: 135px;"
8
+
9
+ .col-auto
10
+ = text_field_tag :search, params[:search], placeholder: "Search", class: "form-control"
11
+
12
+ .col-auto
13
+ button.btn.btn-primary.btn-sm type="submit" Filter
14
+
15
+ - if [:app_name, :search].any?{|x| params[x].present? }
16
+ - link_params = {status: params[:status]}.select{|_,v| v.present?}
17
+ = link_to "Clear", link_params, class: "btn btn-sm space-left"
@@ -0,0 +1,29 @@
1
+ - view_mode = params[:action] == "show"
2
+
3
+ = custom_form_for @translation_key, url: translation_path(@translation_key), method: :patch, defaults: {view_mode: view_mode, field_layout: :horizontal} do |f|
4
+ = f.view_field label: "App Name", value: @translation_key.translation_app.name
5
+ = f.view_field label: "Key", value: @translation_key.key, help_text: ("Nested Keys are denoted with dot (.)" if @translation_key.key.include?("."))
6
+
7
+ - if !@translation_key.active
8
+ = f.view_field label: "Status", value: "Inactive"
9
+ = link_to "Delete", {action: :destroy, id: @translation_key.id}, method: :delete, class: "btn btn-danger btn-sm space-left3", "data-confirm" => "Are you sure you want to delete this record?"
10
+
11
+ = render "sub_nav"
12
+
13
+ .translations-container
14
+ - sorted_translation_values = []
15
+
16
+ - @translation_key.translation_app.all_locales.each do |locale|
17
+ - val_record = @translation_key.translation_values.detect{|x| x.locale == locale.to_s }
18
+ - if val_record.nil?
19
+ - val_record = @translation_key.translation_values.new(locale: locale)
20
+ - sorted_translation_values << val_record
21
+
22
+ = f.fields_for :translation_values, sorted_translation_values do |f2|
23
+ = render "translation_value_fields", f: f2
24
+
25
+ - if !view_mode
26
+ .form-group
27
+ .col-lg-offset-2.col-md-offset-2.col-sm-offset-3.col-lg-10.col-md-10.col-sm-9
28
+ button.btn.btn-primary type="submit" Save
29
+ = link_to "Cancel", {action: :index}, class: 'btn btn-secondary space-left2'
@@ -0,0 +1,7 @@
1
+ ul.nav.nav-tabs.space-below5.space-above3
2
+ = nav_link "View", translation_path(@translation_key)
3
+
4
+ = nav_link "Edit", edit_translation_path(@translation_key), active: ["edit", "update"].include?(action_name)
5
+
6
+ - if @translation_key.any_missing_translations?
7
+ = nav_link "Translate Missing with Google", translate_missing_translations_path(translation_key_id: @translation_key.id), method: :post, "data-confirm" => "Are you sure you want to proceed with translating the missing translations for this entry?"
@@ -0,0 +1,10 @@
1
+ - locale = f.object.locale
2
+ - default_locale = f.object.translation_key.translation_app.default_locale
3
+
4
+ - if default_locale == locale
5
+ - help_text = "This default translation will be utilized whenever a more specific language is not available"
6
+ - required = true
7
+
8
+ .nested-fields.translation-value-fields
9
+ = f.hidden_field :locale
10
+ = f.field :translation, type: :textarea, label: "#{locale}", required: required, help_text: help_text, field_layout: :horizontal, input_html: {rows: 1, width: "100%;"}
@@ -0,0 +1,6 @@
1
+ = render "breadcrumbs"
2
+
3
+ h2.page-title Translations
4
+ /h5.page-title App Name: #{@translation_key.translation_app.name}
5
+
6
+ = render "form"
@@ -0,0 +1,26 @@
1
+ = render "breadcrumbs"
2
+
3
+ h2.page-sub-title Import Translations from Source File
4
+
5
+ .row
6
+ .col-6
7
+ = custom_form_for @form, as: :import_form, url: import_translations_path, method: :post, multipart: true, html: {class: "form-horizontal"} do |f|
8
+ = f.error_notification
9
+
10
+ = f.field :translation_app_id, label: "App Name", type: :select, collection: RailsI18nManager::TranslationApp.order(name: :asc).pluck(:name, :id)
11
+
12
+ = f.field :file, type: :file, label: "Translation File", help_text: "Allowed file types: yml, json"
13
+
14
+ = f.field :mark_inactive_translations, type: :checkbox, label: "Mark Inactive Translations?", help_text: "Any translation keys not found in the source file will be marked as 'Inactive' while found keys will be marked 'Active'. Marking a translation key as inactive excludes it from any data export and allows it to be deletable. Do not check this if you are uploading a partial translation file."
15
+
16
+ = f.field :overwrite_existing, type: :checkbox, label: "Overwrite existing translations?", help_text: "When enabled, if an existing translations exists it will be overwritten with the one contained in the file. If an outdated translation file is uploaded then it has the potential overwrite valuable translations in the app.", input_html: {"onclick" => 'if($(this).is(":checked")) alert("WARNING!\n\nEnabling overwrite can potentially be a highly destructive action. If an outdated translation file is uploaded then it has the potential overwrite valuable translations in the app. Please use caution.")'}
17
+
18
+ .form-group
19
+ button.btn.btn-primary type="submit" Save
20
+ = link_to "Cancel", {action: :index}, class: 'btn btn-secondary space-left'
21
+
22
+ .col-6
23
+ .alert.alert-dark.permanent
24
+ p This action will add translations that exist in the source file but do not exist in the database.
25
+ p This import will not delete any existing translations.
26
+ p You may import partial translations files with only some of the translation keys.
@@ -0,0 +1,50 @@
1
+ .pull-right
2
+ = link_to "Translate with Google", params.to_unsafe_h.merge(action_name: :translate_missing), class: "btn btn-primary btn-sm", "data-confirm" => "Are you sure you want to proceed with translating the missing translations in the currently filtered list?"
3
+ = link_to "Import Translations", import_translations_path, class: "btn btn-secondary btn-sm space-left2"
4
+ = link_to "Delete Inactive", params.to_unsafe_h.merge(action_name: :delete_inactive_keys), class: "btn btn-danger btn-sm space-left2", data: {confirm: "Warning! This is a highly destructive action.\n\nIts possible to incorrectly upload an incomplete or incorrect file to 'Mark Inactive Translations from Source' which can leave you with inactive keys that maybe shouldnt have been inactivated.\n\nPlease proceed only if you are certain that you do not have any keys that are incorrectly marked inactive.\n\nAre you sure you want to proceed with deleting the inactive translations in the currently filtered list?"}
5
+
6
+ h2.page-title Translations
7
+ - if params[:app_name]
8
+ h5.page-title App Name: #{params[:app_name]}
9
+
10
+ br
11
+
12
+ .well.well-sm
13
+ .btn-group.pull-right.text-right
14
+ = link_to "Export to CSV", params.to_unsafe_h.merge(format: :csv), class: "btn btn-sm btn-success"
15
+ = link_to "YAML", params.to_unsafe_h.merge(format: :zip, export_format: :yaml), class: "btn btn-sm btn-success"
16
+ = link_to "JSON", params.to_unsafe_h.merge(format: :zip, export_format: :json), class: "btn btn-sm btn-success"
17
+
18
+ .pull-right.space-right2
19
+
20
+ = render "filter_bar"
21
+
22
+ table.table.table-striped.table-hover.space-above3.list-table
23
+ thead
24
+ tr
25
+ th = sort_link(:app_name)
26
+ th = sort_link(:key)
27
+ th Default Translation
28
+ - if params[:status] == "Inactive"
29
+ th Status
30
+ th = sort_link(:updated_at)
31
+ th Actions
32
+ tbody
33
+ - @translation_keys.each do |x|
34
+ tr
35
+ td = x.translation_app.name
36
+ td = x.key
37
+ td = x.default_translation
38
+ - if params[:status] == "Inactive"
39
+ td Inactive
40
+ td = x.updated_at&.strftime("%Y-%m-%d %l:%M %p")
41
+ td
42
+ span = link_to "View", {action: :show, id: x.id}
43
+
44
+ span.space-left2 = link_to "Edit", {action: :edit, id: x.id}
45
+
46
+ - if !x.active
47
+ span.space-left2 = link_to "Delete", {action: :destroy, id: x.id}, method: :delete, "data-confirm" => "Are you sure you want to delete this translation?"
48
+
49
+ - if x.any_missing_translations?
50
+ span.space-left2 = link_to "Translate with Google", translate_missing_translations_path(id: x.id), method: :post, "data-confirm" => "Are you sure you want to proceed with translating the missing translations for this entry?"
@@ -0,0 +1,5 @@
1
+ en:
2
+ activerecord:
3
+ attributes:
4
+ 'rails_i18n_manager/board':
5
+ num_iterations_to_track: "Number of Iterations to Track"
data/config/routes.rb ADDED
@@ -0,0 +1,22 @@
1
+ RailsI18nManager::Engine.routes.draw do
2
+ resources :translations, only: [:index, :show, :edit, :update, :destroy] do
3
+ collection do
4
+ post :translate_missing
5
+
6
+ get :import
7
+ post :import
8
+
9
+ delete :delete_inactive_keys
10
+ end
11
+ end
12
+
13
+ resources :translation_apps
14
+
15
+ get "/robots", to: "application#robots", constraints: ->(req){ req.format == :text }
16
+
17
+ match "*a", to: "application#render_404", via: :get
18
+
19
+ get "/", to: "translations#index"
20
+
21
+ root "translations#index"
22
+ end