i18n 1.12.0 → 1.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b92d195deaeca5e93f73cc62be2d2fb4c93d8a9787449e168b493773e5072458
4
- data.tar.gz: f1172c9fac93f493c8a5b16cff4b27154ff917aed48cbc46a4a702363863111d
3
+ metadata.gz: e85066f2cf6bbf170ff946391c6037d0335e5314d0c1e0723e980d5ee7df7af0
4
+ data.tar.gz: 2a4629aa0644c2a6e8cc5d175d4912b5f5cd4abc066dd5ee196270b7a3eadd08
5
5
  SHA512:
6
- metadata.gz: 0174d10f7bac17e29cf7622d3ce69850e51755d53fd3d17d2f672bc9ca3c75823b851f6d04cbd367fea8ff5830c8c755fb9c80433bac9ec3c46590cbe9a874d0
7
- data.tar.gz: ef22ed98cb7f223753a65b9d63d0f40e4b6821c93a41b1b41627e4c7b4e3de6b7bd411d8a6ed3d46dc6554432a776535c0461248e8d1fbfeee05dcda2ab89235
6
+ metadata.gz: '01326189eb7b675594bd63c802ae9470135e9aab222de4df1429c99c585b9b59b172ed62386c03b82fd45c862bbb42484dc89e0289c83970315f9a5fa03ba91c'
7
+ data.tar.gz: 290489ae277759fe65197e65d6486b7cca67605c2c98197a472c91bb2496d9db168dfd2082706b87f6f806051827b8df12cc0037258f3bf10d5f74a9ddb9f575
data/README.md CHANGED
@@ -26,7 +26,7 @@ gem 'i18n'
26
26
  Then configure I18n with some translations, and a default locale:
27
27
 
28
28
  ```ruby
29
- I18n.load_path << Dir[File.expand_path("config/locales") + "/*.yml"]
29
+ I18n.load_path += Dir[File.expand_path("config/locales") + "/*.yml"]
30
30
  I18n.default_locale = :en # (note that `en` is already the default!)
31
31
  ```
32
32
 
@@ -54,7 +54,7 @@ module I18n
54
54
  end
55
55
 
56
56
  deep_interpolation = options[:deep_interpolation]
57
- values = Utils.except(options, *RESERVED_KEYS)
57
+ values = Utils.except(options, *RESERVED_KEYS) unless options.empty?
58
58
  if values
59
59
  entry = if deep_interpolation
60
60
  deep_interpolate(locale, entry, values)
@@ -66,7 +66,7 @@ module I18n
66
66
  end
67
67
 
68
68
  def exists?(locale, key, options = EMPTY_HASH)
69
- lookup(locale, key) != nil
69
+ lookup(locale, key, options[:scope]) != nil
70
70
  end
71
71
 
72
72
  # Acts the same as +strftime+, but uses a localized version of the
@@ -123,7 +123,12 @@ module I18n
123
123
  # first translation that can be resolved. Otherwise it tries to resolve
124
124
  # the translation directly.
125
125
  def default(locale, object, subject, options = EMPTY_HASH)
126
- options = options.reject { |key, value| key == :default }
126
+ if options.size == 1 && options.has_key?(:default)
127
+ options = {}
128
+ else
129
+ options = Utils.except(options, :default)
130
+ end
131
+
127
132
  case subject
128
133
  when Array
129
134
  subject.each do |item|
@@ -166,7 +171,7 @@ module I18n
166
171
  # Other backends can implement more flexible or complex pluralization rules.
167
172
  def pluralize(locale, entry, count)
168
173
  entry = entry.reject { |k, _v| k == :attributes } if entry.is_a?(Hash)
169
- return entry unless entry.is_a?(Hash) && count && entry.values.none? { |v| v.is_a?(Hash) }
174
+ return entry unless entry.is_a?(Hash) && count
170
175
 
171
176
  key = pluralization_key(entry, count)
172
177
  raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
@@ -282,8 +287,8 @@ module I18n
282
287
  when '%^b' then I18n.t!(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon].upcase
283
288
  when '%B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon]
