kakimasu 1.0.0

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 (98) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +31 -0
  6. data/.travis.yml +8 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +135 -0
  11. data/Rakefile +6 -0
  12. data/app/controllers/kakimasu/home_controller.rb +18 -0
  13. data/app/controllers/kakimasu/keys_controller.rb +89 -0
  14. data/app/controllers/kakimasu/translations_controller.rb +37 -0
  15. data/app/helpers/counting_helper.rb +11 -0
  16. data/app/helpers/formatting_helper.rb +6 -0
  17. data/app/helpers/grouping_helper.rb +11 -0
  18. data/app/helpers/pagination_helper.rb +15 -0
  19. data/app/helpers/search_key_helper.rb +66 -0
  20. data/app/helpers/search_translations_helper.rb +101 -0
  21. data/app/models/translation.rb +2 -0
  22. data/app/policies/translation_policy.rb +9 -0
  23. data/app/views/kakimasu/home/index.html.erb +35 -0
  24. data/app/views/kakimasu/keys/create.js.erb +1 -0
  25. data/app/views/kakimasu/keys/index.html.erb +183 -0
  26. data/app/views/kakimasu/shared/_navbar.html.erb +15 -0
  27. data/app/views/kakimasu/translations/create.js.erb +1 -0
  28. data/app/views/kakimasu/translations/index.html.erb +168 -0
  29. data/bin/console +14 -0
  30. data/bin/setup +8 -0
  31. data/config/initializers/i18n.rb +58 -0
  32. data/config/initializers/i18n_backend.rb +5 -0
  33. data/config/locales/en.yml +34 -0
  34. data/config/routes.rb +10 -0
  35. data/kakimasu.gemspec +49 -0
  36. data/lib/generators/kakimasu/backup_generator.rb +101 -0
  37. data/lib/generators/kakimasu/policy_generator.rb +18 -0
  38. data/lib/generators/kakimasu/restore_backup_generator.rb +89 -0
  39. data/lib/generators/kakimasu/views_generator.rb +22 -0
  40. data/lib/kakimasu.rb +11 -0
  41. data/lib/kakimasu/engine.rb +3 -0
  42. data/lib/kakimasu/version.rb +3 -0
  43. data/vendor/assets/javascripts/components/activation_button.coffee +10 -0
  44. data/vendor/assets/javascripts/components/modal.coffee +48 -0
  45. data/vendor/assets/javascripts/components/translate.coffee +93 -0
  46. data/vendor/assets/javascripts/components/translation_key_table.coffee +5 -0
  47. data/vendor/assets/javascripts/components/translation_navigation.coffee +13 -0
  48. data/vendor/assets/javascripts/components/translation_panel_table.coffee +5 -0
  49. data/vendor/assets/javascripts/components/translation_percent_chart.coffee +39 -0
  50. data/vendor/assets/javascripts/components/translation_popover.coffee +90 -0
  51. data/vendor/assets/javascripts/jquery/jquery.circliful.min.js +1 -0
  52. data/vendor/assets/javascripts/kakimasu.coffee +18 -0
  53. data/vendor/assets/stylesheets/circliful.scss +38 -0
  54. data/vendor/assets/stylesheets/components/_homepage.scss +17 -0
  55. data/vendor/assets/stylesheets/components/_key_management.scss +53 -0
  56. data/vendor/assets/stylesheets/components/_navbar.scss +52 -0
  57. data/vendor/assets/stylesheets/components/_translate_modal.scss +36 -0
  58. data/vendor/assets/stylesheets/components/_translation_panel.scss +31 -0
  59. data/vendor/assets/stylesheets/components/_translation_popover.scss +31 -0
  60. data/vendor/assets/stylesheets/font-awesome/HELP-US-OUT.txt +7 -0
  61. data/vendor/assets/stylesheets/font-awesome/css/font-awesome.css +2337 -0
  62. data/vendor/assets/stylesheets/font-awesome/css/font-awesome.min.css +4 -0
  63. data/vendor/assets/stylesheets/font-awesome/fonts/FontAwesome.otf +0 -0
  64. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.eot +0 -0
  65. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.svg +2671 -0
  66. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.ttf +0 -0
  67. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff +0 -0
  68. data/vendor/assets/stylesheets/font-awesome/fonts/fontawesome-webfont.woff2 +0 -0
  69. data/vendor/assets/stylesheets/font-awesome/less/animated.less +34 -0
  70. data/vendor/assets/stylesheets/font-awesome/less/bordered-pulled.less +25 -0
  71. data/vendor/assets/stylesheets/font-awesome/less/core.less +12 -0
  72. data/vendor/assets/stylesheets/font-awesome/less/fixed-width.less +6 -0
  73. data/vendor/assets/stylesheets/font-awesome/less/font-awesome.less +18 -0
  74. data/vendor/assets/stylesheets/font-awesome/less/icons.less +789 -0
  75. data/vendor/assets/stylesheets/font-awesome/less/larger.less +13 -0
  76. data/vendor/assets/stylesheets/font-awesome/less/list.less +19 -0
  77. data/vendor/assets/stylesheets/font-awesome/less/mixins.less +60 -0
  78. data/vendor/assets/stylesheets/font-awesome/less/path.less +15 -0
  79. data/vendor/assets/stylesheets/font-awesome/less/rotated-flipped.less +20 -0
  80. data/vendor/assets/stylesheets/font-awesome/less/screen-reader.less +5 -0
  81. data/vendor/assets/stylesheets/font-awesome/less/stacked.less +20 -0
  82. data/vendor/assets/stylesheets/font-awesome/less/variables.less +800 -0
  83. data/vendor/assets/stylesheets/font-awesome/scss/_animated.scss +34 -0
  84. data/vendor/assets/stylesheets/font-awesome/scss/_bordered-pulled.scss +25 -0
  85. data/vendor/assets/stylesheets/font-awesome/scss/_core.scss +12 -0
  86. data/vendor/assets/stylesheets/font-awesome/scss/_fixed-width.scss +6 -0
  87. data/vendor/assets/stylesheets/font-awesome/scss/_icons.scss +789 -0
  88. data/vendor/assets/stylesheets/font-awesome/scss/_larger.scss +13 -0
  89. data/vendor/assets/stylesheets/font-awesome/scss/_list.scss +19 -0
  90. data/vendor/assets/stylesheets/font-awesome/scss/_mixins.scss +60 -0
  91. data/vendor/assets/stylesheets/font-awesome/scss/_path.scss +15 -0
  92. data/vendor/assets/stylesheets/font-awesome/scss/_rotated-flipped.scss +20 -0
  93. data/vendor/assets/stylesheets/font-awesome/scss/_screen-reader.scss +5 -0
  94. data/vendor/assets/stylesheets/font-awesome/scss/_stacked.scss +20 -0
  95. data/vendor/assets/stylesheets/font-awesome/scss/_variables.scss +800 -0
  96. data/vendor/assets/stylesheets/font-awesome/scss/font-awesome.scss +18 -0
  97. data/vendor/assets/stylesheets/kakimasu.scss +35 -0
  98. metadata +364 -0
