i18n 1.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +122 -0
  4. data/lib/i18n.rb +414 -0
  5. data/lib/i18n/backend.rb +21 -0
  6. data/lib/i18n/backend/base.rb +285 -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 +130 -0
  11. data/lib/i18n/backend/fallbacks.rb +95 -0
  12. data/lib/i18n/backend/flatten.rb +118 -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 +109 -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 +59 -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 +99 -0
  30. data/lib/i18n/locale/tag.rb +28 -0
  31. data/lib/i18n/locale/tag/parents.rb +24 -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 +163 -0
  39. data/lib/i18n/tests/link.rb +66 -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 +117 -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 +60 -0
  48. data/lib/i18n/version.rb +5 -0
  49. metadata +128 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Cascade module adds the ability to do cascading lookups to backends that
4
+ # are compatible to the Simple backend.
5
+ #
6
+ # By cascading lookups we mean that for any key that can not be found the
7
+ # Cascade module strips one segment off the scope part of the key and then
8
+ # tries to look up the key in that scope.
9
+ #
10
+ # E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
11
+ # the segment :bar will be stripped off the scope part :"foo.bar" and the new
12
+ # scope :foo will be used to look up the key :baz. If that does not succeed
13
+ # then the remaining scope segment :foo will be omitted, too, and again the
14
+ # key :baz will be looked up (now with no scope).
15
+ #
16
+ # To enable a cascading lookup one passes the :cascade option:
17
+ #
18
+ # I18n.t(:'foo.bar.baz', :cascade => true)
19
+ #
20
+ # This will return the first translation found for :"foo.bar.baz", :"foo.baz"
21
+ # or :baz in this order.
22
+ #
23
+ # The cascading lookup takes precedence over resolving any given defaults.
24
+ # I.e. defaults will kick in after the cascading lookups haven't succeeded.
25
+ #
26
+ # This behavior is useful for libraries like ActiveRecord validations where
27
+ # the library wants to give users a bunch of more or less fine-grained options
28
+ # of scopes for a particular key.
29
+ #
30
+ # Thanks to Clemens Kofler for the initial idea and implementation! See
31
+ # http://github.com/clemens/i18n-cascading-backend
32
+
33
+ module I18n
34
+ module Backend
35
+ module Cascade
36
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
37
+ return super unless cascade = options[:cascade]
38
+
39
+ cascade = { :step => 1 } unless cascade.is_a?(Hash)
40
+ step = cascade[:step] || 1
41
+ offset = cascade[:offset] || 1
42
+ separator = options[:separator] || I18n.default_separator
43
+ skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
44
+
45
+ scope = I18n.normalize_keys(nil, key, scope, separator)
46
+ key = (scope.slice!(-offset, offset) || []).join(separator)
47
+
48
+ begin
49
+ result = super
50
+ return result unless result.nil?
51
+ scope = scope.dup
52
+ end while (!scope.empty? || !skip_root) && scope.slice!(-step, step)
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Backend
5
+ # Backend that chains multiple other backends and checks each of them when
6
+ # a translation needs to be looked up. This is useful when you want to use
7
+ # standard translations with a Simple backend but store custom application
8
+ # translations in a database or other backends.
9
+ #
10
+ # To use the Chain backend instantiate it and set it to the I18n module.
11
+ # You can add chained backends through the initializer or backends
12
+ # accessor:
13
+ #
14
+ # # preserves the existing Simple backend set to I18n.backend
15
+ # I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
16
+ #
17
+ # The implementation assumes that all backends added to the Chain implement
18
+ # a lookup method with the same API as Simple backend does.
19
+ class Chain
20
+ using I18n::HashRefinements
21
+
22
+ module Implementation
23
+ include Base
24
+
25
+ attr_accessor :backends
26
+
27
+ def initialize(*backends)
28
+ self.backends = backends
29
+ end
30
+
31
+ def initialized?
32
+ backends.all? do |backend|
33
+ backend.instance_eval do
34
+ return false unless initialized?
35
+ end
36
+ end
37
+ true
38
+ end
39
+
40
+ def reload!
41
+ backends.each { |backend| backend.reload! }
42
+ end
43
+
44
+ def eager_load!
45
+ backends.each { |backend| backend.eager_load! }
46
+ end
47
+
48
+ def store_translations(locale, data, options = EMPTY_HASH)
49
+ backends.first.store_translations(locale, data, options)
50
+ end
51
+
52
+ def available_locales
53
+ backends.map { |backend| backend.available_locales }.flatten.uniq
54
+ end
55
+
56
+ def translate(locale, key, default_options = EMPTY_HASH)
57
+ namespace = nil
58
+ options = default_options.except(:default)
59
+
60
+ backends.each do |backend|
61
+ catch(:exception) do
62
+ options = default_options if backend == backends.last
63
+ translation = backend.translate(locale, key, options)
64
+ if namespace_lookup?(translation, options)
65
+ namespace = _deep_merge(translation, namespace || {})
66
+ elsif !translation.nil? || (options.key?(:default) && options[:default].nil?)
67
+ return translation
68
+ end
69
+ end
70
+ end
71
+
72
+ return namespace if namespace
73
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
74
+ end
75
+
76
+ def exists?(locale, key, options = EMPTY_HASH)
77
+ backends.any? do |backend|
78
+ backend.exists?(locale, key, options)
79
+ end
80
+ end
81
+
82
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
83
+ backends.each do |backend|
84
+ catch(:exception) do
85
+ result = backend.localize(locale, object, format, options) and return result
86
+ end
87
+ end
88
+ throw(:exception, I18n::MissingTranslation.new(locale, format, options))
89
+ end
90
+
91
+ protected
92
+ def init_translations
93
+ backends.each do |backend|
94
+ backend.send(:init_translations)
95
+ end
96
+ end
97
+
98
+ def translations
99
+ backends.reverse.each_with_object({}) do |backend, memo|
100
+ partial_translations = backend.instance_eval do
101
+ init_translations unless initialized?
102
+ translations
103
+ end
104
+ memo.deep_merge!(partial_translations) { |_, a, b| b || a }
105
+ end
106
+ end
107
+
108
+ def namespace_lookup?(result, options)
109
+ result.is_a?(Hash) && !options.has_key?(:count)
110
+ end
111
+
112
+ private
113
+ # This is approximately what gets used in ActiveSupport.
114
+ # However since we are not guaranteed to run in an ActiveSupport context
115
+ # it is wise to have our own copy. We underscore it
116
+ # to not pollute the namespace of the including class.
117
+ def _deep_merge(hash, other_hash)
118
+ copy = hash.dup
119
+ other_hash.each_pair do |k,v|
120
+ value_from_other = hash[k]
121
+ copy[k] = value_from_other.is_a?(Hash) && v.is_a?(Hash) ? _deep_merge(value_from_other, v) : v
122
+ end
123
+ copy
124
+ end
125
+ end
126
+
127
+ include Implementation
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # I18n locale fallbacks are useful when you want your application to use
4
+ # translations from other locales when translations for the current locale are
5
+ # missing. E.g. you might want to use :en translations when translations in
6
+ # your applications main locale :de are missing.
7
+ #
8
+ # To enable locale fallbacks you can simply include the Fallbacks module to
9
+ # the Simple backend - or whatever other backend you are using:
10
+ #
11
+ # I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
12
+ module I18n
13
+ @@fallbacks = nil
14
+
15
+ class << self
16
+ # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
17
+ def fallbacks
18
+ @@fallbacks ||= I18n::Locale::Fallbacks.new
19
+ end
20
+
21
+ # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
22
+ def fallbacks=(fallbacks)
23
+ @@fallbacks = fallbacks
24
+ end
25
+ end
26
+
27
+ module Backend
28
+ module Fallbacks
29
+ # Overwrites the Base backend translate method so that it will try each
30
+ # locale given by I18n.fallbacks for the given locale. E.g. for the
31
+ # locale :"de-DE" it might try the locales :"de-DE", :de and :en
32
+ # (depends on the fallbacks implementation) until it finds a result with
33
+ # the given options. If it does not find any result for any of the
34
+ # locales it will then throw MissingTranslation as usual.
35
+ #
36
+ # The default option takes precedence over fallback locales only when
37
+ # it's a Symbol. When the default contains a String, Proc or Hash
38
+ # it is evaluated last after all the fallback locales have been tried.
39
+ def translate(locale, key, options = EMPTY_HASH)
40
+ return super unless options.fetch(:fallback, true)
41
+ return super if options[:fallback_in_progress]
42
+ default = extract_non_symbol_default!(options) if options[:default]
43
+
44
+ fallback_options = options.merge(:fallback_in_progress => true)
45
+ I18n.fallbacks[locale].each do |fallback|
46
+ begin
47
+ catch(:exception) do
48
+ result = super(fallback, key, fallback_options)
49
+ unless result.nil?
50
+ on_fallback(locale, fallback, key, options) if locale != fallback
51
+ return result
52
+ end
53
+ end
54
+ rescue I18n::InvalidLocale
55
+ # we do nothing when the locale is invalid, as this is a fallback anyways.
56
+ end
57
+ end
58
+
59
+ return if options.key?(:default) && options[:default].nil?
60
+
61
+ return super(locale, nil, options.merge(:default => default)) if default
62
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
63
+ end
64
+
65
+ def extract_non_symbol_default!(options)
66
+ defaults = [options[:default]].flatten
67
+ first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
68
+ if first_non_symbol_default
69
+ options[:default] = defaults[0, defaults.index(first_non_symbol_default)]
70
+ end
71
+ return first_non_symbol_default
72
+ end
73
+
74
+ def exists?(locale, key, options = EMPTY_HASH)
75
+ return super unless options.fetch(:fallback, true)
76
+ I18n.fallbacks[locale].each do |fallback|
77
+ begin
78
+ return true if super(fallback, key)
79
+ rescue I18n::InvalidLocale
80
+ # we do nothing when the locale is invalid, as this is a fallback anyways.
81
+ end
82
+ end
83
+
84
+ false
85
+ end
86
+
87
+ private
88
+
89
+ # Overwrite on_fallback to add specified logic when the fallback succeeds.
90
+ def on_fallback(_original_locale, _fallback_locale, _key, _optoins)
91
+ nil
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Backend
5
+ # This module contains several helpers to assist flattening translations.
6
+ # You may want to flatten translations for:
7
+ #
8
+ # 1) speed up lookups, as in the Memoize backend;
9
+ # 2) In case you want to store translations in a data store, as in ActiveRecord backend;
10
+ #
11
+ # You can check both backends above for some examples.
12
+ # This module also keeps all links in a hash so they can be properly resolved when flattened.
13
+ module Flatten
14
+ SEPARATOR_ESCAPE_CHAR = "\001"
15
+ FLATTEN_SEPARATOR = "."
16
+
17
+ # normalize_keys the flatten way. This method is significantly faster
18
+ # and creates way less objects than the one at I18n.normalize_keys.
19
+ # It also handles escaping the translation keys.
20
+ def self.normalize_flat_keys(locale, key, scope, separator)
21
+ keys = [scope, key]
22
+ keys.flatten!
23
+ keys.compact!
24
+
25
+ separator ||= I18n.default_separator
26
+
27
+ if separator != FLATTEN_SEPARATOR
28
+ from_str = "#{FLATTEN_SEPARATOR}#{separator}"
29
+ to_str = "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}"
30
+
31
+ keys.map! { |k| k.to_s.tr from_str, to_str }
32
+ end
33
+
34
+ keys.join(".")
35
+ end
36
+
37
+ # Receives a string and escape the default separator.
38
+ def self.escape_default_separator(key) #:nodoc:
39
+ key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
40
+ end
41
+
42
+ # Shortcut to I18n::Backend::Flatten.normalize_flat_keys
43
+ # and then resolve_links.
44
+ def normalize_flat_keys(locale, key, scope, separator)
45
+ key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
46
+ resolve_link(locale, key)
47
+ end
48
+
49
+ # Store flattened links.
50
+ def links
51
+ @links ||= I18n.new_double_nested_cache
52
+ end
53
+
54
+ # Flatten keys for nested Hashes by chaining up keys:
55
+ #
56
+ # >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
57
+ # => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
58
+ #
59
+ def flatten_keys(hash, escape, prev_key=nil, &block)
60
+ hash.each_pair do |key, value|
61
+ key = escape_default_separator(key) if escape
62
+ curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
63
+ yield curr_key, value
64
+ flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
65
+ end
66
+ end
67
+
68
+ # Receives a hash of translations (where the key is a locale and
69
+ # the value is another hash) and return a hash with all
70
+ # translations flattened.
71
+ #
72
+ # Nested hashes are included in the flattened hash just if subtree
73
+ # is true and Symbols are automatically stored as links.
74
+ def flatten_translations(locale, data, escape, subtree)
75
+ hash = {}
76
+ flatten_keys(data, escape) do |key, value|
77
+ if value.is_a?(Hash)
78
+ hash[key] = value if subtree
79
+ else
80
+ store_link(locale, key, value) if value.is_a?(Symbol)
81
+ hash[key] = value
82
+ end
83
+ end
84
+ hash
85
+ end
86
+
87
+ protected
88
+
89
+ def store_link(locale, key, link)
90
+ links[locale.to_sym][key.to_s] = link.to_s
91
+ end
92
+
93
+ def resolve_link(locale, key)
94
+ key, locale = key.to_s, locale.to_sym
95
+ links = self.links[locale]
96
+
97
+ if links.key?(key)
98
+ links[key]
99
+ elsif link = find_link(locale, key)
100
+ store_link(locale, key, key.gsub(*link))
101
+ else
102
+ key
103
+ end
104
+ end
105
+
106
+ def find_link(locale, key) #:nodoc:
107
+ links[locale].each_pair do |from, to|
108
+ return [from, to] if key[0, from.length] == from
109
+ end && nil
110
+ end
111
+
112
+ def escape_default_separator(key) #:nodoc:
113
+ I18n::Backend::Flatten.escape_default_separator(key)
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/gettext'
4
+ require 'i18n/gettext/po_parser'
5
+
6
+ module I18n
7
+ module Backend
8
+ # Experimental support for using Gettext po files to store translations.
9
+ #
10
+ # To use this you can simply include the module to the Simple backend - or
11
+ # whatever other backend you are using.
12
+ #
13
+ # I18n::Backend::Simple.include(I18n::Backend::Gettext)
14
+ #
15
+ # Now you should be able to include your Gettext translation (*.po) files to
16
+ # the +I18n.load_path+ so they're loaded to the backend and you can use them as
17
+ # usual:
18
+ #
19
+ # I18n.load_path += Dir["path/to/locales/*.po"]
20
+ #
21
+ # Following the Gettext convention this implementation expects that your
22
+ # translation files are named by their locales. E.g. the file en.po would
23
+ # contain the translations for the English locale.
24
+ #
25
+ # To translate text <b>you must use</b> one of the translate methods provided by
26
+ # I18n::Gettext::Helpers.
27
+ #
28
+ # include I18n::Gettext::Helpers
29
+ # puts _("some string")
30
+ #
31
+ # Without it strings containing periods (".") will not be translated.
32
+
33
+ module Gettext
34
+ using I18n::HashRefinements
35
+
36
+ class PoData < Hash
37
+ def set_comment(msgid_or_sym, comment)
38
+ # ignore
39
+ end
40
+ end
41
+
42
+ protected
43
+ def load_po(filename)
44
+ locale = ::File.basename(filename, '.po').to_sym
45
+ data = normalize(locale, parse(filename))
46
+ { locale => data }
47
+ end
48
+
49
+ def parse(filename)
50
+ GetText::PoParser.new.parse(::File.read(filename), PoData.new)
51
+ end
52
+
53
+ def normalize(locale, data)
54
+ data.inject({}) do |result, (key, value)|
55
+ unless key.nil? || key.empty?
56
+ key = key.gsub(I18n::Gettext::CONTEXT_SEPARATOR, '|')
57
+ key, value = normalize_pluralization(locale, key, value) if key.index("\000")
58
+
59
+ parts = key.split('|').reverse
60
+ normalized = parts.inject({}) do |_normalized, part|
61
+ { part => _normalized.empty? ? value : _normalized }
62
+ end
63
+
64
+ result.deep_merge!(normalized)
65
+ end
66
+ result
67
+ end
68
+ end
69
+
70
+ def normalize_pluralization(locale, key, value)
71
+ # FIXME po_parser includes \000 chars that can not be turned into Symbols
72
+ key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first
73
+
74
+ keys = I18n::Gettext.plural_keys(locale)
75
+ values = value.split("\000")
76
+ raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect} on #{locale} locale for msgid #{key.inspect} with values #{values.inspect}" if values.size != keys.size
77
+
78
+ result = {}
79
+ values.each_with_index { |_value, ix| result[keys[ix]] = _value }
80
+ [key, result]
81
+ end
82
+
83
+ end
84
+ end
85
+ end