i18n 1.0.0 → 1.14.7

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -32
  3. data/lib/i18n/backend/base.rb +93 -34
  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 +48 -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 +44 -24
  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 +33 -22
  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 +8 -1
  33. data/lib/i18n/tests/interpolation.rb +34 -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 +9 -7
  38. data/lib/i18n/tests/localization/time.rb +27 -5
  39. data/lib/i18n/tests/lookup.rb +11 -5
  40. data/lib/i18n/tests/pluralization.rb +1 -1
  41. data/lib/i18n/tests/procs.rb +23 -7
  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 +179 -58
  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: 39f37c32e4547234644c06bad5f78487e301f8aea4a2aa47c94bebb568c75a05
4
- data.tar.gz: 9b3cb30997b5bb52f1a4a9ee4504211011208be9ce8d308afb2007b291ae40d3
3
+ metadata.gz: cca63854d9dd69dfe1d36eb0388926fc5e7a7cd297ece21e2fdc431ef9c4c286
4
+ data.tar.gz: 1ab0f78752eabfd6a03b7c1f4a6f4e45c03689e9fbc0936cd65d6cd1f4103363
5
5
  SHA512:
6
- metadata.gz: eb93464f794724a7fbc84526765766c2f9f815c0ae093bba9996ab7c7ad2cad266e72a13b37361278b954e3fea84e21dcee913f8f5d4da22e5dba32fade24a32
7
- data.tar.gz: a8223afdd56a948194455df49da3c23f754a492053080bb6c0b7b8e386ac068f8a9756b1cf5e620c517d5bc26e4d8a448aab7d1effd794f974e8d9d233086fca
6
+ metadata.gz: e8dd4e1cf010b24d748b4e2713794296964301234b9dbed36c96ae591677fb9ca3d5df7f5aedd86a48a1f470ad81de07997f2103107af32786486892204222d4
7
+ data.tar.gz: 9e2008459e303dc8e87742231d2468faa3f2edcb61a30c3c26f5240cc5606a8132bccc70ec3e45a9cfcc38eaf04d84c74c6d643ef54f1df2534c66706677180d
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,28 @@ module I18n
50
54
  end
51
55
 
52
56
  deep_interpolation = options[:deep_interpolation]
53
- values = options.except(*RESERVED_KEYS)
54
- if values
57
+ skip_interpolation = options[:skip_interpolation]
58
+ values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
59
+ if !skip_interpolation && values && !values.empty?
55
60
  entry = if deep_interpolation
56
61
  deep_interpolate(locale, entry, values)
57
62
  else
58
63
  interpolate(locale, entry, values)
59
64
  end
65
+ elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
66
+ raise ReservedInterpolationKey.new($1.to_sym, entry)
60
67
  end
61
68
  entry
62
69
  end
63
70
 
64
- def exists?(locale, key)
65
- lookup(locale, key) != nil
71
+ def exists?(locale, key, options = EMPTY_HASH)
72
+ lookup(locale, key, options[:scope]) != nil
66
73
  end
67
74
 
68
75
  # Acts the same as +strftime+, but uses a localized version of the
69
76
  # format string. Takes a key from the date/time formats translations as
70
77
  # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
71
- def localize(locale, object, format = :default, options = {})
78
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
72
79
  if object.nil? && options.include?(:default)
73
80
  return options[:default]
74
81
  end
@@ -78,7 +85,7 @@ module I18n
78
85
  key = format
79
86
  type = object.respond_to?(:sec) ? 'time' : 'date'
80
87
  options = options.merge(:raise => true, :object => object, :locale => locale)
81
- format = I18n.t(:"#{type}.formats.#{key}", options)
88
+ format = I18n.t(:"#{type}.formats.#{key}", **options)
82
89
  end
83
90
 
84
91
  format = translate_localization_format(locale, object, format, options)
@@ -92,12 +99,21 @@ module I18n
92
99
  end
93
100
 
94
101
  def reload!
102
+ eager_load! if eager_loaded?
103
+ end
104
+
105
+ def eager_load!
106
+ @eager_loaded = true
95
107
  end
96
108
 
97
109
  protected
98
110
 
111
+ def eager_loaded?
112
+ @eager_loaded ||= false
113
+ end
114
+
99
115
  # The method which actually looks up for the translation in the store.
100
- def lookup(locale, key, scope = [], options = {})
116
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
101
117
  raise NotImplementedError
102
118
  end
103
119
 
@@ -109,8 +125,13 @@ module I18n
109
125
  # If given subject is an Array, it walks the array and returns the
110
126
  # first translation that can be resolved. Otherwise it tries to resolve
111
127
  # the translation directly.
112
- def default(locale, object, subject, options = {})
113
- options = options.dup.reject { |key, value| key == :default }
128
+ def default(locale, object, subject, options = EMPTY_HASH)
129
+ if options.size == 1 && options.has_key?(:default)
130
+ options = {}
131
+ else
132
+ options = Utils.except(options, :default)
133
+ end
134
+
114
135
  case subject
115
136
  when Array
116
137
  subject.each do |item|
@@ -126,21 +147,29 @@ module I18n
126
147
  # If the given subject is a Symbol, it will be translated with the
127
148
  # given options. If it is a Proc then it will be evaluated. All other
128
149
  # subjects will be returned directly.