@@ -0,0 +1,11 @@
1
+ module CountingHelper
2
+ # Counts how much keys there is in key array
3
+ def countKeys(array)
4
+ array.flatten.count - array.count
5
+ end
6
+
7
+ # Gets translation percentage
8
+ def translationPercentage(all_keys, missing_translations)
9
+ ((1 - ( missing_translations.to_f / all_keys )) * 100).round(2)
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module FormattingHelper
2
+ # Remove surrounding quotes from string
3
+ def remove_surrounding_quotes(string)
4
+ string.gsub!(/^\"|"\Z/, '') unless string.blank?
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ module GroupingHelper
2
+ # Groups all translations by language in array
3
+ def group_by_language(array)
4
+ array.group_by { |el| el.partition('.').first }
5
+ end
6
+
7
+ # Gets all paths from keys array
8
+ def get_translation_key_paths(array)
9
+ array.map { |el| el.last }
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ module PaginationHelper
2
+ # Set page size
3
+ def set_page_size(size)
4
+ @size = size
5
+ end
6
+
7
+ def get_page_size
8
+ @size
9
+ end
10
+
11
+ # Calculates total number of pages
12
+ def total_pages(array)
13
+ (array.length.to_f / @size).ceil
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ # Sets translation key for lazy lookup or unsets it.
2
+ module SearchKeyHelper
3
+ def lazy_lookup_key(key, path)
4
+ path = 'app/views/' + path
5
+ file = File.read(path)
6
+
7
+ matches = Array.new #All places where this key is used in given file
8
+ locations = Array.new #Indexes which determines where to make changes
9
+ file_length_differential = 0 #File length difference because substitutions will change file size.
10
+
11
+ # Finds all places where given translation key is used
12
+ file.scan(/[=|\s][t][\s]*[(]?[\s]*['|:]#{key}/) do |c|
13
+ matches.push [c, $~.offset(0)[0]]
14
+ end
15
+
16
+ matches.each do |array|
17
+ if array.first.match /[=|\s][t][\s]*[(]?[\s]*[:]\w*/ # if translation written with : not '' then its special case
18
+ # Key with ':key' is sustituted with "'.key'"
19
+ index = array.first.index(/:#{key}/)
20
+ file.slice!(array.last + index + file_length_differential, (":#{key}").length)
21
+ file.insert(array.last + index + file_length_differential, "'.#{key}'")
22
+ file_length_differential += ("'.#{key}'").length - (":#{key}").length
23
+ elsif key[0] != '.'
24
+ index = array.first.index(key)
25
+ locations.push array.last + index + file_length_differential
26
+ end
27
+ end
28
+
29
+ # Insert '.' just before key for lazy lookup
30
+ locations.each_with_index do |location, index|
31
+ file.insert(location + index, '.')
32
+ end
33
+
34
+ File.write(path, file)
35
+ end
36
+
37
+ def remove_lazy_lookup(key, path)
38
+ path = 'app/views/' + path
39
+ file = File.read(path)
40
+
41
+ # Get rid of prefix of has lazy lookup prefix
42
+ if key.include? '.'
43
+ key = key.split('.').last
44
+ key.prepend '.'
45
+ end
46
+
47
+ matches = Array.new # All places where given key is used in file
48
+ locations = Array.new #Indexes which determintes where to make changes
49
+
50
+ key.slice! 0 if key[0] == '.'
51
+ file.scan(/[=|\s][t][\s]*[(]?[\s]*['][.]#{key}/) do |c|
52
+ matches.push [c, $~.offset(0)[0]]
53
+ end
54
+
55
+ matches.each do |array|
56
+ index = array.first.index('.' + key)
57
+ locations.push array.last + index
58
+ end
59
+
60
+ locations.each_with_index do |location, index|
61
+ file.slice!(location - index) # Removes '.' from match
62
+ end
63
+
64
+ File.write(path, file)
65
+ end
66
+ end
@@ -0,0 +1,101 @@
1
+ module SearchTranslationsHelper
2
+ # Method to get prefix for lazy lookup
3
+ def prefix(path)
4
+ prefix = path.split('/')
5
+ prefix[-1] = prefix.last.split('.').first
6
+ prefix = prefix.join('.')
7
+ end
8
+
9
+ # Method returns if key has missing translation in one of locales
10
+ def missing_translations?(key)
11
+ missing_translation = false
12
+ I18n.available_locales.each do |locale|
13
+ if !I18n.exists?(key, locale)
14
+ missing_translation = true # If in one of locales key has no translation, method returns true
15
+ end
16
+ end
17
+ missing_translation
18
+ end
19
+
20
+ # Method searches used translations keys in a file
21
+ def find_keys(file, path)
22
+ array = file.scan(/[=|\s][t][\s]*[(]?[\s]*['|:]([.]*\w*)/).uniq
23
+ keys = Array.new
24
+
25
+ # If key has '.' in front of it, it should be completed as lazy lookup key.
26
+ array.each_with_index do |key, index|
27
+ if key.first[0] == '.'
28
+ array[index] = key.first.insert(0, prefix(path))
29
+ end
30
+ end
31
+ array
32
+ end
33
+
34
+ # Method returns array of keys with missing translations from given array
35
+ def find_missing_translations_keys(array)
36
+ missing_translations_keys = []
37
+
38
+ array.each do |key|
39
+ missing_translations_keys.push key if missing_translations?(key)
40
+ end
41
+
42
+ missing_translations_keys
43
+ end
44
+
45
+ # Method searches used active record human attribute name keys in given file
46
+ def find_attribute_keys(file)
47
+ array = file.scan(/(\w*).human_attribute_name[(][:](\w*)/).uniq
48
+ keys = []
49
+ # Keys are transformed into correct active record attribute keys form
50
+ array.each do |arr|
51
+ arr = 'activerecord.' + 'attributes.' + arr.first.downcase + '.' + arr.last
52
+ keys.push arr
53
+ end
54
+ keys
55
+ end
56
+
57
+ # Method searches used Model name keys in given file
58
+ def find_model_name_keys(file)
59
+ array = file.scan(/(\w*).model_name.human/).uniq
60
+ keys = []
61
+ # Keys are transformed into correct active record model name keys form
62
+ array.each do |arr|
63
+ arr = 'activerecord.models.' + arr.first.downcase
64
+ keys.push arr
65
+ end
66
+ keys
67
+ end
68
+
69
+ # Method returns array with requested translation keys
70
+ def search_for_translations(category)
71
+ keys = []
72
+
73
+ Dir.glob("app/views/**/*").each do |path|
74
+ if File.file?(path)
75
+ file = File.read(path)
76
+ case category
77
+ when 'active_record_attribute_translations'
78
+ keys.push find_attribute_keys(file)
79
+ when 'active_record_model_translations'
80
+ keys.push find_model_name_keys(file)
81
+ when 'all_translation_keys'
82
+ path.slice! "app/views/"
83
+ keys.push find_keys(file, path).push path
84
+ when 'missing_translations'
85
+ path.slice! "app/views/"
86
+ # push in array only those keys which have missing translations
87
+ keys.push find_missing_translations_keys(find_keys(file, path)).push path
88
+ end
89
+ end
90
+ end
91
+
92
+ case category
93
+ when 'active_record_attribute_translations'
94
+ keys.flatten.uniq
95
+ when 'active_record_model_translations'
96
+ keys.flatten.uniq
97
+ else
98
+ keys
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,2 @@
1
+ class Translation < ActiveRecord::Base
2
+ end
@@ -0,0 +1,9 @@
1
+ class TranslationPolicy < ApplicationPolicy
2
+ def index?
3
+ user && user.admin?
4
+ end
5
+
6
+ def create?
7
+ user && user.admin?
8
+ end
9
+ end
@@ -0,0 +1,35 @@
1
+ <%= render 'kakimasu/shared/navbar' %>
2
+
3
+ <div class="container">
4
+ <div class="row">
5
+ <div class="col-md-4">
6
+ <h1 class="kakimasu-greetings"> Hello, user! </h1>
7
+
8
+ <% if params[:show_translation_key] == 'true' %>
9
+ <%= link_to t('deactivate_translation_mode'), { show_translation_key: false }, class: 'btn btn-primary' %>
10
+ <% else %>
11
+ <%= link_to t('activate_translation_mode'), { show_translation_key: true }, id: 'activate_translation_mode_button', class: 'btn btn-primary' %>
12
+ <% end %>
13
+ </div>
14
+
15
+ <div class="col-md-8" data-component="TranslationPercentChart">
16
+ <div class="translation-diagram">
17
+ <div id="translation-percentage-circle"></div>
18
+ <table>
19
+ <tr>
20
+ <th><%= t('total_translation_keys') %>:</th>
21
+ <td><%= @all_keys_count %></td>
22
+ </tr>
23
+ <tr>
24
+ <th><%= t('total_keys_with_missing_translation') %>:</th>
25
+ <td><%= @missing_translation_keys_count %></td>
26
+ </tr>
27
+ <tr class="translation-percentage" data-value="<%= @percentage %>">
28
+ <th><%= t('translation_percentage') %>:</th>
29
+ <td><%= @percentage %> %</td>
30
+ </tr>
31
+ </table>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </div>
@@ -0,0 +1 @@
1
+ window.location.reload()
@@ -0,0 +1,183 @@
1
+ <%= render 'kakimasu/shared/navbar' %>
2
+
3
+ <div data-component="TranslationModal">
4
+ <h1 class="margin-left-15"><%= t('translation_keys') %></h1>
5
+ <!-- Category list -->
6
+ <div>
7
+ <div class="col-md-3 translation-key-categories">
8
+ <h3><%=t('categories')%></h3>
9
+ <ul class="list-group">
10
+ <% @categories.each do |category| %>
11
+ <% if @category == category %>
12
+ <li class="list-group-item active"><%= link_to t(category), keys_category: category %></li>
13
+ <% else %>
14
+ <li class="list-group-item"><%= link_to t(category), keys_category: category %></li>
15
+ <% end %>
16
+ <% end %>
17
+ </ul>
18
+ </div>
19
+
20
+ <div class="col-md-9 translation-key-table">
21
+ <h3><%=t(@category)%></h3>
22
+ <!-- Translation keys table -->
23
+ <%-# Check if there are keys to show in tables -%>
24
+ <% if @category == 'missing_translations' && @keys.count == 0 %>
25
+ <h3><%= t('no_keys_with_missing_translations') %></h3>
26
+ <% elsif @category == 'all_translation_keys' && @keys.count == 0 %>
27
+ <h3><%= t('no_translation_keys_found') %></h3>
28
+ <% else %>
29
+ <table class="table" data-component="TranslationKeyTable">
30
+ <thead>
31
+ <tr>
32
+ <th><%= t('key') %></th>
33
+ <% I18n.available_locales.each do |locale| %>
34
+ <th><%= locale.upcase %></th>
35
+ <% end %>
36
+ <% if @category == 'missing_translations' || @category == 'all_translation_keys' %>
37
+ <th><%= t('path') %></th>
38
+ <% end %>
39
+ <th></th>
40
+ </tr>
41
+ </thead>
42
+
43
+ <tbody>
44
+ <%-# Iteration counter for pagination realisation -%>
45
+ <% key_nr = 0 %>
46
+ <% if @category == 'missing_translations' || @category == 'all_translation_keys' %>
47
+ <% @keys.each do |array| %>
48
+ <% path = array.pop %>
49
+ <% array = array.flatten %>
50
+ <% array.each do |key| %>
51
+ <%-# Checks if this key is in this page-%>
52
+ <% if get_page_size * (params[:page].to_i - 1) <= key_nr && key_nr < get_page_size * params[:page].to_i %>
53
+ <tr>
54
+ <td><%= key %></td>
55
+ <% translations = [] %>
56
+ <% I18n.available_locales.each do |locale| %>
57
+ <% if !I18n.exists?(key, locale) %>
58
+ <td class="missing_translation_cell"><%= key %></td>
59
+ <% else %>
60
+ <td><%= t(key, locale: locale) %></td>
61
+ <% end %>
62
+ <% translations.push t(key, locale: locale) %>
63
+ <% end %>
64
+ <td><i><%= path %></i></td>
65
+ <td class="text-center">
66
+ <%= content_tag 'button', class: 'translate-key-button', data: {toggle: 'modal', target: '#translateModal', key: "#{key}", translations: translations, locales: I18n.available_locales, path: path, unique:
67
+ (key.include? '.') } do %>
68
+ <i class="fa fa-pencil" aria-hidden="true"></i>
69
+ <% end %>
70
+ </td>
71
+ </tr>
72
+ <% end %>
73
+ <% key_nr += 1 %>
74
+ <% end %>
75
+ <% end %>
76
+
77
+ <% else %>
78
+ <% @keys.each do |key| %>
79
+ <% if get_page_size * (params[:page].to_i - 1) <= key_nr && key_nr < get_page_size * params[:page].to_i %>
80
+ <tr>
81
+ <td><%= key %></td>
82
+ <% translations = [] %>
83
+ <% I18n.available_locales.each do |locale| %>
84
+ <td><%= t(key, locale: locale) %></td>
85
+ <% translations.push t(key, locale: locale) %>
86
+ <% end %>
87
+ <td class="text-center">
88
+ <%= content_tag 'button', class: 'translate-key-button', data: {toggle: 'modal', target: '#translateModal', key: "#{key}", translations: translations, locales: I18n.available_locales} do %>
89
+ <i class="fa fa-pencil" aria-hidden="true"></i>
90
+ <% end %>
91
+ </td>
92
+ </tr>
93
+ <% end %>
94
+ <% key_nr += 1 %>
95
+ <% end %>
96
+ <% end %>
97
+ </tbody>
98
+ </table>
99
+ <% end %>
100
+
101
+ <!-- Pagination -->
102
+ <div class="text-center">
103
+ <% if total_pages(@keys.flatten) > 1 %>
104
+ <ul class="pagination">
105
+ <li>
106
+ <% if params[:page].to_i > 1 %>
107
+ <%= link_to '&laquo;'.html_safe, page: params[:page].to_i - 1, keys_category: @category %>
108
+ <% end %>
109
+ </li>
110
+ <% for i in 1..total_pages(@keys.flatten) %>
111
+ <% if params[:page].to_i == i %>
112
+ <li class="active"><%= link_to i, page: i, keys_category: @category %></li>
113
+ <% else %>
114
+ <li><%= link_to i, page: i, keys_category: @category %></li>
115
+ <% end %>
116
+ <% end %>
117
+ <li>
118
+ <% unless params[:page].to_i == total_pages(@keys.flatten) %>
119
+ <%= link_to '&raquo;'.html_safe, page: params[:page].to_i + 1, keys_category: @category %>
120
+ <% end %>
121
+ </li>
122
+ </ul>
123
+ <% end %>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- Modal -->
130
+ <div class="modal fade" id="translateModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
131
+ <div class="modal-dialog" role="document">
132
+ <div class="modal-content">
133
+ <div class="modal-header">
134
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
135
+ <p class="text-left translation-key"></p>
136
+ <p class="text-left translation-key-path"></p>
137
+ </div>
138
+ <div class="modal-body">
139
+ <%= form_tag kakimasu_keys_path(keys_category: @category), id: 'translation-modal-form', remote: true do %>
140
+ <p>
141
+ <%= hidden_field_tag :key %>
142
+ </p>
143
+ <p>
144
+ <%= hidden_field_tag :path %>
145
+ </p>
146
+ <% I18n.available_locales.each_with_index do |locale, index| %>
147
+ <div class="form-group">
148
+ <div class="row">
149
+ <div class="col-xs-1">
150
+ <%= label_tag locale %>
151
+ </div>
152
+ <div class="col-xs-7 translation-input">
153
+ <%= text_area_tag locale, nil, class: "form-control" %>
154
+ </div>
155
+ <div class="col-xs-4">
156
+ <div class="suggested-translation-link">
157
+ <a href='<%="#modal-translation-#{index}"%>' data-toggle="collapse"><%=t('suggested_translations')%></a>
158
+ <div id='<%="modal-translation-#{index}"%>' class="collapse suggested-translation">
159
+ <p></p>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ <% end %>
166
+ <% if @category == 'missing_translations' || @category == 'all_translation_keys' %>
167
+ <div class="row">
168
+ <div class="col-xs-4 col-xs-offset-1">
169
+ <div id='lazy-lookup-checkbox'></div>
170
+ </div>
171
+ </div>
172
+ <% end %>
173
+ <p class="text-right">
174
+ <%= button_tag(class: "btn btn-primary") do%>
175
+ <% t('save') %>
176
+ <% end %>
177
+ </p>
178
+ <% end %>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>