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,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,127 @@
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)
77
+ backends.any? do |backend|
78
+ backend.exists?(locale, key)
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.first.instance_eval do
100
+ init_translations unless initialized?
101
+ translations
102
+ end
103
+ end
104
+
105
+ def namespace_lookup?(result, options)
106
+ result.is_a?(Hash) && !options.has_key?(:count)
107
+ end
108
+
109
+ private
110
+ # This is approximately what gets used in ActiveSupport.
111
+ # However since we are not guaranteed to run in an ActiveSupport context
112
+ # it is wise to have our own copy. We underscore it
113
+ # to not pollute the namespace of the including class.
114
+ def _deep_merge(hash, other_hash)
115
+ copy = hash.dup
116
+ other_hash.each_pair do |k,v|
117
+ value_from_other = hash[k]
118
+ copy[k] = value_from_other.is_a?(Hash) && v.is_a?(Hash) ? _deep_merge(value_from_other, v) : v
119
+ end
120
+ copy
121
+ end
122
+ end
123
+
124
+ include Implementation
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,84 @@
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
+ return result unless result.nil?
50
+ end
51
+ rescue I18n::InvalidLocale
52
+ # we do nothing when the locale is invalid, as this is a fallback anyways.
53
+ end
54
+ end
55
+
56
+ return if options.key?(:default) && options[:default].nil?
57
+
58
+ return super(locale, nil, options.merge(:default => default)) if default
59
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
60
+ end
61
+
62
+ def extract_non_symbol_default!(options)
63
+ defaults = [options[:default]].flatten
64
+ first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
65
+ if first_non_symbol_default
66
+ options[:default] = defaults[0, defaults.index(first_non_symbol_default)]
67
+ end
68
+ return first_non_symbol_default
69
+ end
70
+
71
+ def exists?(locale, key)
72
+ I18n.fallbacks[locale].each do |fallback|
73
+ begin
74
+ return true if super(fallback, key)
75
+ rescue I18n::InvalidLocale
76
+ # we do nothing when the locale is invalid, as this is a fallback anyways.
77
+ end
78
+ end
79
+
80
+ false
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,115 @@
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].flatten.compact
22
+ separator ||= I18n.default_separator
23
+
24
+ if separator != FLATTEN_SEPARATOR
25
+ keys.map! do |k|
26
+ k.to_s.tr("#{FLATTEN_SEPARATOR}#{separator}",
27
+ "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}")
28
+ end
29
+ end
30
+
31
+ keys.join(".")
32
+ end
33
+
34
+ # Receives a string and escape the default separator.
35
+ def self.escape_default_separator(key) #:nodoc:
36
+ key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
37
+ end
38
+
39
+ # Shortcut to I18n::Backend::Flatten.normalize_flat_keys
40
+ # and then resolve_links.
41
+ def normalize_flat_keys(locale, key, scope, separator)
42
+ key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
43
+ resolve_link(locale, key)
44
+ end
45
+
46
+ # Store flattened links.
47
+ def links
48
+ @links ||= I18n.new_double_nested_cache
49
+ end
50
+
51
+ # Flatten keys for nested Hashes by chaining up keys:
52
+ #
53
+ # >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
54
+ # => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
55
+ #
56
+ def flatten_keys(hash, escape, prev_key=nil, &block)
57
+ hash.each_pair do |key, value|
58
+ key = escape_default_separator(key) if escape
59
+ curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
60
+ yield curr_key, value
61
+ flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
62
+ end
63
+ end
64
+
65
+ # Receives a hash of translations (where the key is a locale and
66
+ # the value is another hash) and return a hash with all
67
+ # translations flattened.
68
+ #
69
+ # Nested hashes are included in the flattened hash just if subtree
70
+ # is true and Symbols are automatically stored as links.
71
+ def flatten_translations(locale, data, escape, subtree)
72
+ hash = {}
73
+ flatten_keys(data, escape) do |key, value|
74
+ if value.is_a?(Hash)
75
+ hash[key] = value if subtree
76
+ else
77
+ store_link(locale, key, value) if value.is_a?(Symbol)
78
+ hash[key] = value
79
+ end
80
+ end
81
+ hash
82
+ end
83
+
84
+ protected
85
+
86
+ def store_link(locale, key, link)
87
+ links[locale.to_sym][key.to_s] = link.to_s
88
+ end
89
+
90
+ def resolve_link(locale, key)
91
+ key, locale = key.to_s, locale.to_sym
92
+ links = self.links[locale]
93
+
94
+ if links.key?(key)
95
+ links[key]
96
+ elsif link = find_link(locale, key)
97
+ store_link(locale, key, key.gsub(*link))
98
+ else
99
+ key
100
+ end
101
+ end
102
+
103
+ def find_link(locale, key) #:nodoc:
104
+ links[locale].each_pair do |from, to|
105
+ return [from, to] if key[0, from.length] == from
106
+ end && nil
107
+ end
108
+
109
+ def escape_default_separator(key) #:nodoc:
110
+ I18n::Backend::Flatten.escape_default_separator(key)
111
+ end
112
+
113
+ end
114
+ end
115
+ 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