Skip to content

Commit

Permalink
Add code and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
docelic committed Feb 17, 2018
1 parent e0a895c commit 229d218
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 5 deletions.
9 changes: 9 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ authors:
crystal: 0.24.1

license: MIT

dependencies:
amber:
github: amberframework/amber
branch: master
# version: 0.6.7

i18n:
github: TechMagister/i18n.cr
27 changes: 23 additions & 4 deletions spec/citrine-i18n_spec.cr
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
require "./spec_helper"

describe Citrine::I18n do
# TODO: Write tests
require "http"

it "works" do
false.should eq(true)
I18n.load_path += ["./spec/fixtures/"]
I18n.init

describe Citrine::Pipe::I18n do

it "should set language from header" do
request = HTTP::Request.new("GET", "/")
request.headers["Accept-Language"] = "fr,en-US;q=0.7,en;q=0.3"
context = create_context(request)
handler = Citrine::Pipe::I18n.new
handler.call(context)
I18n.locale.should eq "fr"
end

it "should set language from complicated header" do
request = HTTP::Request.new("GET", "/")
request.headers["Accept-Language"] = "fr;q=0.6,en-US;q=0.7"
context = create_context(request)
handler = Citrine::Pipe::I18n.new
handler.call(context)
I18n.locale.should eq "en"
end

end
2 changes: 2 additions & 0 deletions spec/fixtures/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
test: ""
2 changes: 2 additions & 0 deletions spec/fixtures/fr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
test: ""
7 changes: 7 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
require "spec"
require "../src/citrine-i18n"

def create_context(request)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
HTTP::Server::Context.new(request, response)
end

8 changes: 7 additions & 1 deletion src/citrine-i18n.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
require "./citrine-i18n/*"
require "amber"
require "i18n"
require "./citrine-i18n/parser"
require "./citrine-i18n/pipes/i18n"

module Citrine::I18n
def self.configure
yield ::I18n.config
end
end
116 changes: 116 additions & 0 deletions src/citrine-i18n/parser.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
module Citrine::I18n

# Taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
# Taken from https://github.com/TechMagister/kemalyst-i18n/src/kemalyst-i18n/parser.cr
class Parser
getter :header

@header : String
@user_preferred_languages : Array(String)?

def initialize(header)
@header = header
end

# Returns a sorted array based on user preference in HTTP_ACCEPT_LANGUAGE.
# Browsers send this HTTP header, so don't think this is holy.
#
# Example:
#
# request.user_preferred_languages
# # => [ 'nl-NL', 'nl-BE', 'nl', 'en-US', 'en' ]
#
def user_preferred_languages
@user_preferred_languages ||= begin
header.to_s.gsub(/\s+/, "").split(",").map do |language|
splited = language.split(";q=")
locale, quality = splited[0], splited[1]?
raise ArgumentError.new "Not correctly formatted" unless locale =~ /^[a-z\-0-9]+|\*$/i

locale = locale.downcase.gsub(/-[a-z0-9]+$/i, &.upcase) # Uppercase territory
locale = nil if locale == "*" # Ignore wildcards

quality = quality ? quality.to_f : 1.0

{locale, quality}
end.sort do |(_, left), (_, right)|
right <=> left
end.map(&.first).compact
rescue ArgumentError # Just rescue anything if the browser messed up badly.
[] of String
end
end

# Sets the user languages preference, overriding the browser
#
def user_preferred_languages=(languages)
@user_preferred_languages = languages
end

# Finds the locale specifically requested by the browser.
#
# Example:
#
# request.preferred_language_from I18n.available_locales
# # => 'nl'
#
def preferred_language_from(array)
(user_preferred_languages & array.map(&:to_s)).first
end

# Returns the first of the user_preferred_languages that is compatible
# with the available locales. Ignores region.
#
# Example:
#
# request.compatible_language_from I18n.available_locales
#
def compatible_language_from(available_languages)
user_preferred_languages.map do |preferred| #en-US
preferred = preferred.downcase
preferred_language = preferred.split("-", 2).first

available_languages.find do |available| # en
available = available.to_s.downcase
preferred == available || preferred_language == available.split("-", 2).first
end
end.compact.first?
end

# Returns a supplied list of available locals without any extra application info
# that may be attached to the locale for storage in the application.
#
# Example:
# [ja_JP-x1, en-US-x4, en_UK-x5, fr-FR-x3] => [ja-JP, en-US, en-UK, fr-FR]
#
def sanitize_available_locales(available_languages)
available_languages.map do |available|
available.to_s.split(/[_-]/).reject { |part| part.start_with?("x") }.join("-")
end
end

# Returns the first of the user preferred languages that is
# also found in available languages. Finds best fit by matching on
# primary language first and secondarily on region. If no matching region is
# found, return the first language in the group matching that primary language.
#
# Example:
#
# request.language_region_compatible(available_languages)
#
def language_region_compatible_from(available_languages)
available_languages = sanitize_available_locales(available_languages)
user_preferred_languages.map do |preferred| #en-US
preferred = preferred.downcase
preferred_language = preferred.split("-", 2).first

lang_group = available_languages.select do |available| # en
preferred_language == available.downcase.split("-", 2).first
end

lang_group.find { |lang| lang.downcase == preferred } || lang_group.first #en-US, en-UK
end.compact.first
end
end
end

19 changes: 19 additions & 0 deletions src/citrine-i18n/pipes/i18n.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Citrine
module Pipe
# Handler to initialize I18n for the request.
class I18n < Amber::Pipe::Base
HEADER = "Accept-Language"

def call(context : HTTP::Server::Context)
if languages = context.request.headers[HEADER]?
parser = Citrine::I18n::Parser.new languages
compat = parser.compatible_language_from ::I18n.available_locales
::I18n.locale = compat if compat
#Amber.logger.debug "Languages available: #{languages.to_s}"
#Amber.logger.debug "Language chosen: #{::I18n.locale}"
end
call_next(context)
end
end
end
end

0 comments on commit 229d218

Please sign in to comment.