i18n 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +125 -0
- data/lib/i18n.rb +398 -0
- data/lib/i18n/backend.rb +21 -0
- data/lib/i18n/backend/base.rb +284 -0
- data/lib/i18n/backend/cache.rb +113 -0
- data/lib/i18n/backend/cache_file.rb +36 -0
- data/lib/i18n/backend/cascade.rb +56 -0
- data/lib/i18n/backend/chain.rb +127 -0
- data/lib/i18n/backend/fallbacks.rb +84 -0
- data/lib/i18n/backend/flatten.rb +115 -0
- data/lib/i18n/backend/gettext.rb +85 -0
- data/lib/i18n/backend/interpolation_compiler.rb +123 -0
- data/lib/i18n/backend/key_value.rb +206 -0
- data/lib/i18n/backend/memoize.rb +54 -0
- data/lib/i18n/backend/metadata.rb +71 -0
- data/lib/i18n/backend/pluralization.rb +55 -0
- data/lib/i18n/backend/simple.rb +111 -0
- data/lib/i18n/backend/transliterator.rb +108 -0
- data/lib/i18n/config.rb +165 -0
- data/lib/i18n/core_ext/hash.rb +47 -0
- data/lib/i18n/exceptions.rb +111 -0
- data/lib/i18n/gettext.rb +28 -0
- data/lib/i18n/gettext/helpers.rb +75 -0
- data/lib/i18n/gettext/po_parser.rb +329 -0
- data/lib/i18n/interpolate/ruby.rb +39 -0
- data/lib/i18n/locale.rb +8 -0
- data/lib/i18n/locale/fallbacks.rb +96 -0
- data/lib/i18n/locale/tag.rb +28 -0
- data/lib/i18n/locale/tag/parents.rb +22 -0
- data/lib/i18n/locale/tag/rfc4646.rb +74 -0
- data/lib/i18n/locale/tag/simple.rb +39 -0
- data/lib/i18n/middleware.rb +17 -0
- data/lib/i18n/tests.rb +14 -0
- data/lib/i18n/tests/basics.rb +60 -0
- data/lib/i18n/tests/defaults.rb +52 -0
- data/lib/i18n/tests/interpolation.rb +159 -0
- data/lib/i18n/tests/link.rb +56 -0
- data/lib/i18n/tests/localization.rb +19 -0
- data/lib/i18n/tests/localization/date.rb +117 -0
- data/lib/i18n/tests/localization/date_time.rb +103 -0
- data/lib/i18n/tests/localization/procs.rb +116 -0
- data/lib/i18n/tests/localization/time.rb +103 -0
- data/lib/i18n/tests/lookup.rb +81 -0
- data/lib/i18n/tests/pluralization.rb +35 -0
- data/lib/i18n/tests/procs.rb +55 -0
- data/lib/i18n/version.rb +5 -0
- metadata +124 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The Cascade module adds the ability to do cascading lookups to backends that
|
4
|
+
# are compatible to the Simple backend.
|
5
|
+
#
|
6
|
+
# By cascading lookups we mean that for any key that can not be found the
|
7
|
+
# Cascade module strips one segment off the scope part of the key and then
|
8
|
+
# tries to look up the key in that scope.
|
9
|
+
#
|
10
|
+
# E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
|
11
|
+
# the segment :bar will be stripped off the scope part :"foo.bar" and the new
|
12
|
+
# scope :foo will be used to look up the key :baz. If that does not succeed
|
13
|
+
# then the remaining scope segment :foo will be omitted, too, and again the
|
14
|
+
# key :baz will be looked up (now with no scope).
|
15
|
+
#
|
16
|
+
# To enable a cascading lookup one passes the :cascade option:
|
17
|
+
#
|
18
|
+
# I18n.t(:'foo.bar.baz', :cascade => true)
|
19
|
+
#
|
20
|
+
# This will return the first translation found for :"foo.bar.baz", :"foo.baz"
|
21
|
+
# or :baz in this order.
|
22
|
+
#
|
23
|
+
# The cascading lookup takes precedence over resolving any given defaults.
|
24
|
+
# I.e. defaults will kick in after the cascading lookups haven't succeeded.
|
25
|
+
#
|
26
|
+
# This behavior is useful for libraries like ActiveRecord validations where
|
27
|
+
# the library wants to give users a bunch of more or less fine-grained options
|
28
|
+
# of scopes for a particular key.
|
29
|
+
#
|
30
|
+
# Thanks to Clemens Kofler for the initial idea and implementation! See
|
31
|
+
# http://github.com/clemens/i18n-cascading-backend
|
32
|
+
|
33
|
+
module I18n
|
34
|
+
module Backend
|
35
|
+
module Cascade
|
36
|
+
def lookup(locale, key, scope = [], options = EMPTY_HASH)
|
37
|
+
return super unless cascade = options[:cascade]
|
38
|
+
|
39
|
+
cascade = { :step => 1 } unless cascade.is_a?(Hash)
|
40
|
+
step = cascade[:step] || 1
|
41
|
+
offset = cascade[:offset] || 1
|
42
|
+
separator = options[:separator] || I18n.default_separator
|
43
|
+
skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
|
44
|
+
|
45
|
+
scope = I18n.normalize_keys(nil, key, scope, separator)
|
46
|
+
key = (scope.slice!(-offset, offset) || []).join(separator)
|
47
|
+
|
48
|
+
begin
|
49
|
+
result = super
|
50
|
+
return result unless result.nil?
|
51
|
+
scope = scope.dup
|
52
|
+
end while (!scope.empty? || !skip_root) && scope.slice!(-step, step)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Backend
|
5
|
+
# Backend that chains multiple other backends and checks each of them when
|
6
|
+
# a translation needs to be looked up. This is useful when you want to use
|
7
|
+
# standard translations with a Simple backend but store custom application
|
8
|
+
# translations in a database or other backends.
|
9
|
+
#
|
10
|
+
# To use the Chain backend instantiate it and set it to the I18n module.
|
11
|
+
# You can add chained backends through the initializer or backends
|
12
|
+
# accessor:
|
13
|
+
#
|
14
|
+
# # preserves the existing Simple backend set to I18n.backend
|
15
|
+
# I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
|
16
|
+
#
|
17
|
+
# The implementation assumes that all backends added to the Chain implement
|
18
|
+
# a lookup method with the same API as Simple backend does.
|
19
|
+
class Chain
|
20
|
+
using I18n::HashRefinements
|
21
|
+
|
22
|
+
module Implementation
|
23
|
+
include Base
|
24
|
+
|
25
|
+
attr_accessor :backends
|
26
|
+
|
27
|
+
def initialize(*backends)
|
28
|
+
self.backends = backends
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialized?
|
32
|
+
backends.all? do |backend|
|
33
|
+
backend.instance_eval do
|
34
|
+
return false unless initialized?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
def reload!
|
41
|
+
backends.each { |backend| backend.reload! }
|
42
|
+
end
|
43
|
+
|
44
|
+
def eager_load!
|
45
|
+
backends.each { |backend| backend.eager_load! }
|
46
|
+
end
|
47
|
+
|
48
|
+
def store_translations(locale, data, options = EMPTY_HASH)
|
49
|
+
backends.first.store_translations(locale, data, options)
|
50
|
+
end
|
51
|
+
|
52
|
+
def available_locales
|
53
|
+
backends.map { |backend| backend.available_locales }.flatten.uniq
|
54
|
+
end
|
55
|
+
|
56
|
+
def translate(locale, key, default_options = EMPTY_HASH)
|
57
|
+
namespace = nil
|
58
|
+
options = default_options.except(:default)
|
59
|
+
|
60
|
+
backends.each do |backend|
|
61
|
+
catch(:exception) do
|
62
|
+
options = default_options if backend == backends.last
|
63
|
+
translation = backend.translate(locale, key, options)
|
64
|
+
if namespace_lookup?(translation, options)
|
65
|
+
namespace = _deep_merge(translation, namespace || {})
|
66
|
+
elsif !translation.nil? || (options.key?(:default) && options[:default].nil?)
|
67
|
+
return translation
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
return namespace if namespace
|
73
|
+
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
74
|
+
end
|
75
|
+
|
76
|
+
def exists?(locale, key)
|
77
|
+
backends.any? do |backend|
|
78
|
+
backend.exists?(locale, key)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def localize(locale, object, format = :default, options = EMPTY_HASH)
|
83
|
+
backends.each do |backend|
|
84
|
+
catch(:exception) do
|
85
|
+
result = backend.localize(locale, object, format, options) and return result
|
86
|
+
end
|
87
|
+
end
|
88
|
+
throw(:exception, I18n::MissingTranslation.new(locale, format, options))
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
def init_translations
|
93
|
+
backends.each do |backend|
|
94
|
+
backend.send(:init_translations)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def translations
|
99
|
+
backends.first.instance_eval do
|
100
|
+
init_translations unless initialized?
|
101
|
+
translations
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def namespace_lookup?(result, options)
|
106
|
+
result.is_a?(Hash) && !options.has_key?(:count)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
# This is approximately what gets used in ActiveSupport.
|
111
|
+
# However since we are not guaranteed to run in an ActiveSupport context
|
112
|
+
# it is wise to have our own copy. We underscore it
|
113
|
+
# to not pollute the namespace of the including class.
|
114
|
+
def _deep_merge(hash, other_hash)
|
115
|
+
copy = hash.dup
|
116
|
+
other_hash.each_pair do |k,v|
|
117
|
+
value_from_other = hash[k]
|
118
|
+
copy[k] = value_from_other.is_a?(Hash) && v.is_a?(Hash) ? _deep_merge(value_from_other, v) : v
|
119
|
+
end
|
120
|
+
copy
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
include Implementation
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# I18n locale fallbacks are useful when you want your application to use
|
4
|
+
# translations from other locales when translations for the current locale are
|
5
|
+
# missing. E.g. you might want to use :en translations when translations in
|
6
|
+
# your applications main locale :de are missing.
|
7
|
+
#
|
8
|
+
# To enable locale fallbacks you can simply include the Fallbacks module to
|
9
|
+
# the Simple backend - or whatever other backend you are using:
|
10
|
+
#
|
11
|
+
# I18n::Backend::Simple.include(I18n::Backend::Fallbacks)
|
12
|
+
module I18n
|
13
|
+
@@fallbacks = nil
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
|
17
|
+
def fallbacks
|
18
|
+
@@fallbacks ||= I18n::Locale::Fallbacks.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
|
22
|
+
def fallbacks=(fallbacks)
|
23
|
+
@@fallbacks = fallbacks
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Backend
|
28
|
+
module Fallbacks
|
29
|
+
# Overwrites the Base backend translate method so that it will try each
|
30
|
+
# locale given by I18n.fallbacks for the given locale. E.g. for the
|
31
|
+
# locale :"de-DE" it might try the locales :"de-DE", :de and :en
|
32
|
+
# (depends on the fallbacks implementation) until it finds a result with
|
33
|
+
# the given options. If it does not find any result for any of the
|
34
|
+
# locales it will then throw MissingTranslation as usual.
|
35
|
+
#
|
36
|
+
# The default option takes precedence over fallback locales only when
|
37
|
+
# it's a Symbol. When the default contains a String, Proc or Hash
|
38
|
+
# it is evaluated last after all the fallback locales have been tried.
|
39
|
+
def translate(locale, key, options = EMPTY_HASH)
|
40
|
+
return super unless options.fetch(:fallback, true)
|
41
|
+
return super if options[:fallback_in_progress]
|
42
|
+
default = extract_non_symbol_default!(options) if options[:default]
|
43
|
+
|
44
|
+
fallback_options = options.merge(:fallback_in_progress => true)
|
45
|
+
I18n.fallbacks[locale].each do |fallback|
|
46
|
+
begin
|
47
|
+
catch(:exception) do
|
48
|
+
result = super(fallback, key, fallback_options)
|
49
|
+
return result unless result.nil?
|
50
|
+
end
|
51
|
+
rescue I18n::InvalidLocale
|
52
|
+
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return if options.key?(:default) && options[:default].nil?
|
57
|
+
|
58
|
+
return super(locale, nil, options.merge(:default => default)) if default
|
59
|
+
throw(:exception, I18n::MissingTranslation.new(locale, key, options))
|
60
|
+
end
|
61
|
+
|
62
|
+
def extract_non_symbol_default!(options)
|
63
|
+
defaults = [options[:default]].flatten
|
64
|
+
first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)}
|
65
|
+
if first_non_symbol_default
|
66
|
+
options[:default] = defaults[0, defaults.index(first_non_symbol_default)]
|
67
|
+
end
|
68
|
+
return first_non_symbol_default
|
69
|
+
end
|
70
|
+
|
71
|
+
def exists?(locale, key)
|
72
|
+
I18n.fallbacks[locale].each do |fallback|
|
73
|
+
begin
|
74
|
+
return true if super(fallback, key)
|
75
|
+
rescue I18n::InvalidLocale
|
76
|
+
# we do nothing when the locale is invalid, as this is a fallback anyways.
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
module Backend
|
5
|
+
# This module contains several helpers to assist flattening translations.
|
6
|
+
# You may want to flatten translations for:
|
7
|
+
#
|
8
|
+
# 1) speed up lookups, as in the Memoize backend;
|
9
|
+
# 2) In case you want to store translations in a data store, as in ActiveRecord backend;
|
10
|
+
#
|
11
|
+
# You can check both backends above for some examples.
|
12
|
+
# This module also keeps all links in a hash so they can be properly resolved when flattened.
|
13
|
+
module Flatten
|
14
|
+
SEPARATOR_ESCAPE_CHAR = "\001"
|
15
|
+
FLATTEN_SEPARATOR = "."
|
16
|
+
|
17
|
+
# normalize_keys the flatten way. This method is significantly faster
|
18
|
+
# and creates way less objects than the one at I18n.normalize_keys.
|
19
|
+
# It also handles escaping the translation keys.
|
20
|
+
def self.normalize_flat_keys(locale, key, scope, separator)
|
21
|
+
keys = [scope, key].flatten.compact
|
22
|
+
separator ||= I18n.default_separator
|
23
|
+
|
24
|
+
if separator != FLATTEN_SEPARATOR
|
25
|
+
keys.map! do |k|
|
26
|
+
k.to_s.tr("#{FLATTEN_SEPARATOR}#{separator}",
|
27
|
+
"#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
keys.join(".")
|
32
|
+
end
|
33
|
+
|
34
|
+
# Receives a string and escape the default separator.
|
35
|
+
def self.escape_default_separator(key) #:nodoc:
|
36
|
+
key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Shortcut to I18n::Backend::Flatten.normalize_flat_keys
|
40
|
+
# and then resolve_links.
|
41
|
+
def normalize_flat_keys(locale, key, scope, separator)
|
42
|
+
key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
|
43
|
+
resolve_link(locale, key)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Store flattened links.
|
47
|
+
def links
|
48
|
+
@links ||= I18n.new_double_nested_cache
|
49
|
+
end
|
50
|
+
|
51
|
+
# Flatten keys for nested Hashes by chaining up keys:
|
52
|
+
#
|
53
|
+
# >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
|
54
|
+
# => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
|
55
|
+
#
|
56
|
+
def flatten_keys(hash, escape, prev_key=nil, &block)
|
57
|
+
hash.each_pair do |key, value|
|
58
|
+
key = escape_default_separator(key) if escape
|
59
|
+
curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
|
60
|
+
yield curr_key, value
|
61
|
+
flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Receives a hash of translations (where the key is a locale and
|
66
|
+
# the value is another hash) and return a hash with all
|
67
|
+
# translations flattened.
|
68
|
+
#
|
69
|
+
# Nested hashes are included in the flattened hash just if subtree
|
70
|
+
# is true and Symbols are automatically stored as links.
|
71
|
+
def flatten_translations(locale, data, escape, subtree)
|
72
|
+
hash = {}
|
73
|
+
flatten_keys(data, escape) do |key, value|
|
74
|
+
if value.is_a?(Hash)
|
75
|
+
hash[key] = value if subtree
|
76
|
+
else
|
77
|
+
store_link(locale, key, value) if value.is_a?(Symbol)
|
78
|
+
hash[key] = value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
hash
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def store_link(locale, key, link)
|
87
|
+
links[locale.to_sym][key.to_s] = link.to_s
|
88
|
+
end
|
89
|
+
|
90
|
+
def resolve_link(locale, key)
|
91
|
+
key, locale = key.to_s, locale.to_sym
|
92
|
+
links = self.links[locale]
|
93
|
+
|
94
|
+
if links.key?(key)
|
95
|
+
links[key]
|
96
|
+
elsif link = find_link(locale, key)
|
97
|
+
store_link(locale, key, key.gsub(*link))
|
98
|
+
else
|
99
|
+
key
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def find_link(locale, key) #:nodoc:
|
104
|
+
links[locale].each_pair do |from, to|
|
105
|
+
return [from, to] if key[0, from.length] == from
|
106
|
+
end && nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def escape_default_separator(key) #:nodoc:
|
110
|
+
I18n::Backend::Flatten.escape_default_separator(key)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n/gettext'
|
4
|
+
require 'i18n/gettext/po_parser'
|
5
|
+
|
6
|
+
module I18n
|
7
|
+
module Backend
|
8
|
+
# Experimental support for using Gettext po files to store translations.
|
9
|
+
#
|
10
|
+
# To use this you can simply include the module to the Simple backend - or
|
11
|
+
# whatever other backend you are using.
|
12
|
+
#
|
13
|
+
# I18n::Backend::Simple.include(I18n::Backend::Gettext)
|
14
|
+
#
|
15
|
+
# Now you should be able to include your Gettext translation (*.po) files to
|
16
|
+
# the +I18n.load_path+ so they're loaded to the backend and you can use them as
|
17
|
+
# usual:
|
18
|
+
#
|
19
|
+
# I18n.load_path += Dir["path/to/locales/*.po"]
|
20
|
+
#
|
21
|
+
# Following the Gettext convention this implementation expects that your
|
22
|
+
# translation files are named by their locales. E.g. the file en.po would
|
23
|
+
# contain the translations for the English locale.
|
24
|
+
#
|
25
|
+
# To translate text <b>you must use</b> one of the translate methods provided by
|
26
|
+
# I18n::Gettext::Helpers.
|
27
|
+
#
|
28
|
+
# include I18n::Gettext::Helpers
|
29
|
+
# puts _("some string")
|
30
|
+
#
|
31
|
+
# Without it strings containing periods (".") will not be translated.
|
32
|
+
|
33
|
+
module Gettext
|
34
|
+
using I18n::HashRefinements
|
35
|
+
|
36
|
+
class PoData < Hash
|
37
|
+
def set_comment(msgid_or_sym, comment)
|
38
|
+
# ignore
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
def load_po(filename)
|
44
|
+
locale = ::File.basename(filename, '.po').to_sym
|
45
|
+
data = normalize(locale, parse(filename))
|
46
|
+
{ locale => data }
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse(filename)
|
50
|
+
GetText::PoParser.new.parse(::File.read(filename), PoData.new)
|
51
|
+
end
|
52
|
+
|
53
|
+
def normalize(locale, data)
|
54
|
+
data.inject({}) do |result, (key, value)|
|
55
|
+
unless key.nil? || key.empty?
|
56
|
+
key = key.gsub(I18n::Gettext::CONTEXT_SEPARATOR, '|')
|
57
|
+
key, value = normalize_pluralization(locale, key, value) if key.index("\000")
|
58
|
+
|
59
|
+
parts = key.split('|').reverse
|
60
|
+
normalized = parts.inject({}) do |_normalized, part|
|
61
|
+
{ part => _normalized.empty? ? value : _normalized }
|
62
|
+
end
|
63
|
+
|
64
|
+
result.deep_merge!(normalized)
|
65
|
+
end
|
66
|
+
result
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def normalize_pluralization(locale, key, value)
|
71
|
+
# FIXME po_parser includes \000 chars that can not be turned into Symbols
|
72
|
+
key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first
|
73
|
+
|
74
|
+
keys = I18n::Gettext.plural_keys(locale)
|
75
|
+
values = value.split("\000")
|
76
|
+
raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect} on #{locale} locale for msgid #{key.inspect} with values #{values.inspect}" if values.size != keys.size
|
77
|
+
|
78
|
+
result = {}
|
79
|
+
values.each_with_index { |_value, ix| result[keys[ix]] = _value }
|
80
|
+
[key, result]
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|