284
289
  when '%^B' then I18n.t!(:"date.month_names", :locale => locale, :format => format)[object.mon].upcase
285
- when '%p' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).upcase if object.respond_to? :hour
286
- when '%P' then I18n.t!(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format).downcase if object.respond_to? :hour
290
+ when '%p' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).upcase
291
+ when '%P' then I18n.t!(:"time.#{(object.respond_to?(:hour) ? object.hour : 0) < 12 ? :am : :pm}", :locale => locale, :format => format).downcase
287
292
  end
288
293
  end
289
294
  rescue MissingTranslationData => e
@@ -16,26 +16,57 @@ module I18n
16
16
  module Pluralization
17
17
  # Overwrites the Base backend translate method so that it will check the
18
18
  # translation meta data space (:i18n) for a locale specific pluralization
19
- # rule and use it to pluralize the given entry. I.e. the library expects
19
+ # rule and use it to pluralize the given entry. I.e., the library expects
20
20
  # pluralization rules to be stored at I18n.t(:'i18n.plural.rule')
21
21
  #
22
22
  # Pluralization rules are expected to respond to #call(count) and
23
- # return a pluralization key. Valid keys depend on the translation data
24
- # hash (entry) but it is generally recommended to follow CLDR's style,
25
- # i.e., return one of the keys :zero, :one, :few, :many, :other.
23
+ # return a pluralization key. Valid keys depend on the pluralization
24
+ # rules for the locale, as defined in the CLDR.
25
+ # As of v41, 6 locale-specific plural categories are defined:
26
+ # :few, :many, :one, :other, :two, :zero
26
27
  #
27
- # The :zero key is always picked directly when count equals 0 AND the
28
- # translation data has the key :zero. This way translators are free to
29
- # either pick a special :zero translation even for languages where the
30
- # pluralizer does not return a :zero key.
28
+ # n.b., The :one plural category does not imply the number 1.
29
+ # Instead, :one is a category for any number that behaves like 1 in
30
+ # that locale. For example, in some locales, :one is used for numbers
31
+ # that end in "1" (like 1, 21, 151) but that don't end in
32
+ # 11 (like 11, 111, 10311).
33
+ # Similar notes apply to the :two, and :zero plural categories.
34
+ #
35
+ # If you want to have different strings for the categories of count == 0
36
+ # (e.g. "I don't have any cars") or count == 1 (e.g. "I have a single car")
37
+ # use the explicit `"0"` and `"1"` keys.
38
+ # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
31
39
  def pluralize(locale, entry, count)
32
40
  return entry unless entry.is_a?(Hash) && count
33
41
 
34
42
  pluralizer = pluralizer(locale)
35
43
  if pluralizer.respond_to?(:call)
36
- key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count)
37
- raise InvalidPluralizationData.new(entry, count, key) unless entry.has_key?(key)
38
- entry[key]
44
+ # Deprecation: The use of the `zero` key in this way is incorrect.
45
+ # Users that want a different string for the case of `count == 0` should use the explicit "0" key instead.
46
+ # We keep this incorrect behaviour for now for backwards compatibility until we can remove it.
47
+ # Ref: https://github.com/ruby-i18n/i18n/issues/629
48
+ return entry[:zero] if count == 0 && entry.has_key?(:zero)
49
+
50
+ # "0" and "1" are special cases
51
+ # https://unicode-org.github.io/cldr/ldml/tr35-numbers.html#Explicit_0_1_rules
52
+ if count == 0 || count == 1
53
+ value = entry[symbolic_count(count)]
54
+ return value if value
55
+ end
56
+
57
+ # Lateral Inheritance of "count" attribute (http://www.unicode.org/reports/tr35/#Lateral_Inheritance):
58
+ # > If there is no value for a path, and that path has a [@count="x"] attribute and value, then:
59
+ # > 1. If "x" is numeric, the path falls back to the path with [@count=«the plural rules category for x for that locale»], within that the same locale.
60
+ # > 2. If "x" is anything but "other", it falls back to a path [@count="other"], within that the same locale.
61
+ # > 3. If "x" is "other", it falls back to the path that is completely missing the count item, within that the same locale.
62
+ # Note: We don't yet implement #3 above, since we haven't decided how lateral inheritance attributes should be represented.
63
+ plural_rule_category = pluralizer.call(count)
64
+
65
+ value = if entry.has_key?(plural_rule_category) || entry.has_key?(:other)
66
+ entry[plural_rule_category] || entry[:other]
67
+ else
68
+ raise InvalidPluralizationData.new(entry, count, plural_rule_category)
69
+ end
39
70
  else
