turnout2024 3.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +19 -0
- data/README.md +200 -0
- data/lib/rack/turnout.rb +28 -0
- data/lib/tasks/maintenance.rake +48 -0
- data/lib/turnout/configuration.rb +60 -0
- data/lib/turnout/engine.rb +16 -0
- data/lib/turnout/i18n/accept_language_parser.rb +109 -0
- data/lib/turnout/i18n/internationalization.rb +141 -0
- data/lib/turnout/maintenance_file.rb +119 -0
- data/lib/turnout/maintenance_page/base.rb +79 -0
- data/lib/turnout/maintenance_page/erb.rb +22 -0
- data/lib/turnout/maintenance_page/html.rb +21 -0
- data/lib/turnout/maintenance_page/json.rb +26 -0
- data/lib/turnout/maintenance_page.rb +24 -0
- data/lib/turnout/ordered_options.rb +98 -0
- data/lib/turnout/rake_tasks.rb +3 -0
- data/lib/turnout/request.rb +35 -0
- data/lib/turnout/version.rb +3 -0
- data/lib/turnout.rb +15 -0
- data/public/maintenance.html +69 -0
- data/public/maintenance.html.erb +69 -0
- data/public/maintenance.json +1 -0
- metadata +221 -0
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 [](https://codeclimate.com/github/pglombardo/turnout2024) [](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
|
+
```
|
data/lib/rack/turnout.rb
ADDED
@@ -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
|