i18n 1.6.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 +20 -0
- data/README.md +125 -0
- data/lib/i18n.rb +398 -0
- data/lib/i18n/backend.rb +21 -0
- data/lib/i18n/backend/base.rb +284 -0
- data/lib/i18n/backend/cache.rb +113 -0
- data/lib/i18n/backend/cache_file.rb +36 -0
- data/lib/i18n/backend/cascade.rb +56 -0
- data/lib/i18n/backend/chain.rb +127 -0
- data/lib/i18n/backend/fallbacks.rb +84 -0
- data/lib/i18n/backend/flatten.rb +115 -0
- data/lib/i18n/backend/gettext.rb +85 -0
- data/lib/i18n/backend/interpolation_compiler.rb +123 -0
- data/lib/i18n/backend/key_value.rb +206 -0
- data/lib/i18n/backend/memoize.rb +54 -0
- data/lib/i18n/backend/metadata.rb +71 -0
- data/lib/i18n/backend/pluralization.rb +55 -0
- data/lib/i18n/backend/simple.rb +111 -0
- data/lib/i18n/backend/transliterator.rb +108 -0
- data/lib/i18n/config.rb +165 -0
- data/lib/i18n/core_ext/hash.rb +47 -0
- data/lib/i18n/exceptions.rb +111 -0
- data/lib/i18n/gettext.rb +28 -0
- data/lib/i18n/gettext/helpers.rb +75 -0
- data/lib/i18n/gettext/po_parser.rb +329 -0
- data/lib/i18n/interpolate/ruby.rb +39 -0
- data/lib/i18n/locale.rb +8 -0
- data/lib/i18n/locale/fallbacks.rb +96 -0
- data/lib/i18n/locale/tag.rb +28 -0
- data/lib/i18n/locale/tag/parents.rb +22 -0
- data/lib/i18n/locale/tag/rfc4646.rb +74 -0
- data/lib/i18n/locale/tag/simple.rb +39 -0
- data/lib/i18n/middleware.rb +17 -0
- data/lib/i18n/tests.rb +14 -0
- data/lib/i18n/tests/basics.rb +60 -0
- data/lib/i18n/tests/defaults.rb +52 -0
- data/lib/i18n/tests/interpolation.rb +159 -0
- data/lib/i18n/tests/link.rb +56 -0
- data/lib/i18n/tests/localization.rb +19 -0
- data/lib/i18n/tests/localization/date.rb +117 -0
- data/lib/i18n/tests/localization/date_time.rb +103 -0
- data/lib/i18n/tests/localization/procs.rb +116 -0
- data/lib/i18n/tests/localization/time.rb +103 -0
- data/lib/i18n/tests/lookup.rb +81 -0
- data/lib/i18n/tests/pluralization.rb +35 -0
- data/lib/i18n/tests/procs.rb +55 -0
- data/lib/i18n/version.rb +5 -0
- metadata +124 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# heavily based on Masao Mutoh's gettext String interpolation extension
|
2
|
+
# http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
|
3
|
+
|
4
|
+
module I18n
|
5
|
+
DEFAULT_INTERPOLATION_PATTERNS = [
|
6
|
+
/%%/,
|
7
|
+
/%\{(\w+)\}/, # matches placeholders like "%{foo}"
|
8
|
+
/%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%<foo>.d"
|
9
|
+
].freeze
|
10
|
+
INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
|
11
|
+
deprecate_constant :INTERPOLATION_PATTERN if method_defined? :INTERPOLATION_PATTERN
|
12
|
+
|
13
|
+
class << self
|
14
|
+
# Return String or raises MissingInterpolationArgument exception.
|
15
|
+
# Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
|
16
|
+
def interpolate(string, values)
|
17
|
+
raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ RESERVED_KEYS_PATTERN
|
18
|
+
raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash)
|
19
|
+
interpolate_hash(string, values)
|
20
|
+
end
|
21
|
+
|
22
|
+
def interpolate_hash(string, values)
|
23
|
+
string.gsub(Regexp.union(config.interpolation_patterns)) do |match|
|
24
|
+
if match == '%%'
|
25
|
+
'%'
|
26
|
+
else
|
27
|
+
key = ($1 || $2 || match.tr("%{}", "")).to_sym
|
28
|
+
value = if values.key?(key)
|
29
|
+
values[key]
|
30
|
+
else
|
31
|
+
config.missing_interpolation_argument_handler.call(key, values, string)
|
32
|
+
end
|
33
|
+
value = value.call(values) if value.respond_to?(:call)
|
34
|
+
$3 ? sprintf("%#{$3}", value) : value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/i18n/locale.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
# Locale Fallbacks
|
2
|
+
#
|
3
|
+
# Extends the I18n module to hold a fallbacks instance which is set to an
|
4
|
+
# instance of I18n::Locale::Fallbacks by default but can be swapped with a
|
5
|
+
# different implementation.
|
6
|
+
#
|
7
|
+
# Locale fallbacks will compute a number of fallback locales for a given locale.
|
8
|
+
# For example:
|
9
|
+
#
|
10
|
+
# <pre><code>
|
11
|
+
# I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :en] </code></pre>
|
12
|
+
#
|
13
|
+
# Locale fallbacks always fall back to
|
14
|
+
#
|
15
|
+
# * all parent locales of a given locale (e.g. :es for :"es-MX") first,
|
16
|
+
# * the current default locales and all of their parents second
|
17
|
+
#
|
18
|
+
# The default locales are set to [I18n.default_locale] by default but can be
|
19
|
+
# set to something else.
|
20
|
+
#
|
21
|
+
# One can additionally add any number of additional fallback locales manually.
|
22
|
+
# These will be added before the default locales to the fallback chain. For
|
23
|
+
# example:
|
24
|
+
#
|
25
|
+
# # using the default locale as default fallback locale
|
26
|
+
#
|
27
|
+
# I18n.default_locale = :"en-US"
|
28
|
+
# I18n.fallbacks = I18n::Locale::Fallbacks.new(:"de-AT" => :"de-DE")
|
29
|
+
# I18n.fallbacks[:"de-AT"] # => [:"de-AT", :"de-DE", :de, :"en-US", :en]
|
30
|
+
#
|
31
|
+
# # using a custom locale as default fallback locale
|
32
|
+
#
|
33
|
+
# I18n.fallbacks = I18n::Locale::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de)
|
34
|
+
# I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"en-GB", :en]
|
35
|
+
# I18n.fallbacks[:"de-CH"] # => [:"de-CH", :de, :"en-GB", :en]
|
36
|
+
#
|
37
|
+
# # mapping fallbacks to an existing instance
|
38
|
+
#
|
39
|
+
# # people speaking Catalan also speak Spanish as spoken in Spain
|
40
|
+
# fallbacks = I18n.fallbacks
|
41
|
+
# fallbacks.map(:ca => :"es-ES")
|
42
|
+
# fallbacks[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en]
|
43
|
+
#
|
44
|
+
# # people speaking Arabian as spoken in Palestine also speak Hebrew as spoken in Israel
|
45
|
+
# fallbacks.map(:"ar-PS" => :"he-IL")
|
46
|
+
# fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en]
|
47
|
+
# fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en]
|
48
|
+
#
|
49
|
+
# # people speaking Sami as spoken in Finnland also speak Swedish and Finnish as spoken in Finnland
|
50
|
+
# fallbacks.map(:sms => [:"se-FI", :"fi-FI"])
|
51
|
+
# fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en]
|
52
|
+
|
53
|
+
module I18n
|
54
|
+
module Locale
|
55
|
+
class Fallbacks < Hash
|
56
|
+
def initialize(*mappings)
|
57
|
+
@map = {}
|
58
|
+
map(mappings.pop) if mappings.last.is_a?(Hash)
|
59
|
+
self.defaults = mappings.empty? ? [] : mappings
|
60
|
+
end
|
61
|
+
|
62
|
+
def defaults=(defaults)
|
63
|
+
@defaults = defaults.map { |default| compute(default, false) }.flatten
|
64
|
+
end
|
65
|
+
attr_reader :defaults
|
66
|
+
|
67
|
+
def [](locale)
|
68
|
+
raise InvalidLocale.new(locale) if locale.nil?
|
69
|
+
locale = locale.to_sym
|
70
|
+
super || store(locale, compute(locale))
|
71
|
+
end
|
72
|
+
|
73
|
+
def map(mappings)
|
74
|
+
mappings.each do |from, to|
|
75
|
+
from, to = from.to_sym, Array(to)
|
76
|
+
to.each do |_to|
|
77
|
+
@map[from] ||= []
|
78
|
+
@map[from] << _to.to_sym
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def compute(tags, include_defaults = true, exclude = [])
|
86
|
+
result = Array(tags).collect do |tag|
|
87
|
+
tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude
|
88
|
+
tags.each { |_tag| tags += compute(@map[_tag], false, exclude + tags) if @map[_tag] }
|
89
|
+
tags
|
90
|
+
end.flatten
|
91
|
+
result.push(*defaults) if include_defaults
|
92
|
+
result.uniq.compact
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Locale
|
5
|
+
module Tag
|
6
|
+
autoload :Parents, 'i18n/locale/tag/parents'
|
7
|
+
autoload :Rfc4646, 'i18n/locale/tag/rfc4646'
|
8
|
+
autoload :Simple, 'i18n/locale/tag/simple'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Returns the current locale tag implementation. Defaults to +I18n::Locale::Tag::Simple+.
|
12
|
+
def implementation
|
13
|
+
@@implementation ||= Simple
|
14
|
+
end
|
15
|
+
|
16
|
+
# Sets the current locale tag implementation. Use this to set a different locale tag implementation.
|
17
|
+
def implementation=(implementation)
|
18
|
+
@@implementation = implementation
|
19
|
+
end
|
20
|
+
|
21
|
+
# Factory method for locale tags. Delegates to the current locale tag implementation.
|
22
|
+
def tag(tag)
|
23
|
+
implementation.tag(tag)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module I18n
|
2
|
+
module Locale
|
3
|
+
module Tag
|
4
|
+
module Parents
|
5
|
+
def parent
|
6
|
+
@parent ||= begin
|
7
|
+
segs = to_a.compact
|
8
|
+
segs.length > 1 ? self.class.tag(*segs[0..(segs.length-2)].join('-')) : nil
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self_and_parents
|
13
|
+
@self_and_parents ||= [self] + parents
|
14
|
+
end
|
15
|
+
|
16
|
+
def parents
|
17
|
+
@parents ||= ([parent] + (parent ? parent.parents : [])).compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# RFC 4646/47 compliant Locale tag implementation that parses locale tags to
|
2
|
+
# subtags such as language, script, region, variant etc.
|
3
|
+
#
|
4
|
+
# For more information see by http://en.wikipedia.org/wiki/IETF_language_tag
|
5
|
+
#
|
6
|
+
# Rfc4646::Parser does not implement grandfathered tags.
|
7
|
+
|
8
|
+
module I18n
|
9
|
+
module Locale
|
10
|
+
module Tag
|
11
|
+
RFC4646_SUBTAGS = [ :language, :script, :region, :variant, :extension, :privateuse, :grandfathered ]
|
12
|
+
RFC4646_FORMATS = { :language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase }
|
13
|
+
|
14
|
+
class Rfc4646 < Struct.new(*RFC4646_SUBTAGS)
|
15
|
+
class << self
|
16
|
+
# Parses the given tag and returns a Tag instance if it is valid.
|
17
|
+
# Returns false if the given tag is not valid according to RFC 4646.
|
18
|
+
def tag(tag)
|
19
|
+
matches = parser.match(tag)
|
20
|
+
new(*matches) if matches
|
21
|
+
end
|
22
|
+
|
23
|
+
def parser
|
24
|
+
@@parser ||= Rfc4646::Parser
|
25
|
+
end
|
26
|
+
|
27
|
+
def parser=(parser)
|
28
|
+
@@parser = parser
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
include Parents
|
33
|
+
|
34
|
+
RFC4646_FORMATS.each do |name, format|
|
35
|
+
define_method(name) { self[name].send(format) unless self[name].nil? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_sym
|
39
|
+
to_s.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
@tag ||= to_a.compact.join("-")
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_a
|
47
|
+
members.collect { |attr| self.send(attr) }
|
48
|
+
end
|
49
|
+
|
50
|
+
module Parser
|
51
|
+
PATTERN = %r{\A(?:
|
52
|
+
([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language
|
53
|
+
(?:-([a-z]{4}))? # script
|
54
|
+
(?:-([a-z]{2}|\d{3}))? # region
|
55
|
+
(?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant
|
56
|
+
(?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension
|
57
|
+
(?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag
|
58
|
+
(x(?:-[0-9a-z]{1,8})+)| # privateuse tag
|
59
|
+
/* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered
|
60
|
+
)\z}xi
|
61
|
+
|
62
|
+
class << self
|
63
|
+
def match(tag)
|
64
|
+
c = PATTERN.match(tag.to_s).captures
|
65
|
+
c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here?
|
66
|
+
rescue
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Simple Locale tag implementation that computes subtags by simply splitting
|
2
|
+
# the locale tag at '-' occurences.
|
3
|
+
module I18n
|
4
|
+
module Locale
|
5
|
+
module Tag
|
6
|
+
class Simple
|
7
|
+
class << self
|
8
|
+
def tag(tag)
|
9
|
+
new(tag)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
include Parents
|
14
|
+
|
15
|
+
attr_reader :tag
|
16
|
+
|
17
|
+
def initialize(*tag)
|
18
|
+
@tag = tag.join('-').to_sym
|
19
|
+
end
|
20
|
+
|
21
|
+
def subtags
|
22
|
+
@subtags = tag.to_s.split('-').map { |subtag| subtag.to_s }
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_sym
|
26
|
+
tag
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
tag.to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_a
|
34
|
+
subtags
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/i18n/tests.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Tests
|
5
|
+
autoload :Basics, 'i18n/tests/basics'
|
6
|
+
autoload :Defaults, 'i18n/tests/defaults'
|
7
|
+
autoload :Interpolation, 'i18n/tests/interpolation'
|
8
|
+
autoload :Link, 'i18n/tests/link'
|
9
|
+
autoload :Localization, 'i18n/tests/localization'
|
10
|
+
autoload :Lookup, 'i18n/tests/lookup'
|
11
|
+
autoload :Pluralization, 'i18n/tests/pluralization'
|
12
|
+
autoload :Procs, 'i18n/tests/procs'
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module I18n
|
2
|
+
module Tests
|
3
|
+
module Basics
|
4
|
+
def teardown
|
5
|
+
I18n.available_locales = nil
|
6
|
+
end
|
7
|
+
|
8
|
+
test "available_locales returns the locales stored to the backend by default" do
|
9
|
+
I18n.backend.store_translations('de', :foo => 'bar')
|
10
|
+
I18n.backend.store_translations('en', :foo => 'foo')
|
11
|
+
|
12
|
+
assert I18n.available_locales.include?(:de)
|
13
|
+
assert I18n.available_locales.include?(:en)
|
14
|
+
end
|
15
|
+
|
16
|
+
test "available_locales can be set to something else independently from the actual locale data" do
|
17
|
+
I18n.backend.store_translations('de', :foo => 'bar')
|
18
|
+
I18n.backend.store_translations('en', :foo => 'foo')
|
19
|
+
|
20
|
+
I18n.available_locales = :foo
|
21
|
+
assert_equal [:foo], I18n.available_locales
|
22
|
+
|
23
|
+
I18n.available_locales = [:foo, 'bar']
|
24
|
+
assert_equal [:foo, :bar], I18n.available_locales
|
25
|
+
|
26
|
+
I18n.available_locales = nil
|
27
|
+
assert I18n.available_locales.include?(:de)
|
28
|
+
assert I18n.available_locales.include?(:en)
|
29
|
+
end
|
30
|
+
|
31
|
+
test "available_locales memoizes when set explicitely" do
|
32
|
+
I18n.backend.expects(:available_locales).never
|
33
|
+
I18n.available_locales = [:foo]
|
34
|
+
I18n.backend.store_translations('de', :bar => 'baz')
|
35
|
+
I18n.reload!
|
36
|
+
assert_equal [:foo], I18n.available_locales
|
37
|
+
end
|
38
|
+
|
39
|
+
test "available_locales delegates to the backend when not set explicitely" do
|
40
|
+
original_available_locales_value = I18n.backend.available_locales
|
41
|
+
I18n.backend.expects(:available_locales).returns(original_available_locales_value).twice
|
42
|
+
assert_equal I18n.backend.available_locales, I18n.available_locales
|
43
|
+
end
|
44
|
+
|
45
|
+
test "exists? is implemented by the backend" do
|
46
|
+
I18n.backend.store_translations(:foo, :bar => 'baz')
|
47
|
+
assert I18n.exists?(:bar, :foo)
|
48
|
+
end
|
49
|
+
|
50
|
+
test "storing a nil value as a translation removes it from the available locale data" do
|
51
|
+
I18n.backend.store_translations(:en, :to_be_deleted => 'bar')
|
52
|
+
assert_equal 'bar', I18n.t(:to_be_deleted, :default => 'baz')
|
53
|
+
|
54
|
+
I18n.cache_store.clear if I18n.respond_to?(:cache_store) && I18n.cache_store
|
55
|
+
I18n.backend.store_translations(:en, :to_be_deleted => nil)
|
56
|
+
assert_equal 'baz', I18n.t(:to_be_deleted, :default => 'baz')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Tests
|
5
|
+
module Defaults
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' })
|
9
|
+
end
|
10
|
+
|
11
|
+
test "defaults: given nil as a key it returns the given default" do
|
12
|
+
assert_equal 'default', I18n.t(nil, :default => 'default')
|
13
|
+
end
|
14
|
+
|
15
|
+
test "defaults: given a symbol as a default it translates the symbol" do
|
16
|
+
assert_equal 'bar', I18n.t(nil, :default => :'foo.bar')
|
17
|
+
end
|
18
|
+
|
19
|
+
test "defaults: given a symbol as a default and a scope it stays inside the scope when looking up the symbol" do
|
20
|
+
assert_equal 'bar', I18n.t(:missing, :default => :bar, :scope => :foo)
|
21
|
+
end
|
22
|
+
|
23
|
+
test "defaults: given an array as a default it returns the first match" do
|
24
|
+
assert_equal 'bar', I18n.t(:does_not_exist, :default => [:does_not_exist_2, :'foo.bar'])
|
25
|
+
end
|
26
|
+
|
27
|
+
test "defaults: given an array as a default with false it returns false" do
|
28
|
+
assert_equal false, I18n.t(:does_not_exist, :default => [false])
|
29
|
+
end
|
30
|
+
|
31
|
+
test "defaults: given false it returns false" do
|
32
|
+
assert_equal false, I18n.t(:does_not_exist, :default => false)
|
33
|
+
end
|
34
|
+
|
35
|
+
test "defaults: given nil it returns nil" do
|
36
|
+
assert_nil I18n.t(:does_not_exist, :default => nil)
|
37
|
+
end
|
38
|
+
|
39
|
+
test "defaults: given an array of missing keys it raises a MissingTranslationData exception" do
|
40
|
+
assert_raise I18n::MissingTranslationData do
|
41
|
+
I18n.t(:does_not_exist, :default => [:does_not_exist_2, :does_not_exist_3], :raise => true)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
test "defaults: using a custom scope separator" do
|
46
|
+
# data must have been stored using the custom separator when using the ActiveRecord backend
|
47
|
+
I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' })
|
48
|
+
assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|