tater 1.3.2 → 2.0.0

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