tater 1.3.0 → 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6fe610aeeb910795f9f07f52bdbfa529a6f10ac5b807a8252a0d0f50e24ca6a6
4
- data.tar.gz: a4154b7803f31babbb012f2f65f36b16f4ea929a43f30a9388b0be10d717f1d7
3
+ metadata.gz: eb7166fcdf9b548f8837e4411bfe5d83712f202e1de27c299fa0e5694b50bc2b
4
+ data.tar.gz: ab3f53e56fa8538f39ce59ba9f2e42496e5bea116a880fc26e23ad2091e5e93c
5
5
  SHA512:
6
- metadata.gz: e478a0bb35614afcf58b79f4d2511f02a25772decbf5e1ea5f4ccbb574ea1699fb7f13bc7174aca5ee12cb6fe0130f7a5466a069cc1fca0435569a6e5db2c58d
7
- data.tar.gz: f7f31381024e298d33cdf53ae6609218ab671d27c2fd3c0f1e4f8595a28d87f2f46b6f9d03b27e2c186aec6fc4c4a707850287ca98420ff2ef7a2444d8c7fef9
6
+ metadata.gz: ceb821ff34a086711feb0df9e21a7aa4c7891a3e84fc52a9cf62d5972462c6ab0a5f81eef2ab820ef95df7211dda82e2a56299a1ced017e468c50358b6e537db
7
+ data.tar.gz: 2ad3f4c078bb63e39b5650254f92cb3859c39c73f4eaf2dfae18320e60712bf2c632e42c38770c39a456ee62f13fbf6262e1fef527846c17c53d7ebd859fa6de
@@ -1,112 +1,111 @@
1
- # Tater
1
+ * Tater
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/tater.svg)](https://badge.fury.io/rb/tater)
4
- [![Build Status](https://secure.travis-ci.org/evanleck/tater.svg)](https://travis-ci.org/evanleck/tater)
3
+ [[https://badge.fury.io/rb/tater][https://badge.fury.io/rb/tater.svg]]
5
4
 
6
5
  Tater is an internationalization (i18n) and localization (l10n) library designed
7
6
  for simplicity. It doesn't do everything that other libraries do, but that's by
8
7
  design.
9
8
 
10
- Under the hood, Tater uses a Hash to store the messages, the `dig` method for
11
- lookups, `strftime` for date and time localizations, and `format` for
9
+ Under the hood, Tater uses a Hash to store the messages, the =dig= method for
10
+ lookups, =strftime= for date and time localizations, and =format= for
12
11
  interpolation. That's probably 90% of what Tater does.
13
12
 
14
-
15
- ## Installation
13
+ ** Installation
16
14
 
17
15
  Tater requires Ruby 2.5 or higher. To install Tater, add this line to your
18
16
  application's Gemfile (or gems.rb):
19
17
 
20
- ```ruby
18
+ #+begin_src ruby
21
19
  gem 'tater'
22
- ```
20
+ #+end_src
23
21
 
24
22
  And then execute:
25
23
 
26
- ```sh
24
+ #+begin_src sh
27
25
  bundle
28
- ```
26
+ #+end_src
29
27
 
30
- Or install it yourself as:
28
+ Or install it yourself by running:
31
29
 
32
- ```sh
30
+ #+begin_src sh
33
31
  gem install tater
34
- ```
35
-
32
+ #+end_src
36
33
 
37
- ## Usage
34
+ ** Usage
38
35
 
39
- ```ruby
36
+ #+begin_src ruby
40
37
  require 'tater'
41
38
 
42
39
  messages = {
43
- 'some' => {
44
- 'key' => 'This here string!'
45
- },
46
- 'interpolated' => 'Hello %{you}!'
40
+ 'en' => {
41
+ 'some' => {
42
+ 'key' => 'This here string!'
43
+ },
44
+ 'interpolated' => 'Hello %{you}!'
45
+ }
47
46
  }
48
47
 
49
- i18n = Tater.new
48
+ i18n = Tater.new(locale: 'en')
50
49
  i18n.load(messages: messages)
51
50
 
51
+ # OR
52
+ i18n = Tater.new(locale: 'en', messages: messages)
53
+
52
54
  # Basic lookup:
53
55
  i18n.translate('some.key') # => 'This here string!'
54
56
 
55
57
  # Interpolation:
56
58
  i18n.translate('interpolated', you: 'world') # => 'Hello world!'
57
- ```
58
-
59
+ #+end_src
59
60
 
60
- ## Array Localization
61
+ ** Array localization
61
62
 
62
63
  Given an array, Tater will do it's best to join the elements of the array into a
63
64
  sentence based on how many elements there are.
64
65
 
65
- ```yaml
66
+ #+begin_example
66
67
  en:
67
68
  array:
68
69
  last_word_connector: ", and "
69
70
  two_words_connector: " and "
70
71
  words_connector: ", "
71
- ```
72
+ #+end_example
72
73
 
73
- ```ruby
74
+ #+begin_src ruby
74
75
  i18n.localize(%w[tacos enchiladas burritos]) # => "tacos, enchiladas, and burritos"
75
- ```
76
-
76
+ #+end_src
77
77
 
78
- ## Numeric Localization
78
+ ** Numeric localization
79
79
 
80
- Numeric localization (`Numeric`, `Integer`, `Float`, and `BigDecimal`) require
80
+ Numeric localization (=Numeric=, =Integer=, =Float=, and =BigDecimal=) require
81
81
  filling in a separator and delimiter. For example:
82
82
 
83
- ```yaml
83
+ #+begin_example
84
84
  en:
85
85
  numeric:
86
86
  delimiter: ','
87
87
  separator: '.'
88
- ```
88
+ #+end_example
89
89
 
90
90
  With that, you can do things like this:
91
91
 
92
- ```ruby
92
+ #+begin_src ruby
93
93
  i18n.localize(1000.2) # => "1,000.20"
94
- ```
94
+ #+end_src
95
95
 
96
96
  The separator and delimiter can also be passed in per-call:
97
97
 
98
- ```ruby
98
+ #+begin_src ruby
99
99
  i18n.localize(1000.2, delimiter: '_', separator: '+') # => "1_000+20"
100
- ```
100
+ #+end_src
101
101
 
102
+ ** Date and time localization
102
103
 
103
- ## Date and Time Localization
104
-
105
- Date and time localization (`Date`, `Time`, and `DateTime`) require filling in
104
+ Date and time localization (=Date=, =Time=, and =DateTime=) require filling in
106
105
  all of the needed names and abbreviations for days and months. Here's the
107
106
  example for French, which is used in the tests.
108
107
 
109
- ```yaml
108
+ #+begin_example
110
109
  fr:
111
110
  time:
112
111
  am: 'am'
@@ -169,96 +168,95 @@ fr:
169
168
  - oct.
170
169
  - nov.
171
170
  - déc.
172
- ```
171
+ #+end_example
173
172
 
174
- The statically defined keys for dates are `days`, `abbreviated_days`, `months`,
175
- and `abbreviated_months`. Only `am` and `pm` are needed for times and only if
176
- you plan on using the `%p` or `%P` format strings.
173
+ The statically defined keys for dates are =days=, =abbreviated_days=, =months=,
174
+ and =abbreviated_months=. Only =am= and =pm= are needed for times and only if
175
+ you plan on using the =%p= or =%P= format strings.
177
176
 
178
177
  With all of that, you can do something like:
179
178
 
180
- ```ruby
179
+ #+begin_src ruby
181
180
  i18n.localize(Date.new(1970, 1, 1), format: '%A') # => 'jeudi'
182
181
 
183
182
  # Or, using a key defined in "formats":
184
183
  i18n.localize(Date.new(1970, 1, 1), format: 'day') # => 'jeudi'
185
- ```
186
-
184
+ #+end_src
187
185
 
188
- ## Cascading Lookups
186
+ ** Cascading lookups
189
187
 
190
- Lookups can be cascaded, i.e. pieces of the scope of the can be lopped off
188
+ Lookups can be cascaded, i.e. pieces of the scope of the can be lopped off
191
189
  incrementally.
192
190
 
193
- ```ruby
191
+ #+begin_src ruby
194
192
  messages = {
195
- 'login' => {
196
- 'title' => 'Login',
197
- 'description' => 'Normal description.'
193
+ 'en' => {
194
+ 'login' => {
195
+ 'title' => 'Login',
196
+ 'description' => 'Normal description.'
198
197
 
199
- 'special' => {
200
- 'title' => 'Special Login'
198
+ 'special' => {
199
+ 'title' => 'Special Login'
200
+ }
201
201
  }
202
202
  }
203
203
  }
204
204
 
205
- i18n = Tater.new(messages: messages)
205
+ i18n = Tater.new(locale: 'en', messages: messages)
206
206
  i18n.translate('login.special.title') # => 'Special Login'
207
207
  i18n.translate('login.special.description') # => 'Tater lookup failed'
208
208
 
209
209
  i18n.translate('login.special.description', cascade: true) # => 'Normal description.'
210
- ```
210
+ #+end_src
211
211
 
212
212
  With cascade, the final key stays the same, but pieces of the scope get lopped
213
213
  off. In this case, lookups will be tried in this order:
214
214
 
215
- 1. `'login.special.description'`
216
- 2. `'login.description'`
215
+ 1. ='login.special.description'=
216
+ 2. ='login.description'=
217
217
 
218
218
  This can be useful when you want to override some messages but don't want to
219
219
  have to copy all of the other, non-overwritten messages.
220
220
 
221
221
  Cascading can also be enabled by default when initializing an instance of Tater.
222
222
 
223
- ```ruby
223
+ #+begin_src ruby
224
224
  Tater.new(cascade: true)
225
- ```
225
+ #+end_src
226
226
 
227
227
  Cascading is off by default.
228
228
 
229
-
230
- ## Defaults
229
+ ** Defaults
231
230
 
232
231
  If you'd like to default to another value in case of a missed lookup, you can
233
- provide the `:default` option to `#translate`.
232
+ provide the =:default= option to =#translate=.
234
233
 
235
- ```ruby
234
+ #+begin_src ruby
236
235
  Tater.new.translate('nope', default: 'Yep!') # => 'Yep!'
237
- ```
238
-
236
+ #+end_src
239
237
 
240
- ## Procs and Messages in Ruby
238
+ ** Procs and messages in Ruby
241
239
 
242
240
  Ruby files can be used to store messages in addition to YAML, so long as the
243
- Ruby file returns a `Hash` when evalled.
241
+ Ruby file returns a =Hash= when evalled.
244
242
 
245
- ```ruby
243
+ #+begin_src ruby
246
244
  {
247
- en: {
245
+ 'en' => {
248
246
  ruby: proc do |key, options = {}|
249
247
  "Hey #{ key }!"
250
248
  end
251
249
  }
252
250
  }
253
- ```
254
-
251
+ #+end_src
255
252
 
256
- ## Multiple Locales
253
+ ** Multiple locales
257
254
 
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.
255
+ If you would like to check multiple locales and pull the first matching one out,
256
+ you can pass the =:locales= option to initialization or the =translate= method
257
+ with an array of top-level locale keys.
260
258
 
261
- ```ruby
259
+ #+begin_src ruby
262
260
  messages = {
263
261
  'en' => {
264
262
  'title' => 'Login',
@@ -272,31 +270,28 @@ messages = {
272
270
  i18n = Tater.new(messages: messages)
273
271
  i18n.translate('title', locales: %w[fr en]) # => 'la connexion'
274
272
  i18n.translate('description', locales: %w[fr en]) # => 'English description.'
275
- ```
276
273
 
277
- Locales will tried in order and which one matches first will be returned.
274
+ # OR
275
+ i18n = Tater.new(messages: messages, locales: %w[fr en])
276
+ i18n.translate('title') # => 'la connexion'
277
+ i18n.translate('description') # => 'English description.'
278
+ #+end_src
278
279
 
280
+ Locales will be tried in order and whichever one matches first will be returned.
279
281
 
280
- ## Limitations
282
+ ** Limitations
281
283
 
282
- - It is not "pluggable", it does what it does and that's it.
284
+ - It is not pluggable, it does what it does and that's it.
283
285
  - It doesn't handle pluralization yet, though it may in the future.
284
- - It doesn't cache anything, that's up to you.
285
-
286
-
287
- ## Why?
288
-
289
- Because [Ruby I18n][rubyi18n] is amazing and I wanted to try to create a minimum
290
- viable implementation of the bits of I18n that I use 90% of the time. Tater is a
291
- single file that handles the basics of lookup and interpolation.
292
286
 
287
+ ** Why?
293
288
 
294
- ## Trivia
289
+ Because [[https://github.com/ruby-i18n/i18n][Ruby I18n]] is amazing and I wanted to try to create a minimum viable
290
+ implementation of the bits of I18n that I use 90% of the time. Tater is a single
291
+ file that handles the basics of lookup and interpolation.
295
292
 
296
- I was orininally going to call this library "Translator" but with a
297
- [numeronym][numeronym] like I18n: "t8r". I looked at it for a while but I read
298
- it as "tater" instead of "tee-eight-arr" so I figured I'd just name it Tater.
299
- Tater the translator.
293
+ ** Trivia
300
294
 
301
- [numeronym]: https://en.wikipedia.org/wiki/Numeronym
302
- [rubyi18n]: https://github.com/ruby-i18n/i18n
295
+ I was orininally going to call this library "Translator" but with a [[https://en.wikipedia.org/wiki/Numeronym][numeronym]]
296
+ like I18n: "t8r". I looked at it for a while but I read it as "tater" instead
297
+ of "tee-eight-arr" so I figured I'd just name it Tater. Tater the translator.
data/lib/tater.rb CHANGED
@@ -6,20 +6,21 @@ require 'yaml'
6
6
  # designed for speed and simplicity.
7
7
  class Tater
8
8
  class MissingLocalizationFormat < ArgumentError; end
9
+
9
10
  class UnLocalizableObject < ArgumentError; end
10
11
 
11
12
  module Utils # :nodoc:
12
13
  # Merge all the way down.
13
14
  #
14
15
  # @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.
16
+ # The target Hash to merge into.
17
17
  # @param from [Hash]
18
18
  # The Hash to copy values from.
19
- def self.deep_merge!(to, from)
20
- to.merge!(from) do |_key, left, right|
19
+ # @return [Hash]
20
+ def self.deep_merge(to, from)
21
+ to.merge(from) do |_key, left, right|
21
22
  if left.is_a?(Hash) && right.is_a?(Hash)
22
- Utils.deep_merge!(left, right)
23
+ Utils.deep_merge(left, right)
23
24
  else
24
25
  right
25
26
  end
@@ -29,18 +30,32 @@ class Tater
29
30
  # Transform keys all the way down.
30
31
  #
31
32
  # @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|
33
+ # The Hash to stringify keys for.
34
+ # @return [Hash]
35
+ def self.deep_stringify_keys(hash)
36
+ hash.transform_keys(&:to_s).transform_values do |value|
36
37
  if value.is_a?(Hash)
37
- Utils.deep_stringify_keys!(value)
38
+ Utils.deep_stringify_keys(value)
38
39
  else
39
40
  value
40
41
  end
41
42
  end
42
43
  end
43
44
 
45
+ # Freeze all the way down.
46
+ #
47
+ # @param hash [Hash]
48
+ # @return [Hash]
49
+ def self.deep_freeze(hash)
50
+ hash.transform_keys(&:freeze).transform_values do |value|
51
+ if value.is_a?(Hash)
52
+ Utils.deep_freeze(value)
53
+ else
54
+ value.freeze
55
+ end
56
+ end.freeze
57
+ end
58
+
44
59
  # Try to interpolate these things, if one of them is a string.
45
60
  #
46
61
  # @param string [String]
@@ -49,8 +64,9 @@ class Tater
49
64
  # The values to interpolate into the target string.
50
65
  #
51
66
  # @return [String]
52
- def self.interpolate(string, options = {})
67
+ def self.interpolate(string, options = HASH)
53
68
  return string unless string.is_a?(String)
69
+ return string if options.empty?
54
70
 
55
71
  format(string, options)
56
72
  end
@@ -71,8 +87,8 @@ class Tater
71
87
  end
72
88
 
73
89
  DEFAULT = 'default'
74
- DEFAULT_LOCALE = 'en'
75
90
  DELIMITING_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/.freeze
91
+ HASH = {}.freeze
76
92
  SEPARATOR = '.'
77
93
  SUBSTITUTION_REGEX = /%(|\^)[aAbBpP]/.freeze
78
94
 
@@ -82,7 +98,16 @@ class Tater
82
98
  # @return [Hash]
83
99
  attr_reader :messages
84
100
 
85
- def initialize(path: nil, messages: nil, locale: DEFAULT_LOCALE, cascade: false)
101
+ # @param cascade [Boolean]
102
+ # A boolean indicating if lookups should cascade by default.
103
+ # @param locale [String]
104
+ # The default locale.
105
+ # @param messages [Hash]
106
+ # A hash of messages ready to be loaded in.
107
+ # @param path [String]
108
+ # A path to search for YAML or Ruby files to load messages from.
109
+ def initialize(cascade: false, locale: nil, messages: nil, path: nil)
110
+ @cache = {}
86
111
  @cascade = cascade
87
112
  @locale = locale
88
113
  @messages = {}
@@ -98,11 +123,11 @@ class Tater
98
123
  @cascade
99
124
  end
100
125
 
101
- # An array of the available locale codes.
126
+ # An array of the available locale codes found in loaded messages.
102
127
  #
103
128
  # @return [Array]
104
129
  def available
105
- messages.keys.map(&:to_s)
130
+ @available ||= messages.keys
106
131
  end
107
132
 
108
133
  # Is this locale available in our current set of messages?
@@ -113,24 +138,36 @@ class Tater
113
138
  end
114
139
 
115
140
  # Load messages into our internal cache, either from a path containing YAML
116
- # files or a collection of messages.
141
+ # files or a Hash of messages.
117
142
  #
118
143
  # @param path [String]
119
144
  # A path to search for YAML or Ruby files to load messages from.
120
145
  # @param messages [Hash]
121
146
  # A hash of messages ready to be loaded in.
122
147
  def load(path: nil, messages: nil)
148
+ return if path.nil? && messages.nil?
149
+
123
150
  if path
124
151
  Dir.glob(File.join(path, '**', '*.{yml,yaml}')).each do |file|
125
- Utils.deep_merge!(@messages, YAML.load_file(file))
152
+ @messages = Utils.deep_merge(@messages, YAML.load_file(file))
126
153
  end
127
154
 
128
155
  Dir.glob(File.join(path, '**', '*.rb')).each do |file|
129
- Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(eval(IO.read(file), binding, file)))
156
+ @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(IO.read(file), binding, file))) # rubocop:disable Security/Eval
130
157
  end
131
158
  end
132
159
 
133
- Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(messages)) if messages
160
+ @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(messages)) if messages
161
+ @messages = Utils.deep_freeze(@messages)
162
+
163
+ # Gotta recalculate available locales after updating.
164
+ remove_instance_variable(:@available) if instance_variable_defined?(:@available)
165
+
166
+ # Not only does this clear our cache but it establishes the basic structure
167
+ # that we rely on in other methods.
168
+ @messages.each_key do |key|
169
+ @cache[key] = { true => {}, false => {} }
170
+ end
134
171
  end
135
172
 
136
173
  # Set the current locale, if it's available.
@@ -143,7 +180,7 @@ class Tater
143
180
 
144
181
  # Localize an Array, Date, Time, DateTime, or Numeric object.
145
182
  #
146
- # @param object [Date, Time, DateTime, Numeric]
183
+ # @param object [Array<String>, Date, Time, DateTime, Numeric]
147
184
  # The object to localize.
148
185
  # @param options [Hash]
149
186
  # Options to configure localization.
@@ -166,50 +203,50 @@ class Tater
166
203
  #
167
204
  # @return [String]
168
205
  # A localized version of the object passed in.
169
- def localize(object, options = {})
170
- format_key = options.delete(:format) || DEFAULT
171
- locale_override = options.delete(:locale)
172
-
206
+ def localize(object, options = HASH)
173
207
  case object
174
208
  when String
175
209
  object
176
210
  when Numeric
177
- delimiter = options.delete(:delimiter) || lookup('numeric.delimiter', locale_override)
178
- separator = options.delete(:separator) || lookup('numeric.separator', locale_override)
179
- precision = options.delete(:precision) || 2
211
+ delimiter = options[:delimiter] || lookup('numeric.delimiter', locale: options[:locale])
212
+ separator = options[:separator] || lookup('numeric.separator', locale: options[:locale])
213
+ precision = options[:precision] || 2
180
214
 
181
215
  raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing or not passed as option :delimiter") unless delimiter
182
216
  raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing or not passed as option :separator") unless separator
183
217
 
184
- # Heavily cribbed from Rails.
185
- integer, fraction = Utils.string_from_numeric(object).split('.')
186
- integer.gsub!(DELIMITING_REGEX) do |number|
187
- "#{ number }#{ delimiter }"
218
+ # Break the number up into integer and fraction parts.
219
+ integer = Utils.string_from_numeric(object)
220
+ integer, fraction = integer.split('.') unless object.is_a?(Integer)
221
+
222
+ if integer.length > 3
223
+ integer.gsub!(DELIMITING_REGEX) do |number|
224
+ "#{ number }#{ delimiter }"
225
+ end
188
226
  end
189
227
 
190
- if precision.zero?
228
+ if precision.zero? || fraction.nil?
191
229
  integer
192
230
  else
193
- [integer, fraction&.ljust(precision, '0')].compact.join(separator)
231
+ "#{ integer }#{ separator }#{ fraction.ljust(precision, '0').slice(0, precision) }"
194
232
  end
195
233
  when Date, Time, DateTime
196
- key = object.class.to_s.downcase
197
- format = lookup("#{ key }.formats.#{ format_key }", locale_override) || format_key
234
+ format = lookup("#{ object.class.to_s.downcase }.formats.#{ options[:format] || DEFAULT }", locale: options[:locale]) || options[:format] || DEFAULT
198
235
 
199
236
  # Heavily cribbed from I18n, many thanks to the people who sorted this out
200
237
  # before I worked on this library.
201
238
  format = format.gsub(SUBSTITUTION_REGEX) do |match|
202
239
  case match
203
- when '%a' then lookup('date.abbreviated_days', locale_override)[object.wday]
204
- when '%^a' then lookup('date.abbreviated_days', locale_override)[object.wday].upcase
205
- when '%A' then lookup('date.days', locale_override)[object.wday]
206
- when '%^A' then lookup('date.days', locale_override)[object.wday].upcase
207
- when '%b' then lookup('date.abbreviated_months', locale_override)[object.mon - 1]
208
- when '%^b' then lookup('date.abbreviated_months', locale_override)[object.mon - 1].upcase
209
- when '%B' then lookup('date.months', locale_override)[object.mon - 1]
210
- when '%^B' then lookup('date.months', locale_override)[object.mon - 1].upcase
211
- when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale_override).upcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
212
- when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale_override).downcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
240
+ when '%a' then lookup('date.abbreviated_days', locale: options[:locale])[object.wday]
241
+ when '%^a' then lookup('date.abbreviated_days', locale: options[:locale])[object.wday].upcase
242
+ when '%A' then lookup('date.days', locale: options[:locale])[object.wday]
243
+ when '%^A' then lookup('date.days', locale: options[:locale])[object.wday].upcase
244
+ when '%b' then lookup('date.abbreviated_months', locale: options[:locale])[object.mon - 1]
245
+ when '%^b' then lookup('date.abbreviated_months', locale: options[:locale])[object.mon - 1].upcase
246
+ when '%B' then lookup('date.months', locale: options[:locale])[object.mon - 1]
247
+ when '%^B' then lookup('date.months', locale: options[:locale])[object.mon - 1].upcase
248
+ when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale: options[:locale]).upcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
249
+ when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale: options[:locale]).downcase if object.respond_to?(:hour) # rubocop:disable Metrics/BlockNesting
213
250
  end
