turnout2024 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![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
|
+
```
|
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
|