turnout2024 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3b00ca152fc3d0806180d89874a359c21a922a987f1eccd1ea0dba32b628e1a0
4
+ data.tar.gz: 52ba067e7e5b23006b43af7d9145cefb53c94f1d561d488c2b6415818ad73348
5
+ SHA512:
6
+ metadata.gz: 1151228b249536564690e506ffde5e29557a3554698883194dc75f23c1f1cfe9df9a6c51194905cc4201359912763224379b47df69d6ee3f2259659712029081
7
+ data.tar.gz: 7976574ce79f8d1a6671650b799cb37ffba9654b8ef3898c1fd501c2c939b88deb322d9f8064e4cbbc237f6c8a61e8219e4f3df5233b9e6889ee19dab82c608b
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 by Biola University
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,200 @@
1
+ Turnout2024 [![Code Climate](https://codeclimate.com/github/pglombardo/turnout2024.svg)](https://codeclimate.com/github/pglombardo/turnout2024) [![Gem Version](https://badge.fury.io/rb/turnout2024.svg)](https://badge.fury.io/rb/turnout2024)
2
+ =======
3
+ Turnout2024 is [Rack](http://rack.rubyforge.org/) middleware with a [Ruby on Rails](http://rubyonrails.org) engine that allows you to easily put your app in maintenance mode.
4
+
5
+ This project is forked from the original [turnout gem](https://github.com/biola/turnout) which unfortunately hasn't been updated in 6+ years.
6
+
7
+ The gem name has been updated but everything internally is still `Turnout`. Pull requests are more than welcome!
8
+
9
+ Features
10
+ ========
11
+ * Easy installation
12
+ * Rake commands to turn maintenance mode on and off
13
+ * Easily provide a reason for each downtime without editing the maintenance.html file
14
+ * Allow certain IPs or IP ranges to bypass the maintenance page
15
+ * Allow certain paths to be accessible during maintenance
16
+ * Easily override the default maintenance.html file with your own
17
+ * Simple [YAML](http://yaml.org) based config file for easy activation, deactivation and configuration without the rake commands
18
+ * Support for multiple maintenance page formats. Current [HTML](http://en.wikipedia.org/wiki/HTML) and [JSON](http://en.wikipedia.org/wiki/JSON)
19
+ * Supports Rails, [Sinatra](http://sinatrarb.com) and any other Rack application
20
+ * Supports multiple maintenance file paths so that groups of applications can be put into maintenance mode at once.
21
+
22
+ Installation
23
+ ============
24
+ Rails 3+
25
+ --------
26
+ In your `Gemfile` add:
27
+
28
+ gem 'turnout2024'
29
+
30
+ then run
31
+
32
+ bundle install
33
+
34
+ _Note that you'll need to restart your Rails server before it will work_
35
+
36
+ Sinatra
37
+ -------
38
+
39
+ In your Sinatra app file
40
+
41
+ ```ruby
42
+ require 'rack/turnout'
43
+
44
+ class App < Sinatra::Base
45
+ configure do
46
+ use Rack::Turnout
47
+ ```
48
+
49
+ In your Rakefile
50
+
51
+ ```ruby
52
+ require 'turnout/rake_tasks'
53
+ ```
54
+
55
+ Activation
56
+ ==========
57
+
58
+ rake maintenance:start
59
+
60
+ or
61
+
62
+ rake maintenance:start reason="Somebody googled Google!"
63
+
64
+ or
65
+
66
+ rake maintenance:start allowed_paths="/login,^/faqs/[0-9]*"
67
+
68
+ or
69
+
70
+ rake maintenance:start allowed_ips="4.8.15.16"
71
+
72
+ or
73
+
74
+ rake maintenance:start reason="Someone told me I should type <code>sudo rm -rf /</code>" allowed_paths="^/help,^/contact_us" allowed_ips="127.0.0.1,192.168.0.0/24"
75
+
76
+ or if you've configured `named_maintenance_file_paths` with a path named `server`
77
+
78
+ rake maintenance:server:start
79
+
80
+ Notes
81
+ -----
82
+ * The `reason` parameter can contain HTML
83
+ * Multiple `allowed_paths` and `allowed_ips` can be given. Just comma separate them.
84
+ * All `allowed_paths` are treated as regular expressions.
85
+ * If you need to use a comma in an `allowed_paths` regular expression just escape it with a backslash: `\,`.
86
+ * IP ranges can be given to `allowed_ips` using [CIDR notation](http://en.wikipedia.org/wiki/CIDR_notation).
87
+
88
+ Deactivation
89
+ ============
90
+
91
+ rake maintenance:end
92
+
93
+ or if you activated with a named path like `server`
94
+
95
+ rake maintenance:server:end
96
+
97
+ Configuration
98
+ =============
99
+
100
+ Turnout can be configured in two different ways:
101
+
102
+ 1. __Pass a config hash to the middleware__
103
+
104
+ ```ruby
105
+ use Rack::Turnout,
106
+ app_root: '/some/path',
107
+ named_maintenance_file_paths: {app: 'tmp/app.yml', server: '/tmp/server.yml'},
108
+ maintenance_pages_path: 'app/views/maintenance',
109
+ default_maintenance_page: Turnout::MaintenancePage::JSON,
110
+ default_reason: 'Somebody googled Google!',
111
+ default_allowed_paths: ['^/admin/'],
112
+ default_response_code: 418,
113
+ default_retry_after: 3600
114
+ ```
115
+
116
+ 2. __Using a config block__
117
+
118
+ ```ruby
119
+ Turnout.configure do |config|
120
+ config.skip_middleware = true
121
+ config.app_root = '/some/path'
122
+ config.named_maintenance_file_paths = {app: 'tmp/app.yml', server: '/tmp/server.yml'}
123
+ config.maintenance_pages_path = 'app/views/maintenance'
124
+ config.default_maintenance_page = Turnout::MaintenancePage::JSON
125
+ config.default_reason = 'Somebody googled Google!'
126
+ config.default_allowed_paths = ['^/admin/']
127
+ config.default_response_code = 418
128
+ config.default_retry_after = 3600
129
+ end
130
+ ```
131
+
132
+ __NOTICE:__ Any custom configuration should be loaded not only in the app but in the rake task. This should happen automatically in Rails as the `environment` task is run if it exists. But you may want to create your own `environment` task in non-Rails apps.
133
+
134
+ Default Configuration
135
+ ---------------------
136
+
137
+ ```ruby
138
+ Turnout.configure do |config|
139
+ config.app_root = '.'
140
+ config.named_maintenance_file_paths = {default: config.app_root.join('tmp', 'maintenance.yml').to_s}
141
+ config.maintenance_pages_path = config.app_root.join('public').to_s
142
+ config.default_maintenance_page = Turnout::MaintenancePage::HTML
143
+ config.default_reason = "The site is temporarily down for maintenance.\nPlease check back soon."
144
+ config.default_allowed_paths = []
145
+ config.default_response_code = 503
146
+ config.default_retry_after = 7200
147
+ end
148
+ ```
149
+
150
+ Customization
151
+ =============
152
+
153
+ [Default maintenance pages](https://github.com/biola/turnout/blob/master/public/) are provided, but you can create your own `public/maintenance.[html|json|html.erb]` files instead. If you provide a `reason` to the rake task, Turnout will parse the maintenance page file and attempt to replace a [Liquid](http://liquidmarkup.org/)-style `{{ reason }}` tag with the provided reason. So be sure to include a `{{ reason }}` tag in your `maintenance.html` file. In the case of a `.html.erb` file, `reason` will be a local variable.
154
+
155
+ __WARNING:__
156
+ The source code of any custom maintenance files you created in the `/public` directory will be able to be viewed by visiting that URL directly (i.e. `http://example.com/maintenance.html.erb`). This shouldn't be an issue with HTML and JSON files but with ERB files, it could be. If you're going to use a custom `.erb.html` file, we recommend you change the `maintenance_pages_path` setting to something other than the `/public` directory.
157
+
158
+ Tips
159
+ ====
160
+
161
+ Denied Paths
162
+ --------------
163
+ There is no `denied_paths` feature because turnout denies everything by default.
164
+ However you can achieve the same sort of functionality by using
165
+ [negative lookaheads](http://www.regular-expressions.info/lookaround.html) with the `allowed_paths` setting, like so:
166
+
167
+ rake maintenance:start allowed_paths="^(?!/your/under/maintenance/path)"
168
+
169
+ Multi-App Maintenance
170
+ ------------------------
171
+ A central `named_maintenance_file_path` can be configured in all your apps such as `/tmp/turnout.yml` so that all apps on a server can be put into mainteance mode at once. You could even configure service based paths such as `/tmp/mongodb_maintenance.yml` so that all apps using MongoDB could be put into maintenance mode.
172
+
173
+ Detecting Maintenance Mode
174
+ -------------------------------
175
+
176
+ If you'd like to detect if maintenance mode is on in your app (for those users or pages that aren't blocked) just call `!Turnout::MaintenanceFile.find.nil?`.
177
+
178
+ Behind the Scenes
179
+ =================
180
+ On every request the Rack app will check to see if `tmp/maintenance.yml` exists. If the file exists the maintenance page will be shown (unless allowed IPs are given and the requester is in the allowed range).
181
+
182
+ So if you want to get the maintenance page up or down in a hurry `touch tmp/maintenance.yml` and `rm tmp/maintenance.yml` will work.
183
+
184
+ Turnout will attempt to parse the `maintenance.yml` file looking for `reason`, `allowed_ip` and other settings. The file is checked on every request so you can change these values manually or just rerun the `rake maintenance:start` command.
185
+
186
+ Example maintenance.yml File
187
+ ----------------------------
188
+
189
+ ```yaml
190
+ ---
191
+ reason: Someone told me I should type <code>sudo rm -rf /</code>
192
+ allowed_paths:
193
+ - ^/help
194
+ - ^/contact_us
195
+ allowed_ips:
196
+ - 127.0.0.1
197
+ - 192.168.0.0/24
198
+ response_code: 503
199
+ retry_after: 3600
200
+ ```
@@ -0,0 +1,28 @@
1
+ require 'rack'
2
+ require 'turnout'
3
+
4
+ class Rack::Turnout
5
+ def initialize(app, config={})
6
+ @app = app
7
+
8
+ Turnout.config.update config
9
+
10
+ if config[:app_root].nil? && app.respond_to?(:app_root)
11
+ Turnout.config.app_root = app.app_root
12
+ end
13
+ end
14
+
15
+ def call(env)
16
+ request = Turnout::Request.new(env)
17
+ settings = Turnout::MaintenanceFile.find
18
+
19
+ if settings && !request.allowed?(settings)
20
+ page_class = Turnout::MaintenancePage.best_for(env)
21
+ page = page_class.new(settings.reason, env: env)
22
+
23
+ page.rack_response(settings.response_code, settings.retry_after)
24
+ else
25
+ @app.call(env)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,48 @@
1
+ require 'turnout'
2
+
3
+ namespace :maintenance do
4
+ desc 'Enable the maintenance mode page ("reason", "allowed_paths", "allowed_ips" and "response_code" can be passed as environment variables)'
5
+ rule /\Amaintenance:(.*:|)start\Z/ do |task|
6
+ invoke_environment
7
+
8
+ maint_file = maintenance_file_for(task)
9
+ maint_file.import_env_vars(ENV)
10
+ maint_file.write
11
+
12
+ puts "Created #{maint_file.path}"
13
+ puts "Run `rake #{task.name.gsub(/\:start/, ':end')}` to stop maintenance mode"
14
+ end
15
+
16
+ desc 'Disable the maintenance mode page'
17
+ rule /\Amaintenance:(.*:|)end\Z/ do |task|
18
+ invoke_environment
19
+
20
+ maint_file = maintenance_file_for(task)
21
+
22
+ if maint_file.delete
23
+ puts "Deleted #{maint_file.path}"
24
+ else
25
+ fail 'Could not find a maintenance file to delete'
26
+ end
27
+ end
28
+
29
+ def invoke_environment
30
+ if Rake::Task.task_defined? 'environment'
31
+ Rake::Task['environment'].invoke
32
+ end
33
+ end
34
+
35
+ def maintenance_file_for(task)
36
+ path_name = (task.name.split(':') - ['maintenance', 'start', 'end']).join(':')
37
+
38
+ maint_file = if path_name == ''
39
+ Turnout::MaintenanceFile.default
40
+ else
41
+ Turnout::MaintenanceFile.named(path_name)
42
+ end
43
+
44
+ fail %{Unknown path name: "#{path_name}"} if maint_file.nil?
45
+
46
+ maint_file
47
+ end
48
+ end
@@ -0,0 +1,60 @@
1
+ require_relative './ordered_options'
2
+ module Turnout
3
+ class Configuration
4
+ SETTINGS = [
5
+ :app_root,
6
+ :named_maintenance_file_paths,
7
+ :maintenance_pages_path,
8
+ :default_maintenance_page,
9
+ :default_reason,
10
+ :default_allowed_ips,
11
+ :skip_middleware,
12
+ :default_allowed_paths,
13
+ :default_response_code,
14
+ :default_retry_after,
15
+ :i18n
16
+ ].freeze
17
+
18
+ SETTINGS.each do |setting|
19
+ attr_accessor setting
20
+ end
21
+
22
+ def initialize
23
+ @skip_middleware = false
24
+ @app_root = '.'
25
+ @named_maintenance_file_paths = {default: app_root.join('tmp', 'maintenance.yml').to_s}
26
+ @maintenance_pages_path = app_root.join('public').to_s
27
+ @default_maintenance_page = Turnout::MaintenancePage::HTML
28
+ @default_reason = "The site is temporarily down for maintenance.\nPlease check back soon."
29
+ @default_allowed_paths = []
30
+ @default_allowed_ips = []
31
+ @default_response_code = 503
32
+ @default_retry_after = 7200 # 2 hours by default
33
+ @i18n = Turnout::OrderedOptions.new
34
+ @i18n.railties_load_path = []
35
+ @i18n.load_path = []
36
+ @i18n.fallbacks = Turnout::OrderedOptions.new
37
+ @i18n.enabled = false
38
+ @i18n.use_language_header = false
39
+ end
40
+
41
+ def app_root
42
+ Pathname.new(@app_root.to_s)
43
+ end
44
+
45
+ def named_maintenance_file_paths=(named_paths)
46
+ # Force keys to symbols
47
+ @named_maintenance_file_paths = Hash[named_paths.map { |k, v| [k.to_sym, v] }]
48
+ end
49
+
50
+ def update(settings_hash)
51
+ settings_hash.each do |setting, value|
52
+ unless SETTINGS.include? setting.to_sym
53
+ raise ArgumentError, "invalid setting: #{setting}"
54
+ end
55
+
56
+ self.public_send "#{setting}=", value
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ require 'turnout'
2
+ require 'rack/turnout'
3
+ require 'rails'
4
+
5
+ # For Rails 3
6
+ if defined? Rails::Engine
7
+ module Turnout
8
+ class Engine < Rails::Engine
9
+ initializer 'turnout.add_to_middleware_stack' do |app|
10
+ unless Turnout.config.skip_middleware
11
+ app.config.middleware.use(Rack::Turnout)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,109 @@
1
+ module Turnout
2
+ class AcceptLanguageParser
3
+ attr_accessor :header
4
+
5
+ def initialize(header)
6
+ @header = header
7
+ end
8
+
9
+ # Returns a sorted array based on user preference in HTTP_ACCEPT_LANGUAGE.
10
+ # Browsers send this HTTP header, so don't think this is holy.
11
+ #
12
+ # Example:
13
+ #
14
+ # request.user_preferred_languages
15
+ # # => [ 'nl-NL', 'nl-BE', 'nl', 'en-US', 'en' ]
16
+ #
17
+ def user_preferred_languages
18
+ return [] if header.to_s.strip.empty?
19
+ @user_preferred_languages ||= begin
20
+ header.to_s.gsub(/\s+/, '').split(',').map do |language|
21
+ locale, quality = language.split(';q=')
22
+ raise ArgumentError, 'Not correctly formatted' unless locale =~ /^[a-z\-0-9]+|\*$/i
23
+
24
+ locale = locale.downcase.gsub(/-[a-z0-9]+$/i, &:upcase) # Uppercase territory
25
+ locale = nil if locale == '*' # Ignore wildcards
26
+
27
+ quality = quality ? quality.to_f : 1.0
28
+
29
+ [locale, quality]
30
+ end.sort do |(_, left), (_, right)|
31
+ right <=> left
32
+ end.map(&:first).compact
33
+ rescue ArgumentError # Just rescue anything if the browser messed up badly.
34
+ []
35
+ end
36
+ end
37
+
38
+ # Sets the user languages preference, overriding the browser
39
+ #
40
+ def user_preferred_languages=(languages)
41
+ @user_preferred_languages = languages
42
+ end
43
+
44
+ # Finds the locale specifically requested by the browser.
45
+ #
46
+ # Example:
47
+ #
48
+ # request.preferred_language_from I18n.available_locales
49
+ # # => 'nl'
50
+ #
51
+ def preferred_language_from(array)
52
+ (user_preferred_languages & array.map(&:to_s)).first
53
+ end
54
+
55
+ # Returns the first of the user_preferred_languages that is compatible
56
+ # with the available locales. Ignores region.
57
+ #
58
+ # Example:
59
+ #
60
+ # request.compatible_language_from I18n.available_locales
61
+ #
62
+ def compatible_language_from(available_languages)
63
+ user_preferred_languages.map do |preferred| #en-US
64
+ preferred = preferred.downcase
65
+ preferred_language = preferred.split('-', 2).first
66
+
67
+ available_languages.find do |available| # en
68
+ available = available.to_s.downcase
69
+ preferred == available || preferred_language == available.split('-', 2).first
70
+ end
71
+ end.compact.first
72
+ end
73
+
74
+ # Returns a supplied list of available locals without any extra application info
75
+ # that may be attached to the locale for storage in the application.
76
+ #
77
+ # Example:
78
+ # [ja_JP-x1, en-US-x4, en_UK-x5, fr-FR-x3] => [ja-JP, en-US, en-UK, fr-FR]
79
+ #
80
+ def sanitize_available_locales(available_languages)
81
+ available_languages.map do |available|
82
+ available.to_s.split(/[_-]/).reject { |part| part.start_with?("x") }.join("-")
83
+ end
84
+ end
85
+
86
+ # Returns the first of the user preferred languages that is
87
+ # also found in available languages. Finds best fit by matching on
88
+ # primary language first and secondarily on region. If no matching region is
89
+ # found, return the first language in the group matching that primary language.
90
+ #
91
+ # Example:
92
+ #
93
+ # request.language_region_compatible(available_languages)
94
+ #
95
+ def language_region_compatible_from(available_languages)
96
+ available_languages = sanitize_available_locales(available_languages)
97
+ user_preferred_languages.map do |preferred| #en-US
98
+ preferred = preferred.downcase
99
+ preferred_language = preferred.split('-', 2).first
100
+
101
+ lang_group = available_languages.select do |available| # en
102
+ preferred_language == available.downcase.split('-', 2).first
103
+ end
104
+
105
+ lang_group.find { |lang| lang.downcase == preferred } || lang_group.first #en-US, en-UK
106
+ end.compact.first
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,141 @@
1
+ require 'i18n'
2
+ require 'i18n/backend/fallbacks'
3
+ require_relative './accept_language_parser'
4
+ require_relative '../ordered_options'
5
+
6
+ module Turnout
7
+ class Internationalization
8
+ class << self
9
+ attr_reader :env
10
+ attr_writer :env
11
+
12
+ def initialize_i18n(env)
13
+ @env = env
14
+ setup_i18n_config
15
+ end
16
+
17
+ def i18n_config
18
+ @i18n_config = Turnout.config.i18n
19
+ @i18n_config = @i18n_config.is_a?(Turnout::OrderedOptions) ? @i18n_config : Turnout::InheritableOptions.new(@i18n_config)
20
+ end
21
+
22
+ def turnout_page
23
+ @turnout_page ||= Turnout.config.default_maintenance_page
24
+ end
25
+
26
+ def http_accept_language
27
+ language = (env.nil? || env.empty?) ? nil : env["HTTP_ACCEPT_LANGUAGE"]
28
+ @http_accept_language ||= Turnout::AcceptLanguageParser.new(language)
29
+ end
30
+
31
+ def setup_additional_helpers
32
+ i18n_additional_helpers = i18n_config.delete(:additional_helpers)
33
+ i18n_additional_helpers = i18n_additional_helpers.is_a?(Array) ? i18n_additional_helpers : []
34
+
35
+ i18n_additional_helpers.each do |helper|
36
+ turnout_page.send(:include, helper) if helper.is_a?(Module)
37
+ end
38
+ end
39
+
40
+ def expanded(path)
41
+ result = []
42
+ if File.directory?(path)
43
+ result.concat(Dir.glob(File.join(path, '**', '**')).map { |file| file }.sort)
44
+ else
45
+ result << path
46
+ end
47
+ result.uniq!
48
+ result
49
+ end
50
+
51
+ # Returns all expanded paths but only if they exist in the filesystem.
52
+ def existent(path)
53
+ expanded(path).select { |f| File.exist?(f) }
54
+ end
55
+
56
+ # Setup i18n configuration.
57
+ def setup_i18n_config
58
+ return unless i18n_config.enabled
59
+ setup_additional_helpers
60
+ fallbacks = i18n_config.delete(:fallbacks)
61
+
62
+
63
+ # Avoid issues with setting the default_locale by disabling available locales
64
+ # check while configuring.
65
+ enforce_available_locales = i18n_config.delete(:enforce_available_locales)
66
+ enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
67
+ I18n.enforce_available_locales = false
68
+
69
+ i18n_config.except(:enabled, :use_language_header).each do |setting, value|
70
+ case setting
71
+ when :railties_load_path
72
+ I18n.load_path.unshift(*value.map { |file| existent(file) }.flatten)
73
+ when :load_path
74
+ I18n.load_path += value
75
+ else
76
+ I18n.send("#{setting}=", value)
77
+ end
78
+ end
79
+
80
+
81
+ init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
82
+ I18n.backend.load_translations
83
+
84
+ # Restore available locales check so it will take place from now on.
85
+ I18n.enforce_available_locales = enforce_available_locales
86
+
87
+ begin
88
+ if i18n_config.use_language_header
89
+ I18n.locale = http_accept_language.compatible_language_from(I18n.available_locales) || I18n.default_locale
90
+ else
91
+ I18n.locale = I18n.default_locale
92
+ end
93
+ rescue
94
+ #nothing
95
+ end
96
+
97
+ end
98
+
99
+ def array_wrap(object)
100
+ if object.nil?
101
+ []
102
+ elsif object.respond_to?(:to_ary)
103
+ object.to_ary || [object]
104
+ else
105
+ [object]
106
+ end
107
+ end
108
+
109
+ def include_fallbacks_module
110
+ I18n.backend.class.send(:include, I18n::Backend::Fallbacks)
111
+ end
112
+
113
+ def init_fallbacks(fallbacks)
114
+ include_fallbacks_module
115
+
116
+ args = case fallbacks
117
+ when Turnout::OrderedOptions
118
+ [*(fallbacks[:defaults] || []) << fallbacks[:map]].compact
119
+ when Hash, Array
120
+ array_wrap(fallbacks)
121
+ else # TrueClass
122
+ []
123
+ end
124
+
125
+ I18n.fallbacks = I18n::Locale::Fallbacks.new(*args)
126
+ end
127
+
128
+ def validate_fallbacks(fallbacks)
129
+ case fallbacks
130
+ when Turnout::OrderedOptions
131
+ !fallbacks.empty?
132
+ when TrueClass, Array, Hash
133
+ true
134
+ else
135
+ raise "Unexpected fallback type #{fallbacks.inspect}"
136
+ end
137
+ end
138
+
139
+ end
140
+ end
141
+ end