214
251
  end
215
252
 
@@ -221,14 +258,14 @@ class Tater
221
258
  when 1
222
259
  object[0]
223
260
  when 2
224
- two_words_connector = options.delete(:two_words_connector) || lookup('array.two_words_connector', locale_override)
261
+ two_words_connector = options[:two_words_connector] || lookup('array.two_words_connector', locale: options[:locale])
225
262
 
226
263
  raise(MissingLocalizationFormat, "Sentence localization connector ('array.two_words_connector') missing or not passed as option :two_words_connector") unless two_words_connector
227
264
 
228
265
  "#{ object[0] }#{ two_words_connector }#{ object[1] }"
229
266
  else
230
- last_word_connector = options.delete(:last_word_connector) || lookup('array.last_word_connector', locale_override)
231
- words_connector = options.delete(:words_connector) || lookup('array.words_connector', locale_override)
267
+ last_word_connector = options[:last_word_connector] || lookup('array.last_word_connector', locale: options[:locale])
268
+ words_connector = options[:words_connector] || lookup('array.words_connector', locale: options[:locale])
232
269
 
233
270
  raise(MissingLocalizationFormat, "Sentence localization connector ('array.last_word_connector') missing or not passed as option :last_word_connector") unless last_word_connector
234
271
  raise(MissingLocalizationFormat, "Sentence localization connector ('array.words_connector') missing or not passed as option :words_connector") unless words_connector