40
71
  super
41
72
  end
@@ -43,13 +74,23 @@ module I18n
43
74
 
44
75
  protected
45
76
 
46
- def pluralizers
47
- @pluralizers ||= {}
48
- end
77
+ def pluralizers
78
+ @pluralizers ||= {}
79
+ end
49
80
 
50
- def pluralizer(locale)
51
- pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
52
- end
81
+ def pluralizer(locale)
82
+ pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false)
83
+ end
84
+
85
+ private
86
+
87
+ # Normalizes categories of 0.0 and 1.0
88
+ # and returns the symbolic version
89
+ def symbolic_count(count)
90
+ count = 0 if count == 0
91
+ count = 1 if count == 1
92
+ count.to_s.to_sym
93
+ end
53
94
  end
54
95
  end
55
96
  end
@@ -21,6 +21,9 @@ module I18n
21
21
  class Simple
22
22
  module Implementation
23
23
  include Base
24
+
25
+ # Mutex to ensure that concurrent translations loading will be thread-safe
26
+ MUTEX = Mutex.new
24
27
 
25
28
  def initialized?
26
29
  @initialized ||= false
@@ -68,7 +71,11 @@ module I18n
68
71
  # call `init_translations`
69
72
  init_translations if do_init && !initialized?
70
73
 
71
- @translations ||= Concurrent::Hash.new { |h, k| h[k] = Concurrent::Hash.new }
74
+ @translations ||= Concurrent::Hash.new do |h, k|
75
+ MUTEX.synchronize do
76
+ h[k] = Concurrent::Hash.new
77
+ end
78
+ end
72
79
  end
73
80
 
74
81
  protected
@@ -94,7 +101,7 @@ module I18n
94
101
  return nil unless result.has_key?(_key)
95
102
  end
96
103
  result = result[_key]
97
- result = resolve_entry(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol)
104
+ result = resolve_entry(locale, _key, result, Utils.except(options.merge(:scope => nil), :count)) if result.is_a?(Symbol)
98
105
  result
99
106
  end
100
107
  end
@@ -45,30 +45,30 @@ module I18n
45
45
  "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
46
46
  "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
47
47
  "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
48
- "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
49
- "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
50
- "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
51
- "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
52
- "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
53
- "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
54
- "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
55
- "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
56
- "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
57
- "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
58
- "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
59
- "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
60
- "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
61
- "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
62
- "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
63
- "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
64
- "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
65
- "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
66
- "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
67
- "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
68
- "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
69
- "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
70
- "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
71
- "Ž"=>"Z", "ž"=>"z"
48
+ "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", ""=>"SS", "à"=>"a",
49
+ "á"=>"a", "â"=>"a", "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c",
50
+ "è"=>"e", "é"=>"e", "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i",
51
+ "ï"=>"i", "ð"=>"d", "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o",
52
+ "ö"=>"o", "ø"=>"o", "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y",
53
+ "þ"=>"th", "ÿ"=>"y", "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A",
54
+ "ą"=>"a", "Ć"=>"C", "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c",
55
+ "Č"=>"C", "č"=>"c", "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E",
56
+ "ē"=>"e", "Ĕ"=>"E", "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e",
57
+ "Ě"=>"E", "ě"=>"e", "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G",
58
+ "ġ"=>"g", "Ģ"=>"G", "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h",
59
+ "Ĩ"=>"I", "ĩ"=>"i", "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I",
60
+ "į"=>"i", "İ"=>"I", "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j",
61
+ "Ķ"=>"K", "ķ"=>"k", "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l",
62
+ "Ľ"=>"L", "ľ"=>"l", "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N",
63
+ "ń"=>"n", "Ņ"=>"N", "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG",
64
+ "ŋ"=>"ng", "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o",
65
+ "Œ"=>"OE", "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R",
66
+ "ř"=>"r", "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s",
67
+ "Š"=>"S", "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T",
68
+ "ŧ"=>"t", "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u",
69
+ "Ů"=>"U", "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W",
70
+ "ŵ"=>"w", "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z",
71
+ "ż"=>"z", "Ž"=>"Z", "ž"=>"z"
72
72
  }.freeze
