tater 2.0.3 → 3.0.2

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.org +2 -2
  3. data/lib/tater.rb +60 -137
  4. data/test/tater_test.rb +29 -14
  5. metadata +21 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e5ec34021f3488e2ef8ef3365abc37258813e5d9a79b8dbd0a49fb8254c946a
4
- data.tar.gz: 8613b2a54d59d0b4a64f84790e60f55316447714d6013a6751041a2aa9dada6a
3
+ metadata.gz: b543d8ed6a28b2f4059b2c1fc63593bc2f7ced7b2cc9d62e786c78263b97ec35
4
+ data.tar.gz: 0042276af4078a436e7f34e1f50500e5dd92649ae060b24921a98f5dfc859789
5
5
  SHA512:
6
- metadata.gz: f65062d798ace02b68084b1a9189f09c092e32f2a819bd88909d651038a205a36b60816f8c6e890614c9aaea1a17e074b89a7f3a5163969bf10c6f35a8eac56e
7
- data.tar.gz: cf3ae7da9a2d911f90aa93dd1f2e088425becf21d2b5558c485d86002bcccf09a4bdccfed39385442a7865a27e76196aa68bf90b259ea30f5b8beeda8eb1807c
6
+ metadata.gz: cd927d5425f5a8b7356fafb72db449d57494042369eb9ba0018a2ba7d68893fe24c0d633d75a93ab8ff1d3e8f2367d148e87c5bc068ec1717afa27d1168cec8c
7
+ data.tar.gz: 4f0a917429aef2c443288f9c338a0471f86fb608585e29e5fdc8b3f04b8ecfe654ecdc7e09daed29e99b19ddb3bf1a4175057d6db77c64b495b25a232f2c6eb7
data/README.org CHANGED
@@ -212,8 +212,8 @@ i18n.translate('login.special.description', cascade: true) # => 'Normal descript
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.
data/lib/tater.rb CHANGED
@@ -3,97 +3,24 @@ require 'bigdecimal'
3
3
  require 'date'
4
4
  require 'time'
5
5
  require 'yaml'
6
+ require_relative 'tater/utils'
7
+ require_relative 'tater/hash' unless Hash.method_defined?(:except)
6
8
 
7
9
  # Tater is a internationalization (i18n) and localization (l10n) library
8
10
  # designed for speed and simplicity.
9
11
  class Tater
10
12
  class MissingLocalizationFormat < ArgumentError; end
11
-
12
13
  class UnLocalizableObject < ArgumentError; end
13
14
 
14
- module Utils # :nodoc:
15
- # Merge all the way down.
16
- #
17
- # @param to [Hash]
18
- # The target Hash to merge into.
19
- # @param from [Hash]
20
- # The Hash to copy values from.
21
- # @return [Hash]
22
- def self.deep_merge(to, from)
23
- to.merge(from) do |_key, left, right|
24
- if left.is_a?(Hash) && right.is_a?(Hash)
25
- Utils.deep_merge(left, right)
26
- else
27
- right
28
- end
29
- end
30
- end
31
-
32
- # Transform keys all the way down.
33
- #
34
- # @param hash [Hash]
35
- # The Hash to stringify keys for.
36
- # @return [Hash]
37
- def self.deep_stringify_keys(hash)
38
- hash.transform_keys(&:to_s).transform_values do |value|
39
- if value.is_a?(Hash)
40
- Utils.deep_stringify_keys(value)
41
- else
42
- value
43
- end
44
- end
45
- end
46
-
47
- # Freeze all the way down.
48
- #
49
- # @param hash [Hash]
50
- # @return [Hash]
51
- def self.deep_freeze(hash)
52
- hash.transform_keys(&:freeze).transform_values do |value|
53
- if value.is_a?(Hash)
54
- Utils.deep_freeze(value)
55
- else
56
- value.freeze
57
- end
58
- end.freeze
59
- end
60
-
61
- # Try to interpolate these things, if one of them is a string.
62
- #
63
- # @param string [String]
64
- # The target string to interpolate into.
65
- # @param options [Hash]
66
- # The values to interpolate into the target string.
67
- #
68
- # @return [String]
69
- def self.interpolate(string, options = HASH)
70
- return string unless string.is_a?(String)
71
- return string if options.empty?
72
-
73
- format(string, options)
74
- end
75
-
76
- # Convert a Numeric to a string, particularly formatting BigDecimals to a
77
- # Float-like string representation.
78
- #
79
- # @param numeric [Numeric]
80
- #
81
- # @return [String]
82
- def self.string_from_numeric(numeric)
83
- if numeric.is_a?(BigDecimal)
84
- numeric.to_s('F')
85
- else
86
- numeric.to_s
87
- end
88
- end
89
- end
90
-
91
15
  DEFAULT = 'default'
