tater 2.0.3 → 3.0.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 +4 -4
- data/README.org +2 -2
- data/lib/tater.rb +60 -137
- data/test/tater_test.rb +29 -14
- metadata +21 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b543d8ed6a28b2f4059b2c1fc63593bc2f7ced7b2cc9d62e786c78263b97ec35
|
4
|
+
data.tar.gz: 0042276af4078a436e7f34e1f50500e5dd92649ae060b24921a98f5dfc859789
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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. =
|
216
|
-
2. =
|
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(
|
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] = {
|
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 =
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
167
|
+
locale =
|
168
|
+
if locale.nil?
|
169
|
+
@locale
|
170
|
+
else
|
171
|
+
locale.to_s
|
172
|
+
end
|
240
173
|
|
241
|
-
|
174
|
+
cascade = @cascade if cascade.nil?
|
242
175
|
|
243
|
-
|
244
|
-
|
245
|
-
while path.length >= 2
|
246
|
-
attempt = @messages.dig(*path)
|
176
|
+
@cache[locale][cascade][key] ||= begin
|
177
|
+
path = key.split(SEPARATOR)
|
247
178
|
|
248
|
-
|
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
|
-
|
253
|
-
@messages.dig(*path)
|
254
|
-
end
|
189
|
+
end
|
255
190
|
|
256
|
-
|
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
|
-
|
320
|
-
|
321
|
-
options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
|
254
|
+
if options.empty?
|
255
|
+
message = lookup(key)
|
322
256
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
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
|
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
|
-
|
333
|
-
|
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
|
-
|
272
|
+
break found unless found.nil?
|
273
|
+
end
|
274
|
+
else
|
275
|
+
lookup(key, locale: options[:locale], cascade: options[:cascade])
|
276
|
+
end
|
340
277
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
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
|
-
|
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
|
-
|
3
|
-
|
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 '
|
166
|
-
assert_equal
|
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
|
220
|
+
it 'finds Ruby files' do
|
205
221
|
assert_equal 'Hey ruby!', i18n.translate('ruby')
|
206
|
-
|
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:
|
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-
|
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.
|
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:
|