tater 1.3.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -22
  3. data/lib/tater.rb +149 -85
  4. data/test/tater_test.rb +42 -18
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d41ee7aa6cc55a97ad00ee6b96ccbb6127344f9b3e85c4cf3b162f72f3d115d8
4
- data.tar.gz: 2fe1b6c5aff5fdfcc48dbd8212ddbbf0903f9af071c42ff1178c1f20752f536a
3
+ metadata.gz: 16fddfa1cedd9af51e3d2e04422a083a16ee055e1102ad4482bfb28beee3be78
4
+ data.tar.gz: 73273faf3a6bb5842d6adc2854ca98e81363efd0c45835de07fc2e4d807effa2
5
5
  SHA512:
6
- metadata.gz: d686b81192264e1f78f17c0decf0ac3811582e0fe464d2f5f44281481351abca8bd3906fe218a2ae9309a3d84177382f5de7348f63ae3967b7d2af2dc452bcb6
7
- data.tar.gz: 942fd8b6a433b65778f3d7a78f07a8d99c902c5483eec105e0c75689d40db7d0664170963e514d5e78928450b6c0499dce8c3afd63f0eaded6d9843cf3e703a8
6
+ metadata.gz: a9da7fc7081b2043891c83f6ef46c1facdff44f85175ce5052af04a0e6f22bb82ffb0505a29d24a7d2a91ba00e81c5b8fccdb1c6b7118de48680639e474a7f13
7
+ data.tar.gz: 0ccb2a820ea03144b96319c61eb0b645ca86002cf88c657b906669b39702dbdefb83e009a3b34fb5adda4a9fd47deaa30fbd0c0580ff34b80c9e9c5d4051a204
data/README.md CHANGED
@@ -27,7 +27,7 @@ And then execute:
27
27
  bundle
28
28
  ```
29
29
 
30
- Or install it yourself as:
30
+ Or install it yourself by running:
31
31
 
32
32
  ```sh
33
33
  gem install tater
@@ -40,15 +40,20 @@ gem install tater
40
40
  require 'tater'
41
41
 
42
42
  messages = {
43
- 'some' => {
44
- 'key' => 'This here string!'
45
- },
46
- 'interpolated' => 'Hello %{you}!'
43
+ 'en' => {
44
+ 'some' => {
45
+ 'key' => 'This here string!'
46
+ },
47
+ 'interpolated' => 'Hello %{you}!'
48
+ }
47
49
  }
48
50
 
49
- i18n = Tater.new
51
+ i18n = Tater.new(locale: 'en')
50
52
  i18n.load(messages: messages)
51
53
 
54
+ # OR
55
+ i18n = Tater.new(locale: 'en', messages: messages)
56
+
52
57
  # Basic lookup:
53
58
  i18n.translate('some.key') # => 'This here string!'
54
59
 
@@ -57,7 +62,7 @@ i18n.translate('interpolated', you: 'world') # => 'Hello world!'
57
62
  ```
58
63
 
59
64
 
60
- ## Array Localization
65
+ ## Array localization
61
66
 
62
67
  Given an array, Tater will do it's best to join the elements of the array into a
63
68
  sentence based on how many elements there are.
@@ -75,7 +80,7 @@ i18n.localize(%w[tacos enchiladas burritos]) # => "tacos, enchiladas, and burrit
75
80
  ```
76
81
 
77
82
 
78
- ## Numeric Localization
83
+ ## Numeric localization
79
84
 
80
85
  Numeric localization (`Numeric`, `Integer`, `Float`, and `BigDecimal`) require
81
86
  filling in a separator and delimiter. For example:
@@ -100,7 +105,7 @@ i18n.localize(1000.2, delimiter: '_', separator: '+') # => "1_000+20"
100
105
  ```
101
106
 
102
107
 
103
- ## Date and Time Localization
108
+ ## Date and time localization
104
109
 
105
110
  Date and time localization (`Date`, `Time`, and `DateTime`) require filling in
106
111
  all of the needed names and abbreviations for days and months. Here's the
@@ -185,24 +190,26 @@ i18n.localize(Date.new(1970, 1, 1), format: 'day') # => 'jeudi'
185
190
  ```
186
191
 
187
192
 
188
- ## Cascading Lookups
193
+ ## Cascading lookups
189
194
 
190
195
  Lookups can be cascaded, i.e. pieces of the scope of the can be lopped off
191
196
  incrementally.
