2017-04-28 04:04:26 +08:00
|
|
|
#
|
|
|
|
# Copyright (C) 2011 - present Instructure, Inc.
|
|
|
|
#
|
|
|
|
# This file is part of Canvas.
|
|
|
|
#
|
|
|
|
# Canvas is free software: you can redistribute it and/or modify it under
|
|
|
|
# the terms of the GNU Affero General Public License as published by the Free
|
|
|
|
# Software Foundation, version 3 of the License.
|
|
|
|
#
|
|
|
|
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
|
|
# details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License along
|
|
|
|
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2011-07-13 04:31:40 +08:00
|
|
|
module LocaleSelection
|
|
|
|
def infer_locale(options = {})
|
|
|
|
context = options[:context]
|
|
|
|
user = options[:user]
|
|
|
|
root_account = options[:root_account]
|
|
|
|
accept_language = options[:accept_language]
|
2014-08-23 07:04:00 +08:00
|
|
|
session_locale = options[:session_locale]
|
2018-03-26 22:37:26 +08:00
|
|
|
ignore_browser_locale = options[:ignore_browser_locale]
|
2011-07-13 04:31:40 +08:00
|
|
|
|
2012-04-05 06:28:09 +08:00
|
|
|
# groups cheat and set the context to be the group after get_context runs
|
|
|
|
# but before set_locale runs, but we want to do locale lookup based on the
|
|
|
|
# actual context.
|
|
|
|
if context && context.is_a?(Group) && context.context
|
|
|
|
context = context.context
|
|
|
|
end
|
|
|
|
|
2014-09-26 02:20:17 +08:00
|
|
|
sources = [
|
|
|
|
-> { context.locale if context.try(:is_a?, Course) },
|
|
|
|
-> { user.locale if user && user.locale },
|
|
|
|
-> { session_locale if session_locale },
|
|
|
|
-> { context.account.try(:default_locale, true) if context.try(:is_a?, Course) },
|
|
|
|
-> { context.default_locale(true) if context.try(:is_a?, Account) },
|
|
|
|
-> { root_account.try(:default_locale) },
|
|
|
|
-> {
|
2017-11-28 02:26:46 +08:00
|
|
|
if accept_language && locale = infer_browser_locale(accept_language, LocaleSelection.locales_with_aliases)
|
2014-09-26 02:20:17 +08:00
|
|
|
user.update_attribute(:browser_locale, locale) if user && user.browser_locale != locale
|
|
|
|
locale
|
|
|
|
end
|
|
|
|
},
|
2018-03-26 22:37:26 +08:00
|
|
|
-> { !ignore_browser_locale && user.try(:browser_locale) },
|
2014-09-26 02:20:17 +08:00
|
|
|
-> { I18n.default_locale.to_s }
|
|
|
|
]
|
|
|
|
|
|
|
|
sources.each do |source|
|
|
|
|
locale = source.call
|
|
|
|
locale = nil unless I18n.locale_available?(locale)
|
|
|
|
return locale if locale
|
|
|
|
end
|
|
|
|
nil
|
2011-07-13 04:31:40 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
QUALITY_VALUE = /;q=([01]\.(\d{0,3})?)/
|
|
|
|
LANGUAGE_RANGE = /([a-zA-Z]{1,8}(-[a-zA-Z]{1,8})*|\*)(#{QUALITY_VALUE})?/
|
|
|
|
SEPARATOR = /\s*,\s*/
|
|
|
|
ACCEPT_LANGUAGE = /\A#{LANGUAGE_RANGE}(#{SEPARATOR}#{LANGUAGE_RANGE})*\z/
|
|
|
|
|
2017-11-28 02:26:46 +08:00
|
|
|
def infer_browser_locale(accept_language, locales_with_aliases)
|
2011-07-13 04:31:40 +08:00
|
|
|
return nil unless accept_language =~ ACCEPT_LANGUAGE
|
2017-11-28 02:26:46 +08:00
|
|
|
supported_locales = locales_with_aliases.keys
|
2011-07-13 04:31:40 +08:00
|
|
|
|
|
|
|
ranges = accept_language.downcase.split(SEPARATOR).map{ |range|
|
|
|
|
quality = (range =~ QUALITY_VALUE) ? $1.to_f : 1
|
|
|
|
[range.sub(/\s*;.*/, ''), quality]
|
|
|
|
}
|
|
|
|
ranges = ranges.sort_by{ |r,| r == '*' ? 1 : -r.count('-') }
|
|
|
|
# we want the longest ranges first (and * last of all), since the "quality
|
|
|
|
# factor assigned to a [language] ... is the quality value of the longest
|
|
|
|
# language-range ... that matches", e.g.
|
|
|
|
# given that i accept 'en, es;q=0.9, en-US;q=0.8'
|
|
|
|
# and canvas is localized in 'en-US' and 'es'
|
|
|
|
# then i should get 'es' (en and en-US ranges both match en-US, and
|
|
|
|
# en-US range is a longer match, so it loses)
|
|
|
|
|
|
|
|
best_locales = supported_locales.inject([]) { |ary, locale|
|
|
|
|
if best_range = ranges.detect { |r, q| r + '-' == (locale.downcase + '-')[0..r.size] || r == '*' }
|
tweak tie-breaking logic for Accept-Language header
previously in the case of ties in the quality value we sorted by length (so en
beats en-US), and then alphabetically (so ar beats en). this part of the spec
is somewhat ambiguous, but this ordering does not take into account the order
passed into the accept header. so passing "en,ar" would have chosen 'ar'
because quality and length both tie, and 'ar' is first alphabetically.
to fix this we are incorporating the order of the types passed into the accept
header as one of the tie breaking criteria.
this fixes the bug with CutyCapt snapshots of canvas being rendered in arabic
because CutyCapt passes "en,*" as it's accept header. up until recently, 'en'
happened to be alphabetically first in our list of supported languages, so
everything worked out. but when we added support for arabic, a new
alphabetical winner emerged, exposing this bug.
fixes CNVS-3070
test plan:
- in an environment with CutyCapt enabled
- create a web submission assignment that allows url submissions
- submit a canvas url that has this fix applied (note that you can run this
from anywhere, it's the url you submit that needs the fix)
- it should be in english, not arabic
Change-Id: I7cf6d9db02ec0e79ad425bc0479c4fc43942fa52
Reviewed-on: https://gerrit.instructure.com/24348
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Liz Abinante <labinante@instructure.com>
QA-Review: Bryan Madsen <bryan@instructure.com>
Product-Review: Simon Williams <simon@instructure.com>
2013-09-14 03:49:09 +08:00
|
|
|
ary << [locale, best_range.last, ranges.index(best_range)] unless best_range.last == 0
|
2011-07-13 04:31:40 +08:00
|
|
|
end
|
|
|
|
ary
|
tweak tie-breaking logic for Accept-Language header
previously in the case of ties in the quality value we sorted by length (so en
beats en-US), and then alphabetically (so ar beats en). this part of the spec
is somewhat ambiguous, but this ordering does not take into account the order
passed into the accept header. so passing "en,ar" would have chosen 'ar'
because quality and length both tie, and 'ar' is first alphabetically.
to fix this we are incorporating the order of the types passed into the accept
header as one of the tie breaking criteria.
this fixes the bug with CutyCapt snapshots of canvas being rendered in arabic
because CutyCapt passes "en,*" as it's accept header. up until recently, 'en'
happened to be alphabetically first in our list of supported languages, so
everything worked out. but when we added support for arabic, a new
alphabetical winner emerged, exposing this bug.
fixes CNVS-3070
test plan:
- in an environment with CutyCapt enabled
- create a web submission assignment that allows url submissions
- submit a canvas url that has this fix applied (note that you can run this
from anywhere, it's the url you submit that needs the fix)
- it should be in english, not arabic
Change-Id: I7cf6d9db02ec0e79ad425bc0479c4fc43942fa52
Reviewed-on: https://gerrit.instructure.com/24348
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Liz Abinante <labinante@instructure.com>
QA-Review: Bryan Madsen <bryan@instructure.com>
Product-Review: Simon Williams <simon@instructure.com>
2013-09-14 03:49:09 +08:00
|
|
|
}.sort_by{ |l, q, pos| [-q, pos, l.count('-'), l]}
|
2011-07-13 04:31:40 +08:00
|
|
|
# wrt the sorting here, rfc2616 doesn't specify which tag is preferable
|
|
|
|
# if there is a quality tie (due to prefix matching or otherwise).
|
tweak tie-breaking logic for Accept-Language header
previously in the case of ties in the quality value we sorted by length (so en
beats en-US), and then alphabetically (so ar beats en). this part of the spec
is somewhat ambiguous, but this ordering does not take into account the order
passed into the accept header. so passing "en,ar" would have chosen 'ar'
because quality and length both tie, and 'ar' is first alphabetically.
to fix this we are incorporating the order of the types passed into the accept
header as one of the tie breaking criteria.
this fixes the bug with CutyCapt snapshots of canvas being rendered in arabic
because CutyCapt passes "en,*" as it's accept header. up until recently, 'en'
happened to be alphabetically first in our list of supported languages, so
everything worked out. but when we added support for arabic, a new
alphabetical winner emerged, exposing this bug.
fixes CNVS-3070
test plan:
- in an environment with CutyCapt enabled
- create a web submission assignment that allows url submissions
- submit a canvas url that has this fix applied (note that you can run this
from anywhere, it's the url you submit that needs the fix)
- it should be in english, not arabic
Change-Id: I7cf6d9db02ec0e79ad425bc0479c4fc43942fa52
Reviewed-on: https://gerrit.instructure.com/24348
Reviewed-by: Jon Jensen <jon@instructure.com>
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Liz Abinante <labinante@instructure.com>
QA-Review: Bryan Madsen <bryan@instructure.com>
Product-Review: Simon Williams <simon@instructure.com>
2013-09-14 03:49:09 +08:00
|
|
|
# technically they are equally acceptable. we've decided to break ties
|
|
|
|
# with:
|
|
|
|
# * position listed in header (tie here comes from '*')
|
|
|
|
# * length of locale (shorter first)
|
|
|
|
# * alphabetical
|
|
|
|
#
|
|
|
|
# this seems reasonable for scenarios like the following:
|
2011-07-13 04:31:40 +08:00
|
|
|
# given that i accept 'en'
|
|
|
|
# and canvas is localized in 'en-US', 'en-GB-oy' and 'en-CA-eh'
|
|
|
|
# then i should get 'en-US'
|
|
|
|
|
2017-11-28 02:26:46 +08:00
|
|
|
result = best_locales.first&.first
|
|
|
|
|
|
|
|
# translate back to an actual locale, if it happened to be an alias
|
|
|
|
result = locales_with_aliases[result] if locales_with_aliases[result]
|
|
|
|
result
|
2011-07-13 04:31:40 +08:00
|
|
|
end
|
|
|
|
|
2013-05-30 02:54:10 +08:00
|
|
|
# gives you a hash of localized locales, e.g. {"en" => "English", "es" => "Español" }
|
2011-07-13 04:31:40 +08:00
|
|
|
# if the locale name is not yet translated, it won't be included (even if
|
|
|
|
# there are other translations for that locale)
|
|
|
|
def available_locales
|
2016-03-18 04:22:20 +08:00
|
|
|
result = {}
|
|
|
|
settings = Canvas::Plugin.find(:i18n).settings || {}
|
|
|
|
enabled_custom_locales = settings.select { |locale, enabled| enabled }.map(&:first).map(&:to_sym)
|
|
|
|
I18n.available_locales.each do |locale|
|
2014-10-15 04:30:59 +08:00
|
|
|
name = I18n.send(:t, :locales, :locale => locale)[locale]
|
2016-03-18 04:22:20 +08:00
|
|
|
custom = I18n.send(:t, :custom, locale: locale) == true
|
|
|
|
next if custom && !enabled_custom_locales.include?(locale)
|
|
|
|
result[locale.to_s] = name if name
|
2011-07-13 04:31:40 +08:00
|
|
|
end
|
2016-03-18 04:22:20 +08:00
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.custom_locales
|
|
|
|
@custom_locales ||= I18n.available_locales.select{ |locale| I18n.send(:t, :custom, :locale => locale) == true }.sort
|
2011-07-13 04:31:40 +08:00
|
|
|
end
|
2013-03-27 22:57:39 +08:00
|
|
|
|
|
|
|
def crowdsourced_locales
|
2014-10-15 04:30:59 +08:00
|
|
|
@crowdsourced_locales ||= I18n.available_locales.select{ |locale| I18n.send(:t, :crowdsourced, :locale => locale) == true }
|
2013-03-27 22:57:39 +08:00
|
|
|
end
|
2011-07-13 04:31:40 +08:00
|
|
|
|
2017-11-28 02:26:46 +08:00
|
|
|
def self.locales_with_aliases
|
|
|
|
@locales_with_aliases ||= begin
|
|
|
|
locales = I18n.available_locales.map { |l| [l.to_s, nil] }.to_h
|
|
|
|
locales.keys.each do |locale|
|
|
|
|
aliases = Array.wrap(I18n.send(:t, :aliases, locale: locale, default: nil))
|
|
|
|
aliases.each do |a|
|
|
|
|
locales[a] = locale
|
|
|
|
end
|
|
|
|
end
|
|
|
|
locales
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|