sayso-i18n 0.5.0.001

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/CHANGELOG.textile +152 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.textile +103 -0
  4. data/ci/Gemfile.no-rails +5 -0
  5. data/ci/Gemfile.no-rails.lock +14 -0
  6. data/ci/Gemfile.rails-2.3.x +9 -0
  7. data/ci/Gemfile.rails-2.3.x.lock +23 -0
  8. data/ci/Gemfile.rails-3.x +9 -0
  9. data/ci/Gemfile.rails-3.x.lock +23 -0
  10. data/lib/i18n.rb +324 -0
  11. data/lib/i18n/backend.rb +19 -0
  12. data/lib/i18n/backend/base.rb +174 -0
  13. data/lib/i18n/backend/cache.rb +102 -0
  14. data/lib/i18n/backend/cascade.rb +53 -0
  15. data/lib/i18n/backend/chain.rb +80 -0
  16. data/lib/i18n/backend/fallbacks.rb +70 -0
  17. data/lib/i18n/backend/flatten.rb +113 -0
  18. data/lib/i18n/backend/flatten_yml.rb +70 -0
  19. data/lib/i18n/backend/gettext.rb +71 -0
  20. data/lib/i18n/backend/interpolation_compiler.rb +121 -0
  21. data/lib/i18n/backend/key_value.rb +100 -0
  22. data/lib/i18n/backend/memoize.rb +46 -0
  23. data/lib/i18n/backend/metadata.rb +65 -0
  24. data/lib/i18n/backend/pluralization.rb +55 -0
  25. data/lib/i18n/backend/simple.rb +87 -0
  26. data/lib/i18n/backend/transliterator.rb +98 -0
  27. data/lib/i18n/config.rb +86 -0
  28. data/lib/i18n/core_ext/hash.rb +29 -0
  29. data/lib/i18n/core_ext/kernel/surpress_warnings.rb +9 -0
  30. data/lib/i18n/core_ext/string/interpolate.rb +105 -0
  31. data/lib/i18n/exceptions.rb +88 -0
  32. data/lib/i18n/gettext.rb +25 -0
  33. data/lib/i18n/gettext/helpers.rb +64 -0
  34. data/lib/i18n/gettext/po_parser.rb +329 -0
  35. data/lib/i18n/interpolate/ruby.rb +31 -0
  36. data/lib/i18n/locale.rb +6 -0
  37. data/lib/i18n/locale/fallbacks.rb +96 -0
  38. data/lib/i18n/locale/tag.rb +28 -0
  39. data/lib/i18n/locale/tag/parents.rb +22 -0
  40. data/lib/i18n/locale/tag/rfc4646.rb +74 -0
  41. data/lib/i18n/locale/tag/simple.rb +39 -0
  42. data/lib/i18n/tests.rb +12 -0
  43. data/lib/i18n/tests/basics.rb +54 -0
  44. data/lib/i18n/tests/defaults.rb +40 -0
  45. data/lib/i18n/tests/interpolation.rb +133 -0
  46. data/lib/i18n/tests/link.rb +56 -0
  47. data/lib/i18n/tests/localization.rb +19 -0
  48. data/lib/i18n/tests/localization/date.rb +84 -0
  49. data/lib/i18n/tests/localization/date_time.rb +77 -0
  50. data/lib/i18n/tests/localization/procs.rb +116 -0
  51. data/lib/i18n/tests/localization/time.rb +76 -0
  52. data/lib/i18n/tests/lookup.rb +74 -0
  53. data/lib/i18n/tests/pluralization.rb +35 -0
  54. data/lib/i18n/tests/procs.rb +55 -0
  55. data/lib/i18n/version.rb +3 -0
  56. data/test/all.rb +8 -0
  57. data/test/api/all_features_test.rb +58 -0
  58. data/test/api/cascade_test.rb +28 -0
  59. data/test/api/chain_test.rb +24 -0
  60. data/test/api/fallbacks_test.rb +30 -0
  61. data/test/api/flatten_yml_test.rb +32 -0
  62. data/test/api/key_value_test.rb +28 -0
  63. data/test/api/memoize_test.rb +60 -0
  64. data/test/api/pluralization_test.rb +30 -0
  65. data/test/api/simple_test.rb +28 -0
  66. data/test/backend/cache_test.rb +83 -0
  67. data/test/backend/cascade_test.rb +85 -0
  68. data/test/backend/chain_test.rb +72 -0
  69. data/test/backend/exceptions_test.rb +23 -0
  70. data/test/backend/fallbacks_test.rb +120 -0
  71. data/test/backend/flatten_yml_test.rb +49 -0
  72. data/test/backend/interpolation_compiler_test.rb +102 -0
  73. data/test/backend/key_value_test.rb +46 -0
  74. data/test/backend/memoize_test.rb +47 -0
  75. data/test/backend/metadata_test.rb +67 -0
  76. data/test/backend/pluralization_test.rb +44 -0
  77. data/test/backend/simple_test.rb +83 -0
  78. data/test/backend/transliterator_test.rb +81 -0
  79. data/test/core_ext/hash_test.rb +30 -0
  80. data/test/core_ext/string/interpolate_test.rb +99 -0
  81. data/test/gettext/api_test.rb +206 -0
  82. data/test/gettext/backend_test.rb +93 -0
  83. data/test/i18n/exceptions_test.rb +116 -0
  84. data/test/i18n/interpolate_test.rb +61 -0
  85. data/test/i18n/load_path_test.rb +26 -0
  86. data/test/i18n_test.rb +236 -0
  87. data/test/locale/fallbacks_test.rb +124 -0
  88. data/test/locale/tag/rfc4646_test.rb +142 -0
  89. data/test/locale/tag/simple_test.rb +32 -0
  90. data/test/run_all.rb +21 -0
  91. data/test/test_data/locales/de.po +72 -0
  92. data/test/test_data/locales/en.rb +3 -0
  93. data/test/test_data/locales/en.yml +3 -0
  94. data/test/test_data/locales/invalid/empty.yml +1 -0
  95. data/test/test_data/locales/plurals.rb +113 -0
  96. data/test/test_helper.rb +56 -0
  97. metadata +194 -0