@@ -244,28 +281,36 @@ class Tater
244
281
  # Lookup a key in the messages hash, using the current locale or an override.
245
282
  #
246
283
  # @param key [String]
247
- # @param locale_override [String]
284
+ # @param locale [String]
248
285
  # A locale to use instead of our current one.
249
- # @param cascade_override [Boolean]
286
+ # @param cascade [Boolean]
250
287
  # A boolean to forcibly set the cascade option for this lookup.
251
288
  #
252
289
  # @return
253
- # Basically anything that can be stored in YAML, including nil.
254
- def lookup(key, locale_override = nil, cascade_override = nil)
255
- path = key.split(SEPARATOR).prepend(locale_override || locale).map(&:to_s)
290
+ # Basically anything that can be stored in your messages Hash.
291
+ def lookup(key, locale: nil, cascade: nil)
292
+ locale = locale.nil? ? @locale : locale
293
+ cascade = cascade.nil? ? @cascade : cascade
294
+
295
+ cached(key, locale, cascade) || begin
296
+ return nil unless @messages.key?(locale.to_s)
256
297
 
257
- if cascade_override.nil? ? @cascade : cascade_override
258
- while path.length >= 2 do
259
- attempt = @messages.dig(*path)
298
+ path = key.split(SEPARATOR).prepend(locale).map(&:to_s)
260
299
 