192
197
 
193
198
  ```ruby
194
199
  messages = {
195
- 'login' => {
196
- 'title' => 'Login',
197
- 'description' => 'Normal description.'
200
+ 'en' => {
201
+ 'login' => {
202
+ 'title' => 'Login',
203
+ 'description' => 'Normal description.'
198
204
 
199
- 'special' => {
200
- 'title' => 'Special Login'
205
+ 'special' => {
206
+ 'title' => 'Special Login'
207
+ }
201
208
  }
202
209
  }
203
210
  }
204
211
 
205
- i18n = Tater.new(messages: messages)
212
+ i18n = Tater.new(locale: 'en', messages: messages)
206
213
  i18n.translate('login.special.title') # => 'Special Login'
207
214
  i18n.translate('login.special.description') # => 'Tater lookup failed'
208
215
 
@@ -237,14 +244,14 @@ Tater.new.translate('nope', default: 'Yep!') # => 'Yep!'
237
244
  ```
238
245
 
239
246
 
240
- ## Procs and Messages in Ruby
247
+ ## Procs and messages in Ruby
241
248
 
242
249
  Ruby files can be used to store messages in addition to YAML, so long as the
243
250
  Ruby file returns a `Hash` when evalled.
244
251
 
245
252
  ```ruby
246
253
  {
247
- en: {
254
+ 'en' => {
248
255
  ruby: proc do |key, options = {}|
249
256
  "Hey #{ key }!"
250
257
  end
@@ -253,10 +260,11 @@ Ruby file returns a `Hash` when evalled.
253
260
  ```
254
261
 
255
262
 
256
- ## Multiple Locales
263
+ ## Multiple locales
257
264
 
258
- If you like to check multiple locales and pull the first matching one out, you
259
- can pass the `:locales` option an array of top-level locale keys.
265
+ If you would like to check multiple locales and pull the first matching one out,
266
+ you can pass the `:locales` option to initialization or the `translate` method
267
+ with an array of top-level locale keys.
260
268
 
261
269
  ```ruby
262
270
  messages = {
@@ -272,9 +280,14 @@ messages = {
272
280
  i18n = Tater.new(messages: messages)
273
281
  i18n.translate('title', locales: %w[fr en]) # => 'la connexion'
274
282
  i18n.translate('description', locales: %w[fr en]) # => 'English description.'
283
+
284
+ # OR
285
+ i18n = Tater.new(messages: messages, locales: %w[fr en])
286
+ i18n.translate('title') # => 'la connexion'
287
+ i18n.translate('description') # => 'English description.'
275
288
  ```
276
289
 
277
- Locales will tried in order and which one matches first will be returned.
290
+ Locales will be tried in order and whichever one matches first will be returned.
278
291
 
279
292
 
280
293
  ## Limitations
@@ -12,14 +12,14 @@ class Tater
12
12
  # Merge all the way down.
13
13
  #
14
14
  # @param to [Hash]
15
- # The target Hash to merge into. Note that modification is done in-place,
16
- # not on a copy of the object.
15
+ # The target Hash to merge into.
17
16
  # @param from [Hash]
18
17
  # The Hash to copy values from.
19
- def self.deep_merge!(to, from)
20
- to.merge!(from) do |_key, left, right|
18
+ # @return [Hash]
19
+ def self.deep_merge(to, from)
20
+ to.merge(from) do |_key, left, right|
21
21
  if left.is_a?(Hash) && right.is_a?(Hash)
22
- Utils.deep_merge!(left, right)
22
+ Utils.deep_merge(left, right)
23
23
  else
24
24
  right
25
25
  end
@@ -29,18 +29,32 @@ class Tater
29
29
  # Transform keys all the way down.
30
30
  #
31
31
  # @param hash [Hash]
32
- # The Hash to modify. Note that modification is done in-place, not on a copy
33
- # of the object.
34
- def self.deep_stringify_keys!(hash)
35
- hash.transform_keys!(&:to_s).transform_values! do |value|
32
+ # The Hash to stringify keys for.
33
+ # @return [Hash]
34
+ def self.deep_stringify_keys(hash)
35
+ hash.transform_keys(&:to_s).transform_values do |value|
36
36
  if value.is_a?(Hash)
37
- Utils.deep_stringify_keys!(value)
37
+ Utils.deep_stringify_keys(value)
38
38
  else
39
39
  value
40
40
  end
41
41
  end
