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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2cb2ef95ffdfb5f8460ccf2ff0b855e308a31d4375c8b90d977c48160323880c
4
+ data.tar.gz: 78060ff50be1952e29a7113766b4aaccf48b1bf9b9394236b1259c8dd1e43e98
5
+ SHA512:
6
+ metadata.gz: '09dbe770f9e2d99e94c522e313ea3483b2546c0ee9edaa06e907335ade487abe300c931b1415c29f4395917a726c76bdea58089d8e6e296dd154847211b597ce'
7
+ data.tar.gz: 32d1836c17e69a9a9b9f7cae727415def212fde9d01d6c63dc1cfa9a94551a88654a1629ae15f536067ee0fcbab37c38b5f053863aee7bf72edcc5a5aed81646
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2022 Weston Ganger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,177 @@
1
+ # Rails I18n Manager
2
+ > A complete translation editor and workflow
3
+
4
+ <a href='https://github.com/westonganger/rails_i18n_manager/actions' target='_blank'><img src="https://github.com/westonganger/rails_i18n_manager/workflows/Tests/badge.svg" style="max-width:100%;" height='21' style='border:0px;height:21px;' border='0' alt="CI Status"></a>
5
+
6
+ Web interface to manage i18n translations helping to facilitate the editors of your translations. Provides a low-tech and complete workflow for importing, translating, and exporting your I18n translation files. Designed to allow you to keep the translation files inside your projects git repository where they should be.
7
+
8
+ Features:
9
+
10
+ - Import & export translations using standard i18n YAML/JSON files
11
+ - Allows managing translations for any number of apps
12
+ - Built in support for Google Translation for missing translations
13
+ - Provides an API end point to perform automated downloads of your translations
14
+
15
+ ## Screenshots
16
+ ![Screenshot](/screenshot_list.png)
17
+ <br><br>
18
+ ![Screenshot](/screenshot_import.png)
19
+ <br><br>
20
+ ![Screenshot](/screenshot_edit.png)
21
+
22
+ ## Setup
23
+
24
+ Developed as a Rails engine. So you can add to any existing app or create a brand new app with the functionality.
25
+
26
+ First add the gem to your Gemfile
27
+
28
+ ```ruby
29
+ ### Gemfile
30
+ gem "rails_i18n_manager", git: "https://github.com/westonganger/rails_i18n_manager"
31
+ ```
32
+
33
+ Then install and run the database migrations
34
+
35
+ ```sh
36
+ bundle install
37
+ bundle exec rake rails_i18n_manager:install:migrations
38
+ bundle exec rake db:migrate
39
+ ```
40
+
41
+ ### Routes
42
+
43
+ #### Option A: Mount to a path
44
+
45
+ ```ruby
46
+ ### config/routes.rb
47
+
48
+ ### As sub-path
49
+ mount RailsI18nManager::Engine, at: "/rails_i18n_manager", as: "rails_i18n_manager"
50
+
51
+ ### OR as root-path
52
+ mount RailsI18nManager::Engine, at: "/", as: "rails_i18n_manager"
53
+ ```
54
+
55
+ #### Option B: Mount to a subdomain
56
+
57
+ ```ruby
58
+ ### config/routes.rb
59
+
60
+ translations_engine_subdomain = "translations"
61
+
62
+ mount RailsI18nManager::Engine,
63
+ at: "/", as: "translations_engine",
64
+ constraints: Proc.new{|request| request.subdomain == translations_engine_subdomain }
65
+
66
+ not_engine = Proc.new{|request| request.subdomain != translations_engine_subdomain }
67
+
68
+ constraints not_engine do
69
+ # your app routes here...
70
+ end
71
+ ```
72
+
73
+ ### Configuration
74
+
75
+ ```ruby
76
+ ### config/initializers/rails_i18n_manager.rb
77
+
78
+ RailsI18nManager.config do |config|
79
+ config.google_translate_api_key = ENV.fetch("GOOGLE_TRANSLATE_API_KEY", nil)
80
+
81
+ ### You can use our built-in list of all locales Google Translate supports
82
+ ### OR make your own list. These need to be supported by Google Translate
83
+ # config.valid_locales = ["en", "es", "fr"]
84
+ end
85
+ ```
86
+
87
+ ### Customizing Authentication
88
+
89
+ ```ruby
90
+ ### config/routes.rb
91
+
92
+ ### Using Devise
93
+ authenticated :user do
94
+ mount RailsI18nManager::Engine, at: "/rails_i18n_manager", as: "rails_i18n_manager"
95
+ end
96
+
97
+ ### Custom devise-like
98
+ constraints ->(req){ req.session[:user_id].present? && User.find_by(id: req.session[:user_id]) } do
99
+ mount RailsI18nManager::Engine, at: "/rails_i18n_manager", as: "rails_i18n_manager"
100
+ end
101
+
102
+ ### HTTP Basic Auth
103
+ with_http_basic_auth = ->(engine){
104
+ Rack::Builder.new do
105
+ use Rack::Auth::Basic do |username, password|
106
+ ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(username), ::Digest::SHA256.hexdigest(ENV.fetch("RAILS_I18N_MANAGER_USERNAME"))) &&
107
+ ActiveSupport::SecurityUtils.secure_compare(::Digest::SHA256.hexdigest(password), ::Digest::SHA256.hexdigest(ENV.fetch("RAILS_I18N_MANAGER_PASSWORD")))
108
+ end
109
+ run(engine)
110
+ end
111
+ }
112
+ mount with_http_basic_auth.call(RailsI18nManager::Engine), at: "/rails_i18n_manager", as: "rails_i18n_manager"
113
+ ```
114
+
115
+ ### API Endpoint
116
+
117
+ We provide an endpoint to retrieve your translation files at `/translations`.
118
+
119
+ You will likely want to add your own custom authentication strategy and can do so using a routing constraint on the `mount RailsI18nManager` call.
120
+
121
+ From that point you can implement an automated mechanism to update your apps translations using the provided API end point. Some examples
122
+
123
+ An example in Ruby:
124
+
125
+ ```ruby
126
+ require 'open-uri'
127
+
128
+ zip_stream = URI.open('https://translations-manager.example.com/translations.zip?export_format=yaml')
129
+ IO.copy_stream(zip_stream, '/tmp/my-app-locales.zip')
130
+ `unzip /tmp/my-app-locales.zip /tmp/my-app-locales/`
131
+ `rsync --delete-after /tmp/my-app-locales/my-app/ /path/to/my-app/config/locales/`
132
+ puts "Locales are now updated, app restart not-required"
133
+ ```
134
+
135
+ A command line example using curl:
136
+
137
+ ```
138
+ curl https://translations-manager.example.com/translations.zip?export_format=json -o /tmp/my-app-locales.zip \
139
+ && unzip /tmp/my-app-locales.zip /tmp/my-app-locales/ \
140
+ && rsync --delete-after /tmp/my-app-locales/my-app/ \
141
+ && echo "Locales are now updated, app restart not-required"
142
+ ```
143
+
144
+ ## Development
145
+
146
+ Run migrations using: `rails db:migrate`
147
+
148
+ Run server using: `bin/dev` or `cd test/dummy/; rails s`
149
+
150
+ ## Testing
151
+
152
+ ```
153
+ bundle exec rspec
154
+ ```
155
+
156
+ We can locally test different versions of Rails using `ENV['RAILS_VERSION']` and different database gems using `ENV['DB_GEM']`
157
+
158
+ ```
159
+ export RAILS_VERSION=7.0
160
+ export DB_GEM=sqlite3
161
+ bundle install
162
+ bundle exec rspec
163
+ ```
164
+
165
+ ## Other Translation Managers & Web Interfaces
166
+
167
+ For comparison, some other projects for managing Rails translations.
168
+
169
+ - https://github.com/tolk/tolk - This is the project that inspired rails_i18n_manager. UI and file-based approach. I [attempted to revive tolk](https://github.com/tolk/tolk/pull/161) but gave up as I found the codebase and workflow was really just a legacy ball of spagetti.
170
+ - https://github.com/prograils/lit
171
+ - https://github.com/alphagov/rails_translation_manager
172
+ - https://github.com/glebm/i18n-tasks
173
+
174
+
175
+ # Credits
176
+
177
+ Created & Maintained by [Weston Ganger](https://westonganger.com) - [@westonganger](https://github.com/westonganger)
data/Rakefile ADDED
@@ -0,0 +1,18 @@
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
+ APP_RAKEFILE = File.expand_path("spec/dummy/Rakefile", __dir__)
8
+ load 'rails/tasks/engine.rake'
9
+
10
+ load 'rails/tasks/statistics.rake'
11
+
12
+ require 'bundler/gem_tasks'
13
+
14
+ task :test do
15
+ system("rspec", out: STDOUT)
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,5 @@
1
+ module RailsI18nManager
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+ end
5
+ end
@@ -0,0 +1,71 @@
1
+ module RailsI18nManager
2
+ class TranslationAppsController < ApplicationController
3
+ before_action :set_translation_app, only: [:show, :edit, :update, :destroy]
4
+
5
+ def index
6
+ @translation_apps = TranslationApp
7
+ .sort_order(params[:sort], params[:direction], base_sort_order: "#{TranslationApp.table_name}.name ASC")
8
+ .multi_search(params[:search])
9
+ .page(params[:page])
10
+ end
11
+
12
+ def new
13
+ @translation_app = TranslationApp.new
14
+ render "form"
15
+ end
16
+
17
+ def create
18
+ @translation_app = TranslationApp.new(permitted_params)
19
+
20
+ if @translation_app.save
21
+ flash[:notice] = "Successfully created."
22
+ redirect_to action: :edit, id: @translation_app.id
23
+ else
24
+ flash.now[:error] = "Create failed."
25
+ render "rails_i18n_manager/translation_apps/form"
26
+ end
27
+ end
28
+
29
+ def show
30
+ redirect_to action: :edit
31
+ end
32
+
33
+ def edit
34
+ render "form"
35
+ end
36
+
37
+ def update
38
+ if @translation_app.update(permitted_params)
39
+ flash[:notice] = "Update success."
40
+ redirect_to action: :index
41
+ else
42
+ flash.now[:error] = "Update failed."
43
+ render "rails_i18n_manager/translation_apps/form"
44
+ end
45
+ end
46
+
47
+ def destroy
48
+ if @translation_app.destroy
49
+ flash[:notice] = "Deleted '#{@translation_app.name}'"
50
+ else
51
+ flash[:alert] = "Delete failed"
52
+ end
53
+ redirect_to action: :index
54
+ end
55
+
56
+ private
57
+
58
+ def set_translation_app
59
+ @translation_app = TranslationApp.find_by!(id: params[:id])
60
+ end
61
+
62
+ def set_browser_title
63
+ @browser_title = TranslationApp::NAME.pluralize
64
+ end
65
+
66
+ def permitted_params
67
+ params.require(:translation_app).permit(:name, :default_locale, additional_locales: [])
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,248 @@
1
+ module RailsI18nManager
2
+ class TranslationsController < ApplicationController
3
+ before_action :get_translation_key, only: [:show, :edit, :update, :destroy]
4
+
5
+ def index
6
+ case params[:sort]
7
+ when "app_name"
8
+ sort = "#{TranslationApp.table_name}.name"
9
+ when "updated_at"
10
+ sort = "#{TranslationKey.table_name}.updated_at"
11
+ else
12
+ sort = params[:sort]
13
+ end
14
+
15
+ @translation_keys = TranslationKey
16
+ .includes(:translation_app, :translation_values)
17
+ .references(:translation_app)
18
+ .sort_order(sort, params[:direction], base_sort_order: "#{TranslationApp.table_name}.name ASC, #{TranslationKey.table_name}.key ASC")
19
+
20
+ apply_filters
21
+
22
+ if request.format.to_sym != :html && TranslationApp.first.nil?
23
+ request.format = :html
24
+ flash[:alert] = "No Translation apps exists"
25
+ redirect_to action: :index
26
+ return false
27
+ end
28
+
29
+ respond_to do |format|
30
+ format.html do
31
+ @translation_keys = @translation_keys.page(params[:page])
32
+ end
33
+
34
+ format.any do
35
+ @translations_keys = @translation_keys.where(active: true) ### Ensure exported keys are active for any exports
36
+
37
+ case request.format.to_sym
38
+ when :csv
39
+ send_data @translation_keys.to_csv, filename: "translations.csv"
40
+ when :zip
41
+ file = @translation_keys.export_to(format: params[:export_format], zip: true, app_name: params[:app_name].presence)
42
+
43
+ if file
44
+ send_file file, filename: "translations-#{params[:export_format]}-#{params[:app_name].presence || "all-apps"}.zip"
45
+ else
46
+ flash[:alert] = "Sorry, Nothing to export"
47
+ redirect_to action: :index
48
+ end
49
+ else
50
+ raise ActionController::UnknownFormat
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def show
57
+ render "edit"
58
+ end
59
+
60
+ def edit
61
+ end
62
+
63
+ def update
64
+ @translation_key.assign_attributes(allowed_params)
65
+
66
+ if @translation_key.save
67
+ flash[:notice] = "Update success."
68
+ redirect_to edit_translation_path(@translation_key)
69
+ else
70
+ flash[:notice] = "Update failed."
71
+ render "translations/edit"
72
+ end
73
+ end
74
+
75
+ def destroy
76
+ if @translation_key.active
77
+ redirect_to translations_path, alert: "Cannot delete active translations"
78
+ else
79
+ @translation_key.destroy!
80
+ redirect_to translations_path, notice: "Delete Successful"
81
+ end
82
+ end
83
+
84
+ def delete_inactive_keys
85
+ @translation_keys = TranslationKey
86
+ .where(active: false)
87
+ .includes(:translation_app, :translation_values)
88
+ .references(:translation_app)
89
+
90
+ apply_filters
91
+
92
+ ids = translation_keys.pluck(:id)
93
+
94
+ TranslationKey.where(id: ids).delete_all
95
+ TranslationValue.where(translation_key_id: ids).delete_all
96
+ end
97
+
98
+ def import
99
+ @form = Forms::TranslationFileForm.new(params[:import_form])
100
+
101
+ if request.get?
102
+ render
103
+ else
104
+ if @form.valid?
105
+ begin
106
+ TranslationsImportJob.new.perform(
107
+ translation_app_id: @form.translation_app_id,
108
+ import_file: @form.file.path,
109
+ overwrite_existing: @form.overwrite_existing,
110
+ mark_inactive_translations: @form.mark_inactive_translations,
111
+ )
112
+ rescue TranslationsImportJob::ImportAbortedError => e
113
+ flash.now.alert = e.message
114
+ render
115
+ return
116
+ end
117
+
118
+ redirect_to translations_path, notice: "Import Successful"
119
+ else
120
+ flash.now.alert = "Import not started due to form errors."
121
+ render
122
+ end
123
+ end
124
+ end
125
+
126
+ def translate_missing
127
+ @translation_keys = TranslationKey.includes(:translation_values)
128
+
129
+ apply_filters
130
+
131
+ translated_count = 0
132
+ total_missing = 0
133
+
134
+ if params[:app_name]
135
+ app_locales = TranslationApp.find_by(name: params[:app_name]).additional_locales_array
136
+ else
137
+ @translation_keys = @translation_keys.includes(:translation_app)
138
+ end
139
+
140
+ ### Check & Translate for Every i18n key
141
+ @translation_keys.each do |key_record|
142
+ locales = (app_locales || key_record.translation_app.additional_locales_array)
143
+
144
+ ### Filter to just google translate supported languages
145
+ locales = locales.intersection(GoogleTranslate.supported_locales)
146
+
147
+ default_translation_text = key_record.default_translation
148
+
149
+ next if default_translation_text.blank?
150
+
151
+ locales.each do |locale|
152
+ if locale == key_record.translation_app.default_locale
153
+ next ### skip, we dont translate the default locale
154
+ end
155
+
156
+ val_record = key_record.translation_values.detect{|x| x.locale == locale.to_s }
157
+
158
+ ### Translate Missing
159
+ if val_record.nil? || val_record.translation.blank?
160
+ total_missing += 1
161
+
162
+ translated_text = GoogleTranslate.translate(
163
+ default_translation_text,
164
+ from: key_record.translation_app.default_locale,
165
+ to: locale
166
+ )
167
+
168
+ if translated_text.present?
169
+ if val_record.nil?
170
+ val_record = key_record.translation_values.new(locale: locale)
171
+ end
172
+
173
+ val_record.assign_attributes(translation: translated_text)
174
+
175
+ val_record.save!
176
+
177
+ translated_count += 1
178
+ end
179
+ end
180
+ end
181
+ end
182
+
183
+ if params[:translation_key_id]
184
+ url = request.referrer || translation_path(params[:translation_key_id])
185
+ else
186
+ url = params.to_unsafe_h.merge(action: :index)
187
+ end
188
+
189
+ redirect_to url, notice: "Translated #{translated_count} of #{total_missing} total missing translations"
190
+ end
191
+
192
+ private
193
+
194
+ def get_translation_key
195
+ @translation_key = TranslationKey.includes(:translation_values).find_by!(id: params[:id])
196
+ end
197
+
198
+ def allowed_params
199
+ params.require(:translation_key).permit(translation_values_attributes: [:id, :locale, :translation])
200
+ end
201
+
202
+ def apply_filters
203
+ if params[:app_name].present?
204
+ @translation_keys = @translation_keys.joins(:translation_app).where(TranslationApp.table_name => {name: params[:app_name]})
205
+ end
206
+
207
+ if params[:translation_key_id].present?
208
+ @translation_keys = @translation_keys.where(id: params[:translation_key_id])
209
+ end
210
+
211
+ if request.format.html?
212
+ ### ONLY FOR HTML - SO THAT WE DONT DOWNLOAD INCOMPLETE TRANSLATION EXPORT PACKAGES
213
+
214
+ if params[:search].present?
215
+ @translation_keys = @translation_keys.search(params[:search])
216
+ end
217
+
218
+ if params[:status] == "Inactive"
219
+ @translation_keys = @translation_keys.where(active: false)
220
+ elsif params[:status] == "All"
221
+ # Do nothing
222
+ else
223
+ @translation_keys = @translation_keys.where(active: true)
224
+ end
225
+
226
+ if params[:status] == "Missing"
227
+ missing_key_ids = []
228
+ TranslationApp.all.each do |app_record|
229
+ app_record.translation_keys.includes(:translation_values).each do |key_record|
230
+ if key_record.translation_values.size != app_record.all_locales.size
231
+ missing_key_ids << key_record.id
232
+ end
233
+ end
234
+ end
235
+
236
+ @translation_keys = @translation_keys
237
+ .references(:translation_values)
238
+ .where("#{TranslationValue.table_name}.translation IS NULL OR #{TranslationKey.table_name}.id IN (:ids)", ids: missing_key_ids)
239
+ end
240
+ end
241
+ end
242
+
243
+ def set_browser_title
244
+ @browser_title = "Translations"
245
+ end
246
+
247
+ end
248
+ end
@@ -0,0 +1,74 @@
1
+ module RailsI18nManager
2
+ module ApplicationHelper
3
+
4
+ def custom_form_for(*args, **options, &block)
5
+ options[:builder] = CustomFormBuilder
6
+ if options.has_key?(:defaults)
7
+ @_custom_form_for_defaults = options.delete(:defaults)
8
+ end
9
+ form_for(*args, options, &block)
10
+ end
11
+
12
+ def custom_fields_for(*args, **options, &block)
13
+ options[:builder] = CustomFormBuilder
14
+ if options.has_key?(:defaults)
15
+ @_custom_form_for_defaults = options.delete(:defaults)
16
+ end
17
+ fields_for(*args, options, &block)
18
+ end
19
+
20
+ ASSET_VERSION = `git show -s --format=%ci`.parameterize.freeze
21
+ def custom_asset_path(path)
22
+ "#{path}?v=#{Rails.env.development? ? Time.now.to_i : ASSET_VERSION}"
23
+ end
24
+
25
+ def breadcrumb_item(title, url=nil)
26
+ if url.nil?
27
+ %Q(<span class="breadcrumb-item">#{title}</span>).html_safe
28
+ else
29
+ %Q(<span class="breadcrumb-item"><a href="#{url}">#{title}</a></span>).html_safe
30
+ end
31
+ end
32
+
33
+ def sort_link(attr_name, label=nil)
34
+ if label.blank?
35
+ label = attr_name.to_s.titleize
36
+ end
37
+
38
+ direction = params[:direction].present? && params[:direction].casecmp?("asc") ? 'desc' : 'asc'
39
+
40
+ link_to label, params.to_unsafe_h.merge(sort: attr_name, direction: direction)
41
+ end
42
+
43
+ def nav_link(name, url, html_options={}, &block)
44
+ url = url_for(url)
45
+
46
+ if html_options.has_key?(:active)
47
+ active = html_options.delete(:active)
48
+ elsif url == (url.include?("?") ? request.fullpath : request.path)
49
+ active = true
50
+ end
51
+
52
+ html_options[:class] ||= ""
53
+
54
+ html_options[:class] += " nav-link"
55
+
56
+ if active
57
+ html_options[:class] += " active"
58
+ end
59
+
60
+ html_options[:class].strip!
61
+
62
+ content_tag(:li, class: "nav-item #{'active' if active}".strip) do
63
+ link_to(name, url, html_options) + (
64
+ if block_given?
65
+ content_tag(:ul) do
66
+ capture(&block)
67
+ end
68
+ end
69
+ )
70
+ end
71
+ end
72
+
73
+ end
74
+ end