261
- if attempt
262
- break attempt
300
+ message =
301
+ if cascade
302
+ while path.length >= 2
303
+ attempt = @messages.dig(*path)
304
+
305
+ break attempt unless attempt.nil?
306
+
307
+ path.delete_at(path.length - 2)
308
+ end
263
309
  else
264
- path.delete_at(path.length - 2)
310
+ @messages.dig(*path)
265
311
  end
266
- end
267
- else
268
- @messages.dig(*path)
312
+
313
+ cache(key, locale, cascade, message)
269
314
  end
270
315
  end
271
316
 
@@ -285,20 +330,18 @@ class Tater
285
330
  # An array of locales to look within.
286
331
  #
287
332
  # @return [Boolean]
288
- def includes?(key, options = {})
289
- cascade_override = options.delete(:cascade)
290
- locale_override = options.delete(:locale)
291
- locales = options.delete(:locales)
292
-
333
+ def includes?(key, options = HASH)
293
334
  message =
294
- if locale_override || !locales
295
- lookup(key, locale_override, cascade_override)
296
- else
297
- locales.find do |accept|
298
- found = lookup(key, accept, cascade_override)
335
+ if options.key?(:locales)
336
+ options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
337
+
338
+ options[:locales].find do |accept|
339
+ found = lookup(key, locale: accept, cascade: options[:cascade])
299
340
 
