tater 1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +179 -0
- data/lib/tater.rb +225 -0
- data/test/another.yml +2 -0
- data/test/fixtures.yml +87 -0
- data/test/messages/more.yml +12 -0
- data/test/tater_test.rb +285 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: '04904502e539d0ef7eab036d0a3b6c22a635863cdc76e56b27648c9d1005e12c'
|
4
|
+
data.tar.gz: 1ab472fa93cff9180ed718bc54a1209a2151c7a7be77f5ed045047a5865bdfa3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1faf70fb545ce2e0d2211f9af52830421c13f331e0b0519fe1c92b2e9443e1230c5c63ea19c4660d4f8468da4572b304022c8a7d93bf2aa9862b6cbb0e55cd08
|
7
|
+
data.tar.gz: e87befcdfd969ebbce3350c1427070099224bfa24b4cafaafe246b0c1e3917d0e64d09fa5c1fca06678170f6411ffe548b7c3363888ab56061d16ab98657a8ae
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Evan Lecklider
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
# Tater the Translator
|
2
|
+
|
3
|
+
Tater is an internationalization (i18n) and localization (l10n) library designed
|
4
|
+
for simplicity. It doesn't do everything that other libraries do, but that's by
|
5
|
+
design.
|
6
|
+
|
7
|
+
Under the hood, Tater uses a Hash to store the messages, the `dig` method for
|
8
|
+
lookups, `strftime` for date and time localizations, and `format` for
|
9
|
+
interpolation. That's probably 90% of what Tater does.
|
10
|
+
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
gem 'tater'
|
18
|
+
```
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
```sh
|
23
|
+
bundle
|
24
|
+
```
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
```sh
|
29
|
+
gem install tater
|
30
|
+
```
|
31
|
+
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
messages = {
|
37
|
+
'some' => {
|
38
|
+
'key' => 'This here string!'
|
39
|
+
},
|
40
|
+
'interpolated' => 'Hello %{you}!'
|
41
|
+
}
|
42
|
+
|
43
|
+
i18n = Tater.new
|
44
|
+
i18n.load(messages: messages)
|
45
|
+
|
46
|
+
# Basic lookup:
|
47
|
+
i18n.translate('some.key') # => 'This here string!'
|
48
|
+
|
49
|
+
# Interpolation:
|
50
|
+
i18n.translate('interpolated', you: 'world') # => 'Hello world!'
|
51
|
+
```
|
52
|
+
|
53
|
+
## Numeric Localization
|
54
|
+
|
55
|
+
Numeric localization (`Numeric`, `Integer`, `Float`, and `BigDecimal`) require
|
56
|
+
filling in a separater and delimiter. For example:
|
57
|
+
|
58
|
+
```yaml
|
59
|
+
en:
|
60
|
+
numeric:
|
61
|
+
delimiter: ','
|
62
|
+
separator: '.'
|
63
|
+
```
|
64
|
+
|
65
|
+
With that, you can do things like this:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
i18n.localize(1000.2) # => "1,000.20"
|
69
|
+
```
|
70
|
+
|
71
|
+
|
72
|
+
## Date and Time Localization
|
73
|
+
|
74
|
+
Date and time localization (`Date`, `Time`, and `DateTime`) require filling in
|
75
|
+
all of the needed names and abbreviations for days and months. Here's the
|
76
|
+
example for French, which is used in the tests.
|
77
|
+
|
78
|
+
```yaml
|
79
|
+
fr:
|
80
|
+
time:
|
81
|
+
am: 'am'
|
82
|
+
pm: 'pm'
|
83
|
+
|
84
|
+
formats:
|
85
|
+
default: '%I%P'
|
86
|
+
loud: '%I%p'
|
87
|
+
|
88
|
+
date:
|
89
|
+
formats:
|
90
|
+
abbreviated_day: '%a'
|
91
|
+
day: '%A'
|
92
|
+
|
93
|
+
abbreviated_month: '%b'
|
94
|
+
month: '%B'
|
95
|
+
|
96
|
+
days:
|
97
|
+
- dimanche
|
98
|
+
- lundi
|
99
|
+
- mardi
|
100
|
+
- mercredi
|
101
|
+
- jeudi
|
102
|
+
- vendredi
|
103
|
+
- samedi
|
104
|
+
|
105
|
+
abbreviated_days:
|
106
|
+
- dim
|
107
|
+
- lun
|
108
|
+
- mar
|
109
|
+
- mer
|
110
|
+
- jeu
|
111
|
+
- ven
|
112
|
+
- sam
|
113
|
+
|
114
|
+
months:
|
115
|
+
- janvier
|
116
|
+
- février
|
117
|
+
- mars
|
118
|
+
- avril
|
119
|
+
- mai
|
120
|
+
- juin
|
121
|
+
- juillet
|
122
|
+
- août
|
123
|
+
- septembre
|
124
|
+
- octobre
|
125
|
+
- novembre
|
126
|
+
- décembre
|
127
|
+
|
128
|
+
abbreviated_months:
|
129
|
+
- jan.
|
130
|
+
- fév.
|
131
|
+
- mar.
|
132
|
+
- avr.
|
133
|
+
- mai
|
134
|
+
- juin
|
135
|
+
- juil.
|
136
|
+
- août
|
137
|
+
- sept.
|
138
|
+
- oct.
|
139
|
+
- nov.
|
140
|
+
- déc.
|
141
|
+
```
|
142
|
+
|
143
|
+
The statically defined keys for dates are `days`, `abbreviated_days`, `months`,
|
144
|
+
and `abbreviated_months`. Only `am` and `pm` are needed for times and only if
|
145
|
+
you plan on using the `%p` or `%P` format strings.
|
146
|
+
|
147
|
+
With all of that, you can do something like:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
i18n.localize(Date.new(1970, 1, 1), format: '%A') # => 'jeudi'
|
151
|
+
|
152
|
+
# Or, using a key defined in "formats":
|
153
|
+
i18n.localize(Date.new(1970, 1, 1), format: 'day') # => 'jeudi'
|
154
|
+
```
|
155
|
+
|
156
|
+
|
157
|
+
## Limitations
|
158
|
+
|
159
|
+
- It is not "pluggable", it does what it does and that's it.
|
160
|
+
- It doesn't handle pluralization yet, though it may in the future.
|
161
|
+
- It doesn't cache anything, that's up to you.
|
162
|
+
|
163
|
+
|
164
|
+
## Why?
|
165
|
+
|
166
|
+
Because [Ruby I18n][rubyi18n] is amazing and I wanted to try to create a minimum
|
167
|
+
viable implementation of the bits of I18n that I use 90% of the time. Tater is a
|
168
|
+
single file that handles the basics of lookup and interpolation.
|
169
|
+
|
170
|
+
|
171
|
+
## Trivia
|
172
|
+
|
173
|
+
I was orininally going to call this library "Translator" but with a
|
174
|
+
[numeronym][numeronym] like I18n: "t8r". I looked at it for a while but I read
|
175
|
+
it as "tater" instead of "tee-eight-arr" so I figured I'd just name it Tater.
|
176
|
+
Tater the translator.
|
177
|
+
|
178
|
+
[numeronym]: https://en.wikipedia.org/wiki/Numeronym
|
179
|
+
[rubyi18n]: https://github.com/ruby-i18n/i18n
|
data/lib/tater.rb
ADDED
@@ -0,0 +1,225 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'bigdecimal'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
# Tater is a internationalization (i18n) and localization (l10n) library
|
6
|
+
# designed for speed and simplicity.
|
7
|
+
class Tater
|
8
|
+
class MissingLocalizationFormat < ArgumentError; end
|
9
|
+
class UnLocalizableObject < ArgumentError; end
|
10
|
+
|
11
|
+
module Utils # :nodoc:
|
12
|
+
# Merge all the way down.
|
13
|
+
#
|
14
|
+
# @param to [Hash]
|
15
|
+
# The target Hash to merge into. Note that modification is done in-place,
|
16
|
+
# not on a copy of the object.
|
17
|
+
# @param from [Hash]
|
18
|
+
# The Hash to copy values from.
|
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 modify. Note that modification is done in-place, not on a copy
|
33
|
+
# of the object.
|
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
|
+
# Try to interpolate these things, if one of them is a string.
|
45
|
+
#
|
46
|
+
# @param string [String]
|
47
|
+
# The target string to interpolate into.
|
48
|
+
# @param options [Hash]
|
49
|
+
# The values to interpolate into the target string.
|
50
|
+
#
|
51
|
+
# @return [String]
|
52
|
+
def self.interpolate(string, options = {})
|
53
|
+
return string unless string.is_a?(String)
|
54
|
+
|
55
|
+
format(string, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Convert a Numeric to a string, particularly formatting BigDecimals to a
|
59
|
+
# Float-like string representation.
|
60
|
+
#
|
61
|
+
# @param numeric [Numeric]
|
62
|
+
#
|
63
|
+
# @return [String]
|
64
|
+
def self.string_from_numeric(numeric)
|
65
|
+
if numeric.is_a?(BigDecimal)
|
66
|
+
numeric.to_s('F')
|
67
|
+
else
|
68
|
+
numeric.to_s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
DEFAULT = 'default'
|
74
|
+
DEFAULT_LOCALE = 'en'
|
75
|
+
DELIMITING_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/.freeze
|
76
|
+
SEPARATOR = '.'
|
77
|
+
SUBSTITUTION_REGEX = /%(|\^)[aAbBpP]/.freeze
|
78
|
+
|
79
|
+
# @return [String]
|
80
|
+
attr_reader :locale
|
81
|
+
|
82
|
+
# @return [Hash]
|
83
|
+
attr_reader :messages
|
84
|
+
|
85
|
+
def initialize(path: nil, messages: nil, locale: DEFAULT_LOCALE)
|
86
|
+
@locale = locale
|
87
|
+
@messages = {}
|
88
|
+
|
89
|
+
load(path: path) if path
|
90
|
+
load(messages: messages) if messages
|
91
|
+
end
|
92
|
+
|
93
|
+
# An array of the available locale codes.
|
94
|
+
#
|
95
|
+
# @return [Array]
|
96
|
+
def available
|
97
|
+
messages.keys.map(&:to_s)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Is this locale available in our current set of messages?
|
101
|
+
#
|
102
|
+
# @return [Boolean]
|
103
|
+
def available?(locale)
|
104
|
+
available.include?(locale.to_s)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Load messages into our internal cache, either from a path containing YAML
|
108
|
+
# files or a collection of messages.
|
109
|
+
#
|
110
|
+
# @param path [String]
|
111
|
+
# A path to search for YAML files to load messages from.
|
112
|
+
# @param messages [Hash]
|
113
|
+
# A hash of messages ready to be loaded in.
|
114
|
+
def load(path: nil, messages: nil)
|
115
|
+
if path
|
116
|
+
Dir.glob(File.join(path, '**', '*.{yml,yaml}')).each do |file|
|
117
|
+
Utils.deep_merge!(@messages, YAML.safe_load(File.read(file)))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
Utils.deep_merge!(@messages, Utils.deep_stringify_keys!(messages)) if messages
|
122
|
+
end
|
123
|
+
|
124
|
+
# Set the current locale, if it's available.
|
125
|
+
#
|
126
|
+
# @param locale [String]
|
127
|
+
# The locale code to set as our default.
|
128
|
+
def locale=(locale)
|
129
|
+
@locale = locale.to_s if available?(locale)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Localize a Date, Time, DateTime, or Numeric object.
|
133
|
+
#
|
134
|
+
# @param object [Date, Time, DateTime, Numeric]
|
135
|
+
# The object to localize.
|
136
|
+
#
|
137
|
+
# @return [String]
|
138
|
+
# A localized version of the object passed in.
|
139
|
+
def localize(object, options = {})
|
140
|
+
format_key = options.delete(:format) || DEFAULT
|
141
|
+
locale_override = options.delete(:locale)
|
142
|
+
|
143
|
+
case object
|
144
|
+
when String
|
145
|
+
object
|
146
|
+
when Numeric
|
147
|
+
delimiter = lookup('numeric.delimiter', locale_override)
|
148
|
+
separator = lookup('numeric.separator', locale_override)
|
149
|
+
precision = options.fetch(:precision) { 2 }
|
150
|
+
|
151
|
+
raise(MissingLocalizationFormat, "Numeric localization delimiter ('numeric.delimiter') missing") unless delimiter
|
152
|
+
raise(MissingLocalizationFormat, "Numeric localization separator ('numeric.separator') missing") unless separator
|
153
|
+
|
154
|
+
# Heavily cribbed from Rails.
|
155
|
+
integer, fraction = Utils.string_from_numeric(object).split('.')
|
156
|
+
integer.gsub!(DELIMITING_REGEX) do |number|
|
157
|
+
"#{ number }#{ delimiter }"
|
158
|
+
end
|
159
|
+
|
160
|
+
if precision.zero?
|
161
|
+
integer
|
162
|
+
else
|
163
|
+
[integer, fraction&.ljust(precision, '0')].compact.join(separator)
|
164
|
+
end
|
165
|
+
when Date, Time, DateTime
|
166
|
+
key = object.class.to_s.downcase
|
167
|
+
format = lookup("#{ key }.formats.#{ format_key }", locale_override) || format_key
|
168
|
+
|
169
|
+
# Heavily cribbed from I18n, many thanks to the people who sorted this out
|
170
|
+
# before I worked on this library.
|
171
|
+
format = format.gsub(SUBSTITUTION_REGEX) do |match|
|
172
|
+
case match
|
173
|
+
when '%a' then lookup('date.abbreviated_days', locale_override)[object.wday]
|
174
|
+
when '%^a' then lookup('date.abbreviated_days', locale_override)[object.wday].upcase
|
175
|
+
when '%A' then lookup('date.days', locale_override)[object.wday]
|
176
|
+
when '%^A' then lookup('date.days', locale_override)[object.wday].upcase
|
177
|
+
when '%b' then lookup('date.abbreviated_months', locale_override)[object.mon - 1]
|
178
|
+
when '%^b' then lookup('date.abbreviated_months', locale_override)[object.mon - 1].upcase
|
179
|
+
when '%B' then lookup('date.months', locale_override)[object.mon - 1]
|
180
|
+
when '%^B' then lookup('date.months', locale_override)[object.mon - 1].upcase
|
181
|
+
when '%p' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale_override).upcase if object.respond_to?(:hour) # rubocop:disable Metric/BlockNesting
|
182
|
+
when '%P' then lookup("time.#{ object.hour < 12 ? 'am' : 'pm' }", locale_override).downcase if object.respond_to?(:hour) # rubocop:disable Metric/BlockNesting
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
object.strftime(format)
|
187
|
+
else
|
188
|
+
raise(UnLocalizableObject, "The object class #{ object.class } cannot be localized by Tater.")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
alias l localize
|
192
|
+
|
193
|
+
# Lookup a key in the messages hash, using the current locale or an override.
|
194
|
+
#
|
195
|
+
# @param key [String]
|
196
|
+
# @param locale_override [String]
|
197
|
+
# A locale to use instead of our current one.
|
198
|
+
#
|
199
|
+
# @return
|
200
|
+
# Basically anything that can be stored in YAML, including nil.
|
201
|
+
def lookup(key, locale_override = nil)
|
202
|
+
path = key.split(SEPARATOR).prepend(locale_override || locale).map(&:to_s)
|
203
|
+
|
204
|
+
@messages.dig(*path)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Translate a key path and optional interpolation arguments into a string.
|
208
|
+
# It's effectively a combination of #lookup and #interpolate.
|
209
|
+
#
|
210
|
+
# @example
|
211
|
+
# Tater.new(messages: { 'en' => { 'hi' => 'Hello' }}).translate('hi') # => 'Hello'
|
212
|
+
#
|
213
|
+
# @param key [String]
|
214
|
+
# The period-separated key path to look within our messages for.
|
215
|
+
#
|
216
|
+
# @return [String]
|
217
|
+
# The translated and interpreted string, if found, or any data at the
|
218
|
+
# defined key.
|
219
|
+
def translate(key, options = {})
|
220
|
+
locale_override = options.delete(:locale)
|
221
|
+
|
222
|
+
Utils.interpolate(lookup(key, locale_override), options) || "Tater lookup failed: #{ locale_override || locale }.#{ key }"
|
223
|
+
end
|
224
|
+
alias t translate
|
225
|
+
end
|
data/test/another.yml
ADDED
data/test/fixtures.yml
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
en:
|
2
|
+
title: 'This is a title'
|
3
|
+
interpolated: 'This has some %{fancy} text'
|
4
|
+
double: '%{first} %{second}!'
|
5
|
+
|
6
|
+
deep:
|
7
|
+
key: 'This key is deeper'
|
8
|
+
|
9
|
+
date:
|
10
|
+
formats:
|
11
|
+
default: '%Y/%-m/%-d'
|
12
|
+
ohmy: '%-m something %-d oh my %Y'
|
13
|
+
|
14
|
+
time:
|
15
|
+
formats:
|
16
|
+
default: '%Y/%-m/%-d/%H/%M/%S'
|
17
|
+
|
18
|
+
datetime:
|
19
|
+
formats:
|
20
|
+
default: '%Y/%-m/%-d/%H/%M/%S'
|
21
|
+
|
22
|
+
numeric:
|
23
|
+
delimiter: 'TURKEYS'
|
24
|
+
separator: 'NAH'
|
25
|
+
|
26
|
+
fr:
|
27
|
+
time:
|
28
|
+
am: 'am'
|
29
|
+
pm: 'pm'
|
30
|
+
|
31
|
+
formats:
|
32
|
+
default: '%I%P'
|
33
|
+
loud: '%I%p'
|
34
|
+
|
35
|
+
date:
|
36
|
+
formats:
|
37
|
+
abbreviated_day: '%a'
|
38
|
+
day: '%A'
|
39
|
+
|
40
|
+
abbreviated_month: '%b'
|
41
|
+
month: '%B'
|
42
|
+
|
43
|
+
days:
|
44
|
+
- dimanche
|
45
|
+
- lundi
|
46
|
+
- mardi
|
47
|
+
- mercredi
|
48
|
+
- jeudi
|
49
|
+
- vendredi
|
50
|
+
- samedi
|
51
|
+
|
52
|
+
abbreviated_days:
|
53
|
+
- dim
|
54
|
+
- lun
|
55
|
+
- mar
|
56
|
+
- mer
|
57
|
+
- jeu
|
58
|
+
- ven
|
59
|
+
- sam
|
60
|
+
|
61
|
+
months:
|
62
|
+
- janvier
|
63
|
+
- février
|
64
|
+
- mars
|
65
|
+
- avril
|
66
|
+
- mai
|
67
|
+
- juin
|
68
|
+
- juillet
|
69
|
+
- août
|
70
|
+
- septembre
|
71
|
+
- octobre
|
72
|
+
- novembre
|
73
|
+
- décembre
|
74
|
+
|
75
|
+
abbreviated_months:
|
76
|
+
- jan.
|
77
|
+
- fév.
|
78
|
+
- mar.
|
79
|
+
- avr.
|
80
|
+
- mai
|
81
|
+
- juin
|
82
|
+
- juil.
|
83
|
+
- août
|
84
|
+
- sept.
|
85
|
+
- oct.
|
86
|
+
- nov.
|
87
|
+
- déc.
|
data/test/tater_test.rb
ADDED
@@ -0,0 +1,285 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative '../lib/tater'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'date'
|
5
|
+
|
6
|
+
describe Tater do
|
7
|
+
describe Tater::Utils do
|
8
|
+
describe '#deep_merge!' do
|
9
|
+
it 'deeply merges two hashes, modifying the first' do
|
10
|
+
first = { 'one' => 'one', 'two' => { 'three' => 'three' } }
|
11
|
+
second = { 'two' => { 'four' => 'four' } }
|
12
|
+
|
13
|
+
Tater::Utils.deep_merge!(first, second)
|
14
|
+
|
15
|
+
assert_equal({ 'one' => 'one', 'two' => { 'three' => 'three', 'four' => 'four' } }, first)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#deep_stringify_keys!' do
|
20
|
+
it 'converts all keys into strings, recursively modifying the hash passed in' do
|
21
|
+
start = { en: { login: { title: 'Hello!' } } }
|
22
|
+
|
23
|
+
Tater::Utils.deep_stringify_keys!(start)
|
24
|
+
|
25
|
+
assert_equal({ 'en' => { 'login' => { 'title' => 'Hello!' } } }, start)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#interpolate' do
|
30
|
+
it 'interpolates a string and hash' do
|
31
|
+
assert_equal 'this thing', Tater::Utils.interpolate('this %{what}', what: 'thing')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'raises a KeyError when an argument is missing' do
|
35
|
+
assert_raises(KeyError) do
|
36
|
+
Tater::Utils.interpolate('this %{what}')
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#string_from_numeric' do
|
42
|
+
it 'converts numerics to decimal-ish strings' do
|
43
|
+
assert_equal '1', Tater::Utils.string_from_numeric(1)
|
44
|
+
assert_equal '1.0', Tater::Utils.string_from_numeric(1.0)
|
45
|
+
assert_equal '1.0', Tater::Utils.string_from_numeric(BigDecimal(1))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#available?' do
|
51
|
+
let :i18n do
|
52
|
+
Tater.new(path: File.expand_path('test'))
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'tells you if the locale is available' do
|
56
|
+
assert i18n.available?('en')
|
57
|
+
refute i18n.available?('romulan')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#load' do
|
62
|
+
it 'loads from a path on initialization' do
|
63
|
+
i18n = Tater.new(path: File.expand_path('test'))
|
64
|
+
|
65
|
+
assert_instance_of(Hash, i18n.messages)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'loads from a path after initialization' do
|
69
|
+
i18n = Tater.new
|
70
|
+
i18n.load(path: File.expand_path('test'))
|
71
|
+
|
72
|
+
assert_instance_of(Hash, i18n.messages)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'loads from a hash of messages on initialization' do
|
76
|
+
i18n = Tater.new(messages: { 'hey' => 'Oh hi' })
|
77
|
+
|
78
|
+
assert_instance_of(Hash, i18n.messages)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'loads from a hash of messages after initialization' do
|
82
|
+
i18n = Tater.new
|
83
|
+
i18n.load(messages: { 'hey' => 'Oh hi' })
|
84
|
+
|
85
|
+
assert_instance_of(Hash, i18n.messages)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#available' do
|
90
|
+
let :i18n do
|
91
|
+
Tater.new(path: File.expand_path('test'))
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'returns an array with the available locales (i.e. the top-level keys in our messages hash)' do
|
95
|
+
assert_equal %w[en delimiter_only separator_only fr], i18n.available
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#lookup' do
|
100
|
+
let :i18n do
|
101
|
+
Tater.new(path: File.expand_path('test'))
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'returns keys from messages' do
|
105
|
+
assert_equal 'This is a title', i18n.lookup('title')
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'does no interpolation' do
|
109
|
+
assert_equal 'This has some %{fancy} text', i18n.lookup('interpolated')
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'returns nil for missing lookups' do
|
113
|
+
assert_nil i18n.lookup('nope')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe '#translate' do
|
118
|
+
let :i18n do
|
119
|
+
Tater.new(path: File.expand_path('test'))
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'translates strings' do
|
123
|
+
assert_equal 'This is a title', i18n.translate('title')
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'translates nested strings' do
|
127
|
+
assert_equal 'This key is deeper', i18n.translate('deep.key')
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'returns a hash for nested keys' do
|
131
|
+
assert_equal({ 'key' => 'This key is deeper' }, i18n.translate('deep'))
|
132
|
+
end
|
133
|
+
|
134
|
+
it 'interpolates additional variables' do
|
135
|
+
assert_equal 'This has some fancy text', i18n.translate('interpolated', fancy: 'fancy')
|
136
|
+
assert_equal 'Double down!', i18n.translate('double', first: 'Double', second: 'down')
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'works with multiple files' do
|
140
|
+
assert_equal 'This is from a different file', i18n.translate('another')
|
141
|
+
assert_equal "Oh there's more!", i18n.translate('more')
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'returns a message for failed translations' do
|
145
|
+
assert_equal 'Tater lookup failed: en.nope', i18n.translate('nope')
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'is aliased as t' do
|
149
|
+
assert_equal 'This is a title', i18n.t('title')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#localize' do
|
154
|
+
let :i18n do
|
155
|
+
Tater.new(path: File.expand_path('test'))
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'localizes Dates' do
|
159
|
+
assert_equal '1970/1/1', i18n.localize(Date.new(1970, 1, 1))
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'localizes Times' do
|
163
|
+
assert_equal '1970/1/1/00/00/00', i18n.localize(Time.new(1970, 1, 1, 0, 0, 0))
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'localizes DateTimes' do
|
167
|
+
assert_equal '1970/1/1/00/00/00', i18n.localize(DateTime.new(1970, 1, 1, 0, 0, 0))
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'localizes Floats' do
|
171
|
+
assert_equal '10TURKEYS000NAH12', i18n.localize(10_000.12)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'localizes Integers' do
|
175
|
+
assert_equal '10TURKEYS000', i18n.localize(10_000)
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'localizes BigDecimals' do
|
179
|
+
assert_equal '1NAH12', i18n.localize(BigDecimal('1.12'))
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'accepts other formats' do
|
183
|
+
assert_equal '1 something 1 oh my 1970', i18n.localize(Date.new(1970, 1, 1), format: 'ohmy')
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'uses the passed in format if the specified class and format are not present' do
|
187
|
+
assert_equal 'not there', i18n.localize(Date.new(1970, 1, 1), format: 'not there')
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'raises a MissingLocalizationFormat if a delimiter is missing' do
|
191
|
+
assert_raises(Tater::MissingLocalizationFormat) do
|
192
|
+
i18n.localize(10, locale: 'separator_only')
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'raises a MissingLocalizationFormat if a separator is missing' do
|
197
|
+
assert_raises(Tater::MissingLocalizationFormat) do
|
198
|
+
i18n.localize(10, locale: 'delimiter_only')
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'is aliased l' do
|
203
|
+
assert_equal '1970/1/1', i18n.l(Date.new(1970, 1, 1))
|
204
|
+
end
|
205
|
+
|
206
|
+
describe 'month, day, and AM/PM names' do
|
207
|
+
let :i18n do
|
208
|
+
Tater.new(path: File.expand_path('test'), locale: 'fr')
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'localizes day names' do
|
212
|
+
assert_equal 'jeudi', i18n.localize(Date.new(1970, 1, 1), format: 'day')
|
213
|
+
assert_equal 'vendredi', i18n.localize(Date.new(1970, 1, 2), format: 'day')
|
214
|
+
assert_equal 'samedi', i18n.localize(Date.new(1970, 1, 3), format: 'day')
|
215
|
+
assert_equal 'dimanche', i18n.localize(Date.new(1970, 1, 4), format: 'day')
|
216
|
+
assert_equal 'lundi', i18n.localize(Date.new(1970, 1, 5), format: 'day')
|
217
|
+
assert_equal 'mardi', i18n.localize(Date.new(1970, 1, 6), format: 'day')
|
218
|
+
assert_equal 'mercredi', i18n.localize(Date.new(1970, 1, 7), format: 'day')
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'localizes abbreviated day names' do
|
222
|
+
assert_equal 'jeu', i18n.localize(Date.new(1970, 1, 1), format: 'abbreviated_day')
|
223
|
+
assert_equal 'ven', i18n.localize(Date.new(1970, 1, 2), format: 'abbreviated_day')
|
224
|
+
assert_equal 'sam', i18n.localize(Date.new(1970, 1, 3), format: 'abbreviated_day')
|
225
|
+
assert_equal 'dim', i18n.localize(Date.new(1970, 1, 4), format: 'abbreviated_day')
|
226
|
+
assert_equal 'lun', i18n.localize(Date.new(1970, 1, 5), format: 'abbreviated_day')
|
227
|
+
assert_equal 'mar', i18n.localize(Date.new(1970, 1, 6), format: 'abbreviated_day')
|
228
|
+
assert_equal 'mer', i18n.localize(Date.new(1970, 1, 7), format: 'abbreviated_day')
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'localizes months' do
|
232
|
+
assert_equal 'janvier', i18n.localize(Date.new(1970, 1, 1), format: 'month')
|
233
|
+
assert_equal 'février', i18n.localize(Date.new(1970, 2, 1), format: 'month')
|
234
|
+
assert_equal 'mars', i18n.localize(Date.new(1970, 3, 1), format: 'month')
|
235
|
+
assert_equal 'avril', i18n.localize(Date.new(1970, 4, 1), format: 'month')
|
236
|
+
assert_equal 'mai', i18n.localize(Date.new(1970, 5, 1), format: 'month')
|
237
|
+
assert_equal 'juin', i18n.localize(Date.new(1970, 6, 1), format: 'month')
|
238
|
+
assert_equal 'juillet', i18n.localize(Date.new(1970, 7, 1), format: 'month')
|
239
|
+
assert_equal 'août', i18n.localize(Date.new(1970, 8, 1), format: 'month')
|
240
|
+
assert_equal 'septembre', i18n.localize(Date.new(1970, 9, 1), format: 'month')
|
241
|
+
assert_equal 'octobre', i18n.localize(Date.new(1970, 10, 1), format: 'month')
|
242
|
+
assert_equal 'novembre', i18n.localize(Date.new(1970, 11, 1), format: 'month')
|
243
|
+
assert_equal 'décembre', i18n.localize(Date.new(1970, 12, 1), format: 'month')
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'localizes abbreviated months' do
|
247
|
+
assert_equal 'jan.', i18n.localize(Date.new(1970, 1, 1), format: 'abbreviated_month')
|
248
|
+
assert_equal 'fév.', i18n.localize(Date.new(1970, 2, 1), format: 'abbreviated_month')
|
249
|
+
assert_equal 'mar.', i18n.localize(Date.new(1970, 3, 1), format: 'abbreviated_month')
|
250
|
+
assert_equal 'avr.', i18n.localize(Date.new(1970, 4, 1), format: 'abbreviated_month')
|
251
|
+
assert_equal 'mai', i18n.localize(Date.new(1970, 5, 1), format: 'abbreviated_month')
|
252
|
+
assert_equal 'juin', i18n.localize(Date.new(1970, 6, 1), format: 'abbreviated_month')
|
253
|
+
assert_equal 'juil.', i18n.localize(Date.new(1970, 7, 1), format: 'abbreviated_month')
|
254
|
+
assert_equal 'août', i18n.localize(Date.new(1970, 8, 1), format: 'abbreviated_month')
|
255
|
+
assert_equal 'sept.', i18n.localize(Date.new(1970, 9, 1), format: 'abbreviated_month')
|
256
|
+
assert_equal 'oct.', i18n.localize(Date.new(1970, 10, 1), format: 'abbreviated_month')
|
257
|
+
assert_equal 'nov.', i18n.localize(Date.new(1970, 11, 1), format: 'abbreviated_month')
|
258
|
+
assert_equal 'déc.', i18n.localize(Date.new(1970, 12, 1), format: 'abbreviated_month')
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'localizes AM/PM' do
|
262
|
+
assert_equal '05pm', i18n.localize(Time.new(1970, 1, 1, 17))
|
263
|
+
assert_equal '05PM', i18n.localize(Time.new(1970, 1, 1, 17), format: 'loud')
|
264
|
+
assert_equal '05am', i18n.localize(Time.new(1970, 1, 1, 5))
|
265
|
+
assert_equal '05AM', i18n.localize(Time.new(1970, 1, 1, 5), format: 'loud')
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
describe '#locale=' do
|
271
|
+
let :i18n do
|
272
|
+
Tater.new(path: File.expand_path('test'))
|
273
|
+
end
|
274
|
+
|
275
|
+
it 'overrides the locale when available' do
|
276
|
+
i18n.locale = 'delimiter_only'
|
277
|
+
assert_equal 'delimiter_only', i18n.locale
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'does not override the locale when not available' do
|
281
|
+
i18n.locale = 'nopeskies'
|
282
|
+
assert_equal 'en', i18n.locale
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
metadata
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tater
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Evan Lecklider
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-03-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
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: rubocop
|
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
|
+
description: Minimal internationalization and localization library.
|
42
|
+
email:
|
43
|
+
- evan@lecklider.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- lib/tater.rb
|
51
|
+
- test/another.yml
|
52
|
+
- test/fixtures.yml
|
53
|
+
- test/messages/more.yml
|
54
|
+
- test/tater_test.rb
|
55
|
+
homepage: https://github.com/evanleck/tater
|
56
|
+
licenses:
|
57
|
+
- MIT
|
58
|
+
metadata:
|
59
|
+
bug_tracker_uri: https://github.com/evanleck/tater/issues
|
60
|
+
source_code_uri: https://github.com/evanleck/tater
|
61
|
+
post_install_message:
|
62
|
+
rdoc_options: []
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
requirements: []
|
76
|
+
rubygems_version: 3.0.3
|
77
|
+
signing_key:
|
78
|
+
specification_version: 4
|
79
|
+
summary: Minimal internationalization and localization library.
|
80
|
+
test_files:
|
81
|
+
- test/messages/more.yml
|
82
|
+
- test/fixtures.yml
|
83
|
+
- test/tater_test.rb
|
84
|
+
- test/another.yml
|