129
- def resolve(locale, object, subject, options = {})
150
+ def resolve(locale, object, subject, options = EMPTY_HASH)
130
151
  return subject if options[:resolve] == false
131
152
  result = catch(:exception) do
132
153
  case subject
133
154
  when Symbol
134
- I18n.translate(subject, options.merge(:locale => locale, :throw => true))
155
+ I18n.translate(
156
+ subject,
157
+ **options.merge(
158
+ :locale => locale,
159
+ :throw => true,
160
+ :skip_interpolation => true
161
+ )
162
+ )
135
163
  when Proc
136
164
  date_or_time = options.delete(:object) || object
137
- resolve(locale, object, subject.call(date_or_time, options))
165
+ resolve(locale, object, subject.call(date_or_time, **options))
138
166
  else
139
167
  subject
140
168
  end
141
169
  end
142
170
  result unless result.is_a?(MissingTranslation)
143
171
  end
172
+ alias_method :resolve_entry, :resolve
144
173
 
145
174
  # Picks a translation from a pluralized mnemonic subkey according to English
146
175
  # pluralization rules :
@@ -151,6 +180,7 @@ module I18n
151
180
  # not standard with regards to the CLDR pluralization rules.
152
181
  # Other backends can implement more flexible or complex pluralization rules.
153
182
  def pluralize(locale, entry, count)
183
+ entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
154
184
  return entry unless entry.is_a?(Hash) && count
155
185
 
156
186
  key = pluralization_key(entry, count)
@@ -166,9 +196,9 @@ module I18n
166
196
  #
167
197
  # if the given subject is an array then:
168
198
  # each element of the array is recursively interpolated (until it finds a string)
169
- # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
170
- # # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
171
- def interpolate(locale, subject, values = {})
199
+ # method interpolates ["yes, %{user}", ["maybe no, %{user}", "no, %{user}"]], :user => "bartuz"
200
+ # # => ["yes, bartuz", ["maybe no, bartuz", "no, bartuz"]]
201
+ def interpolate(locale, subject, values = EMPTY_HASH)
172
202
  return subject if values.empty?
173
203
 
174
204
  case subject
@@ -184,7 +214,7 @@ module I18n
184
214
  # deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
185
215
  # ann: 'good', john: 'big'
186
216
  # #=> { people: { ann: "Ann is good", john: "John is big" } }
187
- def deep_interpolate(locale, data, values = {})
217
+ def deep_interpolate(locale, data, values = EMPTY_HASH)
188
218
  return data if values.empty?
189
219
 
190
220
  case data
@@ -210,40 +240,69 @@ module I18n
210
240
  def load_file(filename)
211
241
  type = File.extname(filename).tr('.', '').downcase
212
242
  raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
213
- data = send(:"load_#{type}", filename)
243
+ data, keys_symbolized = send(:"load_#{type}", filename)
214
244
  unless data.is_a?(Hash)
215
245
  raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
216
246
  end
217
- data.each { |locale, d| store_translations(locale, d || {}) }
247
+ data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
248
+
249
+ data
218
250
  end
219
251
 
220
252
  # Loads a plain Ruby translations file. eval'ing the file must yield
221
253
  # a Hash containing translation data with locales as toplevel keys.
222
254
  def load_rb(filename)
223
- eval(IO.read(filename), binding, filename)
255
+ translations = eval(IO.read(filename), binding, filename.to_s)
256
+ [translations, false]
224
257
  end
225
258
 
226
259
  # Loads a YAML translations file. The data must have locales as
227
260
  # toplevel keys.
228
261
  def load_yml(filename)
229
262
  begin
230
- YAML.load_file(filename)
263
+ if YAML.respond_to?(:unsafe_load_file) # Psych 4.0 way
264
+ [YAML.unsafe_load_file(filename, symbolize_names: true, freeze: true), true]
265
+ else
266
+ [YAML.load_file(filename), false]
267
+ end
231
268
  rescue TypeError, ScriptError, StandardError => e
232
269
  raise InvalidLocaleData.new(filename, e.inspect)
233
270
  end
234
271
  end
272
+ alias_method :load_yaml, :load_yml
273
+
274
+ # Loads a JSON translations file. The data must have locales as
275
+ # toplevel keys.
276
+ def load_json(filename)
277
+ begin
278
+ # Use #load_file as a proxy for a version of JSON where symbolize_names and freeze are supported.
279
+ if ::JSON.respond_to?(:load_file)
280
+ [::JSON.load_file(filename, symbolize_names: true, freeze: true), true]
281
+ else
282
+ [::JSON.parse(File.read(filename)), false]
283
+ end
284
+ rescue TypeError, StandardError => e
285
+ raise InvalidLocaleData.new(filename, e.inspect)
286
+ end
287
+ end
235
288
 
236
289
  def translate_localization_format(locale, object, format, options)
237
- format.to_s.gsub(/%[aAbBpP]/) do |match|
290
+ format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
238
291
  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
292
+ when '%a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
293
+ when '%^a' then I18n.t!(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday].upcase
294
+ when '%A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday]
295
+ when '%^A' then I18n.t!(:"date.day_names", :locale => locale, :format => format)[object.wday].upcase
296
+ when '%b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
297
+ when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
298
+ when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
299
+ when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
300
+ when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
301
+ when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
245
302
  end
246
303
  end
304
+ rescue MissingTranslationData => e
305
+ e.message
247
306
  end
248
307
 
249
308
  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