300
- break found if found
341
+ break found unless found.nil?
301
342
  end
343
+ else
344
+ lookup(key, locale: options[:locale], cascade: options[:cascade])
302
345
  end
303
346
 
304
347
  !message.nil?
@@ -308,7 +351,7 @@ class Tater
308
351
  # It's effectively a combination of #lookup and #interpolate.
309
352
  #
310
353
  # @example
311
- # Tater.new(messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
354
+ # Tater.new(locale: 'en', messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
312
355
  #
313
356
  # @param key [String]
314
357
  # The period-separated key path to look within our messages for.
@@ -320,36 +363,61 @@ class Tater
320
363
  # @option options [String] :default
321
364
  # A default string to return, should lookup fail.
322
365
  # @option options [String] :locale
323
- # A specific locale to lookup within. This will take precedence over the
324
- # :locales option.
366
+ # A specific locale to lookup within.
325
367
  # @option options [Array<String>] :locales
326
- # An array of locales to look within.
368
+ # An array of locales to look within. This will take precedence over the
369
+ # :locale option and will append the default :locale option passed during
370
+ # initialization if present.
327
371
  #
328
372
  # @return [String]
329
373
  # The translated and interpreted string, if found, or any data at the
330
374
  # defined key.
331
- def translate(key, options = {})
332
- cascade_override = options.delete(:cascade)
333
- locale_override = options.delete(:locale)
334
- locales = options.delete(:locales)
335
-
375
+ def translate(key, options = HASH)
336
376
  message =
