i18n 0.9.5 → 1.14.4

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 +75 -32
  3. data/lib/i18n/backend/base.rb +83 -32
  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 +39 -6
  8. data/lib/i18n/backend/fallbacks.rb +44 -15
  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 +8 -8
  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 +29 -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 +16 -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: ef99abbb2ed698cd9dd77ab484720a5ecf68be96a7064f2f8b432a69758ada07
4
+ data.tar.gz: 0a4ade5577fe636c03d6ef9ca11f7a26b80a5c58078ee87c2f9dcf9fdd6882de
5
5
  SHA512:
6
- metadata.gz: 338556073a2c02752c1d3e5e3ba5ed70cb692d814ab05aca5a3d70ef3ee013aa3bb9d5cfc6aecbbd2a93e4732105a4c05c70968ea7f955c169cf69ad97462796
7
- data.tar.gz: a0a4eff7b73ac32730dae8b77f609c4cc6fca9b6c2f46d81f42d992e10dd8d1467dbc28985b38769f9cab2f6ac1d67e4309eb8a8df65d9fc38baa39b65343f18
6
+ metadata.gz: 21b666c06e7c8ebdb78088ef2ecd8da866ebd414dff3f3a8535b3e950c12bf0f38695a22acac6da7775e8e245c7a99a0d1d9a6bc409e0ea3d552520a0be26f68
7
+ data.tar.gz: c5f81a0bafbf70daa4a2c4374f3906ef31cea1d8f94d5d5e9b980224d1b6bc75f95cdd3e08709f6cae410c34f4285deb375d7e5b06e19f283743221606e23434
data/README.md CHANGED
@@ -1,45 +1,87 @@
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
+ We support Rails versions from 6.0 and up.
17
+
18
+ [See the Rails Guide](https://guides.rubyonrails.org/i18n.html) for an example of its usage.
19
+
20
+ ### Ruby (without Rails)
21
+
22
+ We support Ruby versions from 3.0 and up.
23
+
24
+ If you want to use this library without Rails, you can simply add `i18n` to your `Gemfile`:
25
+
26
+ ```ruby
27
+ gem 'i18n'
28
+ ```
29
+
30
+ Then configure I18n with some translations, and a default locale:
31
+
32
+ ```ruby
33
+ I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
34
+ I18n.default_locale = :en # (note that `en` is already the default!)
35
+ ```
36
+
37
+ A simple translation file in your project might live at `config/locales/en.yml` and look like:
38
+
39
+ ```yml
40
+ en:
41
+ test: "This is a test"
42
+ ```
43
+
44
+ You can then access this translation by doing:
45
+
46
+ ```ruby
47
+ I18n.t(:test)
48
+ ```
49
+
50
+ You can switch locales in your project by setting `I18n.locale` to a different value:
51
+
52
+ ```ruby
53
+ I18n.locale = :de
54
+ I18n.t(:test) # => "Dies ist ein Test"
55
+ ```
56
+
57
+ ## Features
58
+
59
+ * Translation and localization
60
+ * Interpolation of values to translations
61
+ * Pluralization (CLDR compatible)
62
+ * Customizable transliteration to ASCII
63
+ * Flexible defaults
64
+ * Bulk lookup
65
+ * Lambdas as translation data
66
+ * Custom key/scope separator
67
+ * Custom exception handlers
68
+ * Extensible architecture with a swappable backend
69
+
70
+ ## Pluggable Features
23
71
 
24
72
  * Cache
25
73
  * Pluralization: lambda pluralizers stored as translation data
26
74
  * Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation)
27
- * [Gettext support](https://github.com/svenfuchs/i18n/wiki/Gettext)
75
+ * [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext)
28
76
  * Translation metadata
29
77
 
30
- Alternative backends:
78
+ ## Alternative Backend
31
79
 
32
80
  * Chain
33
81
  * ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs)