@@ -0,0 +1,19 @@
1
+ module I18n
2
+ module Backend
3
+ autoload :Base, 'i18n/backend/base'
4
+ autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
5
+ autoload :Cache, 'i18n/backend/cache'
6
+ autoload :Cascade, 'i18n/backend/cascade'
7
+ autoload :Chain, 'i18n/backend/chain'
8
+ autoload :Fallbacks, 'i18n/backend/fallbacks'
9
+ autoload :Flatten, 'i18n/backend/flatten'
10
+ autoload :Gettext, 'i18n/backend/gettext'
11
+ autoload :KeyValue, 'i18n/backend/key_value'
12
+ autoload :Memoize, 'i18n/backend/memoize'
13
+ autoload :Metadata, 'i18n/backend/metadata'
14
+ autoload :Pluralization, 'i18n/backend/pluralization'
15
+ autoload :Simple, 'i18n/backend/simple'
16
+ autoload :Transliterator, 'i18n/backend/transliterator'
17
+ autoload :FlattenYml, 'i18n/backend/flatten_yml'
18
+ end
19
+ end
@@ -0,0 +1,174 @@
1
+ require 'yaml'
2
+ require 'i18n/core_ext/hash'
3
+ require 'i18n/core_ext/kernel/surpress_warnings'
4
+
5
+ module I18n
6
+ module Backend
7
+ module Base
8
+ include I18n::Backend::Transliterator
9
+
10
+ # Accepts a list of paths to translation files. Loads translations from
11
+ # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
12
+ # for details.
13
+ def load_translations(*filenames)
14
+ filenames = I18n.load_path if filenames.empty?
15
+ filenames.flatten.each { |filename| load_file(filename) }
16
+ end
17
+
18
+ # This method receives a locale, a data hash and options for storing translations.
19
+ # Should be implemented
20
+ def store_translations(locale, data, options = {})
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def translate(locale, key, options = {})
25
+ raise InvalidLocale.new(locale) unless locale
26
+ entry = key && lookup(locale, key, options[:scope], options)
27
+
28
+ if options.empty?
29
+ entry = resolve(locale, key, entry, options)
30
+ else
31
+ count, default = options.values_at(:count, :default)
32
+ values = options.except(*RESERVED_KEYS)
33
+ entry = entry.nil? && default ?
34
+ default(locale, key, default, options) : resolve(locale, key, entry, options)
35
+ end
36
+
37
+ raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
38
+ entry = entry.dup if entry.is_a?(String)
39
+
40
+ entry = pluralize(locale, entry, count) if count
41
+ entry = interpolate(locale, entry, values) if values
42
+ entry
43
+ end
44
+
45
+ # Acts the same as +strftime+, but uses a localized version of the
46
+ # format string. Takes a key from the date/time formats translations as
47
+ # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
48
+ def localize(locale, object, format = :default, options = {})
49
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
50
+
51
+ if Symbol === format
52
+ key = format
53
+ type = object.respond_to?(:sec) ? 'time' : 'date'
54
+ options = options.merge(:raise => true, :object => object, :locale => locale)
55
+ format = I18n.t(:"#{type}.formats.#{key}", options)
56
+ end
57
+
58
+ # format = resolve(locale, object, format, options)
59
+ format = format.to_s.gsub(/%[aAbBp]/) do |match|
60
+ case match
61
+ when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
62
+ when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
63
+ when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
64
+ when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
65
+ when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour
66
+ end
67
+ end
68
+
69
+ object.strftime(format)
70
+ end
71
+
72
+ # Returns an array of locales for which translations are available
73
+ # ignoring the reserved translation meta data key :i18n.
74
+ def available_locales
75
+ raise NotImplementedError
76
+ end
77
+
78
+ def reload!
79
+ @skip_syntax_deprecation = false
80
+ end
81
+
82
+ protected
83
+
84
+ # The method which actually looks up for the translation in the store.
85
+ def lookup(locale, key, scope = [], options = {})
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # Evaluates defaults.
90
+ # If given subject is an Array, it walks the array and returns the
91
+ # first translation that can be resolved. Otherwise it tries to resolve
92
+ # the translation directly.
93
+ def default(locale, object, subject, options = {})
94
+ options = options.dup.reject { |key, value| key == :default }
95
+ case subject
96
+ when Array
97
+ subject.each do |item|
98
+ result = resolve(locale, object, item, options) and return result
99
+ end and nil
100
+ else
101
+ resolve(locale, object, subject, options)
102
+ end
103
+ end
104
+
105
+ # Resolves a translation.
106
+ # If the given subject is a Symbol, it will be translated with the
107
+ # given options. If it is a Proc then it will be evaluated. All other
108
+ # subjects will be returned directly.
109
+ def resolve(locale, object, subject, options = {})
110
+ return subject if options[:resolve] == false
111
+ case subject
112
+ when Symbol
113
+ I18n.translate(subject, options.merge(:locale => locale, :raise => true))
114
+ when Proc
115
+ date_or_time = options.delete(:object) || object
116
+ resolve(locale, object, subject.call(date_or_time, options))
117
+ else
118
+ subject
119
+ end
120
+ rescue MissingTranslationData
121
+ nil
122
+ end
123
+
124
+ # Picks a translation from an array according to English pluralization
125
+ # rules. It will pick the first translation if count is not equal to 1
126
+ # and the second translation if it is equal to 1. Other backends can
127
+ # implement more flexible or complex pluralization rules.
128
+ def pluralize(locale, entry, count)
129
+ return entry unless entry.is_a?(Hash) && count
130
+
131
+ key = :zero if count == 0 && entry.has_key?(:zero)
132
+ key ||= count == 1 ? :one : :other
133
+ raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
134
+ entry[key]
135
+ end
136
+
137
+ # Interpolates values into a given string.
138
+ #
139
+ # interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
140
+ # # => "file test.txt opened by %{user}"
141
+ def interpolate(locale, string, values = {})
142
+ if string.is_a?(::String) && !values.empty?
143
+ I18n.interpolate(string, values)
144
+ else
145
+ string
146
+ end
147
+ end
148
+
149
+ # Loads a single translations file by delegating to #load_rb or
150
+ # #load_yml depending on the file extension and directly merges the
151
+ # data to the existing translations. Raises I18n::UnknownFileType
152
+ # for all other file extensions.
153
+ def load_file(filename)
154
+ type = File.extname(filename).tr('.', '').downcase
155
+ raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
156
+ data = send(:"load_#{type}", filename)
157
+ raise InvalidLocaleData.new(filename) unless data.is_a?(Hash)
158
+ data.each { |locale, d| store_translations(locale, d || {}) }
159
+ end
160
+
161
+ # Loads a plain Ruby translations file. eval'ing the file must yield
162
+ # a Hash containing translation data with locales as toplevel keys.
163
+ def load_rb(filename)
164
+ eval(IO.read(filename), binding, filename)
165
+ end
166
+
167
+ # Loads a YAML translations file. The data must have locales as
168
+ # toplevel keys.
169
+ def load_yml(filename)
170
+ YAML.load_file(filename)
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,102 @@
1
+ # This module allows you to easily cache all responses from the backend - thus
2
+ # speeding up the I18n aspects of your application quite a bit.
3
+ #
4
+ # To enable caching you can simply include the Cache module to the Simple
5
+ # backend - or whatever other backend you are using:
6
+ #
7
+ # I18n::Backend::Simple.include(I18n::Backend::Cache)
8
+ #
9
+ # You will also need to set a cache store implementation that you want to use:
10
+ #
11
+ # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
12
+ #
13
+ # You can use any cache implementation you want that provides the same API as
14
+ # ActiveSupport::Cache (only the methods #fetch and #write are being used).
15
+ #
16
+ # The cache_key implementation assumes that you only pass values to
17
+ # I18n.translate that return a valid key from #hash (see
18
+ # http://www.ruby-doc.org/core/classes/Object.html#M000337).
19
+ #
20
+ # If you use a lambda as a default value in your translation like this:
21
+ #
22
+ # I18n.t(:"date.order", :default => lambda {[:month, :day, :year]})
23
+ #
24
+ # Then you will always have a cache miss, because each time this method
25
+ # is called the lambda will have a different hash value. If you know
26
+ # the result of the lambda is a constant as in the example above, then
27
+ # to cache this you can make the lambda a constant, like this:
28
+ #
29
+ # DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]}
30
+ # ...
31
+ # I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER)
32
+ #
33
+ # If the lambda may result in different values for each call then consider
34
+ # also using the Memoize backend.
35
+ #
36
+ module I18n
37
+ class << self
38
+ @@cache_store = nil
39
+ @@cache_namespace = nil
40
+
41
+ def cache_store
42
+ @@cache_store
43
+ end
44
+
45
+ def cache_store=(store)
46
+ @@cache_store = store
47
+ end
48
+
49
+ def cache_namespace
50
+ @@cache_namespace
51
+ end
52
+
53
+ def cache_namespace=(namespace)
54
+ @@cache_namespace = namespace
55
+ end
56
+
57
+ def perform_caching?
58
+ !cache_store.nil?
59
+ end
60
+ end
61
+
62
+ module Backend
63
+ # TODO Should the cache be cleared if new translations are stored?
64
+ module Cache
65
+ def translate(locale, key, options = {})
66
+ I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
67
+ end
68
+
69
+ protected
70
+
71
+ def fetch(cache_key, &block)
72
+ result = fetch_storing_missing_translation_exception(cache_key, &block)
73
+ raise result if result.is_a?(Exception)
74
+ result = result.dup if result.frozen? rescue result
75
+ result
76
+ end
77
+
78
+ def fetch_storing_missing_translation_exception(cache_key, &block)
79
+ fetch_ignoring_procs(cache_key, &block)
80
+ rescue MissingTranslationData => exception
81
+ I18n.cache_store.write(cache_key, exception)
82
+ exception
83
+ end
84
+
85
+ def fetch_ignoring_procs(cache_key, &block)
86
+ I18n.cache_store.read(cache_key) || yield.tap do |result|
87
+ I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc)
88
+ end
89
+ end
90
+
91
+ def cache_key(locale, key, options)
92
+ # This assumes that only simple, native Ruby values are passed to I18n.translate.
93
+ "i18n/#{I18n.cache_namespace}/#{locale}/#{key.hash}/#{USE_INSPECT_HASH ? options.inspect.hash : options.hash}"
94
+ end
95
+
96
+ private
97
+ # In Ruby < 1.9 the following is true: { :foo => 1, :bar => 2 }.hash == { :foo => 2, :bar => 1 }.hash
98
+ # Therefore we must use the hash of the inspect string instead to avoid cache key colisions.
99
+ USE_INSPECT_HASH = RUBY_VERSION <= "1.9"
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,53 @@
1
+ # The Cascade module adds the ability to do cascading lookups to backends that
2
+ # are compatible to the Simple backend.
3
+ #
4
+ # By cascading lookups we mean that for any key that can not be found the
5
+ # Cascade module strips one segment off the scope part of the key and then
6
+ # tries to look up the key in that scope.
7
+ #
8
+ # E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
9
+ # the segment :bar will be stripped off the scope part :"foo.bar" and the new
10
+ # scope :foo will be used to look up the key :baz. If that does not succeed
11
+ # then the remaining scope segment :foo will be omitted, too, and again the
12
+ # key :baz will be looked up (now with no scope).
13
+ #
14
+ # To enable a cascading lookup one passes the :cascade option:
15
+ #
16
+ # I18n.t(:'foo.bar.baz', :cascade => true)
17
+ #
18
+ # This will return the first translation found for :"foo.bar.baz", :"foo.baz"
19
+ # or :baz in this order.
20
+ #
21
+ # The cascading lookup takes precedence over resolving any given defaults.
22
+ # I.e. defaults will kick in after the cascading lookups haven't succeeded.
23
+ #
24
+ # This behavior is useful for libraries like ActiveRecord validations where
25
+ # the library wants to give users a bunch of more or less fine-grained options
26
+ # of scopes for a particular key.
27
+ #
28
+ # Thanks to Clemens Kofler for the initial idea and implementation! See
29
+ # http://github.com/clemens/i18n-cascading-backend
30
+
31
+ module I18n
32
+ module Backend
33
+ module Cascade
34
+ def lookup(locale, key, scope = [], options = {})
35
+ return super unless cascade = options[:cascade]
36
+
37
+ cascade = { :step => 1 } unless cascade.is_a?(Hash)
38
+ step = cascade[:step] || 1
39
+ offset = cascade[:offset] || 1
40
+ separator = options[:separator] || I18n.default_separator
41
+ skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
42
+
43
+ scope = I18n.normalize_keys(nil, key, scope, separator)
44
+ key = (scope.slice!(-offset, offset) || []).join(separator)
45
+
46
+ begin
47
+ result = super
48
+ return result unless result.nil?
49
+ end while (!scope.empty? || !skip_root) && scope.slice!(-step, step)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,80 @@
1
+ module I18n
2
+ module Backend
3
+ # Backend that chains multiple other backends and checks each of them when
4
+ # a translation needs to be looked up. This is useful when you want to use
5
+ # standard translations with a Simple backend but store custom application
6
+ # translations in a database or other backends.
7
+ #
8
+ # To use the Chain backend instantiate it and set it to the I18n module.
9
+ # You can add chained backends through the initializer or backends
10
+ # accessor:
11
+ #
12
+ # # preserves the existing Simple backend set to I18n.backend
13
+ # I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
14
+ #
15
+ # The implementation assumes that all backends added to the Chain implement
16
+ # a lookup method with the same API as Simple backend does.
17
+ class Chain
18
+ module Implementation
19
+ include Base
20
+
21
+ attr_accessor :backends
22
+
23
+ def initialize(*backends)
24
+ self.backends = backends
25
+ end
26
+
27
+ def reload!
28
+ backends.each { |backend| backend.reload! }
29
+ end
30
+
31
+ def store_translations(locale, data, options = {})
32
+ backends.first.store_translations(locale, data, options)
33
+ end
34
+
35
+ def available_locales
36
+ backends.map { |backend| backend.available_locales }.flatten.uniq
37
+ end
38
+
39
+ def translate(locale, key, default_options = {})
40
+ namespace = nil
41
+ options = default_options.except(:default)
42
+
43
+ backends.each do |backend|
44
+ begin
45
+ options = default_options if backend == backends.last
46
+ translation = backend.translate(locale, key, options)
47
+ if namespace_lookup?(translation, options)
48
+ namespace ||= {}
49
+ namespace.merge!(translation)
50
+ elsif !translation.nil?
51
+ return translation
52
+ end
53
+ rescue MissingTranslationData
54
+ end
55
+ end
56
+
57
+ return namespace if namespace
58
+ raise(I18n::MissingTranslationData.new(locale, key, options))
59
+ end
60
+
61
+ def localize(locale, object, format = :default, options = {})
62
+ backends.each do |backend|
63
+ begin
64
+ result = backend.localize(locale, object, format, options) and return result
65
+ rescue MissingTranslationData
66
+ end
67
+ end
68
+ raise(I18n::MissingTranslationData.new(locale, format, options))
69
+ end
70
+
71
+ protected
72
+ def namespace_lookup?(result, options)
73
+ result.is_a?(Hash) && !options.has_key?(:count)
74
+ end
75
+ end
76
+
77
+ include Implementation
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ # I18n locale fallbacks are useful when you want your application to use
2
+ # translations from other locales when translations for the current locale are
3
+ # missing. E.g. you might want to use :en translations when translations in
4
+ # your applications main locale :de are missing.
5
+ #
6
+ # To enable locale fallbacks you can simply include the Fallbacks module to
7
+ # the Simple backend - or whatever other backend you are using:
8
+ #
9
+ # I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
10
+ module I18n
11
+ @@fallbacks = nil
12
+
13
+ class << self
14
+ # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
15
+ def fallbacks
16
+ @@fallbacks ||= I18n::Locale::Fallbacks.new
17
+ end
18
+
19
+ # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
20
+ def fallbacks=(fallbacks)
21
+ @@fallbacks = fallbacks
22
+ end
23
+ end
24
+
25
+ module Backend
26
+ module Fallbacks
27
+ # Overwrites the Base backend translate method so that it will try each
28
+ # locale given by I18n.fallbacks for the given locale. E.g. for the
29
+ # locale :"de-DE" it might try the locales :"de-DE", :de and :en
30
+ # (depends on the fallbacks implementation) until it finds a result with
31
+ # the given options. If it does not find any result for any of the
32
+ # locales it will then raise a MissingTranslationData exception as
33
+ # usual.
34
+ #
35
+ # The default option takes precedence over fallback locales
36
+ # only when it's a Symbol. When the default contains a String or a Proc
37
+ # it is evaluated last after all the fallback locales have been tried.
38
+ def translate(locale, key, options = {})
39
+ return super if options[:fallback]
40
+ default = extract_string_or_lambda_default!(options) if options[:default]
41
+
42
+ options[:fallback] = true
43
+ I18n.fallbacks[locale].each do |fallback|
44
+ begin
45
+ result = super(fallback, key, options)
46
+ return result unless result.nil?
47
+ rescue I18n::MissingTranslationData
48
+ end
49
+ end
50
+ options.delete(:fallback)
51
+
52
+ return super(locale, nil, options.merge(:default => default)) if default
53
+ raise(I18n::MissingTranslationData.new(locale, key, options))
54
+ end
55
+
56
+ def extract_string_or_lambda_default!(options)
57
+ defaults = [options[:default]].flatten
58
+ if index = find_first_string_or_lambda_default(defaults)
59
+ options[:default] = defaults[0, index]
60
+ defaults[index]
61
+ end
62
+ end
63
+
64
+ def find_first_string_or_lambda_default(defaults)
65
+ defaults.each_with_index { |default, ix| return ix if String === default || Proc === default }
66
+ nil
67
+ end
68
+ end
69
+ end
70
+ end