337
- if locale_override || !locales
338
- lookup(key, locale_override, cascade_override)
339
- else
340
- locales.find do |accept|
341
- found = lookup(key, accept, cascade_override)
377
+ if options.key?(:locales)
378
+ options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
379
+
380
+ options[:locales].find do |accept|
381
+ found = lookup(key, locale: accept, cascade: options[:cascade])
342
382
 
343
- break found if found
383
+ break found unless found.nil?
344
384
  end
385
+ else
386
+ lookup(key, locale: options[:locale], cascade: options[:cascade])
345
387
  end
346
388
 
347
389
  # Call procs that should return a string.
348
- if message.is_a?(Proc)
349
- message = message.call(key, options)
350
- end
390
+ message = message.call(key, options) if message.is_a?(Proc)
351
391
 
352
- Utils.interpolate(message, options) || options.delete(:default) { "Tater lookup failed: #{ locale_override || locales || locale }.#{ key }" }
392
+ Utils.interpolate(message, options) || options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
353
393
  end
354
394
  alias t translate
395
+
396
+ private
397
+
398
+ # @param key [String]
399
+ # The cache key, often in the form "something.nested.like.this"
400
+ # @param locale [String]
401
+ # The locale to store the value for.
402
+ # @param cascade [Boolean]
403
+ # Was this a cascading lookup?
404
+ # @param message [String]
405
+ # The message being cached, often a String.
406
+ # @return [String]
407
+ # Whatever value is being cached, often a String.
408
+ def cache(key, locale, cascade, message)
409
+ @cache[locale][cascade][key] = message
410
+ end
411
+
412
+ # @param key [String]
413
+ # The cache key, often in the form "something.nested.like.this"
414
+ # @param locale [String]
415
+ # The locale to store the value for.
416
+ # @param cascade [Boolean]
417
+ # Was this a cascading lookup?
418
+ # @return [String, nil]
419
+ # The cached message or nil.
420
+ def cached(key, locale, cascade)
421
+ @cache.dig(locale, cascade, key)
422
+ end
355
423
  end
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  {
2
3
  en: {
3
- ruby: proc do |key, options = {}|
4
+ ruby: proc do |key, _options = {}|
4
5
  "Hey #{ key }!"
5
6
  end,
6
- options: proc do |_key, options = {}|
7
- "Hey %{options}!"
7
+ options: proc do |_key, _options = {}|
8
+ 'Hey %{options}!'
8
9
  end
9
10
  }
10
11
  }