92
16
  DELIMITING_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/.freeze
93
17
  HASH = {}.freeze
94
18
  SEPARATOR = '.'
95
19
  SUBSTITUTION_REGEX = /%(|\^)[aAbBpP]/.freeze
96
20
 
21
+ # Needed for Ruby < 3.
22
+ using HashExcept unless Hash.method_defined?(:except)
23
+
97
24
  # @return [String]
98
25
  attr_reader :locale
99
26
 
@@ -109,7 +36,6 @@ class Tater
109
36
  # @param path [String]
110
37
  # A path to search for YAML or Ruby files to load messages from.
111
38
  def initialize(cascade: false, locale: nil, messages: nil, path: nil)
112
- @cache = {}
113
39
  @cascade = cascade
114
40
  @locale = locale
115
41
  @messages = {}
@@ -155,7 +81,7 @@ class Tater
155
81
  end
156
82
 
157
83
  Dir.glob(File.join(path, '**', '*.rb')).each do |file|
158
- @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(IO.read(file), binding, file))) # rubocop:disable Security/Eval
84
+ @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(File.read(file), binding, file))) # rubocop:disable Security/Eval
159
85
  end
160
86
  end
161
87
 
@@ -167,8 +93,10 @@ class Tater
167
93
 
168
94
  # Not only does this clear our cache but it establishes the basic structure
169
95
  # that we rely on in other methods.
96
+ @cache = {}
97
+
170
98
  @messages.each_key do |key|
171
- @cache[key] = { true => {}, false => {} }
99
+ @cache[key] = { false => {}, true => {} }
172
100
  end
173
101
  end
174
102
 
@@ -219,41 +147,48 @@ class Tater
219
147
  raise(UnLocalizableObject, "The object class #{ object.class } cannot be localized by Tater.")
220
148
  end
221
149
  end
222
- alias l localize
223
150
 
224
151
  # Lookup a key in the messages hash, using the current locale or an override.
225
152
  #
153
+ # @example Using the default locale, look up a key's value.
154
+ # i18n = Tater.new(locale: 'en', messages: { 'en' => { 'greeting' => { 'world' => 'Hello, world!' } } })
155
+ # i18n.lookup('greeting.world') # => "Hello, world!"
156
+ #
226
157
  # @param key [String]
158
+ # The period-separated key path to look for within our messages.
227
159
  # @param locale [String]
228
- # A locale to use instead of our current one.
160
+ # A locale to use instead of our current one, if any.
229
161
  # @param cascade [Boolean]
230
162
  # A boolean to forcibly set the cascade option for this lookup.
231
163
  #
232
164
  # @return
233
165
  # Basically anything that can be stored in your messages Hash.
234
166
  def lookup(key, locale: nil, cascade: nil)
235
- locale = locale.nil? ? @locale : locale
236
- cascade = cascade.nil? ? @cascade : cascade
237
-
238
- cached(key, locale, cascade) || begin
239
- return nil unless @messages.key?(locale.to_s)
167
+ locale =
168
+ if locale.nil?
169
+ @locale
170
+ else
171
+ locale.to_s
172
+ end
240
173
 
241
- path = key.split(SEPARATOR).prepend(locale).map(&:to_s)
174
+ cascade = @cascade if cascade.nil?
242
175
 
243
- message =
244
- if cascade
245
- while path.length >= 2
246
- attempt = @messages.dig(*path)
176
+ @cache[locale][cascade][key] ||= begin
177
+ path = key.split(SEPARATOR)
247
178
 
