i18n-tasks 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +0 -4
- data/CHANGES.md +7 -0
- data/README.md +10 -14
- data/i18n-tasks.gemspec +1 -1
- data/lib/i18n/tasks.rb +0 -2
- data/lib/i18n/tasks/base_task.rb +4 -2
- data/lib/i18n/tasks/commands.rb +14 -14
- data/lib/i18n/tasks/configuration.rb +10 -2
- data/lib/i18n/tasks/console_context.rb +73 -0
- data/lib/i18n/tasks/data.rb +0 -47
- data/lib/i18n/tasks/data/adapter/yaml_adapter.rb +6 -1
- data/lib/i18n/tasks/data/file_system_base.rb +1 -1
- data/lib/i18n/tasks/data/router/conservative_router.rb +5 -5
- data/lib/i18n/tasks/data/router/pattern_router.rb +2 -2
- data/lib/i18n/tasks/data/tree/node.rb +47 -36
- data/lib/i18n/tasks/data/tree/nodes.rb +0 -4
- data/lib/i18n/tasks/data/tree/siblings.rb +54 -9
- data/lib/i18n/tasks/data/tree/traversal.rb +62 -23
- data/lib/i18n/tasks/fill_tasks.rb +29 -21
- data/lib/i18n/tasks/ignore_keys.rb +1 -1
- data/lib/i18n/tasks/key_pattern_matching.rb +17 -0
- data/lib/i18n/tasks/missing_keys.rb +39 -44
- data/lib/i18n/tasks/plural_keys.rb +14 -1
- data/lib/i18n/tasks/reports/base.rb +28 -8
- data/lib/i18n/tasks/reports/spreadsheet.rb +9 -8
- data/lib/i18n/tasks/reports/terminal.rb +33 -29
- data/lib/i18n/tasks/scanners/base_scanner.rb +22 -14
- data/lib/i18n/tasks/scanners/pattern_scanner.rb +2 -1
- data/lib/i18n/tasks/unused_keys.rb +13 -13
- data/lib/i18n/tasks/used_keys.rb +39 -38
- data/lib/i18n/tasks/version.rb +1 -1
- data/spec/i18n_tasks_spec.rb +41 -40
- data/spec/locale_tree/siblings_spec.rb +26 -1
- data/spec/support/i18n_tasks_output_matcher.rb +4 -1
- data/spec/support/trees.rb +6 -1
- data/spec/used_keys_spec.rb +23 -15
- metadata +4 -11
- data/lib/i18n/tasks/file_structure.rb +0 -19
- data/lib/i18n/tasks/key.rb +0 -48
- data/lib/i18n/tasks/key/key_group.rb +0 -45
- data/lib/i18n/tasks/key/match_pattern.rb +0 -24
- data/lib/i18n/tasks/key/usages.rb +0 -12
- data/lib/i18n/tasks/key_group.rb +0 -68
- data/spec/key_group_spec.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4ae19f0f97a5f8ae9221b45ef316b08a5efbe62
|
4
|
+
data.tar.gz: 2b957a1c692318da437ca4235aeb2a1b9e47f2b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d00e68fb5727c9d95df84b5de56bd57d53a6adc6e9e00e1ce05c16e6f99055b30b0bc9c478dfabba238962a5d75cc75dc75b97ae4724bde1b07e4f07150b396d
|
7
|
+
data.tar.gz: 5bc8932e731220f37abce28f4b637c60ab41ed02b8e3c8ab5f0c273385ae269fae4a996e408a006d9ba59bd95bab1e1599d2cc48670ff828421fe96d4d1380d4
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# i18n-tasks [![Build Status][badge-travis]][travis] [![Coverage Status][badge-coveralls]][coveralls] [![Code Climate][badge-code-climate]][code-climate] [![Gemnasium][badge-gemnasium]][gemnasium]
|
2
2
|
|
3
|
-
i18n-tasks
|
3
|
+
i18n-tasks helps you find and manage missing and unused translations.
|
4
4
|
|
5
5
|
The default approach to locale data management with gems such as [i18n][i18n-gem] is flawed.
|
6
6
|
If you use a key that does not exist, this will only blow up at runtime. Keys left over from removed code accumulate
|
7
7
|
in the resource files and introduce unnecessary overhead on the translators. Translation files can quickly turn to disarray.
|
8
8
|
|
9
|
-
i18n-tasks improves this by
|
9
|
+
i18n-tasks improves this by i18n-tasks analysing code statically, without running it. It scans calls such as `I18n.t('some.key')` and provides reports on key usage, missing, and unused keys.
|
10
10
|
It can also pre-fill missing keys, including from Google Translate, and it can remove unused keys as well.
|
11
11
|
|
12
12
|
i18n-tasks can be used with any project using [i18n][i18n-gem] (default in Rails), or similar, even if it isn't ruby.
|
@@ -18,11 +18,9 @@ i18n-tasks can be used with any project using [i18n][i18n-gem] (default in Rails
|
|
18
18
|
Add to Gemfile:
|
19
19
|
|
20
20
|
```ruby
|
21
|
-
gem 'i18n-tasks', '~> 0.
|
21
|
+
gem 'i18n-tasks', '~> 0.5.0'
|
22
22
|
```
|
23
23
|
|
24
|
-
i18n-tasks does not load or execute any of the application's code but performs static-only analysic.
|
25
|
-
This means you can install the gem and run it on a project without adding it to Gemfile.
|
26
24
|
|
27
25
|
## Usage
|
28
26
|
|
@@ -311,6 +309,10 @@ translation:
|
|
311
309
|
api_key: <Google Translate API key>
|
312
310
|
```
|
313
311
|
|
312
|
+
## Interactive Console
|
313
|
+
|
314
|
+
`i18n-tasks irb` starts an IRB session in i18n-tasks context. Type `guide` for more information.
|
315
|
+
|
314
316
|
## RSpec integration
|
315
317
|
|
316
318
|
You might want to test for missing and unused translations as part of your test suite.
|
@@ -335,8 +337,7 @@ describe 'I18n' do
|
|
335
337
|
end
|
336
338
|
end
|
337
339
|
```
|
338
|
-
|
339
|
-
## XLSX
|
340
|
+
### XLSX
|
340
341
|
|
341
342
|
Export missing and unused data to XLSX:
|
342
343
|
|
@@ -344,14 +345,9 @@ Export missing and unused data to XLSX:
|
|
344
345
|
i18n-tasks xlsx-report
|
345
346
|
```
|
346
347
|
|
348
|
+
### HTML
|
347
349
|
|
348
|
-
|
349
|
-
|
350
|
-
While i18n-tasks does not provide an HTML version of the report, you can add [one like this](https://gist.github.com/glebm/6887030).
|
351
|
-
|
352
|
-
---
|
353
|
-
|
354
|
-
This was originally developed for [Zuigo](http://zuigo.com/), a platform to organize and discover events.
|
350
|
+
While i18n-tasks does not provide an HTML version of the report, you can add [one like this](https://gist.github.com/glebm/bdd3ab6d12d915f0c81b).
|
355
351
|
|
356
352
|
[MIT license]: /LICENSE.txt
|
357
353
|
[travis]: https://travis-ci.org/glebm/i18n-tasks
|
data/i18n-tasks.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.email = ['glex.spb@gmail.com']
|
11
11
|
s.summary = %q{Manage translations in ruby applications with the awesome power of static analysis — Edit}
|
12
12
|
s.description = %q{
|
13
|
-
i18n-tasks
|
13
|
+
i18n-tasks helps you find and manage missing and unused translations.
|
14
14
|
|
15
15
|
It scans calls such as `I18n.t('some.key')` and provides reports on key usage, missing, and unused keys.
|
16
16
|
It can also can pre-fill missing keys, including from Google Translate, and it can remove unused keys as well.
|
data/lib/i18n/tasks.rb
CHANGED
data/lib/i18n/tasks/base_task.rb
CHANGED
@@ -9,7 +9,6 @@ require 'i18n/tasks/missing_keys'
|
|
9
9
|
require 'i18n/tasks/unused_keys'
|
10
10
|
require 'i18n/tasks/google_translation'
|
11
11
|
require 'i18n/tasks/fill_tasks'
|
12
|
-
require 'i18n/tasks/file_structure'
|
13
12
|
require 'i18n/tasks/data'
|
14
13
|
require 'i18n/tasks/configuration'
|
15
14
|
|
@@ -26,12 +25,15 @@ module I18n
|
|
26
25
|
include GoogleTranslation
|
27
26
|
include Logging
|
28
27
|
include Configuration
|
29
|
-
include FileStructure
|
30
28
|
include Data
|
31
29
|
|
32
30
|
def initialize(config = {})
|
33
31
|
self.config = config || {}
|
34
32
|
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"i18n-tasks BaseTask config: #{config_for_inspect}"
|
36
|
+
end
|
35
37
|
end
|
36
38
|
end
|
37
39
|
end
|
data/lib/i18n/tasks/commands.rb
CHANGED
@@ -20,8 +20,12 @@ module I18n::Tasks
|
|
20
20
|
end
|
21
21
|
|
22
22
|
desc 'show unused translations'
|
23
|
-
|
24
|
-
|
23
|
+
opts do
|
24
|
+
on '-l', :locales=, 'Filter by locale (default: all)', on_locale_opt
|
25
|
+
end
|
26
|
+
cmd :unused do |opt = {}|
|
27
|
+
parse_locales! opt
|
28
|
+
terminal_report.unused_keys i18n_task.unused_keys(opt)
|
25
29
|
end
|
26
30
|
|
27
31
|
desc 'translate missing keys with Google Translate'
|
@@ -64,7 +68,7 @@ module I18n::Tasks
|
|
64
68
|
end
|
65
69
|
cmd :find do |opt = {}|
|
66
70
|
opt[:filter] ||= opt.delete(:pattern) || opt[:arguments].try(:first)
|
67
|
-
terminal_report.used_keys i18n_task.
|
71
|
+
terminal_report.used_keys i18n_task.used_tree(key_filter: opt[:filter].presence, source_locations: true)
|
68
72
|
end
|
69
73
|
|
70
74
|
desc 'normalize translation data: sort and move to the right files'
|
@@ -81,15 +85,15 @@ module I18n::Tasks
|
|
81
85
|
on '-l', :locales=, 'Locales to remove unused keys from (default: all)', on_locale_opt
|
82
86
|
end
|
83
87
|
cmd :remove_unused do |opt = {}|
|
84
|
-
parse_locales!
|
85
|
-
unused_keys = i18n_task.unused_keys
|
88
|
+
parse_locales! opt
|
89
|
+
unused_keys = i18n_task.unused_keys(opt)
|
86
90
|
if unused_keys.present?
|
87
91
|
terminal_report.unused_keys(unused_keys)
|
88
92
|
unless ENV['CONFIRM']
|
89
|
-
exit 1 unless agree(red "
|
93
|
+
exit 1 unless agree(red "#{unused_keys.leaves.count} translations will be removed in #{bold opt[:locales] * ', '}#{red '.'} " + yellow('Continue? (yes/no)') + ' ')
|
90
94
|
end
|
91
95
|
i18n_task.remove_unused!(opt[:locales])
|
92
|
-
$stderr.puts "Removed #{unused_keys.
|
96
|
+
$stderr.puts "Removed #{unused_keys.leaves.count} keys"
|
93
97
|
else
|
94
98
|
$stderr.puts bold green 'No unused keys to remove'
|
95
99
|
end
|
@@ -100,7 +104,6 @@ module I18n::Tasks
|
|
100
104
|
cfg = i18n_task.config_for_inspect.to_yaml
|
101
105
|
cfg.sub! /\A---\n/, ''
|
102
106
|
cfg.gsub! /^([^\s-].+?:)/, Term::ANSIColor.cyan(Term::ANSIColor.bold('\1'))
|
103
|
-
cfg.gsub! '!ruby/hash:ActiveSupport::HashWithIndifferentAccess', ''
|
104
107
|
puts cfg
|
105
108
|
end
|
106
109
|
|
@@ -119,13 +122,10 @@ module I18n::Tasks
|
|
119
122
|
spreadsheet_report.save_report opt[:path]
|
120
123
|
end
|
121
124
|
|
122
|
-
desc '
|
125
|
+
desc 'REPL session within i18n-tasks context'
|
123
126
|
cmd :irb do
|
124
|
-
require '
|
125
|
-
|
126
|
-
IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
|
127
|
-
require 'irb/ext/multi-irb'
|
128
|
-
IRB.irb nil, i18n_task
|
127
|
+
require 'i18n/tasks/console_context'
|
128
|
+
::I18n::Tasks::ConsoleContext.start
|
129
129
|
end
|
130
130
|
|
131
131
|
protected
|
@@ -85,6 +85,11 @@ module I18n::Tasks::Configuration
|
|
85
85
|
@config_sections[:base_locale] ||= (config[:base_locale] || 'en').to_s
|
86
86
|
end
|
87
87
|
|
88
|
+
def ignore_config(type = nil)
|
89
|
+
key = type ? "ignore_#{type}" : 'ignore'
|
90
|
+
@config_sections[key] ||= config[key]
|
91
|
+
end
|
92
|
+
|
88
93
|
# evaluated configuration (as the app sees it)
|
89
94
|
def config_sections
|
90
95
|
# init all sections
|
@@ -94,13 +99,16 @@ module I18n::Tasks::Configuration
|
|
94
99
|
search_config
|
95
100
|
relative_roots
|
96
101
|
translation_config
|
102
|
+
[nil, :missing, :unused, :eq_base].each do |ignore_type|
|
103
|
+
ignore_config ignore_type
|
104
|
+
end
|
97
105
|
@config_sections
|
98
106
|
end
|
99
107
|
|
100
108
|
def config_for_inspect
|
101
109
|
# hide empty sections, stringify keys
|
102
|
-
Hash[config_sections.reject { |k, v| v.empty? }.map { |k, v|
|
103
|
-
[k.to_s, v.respond_to?(:
|
110
|
+
Hash[config_sections.reject { |k, v| v.nil? || v.empty? }.map { |k, v|
|
111
|
+
[k.to_s, v.respond_to?(:deep_stringify_keys) ? v.deep_stringify_keys : v] }].tap do |h|
|
104
112
|
h.each do |_k, v|
|
105
113
|
if v.is_a?(Hash) && v.key?('config')
|
106
114
|
v.merge! v.delete('config')
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module I18n::Tasks
|
2
|
+
class ConsoleContext < BaseTask
|
3
|
+
def banner
|
4
|
+
puts Messages.banner
|
5
|
+
end
|
6
|
+
|
7
|
+
def guide
|
8
|
+
puts Messages.guide
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def start
|
13
|
+
require 'irb'
|
14
|
+
IRB.setup nil
|
15
|
+
ctx = IRB::Irb.new.context
|
16
|
+
IRB.conf[:MAIN_CONTEXT] = ctx
|
17
|
+
STDERR.puts Messages.banner
|
18
|
+
require 'irb/ext/multi-irb'
|
19
|
+
IRB.irb nil, new
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Messages
|
24
|
+
include Term::ANSIColor
|
25
|
+
extend self
|
26
|
+
|
27
|
+
def banner
|
28
|
+
bold("i18n-tasks v#{I18n::Tasks::VERSION} IRB") + "\nType #{green 'guide'} to learn more"
|
29
|
+
end
|
30
|
+
|
31
|
+
def guide
|
32
|
+
green(bold "i18n-tasks IRB Quick Start guide") + "\n" + <<-TEXT
|
33
|
+
#{yellow 'Data as trees'}
|
34
|
+
data[base_locale]
|
35
|
+
missing_tree(locale, compared_to = base_locale)
|
36
|
+
used_tree(source_locations: false, key_filter: nil)
|
37
|
+
unused_tree(locale)
|
38
|
+
Tree::Siblings['es' => {'hello' => 'Hola'}]
|
39
|
+
|
40
|
+
#{yellow 'Traversal'}
|
41
|
+
tree = missing_tree(base_locale)
|
42
|
+
tree.nodes { |node| }
|
43
|
+
tree.nodes.to_a
|
44
|
+
tree.leaves { |node| }
|
45
|
+
tree.each { |root_node| }
|
46
|
+
# also levels, depth_first, and breadth_first
|
47
|
+
|
48
|
+
#{yellow 'Select nodes'}
|
49
|
+
tree.select_nodes { |node| } # new tree with only selected nodes
|
50
|
+
|
51
|
+
#{yellow 'Match by full key'}
|
52
|
+
tree.select_keys { |key, leaf| } # new tree with only selected keys
|
53
|
+
tree.grep_keys(/hello/) # grep, using ===
|
54
|
+
tree.keys { |key, leaf| } # enumerate over [full_key, leaf_node]
|
55
|
+
# Pass {root: true} to include root node in full_key (usually locale)
|
56
|
+
|
57
|
+
#{yellow 'Nodes'}
|
58
|
+
node = missing_tree(base_locale).leaves.first
|
59
|
+
node.key # only the part after the last dot
|
60
|
+
node.full_key # full key. Includes root key, pass {root: false} to override.
|
61
|
+
# also: value, value_or_children_hash, data, walk_to_root, walk_from_root
|
62
|
+
Tree::Node.new(key: 'en')
|
63
|
+
|
64
|
+
#{yellow 'Keys'}
|
65
|
+
t(key, locale)
|
66
|
+
key_value?(key, locale)
|
67
|
+
depluralize_key(key, locale) # convert 'hat.one' to 'hat'
|
68
|
+
absolutize_key(key, path) # '.title' to 'users.index.title'
|
69
|
+
TEXT
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/i18n/tasks/data.rb
CHANGED
@@ -21,12 +21,6 @@ module I18n::Tasks
|
|
21
21
|
data.t(key, locale)
|
22
22
|
end
|
23
23
|
|
24
|
-
def missing_tree(locale, compared_to = base_locale)
|
25
|
-
data[compared_to].select_keys(root: false) { |key, node|
|
26
|
-
!key_value?(key, locale) && !ignore_key?(key, :missing)
|
27
|
-
}
|
28
|
-
end
|
29
|
-
|
30
24
|
def tree(locale)
|
31
25
|
data[locale][locale].children
|
32
26
|
end
|
@@ -57,46 +51,5 @@ module I18n::Tasks
|
|
57
51
|
data[target_locale] = data[target_locale]
|
58
52
|
end
|
59
53
|
end
|
60
|
-
|
61
|
-
# if :locales option present, call update_locale_data for each locale
|
62
|
-
# otherwise, call update_locale_data for :locale option or base locale
|
63
|
-
# @option opts [Array] :locales
|
64
|
-
# @option opts [String] :locale
|
65
|
-
def update_data(opts = {})
|
66
|
-
if opts.key?(:locales)
|
67
|
-
locales = (Array(opts[:locales]).presence || self.locales).map(&:to_s)
|
68
|
-
# make sure base_locale always comes first if present
|
69
|
-
locales = [base_locale] + (locales - [base_locale]) if locales.include?(base_locale)
|
70
|
-
opts = opts.except(:locales)
|
71
|
-
locales.each { |locale| update_locale_data(locale, opts.merge(locale: locale)) }
|
72
|
-
else
|
73
|
-
update_locale_data(opts[:locale] || base_locale, opts)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# @param locale
|
78
|
-
# @option opts [Array|Proc] :keys keys to update, if proc call with locale
|
79
|
-
# @option opts [String|Proc] value, if proc call with each key
|
80
|
-
# @option opts [String|Proc] values, if proc call with all the keys
|
81
|
-
def update_locale_data(locale, opts = {})
|
82
|
-
locale = locale.to_s
|
83
|
-
keys = opts[:keys]
|
84
|
-
keys = keys.call(locale) if keys.respond_to?(:call)
|
85
|
-
return if keys.empty?
|
86
|
-
|
87
|
-
values = opts[:values]
|
88
|
-
values = values.call(keys, locale) if values.respond_to?(:call)
|
89
|
-
values ||= begin
|
90
|
-
value = opts[:value] or raise 'pass value or values'
|
91
|
-
if value.respond_to?(:call)
|
92
|
-
keys.map { |key| value.call(key, locale) }
|
93
|
-
else
|
94
|
-
[value] * keys.size
|
95
|
-
end
|
96
|
-
end
|
97
|
-
data[locale] = tree(locale).merge!(
|
98
|
-
Tree::Siblings.from_flat_pairs keys.map(&:to_s).zip(values)
|
99
|
-
).parent
|
100
|
-
end
|
101
54
|
end
|
102
55
|
end
|
@@ -8,7 +8,12 @@ module I18n::Tasks
|
|
8
8
|
|
9
9
|
# @return [Hash] locale tree
|
10
10
|
def parse(str, options)
|
11
|
-
YAML.load
|
11
|
+
if YAML.method(:load).arity.abs == 2
|
12
|
+
YAML.load(str, options || {})
|
13
|
+
else
|
14
|
+
# older jruby and rbx 2.2.7 do not accept options
|
15
|
+
YAML.load(str)
|
16
|
+
end
|
12
17
|
end
|
13
18
|
|
14
19
|
# @return [String]
|
@@ -15,14 +15,14 @@ module I18n::Tasks
|
|
15
15
|
return to_enum(:route, locale, forest) unless block
|
16
16
|
out = {}
|
17
17
|
not_found = Set.new
|
18
|
-
forest.keys
|
18
|
+
forest.keys do |key, node|
|
19
19
|
locale_key = "#{locale}.#{key}"
|
20
|
-
path = adapter[locale][locale_key].data[:path
|
20
|
+
path = adapter[locale][locale_key].try(:data).try(:[], :path)
|
21
21
|
|
22
22
|
# infer from base
|
23
23
|
unless path
|
24
24
|
path = base_tree["#{base_locale}.#{key}"].try(:data).try(:[], :path)
|
25
|
-
path = path.try :sub, /(
|
25
|
+
path = path.try :sub, /(^|[\/.])#{base_locale}(?=\.)/, "\\1#{locale}"
|
26
26
|
end
|
27
27
|
|
28
28
|
if path
|
@@ -32,10 +32,10 @@ module I18n::Tasks
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
out.each do |dest, keys|
|
35
|
-
block.yield dest, forest.select_keys { |key, _| keys.include?(key) }
|
35
|
+
block.yield dest, forest.select_keys(root: true) { |key, _| keys.include?(key) }
|
36
36
|
end
|
37
37
|
if not_found.present?
|
38
|
-
super(locale, forest.select_keys { |key, _| not_found.include?(key) }, &block)
|
38
|
+
super(locale, forest.select_keys(root: true) { |key, _| not_found.include?(key) }, &block)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
@@ -28,7 +28,7 @@ module I18n::Tasks
|
|
28
28
|
return to_enum(:route, locale, forest) unless block
|
29
29
|
locale = locale.to_s
|
30
30
|
out = {}
|
31
|
-
forest.keys
|
31
|
+
forest.keys do |key, _node|
|
32
32
|
pattern, path = routes.detect { |route| route[0] =~ key }
|
33
33
|
if pattern
|
34
34
|
key_match = $~
|
@@ -41,7 +41,7 @@ module I18n::Tasks
|
|
41
41
|
end
|
42
42
|
out.each do |dest, keys|
|
43
43
|
block.yield dest,
|
44
|
-
forest.select_keys { |key, _| keys.include?(key) }
|
44
|
+
forest.select_keys(root: true) { |key, _| keys.include?(key) }
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|