34
82
  * KeyValue (uses active_support/json and cannot store procs)
35
83
 
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
- ```
84
+ For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources).
43
85
 
44
86
  ## Tests
45
87
 
@@ -58,7 +100,7 @@ particular tests in different test cases.
58
100
  The reason for this is that we need to enforce the I18n API across various
59
101
  combinations of extensions. E.g. the Simple backend alone needs to support
60
102
  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
103
+ to the Simple backend. We test this by reusing the same API definition (implemented
62
104
  as test methods) in test cases with different setups.
63
105
 
64
106
  You can find the test cases that enforce the API in test/api. And you can find
@@ -67,17 +109,18 @@ the API definition test methods in test/api/tests.
67
109
  All other test cases (e.g. as defined in test/backend, test/core_ext) etc.
68
110
  follow the usual test setup and should be easy to grok.
69
111
 
70
- ## Authors
112
+ ## More Documentation
71
113
 
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/)
114
+ Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki
77
115
 
78
116
  ## Contributors
79
117
 
80
- https://github.com/svenfuchs/i18n/graphs/contributors
118
+ * @radar
119
+ * @carlosantoniodasilva
120
+ * @josevalim
121
+ * @knapo
122
+ * @tigrish
123
+ * [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors)
81
124
 
82
125
  ## License
83
126
 
@@ -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,25 +54,27 @@ module I18n
50
54
  end
51
55
 
52
56
  deep_interpolation = options[:deep_interpolation]
53
- values = options.except(*RESERVED_KEYS)
54
- if values
57
+ values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
58
+ if values && !values.empty?
55
59
  entry = if deep_interpolation
56
60
  deep_interpolate(locale, entry, values)
57
61
  else
58
62
  interpolate(locale, entry, values)
59
63
  end
64
+ elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
65
+ raise ReservedInterpolationKey.new($1.to_sym, entry)
60
66
  end
61
67
  entry
62
68
  end
63
69
 
64
- def exists?(locale, key)
65
- lookup(locale, key) != nil
70
+ def exists?(locale, key, options = EMPTY_HASH)
71
+ lookup(locale, key, options[:scope]) != nil
66
72
  end
67
73
 
68
74
  # Acts the same as +strftime+, but uses a localized version of the
69
75
  # format string. Takes a key from the date/time formats translations as
70
76
  # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
71
- def localize(locale, object, format = :default, options = {})
77
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
72
78
  if object.nil? && options.include?(:default)
73
79
  return options[:default]
74
80
  end
@@ -78,7 +84,7 @@ module I18n
78
84
  key = format
79
85
  type = object.respond_to?(:sec) ? 'time' : 'date'
80
86
  options = options.merge(:raise => true, :object => object, :locale => locale)
81
- format = I18n.t(:"#{type}.formats.#{key}", options)
87
+ format = I18n.t(:"#{type}.formats.#{key}", **options)
82
88
  end
83
89
 
84
90
  format = translate_localization_format(locale, object, format, options)
@@ -92,12 +98,21 @@ module I18n
92
98
  end
93
99
 
94
100
  def reload!
101
+ eager_load! if eager_loaded?
102
+ end
103
+
104
+ def eager_load!
105
+ @eager_loaded = true
95
106
  end
96
107
 
97
108
  protected
98
109
 
110
+ def eager_loaded?
111
+ @eager_loaded ||= false
112
+ end
113
+
99
114
  # The method which actually looks up for the translation in the store.
100
- def lookup(locale, key, scope = [], options = {})
115
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
101
116
  raise NotImplementedError
102
117
  end
103
118
 
@@ -109,8 +124,13 @@ module I18n
109
124
  # If given subject is an Array, it walks the array and returns the
110
125
  # first translation that can be resolved. Otherwise it tries to resolve
111
126
  # the translation directly.
112
- def default(locale, object, subject, options = {})
113
- options = options.dup.reject { |key, value| key == :default }
127
+ def default(locale, object, subject, options = EMPTY_HASH)
128
+ if options.size == 1 && options.has_key?(:default)
129
+ options = {}
130
+ else
131
+ options = Utils.except(options, :default)
132
+ end
133
+
114
134
  case subject
115
135
  when Array
116
136
  subject.each do |item|
@@ -126,21 +146,22 @@ module I18n
126
146
  # If the given subject is a Symbol, it will be translated with the
127
147
  # given options. If it is a Proc then it will be evaluated. All other
128
148
  # subjects will be returned directly.
129
- def resolve(locale, object, subject, options = {})
149
+ def resolve(locale, object, subject, options = EMPTY_HASH)
130
150
  return subject if options[:resolve] == false
131
151
  result = catch(:exception) do
132
152
  case subject
133
153
  when Symbol
134
- I18n.translate(subject, options.merge(:locale => locale, :throw => true))
154
+ I18n.translate(subject, **options.merge(:locale => locale, :throw => true))
135
155
  when Proc
136
156
  date_or_time = options.delete(:object) || object
137
- resolve(locale, object, subject.call(date_or_time, options))
157
+ resolve(locale, object, subject.call(date_or_time, **options))
138
158
  else
139
159
  subject
140
160
  end
141
161
  end
142
162
  result unless result.is_a?(MissingTranslation)
143
163
  end
164
+ alias_method :resolve_entry, :resolve
144
165
 
145
166
  # Picks a translation from a pluralized mnemonic subkey according to English
146
167
  # pluralization rules :
@@ -151,6 +172,7 @@ module I18n
151
172
  # not standard with regards to the CLDR pluralization rules.
152
173
  # Other backends can implement more flexible or complex pluralization rules.
153
174
  def pluralize(locale, entry, count)
175
+ entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
154
176
  return entry unless entry.is_a?(Hash) && count
155
177
 
156
178
  key = pluralization_key(entry, count)
@@ -168,7 +190,7 @@ module I18n
168
190
  # each element of the array is recursively interpolated (until it finds a string)
169
191
  # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
170
192
  # # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
171
- def interpolate(locale, subject, values = {})
193
+ def interpolate(locale, subject, values = EMPTY_HASH)
172
194
  return subject if values.empty?
173
195
 
174
196
  case subject
@@ -184,7 +206,7 @@ module I18n
184
206
  # deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
185
207
  # ann: 'good', john: 'big'
186
208
  # #=> { people: { ann: "Ann is good", john: "John is big" } }
187
- def deep_interpolate(locale, data, values = {})
209
+ def deep_interpolate(locale, data, values = EMPTY_HASH)
188
210
  return data if values.empty?
189
211
 
190
212
  case data
@@ -210,40 +232,69 @@ module I18n
210
232
  def load_file(filename)
211
233
  type = File.extname(filename).tr('.', '').downcase
212
234
  raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
213
- data = send(:"load_#{type}", filename)
235
+ data, keys_symbolized = send(:"load_#{type}", filename)
214
236
  unless data.is_a?(Hash)
215
237
  raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
216
238
  end
217
- data.each { |locale, d| store_translations(locale, d || {}) }
239
+ data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
240
+
241
+ data
218
242
  end
219
243
 
220
244
  # Loads a plain Ruby translations file. eval'ing the file must yield
221
245
  # a Hash containing translation data with locales as toplevel keys.
222
246
  def load_rb(filename)
223
- eval(IO.read(filename), binding, filename)
247
+ translations = eval(IO.read(filename), binding, filename)
248
+ [translations, false]
224
249
  end
225
250
 
226
251
  # Loads a YAML translations file. The data must have locales as
227
252
  # toplevel keys.
228
253
  def load_yml(filename)
229
254
  begin
230
- YAML.load_file(filename)
255
+ if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
256
+ [YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
257
+ else
258
+ [YAML.load_file(filename), false]
259
+ end
231
260
  rescue TypeError, ScriptError, StandardError => e
232
261
  raise InvalidLocaleData.new(filename, e.inspect)
233
262
  end
234
263
  end
264
+ alias_method :load_yaml, :load_yml
265
+
266
+ # Loads a JSON translations file. The data must have locales as
267
+ # toplevel keys.
268
+ def load_json(filename)
269
+ begin
270
+ # Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
271
+ if ::JSON.respond_to?(:load_file)
272
+ [::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
273
+ else
274
+ [::JSON.parse(File.read(filename)), false]
275
+ end
276
+ rescue TypeError, StandardError => e
277
+ raise InvalidLocaleData.new(filename, e.inspect)
278
+ end
279
+ end
235
280
 
236
281
  def translate_localization_format(locale, object, format, options)
237
- format.to_s.gsub(/%[aAbBpP]/) do |match|
282
+ format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
238
283
  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
284
+ when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
285
+ when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
286
+ when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
287
+ when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
288
+ when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
289
+ when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
290
+ when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
291
+ when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
292
+ when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
293
+ when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
245
294
  end
246
295
  end
296
+ rescue MissingTranslationData => e
297
+ e.message
247
298
  end
248
299
 
249
300
  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
@@ -14,6 +16,8 @@ module I18n
14
16
  #
15
17
  # The implementation assumes that all backends added to the Chain implement
16
18
  # a lookup method with the same API as Simple backend does.
19
+ #
20
+ # Fallback translations using the :default option are only used by the last backend of a chain.
17
21
  class Chain
18
22
  module Implementation
19
23
  include Base
@@ -24,11 +28,24 @@ module I18n
24
28
  self.backends = backends
25
29
  end
26
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
+
27
40
  def reload!
28
41
  backends.each { |backend| backend.reload! }
29
42
  end
30
43
 
31
- def store_translations(locale, data, options = {})
44
+ def eager_load!
45
+ backends.each { |backend| backend.eager_load! }
46
+ end
47
+
48
+ def store_translations(locale, data, options = EMPTY_HASH)
32
49
  backends.first.store_translations(locale, data, options)
33
50
  end
34
51
 
@@ -36,9 +53,9 @@ module I18n
36
53
  backends.map { |backend| backend.available_locales }.flatten.uniq
37
54
  end
38
55
 
39
- def translate(locale, key, default_options = {})
56
+ def translate(locale, key, default_options = EMPTY_HASH)
40
57
  namespace = nil
41
- options = default_options.except(:default)
58
+ options = Utils.except(default_options, :default)
42
59
 
43
60
  backends.each do |backend|
44
61
  catch(:exception) do
@@ -56,13 +73,13 @@ module I18n
56
73
  throw(:exception, I18n::MissingTranslation.new(locale, key, options))
57
74
  end
58
75
 
59
- def exists?(locale, key)
76
+ def exists?(locale, key, options = EMPTY_HASH)
60
77
  backends.any? do |backend|
61
- backend.exists?(locale, key)
78
+ backend.exists?(locale, key, options)
62
79
  end
63
80
  end
64
81
 
65
- def localize(locale, object, format = :default, options = {})
82
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
66
83
  backends.each do |backend|
67
84
  catch(:exception) do
68
85
  result = backend.localize(locale, object, format, options) and return result
@@ -72,6 +89,22 @@ module I18n
72
89
  end
73
90
 
74
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.reverse.each_with_object({}) do |backend, memo|
100
+ partial_translations = backend.instance_eval do
101
+ init_translations unless initialized?
102
+ translations
103
+ end
104
+ Utils.deep_merge!(memo, partial_translations) { |_, a, b| b || a }
105
+ end
106
+ end
107
+
75
108
  def namespace_lookup?(result, options)
76
109
  result.is_a?(Hash) && !options.has_key?(:count)
77
110
  end