tater 3.0.4 → 3.0.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1913c7bc9ecb58cbb7ff7c4239978c8401116a954f967e1fd1e557ad30b0e7bd
4
- data.tar.gz: cd65a35921e999375affaf4a38717fc14c6938285f3ad1d4258727494a212d40
3
+ metadata.gz: 18944b6a0817574ffa53d289a770dc0d014f7e6cc2a7288bcea2f759ed317aa4
4
+ data.tar.gz: 0716a6538423a4722bae9929a4fa7668eeace79517680c1e5c30a9e5a9635a28
5
5
  SHA512:
6
- metadata.gz: 8744cffa6df1821728ee83f221899d7b5dae739c9420f0ce40bbd02f9b7a7d1388aeed8f54a3d029663a2843b0fca915cd277dc688b044a559c994630695bfc1
7
- data.tar.gz: 4e0cfde2f00feaf6e3ce76aec920b0f56a208dfc59e18f0af0c679fe1f8d733ce6c5343bdafc36df33bb2170b6ac660e36d906161273ddf7a3c9fc0a96cf78aa
6
+ metadata.gz: '0690c57c5f348ace3b92d3156efadb40de9256843b20bd8e0ea36be327f48649e70d7c0baf68abe337d0cd4883a9c6c4690c9f54b5017dd3298bc6fa8b4920eb'
7
+ data.tar.gz: 91b9b6d51ed90b46b27263d6e911bace16959ea21a8da3aa501bf684acac3fada973c3bf48aec3ef6c0cf67567f7dd5f4494ca5312f23573474c9f71272441ce
@@ -1,39 +1,40 @@
1
- * Tater
1
+ # Tater
2
2
 