data/test/tater_test.rb CHANGED
@@ -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
 
@@ -31,25 +41,29 @@ describe Tater do
31
41
  assert_equal 'this thing', Tater::Utils.interpolate('this %{what}', what: 'thing')
32
42
  end
33
43
 
34
- it 'raises a KeyError when an argument is missing' do
44
+ it 'raises a KeyError when an argument is missing (but options are passed)' do
35
45
  assert_raises(KeyError) do
36
- Tater::Utils.interpolate('this %{what}')
46
+ Tater::Utils.interpolate('this %{what}', nope: 'thing')
37
47
  end
38
48
  end
49
+
50
+ it 'returns the string unchanged when options are empty (does not raise a KeyError)' do
51
+ assert_equal 'this %{what}', Tater::Utils.interpolate('this %{what}')
52
+ end
39
53
  end
40
54
 
41
55
  describe '#string_from_numeric' do
42
56
  it 'converts numerics to decimal-ish strings' do
43
57
  assert_equal '1', Tater::Utils.string_from_numeric(1)
44
58
  assert_equal '1.0', Tater::Utils.string_from_numeric(1.0)
45
- assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal(1))
59
+ assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal('1'))
46
60
  end
47
61
  end
48
62
  end
49
63
 
50
64
  describe '#available?' do
51
65
  let :i18n do
52
- Tater.new(path: File.expand_path('test/fixtures'))
66
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
53
67
  end
54
68
 
55
69
  it 'tells you if the locale is available' do
@@ -84,6 +98,14 @@ describe Tater do
84
98
 
85
99
  assert_instance_of(Hash, i18n.messages)
86
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
87
109
  end
88
110
 
89
111
  describe '#available' do
@@ -94,11 +116,17 @@ describe Tater do
94
116
  it 'returns an array with the available locales (i.e. the top-level keys in our messages hash)' do
95
117
  assert_equal %w[en delimiter_only separator_only fr].sort, i18n.available.sort
96
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
97
125
  end
98
126
 
99
127
  describe '#lookup' do
100
128
  let :i18n do
101
- Tater.new(path: File.expand_path('test/fixtures'))
129
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
102
130
  end
103
131
 
104
132
  it 'returns keys from messages' do
@@ -114,16 +142,16 @@ describe Tater do
114
142
  end
115
143
 
116
144
  it 'cascades' do
117
- assert_equal 'Delicious', i18n.lookup('cascade.nope.tacos', nil, true)
118
- assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy', nil, true)
119
- 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)
120
148
  assert_nil i18n.lookup('cascade.nahhhhhh')
121
149
  end
122
150
  end
123
151
 
124
152
  describe '#translate' do
125
153
  let :i18n do
126
- Tater.new(path: File.expand_path('test/fixtures'))
154
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
127
155
  end
128
156
 
129
157
  it 'translates strings' do
@@ -181,7 +209,7 @@ describe Tater do
181
209
 
182
210
  describe '#localize' do
183
211
  let :i18n do
184
- Tater.new(path: File.expand_path('test/fixtures'))
212
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
185
213
  end
186
214
 
187
215
  let :fr do
@@ -237,6 +265,30 @@ describe Tater do
237
265
  assert_equal '1NAH12', i18n.localize(BigDecimal('1.12'))
238
266
  end
239
267
 
268
+ describe 'precision option' do
269
+ it 'defaults to 2' do
270
+ assert_equal '10NAH00', i18n.localize(BigDecimal('10'))
271
+ assert_equal '10NAH00', i18n.localize(10.0)
272
+ end
273
+
274
+ it 'defaults to zero for integers' do
275
+ assert_equal '10', i18n.localize(10)
276
+ end
277
+
278
+ it 'removes fractional pieces when the precision is 0' do
279
+ assert_equal '10', i18n.localize(BigDecimal('10.123456'), precision: 0)
280
+ assert_equal '10', i18n.localize(10.123456, precision: 0)
281
+
282
+ assert_equal '10', i18n.localize(BigDecimal('10.12'), precision: 0)
283
+ assert_equal '10', i18n.localize(10.12, precision: 0)
284
+ end
285
+
286
+ it 'truncates long values to the desired precision' do
287
+ assert_equal '10NAH00', i18n.localize(BigDecimal('10.00234'))
288
+ assert_equal '10NAH00', i18n.localize(10.00234)
289
+ end
290
+ end
291
+
240
292
  it 'allows overriding the delimiter and separator' do
241
293
  assert_equal '10WOO000NAH12', i18n.localize(10_000.12, delimiter: 'WOO')
242
294
  assert_equal '10TURKEYS000YA12', i18n.localize(10_000.12, separator: 'YA')
@@ -332,7 +384,7 @@ describe Tater do
332
384
 
333
385
  describe '#locale=' do
334
386
  let :i18n do
335
- Tater.new(path: File.expand_path('test/fixtures'))
387
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
336
388
  end
337
389
 
338
390
  it 'overrides the locale when available' do
@@ -366,7 +418,7 @@ describe Tater do
366
418
 
367
419
  describe '#includes?' do
368
420
  let :i18n do
369
- Tater.new(path: File.expand_path('test/fixtures'))
421
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
370
422
  end
371
423
 
372
424
  it 'tells you if you have a translation' do
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tater
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Lecklider
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-19 00:00:00.000000000 Z
11
+ date: 2021-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: minitest
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,20 @@ dependencies:
24
38
  - - ">="
25
39
  - !ruby/object:Gem::Version
26
40
  version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rubocop
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +66,48 @@ dependencies:
38
66
  - - ">="
39
67
  - !ruby/object:Gem::Version
40
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-performance
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
41
111
  description: Minimal internationalization and localization library.
42
112
  email:
43
113
  - evan@lecklider.com
@@ -46,7 +116,7 @@ extensions: []
46
116
  extra_rdoc_files: []
47
117
  files:
48
118
  - LICENSE.txt
49
- - README.md
119
+ - README.org
50
120
  - lib/tater.rb
51
121
  - test/fixtures/another.yml
52
122
  - test/fixtures/fixtures.yml
@@ -74,13 +144,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
144
  - !ruby/object:Gem::Version
75
145
  version: '2.0'
76
146
  requirements: []
77
- rubygems_version: 3.0.4
147
+ rubygems_version: 3.2.15
78
148
  signing_key:
79
149
  specification_version: 4
80
150
  summary: Minimal internationalization and localization library.
81
151
  test_files:
152
+ - test/fixtures/another.yml
153
+ - test/fixtures/fixtures.yml
82
154
  - test/fixtures/messages/more.yml
83
155
  - test/fixtures/ruby.rb
84
- - test/fixtures/fixtures.yml
85
- - test/fixtures/another.yml
86
156
  - test/tater_test.rb