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,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module I18n
4
+ module Backend
5
+ autoload :Base, 'i18n/backend/base'
6
+ autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
7
+ autoload :Cache, 'i18n/backend/cache'
8
+ autoload :CacheFile, 'i18n/backend/cache_file'
9
+ autoload :Cascade, 'i18n/backend/cascade'
10
+ autoload :Chain, 'i18n/backend/chain'
11
+ autoload :Fallbacks, 'i18n/backend/fallbacks'
12
+ autoload :Flatten, 'i18n/backend/flatten'
13
+ autoload :Gettext, 'i18n/backend/gettext'
14
+ autoload :KeyValue, 'i18n/backend/key_value'
15
+ autoload :Memoize, 'i18n/backend/memoize'
16
+ autoload :Metadata, 'i18n/backend/metadata'
17
+ autoload :Pluralization, 'i18n/backend/pluralization'
18
+ autoload :Simple, 'i18n/backend/simple'
19
+ autoload :Transliterator, 'i18n/backend/transliterator'
20
+ end
21
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'json'
5
+ require 'i18n/core_ext/hash'
6
+
7
+ module I18n
8
+ module Backend
9
+ module Base
10
+ using I18n::HashRefinements
11
+ include I18n::Backend::Transliterator
12
+
13
+ # Accepts a list of paths to translation files. Loads translations from
14
+ # plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
15
+ # for details.
16
+ def load_translations(*filenames)
17
+ filenames = I18n.load_path if filenames.empty?
18
+ filenames.flatten.each { |filename| load_file(filename) }
19
+ end
20
+
21
+ # This method receives a locale, a data hash and options for storing translations.
22
+ # Should be implemented
23
+ def store_translations(locale, data, options = EMPTY_HASH)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def translate(locale, key, options = EMPTY_HASH)
28
+ raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
29
+ raise InvalidLocale.new(locale) unless locale
30
+ return nil if key.nil? && !options.key?(:default)
31
+
32
+ entry = lookup(locale, key, options[:scope], options) unless key.nil?
33
+
34
+ if entry.nil? && options.key?(:default)
35
+ entry = default(locale, key, options[:default], options)
36
+ else
37
+ entry = resolve(locale, key, entry, options)
38
+ end
39
+
40
+ count = options[:count]
41
+
42
+ if entry.nil? && (subtrees? || !count)
43
+ if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
44
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
45
+ end
46
+ end
47
+
48
+ entry = entry.dup if entry.is_a?(String)
49
+ entry = pluralize(locale, entry, count) if count
50
+
51
+ if entry.nil? && !subtrees?
52
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
53
+ end
54
+
55
+ deep_interpolation = options[:deep_interpolation]
56
+ values = options.except(*RESERVED_KEYS)
57
+ if values
58
+ entry = if deep_interpolation
59
+ deep_interpolate(locale, entry, values)
60
+ else
61
+ interpolate(locale, entry, values)
62
+ end
63
+ end
64
+ entry
65
+ end
66
+
67
+ def exists?(locale, key)
68
+ lookup(locale, key) != nil
69
+ end
70
+
71
+ # Acts the same as +strftime+, but uses a localized version of the
72
+ # format string. Takes a key from the date/time formats translations as
73
+ # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
74
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
75
+ if object.nil? && options.include?(:default)
76
+ return options[:default]
77
+ end
78
+ raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
79
+
80
+ if Symbol === format
81
+ key = format
82
+ type = object.respond_to?(:sec) ? 'time' : 'date'
83
+ options = options.merge(:raise => true, :object => object, :locale => locale)
84
+ format = I18n.t(:"#{type}.formats.#{key}", options)
85
+ end
86
+
87
+ format = translate_localization_format(locale, object, format, options)
88
+ object.strftime(format)
89
+ end
90
+
91
+ # Returns an array of locales for which translations are available
92
+ # ignoring the reserved translation meta data key :i18n.
93
+ def available_locales
94
+ raise NotImplementedError
95
+ end
96
+
97
+ def reload!
98
+ eager_load! if eager_loaded?
99
+ end
100
+
101
+ def eager_load!
102
+ @eager_loaded = true
103
+ end
104
+
105
+ protected
106
+
107
+ def eager_loaded?
108
+ @eager_loaded ||= false
109
+ end
110
+
111
+ # The method which actually looks up for the translation in the store.
112
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
113
+ raise NotImplementedError
114
+ end
115
+
116
+ def subtrees?
117
+ true
118
+ end
119
+
120
+ # Evaluates defaults.
121
+ # If given subject is an Array, it walks the array and returns the
122
+ # first translation that can be resolved. Otherwise it tries to resolve
123
+ # the translation directly.
124
+ def default(locale, object, subject, options = EMPTY_HASH)
125
+ options = options.dup.reject { |key, value| key == :default }
126
+ case subject
127
+ when Array
128
+ subject.each do |item|
129
+ result = resolve(locale, object, item, options)
130
+ return result unless result.nil?
131
+ end and nil
132
+ else
133
+ resolve(locale, object, subject, options)
134
+ end
135
+ end
136
+
137
+ # Resolves a translation.
138
+ # If the given subject is a Symbol, it will be translated with the
139
+ # given options. If it is a Proc then it will be evaluated. All other
140
+ # subjects will be returned directly.
141
+ def resolve(locale, object, subject, options = EMPTY_HASH)
142
+ return subject if options[:resolve] == false
143
+ result = catch(:exception) do
144
+ case subject
145
+ when Symbol
146
+ I18n.translate(subject, options.merge(:locale => locale, :throw => true))
147
+ when Proc
148
+ date_or_time = options.delete(:object) || object
149
+ resolve(locale, object, subject.call(date_or_time, options))
150
+ else
151
+ subject
152
+ end
153
+ end
154
+ result unless result.is_a?(MissingTranslation)
155
+ end
156
+
157
+ # Picks a translation from a pluralized mnemonic subkey according to English
158
+ # pluralization rules :
159
+ # - It will pick the :one subkey if count is equal to 1.
160
+ # - It will pick the :other subkey otherwise.
161
+ # - It will pick the :zero subkey in the special case where count is
162
+ # equal to 0 and there is a :zero subkey present. This behaviour is
163
+ # not standard with regards to the CLDR pluralization rules.
164
+ # Other backends can implement more flexible or complex pluralization rules.
165
+ def pluralize(locale, entry, count)
166
+ return entry unless entry.is_a?(Hash) && count
167
+
168
+ key = pluralization_key(entry, count)
169
+ raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
170
+ entry[key]
171
+ end
172
+
173
+ # Interpolates values into a given subject.
174
+ #
175
+ # if the given subject is a string then:
176
+ # method interpolates "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
177
+ # # => "file test.txt opened by %{user}"
178
+ #
179
+ # if the given subject is an array then:
180
+ # each element of the array is recursively interpolated (until it finds a string)
181
+ # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
182
+ # # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
183
+ def interpolate(locale, subject, values = EMPTY_HASH)
184
+ return subject if values.empty?
185
+
186
+ case subject
187
+ when ::String then I18n.interpolate(subject, values)
188
+ when ::Array then subject.map { |element| interpolate(locale, element, values) }
189
+ else
190
+ subject
191
+ end
192
+ end
193
+
194
+ # Deep interpolation
195
+ #
196
+ # deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
197
+ # ann: 'good', john: 'big'
198
+ # #=> { people: { ann: "Ann is good", john: "John is big" } }
199
+ def deep_interpolate(locale, data, values = EMPTY_HASH)
200
+ return data if values.empty?
201
+
202
+ case data
203
+ when ::String
204
+ I18n.interpolate(data, values)
205
+ when ::Hash
206
+ data.each_with_object({}) do |(k, v), result|
207
+ result[k] = deep_interpolate(locale, v, values)
208
+ end
209
+ when ::Array
210
+ data.map do |v|
211
+ deep_interpolate(locale, v, values)
212
+ end
213
+ else
214
+ data
215
+ end
216
+ end
217
+
218
+ # Loads a single translations file by delegating to #load_rb or
219
+ # #load_yml depending on the file extension and directly merges the
220
+ # data to the existing translations. Raises I18n::UnknownFileType
221
+ # for all other file extensions.
222
+ def load_file(filename)
223
+ type = File.extname(filename).tr('.', '').downcase
224
+ raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
225
+ data = send(:"load_#{type}", filename)
226
+ unless data.is_a?(Hash)
227
+ raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
228
+ end
229
+ data.each { |locale, d| store_translations(locale, d || {}) }
230
+ end
231
+
232
+ # Loads a plain Ruby translations file. eval'ing the file must yield
233
+ # a Hash containing translation data with locales as toplevel keys.
234
+ def load_rb(filename)
235
+ eval(IO.read(filename), binding, filename)
236
+ end
237
+
238
+ # Loads a YAML translations file. The data must have locales as
239
+ # toplevel keys.
240
+ def load_yml(filename)
241
+ begin
242
+ YAML.load_file(filename)
243
+ rescue TypeError, ScriptError, StandardError => e
244
+ raise InvalidLocaleData.new(filename, e.inspect)
245
+ end
246
+ end
247
+ alias_method :load_yaml, :load_yml
248
+
249
+ # Loads a JSON translations file. The data must have locales as
250
+ # toplevel keys.
251
+ def load_json(filename)
252
+ begin
253
+ ::JSON.parse(File.read(filename))
254
+ rescue TypeError, StandardError => e
255
+ raise InvalidLocaleData.new(filename, e.inspect)
256
+ end
257
+ end
258
+
259
+ def translate_localization_format(locale, object, format, options)
260
+ format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
261
+ case match
262
+ when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
263
+ when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
264
+ when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
265
+ when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
266
+ when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
267
+ when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
268
+ when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
269
+ when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
270
+ when '%p' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
271
+ when '%P' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
272
+ end
273
+ end
274
+ rescue MissingTranslationData => e
275
+ e.message
276
+ end
277
+
278
+ def pluralization_key(entry, count)
279
+ key = :zero if count == 0 && entry.has_key?(:zero)
280
+ key ||= count == 1 ? :one : :other
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module allows you to easily cache all responses from the backend - thus
4
+ # speeding up the I18n aspects of your application quite a bit.
5
+ #
6
+ # To enable caching you can simply include the Cache module to the Simple
7
+ # backend - or whatever other backend you are using:
8
+ #
9
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
10
+ #
11
+ # You will also need to set a cache store implementation that you want to use:
12
+ #
13
+ # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
14
+ #
15
+ # You can use any cache implementation you want that provides the same API as
16
+ # ActiveSupport::Cache (only the methods #fetch and #write are being used).
17
+ #
18
+ # The cache_key implementation by default assumes you pass values that return
19
+ # a valid key from #hash (see
20
+ # http://www.ruby-doc.org/core/classes/Object.html#M000337). However, you can
21
+ # configure your own digest method via which responds to #hexdigest (see
22
+ # http://ruby-doc.org/stdlib/libdoc/digest/rdoc/index.html):
23
+ #
24
+ # I18n.cache_key_digest = Digest::MD5.new
25
+ #
26
+ # If you use a lambda as a default value in your translation like this:
27
+ #
28
+ # I18n.t(:"date.order", :default => lambda {[:month, :day, :year]})
29
+ #
30
+ # Then you will always have a cache miss, because each time this method
31
+ # is called the lambda will have a different hash value. If you know
32
+ # the result of the lambda is a constant as in the example above, then
33
+ # to cache this you can make the lambda a constant, like this:
34
+ #
35
+ # DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]}
36
+ # ...
37
+ # I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER)
38
+ #
39
+ # If the lambda may result in different values for each call then consider
40
+ # also using the Memoize backend.
41
+ #
42
+ module I18n
43
+ class << self
44
+ @@cache_store = nil
45
+ @@cache_namespace = nil
46
+ @@cache_key_digest = nil
47
+
48
+ def cache_store
49
+ @@cache_store
50
+ end
51
+
52
+ def cache_store=(store)
53
+ @@cache_store = store
54
+ end
55
+
56
+ def cache_namespace
57
+ @@cache_namespace
58
+ end
59
+
60
+ def cache_namespace=(namespace)
61
+ @@cache_namespace = namespace
62
+ end
63
+
64
+ def cache_key_digest
65
+ @@cache_key_digest
66
+ end
67
+
68
+ def cache_key_digest=(key_digest)
69
+ @@cache_key_digest = key_digest
70
+ end
71
+
72
+ def perform_caching?
73
+ !cache_store.nil?
74
+ end
75
+ end
76
+
77
+ module Backend
78
+ # TODO Should the cache be cleared if new translations are stored?
79
+ module Cache
80
+ def translate(locale, key, options = EMPTY_HASH)
81
+ I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
82
+ end
83
+
84
+ protected
85
+
86
+ def fetch(cache_key, &block)
87
+ result = _fetch(cache_key, &block)
88
+ throw(:exception, result) if result.is_a?(MissingTranslation)
89
+ result = result.dup if result.frozen? rescue result
90
+ result
91
+ end
92
+
93
+ def _fetch(cache_key, &block)
94
+ result = I18n.cache_store.read(cache_key)
95
+ return result unless result.nil?
96
+ result = catch(:exception, &block)
97
+ I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc)
98
+ result
99
+ end
100
+
101
+ def cache_key(locale, key, options)
102
+ # This assumes that only simple, native Ruby values are passed to I18n.translate.
103
+ "i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}"
104
+ end
105
+
106
+ private
107
+
108
+ def digest_item(key)
109
+ I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest/sha2'
4
+
5
+ module I18n
6
+ module Backend
7
+ # Overwrites the Base load_file method to cache loaded file contents.
8
+ module CacheFile
9
+ # Optionally provide path_roots array to normalize filename paths,
10
+ # to make the cached i18n data portable across environments.
11
+ attr_accessor :path_roots
12
+
13
+ protected
14
+
15
+ # Track loaded translation files in the `i18n.load_file` scope,
16
+ # and skip loading the file if its contents are still up-to-date.
17
+ def load_file(filename)
18
+ initialized = !respond_to?(:initialized?) || initialized?
19
+ key = I18n::Backend::Flatten.escape_default_separator(normalized_path(filename))
20
+ old_mtime, old_digest = initialized && lookup(:i18n, key, :load_file)
21
+ return if (mtime = File.mtime(filename).to_i) == old_mtime ||
22
+ (digest = Digest::SHA2.file(filename).hexdigest) == old_digest
23
+ super
24
+ store_translations(:i18n, load_file: { key => [mtime, digest] })
25
+ end
26
+
27
+ # Translate absolute filename to relative path for i18n key.
28
+ def normalized_path(file)
29
+ return file unless path_roots
30
+ path = path_roots.find(&file.method(:start_with?)) ||
31
+ raise(InvalidLocaleData.new(file, 'outside expected path roots'))
32
+ file.sub(path, path_roots.index(path).to_s)
33
+ end
34
+ end
35
+ end
36
+ end