3
- [[https://badge.fury.io/rb/tater][https://badge.fury.io/rb/tater.svg]]
3
+ [![](https://badge.fury.io/rb/tater.svg)](https://badge.fury.io/rb/tater)
4
+ [![](https://github.com/evanleck/tater/actions/workflows/main.yml/badge.svg)](https://github.com/evanleck/tater/actions/workflows/main.yml)
4
5
 
5
6
  Tater is an internationalization (i18n) and localization (l10n) library designed
6
- for simplicity. It doesn't do everything that other libraries do, but that's by
7
- design.
7
+ for simplicity. It doesn't do everything that other libraries do, but that's
8
+ by design.
8
9
 
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
10
+ Under the hood, Tater uses a Hash to store the messages, the `dig` method
11
+ for lookups, `strftime` for date and time localizations, and `format` for
11
12
  interpolation. That's probably 90% of what Tater does.
12
13
 
13
- ** Installation
14
+ ## Installation
14
15
 
15
- Tater requires Ruby 2.5 or higher. To install Tater, add this line to your
16
+ Tater requires Ruby 2.7 or higher. To install Tater, add this line to your
16
17
  application's Gemfile (or gems.rb):
17
18
 
18
- #+begin_src ruby
19
+ ``` ruby
19
20
  gem 'tater'
20
- #+end_src
21
+ ```
21
22
 
22
23
  And then execute:
23
24
 
24
- #+begin_src sh
25
+ ``` sh
25
26
  bundle
26
- #+end_src
27
+ ```
27
28
 
28
29
  Or install it yourself by running:
29
30
 
30
- #+begin_src sh
31
+ ``` sh
31
32
  gem install tater
32
- #+end_src
33
+ ```
33
34
 
34
- ** Usage
35
+ ## Usage
35
36
 
36
- #+begin_src ruby
37
+ ``` ruby
37
38
  require 'tater'
38
39
 
39
40
  messages = {
@@ -56,56 +57,56 @@ i18n.translate('some.key') # => 'This here string!'
56
57
 
57
58
  # Interpolation:
58
59
  i18n.translate('interpolated', you: 'world') # => 'Hello world!'
59
- #+end_src
60
+ ```
60
61
 
61
- ** Array localization
62
+ ## Array localization
62
63
 
63
64
  Given an array, Tater will do it's best to join the elements of the array into a
64
65
  sentence based on how many elements there are.
65
66
 
66
- #+begin_example
67
+ ``` example
67
68
  en:
68
69
  array:
69
70
  last_word_connector: ", and "
70
71
  two_words_connector: " and "
71
72
  words_connector: ", "
72
- #+end_example
73
+ ```
73
74
 
74
- #+begin_src ruby
75
+ ``` ruby
75
76
  i18n.localize(%w[tacos enchiladas burritos]) # => "tacos, enchiladas, and burritos"
76
- #+end_src
77
+ ```
77
78
 
78
- ** Numeric localization
79
+ ## Numeric localization
79
80
 
80
- Numeric localization (=Numeric=, =Integer=, =Float=, and =BigDecimal=) require
81
+ Numeric localization (`Numeric`, `Integer`, `Float`, and `BigDecimal`) require
81
82
  filling in a separator and delimiter. For example:
82
83
 
83
- #+begin_example
84
+ ``` example
84
85
  en:
85
86
  numeric:
86
87
  delimiter: ','
87
88
  separator: '.'
88
- #+end_example
89
+ ```
89
90
 
90
91
  With that, you can do things like this:
91
92
 
92
- #+begin_src ruby
93
+ ``` ruby
93
94
  i18n.localize(1000.2) # => "1,000.20"
94
- #+end_src
95
+ ```
95
96
 
96
97
  The separator and delimiter can also be passed in per-call:
97
98
 
98
- #+begin_src ruby
99
+ ``` ruby
99
100
  i18n.localize(1000.2, delimiter: '_', separator: '+') # => "1_000+20"
100
- #+end_src
101
+ ```
101
102
 
102
- ** Date and time localization
103
+ ## Date and time localization
103
104
 
104
- Date and time localization (=Date=, =Time=, and =DateTime=) require filling in
105
- all of the needed names and abbreviations for days and months. Here's the
105
+ Date and time localization (`Date`, `Time`, and `DateTime`) require filling
106
+ in all of the needed names and abbreviations for days and months. Here's the
106
107
  example for French, which is used in the tests.
107
108
 
108
- #+begin_example
109
+ ``` example
109
110
  fr:
110
111
  time:
111
112
  am: 'am'
@@ -168,27 +169,27 @@ fr:
168
169
  - oct.
169
170
  - nov.
170
171
  - déc.
171
- #+end_example
172
+ ```
172
173
 
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.
174
+ The statically defined keys for dates are `days`, `abbreviated_days`, `months`,
175
+ and `abbreviated_months`. Only `am` and `pm` are needed for times and only if
176
+ you plan on using the `%p` or `%P` format strings.
176
177
 
177
178
  With all of that, you can do something like:
178
179
 
179
- #+begin_src ruby
180
+ ``` ruby
180
181
  i18n.localize(Date.new(1970, 1, 1), format: '%A') # => 'jeudi'
181
182
 
182
183
  # Or, using a key defined in "formats":
183
184
  i18n.localize(Date.new(1970, 1, 1), format: 'day') # => 'jeudi'
184
- #+end_src
185
+ ```
185
186
 
186
- ** Cascading lookups
187
+ ## Cascading lookups
187
188
 
188
189
  Lookups can be cascaded, i.e. pieces of the scope of the can be lopped off
189
190
  incrementally.
190
191
 
191
- #+begin_src ruby
192
+ ``` ruby
192
193
  messages = {
193
194
  'en' => {
194
195
  'login' => {
@@ -207,40 +208,40 @@ i18n.translate('login.special.title') # => 'Special Login'
207
208
  i18n.translate('login.special.description') # => 'Tater lookup failed'
208
209
 
209
210
  i18n.translate('login.special.description', cascade: true) # => 'Normal description.'
210
- #+end_src
211
+ ```
211
212
 
212
213
  With cascade, the final key stays the same, but pieces of the scope get lopped
213
214
  off. In this case, lookups will be tried in this order:
214
215
 
215
- 1. =login.special.description=
216
- 2. =login.description=
216
+ 1. `login.special.description`
217
+ 2. `login.description`
217
218
 
218
219
  This can be useful when you want to override some messages but don't want to
219
220
  have to copy all of the other, non-overwritten messages.
220
221
 
221
222
  Cascading can also be enabled by default when initializing an instance of Tater.
222
223
 
223
- #+begin_src ruby
224
+ ``` ruby
224
225
  Tater.new(cascade: true)
225
- #+end_src
226
+ ```
226
227
 
227
228
  Cascading is off by default.
228
229
 
229
- ** Defaults
230
+ ## Defaults
230
231
 
231
232
  If you'd like to default to another value in case of a missed lookup, you can
232
- provide the =:default= option to =#translate=.
233
+ provide the `:default` option to `#translate`.
233
234
 
234
- #+begin_src ruby
235
+ ``` ruby
235
236
  Tater.new.translate('nope', default: 'Yep!') # => 'Yep!'
236
- #+end_src
237
+ ```
237
238
 
238
- ** Procs and messages in Ruby
239
+ ## Procs and messages in Ruby
239
240
 
240
241
  Ruby files can be used to store messages in addition to YAML, so long as the
241
- Ruby file returns a =Hash= when evalled.
242
+ Ruby file returns a `Hash` when evaluated.
242
243
 
243
- #+begin_src ruby
244
+ ``` ruby
244
245
  {
245
246
  'en' => {
246
247
  ruby: proc do |key, options = {}|
@@ -248,15 +249,15 @@ Ruby file returns a =Hash= when evalled.
248
249
  end
249
250
  }
250
251
  }
251
- #+end_src
252
+ ```
252
253
 
253
- ** Multiple locales
254
+ ## Multiple locales
254
255
 
255
256
  If you would like to check multiple locales and pull the first matching one out,
256
- you can pass the =:locales= option to initialization or the =translate= method
257
+ you can pass the `:locales` option to initialization or the `translate` method
257
258
  with an array of top-level locale keys.
258
259
 
259
- #+begin_src ruby
260
+ ``` ruby
260
261
  messages = {
261
262
  'en' => {
262
263
  'title' => 'Login',
@@ -275,23 +276,25 @@ i18n.translate('description', locales: %w[fr en]) # => 'English description.'
275
276
  i18n = Tater.new(messages: messages, locales: %w[fr en])
276
277
  i18n.translate('title') # => 'la connexion'
277
278
  i18n.translate('description') # => 'English description.'
278
- #+end_src
279
+ ```
279
280
 
280
281
  Locales will be tried in order and whichever one matches first will be returned.
281
282
 
282
- ** Limitations
283
+ ## Limitations
283
284
 
284
- - It is not pluggable, it does what it does and that's it.
285
+ - It is not plug-able, it does what it does and that's it.
285
286
  - It doesn't handle pluralization yet, though it may in the future.
286
287
 
287
- ** Why?
288
+ ## Why?
288
289
 
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.
290
+ Because [Ruby I18n](https://github.com/ruby-i18n/i18n) is amazing and I wanted
291
+ to try to create a minimum viable implementation of the bits of I18n that I
292
+ use 90% of the time. Tater is a single file that handles the basics of lookup
293
+ and interpolation.
292
294
 
293
- ** Trivia
295
+ ## Trivia
294
296
 
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.
297
+ I was originally going to call this library "Translator" but with a
298
+ [numeronym](https://en.wikipedia.org/wiki/Numeronym) like I18n: "t8r". I looked
299
+ at it for a while but I read it as "tater" instead of "tee-eight-arr" so I
300
+ figured I'd just name it Tater. Tater the translator.
data/lib/tater/utils.rb CHANGED
@@ -52,7 +52,8 @@ class Tater
52
52
  end.freeze
53
53
  end
54
54
 
55
- # Format values into a string if appropriate.
55
+ # Format values into a string, conditionally checking the string and options
56
+ # before interpolating.
56
57
  #
57
58
  # @param string [String]
58
59
  # The target string to interpolate into.
@@ -64,6 +65,18 @@ class Tater
64
65
  return string if options.empty?
65
66
  return string unless interpolation_string?(string)
66
67
 
68
+ interpolate!(string, options)
69
+ end
70
+
71
+ # Format values into a string unconditionally.
72
+ #
73
+ # @param string [String]
74
+ # The target string to interpolate into.
75
+ # @param options [Hash]
76
+ # The values to interpolate into the target string.
77
+ #
78
+ # @return [String]
79
+ def self.interpolate!(string, options)
67
80
  format(string, options)
68
81
  end
69
82
 
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ class Tater
3
+ VERSION = '3.0.6'
4
+ end
data/lib/tater.rb CHANGED
@@ -4,8 +4,9 @@ require 'date'
4
4
  require 'time'
5
5
  require 'yaml'
6
6
 
7
- require 'tater/utils'
8
7
  require 'tater/hash' unless Hash.method_defined?(:except)
8
+ require 'tater/utils'
9
+ require 'tater/version'
9
10
 
10
11
  # Tater is a internationalization (i18n) and localization (l10n) library
11
12
  # designed for speed and simplicity.
@@ -22,6 +23,11 @@ class Tater
22
23
  # Needed for Ruby < 3.
23
24
  using HashExcept unless Hash.method_defined?(:except)
24
25
 
26
+ # An array of the available locale codes found in loaded messages.
27
+ #
28
+ # @return [Array<String>]
29
+ attr_reader :available
30
+
25
31
  # @return [String]
26
32
  attr_reader :locale
27
33
 
@@ -37,6 +43,8 @@ class Tater
37
43
  # @param path [String]
38
44
  # A path to search for YAML or Ruby files to load messages from.
39
45
  def initialize(cascade: false, locale: nil, messages: nil, path: nil)
46
+ @available = []
47
+ @cache = {}
40
48
  @cascade = cascade
41
49
  @locale = locale
42
50
  @messages = {}
@@ -45,6 +53,11 @@ class Tater
45
53
  load(messages: messages) if messages
46
54
  end
47
55
 
56
+ # @return [String]
57
+ def inspect
58
+ %(#<Tater:#{ object_id } @cascade=#{ @cascade } @locale="#{ @locale }" @available=#{ @available }>)
59
+ end
60
+
48
61
  # Do lookups cascade by default?
49
62
  #
50
63
  # @return [Boolean]
@@ -52,13 +65,6 @@ class Tater
52
65
  @cascade
53
66
  end
54
67
 
55
- # An array of the available locale codes found in loaded messages.
56
- #
57
- # @return [Array]
58
- def available
59
- @available ||= messages.keys
60
- end
61
-
62
68
  # Is this locale available in our current set of messages?
63
69
  #
64
70
  # @return [Boolean]
@@ -89,12 +95,12 @@ class Tater
89
95
  @messages = Utils.deep_merge(@messages, Utils.deep_stringify_keys(messages)) if messages
90
96
  @messages = Utils.deep_freeze(@messages)
91
97
 
92
- # Gotta recalculate available locales after updating.
93
- remove_instance_variable(:@available) if instance_variable_defined?(:@available)
98
+ # Update our available locales.
99
+ @available.replace(@messages.keys.map(&:to_s).sort)
94
100
 
95
101
  # Not only does this clear our cache but it establishes the basic structure
96
102
  # that we rely on in other methods.
97
- @cache = {}
103
+ @cache.clear
98
104
 
99
105
  @messages.each_key do |key|
100
106
  @cache[key] = { false => {}, true => {} }
@@ -106,7 +112,8 @@ class Tater
106
112
  # @param locale [String]
107
113
  # The locale code to set as our default.
108
114
  def locale=(locale)
109
- @locale = locale.to_s if available?(locale)
115
+ str = locale.to_s
116
+ @locale = str if available?(str)
110
117
  end
111
118
 
112
119
  # Localize an Array, Date, Time, DateTime, or Numeric object.
@@ -210,24 +217,9 @@ class Tater
210
217
  #
211
218
  # @return [Boolean]
212
219
  def includes?(key, options = HASH)
213
- if options.empty?
214
- !lookup(key).nil?
215
- else
216
- message =
217
- if options.key?(:locales)
218
- options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
220
+ return !lookup(key).nil? if options.empty?
219
221
 
220
- options[:locales].find do |accept|
221
- found = lookup(key, locale: accept, cascade: options[:cascade])
222
-
223
- break found unless found.nil?
224
- end
225
- else
226
- lookup(key, locale: options[:locale], cascade: options[:cascade])
227
- end
228
-
229
- !message.nil?
230
- end
222
+ !lookup_with_options(key, options).nil?
231
223
  end
232
224
 
233
225
  # Translate a key path and optional interpolation arguments into a string.
@@ -257,33 +249,22 @@ class Tater
257
249
  # defined key.
258
250
  def translate(key, options = HASH)
259
251
  if options.empty?
260
- message = lookup(key)
261
-
262
- if message.is_a?(Proc) # rubocop:disable Style/CaseLikeIf
263
- message.call(key)
264
- elsif message.is_a?(String)
252
+ case (message = lookup(key))
253
+ when String
265
254
  message
255
+ when Proc
256
+ message.call(key)
266
257
  else
267
258
  "Tater lookup failed: #{ locale }.#{ key }"
268
259
  end
269
260
  else
270
- message =
271
- if options.key?(:locales)
272
- options[:locales].append(@locale) if @locale && !options[:locales].include?(@locale)
273
-
274
- options[:locales].find do |accept|
275
- found = lookup(key, locale: accept, cascade: options[:cascade])
276
-
277
- break found unless found.nil?
278
- end
279
- else
280
- lookup(key, locale: options[:locale], cascade: options[:cascade])
281
- end
261
+ case (message = lookup_with_options(key, options))
262
+ when String
263
+ return message unless Utils.interpolation_string?(message)
282
264
 
283
- if message.is_a?(Proc) # rubocop:disable Style/CaseLikeIf
265
+ Utils.interpolate!(message, options.except(:cascade, :default, :locale, :locales))
266
+ when Proc
284
267
  message.call(key, options.except(:cascade, :default, :locale, :locales))
285
- elsif message.is_a?(String)
286
- Utils.interpolate(message, options.except(:cascade, :default, :locale, :locales))
287
268
  else
288
269
  options[:default] || "Tater lookup failed: #{ options[:locale] || options[:locales] || locale }.#{ key }"
289
270
  end
@@ -292,6 +273,28 @@ class Tater
292
273
 
293
274
  private
294
275
 
276
+ # @param key [String]
277
+ # The period-separated key path to look within our messages for.
278
+ # @param options [Hash]
279
+ # Options, including values to interpolate to any found string.
280
+ #
281
+ # @return [String]
282
+ # The translated and interpreted string, if found, or any data at the
283
+ # defined key.
284
+ def lookup_with_options(key, options)
285
+ if (locs = options[:locales])
286
+ locs.append(@locale) if @locale && !locs.include?(@locale)
287
+
288
+ locs.find do |accept|
289
+ found = lookup(key, locale: accept, cascade: options[:cascade])
290
+
291
+ break found unless found.nil?
292
+ end
293
+ else
294
+ lookup(key, locale: options[:locale], cascade: options[:cascade])
295
+ end
296
+ end
297
+
295
298
  # Localize an Array object.
296
299
  #
297
300
  # @param object [Array<String>]
metadata CHANGED
@@ -1,127 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tater
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.4
4
+ version: 3.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Lecklider
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-22 00:00:00.000000000 Z
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'
27
- - !ruby/object:Gem::Dependency
28
- name: minitest
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'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
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'
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'
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop-performance
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
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'
11
+ date: 2023-05-26 00:00:00.000000000 Z
12
+ dependencies: []
125
13
  description: Minimal internationalization and localization library.
126
14
  email:
127
15
  - evan@lecklider.com
@@ -130,21 +18,19 @@ extensions: []
130
18
  extra_rdoc_files: []
131
19
  files:
132
20
  - LICENSE.txt
133
- - README.org
21
+ - README.md
134
22
  - lib/tater.rb
135
23
  - lib/tater/aliases.rb
136
24
  - lib/tater/hash.rb
137
25
  - lib/tater/utils.rb
138
- - test/fixtures/another.yml
139
- - test/fixtures/fixtures.yml
140
- - test/fixtures/messages/more.yml
141
- - test/fixtures/ruby.rb
142
- - test/tater_test.rb
26
+ - lib/tater/version.rb
143
27
  homepage: https://github.com/evanleck/tater
144
28
  licenses:
145
29
  - MIT
146
30
  metadata:
147
31
  bug_tracker_uri: https://github.com/evanleck/tater/issues
32
+ changelog_uri: https://github.com/evanleck/tater/blob/main/CHANGELOG.md
33
+ homepage_uri: https://github.com/evanleck/tater
148
34
  rubygems_mfa_required: 'true'
149
35
  source_code_uri: https://github.com/evanleck/tater
150
36
  post_install_message:
@@ -155,20 +41,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
41
  requirements:
156
42
  - - ">="
157
43
  - !ruby/object:Gem::Version
158
- version: 2.5.0
44
+ version: 2.7.0
159
45
  required_rubygems_version: !ruby/object:Gem::Requirement
160
46
  requirements:
161
47
  - - ">="
162
48
  - !ruby/object:Gem::Version
163
49
  version: '2.0'
164
50
  requirements: []
165
- rubygems_version: 3.2.32
51
+ rubygems_version: 3.4.10
166
52
  signing_key:
167
53
  specification_version: 4
168
54
  summary: Minimal internationalization and localization library.
169
- test_files:
170
- - test/fixtures/another.yml
171
- - test/fixtures/fixtures.yml
172
- - test/fixtures/messages/more.yml
173
- - test/fixtures/ruby.rb
174
- - test/tater_test.rb
55
+ test_files: []
@@ -1,2 +0,0 @@
1
- en:
2
- another: 'This is from a different file'
@@ -1,102 +0,0 @@
1
- en:
2
- english: 'Found in English'
3
-
4
- title: 'This is a title'
5
- interpolated: 'This has some %{fancy} text'
6
- double: '%{first} %{second}!'
7
-
8
- deep:
9
- key: 'This key is deeper'
10
-
11
- array:
12
- last_word_connector: ", and "
13
- two_words_connector: " and "
14
- words_connector: ", "
15
-
16
- date:
17
- formats:
18
- default: '%Y/%-m/%-d'
19
- ohmy: '%-m something %-d oh my %Y'
20
-
21
- time:
22
- formats:
23
- default: '%Y/%-m/%-d/%H/%M/%S'
24
-
25
- datetime:
26
- formats:
27
- default: '%Y/%-m/%-d/%H/%M/%S'
28
-
29
- numeric:
30
- delimiter: 'TURKEYS'
31
- separator: 'NAH'
32
-
33
- cascade:
34
- tacos: 'Delicious'
35
-
36
- another:
37
- crazy: 'Whoaa'
38
-
39
- fr:
40
- french: 'Found in French'
41
-
42
- time:
43
- am: 'am'
44
- pm: 'pm'
45
-
46
- formats:
47
- default: '%I%P'
48
- loud: '%I%p'
49
-
50
- date:
51
- formats:
52
- abbreviated_day: '%a'
53
- day: '%A'
54
-
55
- abbreviated_month: '%b'
56
- month: '%B'
57
-
58
- days:
59
- - dimanche
60
- - lundi
61
- - mardi
62
- - mercredi
63
- - jeudi
64
- - vendredi
65
- - samedi
66
-
67
- abbreviated_days:
68
- - dim
69
- - lun
70
- - mar
71
- - mer
72
- - jeu
73
- - ven
74
- - sam
75
-
76
- months:
77
- - janvier
78
- - février
79
- - mars
80
- - avril
81
- - mai
82
- - juin
83
- - juillet
84
- - août
85
- - septembre
86
- - octobre
87
- - novembre
88
- - décembre
89
-
90
- abbreviated_months:
91
- - jan.
92
- - fév.
93
- - mar.
94
- - avr.
95
- - mai
96
- - juin
97
- - juil.
98
- - août
99
- - sept.
100
- - oct.
101
- - nov.
102
- - déc.
@@ -1,12 +0,0 @@
1
- en:
2
- more: "Oh there's more!"
3
-
4
- delimiter_only:
5
- numeric:
6
- formats:
7
- delimiter: 'Just me'
8
-
9
- separator_only:
10
- numeric:
11
- formats:
12
- separator: 'Just me'
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
- {
3
- en: {
4
- ruby: proc do |key, _options = {}|
5
- "Hey #{ key }!"
6
- end,
7
- options: proc do |_key, _options = {}|
8
- 'Hey %{options}!'
9
- end
10
- }
11
- }
data/test/tater_test.rb DELETED
@@ -1,458 +0,0 @@
1
- # frozen_string_literal: true
2
- $LOAD_PATH.unshift File.expand_path('lib', __dir__)
3
-
4
- require 'date'
5
- require 'minitest/autorun'
6
- require 'tater'
7
-
8
- describe Tater do
9
- describe Tater::Utils do
10
- describe '#deep_merge' do
11
- it 'deeply merges two hashes, returning a new one' do
12
- first = { 'one' => 'one', 'two' => { 'three' => 'three' } }
13
- second = { 'two' => { 'four' => 'four' } }
14
-
15
- third = Tater::Utils.deep_merge(first, second)
16
-
17
- assert_equal({ 'one' => 'one', 'two' => { 'three' => 'three', 'four' => 'four' } }, third)
18
- end
19
- end
20
-
21
- describe '#deep_stringify_keys' do
22
- it 'converts all keys into strings, recursively' do
23
- start = { en: { login: { title: 'Hello!' } } }
24
- finish = Tater::Utils.deep_stringify_keys(start)
25
-
26
- assert_equal({ 'en' => { 'login' => { 'title' => 'Hello!' } } }, finish)
27
- end
28
- end
29
-
30
- describe '#deep_freeze' do
31
- it 'freezes the keys and values, recursively' do
32
- start = Tater::Utils.deep_stringify_keys({ en: { login: { title: 'Hello!' } } })
33
- finish = Tater::Utils.deep_freeze(start)
34
-
35
- assert finish.frozen?
36
- assert finish.keys.all?(&:frozen?)
37
- assert finish.values.all?(&:frozen?)
38
- end
39
- end
40
-
41
- describe '#interpolate' do
42
- it 'interpolates a string and hash' do
43
- assert_equal 'this thing', Tater::Utils.interpolate('this %{what}', what: 'thing')
44
- end
45
-
46
- it 'raises a KeyError when an argument is missing (but options are passed)' do
47
- assert_raises(KeyError) do
48
- Tater::Utils.interpolate('this %{what}', nope: 'thing')
49
- end
50
- end
51
-
52
- it 'returns the string unchanged when options are empty (does not raise a KeyError)' do
53
- assert_equal 'this %{what}', Tater::Utils.interpolate('this %{what}')
54
- end
55
- end
56
-
57
- describe '#string_from_numeric' do
58
- it 'converts numerics to decimal-ish strings' do
59
- assert_equal '1', Tater::Utils.string_from_numeric(1)
60
- assert_equal '1.0', Tater::Utils.string_from_numeric(1.0)
61
- assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal('1'))
62
- end
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
78
- end
79
-
80
- describe '#available?' do
81
- let :i18n do
82
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
83
- end
84
-
85
- it 'tells you if the locale is available' do
86
- assert i18n.available?('en')
87
- refute i18n.available?('romulan')
88
- end
89
- end
90
-
91
- describe '#load' do
92
- it 'loads from a path on initialization' do
93
- i18n = Tater.new(path: File.expand_path('test/fixtures'))
94
-
95
- assert_instance_of(Hash, i18n.messages)
96
- end
97
-
98
- it 'loads from a path after initialization' do
99
- i18n = Tater.new
100
- i18n.load(path: File.expand_path('test/fixtures'))
101
-
102
- assert_instance_of(Hash, i18n.messages)
103
- end
104
-
105
- it 'loads from a hash of messages on initialization' do
106
- i18n = Tater.new(messages: { 'hey' => 'Oh hi' })
107
-
108
- assert_instance_of(Hash, i18n.messages)
109
- end
110
-
111
- it 'loads from a hash of messages after initialization' do
112
- i18n = Tater.new
113
- i18n.load(messages: { 'hey' => 'Oh hi' })
114
-
115
- assert_instance_of(Hash, i18n.messages)
116
- end
117
-
118
- it 'freezes messages after loading' do
119
- i18n = Tater.new(messages: { 'hey' => 'Oh hi' })
120
-
121
- assert i18n.messages.frozen?
122
- assert i18n.messages.keys.all?(&:frozen?)
123
- assert i18n.messages.values.all?(&:frozen?)
124
- end
125
- end
126
-
127
- describe '#available' do
128
- let :i18n do
129
- Tater.new(path: File.expand_path('test/fixtures'))
130
- end
131
-
132
- it 'returns an array with the available locales (i.e. the top-level keys in our messages hash)' do
133
- assert_equal %w[en delimiter_only separator_only fr].sort, i18n.available.sort
134
- end
135
-
136
- it 'updates the available list when new messages are loaded' do
137
- i18n.load(messages: { 'added' => { 'hey' => 'yeah' } })
138
-
139
- assert_equal %w[en delimiter_only separator_only fr added].sort, i18n.available.sort
140
- end
141
- end
142
-
143
- describe '#lookup' do
144
- let :i18n do
145
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
146
- end
147
-
148
- it 'returns keys from messages' do
149
- assert_equal 'This is a title', i18n.lookup('title')
150
- end
151
-
152
- it 'does no interpolation' do
153
- assert_equal 'This has some %{fancy} text', i18n.lookup('interpolated')
154
- end
155
-
156
- it 'returns nil for missing lookups' do
157
- assert_nil i18n.lookup('nope')
158
- end
159
-
160
- it 'returns arbitrary data at keys' do
161
- assert_equal({ 'key' => 'This key is deeper' }, i18n.lookup('deep'))
162
- end
163
-
164
- it 'cascades' do
165
- assert_equal 'Delicious', i18n.lookup('cascade.nope.tacos', cascade: true)
166
- assert_equal 'Whoaa', i18n.lookup('cascade.another.nope.crazy', cascade: true)
167
- assert_nil i18n.lookup('cascade.another.nope.crazy', cascade: false)
168
- assert_nil i18n.lookup('cascade.nahhhhhh')
169
- end
170
- end
171
-
172
- describe '#translate' do
173
- let :i18n do
174
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
175
- end
176
-
177
- it 'translates strings' do
178
- assert_equal 'This is a title', i18n.translate('title')
179
- end
180
-
181
- it 'translates nested strings' do
182
- assert_equal 'This key is deeper', i18n.translate('deep.key')
183
- end
184
-
185
- it 'does not return a hash for nested keys' do
186
- assert_equal 'Tater lookup failed: en.deep', i18n.translate('deep')
187
- end
188
-
189
- it 'interpolates additional variables' do
190
- assert_equal 'This has some fancy text', i18n.translate('interpolated', fancy: 'fancy')
191
- assert_equal 'Double down!', i18n.translate('double', first: 'Double', second: 'down')
192
- end
193
-
194
- it 'works with multiple files' do
195
- assert_equal 'This is from a different file', i18n.translate('another')
196
- assert_equal "Oh there's more!", i18n.translate('more')
197
- end
198
-
199
- it 'returns a message for failed translations' do
200
- assert_equal 'Tater lookup failed: en.nope', i18n.translate('nope')
201
- end
202
-
203
- it 'cascades lookups' do
204
- assert_equal 'Tater lookup failed: en.cascade.another.nope.crazy', i18n.translate('cascade.another.nope.crazy', cascade: false)
205
- assert_equal 'Tater lookup failed: en.cascade.nope.tacos', i18n.translate('cascade.nope.tacos')
206
- assert_equal 'Delicious', i18n.translate('cascade.nope.tacos', cascade: true)
207
- end
208
-
209
- it 'defaults lookups' do
210
- assert_equal 'Tater lookup failed: en.default.missing', i18n.translate('default.missing')
211
- assert_equal 'Nope', i18n.translate('default.missing', default: 'Nope')
212
- end
213
-
214
- it 'does lookups across different locales' do
215
- assert_equal 'Found in French', i18n.translate('french', locales: %w[fr en])
216
- assert_equal 'Found in English', i18n.translate('english', locales: %w[fr en])
217
- assert_equal 'Tater lookup failed: ["fr", "en"].neither', i18n.translate('neither', locales: %w[fr en])
218
- end
219
-
220
- it 'finds Ruby files' do
221
- assert_equal 'Hey ruby!', i18n.translate('ruby')
222
- end
223
-
224
- it 'does not interpolate messages returned by procs' do
225
- assert_equal 'Hey %{options}!', i18n.translate('options', options: 'options')
226
- end
227
- end
228
-
229
- describe '#localize' do
230
- let :i18n do
231
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
232
- end
233
-
234
- let :fr do
235
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'fr')
236
- end
237
-
238
- it 'localizes arrays' do
239
- assert_equal 'tacos and burritos', i18n.localize(%w[tacos burritos])
240
- assert_equal 'tacos', i18n.localize(%w[tacos])
241
- assert_equal 'tacos, enchiladas, and burritos', i18n.localize(%w[tacos enchiladas burritos])
242
-
243
- assert_equal 'tacos + enchiladas ++ burritos', fr.localize(%w[tacos enchiladas burritos], words_connector: ' + ', last_word_connector: ' ++ ')
244
- assert_equal 'tacostwoburritos', fr.localize(%w[tacos burritos], two_words_connector: 'two')
245
-
246
- assert_raises(Tater::MissingLocalizationFormat) do
247
- fr.localize(%w[tacos burritos])
248
- end
249
-
250
- assert_raises(Tater::MissingLocalizationFormat) do
251
- fr.localize(%w[tacos burritos], last_word_connector: 'last', words_connector: 'words')
252
- end
253
-
254
- assert_raises(Tater::MissingLocalizationFormat) do
255
- fr.localize(%w[tacos burritos], last_word_connector: 'last')
256
- end
257
-
258
- assert_raises(Tater::MissingLocalizationFormat) do
259
- fr.localize(%w[tacos burritos], words_connector: 'words')
260
- end
261
- end
262
-
263
- it 'localizes Dates' do
264
- assert_equal '1970/1/1', i18n.localize(Date.new(1970, 1, 1))
265
- end
266
-
267
- it 'localizes Times' do
268
- assert_equal '1970/1/1/00/00/00', i18n.localize(Time.new(1970, 1, 1, 0, 0, 0))
269
- end
270
-
271
- it 'localizes DateTimes' do
272
- assert_equal '1970/1/1/00/00/00', i18n.localize(DateTime.new(1970, 1, 1, 0, 0, 0))
273
- end
274
-
275
- it 'localizes Floats' do
276
- assert_equal '10TURKEYS000NAH12', i18n.localize(10_000.12)
277
- end
278
-
279
- it 'localizes Integers' do
280
- assert_equal '10TURKEYS000', i18n.localize(10_000)
281
- end
282
-
283
- it 'localizes BigDecimals' do
284
- assert_equal '1NAH12', i18n.localize(BigDecimal('1.12'))
285
- end
286
-
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
309
- end
310
-
311
- it 'allows overriding the delimiter and separator' do
312
- assert_equal '10WOO000NAH12', i18n.localize(10_000.12, delimiter: 'WOO')
313
- assert_equal '10TURKEYS000YA12', i18n.localize(10_000.12, separator: 'YA')
314
- end
315
-
316
- it 'accepts other formats' do
317
- assert_equal '1 something 1 oh my 1970', i18n.localize(Date.new(1970, 1, 1), format: 'ohmy')
318
- end
319
-
320
- it 'uses the passed in format if the specified class and format are not present' do
321
- assert_equal 'not there', i18n.localize(Date.new(1970, 1, 1), format: 'not there')
322
- end
323
-
324
- it 'raises a MissingLocalizationFormat if a delimiter is missing' do
325
- assert_raises(Tater::MissingLocalizationFormat) do
326
- i18n.localize(10, locale: 'separator_only')
327
- end
328
- end
329
-
330
- it 'raises a MissingLocalizationFormat if a separator is missing' do
331
- assert_raises(Tater::MissingLocalizationFormat) do
332
- i18n.localize(10, locale: 'delimiter_only')
333
- end
334
- end
335
-
336
- describe 'month, day, and AM/PM names' do
337
- let :i18n do
338
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'fr')
339
- end
340
-
341
- it 'localizes day names' do
342
- assert_equal 'jeudi', i18n.localize(Date.new(1970, 1, 1), format: 'day')
343
- assert_equal 'vendredi', i18n.localize(Date.new(1970, 1, 2), format: 'day')
344
- assert_equal 'samedi', i18n.localize(Date.new(1970, 1, 3), format: 'day')
345
- assert_equal 'dimanche', i18n.localize(Date.new(1970, 1, 4), format: 'day')
346
- assert_equal 'lundi', i18n.localize(Date.new(1970, 1, 5), format: 'day')
347
- assert_equal 'mardi', i18n.localize(Date.new(1970, 1, 6), format: 'day')
348
- assert_equal 'mercredi', i18n.localize(Date.new(1970, 1, 7), format: 'day')
349
- end
350
-
351
- it 'localizes abbreviated day names' do
352
- assert_equal 'jeu', i18n.localize(Date.new(1970, 1, 1), format: 'abbreviated_day')
353
- assert_equal 'ven', i18n.localize(Date.new(1970, 1, 2), format: 'abbreviated_day')
354
- assert_equal 'sam', i18n.localize(Date.new(1970, 1, 3), format: 'abbreviated_day')
355
- assert_equal 'dim', i18n.localize(Date.new(1970, 1, 4), format: 'abbreviated_day')
356
- assert_equal 'lun', i18n.localize(Date.new(1970, 1, 5), format: 'abbreviated_day')
357
- assert_equal 'mar', i18n.localize(Date.new(1970, 1, 6), format: 'abbreviated_day')
358
- assert_equal 'mer', i18n.localize(Date.new(1970, 1, 7), format: 'abbreviated_day')
359
- end
360
-
361
- it 'localizes months' do
362
- assert_equal 'janvier', i18n.localize(Date.new(1970, 1, 1), format: 'month')
363
- assert_equal 'février', i18n.localize(Date.new(1970, 2, 1), format: 'month')
364
- assert_equal 'mars', i18n.localize(Date.new(1970, 3, 1), format: 'month')
365
- assert_equal 'avril', i18n.localize(Date.new(1970, 4, 1), format: 'month')
366
- assert_equal 'mai', i18n.localize(Date.new(1970, 5, 1), format: 'month')
367
- assert_equal 'juin', i18n.localize(Date.new(1970, 6, 1), format: 'month')
368
- assert_equal 'juillet', i18n.localize(Date.new(1970, 7, 1), format: 'month')
369
- assert_equal 'août', i18n.localize(Date.new(1970, 8, 1), format: 'month')
370
- assert_equal 'septembre', i18n.localize(Date.new(1970, 9, 1), format: 'month')
371
- assert_equal 'octobre', i18n.localize(Date.new(1970, 10, 1), format: 'month')
372
- assert_equal 'novembre', i18n.localize(Date.new(1970, 11, 1), format: 'month')
373
- assert_equal 'décembre', i18n.localize(Date.new(1970, 12, 1), format: 'month')
374
- end
375
-
376
- it 'localizes abbreviated months' do
377
- assert_equal 'jan.', i18n.localize(Date.new(1970, 1, 1), format: 'abbreviated_month')
378
- assert_equal 'fév.', i18n.localize(Date.new(1970, 2, 1), format: 'abbreviated_month')
379
- assert_equal 'mar.', i18n.localize(Date.new(1970, 3, 1), format: 'abbreviated_month')
380
- assert_equal 'avr.', i18n.localize(Date.new(1970, 4, 1), format: 'abbreviated_month')
381
- assert_equal 'mai', i18n.localize(Date.new(1970, 5, 1), format: 'abbreviated_month')
382
- assert_equal 'juin', i18n.localize(Date.new(1970, 6, 1), format: 'abbreviated_month')
383
- assert_equal 'juil.', i18n.localize(Date.new(1970, 7, 1), format: 'abbreviated_month')
384
- assert_equal 'août', i18n.localize(Date.new(1970, 8, 1), format: 'abbreviated_month')
385
- assert_equal 'sept.', i18n.localize(Date.new(1970, 9, 1), format: 'abbreviated_month')
386
- assert_equal 'oct.', i18n.localize(Date.new(1970, 10, 1), format: 'abbreviated_month')
387
- assert_equal 'nov.', i18n.localize(Date.new(1970, 11, 1), format: 'abbreviated_month')
388
- assert_equal 'déc.', i18n.localize(Date.new(1970, 12, 1), format: 'abbreviated_month')
389
- end
390
-
391
- it 'localizes AM/PM' do
392
- assert_equal '05pm', i18n.localize(Time.new(1970, 1, 1, 17))
393
- assert_equal '05PM', i18n.localize(Time.new(1970, 1, 1, 17), format: 'loud')
394
- assert_equal '05am', i18n.localize(Time.new(1970, 1, 1, 5))
395
- assert_equal '05AM', i18n.localize(Time.new(1970, 1, 1, 5), format: 'loud')
396
- end
397
- end
398
- end
399
-
400
- describe '#locale=' do
401
- let :i18n do
402
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
403
- end
404
-
405
- it 'overrides the locale when available' do
406
- i18n.locale = 'delimiter_only'
407
- assert_equal 'delimiter_only', i18n.locale
408
- end
409
-
410
- it 'does not override the locale when not available' do
411
- i18n.locale = 'nopeskies'
412
- assert_equal 'en', i18n.locale
413
- end
414
- end
415
-
416
- describe '#cascades?' do
417
- let :default do
418
- Tater.new
419
- end
420
-
421
- let :cascade do
422
- Tater.new(cascade: true)
423
- end
424
-
425
- it 'returns false by default' do
426
- refute default.cascades?
427
- end
428
-
429
- it 'returns true when passed during initialization' do
430
- assert cascade.cascades?
431
- end
432
- end
433
-
434
- describe '#includes?' do
435
- let :i18n do
436
- Tater.new(path: File.expand_path('test/fixtures'), locale: 'en')
437
- end
438
-
439
- it 'tells you if you have a translation' do
440
- assert i18n.includes?('deep')
441
- assert i18n.includes?('deep.key')
442
- refute i18n.includes?('deep.nope')
443
- refute i18n.includes?('nope')
444
- end
445
-
446
- it 'allows overriding the locale' do
447
- assert i18n.includes?('french', locale: 'fr')
448
- assert i18n.includes?('french', locales: %w[en fr])
449
- refute i18n.includes?('french', locales: %w[en])
450
- refute i18n.includes?('french')
451
- end
452
-
453
- it 'allows cascading' do
454
- assert i18n.includes?('cascade.nope.tacos', cascade: true)
455
- refute i18n.includes?('cascade.nope.tacos', cascade: false)
456
- end
457
- end
458
- end