tater 1.1.0 → 1.3.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bce1afa07abcf6f94618ca249808f14d4a16b107c112bc074b4f0e6e2703b86d
4
- data.tar.gz: 3f481fbb8e9551fee057472c3bef0fbc2420c9e267a37e09bf353a19e9877130
3
+ metadata.gz: d41ee7aa6cc55a97ad00ee6b96ccbb6127344f9b3e85c4cf3b162f72f3d115d8
4
+ data.tar.gz: 2fe1b6c5aff5fdfcc48dbd8212ddbbf0903f9af071c42ff1178c1f20752f536a
5
5
  SHA512:
6
- metadata.gz: 9096e096441db2696735213dd21c313aa333877062f3094680a2ff7bbb14c5e3e699d817a8df59626f570eff345b5b69a942ec99cd19ab3bc74cbdee0cb9b398
7
- data.tar.gz: 8511e008cf509bd76869aef9957e98e7f73bb60b50649abe811f09ad4ee01447553689ffe52451f8073110d1338ad430ef7848f25a4e3fce1614c22d60153dc3
6
+ metadata.gz: d686b81192264e1f78f17c0decf0ac3811582e0fe464d2f5f44281481351abca8bd3906fe218a2ae9309a3d84177382f5de7348f63ae3967b7d2af2dc452bcb6
7
+ data.tar.gz: 942fd8b6a433b65778f3d7a78f07a8d99c902c5483eec105e0c75689d40db7d0664170963e514d5e78928450b6c0499dce8c3afd63f0eaded6d9843cf3e703a8
data/README.md CHANGED
@@ -56,6 +56,25 @@ i18n.translate('some.key') # => 'This here string!'
56
56
  i18n.translate('interpolated', you: 'world') # => 'Hello world!'
57
57
  ```
58
58
 
59
+
60
+ ## Array Localization
61
+
62
+ Given an array, Tater will do it's best to join the elements of the array into a
63
+ sentence based on how many elements there are.
64
+
65
+ ```yaml
66
+ en:
67
+ array:
68
+ last_word_connector: ", and "
69
+ two_words_connector: " and "
70
+ words_connector: ", "
71
+ ```
72
+
73
+ ```ruby
74
+ i18n.localize(%w[tacos enchiladas burritos]) # => "tacos, enchiladas, and burritos"
75
+ ```
76
+
77
+
59
78
  ## Numeric Localization
60
79
 
61
80
  Numeric localization (`Numeric`, `Integer`, `Float`, and `BigDecimal`) require
@@ -51,6 +51,7 @@ class Tater
51
51
  # @return [String]
52
52
  def self.interpolate(string, options = {})
53
53
  return string unless string.is_a?(String)
54
+ return string if options.empty?
54
55
 
55
56
  format(string, options)
56
57
  end
@@ -91,6 +92,8 @@ class Tater
91
92
  load(messages: messages) if messages
92
93
  end
93
94
 
95
+ # Do lookups cascade by default?
96
+ #
94
97
  # @return [Boolean]
95
98
  def cascades?
96
99
  @cascade
@@ -114,7 +117,7 @@ class Tater
114
117
  # files or a collection of messages.
115
118
  #
116
119
  # @param path [String]
117
- # A path to search for YAML files to load messages from.
120
+ # A path to search for YAML or Ruby files to load messages from.
118
121
  # @param messages [Hash]
119
122
  # A hash of messages ready to be loaded in.
120
123
  def load(path: nil, messages: nil)
@@ -124,7 +127,7 @@ class Tater
124
127
  end
125
128
 
126
129
  Dir.glob(File.join(path, '**', '*.rb')).each do |file|
127
- Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(eval(IO.read(file), binding, file)))
130
+ Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(eval(IO.read(file), binding, file))) # rubocop:disable Security/Eval
128
131
  end
129
132
  end
130
133
 
@@ -139,10 +142,28 @@ class Tater
139
142
  @locale = locale.to_s if available?(locale)
140
143
  end
141
144
 
142
- # Localize a Date, Time, DateTime, or Numeric object.
145
+ # Localize an Array, Date, Time, DateTime, or Numeric object.
143
146
  #
144
147
  # @param object [Date, Time, DateTime, Numeric]
145
148
  # The object to localize.
149
+ # @param options [Hash]
150
+ # Options to configure localization.
151
+ #
152
+ # @option options [String] :format
153
+ # The key or format string to use for localizing the current object.
154
+ # @option options [String] :locale
155
+ # The locale to use in lieu of the current default.
156
+ # @option options [String] :delimiter
157
+ # The delimiter to use when localizing numberic values.
158
+ # @option options [String] :separator
159
+ # The separator to use when localizing numberic values.
160
+ # @option options [String] :two_words_connector
161
+ # The string used to join two array elements together e.g. " and ".
162
+ # @option options [String] :words_connector
163
+ # The string used to connect multiple array elements e.g. ", ".
164
+ # @option options [String] :last_word_connector
165
+ # The string used to connect the final element with preceding array elements
166
+ # e.g. ", and ".
146
167
  #
147
168
  # @return [String]
148
169
  # A localized version of the object passed in.
@@ -156,10 +177,10 @@ class Tater
156
177
  when Numeric
157
178
  delimiter = options.delete(:delimiter) || lookup('numeric.delimiter', locale_override)
158
179
  separator = options.delete(:separator) || lookup('numeric.separator', locale_override)
159
- precision = options.fetch(:precision) { 2 }
180
+ precision = options.delete(:precision) || 2
160
181
 
161
- raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing") unless delimiter
162
- raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing") unless separator
182
+ raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing or not passed as option :delimiter") unless delimiter
183
+ raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing or not passed as option :separator") unless separator
163
184
 
164
185
  # Heavily cribbed from Rails.
165
186
  integer, fraction = Utils.string_from_numeric(object).split('.')
@@ -194,6 +215,27 @@ class Tater
194
215
  end
195
216
 
196
217
  object.strftime(format)
218
+ when Array
219
+ case object.length
220
+ when 0
221
+ ''
222
+ when 1
223
+ object[0]
224
+ when 2
225
+ two_words_connector = options.delete(:two_words_connector) || lookup('array.two_words_connector', locale_override)
226
+
227
+ raise(MissingLocalizationFormat, "Sentence localization connector ('array.two_words_connector') missing or not passed as option :two_words_connector") unless two_words_connector
228
+
229
+ "#{ object[0] }#{ two_words_connector }#{ object[1] }"
230
+ 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)
233
+
234
+ raise(MissingLocalizationFormat, "Sentence localization connector ('array.last_word_connector') missing or not passed as option :last_word_connector") unless last_word_connector
235
+ raise(MissingLocalizationFormat, "Sentence localization connector ('array.words_connector') missing or not passed as option :words_connector") unless words_connector
236
+
237
+ "#{ object[0...-1].join(words_connector) }#{ last_word_connector }#{ object[-1] }"
238
+ end
197
239
  else
198
240
  raise(UnLocalizableObject, "The object class #{ object.class } cannot be localized by Tater.")
199
241
  end
@@ -205,6 +247,8 @@ class Tater
205
247
  # @param key [String]
206
248
  # @param locale_override [String]
207
249
  # A locale to use instead of our current one.
250
+ # @param cascade_override [Boolean]
251
+ # A boolean to forcibly set the cascade option for this lookup.
208
252
  #
209
253
  # @return
210
254
  # Basically anything that can be stored in YAML, including nil.
@@ -212,20 +256,53 @@ class Tater
212
256
  path = key.split(SEPARATOR).prepend(locale_override || locale).map(&:to_s)
213
257
 
214
258
  if cascade_override.nil? ? @cascade : cascade_override
215
- while path.length >= 2 do
259
+ while path.length >= 2
216
260
  attempt = @messages.dig(*path)
217
261
 
218
- if attempt
219
- break attempt
220
- else
221
- path.delete_at(path.length - 2)
222
- end
262
+ break attempt if attempt
263
+
264
+ path.delete_at(path.length - 2)
223
265
  end
224
266
  else
225
267
  @messages.dig(*path)
226
268
  end
227
269
  end
228
270
 
271
+ # Check that there's a key at the given path.
272
+ #
273
+ # @param key [String]
274
+ # The period-separated key path to look within our messages for.
275
+ # @param options [Hash]
276
+ # Options to pass to the #lookup method, including locale overrides.
277
+ #
278
+ # @option options [Boolean] :cascade
279
+ # Should this lookup cascade or not? Can override @cascade.
280
+ # @option options [String] :locale
281
+ # A specific locale to lookup within. This will take precedence over the
282
+ # :locales option.
283
+ # @option options [Array<String>] :locales
284
+ # An array of locales to look within.
285
+ #
286
+ # @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
+
292
+ 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)
298
+
299
+ break found if found
300
+ end
301
+ end
302
+
303
+ !message.nil?
304
+ end
305
+
229
306
  # Translate a key path and optional interpolation arguments into a string.
230
307
  # It's effectively a combination of #lookup and #interpolate.
231
308
  #
@@ -234,23 +311,31 @@ class Tater
234
311
  #
235
312
  # @param key [String]
236
313
  # The period-separated key path to look within our messages for.
314
+ # @param options [Hash]
315
+ # Options, including values to interpolate to any found string.
316
+ #
237
317
  # @option options [Boolean] :cascade
318
+ # Should this lookup cascade or not? Can override @cascade.
238
319
  # @option options [String] :default
320
+ # A default string to return, should lookup fail.
239
321
  # @option options [String] :locale
322
+ # A specific locale to lookup within. This will take precedence over the
323
+ # :locales option.
324
+ # @option options [Array<String>] :locales
325
+ # An array of locales to look within.
240
326
  #
241
327
  # @return [String]
242
328
  # The translated and interpreted string, if found, or any data at the
243
329
  # defined key.
244
330
  def translate(key, options = {})
245
331
  cascade_override = options.delete(:cascade)
246
- default = options.delete(:default)
247
332
  locale_override = options.delete(:locale)
248
333
  locales = options.delete(:locales)
249
334
 
250
335
  message =
251
336
  if locale_override || !locales
252
337
  lookup(key, locale_override, cascade_override)
253
- elsif locales
338
+ else
254
339
  locales.find do |accept|
255
340
  found = lookup(key, accept, cascade_override)
256
341
 
@@ -263,7 +348,7 @@ class Tater
263
348
  message = message.call(key, options)
264
349
  end
265
350
 
266
- Utils.interpolate(message, options) || default || "Tater lookup failed: #{ locale_override || locales || locale }.#{ key }"
351
+ Utils.interpolate(message, options) || options.delete(:default) { "Tater lookup failed: #{ locale_override || locales || locale }.#{ key }" }
267
352
  end
268
353
  alias t translate
269
354
  end
@@ -8,6 +8,11 @@ en:
8
8
  deep:
9
9
  key: 'This key is deeper'
10
10
 
11
+ array:
12
+ last_word_connector: ", and "
13
+ two_words_connector: " and "
14
+ words_connector: ", "
15
+
11
16
  date:
12
17
  formats:
13
18
  default: '%Y/%-m/%-d'
@@ -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
  }
@@ -31,11 +31,15 @@ describe Tater do
31
31
  assert_equal 'this thing', Tater::Utils.interpolate('this %{what}', what: 'thing')
32
32
  end
33
33
 
34
- it 'raises a KeyError when an argument is missing' do
34
+ it 'raises a KeyError when an argument is missing (but options are passed)' do
35
35
  assert_raises(KeyError) do
36
- Tater::Utils.interpolate('this %{what}')
36
+ Tater::Utils.interpolate('this %{what}', nope: 'thing')
37
37
  end
38
38
  end
39
+
40
+ it 'returns the string unchanged when options are empty (does not raise a KeyError)' do
41
+ assert_equal 'this %{what}', Tater::Utils.interpolate('this %{what}')
42
+ end
39
43
  end
40
44
 
41
45
  describe '#string_from_numeric' do
@@ -116,6 +120,7 @@ describe Tater do
116
120
  it 'cascades' do
117
121
  assert_equal 'Delicious', i18n.lookup('cascade.nope.tacos', nil, true)
118
122
  assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy', nil, true)
123
+ assert_nil i18n.lookup('cascade.another.nope.crazy', nil, false)
119
124
  assert_nil i18n.lookup('cascade.nahhhhhh')
120
125
  end
121
126
  end
@@ -156,6 +161,7 @@ describe Tater do
156
161
  end
157
162
 
158
163
  it 'cascades lookups' do
164
+ assert_equal 'Tater lookup failed: en.cascade.another.nope.crazy', i18n.translate('cascade.another.nope.crazy', cascade: false)
159
165
  assert_equal 'Tater lookup failed: en.cascade.nope.tacos', i18n.translate('cascade.nope.tacos')
160
166
  assert_equal 'Delicious', i18n.translate('cascade.nope.tacos', cascade: true)
161
167
  end
@@ -182,6 +188,35 @@ describe Tater do
182
188
  Tater.new(path: File.expand_path('test/fixtures'))
183
189
  end
184
190
 
191
+ let :fr do
192
+ Tater.new(path: File.expand_path('test/fixtures'), locale: 'fr')
193
+ end
194
+
195
+ it 'localizes arrays' do
196
+ assert_equal 'tacos and burritos', i18n.localize(%w[tacos burritos])
197
+ assert_equal 'tacos', i18n.localize(%w[tacos])
198
+ assert_equal 'tacos, enchiladas, and burritos', i18n.localize(%w[tacos enchiladas burritos])
199
+
200
+ assert_equal 'tacos + enchiladas ++ burritos', fr.localize(%w[tacos enchiladas burritos], words_connector: ' + ', last_word_connector: ' ++ ')
201
+ assert_equal 'tacostwoburritos', fr.localize(%w[tacos burritos], two_words_connector: 'two')
202
+
203
+ assert_raises(Tater::MissingLocalizationFormat) do
204
+ fr.localize(%w[tacos burritos])
205
+ end
206
+
207
+ assert_raises(Tater::MissingLocalizationFormat) do
208
+ fr.localize(%w[tacos burritos], last_word_connector: 'last', words_connector: 'words')
209
+ end
210
+
211
+ assert_raises(Tater::MissingLocalizationFormat) do
212
+ fr.localize(%w[tacos burritos], last_word_connector: 'last')
213
+ end
214
+
215
+ assert_raises(Tater::MissingLocalizationFormat) do
216
+ fr.localize(%w[tacos burritos], words_connector: 'words')
217
+ end
218
+ end
219
+
185
220
  it 'localizes Dates' do
186
221
  assert_equal '1970/1/1', i18n.localize(Date.new(1970, 1, 1))
187
222
  end
@@ -332,4 +367,29 @@ describe Tater do
332
367
  assert cascade.cascades?
333
368
  end
334
369
  end
370
+
371
+ describe '#includes?' do
372
+ let :i18n do
373
+ Tater.new(path: File.expand_path('test/fixtures'))
374
+ end
375
+
376
+ it 'tells you if you have a translation' do
377
+ assert i18n.includes?('deep')
378
+ assert i18n.includes?('deep.key')
379
+ refute i18n.includes?('deep.nope')
380
+ refute i18n.includes?('nope')
381
+ end
382
+
383
+ it 'allows overriding the locale' do
384
+ assert i18n.includes?('french', locale: 'fr')
385
+ assert i18n.includes?('french', locales: %w[en fr])
386
+ refute i18n.includes?('french', locales: %w[en])
387
+ refute i18n.includes?('french')
388
+ end
389
+
390
+ it 'allows cascading' do
391
+ assert i18n.includes?('cascade.nope.tacos', cascade: true)
392
+ refute i18n.includes?('cascade.nope.tacos', cascade: false)
393
+ end
394
+ end
335
395
  end
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.1.0
4
+ version: 1.3.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-09 00:00:00.000000000 Z
11
+ date: 2020-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rubocop
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -38,6 +52,20 @@ dependencies:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-performance
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
41
69
  description: Minimal internationalization and localization library.
42
70
  email:
43
71
  - evan@lecklider.com
@@ -74,13 +102,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
102
  - !ruby/object:Gem::Version
75
103
  version: '2.0'
76
104
  requirements: []
77
- rubygems_version: 3.0.4
105
+ rubygems_version: 3.1.4
78
106
  signing_key:
79
107
  specification_version: 4
80
108
  summary: Minimal internationalization and localization library.
81
109
  test_files:
82
- - test/fixtures/messages/more.yml
83
110
  - test/fixtures/ruby.rb
84
- - test/fixtures/fixtures.yml
85
111
  - test/fixtures/another.yml
112
+ - test/fixtures/fixtures.yml
113
+ - test/fixtures/messages/more.yml
86
114
  - test/tater_test.rb