248
- break attempt unless attempt.nil?
179
+ message = @messages[locale].dig(*path)
249
180
 
181
+ if message.nil? && cascade
182
+ message =
183
+ while path.length > 1
250
184
  path.delete_at(path.length - 2)
185
+ attempt = @messages[locale].dig(*path)
186
+
187
+ break attempt unless attempt.nil?
251
188
  end
252
- else
253
- @messages.dig(*path)
254
- end
189
+ end
255
190
 
256
- cache(key, locale, cascade, message)
191
+ message
257
192
  end
258
193
  end
259
194
 
@@ -316,53 +251,41 @@ class Tater
316
251
  # The translated and interpreted string, if found, or any data at the
317
252
  # defined key.
318
253
  def translate(key, options = HASH)
319
- message =
320
- if options.key?(:locales)
321
- options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
254
+ if options.empty?
255
+ message = lookup(key)
322
256
 
323
- options[:locales].find do |accept|
324
- found = lookup(key, locale: accept, cascade: options[:cascade])
325
-
326
- break found unless found.nil?
327
- end
257
+ if message.is_a?(Proc) # rubocop:disable Style/CaseLikeIf
258
+ message.call(key)
259
+ elsif message.is_a?(String)
260
+ message
328
261
  else
329
- lookup(key, locale: options[:locale], cascade: options[:cascade])
262
+ "Tater lookup failed: #{ locale }.#{ key }"
330
263
  end
264
+ else
265
+ message =
266
+ if options.key?(:locales)
267
+ options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
331
268
 
332
- # Call procs that should return a string.
333
- message = message.call(key, options) if message.is_a?(Proc)
334
-
335
- Utils.interpolate(message, options) || options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
336
- end
337
- alias t translate
269
+ options[:locales].find do |accept|
270
+ found = lookup(key, locale: accept, cascade: options[:cascade])
338
271
 
339
- private
272
+ break found unless found.nil?
273
+ end
274
+ else
275
+ lookup(key, locale: options[:locale], cascade: options[:cascade])
276
+ end
340
277
 
341
- # @param key [String]
342
- # The cache key, often in the form "something.nested.like.this"
343
- # @param locale [String]
344
- # The locale to store the value for.
345
- # @param cascade [Boolean]
346
- # Was this a cascading lookup?
347
- # @param message [String]
348
- # The message being cached, often a String.
349
- # @return [String]
350
- # Whatever value is being cached, often a String.
351
- def cache(key, locale, cascade, message)
352
- @cache[locale][cascade][key] = message
278
+ if message.is_a?(Proc) # rubocop:disable Style/CaseLikeIf
279
+ message.call(key, options.except(:cascade, :default, :locale, :locales))
280
+ elsif message.is_a?(String)
281
+ Utils.interpolate(message, options.except(:cascade, :default, :locale, :locales))
282
+ else
283
+ options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
284
+ end
285
+ end
353
286
  end
354
287
 
355
- # @param key [String]
356
- # The cache key, often in the form "something.nested.like.this"
357
- # @param locale [String]
358
- # The locale to store the value for.
359
- # @param cascade [Boolean]
360
- # Was this a cascading lookup?
361
- # @return [String, nil]
362
- # The cached message or nil.
363
- def cached(key, locale, cascade)
364
- @cache.dig(locale, cascade, key)
365
- end
288
+ private
366
289
 
367
290
  # Localize an Array object.
368
291
  #
data/test/tater_test.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require_relative '../lib/tater'
3
- require 'minitest/autorun'
2
+ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
3
+
4
4
  require 'date'
5
+ require 'minitest/autorun'
6
+ require 'tater'
5
7
 
6
8
  describe Tater do
7
9
  describe Tater::Utils do
@@ -59,6 +61,20 @@ describe Tater do
59
61
  assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal('1'))
60
62
  end
61
63
  end
64
+
65
+ describe '#interpolation_string?' do
66
+ def is?(arg)
67
+ Tater::Utils.interpolation_string?(arg)
68
+ end
69
+
70
+ it 'checks whether a string contains interpolation placeholders' do
71
+ assert is?('Hey %{there}!')
72
+ assert is?('Hey %<there>s!')
73
+ refute is?('Nah, this is fine')
74
+ refute is?("<b>HTML shouldn't count")
75
+ refute is?("A single % shouldn't count")
76
+ end
77
+ end
62
78
  end
