i18n 1.6.0

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 +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