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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +125 -0
  4. data/lib/i18n.rb +398 -0
  5. data/lib/i18n/backend.rb +21 -0
  6. data/lib/i18n/backend/base.rb +284 -0
  7. data/lib/i18n/backend/cache.rb +113 -0
  8. data/lib/i18n/backend/cache_file.rb +36 -0
  9. data/lib/i18n/backend/cascade.rb +56 -0
  10. data/lib/i18n/backend/chain.rb +127 -0
  11. data/lib/i18n/backend/fallbacks.rb +84 -0
  12. data/lib/i18n/backend/flatten.rb +115 -0
  13. data/lib/i18n/backend/gettext.rb +85 -0
  14. data/lib/i18n/backend/interpolation_compiler.rb +123 -0
  15. data/lib/i18n/backend/key_value.rb +206 -0
  16. data/lib/i18n/backend/memoize.rb +54 -0
  17. data/lib/i18n/backend/metadata.rb +71 -0
  18. data/lib/i18n/backend/pluralization.rb +55 -0
  19. data/lib/i18n/backend/simple.rb +111 -0
  20. data/lib/i18n/backend/transliterator.rb +108 -0
  21. data/lib/i18n/config.rb +165 -0
  22. data/lib/i18n/core_ext/hash.rb +47 -0
  23. data/lib/i18n/exceptions.rb +111 -0
  24. data/lib/i18n/gettext.rb +28 -0
  25. data/lib/i18n/gettext/helpers.rb +75 -0
  26. data/lib/i18n/gettext/po_parser.rb +329 -0
  27. data/lib/i18n/interpolate/ruby.rb +39 -0
  28. data/lib/i18n/locale.rb +8 -0
  29. data/lib/i18n/locale/fallbacks.rb +96 -0
  30. data/lib/i18n/locale/tag.rb +28 -0
  31. data/lib/i18n/locale/tag/parents.rb +22 -0
  32. data/lib/i18n/locale/tag/rfc4646.rb +74 -0
  33. data/lib/i18n/locale/tag/simple.rb +39 -0
  34. data/lib/i18n/middleware.rb +17 -0
  35. data/lib/i18n/tests.rb +14 -0
  36. data/lib/i18n/tests/basics.rb +60 -0
  37. data/lib/i18n/tests/defaults.rb +52 -0
  38. data/lib/i18n/tests/interpolation.rb +159 -0
  39. data/lib/i18n/tests/link.rb +56 -0
  40. data/lib/i18n/tests/localization.rb +19 -0
  41. data/lib/i18n/tests/localization/date.rb +117 -0
  42. data/lib/i18n/tests/localization/date_time.rb +103 -0
  43. data/lib/i18n/tests/localization/procs.rb +116 -0
  44. data/lib/i18n/tests/localization/time.rb +103 -0
  45. data/lib/i18n/tests/lookup.rb +81 -0
  46. data/lib/i18n/tests/pluralization.rb +35 -0
  47. data/lib/i18n/tests/procs.rb +55 -0
  48. data/lib/i18n/version.rb +5 -0
  49. 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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Locale
5
+ autoload :Fallbacks, 'i18n/locale/fallbacks'
6
+ autoload :Tag, 'i18n/locale/tag'
7
+ end
8
+ end
@@ -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
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ class Middleware
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ @app.call(env)
12
+ ensure
13
+ Thread.current[:i18n_config] = I18n::Config.new
14
+ end
15
+
16
+ end
17
+ end
@@ -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