tater 1.1.1 → 2.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 +53 -21
- data/lib/tater.rb +199 -73
- data/test/fixtures/fixtures.yml +5 -0
- data/test/fixtures/ruby.rb +4 -3
- data/test/tater_test.rb +101 -19
- metadata +33 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 16fddfa1cedd9af51e3d2e04422a083a16ee055e1102ad4482bfb28beee3be78
|
4
|
+
data.tar.gz: 73273faf3a6bb5842d6adc2854ca98e81363efd0c45835de07fc2e4d807effa2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9da7fc7081b2043891c83f6ef46c1facdff44f85175ce5052af04a0e6f22bb82ffb0505a29d24a7d2a91ba00e81c5b8fccdb1c6b7118de48680639e474a7f13
|
7
|
+
data.tar.gz: 0ccb2a820ea03144b96319c61eb0b645ca86002cf88c657b906669b39702dbdefb83e009a3b34fb5adda4a9fd47deaa30fbd0c0580ff34b80c9e9c5d4051a204
|
data/README.md
CHANGED
@@ -27,7 +27,7 @@ And then execute:
|
|
27
27
|
bundle
|
28
28
|
```
|
29
29
|
|
30
|
-
Or install it yourself
|
30
|
+
Or install it yourself by running:
|
31
31
|
|
32
32
|
```sh
|
33
33
|
gem install tater
|
@@ -40,15 +40,20 @@ gem install tater
|
|
40
40
|
require 'tater'
|
41
41
|
|
42
42
|
messages = {
|
43
|
-
'
|
44
|
-
'
|
45
|
-
|
46
|
-
|
43
|
+
'en' => {
|
44
|
+
'some' => {
|
45
|
+
'key' => 'This here string!'
|
46
|
+
},
|
47
|
+
'interpolated' => 'Hello %{you}!'
|
48
|
+
}
|
47
49
|
}
|
48
50
|
|
49
|
-
i18n = Tater.new
|
51
|
+
i18n = Tater.new(locale: 'en')
|
50
52
|
i18n.load(messages: messages)
|
51
53
|
|
54
|
+
# OR
|
55
|
+
i18n = Tater.new(locale: 'en', messages: messages)
|
56
|
+
|
52
57
|
# Basic lookup:
|
53
58
|
i18n.translate('some.key') # => 'This here string!'
|
54
59
|
|
@@ -56,7 +61,26 @@ i18n.translate('some.key') # => 'This here string!'
|
|
56
61
|
i18n.translate('interpolated', you: 'world') # => 'Hello world!'
|
57
62
|
```
|
58
63
|
|
59
|
-
|
64
|
+
|
65
|
+
## Array localization
|
66
|
+
|
67
|
+
Given an array, Tater will do it's best to join the elements of the array into a
|
68
|
+
sentence based on how many elements there are.
|
69
|
+
|
70
|
+
```yaml
|
71
|
+
en:
|
72
|
+
array:
|
73
|
+
last_word_connector: ", and "
|
74
|
+
two_words_connector: " and "
|
75
|
+
words_connector: ", "
|
76
|
+
```
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
i18n.localize(%w[tacos enchiladas burritos]) # => "tacos, enchiladas, and burritos"
|
80
|
+
```
|
81
|
+
|
82
|
+
|
83
|
+
## Numeric localization
|
60
84
|
|
61
85
|
Numeric localization (`Numeric`, `Integer`, `Float`, and `BigDecimal`) require
|
62
86
|
filling in a separator and delimiter. For example:
|
@@ -81,7 +105,7 @@ i18n.localize(1000.2, delimiter: '_', separator: '+') # => "1_000+20"
|
|
81
105
|
```
|
82
106
|
|
83
107
|
|
84
|
-
## Date and
|
108
|
+
## Date and time localization
|
85
109
|
|
86
110
|
Date and time localization (`Date`, `Time`, and `DateTime`) require filling in
|
87
111
|
all of the needed names and abbreviations for days and months. Here's the
|
@@ -166,24 +190,26 @@ i18n.localize(Date.new(1970, 1, 1), format: 'day') # => 'jeudi'
|
|
166
190
|
```
|
167
191
|
|
168
192
|
|
169
|
-
## Cascading
|
193
|
+
## Cascading lookups
|
170
194
|
|
171
195
|
Lookups can be cascaded, i.e. pieces of the scope of the can be lopped off
|
172
196
|
incrementally.
|
173
197
|
|
174
198
|
```ruby
|
175
199
|
messages = {
|
176
|
-
'
|
177
|
-
'
|
178
|
-
|
200
|
+
'en' => {
|
201
|
+
'login' => {
|
202
|
+
'title' => 'Login',
|
203
|
+
'description' => 'Normal description.'
|
179
204
|
|
180
|
-
|
181
|
-
|
205
|
+
'special' => {
|
206
|
+
'title' => 'Special Login'
|
207
|
+
}
|
182
208
|
}
|
183
209
|
}
|
184
210
|
}
|
185
211
|
|
186
|
-
i18n = Tater.new(messages: messages)
|
212
|
+
i18n = Tater.new(locale: 'en', messages: messages)
|
187
213
|
i18n.translate('login.special.title') # => 'Special Login'
|
188
214
|
i18n.translate('login.special.description') # => 'Tater lookup failed'
|
189
215
|
|
@@ -218,14 +244,14 @@ Tater.new.translate('nope', default: 'Yep!') # => 'Yep!'
|
|
218
244
|
```
|
219
245
|
|
220
246
|
|
221
|
-
## Procs and
|
247
|
+
## Procs and messages in Ruby
|
222
248
|
|
223
249
|
Ruby files can be used to store messages in addition to YAML, so long as the
|
224
250
|
Ruby file returns a `Hash` when evalled.
|
225
251
|
|
226
252
|
```ruby
|
227
253
|
{
|
228
|
-
en
|
254
|
+
'en' => {
|
229
255
|
ruby: proc do |key, options = {}|
|
230
256
|
"Hey #{ key }!"
|
231
257
|
end
|
@@ -234,10 +260,11 @@ Ruby file returns a `Hash` when evalled.
|
|
234
260
|
```
|
235
261
|
|
236
262
|
|
237
|
-
## Multiple
|
263
|
+
## Multiple locales
|
238
264
|
|
239
|
-
If you like to check multiple locales and pull the first matching one out,
|
240
|
-
can pass the `:locales` option
|
265
|
+
If you would like to check multiple locales and pull the first matching one out,
|
266
|
+
you can pass the `:locales` option to initialization or the `translate` method
|
267
|
+
with an array of top-level locale keys.
|
241
268
|
|
242
269
|
```ruby
|
243
270
|
messages = {
|
@@ -253,9 +280,14 @@ messages = {
|
|
253
280
|
i18n = Tater.new(messages: messages)
|
254
281
|
i18n.translate('title', locales: %w[fr en]) # => 'la connexion'
|
255
282
|
i18n.translate('description', locales: %w[fr en]) # => 'English description.'
|
283
|
+
|
284
|
+
# OR
|
285
|
+
i18n = Tater.new(messages: messages, locales: %w[fr en])
|
286
|
+
i18n.translate('title') # => 'la connexion'
|
287
|
+
i18n.translate('description') # => 'English description.'
|
256
288
|
```
|
257
289
|
|
258
|
-
Locales will tried in order and
|
290
|
+
Locales will be tried in order and whichever one matches first will be returned.
|
259
291
|
|
260
292
|
|
261
293
|
## Limitations
|
data/lib/tater.rb
CHANGED
@@ -12,14 +12,14 @@ class Tater
|
|
12
12
|
# Merge all the way down.
|
13
13
|
#
|
14
14
|
# @param to [Hash]
|
15
|
-
# The target Hash to merge into.
|
16
|
-
# not on a copy of the object.
|
15
|
+
# The target Hash to merge into.
|
17
16
|
# @param from [Hash]
|
18
17
|
# The Hash to copy values from.
|
19
|
-
|
20
|
-
|
18
|
+
# @return [Hash]
|
19
|
+
def self.deep_merge(to, from)
|
20
|
+
to.merge(from) do |_key, left, right|
|
21
21
|
if left.is_a?(Hash) && right.is_a?(Hash)
|
22
|
-
Utils.deep_merge
|
22
|
+
Utils.deep_merge(left, right)
|
23
23
|
else
|
24
24
|
right
|
25
25
|
end
|
@@ -29,18 +29,32 @@ class Tater
|
|
29
29
|
# Transform keys all the way down.
|
30
30
|
#
|
31
31
|
# @param hash [Hash]
|
32
|
-
# The Hash to
|
33
|
-
#
|
34
|
-
def self.deep_stringify_keys
|
35
|
-
hash.transform_keys
|
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
36
|
if value.is_a?(Hash)
|
37
|
-
Utils.deep_stringify_keys
|
37
|
+
Utils.deep_stringify_keys(value)
|
38
38
|
else
|
39
39
|
value
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
43
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
|
+
|
44
58
|
# Try to interpolate these things, if one of them is a string.
|
45
59
|
#
|
46
60
|
# @param string [String]
|
@@ -49,8 +63,9 @@ class Tater
|
|
49
63
|
# The values to interpolate into the target string.
|
50
64
|
#
|
51
65
|
# @return [String]
|
52
|
-
def self.interpolate(string, options =
|
66
|
+
def self.interpolate(string, options = HASH)
|
53
67
|
return string unless string.is_a?(String)
|
68
|
+
return string if options.empty?
|
54
69
|
|
55
70
|
format(string, options)
|
56
71
|
end
|
@@ -71,8 +86,8 @@ class Tater
|
|
71
86
|
end
|
72
87
|
|
73
88
|
DEFAULT = 'default'
|
74
|
-
DEFAULT_LOCALE = 'en'
|
75
89
|
DELIMITING_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/.freeze
|
90
|
+
HASH = {}.freeze
|
76
91
|
SEPARATOR = '.'
|
77
92
|
SUBSTITUTION_REGEX = /%(|\^)[aAbBpP]/.freeze
|
78
93
|
|
@@ -82,7 +97,16 @@ class Tater
|
|
82
97
|
# @return [Hash]
|
83
98
|
attr_reader :messages
|
84
99
|
|
85
|
-
|
100
|
+
# @param cascade [Boolean]
|
101
|
+
# A boolean indicating if lookups should cascade by default.
|
102
|
+
# @param locale [String]
|
103
|
+
# The default locale.
|
104
|
+
# @param messages [Hash]
|
105
|
+
# A hash of messages ready to be loaded in.
|
106
|
+
# @param path [String]
|
107
|
+
# A path to search for YAML or Ruby files to load messages from.
|
108
|
+
def initialize(cascade: false, locale: nil, messages: nil, path: nil)
|
109
|
+
@cache = {}
|
86
110
|
@cascade = cascade
|
87
111
|
@locale = locale
|
88
112
|
@messages = {}
|
@@ -98,11 +122,11 @@ class Tater
|
|
98
122
|
@cascade
|
99
123
|
end
|
100
124
|
|
101
|
-
# An array of the available locale codes.
|
125
|
+
# An array of the available locale codes found in loaded messages.
|
102
126
|
#
|
103
127
|
# @return [Array]
|
104
128
|
def available
|
105
|
-
messages.keys
|
129
|
+
@available ||= messages.keys
|
106
130
|
end
|
107
131
|
|
108
132
|
# Is this locale available in our current set of messages?
|
@@ -113,24 +137,36 @@ class Tater
|
|
113
137
|
end
|
114
138
|
|
115
139
|
# Load messages into our internal cache, either from a path containing YAML
|
116
|
-
# files or a
|
140
|
+
# files or a Hash of messages.
|
117
141
|
#
|
118
142
|
# @param path [String]
|
119
143
|
# A path to search for YAML or Ruby files to load messages from.
|
120
144
|
# @param messages [Hash]
|
121
145
|
# A hash of messages ready to be loaded in.
|
122
146
|
def load(path: nil, messages: nil)
|
147
|
+
return if path.nil? && messages.nil?
|
148
|
+
|
123
149
|
if path
|
124
150
|
Dir.glob(File.join(path, '**', '*.{yml,yaml}')).each do |file|
|
125
|
-
Utils.deep_merge
|
151
|
+
@messages = Utils.deep_merge(@messages, YAML.load_file(file))
|
126
152
|
end
|
127
153
|
|
128
154
|
Dir.glob(File.join(path, '**', '*.rb')).each do |file|
|
129
|
-
Utils.deep_merge
|
155
|
+
@messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(eval(IO.read(file), binding, file))) # rubocop:disable Security/Eval
|
130
156
|
end
|
131
157
|
end
|
132
158
|
|
133
|
-
Utils.deep_merge
|
159
|
+
@messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(messages)) if messages
|
160
|
+
@messages = Utils.deep_freeze(@messages)
|
161
|
+
|
162
|
+
# Gotta recalculate available locales after updating.
|
163
|
+
remove_instance_variable(:@available) if instance_variable_defined?(:@available)
|
164
|
+
|
165
|
+
# Not only does this clear our cache but it establishes the basic structure
|
166
|
+
# that we rely on in other methods.
|
167
|
+
@messages.each_key do |key|
|
168
|
+
@cache[key] = { true => {}, false => {} }
|
169
|
+
end
|
134
170
|
end
|
135
171
|
|
136
172
|
# Set the current locale, if it's available.
|
@@ -141,9 +177,9 @@ class Tater
|
|
141
177
|
@locale = locale.to_s if available?(locale)
|
142
178
|
end
|
143
179
|
|
144
|
-
# Localize
|
180
|
+
# Localize an Array, Date, Time, DateTime, or Numeric object.
|
145
181
|
#
|
146
|
-
# @param object [Date, Time, DateTime, Numeric]
|
182
|
+
# @param object [Array<String>, Date, Time, DateTime, Numeric]
|
147
183
|
# The object to localize.
|
148
184
|
# @param options [Hash]
|
149
185
|
# Options to configure localization.
|
@@ -156,20 +192,24 @@ class Tater
|
|
156
192
|
# The delimiter to use when localizing numberic values.
|
157
193
|
# @option options [String] :separator
|
158
194
|
# The separator to use when localizing numberic values.
|
195
|
+
# @option options [String] :two_words_connector
|
196
|
+
# The string used to join two array elements together e.g. " and ".
|
197
|
+
# @option options [String] :words_connector
|
198
|
+
# The string used to connect multiple array elements e.g. ", ".
|
199
|
+
# @option options [String] :last_word_connector
|
200
|
+
# The string used to connect the final element with preceding array elements
|
201
|
+
# e.g. ", and ".
|
159
202
|
#
|
160
203
|
# @return [String]
|
161
204
|
# A localized version of the object passed in.
|
162
|
-
def localize(object, options =
|
163
|
-
format_key = options.delete(:format) || DEFAULT
|
164
|
-
locale_override = options.delete(:locale)
|
165
|
-
|
205
|
+
def localize(object, options = HASH)
|
166
206
|
case object
|
167
207
|
when String
|
168
208
|
object
|
169
209
|
when Numeric
|
170
|
-
delimiter = options
|
171
|
-
separator = options
|
172
|
-
precision = options
|
210
|
+
delimiter = options[:delimiter] || lookup('numeric.delimiter', locale: options[:locale])
|
211
|
+
separator = options[:separator] || lookup('numeric.separator', locale: options[:locale])
|
212
|
+
precision = options[:precision] || 2
|
173
213
|
|
174
214
|
raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing or not passed as option :delimiter") unless delimiter
|
175
215
|
raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing or not passed as option :separator") unless separator
|
@@ -186,27 +226,47 @@ class Tater
|
|
186
226
|
[integer, fraction&.ljust(precision, '0')].compact.join(separator)
|
187
227
|
end
|
188
228
|
when Date, Time, DateTime
|
189
|
-
|
190
|
-
format = lookup("#{ key }.formats.#{ format_key }", locale_override) || format_key
|
229
|
+
format = lookup("#{ object.class.to_s.downcase }.formats.#{ options[:format] || DEFAULT }", locale: options[:locale]) || options[:format] || DEFAULT
|
191
230
|
|
192
231
|
# Heavily cribbed from I18n, many thanks to the people who sorted this out
|
193
232
|
# before I worked on this library.
|
194
233
|
format = format.gsub(SUBSTITUTION_REGEX) do |match|
|
195
234
|
case match
|
196
|
-
when '%a' then lookup('date.abbreviated_days',
|
197
|
-
when '%^a' then lookup('date.abbreviated_days',
|
198
|
-
when '%A' then lookup('date.days',
|
199
|
-
when '%^A' then lookup('date.days',
|
200
|
-
when '%b' then lookup('date.abbreviated_months',
|
201
|
-
when '%^b' then lookup('date.abbreviated_months',
|
202
|
-
when '%B' then lookup('date.months',
|
203
|
-
when '%^B' then lookup('date.months',
|
204
|
-
when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }",
|
205
|
-
when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }",
|
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
|
206
245
|
end
|
207
246
|
end
|
208
247
|
|
209
248
|
object.strftime(format)
|
249
|
+
when Array
|
250
|
+
case object.length
|
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
|
210
270
|
else
|
211
271
|
raise(UnLocalizableObject, "The object class #{ object.class } cannot be localized by Tater.")
|
212
272
|
end
|
@@ -216,36 +276,77 @@ class Tater
|
|
216
276
|
# Lookup a key in the messages hash, using the current locale or an override.
|
217
277
|
#
|
218
278
|
# @param key [String]
|
219
|
-
# @param
|
279
|
+
# @param locale [String]
|
220
280
|
# A locale to use instead of our current one.
|
221
|
-
# @param
|
281
|
+
# @param cascade [Boolean]
|
222
282
|
# A boolean to forcibly set the cascade option for this lookup.
|
223
283
|
#
|
224
284
|
# @return
|
225
|
-
# Basically anything that can be stored in
|
226
|
-
def lookup(key,
|
227
|
-
|
285
|
+
# Basically anything that can be stored in your messages Hash.
|
286
|
+
def lookup(key, locale: nil, cascade: nil)
|
287
|
+
locale = locale.nil? ? @locale : locale
|
288
|
+
cascade = cascade.nil? ? @cascade : cascade
|
289
|
+
|
290
|
+
cached(key, locale, cascade) || begin
|
291
|
+
return nil unless @messages.key?(locale.to_s)
|
228
292
|
|
229
|
-
|
230
|
-
while path.length >= 2 do
|
231
|
-
attempt = @messages.dig(*path)
|
293
|
+
path = key.split(SEPARATOR).prepend(locale).map(&:to_s)
|
232
294
|
|
233
|
-
|
234
|
-
|
295
|
+
message =
|
296
|
+
if cascade
|
297
|
+
while path.length >= 2
|
298
|
+
attempt = @messages.dig(*path)
|
299
|
+
|
300
|
+
break attempt unless attempt.nil?
|
301
|
+
|
302
|
+
path.delete_at(path.length - 2)
|
303
|
+
end
|
235
304
|
else
|
236
|
-
|
305
|
+
@messages.dig(*path)
|
237
306
|
end
|
238
|
-
|
239
|
-
|
240
|
-
@messages.dig(*path)
|
307
|
+
|
308
|
+
cache(key, locale, cascade, message)
|
241
309
|
end
|
242
310
|
end
|
243
311
|
|
312
|
+
# Check that there's a key at the given path.
|
313
|
+
#
|
314
|
+
# @param key [String]
|
315
|
+
# The period-separated key path to look within our messages for.
|
316
|
+
# @param options [Hash]
|
317
|
+
# Options to pass to the #lookup method, including locale overrides.
|
318
|
+
#
|
319
|
+
# @option options [Boolean] :cascade
|
320
|
+
# Should this lookup cascade or not? Can override @cascade.
|
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.
|
326
|
+
#
|
327
|
+
# @return [Boolean]
|
328
|
+
def includes?(key, options = HASH)
|
329
|
+
message =
|
330
|
+
if options.key?(:locales)
|
331
|
+
options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
|
332
|
+
|
333
|
+
options[:locales].find do |accept|
|
334
|
+
found = lookup(key, locale: accept, cascade: options[:cascade])
|
335
|
+
|
336
|
+
break found unless found.nil?
|
337
|
+
end
|
338
|
+
else
|
339
|
+
lookup(key, locale: options[:locale], cascade: options[:cascade])
|
340
|
+
end
|
341
|
+
|
342
|
+
!message.nil?
|
343
|
+
end
|
344
|
+
|
244
345
|
# Translate a key path and optional interpolation arguments into a string.
|
245
346
|
# It's effectively a combination of #lookup and #interpolate.
|
246
347
|
#
|
247
348
|
# @example
|
248
|
-
# Tater.new(messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
|
349
|
+
# Tater.new(locale: 'en', messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
|
249
350
|
#
|
250
351
|
# @param key [String]
|
251
352
|
# The period-separated key path to look within our messages for.
|
@@ -257,36 +358,61 @@ class Tater
|
|
257
358
|
# @option options [String] :default
|
258
359
|
# A default string to return, should lookup fail.
|
259
360
|
# @option options [String] :locale
|
260
|
-
# A specific locale to lookup within.
|
261
|
-
# :locales option.
|
361
|
+
# A specific locale to lookup within.
|
262
362
|
# @option options [Array<String>] :locales
|
263
|
-
# An array of locales to look within.
|
363
|
+
# An array of locales to look within. This will take precedence over the
|
364
|
+
# :locale option and will append the default :locale option passed during
|
365
|
+
# initialization if present.
|
264
366
|
#
|
265
367
|
# @return [String]
|
266
368
|
# The translated and interpreted string, if found, or any data at the
|
267
369
|
# defined key.
|
268
|
-
def translate(key, options =
|
269
|
-
cascade_override = options.delete(:cascade)
|
270
|
-
locale_override = options.delete(:locale)
|
271
|
-
locales = options.delete(:locales)
|
272
|
-
|
370
|
+
def translate(key, options = HASH)
|
273
371
|
message =
|
274
|
-
if
|
275
|
-
|
276
|
-
|
277
|
-
locales.find do |accept|
|
278
|
-
found = lookup(key, accept,
|
372
|
+
if options.key?(:locales)
|
373
|
+
options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
|
374
|
+
|
375
|
+
options[:locales].find do |accept|
|
376
|
+
found = lookup(key, locale: accept, cascade: options[:cascade])
|
279
377
|
|
280
|
-
break found
|
378
|
+
break found unless found.nil?
|
281
379
|
end
|
380
|
+
else
|
381
|
+
lookup(key, locale: options[:locale], cascade: options[:cascade])
|
282
382
|
end
|
283
383
|
|
284
384
|
# Call procs that should return a string.
|
285
|
-
if message.is_a?(Proc)
|
286
|
-
message = message.call(key, options)
|
287
|
-
end
|
385
|
+
message = message.call(key, options) if message.is_a?(Proc)
|
288
386
|
|
289
|
-
Utils.interpolate(message, options) || options
|
387
|
+
Utils.interpolate(message, options) || options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
|
290
388
|
end
|
291
389
|
alias t translate
|
390
|
+
|
391
|
+
private
|
392
|
+
|
393
|
+
# @param key [String]
|
394
|
+
# The cache key, often in the form "something.nested.like.this"
|
395
|
+
# @param locale [String]
|
396
|
+
# The locale to store the value for.
|
397
|
+
# @param cascade [Boolean]
|
398
|
+
# Was this a cascading lookup?
|
399
|
+
# @param message [String]
|
400
|
+
# The message being cached, often a String.
|
401
|
+
# @return [String]
|
402
|
+
# Whatever value is being cached, often a String.
|
403
|
+
def cache(key, locale, cascade, message)
|
404
|
+
@cache[locale][cascade][key] = message
|
405
|
+
end
|
406
|
+
|
407
|
+
# @param key [String]
|
408
|
+
# The cache key, often in the form "something.nested.like.this"
|
409
|
+
# @param locale [String]
|
410
|
+
# The locale to store the value for.
|
411
|
+
# @param cascade [Boolean]
|
412
|
+
# Was this a cascading lookup?
|
413
|
+
# @return [String, nil]
|
414
|
+
# The cached message or nil.
|
415
|
+
def cached(key, locale, cascade)
|
416
|
+
@cache.dig(locale, cascade, key)
|
417
|
+
end
|
292
418
|
end
|
data/test/fixtures/fixtures.yml
CHANGED
data/test/fixtures/ruby.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
{
|
2
3
|
en: {
|
3
|
-
ruby: proc do |key,
|
4
|
+
ruby: proc do |key, _options = {}|
|
4
5
|
"Hey #{ key }!"
|
5
6
|
end,
|
6
|
-
options: proc do |_key,
|
7
|
-
|
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
|
9
|
-
it 'deeply merges two hashes,
|
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
|
13
|
+
third = Tater::Utils.deep_merge(first, second)
|
14
14
|
|
15
|
-
assert_equal({ 'one' => 'one', 'two' => { 'three' => 'three', 'four' => 'four' } },
|
15
|
+
assert_equal({ 'one' => 'one', 'two' => { 'three' => 'three', 'four' => 'four' } }, third)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
describe '#deep_stringify_keys
|
20
|
-
it 'converts all keys into strings, recursively
|
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
|
-
|
24
|
+
assert_equal({ 'en' => { 'login' => { 'title' => 'Hello!' } } }, finish)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
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)
|
24
32
|
|
25
|
-
|
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',
|
118
|
-
assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy',
|
119
|
-
assert_nil i18n.lookup('cascade.another.nope.crazy',
|
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,36 @@ 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')
|
213
|
+
end
|
214
|
+
|
215
|
+
let :fr do
|
216
|
+
Tater.new(path: File.expand_path('test/fixtures'), locale: 'fr')
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'localizes arrays' do
|
220
|
+
assert_equal 'tacos and burritos', i18n.localize(%w[tacos burritos])
|
221
|
+
assert_equal 'tacos', i18n.localize(%w[tacos])
|
222
|
+
assert_equal 'tacos, enchiladas, and burritos', i18n.localize(%w[tacos enchiladas burritos])
|
223
|
+
|
224
|
+
assert_equal 'tacos + enchiladas ++ burritos', fr.localize(%w[tacos enchiladas burritos], words_connector: ' + ', last_word_connector: ' ++ ')
|
225
|
+
assert_equal 'tacostwoburritos', fr.localize(%w[tacos burritos], two_words_connector: 'two')
|
226
|
+
|
227
|
+
assert_raises(Tater::MissingLocalizationFormat) do
|
228
|
+
fr.localize(%w[tacos burritos])
|
229
|
+
end
|
230
|
+
|
231
|
+
assert_raises(Tater::MissingLocalizationFormat) do
|
232
|
+
fr.localize(%w[tacos burritos], last_word_connector: 'last', words_connector: 'words')
|
233
|
+
end
|
234
|
+
|
235
|
+
assert_raises(Tater::MissingLocalizationFormat) do
|
236
|
+
fr.localize(%w[tacos burritos], last_word_connector: 'last')
|
237
|
+
end
|
238
|
+
|
239
|
+
assert_raises(Tater::MissingLocalizationFormat) do
|
240
|
+
fr.localize(%w[tacos burritos], words_connector: 'words')
|
241
|
+
end
|
185
242
|
end
|
186
243
|
|
187
244
|
it 'localizes Dates' do
|
@@ -303,7 +360,7 @@ describe Tater do
|
|
303
360
|
|
304
361
|
describe '#locale=' do
|
305
362
|
let :i18n do
|
306
|
-
Tater.new(path: File.expand_path('test/fixtures'))
|
363
|
+
Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
|
307
364
|
end
|
308
365
|
|
309
366
|
it 'overrides the locale when available' do
|
@@ -334,4 +391,29 @@ describe Tater do
|
|
334
391
|
assert cascade.cascades?
|
335
392
|
end
|
336
393
|
end
|
394
|
+
|
395
|
+
describe '#includes?' do
|
396
|
+
let :i18n do
|
397
|
+
Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
|
398
|
+
end
|
399
|
+
|
400
|
+
it 'tells you if you have a translation' do
|
401
|
+
assert i18n.includes?('deep')
|
402
|
+
assert i18n.includes?('deep.key')
|
403
|
+
refute i18n.includes?('deep.nope')
|
404
|
+
refute i18n.includes?('nope')
|
405
|
+
end
|
406
|
+
|
407
|
+
it 'allows overriding the locale' do
|
408
|
+
assert i18n.includes?('french', locale: 'fr')
|
409
|
+
assert i18n.includes?('french', locales: %w[en fr])
|
410
|
+
refute i18n.includes?('french', locales: %w[en])
|
411
|
+
refute i18n.includes?('french')
|
412
|
+
end
|
413
|
+
|
414
|
+
it 'allows cascading' do
|
415
|
+
assert i18n.includes?('cascade.nope.tacos', cascade: true)
|
416
|
+
refute i18n.includes?('cascade.nope.tacos', cascade: false)
|
417
|
+
end
|
418
|
+
end
|
337
419
|
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:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Evan Lecklider
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-29 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.
|
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
|