i18n 0.4.0 → 1.14.4

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 (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