i18n 0.4.0 → 1.14.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +0 -0
  3. data/README.md +127 -0
  4. data/lib/i18n/backend/base.rb +189 -111
  5. data/lib/i18n/backend/cache.rb +58 -22
  6. data/lib/i18n/backend/cache_file.rb +36 -0
  7. data/lib/i18n/backend/cascade.rb +9 -10
  8. data/lib/i18n/backend/chain.rb +95 -42
  9. data/lib/i18n/backend/fallbacks.rb +68 -22
  10. data/lib/i18n/backend/flatten.rb +13 -8
  11. data/lib/i18n/backend/gettext.rb +33 -25
  12. data/lib/i18n/backend/interpolation_compiler.rb +12 -14
  13. data/lib/i18n/backend/key_value.rb +112 -10
  14. data/lib/i18n/backend/lazy_loadable.rb +184 -0
  15. data/lib/i18n/backend/memoize.rb +13 -7
  16. data/lib/i18n/backend/metadata.rb +12 -6
  17. data/lib/i18n/backend/pluralization.rb +64 -25
  18. data/lib/i18n/backend/simple.rb +44 -18
  19. data/lib/i18n/backend/transliterator.rb +43 -33
  20. data/lib/i18n/backend.rb +5 -3
  21. data/lib/i18n/config.rb +91 -10
  22. data/lib/i18n/exceptions.rb +118 -22
  23. data/lib/i18n/gettext/helpers.rb +14 -4
  24. data/lib/i18n/gettext/po_parser.rb +7 -7
  25. data/lib/i18n/gettext.rb +6 -5
  26. data/lib/i18n/interpolate/ruby.rb +53 -0
  27. data/lib/i18n/locale/fallbacks.rb +33 -26
  28. data/lib/i18n/locale/tag/parents.rb +8 -8
  29. data/lib/i18n/locale/tag/rfc4646.rb +0 -2
  30. data/lib/i18n/locale/tag/simple.rb +2 -4
  31. data/lib/i18n/locale.rb +2 -0
  32. data/lib/i18n/middleware.rb +17 -0
  33. data/lib/i18n/tests/basics.rb +58 -0
  34. data/lib/i18n/tests/defaults.rb +52 -0
  35. data/lib/i18n/tests/interpolation.rb +167 -0
  36. data/lib/i18n/tests/link.rb +66 -0
  37. data/lib/i18n/tests/localization/date.rb +122 -0
  38. data/lib/i18n/tests/localization/date_time.rb +103 -0
  39. data/lib/i18n/tests/localization/procs.rb +118 -0
  40. data/lib/i18n/tests/localization/time.rb +103 -0
  41. data/lib/i18n/tests/localization.rb +19 -0
  42. data/lib/i18n/tests/lookup.rb +81 -0
  43. data/lib/i18n/tests/pluralization.rb +35 -0
  44. data/lib/i18n/tests/procs.rb +66 -0
  45. data/lib/i18n/tests.rb +14 -0
  46. data/lib/i18n/utils.rb +55 -0
  47. data/lib/i18n/version.rb +3 -1
  48. data/lib/i18n.rb +200 -87
  49. metadata +64 -56
  50. data/CHANGELOG.textile +0 -135
  51. data/README.textile +0 -93
  52. data/lib/i18n/backend/active_record/missing.rb +0 -65
  53. data/lib/i18n/backend/active_record/store_procs.rb +0 -38
  54. data/lib/i18n/backend/active_record/translation.rb +0 -93
  55. data/lib/i18n/backend/active_record.rb +0 -61
  56. data/lib/i18n/backend/cldr.rb +0 -100
  57. data/lib/i18n/core_ext/hash.rb +0 -29
  58. data/lib/i18n/core_ext/string/interpolate.rb +0 -98
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ef99abbb2ed698cd9dd77ab484720a5ecf68be96a7064f2f8b432a69758ada07
4
+ data.tar.gz: 0a4ade5577fe636c03d6ef9ca11f7a26b80a5c58078ee87c2f9dcf9fdd6882de
5
+ SHA512:
6
+ metadata.gz: 21b666c06e7c8ebdb78088ef2ecd8da866ebd414dff3f3a8535b3e950c12bf0f38695a22acac6da7775e8e245c7a99a0d1d9a6bc409e0ea3d552520a0be26f68
7
+ data.tar.gz: c5f81a0bafbf70daa4a2c4374f3906ef31cea1d8f94d5d5e9b980224d1b6bc75f95cdd3e08709f6cae410c34f4285deb375d7e5b06e19f283743221606e23434
data/MIT-LICENSE CHANGED
File without changes
data/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # Ruby I18n
2
+
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)
5
+
6
+ Ruby internationalization and localization (i18n) solution.
7
+
8
+ Currently maintained by @radar.
9
+
10
+ ## Usage
11
+
12
+ ### Rails
13
+
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
71
+
72
+ * Cache
73
+ * Pluralization: lambda pluralizers stored as translation data
74
+ * Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation)
75
+ * [Gettext support](https://github.com/ruby-i18n/i18n/wiki/Gettext)
76
+ * Translation metadata
77
+
78
+ ## Alternative Backend
79
+
80
+ * Chain
81
+ * ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs)
82
+ * KeyValue (uses active_support/json and cannot store procs)
83
+
84
+ For more information and lots of resources see [the 'Resources' page on the wiki](https://github.com/ruby-i18n/i18n/wiki/Resources).
85
+
86
+ ## Tests
87
+
88
+ You can run tests both with
89
+
90
+ * `rake test` or just `rake`
91
+ * run any test file directly, e.g. `ruby -Ilib:test test/api/simple_test.rb`
92
+
93
+ You can run all tests against all Gemfiles with
94
+
95
+ * `ruby test/run_all.rb`
96
+
97
+ The structure of the test suite is a bit unusual as it uses modules to reuse
98
+ particular tests in different test cases.
99
+
100
+ The reason for this is that we need to enforce the I18n API across various
101
+ combinations of extensions. E.g. the Simple backend alone needs to support
102
+ the same API as any combination of feature and/or optimization modules included
103
+ to the Simple backend. We test this by reusing the same API definition (implemented
104
+ as test methods) in test cases with different setups.
105
+
106
+ You can find the test cases that enforce the API in test/api. And you can find
107
+ the API definition test methods in test/api/tests.
108
+
109
+ All other test cases (e.g. as defined in test/backend, test/core_ext) etc.
110
+ follow the usual test setup and should be easy to grok.
111
+
112
+ ## More Documentation
113
+
114
+ Additional documentation can be found here: https://github.com/ruby-i18n/i18n/wiki
115
+
116
+ ## Contributors
117
+
118
+ * @radar
119
+ * @carlosantoniodasilva
120
+ * @josevalim
121
+ * @knapo
122
+ * @tigrish
123
+ * [and many more](https://github.com/ruby-i18n/i18n/graphs/contributors)
124
+
125
+ ## License
126
+
127
+ MIT License. See the included MIT-LICENSE file.
@@ -1,77 +1,93 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
- require 'i18n/core_ext/hash'
4
+ require 'json'
5
5
 
6
6
  module I18n
7
7
  module Backend
8
8
  module Base
9
9
  include I18n::Backend::Transliterator
10
10
 
11
- RESERVED_KEYS = [:scope, :default, :separator, :resolve]
12
- RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/
13
- DEPRECATED_INTERPOLATION_SYNTAX_PATTERN = /(\\)?\{\{([^\}]+)\}\}/
14
-
15
11
  # Accepts a list of paths to translation files. Loads translations from
16
- # 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
17
13
  # for details.
18
14
  def load_translations(*filenames)
19
- filenames = I18n.load_path.flatten if filenames.empty?
20
- filenames.each { |filename| load_file(filename) }
15
+ filenames = I18n.load_path if filenames.empty?
16
+ filenames.flatten.each do |filename|
17
+ loaded_translations = load_file(filename)
18
+ yield filename, loaded_translations if block_given?
19
+ end
21
20
  end
22
21
 
23
22
  # This method receives a locale, a data hash and options for storing translations.
24
23
  # Should be implemented
25
- def store_translations(locale, data, options = {})
24
+ def store_translations(locale, data, options = EMPTY_HASH)
26
25
  raise NotImplementedError
27
26
  end
28
27
 
29
- def translate(locale, key, options = {})
28
+ def translate(locale, key, options = EMPTY_HASH)
29
+ raise I18n::ArgumentError if (key.is_a?(String) || key.is_a?(Symbol)) && key.empty?
30
30
  raise InvalidLocale.new(locale) unless locale
31
- return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
31
+ return nil if key.nil? && !options.key?(:default)
32
32
 
33
- entry = key && lookup(locale, key, options[:scope], options)
33
+ entry = lookup(locale, key, options[:scope], options) unless key.nil?
34
34
 
35
- if options.empty?
36
- entry = resolve(locale, key, entry, options)
35
+ if entry.nil? && options.key?(:default)
36
+ entry = default(locale, key, options[:default], options)
37
37
  else
38
- count, default = options.values_at(:count, :default)
39
- values = options.except(*RESERVED_KEYS)
40
- entry = entry.nil? && default ?
41
- default(locale, key, default, options) : resolve(locale, key, entry, options)
38
+ entry = resolve_entry(locale, key, entry, options)
42
39
  end
43
40
 
44
- raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
45
- entry = entry.dup if entry.is_a?(String)
41
+ count = options[:count]
42
+
43
+ if entry.nil? && (subtrees? || !count)
44
+ if (options.key?(:default) && !options[:default].nil?) || !options.key?(:default)
45
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
46
+ end
47
+ end
46
48
 
49
+ entry = entry.dup if entry.is_a?(String)
47
50
  entry = pluralize(locale, entry, count) if count
48
- entry = interpolate(locale, entry, values) if values
51
+
52
+ if entry.nil? && !subtrees?
53
+ throw(:exception, I18n::MissingTranslation.new(locale, key, options))
54
+ end
55
+
56
+ deep_interpolation = options[:deep_interpolation]
57
+ values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
58
+ if values && !values.empty?
59
+ entry = if deep_interpolation
60
+ deep_interpolate(locale, entry, values)
61
+ else
62
+ interpolate(locale, entry, values)
63
+ end
64
+ elsif entry.is_a?(String) && entry =~ I18n.reserved_keys_pattern
65
+ raise ReservedInterpolationKey.new($1.to_sym, entry)
66
+ end
49
67
  entry
50
68
  end
51
69
 
70
+ def exists?(locale, key, options = EMPTY_HASH)
71
+ lookup(locale, key, options[:scope]) != nil
72
+ end
73
+
52
74
  # Acts the same as +strftime+, but uses a localized version of the
53
75
  # format string. Takes a key from the date/time formats translations as
54
76
  # a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
55
- def localize(locale, object, format = :default, options = {})
77
+ def localize(locale, object, format = :default, options = EMPTY_HASH)
78
+ if object.nil? && options.include?(:default)
79
+ return options[:default]
80
+ end
56
81
  raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
57
82
 
58
83
  if Symbol === format
59
- key = format
84
+ key = format
60
85
  type = object.respond_to?(:sec) ? 'time' : 'date'
61
- format = I18n.t(:"#{type}.formats.#{key}", options.merge(:raise => true, :object => object, :locale => locale))
62
- end
63
-
64
- # format = resolve(locale, object, format, options)
65
- format = format.to_s.gsub(/%[aAbBp]/) do |match|
66
- case match
67
- when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
68
- when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
69
- when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
70
- when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
71
- when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour
72
- end
86
+ options = options.merge(:raise => true, :object => object, :locale => locale)
87
+ format = I18n.t(:"#{type}.formats.#{key}", **options)
73
88
  end
74
89
 
90
+ format = translate_localization_format(locale, object, format, options)
75
91
  object.strftime(format)
76
92
  end
77
93
 
@@ -82,26 +98,44 @@ module I18n
82
98
  end
83
99
 
84
100
  def reload!
85
- @skip_syntax_deprecation = false
101
+ eager_load! if eager_loaded?
102
+ end
103
+
104
+ def eager_load!
105
+ @eager_loaded = true
86
106
  end
87
107
 
88
108
  protected
89
109
 
110
+ def eager_loaded?
111
+ @eager_loaded ||= false
112
+ end
113
+
90
114
  # The method which actually looks up for the translation in the store.
91
- def lookup(locale, key, scope = [], options = {})
115
+ def lookup(locale, key, scope = [], options = EMPTY_HASH)
92
116
  raise NotImplementedError
93
117
  end
94
118
 
119
+ def subtrees?
120
+ true
121
+ end
122
+
95
123
  # Evaluates defaults.
96
124
  # If given subject is an Array, it walks the array and returns the
97
125
  # first translation that can be resolved. Otherwise it tries to resolve
98
126
  # the translation directly.
99
- def default(locale, object, subject, options = {})
100
- 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
+
101
134
  case subject
102
135
  when Array
103
136
  subject.each do |item|
104
- result = resolve(locale, object, item, options) and return result
137
+ result = resolve(locale, object, item, options)
138
+ return result unless result.nil?
105
139
  end and nil
106
140
  else
107
141
  resolve(locale, object, subject, options)
@@ -112,116 +146,160 @@ module I18n
112
146
  # If the given subject is a Symbol, it will be translated with the
113
147
  # given options. If it is a Proc then it will be evaluated. All other
114
148
  # subjects will be returned directly.
115
- def resolve(locale, object, subject, options = nil)
149
+ def resolve(locale, object, subject, options = EMPTY_HASH)
116
150
  return subject if options[:resolve] == false
117
- case subject
118
- when Symbol
119
- I18n.translate(subject, (options || {}).merge(:locale => locale, :raise => true))
120
- when Proc
121
- date_or_time = options.delete(:object) || object
122
- resolve(locale, object, subject.call(date_or_time, options), options = {})
123
- else
124
- subject
151
+ result = catch(:exception) do
152
+ case subject
153
+ when Symbol
154
+ I18n.translate(subject, **options.merge(:locale => locale, :throw => true))
155
+ when Proc
156
+ date_or_time = options.delete(:object) || object
157
+ resolve(locale, object, subject.call(date_or_time, **options))
158
+ else
159
+ subject
160
+ end
125
161
  end
126
- rescue MissingTranslationData
127
- nil
162
+ result unless result.is_a?(MissingTranslation)
128
163
  end
164
+ alias_method :resolve_entry, :resolve
129
165
 
130
- # Picks a translation from an array according to English pluralization
131
- # rules. It will pick the first translation if count is not equal to 1
132
- # and the second translation if it is equal to 1. Other backends can
133
- # implement more flexible or complex pluralization rules.
166
+ # Picks a translation from a pluralized mnemonic subkey according to English
167
+ # pluralization rules :
168
+ # - It will pick the :one subkey if count is equal to 1.
169
+ # - It will pick the :other subkey otherwise.
170
+ # - It will pick the :zero subkey in the special case where count is
171
+ # equal to 0 and there is a :zero subkey present. This behaviour is
172
+ # not standard with regards to the CLDR pluralization rules.
173
+ # Other backends can implement more flexible or complex pluralization rules.
134
174
  def pluralize(locale, entry, count)
175
+ entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
135
176
  return entry unless entry.is_a?(Hash) && count
136
177
 
137
- key = :zero if count == 0 && entry.has_key?(:zero)
138
- key ||= count == 1 ? :one : :other
139
- raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
178
+ key = pluralization_key(entry, count)
179
+ raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
140
180
  entry[key]
141
181
  end
142
182
 
143
- # Interpolates values into a given string.
183
+ # Interpolates values into a given subject.
144
184
  #
145
- # interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
185
+ # if the given subject is a string then:
186
+ # method interpolates "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
146
187
  # # => "file test.txt opened by %{user}"
147
188
  #
148
- # Note that you have to double escape the <tt>\\</tt> when you want to escape
149
- # the <tt>{{...}}</tt> key in a string (once for the string and once for the
150
- # interpolation).
151
- def interpolate(locale, string, values = {})
152
- return string unless string.is_a?(::String) && !values.empty?
153
-
154
- preserve_encoding(string) do
155
- string = string.gsub(DEPRECATED_INTERPOLATION_SYNTAX_PATTERN) do
156
- escaped, key = $1, $2.to_sym
157
- if escaped
158
- "{{#{key}}}"
159
- else
160
- warn_syntax_deprecation!
161
- "%{#{key}}"
162
- end
163
- end
189
+ # if the given subject is an array then:
190
+ # each element of the array is recursively interpolated (until it finds a string)
191
+ # method interpolates ["yes, %{user}", ["maybe no, %{user}, "no, %{user}"]], :user => "bartuz"
192
+ # # => "["yes, bartuz",["maybe no, bartuz", "no, bartuz"]]"
193
+ def interpolate(locale, subject, values = EMPTY_HASH)
194
+ return subject if values.empty?
164
195
 
165
- values.each do |key, value|
166
- value = value.call(values) if interpolate_lambda?(value, string, key)
167
- value = value.to_s unless value.is_a?(::String)
168
- values[key] = value
169
- end
170
-
171
- string % values
172
- end
173
- rescue KeyError => e
174
- if string =~ RESERVED_KEYS_PATTERN
175
- raise ReservedInterpolationKey.new($1.to_sym, string)
196
+ case subject
197
+ when ::String then I18n.interpolate(subject, values)
198
+ when ::Array then subject.map { |element| interpolate(locale, element, values) }
176
199
  else
177
- raise MissingInterpolationArgument.new(values, string)
200
+ subject
178
201
  end
179
202
  end
180
203
 
181
- def preserve_encoding(string)
182
- if string.respond_to?(:encoding)
183
- encoding = string.encoding
184
- result = yield
185
- result.force_encoding(encoding) if result.respond_to?(:force_encoding)
186
- result
204
+ # Deep interpolation
205
+ #
206
+ # deep_interpolate { people: { ann: "Ann is %{ann}", john: "John is %{john}" } },
207
+ # ann: 'good', john: 'big'
208
+ # #=> { people: { ann: "Ann is good", john: "John is big" } }
209
+ def deep_interpolate(locale, data, values = EMPTY_HASH)
210
+ return data if values.empty?
211
+
212
+ case data
213
+ when ::String
214
+ I18n.interpolate(data, values)
215
+ when ::Hash
216
+ data.each_with_object({}) do |(k, v), result|
217
+ result[k] = deep_interpolate(locale, v, values)
218
+ end
219
+ when ::Array
220
+ data.map do |v|
221
+ deep_interpolate(locale, v, values)
222
+ end
187
223
  else
188
- yield
224
+ data
189
225
  end
190
226
  end
191
227
 
192
- # returns true when the given value responds to :call and the key is
193
- # an interpolation placeholder in the given string
194
- def interpolate_lambda?(object, string, key)
195
- object.respond_to?(:call) && string =~ /%\{#{key}\}|%\<#{key}>.*?\d*\.?\d*[bBdiouxXeEfgGcps]\}/
196
- end
197
-
198
228
  # Loads a single translations file by delegating to #load_rb or
199
229
  # #load_yml depending on the file extension and directly merges the
200
230
  # data to the existing translations. Raises I18n::UnknownFileType
201
231
  # for all other file extensions.
202
232
  def load_file(filename)
203
233
  type = File.extname(filename).tr('.', '').downcase
204
- raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}")
205
- data = send(:"load_#{type}", filename) # TODO raise a meaningful exception if this does not yield a Hash
206
- data.each { |locale, d| store_translations(locale, d) }
234
+ raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
235
+ data, keys_symbolized = send(:"load_#{type}", filename)
236
+ unless data.is_a?(Hash)
237
+ raise InvalidLocaleData.new(filename, 'expects it to return a hash, but does not')
238
+ end
239
+ data.each { |locale, d| store_translations(locale, d || {}, skip_symbolize_keys: keys_symbolized) }
240
+
241
+ data
207
242
  end
208
243
 
209
244
  # Loads a plain Ruby translations file. eval'ing the file must yield
210
245
  # a Hash containing translation data with locales as toplevel keys.
211
246
  def load_rb(filename)
212
- eval(IO.read(filename), binding, filename)
247
+ translations = eval(IO.read(filename), binding, filename)
248
+ [translations, false]
213
249
  end
214
250
 
215
251
  # Loads a YAML translations file. The data must have locales as
216
252
  # toplevel keys.
217
253
  def load_yml(filename)
218
- YAML::load(IO.read(filename))
254
+ begin
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
260
+ rescue TypeError, ScriptError, StandardError => e
261
+ raise InvalidLocaleData.new(filename, e.inspect)
262
+ end
219
263
  end
264
+ alias_method :load_yaml, :load_yml
220
265
 
221
- def warn_syntax_deprecation! #:nodoc:
222
- return if @skip_syntax_deprecation
223
- warn "The {{key}} interpolation syntax in I18n messages is deprecated. Please use %{key} instead.\n#{caller.join("\n")}"
224
- @skip_syntax_deprecation = true
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
280
+
281
+ def translate_localization_format(locale, object, format, options)
282
+ format.to_s.gsub(/%(|\^)[aAbBpP]/) do |match|
283
+ case match
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
294
+ end
295
+ end
296
+ rescue MissingTranslationData => e
297
+ e.message
298
+ end
299
+
300
+ def pluralization_key(entry, count)
301
+ key = :zero if count == 0 && entry.has_key?(:zero)
302
+ key ||= count == 1 ? :one : :other
225
303
  end
226
304
  end
227
305
  end
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  # This module allows you to easily cache all responses from the backend - thus
4
4
  # speeding up the I18n aspects of your application quite a bit.
@@ -6,22 +6,44 @@
6
6
  # To enable caching you can simply include the Cache module to the Simple
7
7
  # backend - or whatever other backend you are using:
8
8
  #
9
- # I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
9
+ # I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
10
10
  #
11
11
  # You will also need to set a cache store implementation that you want to use:
12
12
  #
13
- # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
13
+ # I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
14
14
  #
15
15
  # You can use any cache implementation you want that provides the same API as
16
16
  # ActiveSupport::Cache (only the methods #fetch and #write are being used).
17
17
  #
18
- # The cache_key implementation assumes that you only pass values to
19
- # I18n.translate that return a valid key from #hash (see
20
- # http://www.ruby-doc.org/core/classes/Object.html#M000337).
18
+ # The cache_key implementation by default assumes you pass values that return
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):
23
+ #
24
+ # I18n.cache_key_digest = OpenSSL::Digest::SHA256.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
+ #
21
42
  module I18n
22
43
  class << self
23
44
  @@cache_store = nil
24
45
  @@cache_namespace = nil
46
+ @@cache_key_digest = nil
25
47
 
26
48
  def cache_store
27
49
  @@cache_store
@@ -39,6 +61,14 @@ module I18n
39
61
  @@cache_namespace = namespace
40
62
  end
41
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
+
42
72
  def perform_caching?
43
73
  !cache_store.nil?
44
74
  end
@@ -47,31 +77,37 @@ module I18n
47
77
  module Backend
48
78
  # TODO Should the cache be cleared if new translations are stored?
49
79
  module Cache
50
- def translate(*args)
51
- I18n.perform_caching? ? fetch(*args) { super } : super
80
+ def translate(locale, key, options = EMPTY_HASH)
81
+ I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super
52
82
  end
53
83
 
54
84
  protected
55
85
 
56
- def fetch(*args, &block)
57
- result = I18n.cache_store.fetch(cache_key(*args), &block)
58
- raise result if result.is_a?(Exception)
86
+ def fetch(cache_key, &block)
87
+ result = _fetch(cache_key, &block)
88
+ throw(:exception, result) if result.is_a?(MissingTranslation)
59
89
  result = result.dup if result.frozen? rescue result
60
90
  result
61
- rescue MissingTranslationData => exception
62
- I18n.cache_store.write(cache_key(*args), exception)
63
- raise exception
64
91
  end
65
92
 
66
- def cache_key(*args)
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)
67
102
  # This assumes that only simple, native Ruby values are passed to I18n.translate.
68
- # Also, in Ruby < 1.8.7 {}.hash != {}.hash
69
- # (see http://paulbarry.com/articles/2009/09/14/why-rails-3-will-require-ruby-1-8-7)
70
- # If args.inspect does not work for you for some reason, patches are very welcome :)
71
- hash = RUBY_VERSION >= "1.8.7" ? args.hash : args.inspect
72
- keys = ['i18n', I18n.cache_namespace, hash]
73
- keys.compact.join('-')
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
74
110
  end
75
111
  end
76
112
  end
77
- end
113
+ end