63
79
 
64
80
  describe '#available?' do
@@ -141,6 +157,10 @@ describe Tater do
141
157
  assert_nil i18n.lookup('nope')
142
158
  end
143
159
 
160
+ it 'returns arbitrary data at keys' do
161
+ assert_equal({ 'key' => 'This key is deeper' }, i18n.lookup('deep'))
162
+ end
163
+
144
164
  it 'cascades' do
145
165
  assert_equal 'Delicious', i18n.lookup('cascade.nope.tacos', cascade: true)
146
166
  assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy', cascade: true)
@@ -162,8 +182,8 @@ describe Tater do
162
182
  assert_equal 'This key is deeper', i18n.translate('deep.key')
163
183
  end
164
184
 
165
- it 'returns a hash for nested keys' do
166
- assert_equal({ 'key' => 'This key is deeper' }, i18n.translate('deep'))
185
+ it 'does not return a hash for nested keys' do
186
+ assert_equal 'Tater lookup failed: en.deep', i18n.translate('deep')
167
187
  end
168
188
 
169
189
  it 'interpolates additional variables' do
@@ -180,10 +200,6 @@ describe Tater do
180
200
  assert_equal 'Tater lookup failed: en.nope', i18n.translate('nope')
181
201
  end
182
202
 
183
- it 'is aliased as t' do
184
- assert_equal 'This is a title', i18n.t('title')
185
- end
186
-
187
203
  it 'cascades lookups' do
188
204
  assert_equal 'Tater lookup failed: en.cascade.another.nope.crazy', i18n.translate('cascade.another.nope.crazy', cascade: false)
189
205
  assert_equal 'Tater lookup failed: en.cascade.nope.tacos', i18n.translate('cascade.nope.tacos')
@@ -201,9 +217,12 @@ describe Tater do
201
217
  assert_equal 'Tater lookup failed: ["fr", "en"].neither', i18n.translate('neither', locales: %w[fr en])
202
218
  end
203
219
 
204
- it 'finds Ruby files as well' do
220
+ it 'finds Ruby files' do
205
221
  assert_equal 'Hey ruby!', i18n.translate('ruby')
206
- assert_equal 'Hey options!', i18n.translate('options', options: 'options')
222
+ end
223
+
224
+ it 'does not interpolate messages returned by procs' do
225
+ assert_equal 'Hey %{options}!', i18n.translate('options', options: 'options')
207
226
  end
208
227
  end
209
228
 
@@ -314,10 +333,6 @@ describe Tater do
314
333
  end
315
334
  end
316
335
 
317
- it 'is aliased l' do
318
- assert_equal '1970/1/1', i18n.l(Date.new(1970, 1, 1))
319
- end
320
-
321
336
  describe 'month, day, and AM/PM names' do
322
337
  let :i18n do
323
338
  Tater.new(path: File.expand_path('test/fixtures'), locale: 'fr')
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: 2.0.3
4
+ version: 3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Lecklider
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-06-22 00:00:00.000000000 Z
11
+ date: 2021-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-packaging
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'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: rubocop-performance
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -128,8 +142,9 @@ licenses:
128
142
  - MIT
129
143
  metadata:
130
144
  bug_tracker_uri: https://github.com/evanleck/tater/issues
145
+ rubygems_mfa_required: 'true'
131
146
  source_code_uri: https://github.com/evanleck/tater
132
- post_install_message:
147
+ post_install_message:
133
148
  rdoc_options: []
134
149
  require_paths:
135
150
  - lib
@@ -144,8 +159,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
159
  - !ruby/object:Gem::Version
145
160
  version: '2.0'
146
161
  requirements: []
147
- rubygems_version: 3.2.15
148
- signing_key:
162
+ rubygems_version: 3.2.32
163
+ signing_key:
149
164
  specification_version: 4
150
165
  summary: Minimal internationalization and localization library.
151
166
  test_files: