i18n 0.9.5 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -32
  3. data/lib/i18n/backend/base.rb +80 -31
  4. data/lib/i18n/backend/cache.rb +10 -11
  5. data/lib/i18n/backend/cache_file.rb +36 -0
  6. data/lib/i18n/backend/cascade.rb +3 -1
  7. data/lib/i18n/backend/chain.rb +37 -6
  8. data/lib/i18n/backend/fallbacks.rb +43 -14
  9. data/lib/i18n/backend/flatten.rb +10 -5
  10. data/lib/i18n/backend/gettext.rb +4 -2
  11. data/lib/i18n/backend/interpolation_compiler.rb +3 -1
  12. data/lib/i18n/backend/key_value.rb +31 -4
  13. data/lib/i18n/backend/lazy_loadable.rb +184 -0
  14. data/lib/i18n/backend/memoize.rb +10 -2
  15. data/lib/i18n/backend/metadata.rb +5 -3
  16. data/lib/i18n/backend/pluralization.rb +61 -18
  17. data/lib/i18n/backend/simple.rb +36 -16
  18. data/lib/i18n/backend/transliterator.rb +26 -24
  19. data/lib/i18n/backend.rb +5 -1
  20. data/lib/i18n/config.rb +22 -4
  21. data/lib/i18n/exceptions.rb +71 -18
  22. data/lib/i18n/gettext/helpers.rb +4 -2
  23. data/lib/i18n/gettext/po_parser.rb +7 -7
  24. data/lib/i18n/gettext.rb +2 -0
  25. data/lib/i18n/interpolate/ruby.rb +22 -6
  26. data/lib/i18n/locale/fallbacks.rb +21 -20
  27. data/lib/i18n/locale/tag/parents.rb +8 -6
  28. data/lib/i18n/locale/tag/simple.rb +2 -2
  29. data/lib/i18n/locale.rb +2 -0
  30. data/lib/i18n/middleware.rb +2 -0
  31. data/lib/i18n/tests/basics.rb +5 -7
  32. data/lib/i18n/tests/defaults.rb +1 -1
  33. data/lib/i18n/tests/interpolation.rb +12 -7
  34. data/lib/i18n/tests/link.rb +11 -1
  35. data/lib/i18n/tests/localization/date.rb +37 -10
  36. data/lib/i18n/tests/localization/date_time.rb +28 -7
  37. data/lib/i18n/tests/localization/procs.rb +7 -5
  38. data/lib/i18n/tests/localization/time.rb +27 -5
  39. data/lib/i18n/tests/lookup.rb +5 -5
  40. data/lib/i18n/tests/pluralization.rb +1 -1
  41. data/lib/i18n/tests/procs.rb +12 -1
  42. data/lib/i18n/tests.rb +2 -0
  43. data/lib/i18n/utils.rb +55 -0
  44. data/lib/i18n/version.rb +3 -1
  45. data/lib/i18n.rb +134 -55
  46. metadata +16 -61
  47. data/gemfiles/Gemfile.rails-3.2.x +0 -10
  48. data/gemfiles/Gemfile.rails-4.0.x +0 -10
  49. data/gemfiles/Gemfile.rails-4.1.x +0 -10
  50. data/gemfiles/Gemfile.rails-4.2.x +0 -10
  51. data/gemfiles/Gemfile.rails-5.0.x +0 -10
  52. data/gemfiles/Gemfile.rails-5.1.x +0 -10
  53. data/gemfiles/Gemfile.rails-master +0 -10
  54. data/lib/i18n/core_ext/hash.rb +0 -29
  55. data/lib/i18n/core_ext/kernel/suppress_warnings.rb +0 -8
  56. data/lib/i18n/core_ext/string/interpolate.rb +0 -9
  57. data/test/api/all_features_test.rb +0 -58
  58. data/test/api/cascade_test.rb +0 -28
  59. data/test/api/chain_test.rb +0 -24
  60. data/test/api/fallbacks_test.rb +0 -30
  61. data/test/api/key_value_test.rb +0 -24
  62. data/test/api/memoize_test.rb +0 -56
  63. data/test/api/override_test.rb +0 -42
  64. data/test/api/pluralization_test.rb +0 -30
  65. data/test/api/simple_test.rb +0 -28
  66. data/test/backend/cache_test.rb +0 -109
  67. data/test/backend/cascade_test.rb +0 -86
  68. data/test/backend/chain_test.rb +0 -122
  69. data/test/backend/exceptions_test.rb +0 -36
  70. data/test/backend/fallbacks_test.rb +0 -219
  71. data/test/backend/interpolation_compiler_test.rb +0 -118
  72. data/test/backend/key_value_test.rb +0 -61
  73. data/test/backend/memoize_test.rb +0 -79
  74. data/test/backend/metadata_test.rb +0 -48
  75. data/test/backend/pluralization_test.rb +0 -45
  76. data/test/backend/simple_test.rb +0 -103
  77. data/test/backend/transliterator_test.rb +0 -84
  78. data/test/core_ext/hash_test.rb +0 -36
  79. data/test/gettext/api_test.rb +0 -214
  80. data/test/gettext/backend_test.rb +0 -92
  81. data/test/i18n/exceptions_test.rb +0 -117
  82. data/test/i18n/gettext_plural_keys_test.rb +0 -20
  83. data/test/i18n/interpolate_test.rb +0 -91
  84. data/test/i18n/load_path_test.rb +0 -34
  85. data/test/i18n/middleware_test.rb +0 -24
  86. data/test/i18n_test.rb +0 -462
  87. data/test/locale/fallbacks_test.rb +0 -133
  88. data/test/locale/tag/rfc4646_test.rb +0 -143
  89. data/test/locale/tag/simple_test.rb +0 -32
  90. data/test/run_all.rb +0 -20
  91. data/test/test_data/locales/de.po +0 -82
  92. data/test/test_data/locales/en.rb +0 -3
  93. data/test/test_data/locales/en.yml +0 -3
  94. data/test/test_data/locales/invalid/empty.yml +0 -0
  95. data/test/test_data/locales/invalid/syntax.yml +0 -4
  96. data/test/test_data/locales/plurals.rb +0 -113
  97. data/test/test_helper.rb +0 -61
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09643c444c40fe98042352f7f3e3e2df08e15ec5bee9f3d658b3e2fdeeb627a7'
4
- data.tar.gz: b924468907cca779632052d4ec166c79a9fc10f305a4a4f2c7be6ffeef6a5e47
3
+ metadata.gz: 56f42c8544a7ce959616902c3a1dcabe350581001f708b43883504995c29835a
4
+ data.tar.gz: 8502fbd1b16386dc75b9feac93e023915671ade132423b04538324e8c855de8c
5
5
  SHA512:
6
- metadata.gz: 338556073a2c02752c1d3e5e3ba5ed70cb692d814ab05aca5a3d70ef3ee013aa3bb9d5cfc6aecbbd2a93e4732105a4c05c70968ea7f955c169cf69ad97462796
7
- data.tar.gz: a0a4eff7b73ac32730dae8b77f609c4cc6fca9b6c2f46d81f42d992e10dd8d1467dbc28985b38769f9cab2f6ac1d67e4309eb8a8df65d9fc38baa39b65343f18
6
+ metadata.gz: 8b19cf52d0c49c3f521d3917958b47a235d61d374af806ade81da6092bf68577153012cf98cb61c85b4f951284a96aeace9280c8ca64d78e6f0b7546ef3f8d64
7
+ data.tar.gz: ac025237950bb63b2f66ff28ceb9cadb9de16bd9b55853ee47a48f61552c5192805bbc4415e4f8394481c74130c8163938707cf861ad65d58438636be096ccae
data/README.md CHANGED
@@ -1,45 +1,83 @@
1
1
  # Ruby I18n
2
2
 
3
- [![Build Status](https://api.travis-ci.org/svenfuchs/i18n.svg?branch=master)](https://travis-ci.org/svenfuchs/i18n)
3
+ [![Gem Version](https://badge.fury.io/rb/i18n.svg)](https://badge.fury.io/rb/i18n)
4
+ [![Build Status](https://github.com/ruby-i18n/i18n/workflows/Ruby/badge.svg)](https://github.com/ruby-i18n/i18n/actions?query=workflow%3ARuby)
4
5
 
5
- Ruby Internationalization and localization solution.
6
+ Ruby internationalization and localization (i18n) solution.
6
7
 
7
- [See the Rails Guide](http://guides.rubyonrails.org/i18n.html) for an example of its usage. (Note: This library can be used independently from Rails.)
8
+ Currently maintained by @radar.
8
9
 
9
- Features:
10
+ ## Usage
10
11
 
11
- * translation and localization
12
- * interpolation of values to translations (Ruby 1.9 compatible syntax)
13
- * pluralization (CLDR compatible)
14
- * customizable transliteration to ASCII
15
- * flexible defaults
16
- * bulk lookup
17
- * lambdas as translation data
18
- * custom key/scope separator
19
- * custom exception handlers
20
- * extensible architecture with a swappable backend
12
+ ### Rails
21
13
 
22
- Pluggable features:
14
+ You will most commonly use this library within a Rails app.
15
+
16
+ [See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage.
17
+
18
+ ### Ruby (without Rails)
19
+
20
+ If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`:
21
+
22
+ ```ruby
23
+ gem 'i18n'
24
+ ```
25
+
26
+ Then configure I18n with some translations, and a default locale:
27
+
28
+ ```ruby
29
+ I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
30
+ I18n.default_locale = :en # (note that `en` is already the default!)
31
+ ```
32
+
33
+ A simple translation file in your project might live at `config/locales/en.yml` and look like:
34
+
35
+ ```yml
36
+ en:
37
+ test: "This is a test"
38
+ ```
39
+
40
+ You can then access this translation by doing:
41
+
42
+ ```ruby
43
+ I18n.t(:test)
44
+ ```
45
+
46
+ You can switch locales in your project by setting `I18n.locale` to a different value:
47
+
48
+ ```ruby
49
+ I18n.locale = :de
50
+ I18n.t(:test) # => "Dies ist ein Test"
51
+ ```
52
+
53
+ ## Features
54
+
55
+ * Translation and localization
56
+ * Interpolation of values to translations
57
+ * Pluralization (CLDR compatible)
58
+ * Customizable transliteration to ASCII
59
+ * Flexible defaults
60
+ * Bulk lookup
61
+ * Lambdas as translation data
62
+ * Custom key/scope separator
63
+ * Custom exception handlers
64
+ * Extensible architecture with a swappable backend
65
+
66
+ ## Pluggable Features
23
67
 
24
68
  * Cache
25
69
  * Pluralization: lambda pluralizers stored as translation data
26
70
  * Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation)
27
- * [Gettext support](https://github.com/svenfuchs/i18n/wiki/Gettext)
71
+ * [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext)
28
72
  * Translation metadata
29
73
 
30
- Alternative backends:
74
+ ## Alternative Backend
31
75
 
32
76
  * Chain
33
77
  * ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs)
34
78
  * KeyValue (uses active_support/json and cannot store procs)
35
79
 
36
- For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/svenfuchs/i18n/wiki/Resources).
37
-
38
- ## Installation
39
-
40
- ```
41
- gem install i18n
42
- ```
80
+ For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources).
43
81
 
44
82
  ## Tests
45
83
 
@@ -58,7 +96,7 @@ particular tests in different test cases.
58
96
  The reason for this is that we need to enforce the I18n API across various
59
97
  combinations of extensions. E.g. the Simple backend alone needs to support
60
98
  the same API as any combination of feature and/or optimization modules included
61
- to the Simple backend. We test this by reusing the same API defition (implemented
99
+ to the Simple backend. We test this by reusing the same API definition (implemented
62
100
  as test methods) in test cases with different setups.
63
101
 
64
102
  You can find the test cases that enforce the API in test/api. And you can find
@@ -67,17 +105,18 @@ the API definition test methods in test/api/tests.
67
105
  All other test cases (e.g. as defined in test/backend, test/core_ext) etc.
68
106
  follow the usual test setup and should be easy to grok.
69
107
 
70
- ## Authors
108
+ ## More Documentation
71
109
 
72
- * [Sven Fuchs](http://www.artweb-design.de)
73
- * [Joshua Harvey](http://www.workingwithrails.com/person/759-joshua-harvey)
74
- * [Stephan Soller](http://www.arkanis-development.de)
75
- * [Saimon Moore](http://saimonmoore.net)
76
- * [Matt Aimonetti](https://matt.aimonetti.net/)
110
+ Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki
77
111
 
78
112
  ## Contributors
79
113
 
80
- https://github.com/svenfuchs/i18n/graphs/contributors
114
+ * @radar
115
+ * @carlosantoniodasilva
116
+ * @josevalim
117
+ * @knapo
118
+ * @tigrish
119
+ * [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors)
81
120
 
82
121
  ## License
83
122
 
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
- require 'i18n/core_ext/hash'
3
- require 'i18n/core_ext/kernel/suppress_warnings'
4
+ require 'json'
4
5
 
5
6
  module I18n
6
7
  module Backend
@@ -8,20 +9,23 @@ module I18n
8
9
  include I18n::Backend::Transliterator
9
10
 
10
11
  # 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
+ # plain Ruby (*.rb), YAML files (*.yml), or JSON files (*.json). See #load_rb, #load_yml, and #load_json
12
13
  # for details.
13
14
  def load_translations(*filenames)
14
15
  filenames = I18n.load_path if filenames.empty?
15
- filenames.flatten.each { |filename| load_file(filename) }
16
+ filenames.flatten.each do |filename|
17
+ loaded_translations = load_file(filename)
18
+ yield filename, loaded_translations if block_given?
19
+ end
16
20
  end
17
21
 
18
22
  # This method receives a locale, a data hash and options for storing translations.
19
23
  # Should be implemented
20
- def store_translations(locale, data, options = {})
24
+ def store_translations(locale, data, options = EMPTY_HASH)
21
25
  raise NotImplementedError
22
26
  end
23
27
 
24
- def translate(locale, key, options = {})
28
+ def translate(locale, key, options = EMPTY_HASH)
25
29
  raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
26
30
  raise InvalidLocale.new(locale) unless locale
27
31
  return nil if key.nil? && !options.key?(:default)
@@ -31,7 +35,7 @@ module I18n
31
35
  if entry.nil? && options.key?(:default)
32
36
  entry = default(locale, key, options[:default], options)
33
37
  else
34
- entry = resolve(locale, key, entry, options)
38
+ entry = resolve_entry(locale, key, entry, options)
35
39
  end
36
40
 
37
41
  count = options[:count]
@@ -50,7 +54,7 @@ module I18n
50
54
  end
51
55
 
52
56
  deep_interpolation = options[:deep_interpolation]
53
- values = options.except(*RESERVED_KEYS)
57
+ values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
54
58
  if values
55
59
  entry = if deep_interpolation
56
60
  deep_interpolate(locale, entry, values)
@@ -61,14 +65,14 @@ module I18n
61
65
  entry
62
66
  end
63
67
 
64
- def exists?(locale, key)
65
- lookup(locale, key) != nil
68
+ def exists?(locale, key, options = EMPTY_HASH)
69
+ lookup(locale, key, options[:scope]) != nil
66
70
  end
67
71
 
68
72
  # Acts the same as +strftime+, but uses a localized version of the
69
73
  # format string. Takes a key from the date/time formats translations as
70
74
  # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
71
- def localize(locale, object, format = :default, options = {})
75
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
72
76
  if object.nil? && options.include?(:default)
73
77
  return options[:default]
74
78
  end
@@ -78,7 +82,7 @@ module I18n
78
82
  key = format
79
83
  type = object.respond_to?(:sec) ? 'time' : 'date'
80
84
  options = options.merge(:raise => true, :object => object, :locale => locale)
81
- format = I18n.t(:"#{type}.formats.#{key}", options)
85
+ format = I18n.t(:"#{type}.formats.#{key}", **options)
82
86
  end
83
87
 
84
88
  format = translate_localization_format(locale, object, format, options)
@@ -92,12 +96,21 @@ module I18n
92
96
  end
93
97
 
94
98
  def reload!
99
+ eager_load! if eager_loaded?
100
+ end
101
+
102
+ def eager_load!
103
+ @eager_loaded = true
95
104
  end
96
105
 
97
106
  protected
98
107
 
108
+ def eager_loaded?
109
+ @eager_loaded ||= false
110
+ end
111
+
99
112
  # The method which actually looks up for the translation in the store.
100
- def lookup(locale, key, scope = [], options = {})
113
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
101
114
  raise NotImplementedError
102
115
  end
103
116
 
@@ -109,8 +122,13 @@ module I18n
109
122
  # If given subject is an Array, it walks the array and returns the
110
123
  # first translation that can be resolved. Otherwise it tries to resolve
111
124
  # the translation directly.
112
- def default(locale, object, subject, options = {})
113
- options = options.dup.reject { |key, value| key == :default }
125
+ def default(locale, object, subject, options = EMPTY_HASH)
126
+ if options.size == 1 && options.has_key?(:default)
127
+ options = {}
128
+ else
129
+ options = Utils.except(options, :default)
130
+ end
131
+
114
132
  case subject
115
133
  when Array
116
134
  subject.each do |item|
@@ -126,21 +144,22 @@ module I18n
126
144
  # If the given subject is a Symbol, it will be translated with the
127
145
  # given options. If it is a Proc then it will be evaluated. All other
128
146
  # subjects will be returned directly.
129
- def resolve(locale, object, subject, options = {})
147
+ def resolve(locale, object, subject, options = EMPTY_HASH)
130
148
  return subject if options[:resolve] == false
131
149
  result = catch(:exception) do
132
150
  case subject
133
151
  when Symbol
134
- I18n.translate(subject, options.merge(:locale => locale, :throw => true))
152
+ I18n.translate(subject, **options.merge(:locale => locale, :throw => true))
135
153
  when Proc
136
154
  date_or_time = options.delete(:object) || object
137
- resolve(locale, object, subject.call(date_or_time, options))
155
+ resolve(locale, object, subject.call(date_or_time, **options))
138
156
  else
139
157
  subject
140
158
  end
141
159
  end
142
160
  result unless result.is_a?(MissingTranslation)
143
161
  end
162
+ alias_method :resolve_entry, :resolve
144
163
 
145
164
  # Picks a translation from a pluralized mnemonic subkey according to English
146
165
  # pluralization rules :
@@ -151,6 +170,7 @@ module I18n
151
170
  # not standard with regards to the CLDR pluralization rules.
152
171
  # Other backends can implement more flexible or complex pluralization rules.
153
172
  def pluralize(locale, entry, count)
173
+ entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
154
174
  return entry unless entry.is_a?(Hash) && count
155
175
 
156
176
  key = pluralization_key(entry, count)
@@ -168,7 +188,7 @@ module I18n
168
188
  # each element of the array is recursively interpolated (until it finds a string)
169
189
  # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
170
190
  # # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
171
- def interpolate(locale, subject, values = {})
191
+ def interpolate(locale, subject, values = EMPTY_HASH)
172
192
  return subject if values.empty?
173
193
 
174
194
  case subject
@@ -184,7 +204,7 @@ module I18n
184
204
  # deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
185
205
  # ann: 'good', john: 'big'
186
206
  # #=> { people: { ann: "Ann is good", john: "John is big" } }
187
- def deep_interpolate(locale, data, values = {})
207
+ def deep_interpolate(locale, data, values = EMPTY_HASH)
188
208
  return data if values.empty?
189
209
 
190
210
  case data
@@ -210,40 +230,69 @@ module I18n
210
230
  def load_file(filename)
211
231
  type = File.extname(filename).tr('.', '').downcase
212
232
  raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
213
- data = send(:"load_#{type}", filename)
233
+ data, keys_symbolized = send(:"load_#{type}", filename)
214
234
  unless data.is_a?(Hash)
215
235
  raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
216
236
  end
217
- data.each { |locale, d| store_translations(locale, d || {}) }
237
+ data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
238
+
239
+ data
218
240
  end
219
241
 
220
242
  # Loads a plain Ruby translations file. eval'ing the file must yield
221
243
  # a Hash containing translation data with locales as toplevel keys.
222
244
  def load_rb(filename)
223
- eval(IO.read(filename), binding, filename)
245
+ translations = eval(IO.read(filename), binding, filename)
246
+ [translations, false]
224
247
  end
225
248
 
226
249
  # Loads a YAML translations file. The data must have locales as
227
250
  # toplevel keys.
228
251
  def load_yml(filename)
229
252
  begin
230
- YAML.load_file(filename)
253
+ if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
254
+ [YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
255
+ else
256
+ [YAML.load_file(filename), false]
257
+ end
231
258
  rescue TypeError, ScriptError, StandardError => e
232
259
  raise InvalidLocaleData.new(filename, e.inspect)
233
260
  end
234
261
  end
262
+ alias_method :load_yaml, :load_yml
263
+
264
+ # Loads a JSON translations file. The data must have locales as
265
+ # toplevel keys.
266
+ def load_json(filename)
267
+ begin
268
+ # Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
269
+ if ::JSON.respond_to?(:load_file)
270
+ [::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
271
+ else
272
+ [::JSON.parse(File.read(filename)), false]
273
+ end
274
+ rescue TypeError, StandardError => e
275
+ raise InvalidLocaleData.new(filename, e.inspect)
276
+ end
277
+ end
235
278
 
236
279
  def translate_localization_format(locale, object, format, options)
237
- format.to_s.gsub(/%[aAbBpP]/) do |match|
280
+ format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
238
281
  case match
239
- when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
240
- when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
241
- when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
242
- when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
243
- when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
244
- when '%P' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
282
+ when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
283
+ when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
284
+ when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
285
+ when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
286
+ when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
287
+ when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
288
+ when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
289
+ when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
290
+ when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
291
+ when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
245
292
  end
246
293
  end
294
+ rescue MissingTranslationData => e
295
+ e.message
247
296
  end
248
297
 
249
298
  def pluralization_key(entry, count)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This module allows you to easily cache all responses from the backend - thus
2
4
  # speeding up the I18n aspects of your application quite a bit.
3
5
  #
@@ -14,12 +16,12 @@
14
16
  # ActiveSupport::Cache (only the methods #fetch and #write are being used).
15
17
  #
16
18
  # The cache_key implementation by default assumes you pass values that return
17
- # a valid key from #hash (see
18
- # http://www.ruby-doc.org/core/classes/Object.html#M000337). However, you can
19
- # configure your own digest method via which responds to #hexdigest (see
20
- # http://ruby-doc.org/stdlib/libdoc/digest/rdoc/index.html):
19
+ # a valid key from #hash (see
20
+ # https://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
+ # https://ruby-doc.org/stdlib/libdoc/openssl/rdoc/OpenSSL/Digest.html):
21
23
  #
22
- # I18n.cache_key_digest = Digest::MD5.new
24
+ # I18n.cache_key_digest = OpenSSL::Digest::SHA256.new
23
25
  #
24
26
  # If you use a lambda as a default value in your translation like this:
25
27
  #
@@ -75,7 +77,7 @@ module I18n
75
77
  module Backend
76
78
  # TODO Should the cache be cleared if new translations are stored?
77
79
  module Cache
78
- def translate(locale, key, options = {})
80
+ def translate(locale, key, options = EMPTY_HASH)
79
81
  I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
80
82
  end
81
83
 
@@ -98,16 +100,13 @@ module I18n
98
100
 
99
101
  def cache_key(locale, key, options)
100
102
  # This assumes that only simple, native Ruby values are passed to I18n.translate.
101
- "i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{USE_INSPECT_HASH ? digest_item(options.inspect) : digest_item(options)}"
103
+ "i18n/#{I18n.cache_namespace}/#{locale}/#{digest_item(key)}/#{digest_item(options)}"
102
104
  end
103
105
 
104
106
  private
105
- # In Ruby < 1.9 the following is true: { :foo => 1, :bar => 2 }.hash == { :foo => 2, :bar => 1 }.hash
106
- # Therefore we must use the hash of the inspect string instead to avoid cache key colisions.
107
- USE_INSPECT_HASH = RUBY_VERSION <= "1.9"
108
107
 
109
108
  def digest_item(key)
110
- I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.hash
109
+ I18n.cache_key_digest ? I18n.cache_key_digest.hexdigest(key.to_s) : key.to_s.hash
111
110
  end
112
111
  end
113
112
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
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 = OpenSSL::Digest::SHA256.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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # The Cascade module adds the ability to do cascading lookups to backends that
2
4
  # are compatible to the Simple backend.
3
5
  #
@@ -31,7 +33,7 @@
31
33
  module I18n
32
34
  module Backend
33
35
  module Cascade
34
- def lookup(locale, key, scope = [], options = {})
36
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
35
37
  return super unless cascade = options[:cascade]
36
38
 
37
39
  cascade = { :step => 1 } unless cascade.is_a?(Hash)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module I18n
2
4
  module Backend
3
5
  # Backend that chains multiple other backends and checks each of them when
@@ -24,11 +26,24 @@ module I18n
24
26
  self.backends = backends
25
27
  end
26
28
 
29
+ def initialized?
30
+ backends.all? do |backend|
31
+ backend.instance_eval do
32
+ return false unless initialized?
33
+ end
34
+ end
35
+ true
36
+ end
37
+
27
38
  def reload!
28
39
  backends.each { |backend| backend.reload! }
29
40
  end
30
41
 
31
- def store_translations(locale, data, options = {})
42
+ def eager_load!
43
+ backends.each { |backend| backend.eager_load! }
44
+ end
45
+
46
+ def store_translations(locale, data, options = EMPTY_HASH)
32
47
  backends.first.store_translations(locale, data, options)
33
48
  end
34
49
 
@@ -36,9 +51,9 @@ module I18n
36
51
  backends.map { |backend| backend.available_locales }.flatten.uniq
37
52
  end
38
53
 
39
- def translate(locale, key, default_options = {})
54
+ def translate(locale, key, default_options = EMPTY_HASH)
40
55
  namespace = nil
41
- options = default_options.except(:default)
56
+ options = Utils.except(default_options, :default)
42
57
 
43
58
  backends.each do |backend|
44
59
  catch(:exception) do
@@ -56,13 +71,13 @@ module I18n
56
71
  throw(:exception, I18n::MissingTranslation.new(locale, key, options))
57
72
  end
58
73
 
59
- def exists?(locale, key)
74
+ def exists?(locale, key, options = EMPTY_HASH)
60
75
  backends.any? do |backend|
61
- backend.exists?(locale, key)
76
+ backend.exists?(locale, key, options)
62
77
  end
63
78
  end
64
79
 
65
- def localize(locale, object, format = :default, options = {})
80
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
66
81
  backends.each do |backend|
67
82
  catch(:exception) do
68
83
  result = backend.localize(locale, object, format, options) and return result
@@ -72,6 +87,22 @@ module I18n
72
87
  end
73
88
 
74
89
  protected
90
+ def init_translations
91
+ backends.each do |backend|
92
+ backend.send(:init_translations)
93
+ end
94
+ end
95
+
96
+ def translations
97
+ backends.reverse.each_with_object({}) do |backend, memo|
98
+ partial_translations = backend.instance_eval do
99
+ init_translations unless initialized?
100
+ translations
101
+ end
102
+ Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
103
+ end
104
+ end
105
+
75
106
  def namespace_lookup?(result, options)
76
107
  result.is_a?(Hash) && !options.has_key?(:count)
77
108
  end