locale_setter 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in locale_setter.gemspec
4
+ gemspec
5
+
6
+ gem 'rspec'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jeff Casimir
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # LocaleSetter
2
+
3
+ `LocaleSetter` sets the locale for the current request in a Rails application.
4
+
5
+ ## Installation
6
+
7
+ ### Gem Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'locale_setter'
13
+ ```
14
+
15
+ Then execute:
16
+
17
+ ```
18
+ $ bundle
19
+ ```
20
+
21
+ ### Application Configuration
22
+
23
+ Include the module in your `app/controllers/application_controller.rb`:
24
+
25
+ ```ruby
26
+ class ApplicationController < ActionController::Base
27
+ protect_from_forgery
28
+
29
+ include LocaleSetter
30
+ end
31
+ ```
32
+
33
+ *Note:* If you have before filters or a module that handles user authentication, have that _above_ the `include LocaleSetter` so it happens first.
34
+
35
+ ## How It Works
36
+
37
+ One of the challenges with internationalization is knowing which locale a user actually wants. We recommend the following hierarchy of sources:
38
+
39
+ 1. URL Parameter
40
+ 2. User Preference
41
+ 3. HTTP Headers
42
+ 4. Default
43
+
44
+ ### URL Parameter
45
+
46
+ As a developer or designer, it's incredibly handy to be able to manipulate the URL to change locales. You might even use this with CI to run your integration tests using each locale you support.
47
+
48
+ If you're currently using the default locale for the application, generated URLs on your site will be untouched.
49
+
50
+ For example, say my default is `:en` for English and I am viewing in English, my URL might look like:
51
+
52
+ ```
53
+ http://example.com/articles/1
54
+ ```
55
+
56
+ If you're using a locale other than the default, then the parameter `locale` will be appended to every link.
57
+
58
+ For example, my default is still `:en` but I'm currently reading in Spanish (`:es`):
59
+
60
+ ```
61
+ http://example.com/articles/1&locale=es
62
+ ```
63
+
64
+ You do not need to do any handling of this URL parameter, `LocaleSetter` will take care of it.
65
+
66
+ If you *do* want to change the currently viewed locale, manipulate the URL parameter manually:
67
+
68
+ Starting with the default...
69
+
70
+ ```
71
+ http://example.com/articles/1
72
+ ```
73
+
74
+ I want to check out German, so I add `&locale=de`...
75
+
76
+ ```
77
+ http://example.com/articles/1&locale=de
78
+ ```
79
+
80
+ Then check things out in Spanish...
81
+
82
+ ```
83
+ http://example.com/articles/1&locale=es
84
+ ```
85
+
86
+ #### Non-Supported Locales
87
+
88
+ If the locale specified in the URL is not supported, `LocaleSetter` will revert to the default locale.
89
+
90
+ Note that care has been taken to prevent a symbol-table-overflow denial of service attack. Unsupported locales are not symbolized, so there is no danger.
91
+
92
+ ### User Preference
93
+
94
+ If your system has authentication, then you likely use have a `current_user` helper method available. `LocaleSetter` will call `locale` on current user, expecting to get back a string response.
95
+
96
+ It's up to you to compute / store this data.
97
+
98
+ #### Storing a User Preference
99
+
100
+ The easiest solution is to add a column to your users table:
101
+
102
+ ```
103
+ rails generate migration add_locale_to_users locale:string
104
+ rake db:migrate
105
+ ```
106
+
107
+ Then, allow them to edit this preference wherever they edit other profile items (email, name, etc). You might use a selector like this:
108
+
109
+ ```erb
110
+ <%= form_for @user do |f| %>
111
+ <%= f.collection_select :locale, I18n.available_locales %>
112
+ <% end %>
113
+ ```
114
+
115
+ Remember that you may need to modify the `user.rb` if you're filtering mass-assignment parameters.
116
+
117
+ ### HTTP Headers
118
+
119
+ Every request coming into your web server includes a ton of header information. One key/value pair looks like this:
120
+
121
+ ```
122
+ HTTP_ACCEPT_LANGUAGE='en-US,en;0.8,es;0.4'
123
+ ```
124
+
125
+ This string is created and sent by the user's browser. Most users have never configured it, the browser just picks it up from the host OS. It can usually be controlled through some kind of advanced preference pane.
126
+
127
+ The sample string above means...
128
+
129
+ * I prefer US English (`en-US`) for full comprehension
130
+ * I will take general English (`en`) and will understand about 80% of the content
131
+ * I will take general Spanish (`es`) and will understand about 40% of the content
132
+
133
+ `LocaleSetter` will take care of processing this string and will use the highest-comprehension locale that your application supports.
134
+
135
+ ### Default
136
+
137
+ Finally, if none of those previous options worked, the library will fall back to the currently specified `I18n.default_locale`.
138
+
139
+ ## Contributing
140
+
141
+ This library is considered "experimental" quality. Your feedback would be very welcome! Pull requests are great, but issues are good too.
142
+
143
+ ### Test Application / Example Usage
144
+
145
+ Check out https://github.com/jcasimir/locale_setter_test for a simple Rails application used to black-box test the library in real usage.
146
+
147
+ ## License
148
+
149
+ Please see the included LICENSE.txt
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ module LocaleSetter
2
+ module HTTP
3
+ def self.for(accept_language)
4
+ LocaleSetter::Matcher.match(AcceptLanguageParser.parse(accept_language))
5
+ end
6
+
7
+ module AcceptLanguageParser
8
+ LOCALE_SEPARATOR = ','
9
+ WEIGHT_SEPARATOR = ';'
10
+
11
+ def self.parse(accept_language)
12
+ locale_fragments = accept_language.split(LOCALE_SEPARATOR)
13
+ weighted_fragments = locale_fragments.map{|f| f.split(WEIGHT_SEPARATOR)}
14
+ sorted_fragments = weighted_fragments.sort_by{|f| -f.last.to_f }
15
+ sorted_fragments.map{|locale, weight| locale}
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ module LocaleSetter
2
+ module Matcher
3
+ def self.match(requested, against = available)
4
+ matched = (sanitize(requested) & against).first
5
+ matched.to_sym if matched
6
+ end
7
+
8
+ def self.sanitize(input)
9
+ if input.respond_to? :map
10
+ input.map{|l| sanitize_one(l)}
11
+ else
12
+ [sanitize_one(input)]
13
+ end
14
+ end
15
+
16
+ def self.sanitize_one(locale)
17
+ locale.to_s.downcase.strip
18
+ end
19
+
20
+ def self.available
21
+ I18n.available_locales.map(&:to_s)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module LocaleSetter
2
+ module Param
3
+ def self.for(param)
4
+ LocaleSetter::Matcher.match([param])
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ module LocaleSetter
2
+ module Rails
3
+ attr_accessor :i18n
4
+
5
+ def default_url_options(options = {})
6
+ if i18n.locale == i18n.default_locale
7
+ options
8
+ else
9
+ {:locale => i18n.locale}.merge(options)
10
+ end
11
+ end
12
+
13
+ def i18n
14
+ @i18n ||= I18n
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module LocaleSetter
2
+ module User
3
+ def self.for(user)
4
+ if user && user.locale && !user.locale.empty?
5
+ LocaleSetter::Matcher.match(user.locale)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module LocaleSetter
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,41 @@
1
+ require "locale_setter/version"
2
+ require "locale_setter/matcher"
3
+ require "locale_setter/rails"
4
+ require "locale_setter/http"
5
+ require "locale_setter/user"
6
+ require "locale_setter/param"
7
+
8
+ module LocaleSetter
9
+ include LocaleSetter::Rails
10
+
11
+ def self.included(controller)
12
+ if controller.respond_to?(:before_filter)
13
+ controller.before_filter :set_locale
14
+ end
15
+ end
16
+
17
+ def set_locale
18
+ i18n.locale = from_params ||
19
+ from_user ||
20
+ from_http ||
21
+ i18n.default_locale
22
+ end
23
+
24
+ def from_user
25
+ if respond_to?(:current_user) && current_user
26
+ LocaleSetter::User.for(current_user)
27
+ end
28
+ end
29
+
30
+ def from_http
31
+ if respond_to?(:request) && request.env && request.env['HTTP_ACCEPT_LANGUAGE']
32
+ LocaleSetter::HTTP.for(request.env['HTTP_ACCEPT_LANGUAGE'])
33
+ end
34
+ end
35
+
36
+ def from_params
37
+ if respond_to?(:params) && params[:locale]
38
+ LocaleSetter::Param.for(params[:locale])
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'locale_setter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "locale_setter"
8
+ gem.version = LocaleSetter::VERSION
9
+ gem.authors = ["Jeff Casimir"]
10
+ gem.email = ["jeff@casimircreative.com"]
11
+ gem.description = "Automatically set per-request locale in Rails applications"
12
+ gem.summary = "Automatically set per-request locale in Rails applications"
13
+ gem.homepage = "http://github.com/jcasimir/locale_setter"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe "LocaleSetter::HTTP" do
4
+ describe ".for" do
5
+ context "when the first choice is supported" do
6
+ before(:each){ I18n.available_locales = [:en, :es] }
7
+
8
+ context "given 'en'" do
9
+ it "returns :en" do
10
+ LocaleSetter::HTTP.for("en").should == :en
11
+ end
12
+ end
13
+
14
+ context "given two acceptable locales" do
15
+ it "returns :en" do
16
+ LocaleSetter::HTTP.for("en,es").should == :en
17
+ end
18
+ end
19
+ end
20
+
21
+ context "when the first choice is not supported" do
22
+ before(:each){ I18n.available_locales = [:es] }
23
+
24
+ context "given 'en,es'" do
25
+ it "returns :es" do
26
+ LocaleSetter::HTTP.for("en,es").should == :es
27
+ end
28
+ end
29
+ end
30
+
31
+ context "when using preference weightings" do
32
+ before(:each){ I18n.available_locales = [:en, :es] }
33
+
34
+ it "returns :en" do
35
+ LocaleSetter::HTTP.for("en;1,es;0.8").should == :en
36
+ end
37
+
38
+ it "handles misordered preferences" do
39
+ LocaleSetter::HTTP.for("es;0.8,en;1").should == :en
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ describe "LocaleSetter::Matcher" do
4
+ it "can properly match mixed case locales" do
5
+ LocaleSetter::Matcher.match(['EN-US'],['en-us']).should == :'en-us'
6
+ end
7
+
8
+ it "can match using a single string" do
9
+ LocaleSetter::Matcher.match('en', ['en']).should == :en
10
+ end
11
+
12
+ it "can match using an array" do
13
+ LocaleSetter::Matcher.match(['en','es'],['en','es']).should == :en
14
+ end
15
+
16
+ it "can fuzzy-match between locales with country and those without"
17
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'LocaleSetter::User' do
4
+ describe '#for' do
5
+ it "ignores a blank stored locale" do
6
+ blank = OpenStruct.new({:locale => ""})
7
+ LocaleSetter::User.for(blank).should_not be
8
+ end
9
+
10
+ it "ignores a stored locale that is not available" do
11
+ invalid = OpenStruct.new({:locale => "woof"})
12
+ LocaleSetter::User.for(invalid).should_not be
13
+ end
14
+
15
+ it "only tries current_user if it offers a locale" do
16
+ no_locale = OpenStruct.new()
17
+ LocaleSetter::User.for(no_locale).should_not be
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,172 @@
1
+ require 'spec_helper'
2
+
3
+ describe LocaleSetter do
4
+ it "exists" do
5
+ expect{ LocaleSetter }.to_not raise_error
6
+ end
7
+
8
+ class BareController; end
9
+
10
+ describe ".included" do
11
+ it "sets a before filter" do
12
+ BareController.should_receive(:before_filter).with(:set_locale)
13
+ BareController.send(:include, LocaleSetter)
14
+ end
15
+
16
+ it "skips setting the before_filter if not supported" do
17
+ expect{ BareController.send(:include, LocaleSetter) }.to_not raise_error
18
+ end
19
+ end
20
+
21
+ class Controller
22
+ include LocaleSetter
23
+ end
24
+
25
+ let(:controller){ Controller.new }
26
+
27
+ describe "#default_url_options" do
28
+ it "adds a :locale key" do
29
+ controller.default_url_options({})[:locale].should be
30
+ end
31
+
32
+ it "does not require a parameter" do
33
+ expect{ controller.default_url_options }.to_not raise_error
34
+ end
35
+
36
+ it "builds on passed in options" do
37
+ result = controller.default_url_options({:test => true})
38
+ result[:test].should be
39
+ result[:locale].should be
40
+ end
41
+
42
+ it "defers to a passed in locale" do
43
+ result = controller.default_url_options({:locale => 'abc'})
44
+ result[:locale].should == 'abc'
45
+ end
46
+
47
+ it "doesn't appent a locale if it's the default" do
48
+ controller.i18n.locale = controller.i18n.default_locale
49
+ controller.default_url_options({})[:locale].should_not be
50
+ end
51
+
52
+ it "appends a locale when not the default" do
53
+ controller.i18n.locale = :sample
54
+ controller.default_url_options({})[:locale].should == :sample
55
+ end
56
+ end
57
+
58
+ describe "#set_locale" do
59
+ context "with nothing" do
60
+ let(:controller){ Controller.new }
61
+
62
+ it "uses the default" do
63
+ controller.set_locale
64
+ controller.i18n.locale.should == controller.i18n.default_locale
65
+ end
66
+ end
67
+
68
+ context "with HTTP headers" do
69
+ let(:controller){ HTTPController.new }
70
+
71
+ class HTTPController < Controller
72
+ def request
73
+ OpenStruct.new(:env => {'HTTP_ACCEPT_LANGUAGE' => "es,en"})
74
+ end
75
+ end
76
+
77
+ it "makes use of the HTTP headers" do
78
+ controller.set_locale
79
+ controller.i18n.locale.should == :es
80
+ end
81
+
82
+ it "only sets an available locale" do
83
+ I18n.available_locales = [:arr, :en]
84
+ controller.set_locale
85
+ controller.i18n.locale.should == :en
86
+ end
87
+
88
+ it "does nothing when HTTP_ACCEPT_LANGUAGE is missing" do
89
+ class BlankHTTPController < Controller
90
+ def request
91
+ OpenStruct.new(:env => {})
92
+ end
93
+ end
94
+
95
+ blank = BlankHTTPController.new
96
+ blank.set_locale.should == blank.i18n.default_locale
97
+ end
98
+ end
99
+
100
+ context "with a current_user who has a locale" do
101
+ let(:controller){ UserController.new }
102
+
103
+ before(:each) do
104
+ controller.i18n.available_locales = [:en, :user_specified]
105
+ end
106
+
107
+ class UserController < HTTPController
108
+ def current_user
109
+ OpenStruct.new({:locale => :user_specified})
110
+ end
111
+ end
112
+
113
+ it "prioritizes the current_user preference over HTTP" do
114
+ LocaleSetter::HTTP.should_not_receive(:for)
115
+ controller.set_locale
116
+ end
117
+
118
+ it "uses the stored locale" do
119
+ controller.set_locale
120
+ controller.i18n.locale.should == :user_specified
121
+ end
122
+ end
123
+
124
+ context "with url parameters" do
125
+ let(:controller){ ParamController.new }
126
+
127
+ before(:each) do
128
+ controller.i18n.available_locales = [:en, :param_specified]
129
+ end
130
+
131
+ class ParamController < Controller
132
+ def params
133
+ {:locale => "param_specified"}
134
+ end
135
+ end
136
+
137
+ it "uses the URL parameter" do
138
+ controller.set_locale
139
+ controller.i18n.locale.should == :param_specified
140
+ end
141
+
142
+ it "only allows supported locales" do
143
+ controller.i18n.available_locales = [:en]
144
+ controller.set_locale
145
+ controller.i18n.locale.should == controller.i18n.default_locale
146
+ end
147
+
148
+ it "does not pollute the symbol table when given an unsuported locale" do
149
+ class BadParamController < Controller
150
+ def params
151
+ {:locale => "bad_param"}
152
+ end
153
+ end
154
+ expect { BadParamController.new.set_locale }.to_not change{ Symbol.all_symbols.count }
155
+ end
156
+ end
157
+ end
158
+
159
+ describe "#i18n" do
160
+ it "uses the pre-set i18n library" do
161
+ stand_in = OpenStruct
162
+ controller.i18n = stand_in
163
+ controller.i18n.should == stand_in
164
+ end
165
+
166
+ class I18n; end
167
+
168
+ it "uses the default I18n library when not overridden" do
169
+ controller.i18n.should == I18n
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,5 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ require 'locale_setter'
4
+ require 'ostruct'
5
+ require 'support/i18n'
@@ -0,0 +1,25 @@
1
+ class I18n
2
+ def self.locale
3
+ @@locale ||= ""
4
+ end
5
+
6
+ def self.locale=(input)
7
+ @@locale = input
8
+ end
9
+
10
+ def self.available_locales
11
+ @@available_locales ||= []
12
+ end
13
+
14
+ def self.available_locales=(input)
15
+ @@available_locales = input
16
+ end
17
+
18
+ def self.default_locale
19
+ @@default_locale ||= :default
20
+ end
21
+
22
+ def self.default_locale=(input)
23
+ @@default_locale = input
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: locale_setter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeff Casimir
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-20 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Automatically set per-request locale in Rails applications
15
+ email:
16
+ - jeff@casimircreative.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - .gitignore
22
+ - .rspec
23
+ - Gemfile
24
+ - LICENSE.txt
25
+ - README.md
26
+ - Rakefile
27
+ - lib/locale_setter.rb
28
+ - lib/locale_setter/http.rb
29
+ - lib/locale_setter/matcher.rb
30
+ - lib/locale_setter/param.rb
31
+ - lib/locale_setter/rails.rb
32
+ - lib/locale_setter/user.rb
33
+ - lib/locale_setter/version.rb
34
+ - locale_setter.gemspec
35
+ - spec/locale/http_spec.rb
36
+ - spec/locale/matcher_spec.rb
37
+ - spec/locale/user_spec.rb
38
+ - spec/locale_setter_spec.rb
39
+ - spec/spec_helper.rb
40
+ - spec/support/i18n.rb
41
+ homepage: http://github.com/jcasimir/locale_setter
42
+ licenses: []
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubyforge_project:
61
+ rubygems_version: 1.8.24
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: Automatically set per-request locale in Rails applications
65
+ test_files:
66
+ - spec/locale/http_spec.rb
67
+ - spec/locale/matcher_spec.rb
68
+ - spec/locale/user_spec.rb
69
+ - spec/locale_setter_spec.rb
70
+ - spec/spec_helper.rb
71
+ - spec/support/i18n.rb