73
73
 
74
74
  def initialize(rule = nil)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # heavily based on Masao Mutoh's gettext String interpolation extension
2
4
  # http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb
3
5
 
@@ -10,6 +12,11 @@ module I18n
10
12
  INTERPOLATION_PATTERN = Regexp.union(DEFAULT_INTERPOLATION_PATTERNS)
11
13
  deprecate_constant :INTERPOLATION_PATTERN
12
14
 
15
+ INTERPOLATION_PATTERNS_CACHE = Hash.new do |hash, patterns|
16
+ hash[patterns] = Regexp.union(patterns)
17
+ end
18
+ private_constant :INTERPOLATION_PATTERNS_CACHE
19
+
13
20
  class << self
14
21
  # Return String or raises MissingInterpolationArgument exception.
15
22
  # Missing argument's logic is handled by I18n.config.missing_interpolation_argument_handler.
@@ -20,7 +27,12 @@ module I18n
20
27
  end
21
28
 
22
29
  def interpolate_hash(string, values)
23
- string.gsub(Regexp.union(config.interpolation_patterns)) do |match|
30
+ pattern = INTERPOLATION_PATTERNS_CACHE[config.interpolation_patterns]
31
+ interpolated = false
32
+
33
+ interpolated_string = string.gsub(pattern) do |match|
34
+ interpolated = true
35
+
24
36
  if match == '%%'
25
37
  '%'
26
38
  else
@@ -34,6 +46,8 @@ module I18n
34
46
  $3 ? sprintf("%#{$3}", value) : value
35
47
  end
36
48
  end
49
+
50
+ interpolated ? interpolated_string : string
37
51
  end
38
52
  end
39
53
  end
@@ -34,6 +34,11 @@ module I18n
34
34
  assert_equal 'Sa', I18n.l(@date, :format => '%a', :locale => :de)
35
35
  end
36
36
 
37
+ test "localize Date: given an meridian indicator format it returns the correct meridian indicator" do
38
+ assert_equal 'AM', I18n.l(@date, :format => '%p', :locale => :de)
39
+ assert_equal 'am', I18n.l(@date, :format => '%P', :locale => :de)
40
+ end
41
+
37
42
  test "localize Date: given an abbreviated and uppercased day name format it returns the correct abbreviated day name in upcase" do
38
43
  assert_equal 'sa'.upcase, I18n.l(@date, :format => '%^a', :locale => :de)
39
44
  end
data/lib/i18n/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module I18n
4
- VERSION = "1.12.0"
4
+ VERSION = "1.13.0"
5
5
  end
data/lib/i18n.rb CHANGED
@@ -331,12 +331,11 @@ module I18n
331
331
  # keys are Symbols.
332
332
  def normalize_keys(locale, key, scope, separator = nil)
333
333
  separator ||= I18n.default_separator
334
+ locale = locale.to_sym if locale
334
335
 
335
- [
336
- *normalize_key(locale, separator),
337
- *normalize_key(scope, separator),
338
- *normalize_key(key, separator)
339
- ]
336
+ result = [locale]
337
+ result.concat(normalize_key(scope, separator)) if scope
338
+ result.concat(normalize_key(key, separator))
340
339
  end
341
340
 
342
341
  # Returns true when the passed locale, which can be either a String or a
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.12.0
4
+ version: 1.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Fuchs
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2022-07-13 00:00:00.000000000 Z
16
+ date: 2023-04-26 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: concurrent-ruby