tater 2.0.1 → 3.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.
- checksums.yaml +4 -4
- data/{README.md → README.org} +66 -84
- data/lib/tater.rb +159 -191
- data/test/tater_test.rb +52 -19
- metadata +64 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c34d7058c3566313243c4ccb7b99d967c60bf2e00e091d1fbb66dfbd257e853f
|
4
|
+
data.tar.gz: 0a5c1d4df6a34b9953086af0459af4c72cb8d9faf674d3216e1eb9e00aa0e82f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac657b28f3e3421def12e3d0b50bcd895c5d1a1b23de7c97da6bf0e20bc5e0a51e20abf7394ec9e99b2137e9b2261392213f0dc85d2d3b0c598c4e73184d8857
|
7
|
+
data.tar.gz: 4b5b32cf7b23a66afcde0a73f129a76d791f62a8efa45a7223204d9e017328796cf528ad8884a2c25a568ea47d3047fff57c4973741aaae20e1e7574350a9068
|
data/{README.md → README.org}
RENAMED
@@ -1,42 +1,39 @@
|
|
1
|
-
|
1
|
+
* Tater
|
2
2
|
|
3
|
-
[
|
4
|
-
[](https://travis-ci.com/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
|
11
|
-
lookups,
|
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
|
-
|
18
|
+
#+begin_src ruby
|
21
19
|
gem 'tater'
|
22
|
-
|
20
|
+
#+end_src
|
23
21
|
|
24
22
|
And then execute:
|
25
23
|
|
26
|
-
|
24
|
+
#+begin_src sh
|
27
25
|
bundle
|
28
|
-
|
26
|
+
#+end_src
|
29
27
|
|
30
28
|
Or install it yourself by running:
|
31
29
|
|
32
|
-
|
30
|
+
#+begin_src sh
|
33
31
|
gem install tater
|
34
|
-
|
35
|
-
|
32
|
+
#+end_src
|
36
33
|
|
37
|
-
|
34
|
+
** Usage
|
38
35
|
|
39
|
-
|
36
|
+
#+begin_src ruby
|
40
37
|
require 'tater'
|
41
38
|
|
42
39
|
messages = {
|
@@ -59,59 +56,56 @@ i18n.translate('some.key') # => 'This here string!'
|
|
59
56
|
|
60
57
|
# Interpolation:
|
61
58
|
i18n.translate('interpolated', you: 'world') # => 'Hello world!'
|
62
|
-
|
59
|
+
#+end_src
|
63
60
|
|
64
|
-
|
65
|
-
## Array localization
|
61
|
+
** Array localization
|
66
62
|
|
67
63
|
Given an array, Tater will do it's best to join the elements of the array into a
|
68
64
|
sentence based on how many elements there are.
|
69
65
|
|
70
|
-
|
66
|
+
#+begin_example
|
71
67
|
en:
|
72
68
|
array:
|
73
69
|
last_word_connector: ", and "
|
74
70
|
two_words_connector: " and "
|
75
71
|
words_connector: ", "
|
76
|
-
|
72
|
+
#+end_example
|
77
73
|
|
78
|
-
|
74
|
+
#+begin_src ruby
|
79
75
|
i18n.localize(%w[tacos enchiladas burritos]) # => "tacos, enchiladas, and burritos"
|
80
|
-
|
81
|
-
|
76
|
+
#+end_src
|
82
77
|
|
83
|
-
|
78
|
+
** Numeric localization
|
84
79
|
|
85
|
-
Numeric localization (
|
80
|
+
Numeric localization (=Numeric=, =Integer=, =Float=, and =BigDecimal=) require
|
86
81
|
filling in a separator and delimiter. For example:
|
87
82
|
|
88
|
-
|
83
|
+
#+begin_example
|
89
84
|
en:
|
90
85
|
numeric:
|
91
86
|
delimiter: ','
|
92
87
|
separator: '.'
|
93
|
-
|
88
|
+
#+end_example
|
94
89
|
|
95
90
|
With that, you can do things like this:
|
96
91
|
|
97
|
-
|
92
|
+
#+begin_src ruby
|
98
93
|
i18n.localize(1000.2) # => "1,000.20"
|
99
|
-
|
94
|
+
#+end_src
|
100
95
|
|
101
96
|
The separator and delimiter can also be passed in per-call:
|
102
97
|
|
103
|
-
|
98
|
+
#+begin_src ruby
|
104
99
|
i18n.localize(1000.2, delimiter: '_', separator: '+') # => "1_000+20"
|
105
|
-
|
100
|
+
#+end_src
|
106
101
|
|
102
|
+
** Date and time localization
|
107
103
|
|
108
|
-
|
109
|
-
|
110
|
-
Date and time localization (`Date`, `Time`, and `DateTime`) require filling in
|
104
|
+
Date and time localization (=Date=, =Time=, and =DateTime=) require filling in
|
111
105
|
all of the needed names and abbreviations for days and months. Here's the
|
112
106
|
example for French, which is used in the tests.
|
113
107
|
|
114
|
-
|
108
|
+
#+begin_example
|
115
109
|
fr:
|
116
110
|
time:
|
117
111
|
am: 'am'
|
@@ -174,28 +168,27 @@ fr:
|
|
174
168
|
- oct.
|
175
169
|
- nov.
|
176
170
|
- déc.
|
177
|
-
|
171
|
+
#+end_example
|
178
172
|
|
179
|
-
The statically defined keys for dates are
|
180
|
-
and
|
181
|
-
you plan on using the
|
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.
|
182
176
|
|
183
177
|
With all of that, you can do something like:
|
184
178
|
|
185
|
-
|
179
|
+
#+begin_src ruby
|
186
180
|
i18n.localize(Date.new(1970, 1, 1), format: '%A') # => 'jeudi'
|
187
181
|
|
188
182
|
# Or, using a key defined in "formats":
|
189
183
|
i18n.localize(Date.new(1970, 1, 1), format: 'day') # => 'jeudi'
|
190
|
-
|
191
|
-
|
184
|
+
#+end_src
|
192
185
|
|
193
|
-
|
186
|
+
** Cascading lookups
|
194
187
|
|
195
|
-
Lookups can be cascaded, i.e.
|
188
|
+
Lookups can be cascaded, i.e. pieces of the scope of the can be lopped off
|
196
189
|
incrementally.
|
197
190
|
|
198
|
-
|
191
|
+
#+begin_src ruby
|
199
192
|
messages = {
|
200
193
|
'en' => {
|
201
194
|
'login' => {
|
@@ -214,42 +207,40 @@ i18n.translate('login.special.title') # => 'Special Login'
|
|
214
207
|
i18n.translate('login.special.description') # => 'Tater lookup failed'
|
215
208
|
|
216
209
|
i18n.translate('login.special.description', cascade: true) # => 'Normal description.'
|
217
|
-
|
210
|
+
#+end_src
|
218
211
|
|
219
212
|
With cascade, the final key stays the same, but pieces of the scope get lopped
|
220
213
|
off. In this case, lookups will be tried in this order:
|
221
214
|
|
222
|
-
1.
|
223
|
-
2.
|
215
|
+
1. ='login.special.description'=
|
216
|
+
2. ='login.description'=
|
224
217
|
|
225
218
|
This can be useful when you want to override some messages but don't want to
|
226
219
|
have to copy all of the other, non-overwritten messages.
|
227
220
|
|
228
221
|
Cascading can also be enabled by default when initializing an instance of Tater.
|
229
222
|
|
230
|
-
|
223
|
+
#+begin_src ruby
|
231
224
|
Tater.new(cascade: true)
|
232
|
-
|
225
|
+
#+end_src
|
233
226
|
|
234
227
|
Cascading is off by default.
|
235
228
|
|
236
|
-
|
237
|
-
## Defaults
|
229
|
+
** Defaults
|
238
230
|
|
239
231
|
If you'd like to default to another value in case of a missed lookup, you can
|
240
|
-
provide the
|
232
|
+
provide the =:default= option to =#translate=.
|
241
233
|
|
242
|
-
|
234
|
+
#+begin_src ruby
|
243
235
|
Tater.new.translate('nope', default: 'Yep!') # => 'Yep!'
|
244
|
-
|
236
|
+
#+end_src
|
245
237
|
|
246
|
-
|
247
|
-
## Procs and messages in Ruby
|
238
|
+
** Procs and messages in Ruby
|
248
239
|
|
249
240
|
Ruby files can be used to store messages in addition to YAML, so long as the
|
250
|
-
Ruby file returns a
|
241
|
+
Ruby file returns a =Hash= when evalled.
|
251
242
|
|
252
|
-
|
243
|
+
#+begin_src ruby
|
253
244
|
{
|
254
245
|
'en' => {
|
255
246
|
ruby: proc do |key, options = {}|
|
@@ -257,16 +248,15 @@ Ruby file returns a `Hash` when evalled.
|
|
257
248
|
end
|
258
249
|
}
|
259
250
|
}
|
260
|
-
|
261
|
-
|
251
|
+
#+end_src
|
262
252
|
|
263
|
-
|
253
|
+
** Multiple locales
|
264
254
|
|
265
255
|
If you would like to check multiple locales and pull the first matching one out,
|
266
|
-
you can pass the
|
256
|
+
you can pass the =:locales= option to initialization or the =translate= method
|
267
257
|
with an array of top-level locale keys.
|
268
258
|
|
269
|
-
|
259
|
+
#+begin_src ruby
|
270
260
|
messages = {
|
271
261
|
'en' => {
|
272
262
|
'title' => 'Login',
|
@@ -285,31 +275,23 @@ i18n.translate('description', locales: %w[fr en]) # => 'English description.'
|
|
285
275
|
i18n = Tater.new(messages: messages, locales: %w[fr en])
|
286
276
|
i18n.translate('title') # => 'la connexion'
|
287
277
|
i18n.translate('description') # => 'English description.'
|
288
|
-
|
278
|
+
#+end_src
|
289
279
|
|
290
280
|
Locales will be tried in order and whichever one matches first will be returned.
|
291
281
|
|
282
|
+
** Limitations
|
292
283
|
|
293
|
-
|
294
|
-
|
295
|
-
- 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.
|
296
285
|
- It doesn't handle pluralization yet, though it may in the future.
|
297
|
-
- It doesn't cache anything, that's up to you.
|
298
|
-
|
299
|
-
|
300
|
-
## Why?
|
301
|
-
|
302
|
-
Because [Ruby I18n][rubyi18n] is amazing and I wanted to try to create a minimum
|
303
|
-
viable implementation of the bits of I18n that I use 90% of the time. Tater is a
|
304
|
-
single file that handles the basics of lookup and interpolation.
|
305
286
|
|
287
|
+
** Why?
|
306
288
|
|
307
|
-
|
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.
|
308
292
|
|
309
|
-
|
310
|
-
[numeronym][numeronym] like I18n: "t8r". I looked at it for a while but I read
|
311
|
-
it as "tater" instead of "tee-eight-arr" so I figured I'd just name it Tater.
|
312
|
-
Tater the translator.
|
293
|
+
** Trivia
|
313
294
|
|
314
|
-
[
|
315
|
-
|
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
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require 'bigdecimal'
|
3
|
+
require 'date'
|
4
|
+
require 'time'
|
3
5
|
require 'yaml'
|
6
|
+
require 'tater/utils'
|
4
7
|
|
5
8
|
# Tater is a internationalization (i18n) and localization (l10n) library
|
6
9
|
# designed for speed and simplicity.
|
@@ -8,83 +11,6 @@ class Tater
|
|
8
11
|
class MissingLocalizationFormat < ArgumentError; end
|
9
12
|
class UnLocalizableObject < ArgumentError; end
|
10
13
|
|
11
|
-
module Utils # :nodoc:
|
12
|
-
# Merge all the way down.
|
13
|
-
#
|
14
|
-
# @param to [Hash]
|
15
|
-
# The target Hash to merge into.
|
16
|
-
# @param from [Hash]
|
17
|
-
# The Hash to copy values from.
|
18
|
-
# @return [Hash]
|
19
|
-
def self.deep_merge(to, from)
|
20
|
-
to.merge(from) do |_key, left, right|
|
21
|
-
if left.is_a?(Hash) && right.is_a?(Hash)
|
22
|
-
Utils.deep_merge(left, right)
|
23
|
-
else
|
24
|
-
right
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# Transform keys all the way down.
|
30
|
-
#
|
31
|
-
# @param hash [Hash]
|
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
|
-
if value.is_a?(Hash)
|
37
|
-
Utils.deep_stringify_keys(value)
|
38
|
-
else
|
39
|
-
value
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
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
|
-
|
58
|
-
# Try to interpolate these things, if one of them is a string.
|
59
|
-
#
|
60
|
-
# @param string [String]
|
61
|
-
# The target string to interpolate into.
|
62
|
-
# @param options [Hash]
|
63
|
-
# The values to interpolate into the target string.
|
64
|
-
#
|
65
|
-
# @return [String]
|
66
|
-
def self.interpolate(string, options = HASH)
|
67
|
-
return string unless string.is_a?(String)
|
68
|
-
return string if options.empty?
|
69
|
-
|
70
|
-
format(string, options)
|
71
|
-
end
|
72
|
-
|
73
|
-
# Convert a Numeric to a string, particularly formatting BigDecimals to a
|
74
|
-
# Float-like string representation.
|
75
|
-
#
|
76
|
-
# @param numeric [Numeric]
|
77
|
-
#
|
78
|
-
# @return [String]
|
79
|
-
def self.string_from_numeric(numeric)
|
80
|
-
if numeric.is_a?(BigDecimal)
|
81
|
-
numeric.to_s('F')
|
82
|
-
else
|
83
|
-
numeric.to_s
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
14
|
DEFAULT = 'default'
|
89
15
|
DELIMITING_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/.freeze
|
90
16
|
HASH = {}.freeze
|
@@ -106,7 +32,6 @@ class Tater
|
|
106
32
|
# @param path [String]
|
107
33
|
# A path to search for YAML or Ruby files to load messages from.
|
108
34
|
def initialize(cascade: false, locale: nil, messages: nil, path: nil)
|
109
|
-
@cache = {}
|
110
35
|
@cascade = cascade
|
111
36
|
@locale = locale
|
112
37
|
@messages = {}
|
@@ -152,7 +77,7 @@ class Tater
|
|
152
77
|
end
|
153
78
|
|
154
79
|
Dir.glob(File.join(path, '**', '*.rb')).each do |file|
|
155
|
-
@messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(
|
80
|
+
@messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(File.read(file), binding, file))) # rubocop:disable Security/Eval
|
156
81
|
end
|
157
82
|
end
|
158
83
|
|
@@ -164,8 +89,10 @@ class Tater
|
|
164
89
|
|
165
90
|
# Not only does this clear our cache but it establishes the basic structure
|
166
91
|
# that we rely on in other methods.
|
92
|
+
@cache = {}
|
93
|
+
|
167
94
|
@messages.each_key do |key|
|
168
|
-
@cache[key] = {
|
95
|
+
@cache[key] = { false => {}, true => {} }
|
169
96
|
end
|
170
97
|
end
|
171
98
|
|
@@ -189,9 +116,9 @@ class Tater
|
|
189
116
|
# @option options [String] :locale
|
190
117
|
# The locale to use in lieu of the current default.
|
191
118
|
# @option options [String] :delimiter
|
192
|
-
# The delimiter to use when localizing
|
119
|
+
# The delimiter to use when localizing numeric values.
|
193
120
|
# @option options [String] :separator
|
194
|
-
# The separator to use when localizing
|
121
|
+
# The separator to use when localizing numeric values.
|
195
122
|
# @option options [String] :two_words_connector
|
196
123
|
# The string used to join two array elements together e.g. " and ".
|
197
124
|
# @option options [String] :words_connector
|
@@ -207,105 +134,57 @@ class Tater
|
|
207
134
|
when String
|
208
135
|
object
|
209
136
|
when Numeric
|
210
|
-
|
211
|
-
separator = options[:separator] || lookup('numeric.separator', locale: options[:locale])
|
212
|
-
precision = options[:precision] || 2
|
213
|
-
|
214
|
-
raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing or not passed as option :delimiter") unless delimiter
|
215
|
-
raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing or not passed as option :separator") unless separator
|
216
|
-
|
217
|
-
# Heavily cribbed from Rails.
|
218
|
-
integer, fraction = Utils.string_from_numeric(object).split('.')
|
219
|
-
integer.gsub!(DELIMITING_REGEX) do |number|
|
220
|
-
"#{ number }#{ delimiter }"
|
221
|
-
end
|
222
|
-
|
223
|
-
if precision.zero?
|
224
|
-
integer
|
225
|
-
else
|
226
|
-
[integer, fraction&.ljust(precision, '0')&.slice(0, precision)].compact.join(separator)
|
227
|
-
end
|
137
|
+
localize_numeric(object, options)
|
228
138
|
when Date, Time, DateTime
|
229
|
-
|
230
|
-
|
231
|
-
# Heavily cribbed from I18n, many thanks to the people who sorted this out
|
232
|
-
# before I worked on this library.
|
233
|
-
format = format.gsub(SUBSTITUTION_REGEX) do |match|
|
234
|
-
case match
|
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
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
object.strftime(format)
|
139
|
+
localize_datetime(object, options)
|
249
140
|
when Array
|
250
|
-
|
251
|
-
when 0
|
252
|
-
''
|
253
|
-
when 1
|
254
|
-
object[0]
|
255
|
-
when 2
|
256
|
-
two_words_connector = options[:two_words_connector] || lookup('array.two_words_connector', locale: options[:locale])
|
257
|
-
|
258
|
-
raise(MissingLocalizationFormat, "Sentence localization connector ('array.two_words_connector') missing or not passed as option :two_words_connector") unless two_words_connector
|
259
|
-
|
260
|
-
"#{ object[0] }#{ two_words_connector }#{ object[1] }"
|
261
|
-
else
|
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])
|
264
|
-
|
265
|
-
raise(MissingLocalizationFormat, "Sentence localization connector ('array.last_word_connector') missing or not passed as option :last_word_connector") unless last_word_connector
|
266
|
-
raise(MissingLocalizationFormat, "Sentence localization connector ('array.words_connector') missing or not passed as option :words_connector") unless words_connector
|
267
|
-
|
268
|
-
"#{ object[0...-1].join(words_connector) }#{ last_word_connector }#{ object[-1] }"
|
269
|
-
end
|
141
|
+
localize_array(object, options)
|
270
142
|
else
|
271
143
|
raise(UnLocalizableObject, "The object class #{ object.class } cannot be localized by Tater.")
|
272
144
|
end
|
273
145
|
end
|
274
|
-
alias l localize
|
275
146
|
|
276
147
|
# Lookup a key in the messages hash, using the current locale or an override.
|
277
148
|
#
|
149
|
+
# @example Using the default locale, look up a key's value.
|
150
|
+
# i18n = Tater.new(locale: 'en', messages: { 'en' => { 'greeting' => { 'world' => 'Hello, world!' } } })
|
151
|
+
# i18n.lookup('greeting.world') # => "Hello, world!"
|
152
|
+
#
|
278
153
|
# @param key [String]
|
154
|
+
# The period-separated key path to look for within our messages.
|
279
155
|
# @param locale [String]
|
280
|
-
# A locale to use instead of our current one.
|
156
|
+
# A locale to use instead of our current one, if any.
|
281
157
|
# @param cascade [Boolean]
|
282
158
|
# A boolean to forcibly set the cascade option for this lookup.
|
283
159
|
#
|
284
160
|
# @return
|
285
161
|
# Basically anything that can be stored in your messages Hash.
|
286
162
|
def lookup(key, locale: nil, cascade: nil)
|
287
|
-
locale =
|
288
|
-
|
163
|
+
locale =
|
164
|
+
if locale.nil?
|
165
|
+
@locale
|
166
|
+
else
|
167
|
+
locale.to_s
|
168
|
+
end
|
289
169
|
|
290
|
-
|
291
|
-
return nil unless @messages.key?(locale.to_s)
|
170
|
+
cascade = @cascade if cascade.nil?
|
292
171
|
|
293
|
-
|
172
|
+
@cache[locale][cascade][key] ||= begin
|
173
|
+
path = key.split(SEPARATOR)
|
294
174
|
|
295
|
-
message =
|
296
|
-
if cascade
|
297
|
-
while path.length >= 2
|
298
|
-
attempt = @messages.dig(*path)
|
299
|
-
|
300
|
-
break attempt unless attempt.nil?
|
175
|
+
message = @messages[locale].dig(*path)
|
301
176
|
|
177
|
+
if message.nil? && cascade
|
178
|
+
message =
|
179
|
+
while path.length > 1
|
302
180
|
path.delete_at(path.length - 2)
|
181
|
+
attempt = @messages[locale].dig(*path)
|
182
|
+
|
183
|
+
break attempt unless attempt.nil?
|
303
184
|
end
|
304
|
-
|
305
|
-
@messages.dig(*path)
|
306
|
-
end
|
185
|
+
end
|
307
186
|
|
308
|
-
|
187
|
+
message
|
309
188
|
end
|
310
189
|
end
|
311
190
|
|
@@ -368,51 +247,140 @@ class Tater
|
|
368
247
|
# The translated and interpreted string, if found, or any data at the
|
369
248
|
# defined key.
|
370
249
|
def translate(key, options = HASH)
|
371
|
-
|
372
|
-
|
373
|
-
options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
|
250
|
+
if options.empty?
|
251
|
+
message = lookup(key)
|
374
252
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
end
|
253
|
+
if message.is_a?(Proc) # rubocop:disable Style/CaseLikeIf
|
254
|
+
message.call(key)
|
255
|
+
elsif message.is_a?(String)
|
256
|
+
message
|
380
257
|
else
|
381
|
-
lookup
|
258
|
+
"Tater lookup failed: #{ locale }.#{ key }"
|
382
259
|
end
|
260
|
+
else
|
261
|
+
message =
|
262
|
+
if options.key?(:locales)
|
263
|
+
options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
|
383
264
|
|
384
|
-
|
385
|
-
|
265
|
+
options[:locales].find do |accept|
|
266
|
+
found = lookup(key, locale: accept, cascade: options[:cascade])
|
386
267
|
|
387
|
-
|
268
|
+
break found unless found.nil?
|
269
|
+
end
|
270
|
+
else
|
271
|
+
lookup(key, locale: options[:locale], cascade: options[:cascade])
|
272
|
+
end
|
273
|
+
|
274
|
+
if message.is_a?(Proc) # rubocop:disable Style/CaseLikeIf
|
275
|
+
message.call(key, options.except(:cascade, :default, :locale, :locales))
|
276
|
+
elsif message.is_a?(String)
|
277
|
+
Utils.interpolate(message, options.except(:cascade, :default, :locale, :locales))
|
278
|
+
else
|
279
|
+
options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
|
280
|
+
end
|
281
|
+
end
|
388
282
|
end
|
389
|
-
alias t translate
|
390
283
|
|
391
284
|
private
|
392
285
|
|
393
|
-
#
|
394
|
-
#
|
395
|
-
# @param
|
396
|
-
# The
|
397
|
-
# @param
|
398
|
-
#
|
399
|
-
# @param message [String]
|
400
|
-
# The message being cached, often a String.
|
286
|
+
# Localize an Array object.
|
287
|
+
#
|
288
|
+
# @param object [Array<String>]
|
289
|
+
# The array to localize.
|
290
|
+
# @param options [Hash]
|
291
|
+
# Options to configure localization.
|
401
292
|
# @return [String]
|
402
|
-
#
|
403
|
-
def
|
404
|
-
|
293
|
+
# The localize array string.
|
294
|
+
def localize_array(object, options)
|
295
|
+
case object.length
|
296
|
+
when 0
|
297
|
+
''
|
298
|
+
when 1
|
299
|
+
object[0]
|
300
|
+
when 2
|
301
|
+
two_words_connector = options[:two_words_connector] || lookup('array.two_words_connector', locale: options[:locale])
|
302
|
+
|
303
|
+
raise(MissingLocalizationFormat, "Sentence localization connector ('array.two_words_connector') missing or not passed as option :two_words_connector") unless two_words_connector
|
304
|
+
|
305
|
+
"#{ object[0] }#{ two_words_connector }#{ object[1] }"
|
306
|
+
else
|
307
|
+
last_word_connector = options[:last_word_connector] || lookup('array.last_word_connector', locale: options[:locale])
|
308
|
+
words_connector = options[:words_connector] || lookup('array.words_connector', locale: options[:locale])
|
309
|
+
|
310
|
+
raise(MissingLocalizationFormat, "Sentence localization connector ('array.last_word_connector') missing or not passed as option :last_word_connector") unless last_word_connector
|
311
|
+
raise(MissingLocalizationFormat, "Sentence localization connector ('array.words_connector') missing or not passed as option :words_connector") unless words_connector
|
312
|
+
|
313
|
+
"#{ object[0...-1].join(words_connector) }#{ last_word_connector }#{ object[-1] }"
|
314
|
+
end
|
405
315
|
end
|
406
316
|
|
407
|
-
#
|
408
|
-
#
|
409
|
-
# @param
|
410
|
-
# The
|
411
|
-
# @param
|
412
|
-
#
|
413
|
-
# @return [String
|
414
|
-
# The
|
415
|
-
def
|
416
|
-
|
317
|
+
# Localize a Date, DateTime, or Time object.
|
318
|
+
#
|
319
|
+
# @param object [Date, DateTime, Time]
|
320
|
+
# The date-ish object to localize.
|
321
|
+
# @param options [Hash]
|
322
|
+
# Options to configure localization.
|
323
|
+
# @return [String]
|
324
|
+
# The localized date string.
|
325
|
+
def localize_datetime(object, options)
|
326
|
+
frmt = options[:format] || DEFAULT
|
327
|
+
loc = options[:locale]
|
328
|
+
format = lookup("#{ object.class.to_s.downcase }.formats.#{ frmt }", locale: loc) || frmt
|
329
|
+
|
330
|
+
# Heavily cribbed from I18n, many thanks to the people who sorted this out
|
331
|
+
# before I worked on this library.
|
332
|
+
format = format.gsub(SUBSTITUTION_REGEX) do |match|
|
333
|
+
case match
|
334
|
+
when '%a' then lookup('date.abbreviated_days', locale: loc)[object.wday]
|
335
|
+
when '%^a' then lookup('date.abbreviated_days', locale: loc)[object.wday].upcase
|
336
|
+
when '%A' then lookup('date.days', locale: loc)[object.wday]
|
337
|
+
when '%^A' then lookup('date.days', locale: loc)[object.wday].upcase
|
338
|
+
when '%b' then lookup('date.abbreviated_months', locale: loc)[object.mon - 1]
|
339
|
+
when '%^b' then lookup('date.abbreviated_months', locale: loc)[object.mon - 1].upcase
|
340
|
+
when '%B' then lookup('date.months', locale: loc)[object.mon - 1]
|
341
|
+
when '%^B' then lookup('date.months', locale: loc)[object.mon - 1].upcase
|
342
|
+
when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale: loc).upcase if object.respond_to?(:hour)
|
343
|
+
when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale: loc).downcase if object.respond_to?(:hour)
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
if format.include?('%')
|
348
|
+
object.strftime(format)
|
349
|
+
else
|
350
|
+
format
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
# Localize a Numeric object.
|
355
|
+
#
|
356
|
+
# @param object [Array<String>, Date, Time, DateTime, Numeric]
|
357
|
+
# The object to localize.
|
358
|
+
# @param options [Hash]
|
359
|
+
# Options to configure localization.
|
360
|
+
# @return [String]
|
361
|
+
# The localized numeric string.
|
362
|
+
def localize_numeric(object, options)
|
363
|
+
delimiter = options[:delimiter] || lookup('numeric.delimiter', locale: options[:locale])
|
364
|
+
separator = options[:separator] || lookup('numeric.separator', locale: options[:locale])
|
365
|
+
precision = options[:precision] || 2
|
366
|
+
|
367
|
+
raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing or not passed as option :delimiter") unless delimiter
|
368
|
+
raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing or not passed as option :separator") unless separator
|
369
|
+
|
370
|
+
# Break the number up into integer and fraction parts.
|
371
|
+
integer = Utils.string_from_numeric(object)
|
372
|
+
integer, fraction = integer.split('.') unless object.is_a?(Integer)
|
373
|
+
|
374
|
+
if object >= 1_000
|
375
|
+
integer.gsub!(DELIMITING_REGEX) do |number|
|
376
|
+
"#{ number }#{ delimiter }"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
if precision.zero? || fraction.nil?
|
381
|
+
integer
|
382
|
+
else
|
383
|
+
"#{ integer }#{ separator }#{ fraction.ljust(precision, '0').slice(0, precision) }"
|
384
|
+
end
|
417
385
|
end
|
418
386
|
end
|
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
|
@@ -118,7 +134,7 @@ describe Tater do
|
|
118
134
|
end
|
119
135
|
|
120
136
|
it 'updates the available list when new messages are loaded' do
|
121
|
-
i18n.load(messages: { 'added' => { 'hey' => 'yeah' }})
|
137
|
+
i18n.load(messages: { 'added' => { 'hey' => 'yeah' } })
|
122
138
|
|
123
139
|
assert_equal %w[en delimiter_only separator_only fr added].sort, i18n.available.sort
|
124
140
|
end
|
@@ -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
|
|
@@ -265,10 +284,28 @@ describe Tater do
|
|
265
284
|
assert_equal '1NAH12', i18n.localize(BigDecimal('1.12'))
|
266
285
|
end
|
267
286
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
287
|
+
describe 'precision option' do
|
288
|
+
it 'defaults to 2' do
|
289
|
+
assert_equal '10NAH00', i18n.localize(BigDecimal('10'))
|
290
|
+
assert_equal '10NAH00', i18n.localize(10.0)
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'defaults to zero for integers' do
|
294
|
+
assert_equal '10', i18n.localize(10)
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'removes fractional pieces when the precision is 0' do
|
298
|
+
assert_equal '10', i18n.localize(BigDecimal('10.123456'), precision: 0)
|
299
|
+
assert_equal '10', i18n.localize(10.123456, precision: 0)
|
300
|
+
|
301
|
+
assert_equal '10', i18n.localize(BigDecimal('10.12'), precision: 0)
|
302
|
+
assert_equal '10', i18n.localize(10.12, precision: 0)
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'truncates long values to the desired precision' do
|
306
|
+
assert_equal '10NAH00', i18n.localize(BigDecimal('10.00234'))
|
307
|
+
assert_equal '10NAH00', i18n.localize(10.00234)
|
308
|
+
end
|
272
309
|
end
|
273
310
|
|
274
311
|
it 'allows overriding the delimiter and separator' do
|
@@ -296,10 +333,6 @@ describe Tater do
|
|
296
333
|
end
|
297
334
|
end
|
298
335
|
|
299
|
-
it 'is aliased l' do
|
300
|
-
assert_equal '1970/1/1', i18n.l(Date.new(1970, 1, 1))
|
301
|
-
end
|
302
|
-
|
303
336
|
describe 'month, day, and AM/PM names' do
|
304
337
|
let :i18n do
|
305
338
|
Tater.new(path: File.expand_path('test/fixtures'), locale: 'fr')
|
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:
|
4
|
+
version: 3.0.0
|
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-18 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
|
@@ -52,6 +66,34 @@ dependencies:
|
|
52
66
|
- - ">="
|
53
67
|
- !ruby/object:Gem::Version
|
54
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-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'
|
55
97
|
- !ruby/object:Gem::Dependency
|
56
98
|
name: rubocop-performance
|
57
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +108,20 @@ dependencies:
|
|
66
108
|
- - ">="
|
67
109
|
- !ruby/object:Gem::Version
|
68
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
69
125
|
description: Minimal internationalization and localization library.
|
70
126
|
email:
|
71
127
|
- evan@lecklider.com
|
@@ -74,7 +130,7 @@ extensions: []
|
|
74
130
|
extra_rdoc_files: []
|
75
131
|
files:
|
76
132
|
- LICENSE.txt
|
77
|
-
- README.
|
133
|
+
- README.org
|
78
134
|
- lib/tater.rb
|
79
135
|
- test/fixtures/another.yml
|
80
136
|
- test/fixtures/fixtures.yml
|
@@ -86,8 +142,9 @@ licenses:
|
|
86
142
|
- MIT
|
87
143
|
metadata:
|
88
144
|
bug_tracker_uri: https://github.com/evanleck/tater/issues
|
145
|
+
rubygems_mfa_required: 'true'
|
89
146
|
source_code_uri: https://github.com/evanleck/tater
|
90
|
-
post_install_message:
|
147
|
+
post_install_message:
|
91
148
|
rdoc_options: []
|
92
149
|
require_paths:
|
93
150
|
- lib
|
@@ -102,8 +159,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
159
|
- !ruby/object:Gem::Version
|
103
160
|
version: '2.0'
|
104
161
|
requirements: []
|
105
|
-
rubygems_version: 3.2.
|
106
|
-
signing_key:
|
162
|
+
rubygems_version: 3.2.32
|
163
|
+
signing_key:
|
107
164
|
specification_version: 4
|
108
165
|
summary: Minimal internationalization and localization library.
|
109
166
|
test_files:
|