42
42
  end
43
43
 
44
+ # Freeze all the way down.
45
+ #
46
+ # @param hash [Hash]
47
+ # @return [Hash]
48
+ def self.deep_freeze(hash)
49
+ hash.transform_keys(&:freeze).transform_values do |value|
50
+ if value.is_a?(Hash)
51
+ Utils.deep_freeze(value)
52
+ else
53
+ value.freeze
54
+ end
55
+ end.freeze
56
+ end
57
+
44
58
  # Try to interpolate these things, if one of them is a string.
45
59
  #
46
60
  # @param string [String]
@@ -49,7 +63,7 @@ class Tater
49
63
  # The values to interpolate into the target string.
50
64
  #
51
65
  # @return [String]
52
- def self.interpolate(string, options = {})
66
+ def self.interpolate(string, options = HASH)
53
67
  return string unless string.is_a?(String)
54
68
  return string if options.empty?
55
69
 
@@ -72,8 +86,8 @@ class Tater
72
86
  end
73
87
 
74
88
  DEFAULT = 'default'
75
- DEFAULT_LOCALE = 'en'
76
89
  DELIMITING_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/.freeze
90
+ HASH = {}.freeze
77
91
  SEPARATOR = '.'
78
92
  SUBSTITUTION_REGEX = /%(|\^)[aAbBpP]/.freeze
79
93
 
@@ -83,7 +97,16 @@ class Tater
83
97
  # @return [Hash]
84
98
  attr_reader :messages
85
99
 
86
- def initialize(path: nil, messages: nil, locale: DEFAULT_LOCALE, cascade: false)
100
+ # @param cascade [Boolean]
101
+ # A boolean indicating if lookups should cascade by default.
102
+ # @param locale [String]
103
+ # The default locale.
104
+ # @param messages [Hash]
105
+ # A hash of messages ready to be loaded in.
106
+ # @param path [String]
107
+ # A path to search for YAML or Ruby files to load messages from.
108
+ def initialize(cascade: false, locale: nil, messages: nil, path: nil)
109
+ @cache = {}
87
110
  @cascade = cascade
88
111
  @locale = locale
89
112
  @messages = {}
@@ -99,11 +122,11 @@ class Tater
99
122
  @cascade
100
123
  end
101
124
 
102
- # An array of the available locale codes.
125
+ # An array of the available locale codes found in loaded messages.
103
126
  #
104
127
  # @return [Array]
105
128
  def available
106
- messages.keys.map(&:to_s)
129
+ @available ||= messages.keys
107
130
  end
108
131
 
109
132
  # Is this locale available in our current set of messages?
@@ -114,24 +137,36 @@ class Tater
114
137
  end
115
138
 
116
139
  # Load messages into our internal cache, either from a path containing YAML
117
- # files or a collection of messages.
140
+ # files or a Hash of messages.
118
141
  #
119
142
  # @param path [String]
120
143
  # A path to search for YAML or Ruby files to load messages from.
121
144
  # @param messages [Hash]
122
145
  # A hash of messages ready to be loaded in.
123
146
  def load(path: nil, messages: nil)
147
+ return if path.nil? && messages.nil?
148
+
124
149
  if path
125
150
  Dir.glob(File.join(path, '**', '*.{yml,yaml}')).each do |file|
126
- Utils.deep_merge!(@messages, YAML.load_file(file))
151
+ @messages = Utils.deep_merge(@messages, YAML.load_file(file))
127
152
  end
128
153
 
129
154
  Dir.glob(File.join(path, '**', '*.rb')).each do |file|
130
- Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(eval(IO.read(file), binding, file))) # rubocop:disable Security/Eval
155
+ @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(IO.read(file), binding, file))) # rubocop:disable Security/Eval
131
156
  end
132
157
  end
133
158
 
134
- Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(messages)) if messages
159
+ @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(messages)) if messages
160
+ @messages = Utils.deep_freeze(@messages)
161
+
162
+ # Gotta recalculate available locales after updating.
163
+ remove_instance_variable(:@available) if instance_variable_defined?(:@available)
164
+
165
+ # Not only does this clear our cache but it establishes the basic structure
166
+ # that we rely on in other methods.
167
+ @messages.each_key do |key|
168
+ @cache[key] = { true => {}, false => {} }
169
+ end
135
170
  end
136
171
 
137
172
  # Set the current locale, if it's available.
@@ -144,7 +179,7 @@ class Tater
144
179
 
145
180
  # Localize an Array, Date, Time, DateTime, or Numeric object.
146
181
  #
147
- # @param object [Date, Time, DateTime, Numeric]
182
+ # @param object [Array<String>, Date, Time, DateTime, Numeric]
148
183
  # The object to localize.
149
184
  # @param options [Hash]
150
185
  # Options to configure localization.
@@ -167,17 +202,14 @@ class Tater
167
202
  #
168
203
  # @return [String]
169
204
  # A localized version of the object passed in.
170
- def localize(object, options = {})
171
- format_key = options.delete(:format) || DEFAULT
172
- locale_override = options.delete(:locale)
173
-
205
+ def localize(object, options = HASH)
174
206
  case object
175
207
  when String
176
208
  object
177
209
  when Numeric
178
- delimiter = options.delete(:delimiter) || lookup('numeric.delimiter', locale_override)
179
- separator = options.delete(:separator) || lookup('numeric.separator', locale_override)
180
- precision = options.delete(:precision) || 2
210
+ delimiter = options[:delimiter] || lookup('numeric.delimiter', locale: options[:locale])
211
+ separator = options[:separator] || lookup('numeric.separator', locale: options[:locale])
212
+ precision = options[:precision] || 2
181
213
 
182
214
  raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing or not passed as option :delimiter") unless delimiter
183
215
  raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing or not passed as option :separator") unless separator
@@ -194,23 +226,22 @@ class Tater
194
226
  [integer, fraction&.ljust(precision, '0')].compact.join(separator)
195
227
  end
196
228
  when Date, Time, DateTime
197
- key = object.class.to_s.downcase
198
- format = lookup("#{ key }.formats.#{ format_key }", locale_override) || format_key
229
+ format = lookup("#{ object.class.to_s.downcase }.formats.#{ options[:format] || DEFAULT }", locale: options[:locale]) || options[:format] || DEFAULT
199
230
 
200
231
  # Heavily cribbed from I18n, many thanks to the people who sorted this out
201
232
  # before I worked on this library.
202
233
  format = format.gsub(SUBSTITUTION_REGEX) do |match|
203
234
  case match
204
- when '%a' then lookup('date.abbreviated_days', locale_override)[object.wday]
205
- when '%^a' then lookup('date.abbreviated_days', locale_override)[object.wday].upcase
206
- when '%A' then lookup('date.days', locale_override)[object.wday]
207
- when '%^A' then lookup('date.days', locale_override)[object.wday].upcase
208
- when '%b' then lookup('date.abbreviated_months', locale_override)[object.mon - 1]
209
- when '%^b' then lookup('date.abbreviated_months', locale_override)[object.mon - 1].upcase
210
- when '%B' then lookup('date.months', locale_override)[object.mon - 1]
211
- when '%^B' then lookup('date.months', locale_override)[object.mon - 1].upcase
212
- when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale_override).upcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
213
- when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale_override).downcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
235
+ when '%a' then lookup('date.abbreviated_days', locale: options[:locale])[object.wday]
236
+ when '%^a' then lookup('date.abbreviated_days', locale: options[:locale])[object.wday].upcase
237
+ when '%A' then lookup('date.days', locale: options[:locale])[object.wday]
238
+ when '%^A' then lookup('date.days', locale: options[:locale])[object.wday].upcase
239
+ when '%b' then lookup('date.abbreviated_months', locale: options[:locale])[object.mon - 1]
240
+ when '%^b' then lookup('date.abbreviated_months', locale: options[:locale])[object.mon - 1].upcase
241
+ when '%B' then lookup('date.months', locale: options[:locale])[object.mon - 1]
242
+ when '%^B' then lookup('date.months', locale: options[:locale])[object.mon - 1].upcase
243
+ when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale: options[:locale]).upcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
244
+ when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale: options[:locale]).downcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
214
245
  end
215
246
  end
216
247
 
@@ -222,14 +253,14 @@ class Tater
222
253
  when 1
223
254
  object[0]
224
255
  when 2
225
- two_words_connector = options.delete(:two_words_connector) || lookup('array.two_words_connector', locale_override)
256
+ two_words_connector = options[:two_words_connector] || lookup('array.two_words_connector', locale: options[:locale])
226
257
 
227
258
  raise(MissingLocalizationFormat, "Sentence localization connector ('array.two_words_connector') missing or not passed as option :two_words_connector") unless two_words_connector
228
259
 
229
260
  "#{ object[0] }#{ two_words_connector }#{ object[1] }"
230
261
  else
231
- last_word_connector = options.delete(:last_word_connector) || lookup('array.last_word_connector', locale_override)
232
- words_connector = options.delete(:words_connector) || lookup('array.words_connector', locale_override)
262
+ last_word_connector = options[:last_word_connector] || lookup('array.last_word_connector', locale: options[:locale])
263
+ words_connector = options[:words_connector] || lookup('array.words_connector', locale: options[:locale])
233
264
 
234
265
  raise(MissingLocalizationFormat, "Sentence localization connector ('array.last_word_connector') missing or not passed as option :last_word_connector") unless last_word_connector
235
266
  raise(MissingLocalizationFormat, "Sentence localization connector ('array.words_connector') missing or not passed as option :words_connector") unless words_connector
@@ -245,26 +276,36 @@ class Tater
245
276
  # Lookup a key in the messages hash, using the current locale or an override.
246
277
  #
247
278
  # @param key [String]
248
- # @param locale_override [String]
279
+ # @param locale [String]
249
280
  # A locale to use instead of our current one.
250
- # @param cascade_override [Boolean]
281
+ # @param cascade [Boolean]
251
282
  # A boolean to forcibly set the cascade option for this lookup.
252
283
  #
253
284
  # @return
254
- # Basically anything that can be stored in YAML, including nil.
255
- def lookup(key, locale_override = nil, cascade_override = nil)
256
- path = key.split(SEPARATOR).prepend(locale_override || locale).map(&:to_s)
285
+ # Basically anything that can be stored in your messages Hash.
286
+ def lookup(key, locale: nil, cascade: nil)
287
+ locale = locale.nil? ? @locale : locale
288
+ cascade = cascade.nil? ? @cascade : cascade
257
289
 
258
- if cascade_override.nil? ? @cascade : cascade_override
259
- while path.length >= 2
260
- attempt = @messages.dig(*path)
290
+ cached(key, locale, cascade) || begin
291
+ return nil unless @messages.key?(locale.to_s)
261
292
 
262
- break attempt if attempt
293
+ path = key.split(SEPARATOR).prepend(locale).map(&:to_s)
263
294
 
264
- path.delete_at(path.length - 2)
265
- end
266
- else
267
- @messages.dig(*path)
295
+ message =
296
+ if cascade
297
+ while path.length >= 2
298
+ attempt = @messages.dig(*path)
299
+
300
+ break attempt unless attempt.nil?
301
+
302
+ path.delete_at(path.length - 2)
303
+ end
304
+ else
305
+ @messages.dig(*path)
306
+ end
307
+
308
+ cache(key, locale, cascade, message)
268
309
  end
269
310
  end
270
311
 
@@ -284,20 +325,18 @@ class Tater
284
325
  # An array of locales to look within.
285
326
  #
286
327
  # @return [Boolean]
287
- def includes?(key, options = {})
288
- cascade_override = options.delete(:cascade)
289
- locale_override = options.delete(:locale)
290
- locales = options.delete(:locales)
291
-
328
+ def includes?(key, options = HASH)
292
329
  message =
293
- if locale_override || !locales
294
- lookup(key, locale_override, cascade_override)
295
- else
296
- locales.find do |accept|
297
- found = lookup(key, accept, cascade_override)
330
+ if options.key?(:locales)
331
+ options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
298
332
 
299
- break found if found
333
+ options[:locales].find do |accept|
334
+ found = lookup(key, locale: accept, cascade: options[:cascade])
335
+
336
+ break found unless found.nil?
300
337
  end
338
+ else
339
+ lookup(key, locale: options[:locale], cascade: options[:cascade])
301
340
  end
302
341
 
303
342
  !message.nil?
@@ -307,7 +346,7 @@ class Tater
307
346
  # It's effectively a combination of #lookup and #interpolate.
308
347
  #
309
348
  # @example
310
- # Tater.new(messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
349
+ # Tater.new(locale: 'en', messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
311
350
  #
312
351
  # @param key [String]
313
352
  # The period-separated key path to look within our messages for.
@@ -319,36 +358,61 @@ class Tater
319
358
  # @option options [String] :default
320
359
  # A default string to return, should lookup fail.
321
360
  # @option options [String] :locale
322
- # A specific locale to lookup within. This will take precedence over the
323
- # :locales option.
361
+ # A specific locale to lookup within.
324
362
  # @option options [Array<String>] :locales
325
- # An array of locales to look within.
363
+ # An array of locales to look within. This will take precedence over the
364
+ # :locale option and will append the default :locale option passed during
365
+ # initialization if present.
326
366
  #
327
367
  # @return [String]
328
368
  # The translated and interpreted string, if found, or any data at the
329
369
  # defined key.
330
- def translate(key, options = {})
331
- cascade_override = options.delete(:cascade)
332
- locale_override = options.delete(:locale)
333
- locales = options.delete(:locales)
334
-
370
+ def translate(key, options = HASH)
335
371
  message =
336
- if locale_override || !locales
337
- lookup(key, locale_override, cascade_override)
338
- else
339
- locales.find do |accept|
340
- found = lookup(key, accept, cascade_override)
372
+ if options.key?(:locales)
373
+ options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
374
+
375
+ options[:locales].find do |accept|
376
+ found = lookup(key, locale: accept, cascade: options[:cascade])
341
377
 
342
- break found if found
378
+ break found unless found.nil?
343
379
  end
380
+ else
381
+ lookup(key, locale: options[:locale], cascade: options[:cascade])
344
382
  end
345
383
 
346
384
  # Call procs that should return a string.
347
- if message.is_a?(Proc)
348
- message = message.call(key, options)
349
- end
385
+ message = message.call(key, options) if message.is_a?(Proc)
350
386
 
351
- Utils.interpolate(message, options) || options.delete(:default) { "Tater lookup failed: #{ locale_override || locales || locale }.#{ key }" }
387
+ Utils.interpolate(message, options) || options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
352
388
  end
353
389
  alias t translate
390
+
391
+ private
392
+
393
+ # @param key [String]
394
+ # The cache key, often in the form "something.nested.like.this"
395
+ # @param locale [String]
396
+ # The locale to store the value for.
397
+ # @param cascade [Boolean]
398
+ # Was this a cascading lookup?
399
+ # @param message [String]
400
+ # The message being cached, often a String.
401
+ # @return [String]
402
+ # Whatever value is being cached, often a String.
403
+ def cache(key, locale, cascade, message)
404
+ @cache[locale][cascade][key] = message
405
+ end
406
+
407
+ # @param key [String]
408
+ # The cache key, often in the form "something.nested.like.this"
409
+ # @param locale [String]
410
+ # The locale to store the value for.
411
+ # @param cascade [Boolean]
412
+ # Was this a cascading lookup?
413
+ # @return [String, nil]
414
+ # The cached message or nil.
415
+ def cached(key, locale, cascade)
416
+ @cache.dig(locale, cascade, key)
417
+ end
354
418
  end
@@ -5,24 +5,34 @@ require 'date'
5
5
 
6
6
  describe Tater do
7
7
  describe Tater::Utils do
8
- describe '#deep_merge!' do
9
- it 'deeply merges two hashes, modifying the first' do
8
+ describe '#deep_merge' do
9
+ it 'deeply merges two hashes, returning a new one' do
10
10
  first = { 'one' => 'one', 'two' => { 'three' => 'three' } }
11
11
  second = { 'two' => { 'four' => 'four' } }
12
12
 
13
- Tater::Utils.deep_merge!(first, second)
13
+ third = Tater::Utils.deep_merge(first, second)
14
14
 
15
- assert_equal({ 'one' => 'one', 'two' => { 'three' => 'three', 'four' => 'four' } }, first)
15
+ assert_equal({ 'one' => 'one', 'two' => { 'three' => 'three', 'four' => 'four' } }, third)
16
16
  end
17
17
  end
18
18
 
19
- describe '#deep_stringify_keys!' do
20
- it 'converts all keys into strings, recursively modifying the hash passed in' do
19
+ describe '#deep_stringify_keys' do
20
+ it 'converts all keys into strings, recursively' do
21
21
  start = { en: { login: { title: 'Hello!' } } }
22
+ finish = Tater::Utils.deep_stringify_keys(start)
22
23
 
23
- Tater::Utils.deep_stringify_keys!(start)
24
+ assert_equal({ 'en' => { 'login' => { 'title' => 'Hello!' } } }, finish)
25
+ end
26
+ end
24
27
 
25
- assert_equal({ 'en' => { 'login' => { 'title' => 'Hello!' } } }, start)
28
+ describe '#deep_freeze' do
29
+ it 'freezes the keys and values, recursively' do
30
+ start = Tater::Utils.deep_stringify_keys({ en: { login: { title: 'Hello!' } } })
31
+ finish = Tater::Utils.deep_freeze(start)
32
+
33
+ assert finish.frozen?
34
+ assert finish.keys.all?(&:frozen?)
35
+ assert finish.values.all?(&:frozen?)
26
36
  end
27
37
  end
28
38
 
@@ -46,14 +56,14 @@ describe Tater do
46
56
  it 'converts numerics to decimal-ish strings' do
47
57
  assert_equal '1', Tater::Utils.string_from_numeric(1)
48
58
  assert_equal '1.0', Tater::Utils.string_from_numeric(1.0)
49
- assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal(1))
59
+ assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal('1'))
50
60
  end
51
61
  end
52
62
  end
53
63
 
54
64
  describe '#available?' do
55
65
  let :i18n do
56
- Tater.new(path: File.expand_path('test/fixtures'))
66
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
57
67
  end
58
68
 
59
69
  it 'tells you if the locale is available' do
@@ -88,6 +98,14 @@ describe Tater do
88
98
 
89
99
  assert_instance_of(Hash, i18n.messages)
90
100
  end
101
+
102
+ it 'freezes messages after loading' do
103
+ i18n = Tater.new(messages: { 'hey' => 'Oh hi' })
104
+
105
+ assert i18n.messages.frozen?
106
+ assert i18n.messages.keys.all?(&:frozen?)
107
+ assert i18n.messages.values.all?(&:frozen?)
108
+ end
91
109
  end
92
110
 
93
111
  describe '#available' do
@@ -98,11 +116,17 @@ describe Tater do
98
116
  it 'returns an array with the available locales (i.e. the top-level keys in our messages hash)' do
99
117
  assert_equal %w[en delimiter_only separator_only fr].sort, i18n.available.sort
100
118
  end
119
+
120
+ it 'updates the available list when new messages are loaded' do
121
+ i18n.load(messages: { 'added' => { 'hey' => 'yeah' }})
122
+
123
+ assert_equal %w[en delimiter_only separator_only fr added].sort, i18n.available.sort
124
+ end
101
125
  end
102
126
 
103
127
  describe '#lookup' do
104
128
  let :i18n do
105
- Tater.new(path: File.expand_path('test/fixtures'))
129
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
106
130
  end
107
131
 
108
132
  it 'returns keys from messages' do
@@ -118,16 +142,16 @@ describe Tater do
118
142
  end
119
143
 
120
144
  it 'cascades' do
121
- assert_equal 'Delicious', i18n.lookup('cascade.nope.tacos', nil, true)
122
- assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy', nil, true)
123
- assert_nil i18n.lookup('cascade.another.nope.crazy', nil, false)
145
+ assert_equal 'Delicious', i18n.lookup('cascade.nope.tacos', cascade: true)
146
+ assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy', cascade: true)
147
+ assert_nil i18n.lookup('cascade.another.nope.crazy', cascade: false)
124
148
  assert_nil i18n.lookup('cascade.nahhhhhh')
125
149
  end
126
150
  end
127
151
 
128
152
  describe '#translate' do
129
153
  let :i18n do
130
- Tater.new(path: File.expand_path('test/fixtures'))
154
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
131
155
  end
132
156
 
133
157
  it 'translates strings' do
@@ -185,7 +209,7 @@ describe Tater do
185
209
 
186
210
  describe '#localize' do
187
211
  let :i18n do
188
- Tater.new(path: File.expand_path('test/fixtures'))
212
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
189
213
  end
190
214
 
191
215
  let :fr do
@@ -336,7 +360,7 @@ describe Tater do
336
360
 
337
361
  describe '#locale=' do
338
362
  let :i18n do
339
- Tater.new(path: File.expand_path('test/fixtures'))
363
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
340
364
  end
341
365
 
342
366
  it 'overrides the locale when available' do
@@ -370,7 +394,7 @@ describe Tater do
370
394
 
371
395
  describe '#includes?' do
372
396
  let :i18n do
373
- Tater.new(path: File.expand_path('test/fixtures'))
397
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
374
398
  end
375
399
 
376
400
  it 'tells you if you have a translation' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tater
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Lecklider
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-06 00:00:00.000000000 Z
11
+ date: 2020-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest