rails_i18n_manager 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +177 -0
- data/Rakefile +18 -0
- data/app/controllers/rails_i18n_manager/application_controller.rb +5 -0
- data/app/controllers/rails_i18n_manager/translation_apps_controller.rb +71 -0
- data/app/controllers/rails_i18n_manager/translations_controller.rb +248 -0
- data/app/helpers/rails_i18n_manager/application_helper.rb +74 -0
- data/app/helpers/rails_i18n_manager/custom_form_builder.rb +223 -0
- data/app/jobs/rails_i18n_manager/application_job.rb +5 -0
- data/app/jobs/rails_i18n_manager/translations_import_job.rb +70 -0
- data/app/lib/rails_i18n_manager/forms/base.rb +25 -0
- data/app/lib/rails_i18n_manager/forms/translation_file_form.rb +55 -0
- data/app/lib/rails_i18n_manager/google_translate.rb +72 -0
- data/app/models/rails_i18n_manager/application_record.rb +20 -0
- data/app/models/rails_i18n_manager/translation_app.rb +108 -0
- data/app/models/rails_i18n_manager/translation_key.rb +161 -0
- data/app/models/rails_i18n_manager/translation_value.rb +11 -0
- data/app/views/layouts/rails_i18n_manager/application.html.slim +55 -0
- data/app/views/layouts/rails_i18n_manager/application.js.erb +5 -0
- data/app/views/rails_i18n_manager/form_builder/_basic_field.html.erb +108 -0
- data/app/views/rails_i18n_manager/form_builder/_error_notification.html.erb +7 -0
- data/app/views/rails_i18n_manager/shared/_flash.html.slim +6 -0
- data/app/views/rails_i18n_manager/translation_apps/_breadcrumbs.html.slim +12 -0
- data/app/views/rails_i18n_manager/translation_apps/_filter_bar.html.slim +9 -0
- data/app/views/rails_i18n_manager/translation_apps/form.html.slim +30 -0
- data/app/views/rails_i18n_manager/translation_apps/index.html.slim +27 -0
- data/app/views/rails_i18n_manager/translations/_breadcrumbs.html.slim +17 -0
- data/app/views/rails_i18n_manager/translations/_filter_bar.html.slim +17 -0
- data/app/views/rails_i18n_manager/translations/_form.html.slim +29 -0
- data/app/views/rails_i18n_manager/translations/_sub_nav.html.slim +7 -0
- data/app/views/rails_i18n_manager/translations/_translation_value_fields.html.slim +10 -0
- data/app/views/rails_i18n_manager/translations/edit.html.slim +6 -0
- data/app/views/rails_i18n_manager/translations/import.html.slim +26 -0
- data/app/views/rails_i18n_manager/translations/index.html.slim +50 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +22 -0
- data/db/migrate/20221001001344_add_rails_i18n_manager_tables.rb +26 -0
- data/lib/rails_i18n_manager/config.rb +20 -0
- data/lib/rails_i18n_manager/engine.rb +42 -0
- data/lib/rails_i18n_manager/version.rb +3 -0
- data/lib/rails_i18n_manager.rb +60 -0
- data/public/rails_i18n_manager/application.css +67 -0
- data/public/rails_i18n_manager/application.js +37 -0
- data/public/rails_i18n_manager/favicon.ico +0 -0
- data/public/rails_i18n_manager/utility.css +99 -0
- 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,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,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,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?"
|
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
|