rubocop-i18n 0.0.1 → 1.0.0

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
  SHA1:
3
- metadata.gz: 8d3c4fbbfe759ead1c4d19be86d280092252ca70
4
- data.tar.gz: e516b0a7acbbe8d554cf0fffea3faa243852eb70
3
+ metadata.gz: e2792a2344ae871fa62e564786c03d892bd5072d
4
+ data.tar.gz: 1354b5ac2e3a9606deb1b9c594edf8a23327db38
5
5
  SHA512:
6
- metadata.gz: 7d1ab20e74da22d5ff3f9e57347f460615a2393cda816008a18216dec4f184ce7c7e53b787aaee34a5f60b3352fef868c990cd6a11b9ceafae5c4e5635eba02e
7
- data.tar.gz: 01c62345a6f67c485d48d0a7e524b6073fc6fe93315303d48ed0f6f04f259377b0103790b8098b0465ac340816ea283520203824e93ec228f76cf10e65b23b08
6
+ metadata.gz: 76633906bc7a3ad0612fff96eab7a80eb21bf26f58b95d6a31c8e24ca2958df6c95f079000c5002785ad184711e207b22b2059383d1791665728a3a73330697e
7
+ data.tar.gz: 4f58a7770df70e74ea4d5150140ee35aaef1b31aacd65b776de7c6ceb6f638715756d71bf43dd5a4b870c724496e01fda0e3f95bdee02da3f7755e98935997bc
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ matrix:
3
+ fast_finish: true
4
+ include:
5
+ - rvm: 2.4.1
6
+ notifications:
7
+ email: false
data/README.md CHANGED
@@ -31,12 +31,142 @@ GetText/DecorateFunctionMessage:
31
31
  Enabled: true
32
32
  ```
33
33
 
34
+ ## Cops
35
+
36
+ ### GetText/DecorateFunctionMessage
37
+
38
+ This cop looks for any raise or fail functions and checks that the user visible message is using gettext decoration with the _() function.
39
+ This cop makes sure the message is decorated, as well as checking that the formatting of the message is compliant according to the follow rules.
40
+ This cop supports autocorrecting of [Simple decoration of a message](#Simple-decoration-of-a-message). See the rubocop documentation on how to run autocorrect.
41
+
42
+ #### Simple decoration of a message
43
+
44
+ Simple message strings should be decorated with the _() function
45
+
46
+ ##### Error message thrown
47
+
48
+ ```
49
+ 'raise' function, message string should be decorated
50
+ ```
51
+
52
+ ##### Bad
53
+
54
+ ``` ruby
55
+ raise("Warning")
56
+ ```
57
+
58
+ ##### Good
59
+
60
+ ``` ruby
61
+ raise(_("Warning"))
62
+ ```
63
+
64
+ #### Multi-line message
65
+
66
+ The message should not span multiple lines, it causes issues during the translation process.
67
+
68
+ ##### Error message thrown
69
+
70
+ ```
71
+ 'raise' function, message should not be a multi-line string
72
+ ```
73
+
74
+ ##### Bad
75
+
76
+ ``` ruby
77
+ raise("this is a multi" \
78
+ "line message")
79
+ ```
80
+
81
+ ##### Good
82
+
83
+ ``` ruby
84
+ raise(_("this is a multi line message"))
85
+ ```
86
+
87
+ #### Concatenated message
88
+
89
+ The message should not concatenate multiple strings, it causes issues during translation and with the gettext.
90
+
91
+ ##### Error message thrown
92
+
93
+ ```
94
+ 'raise' function, message should not be a concatenated string
95
+ ```
96
+
97
+ ##### Bad
98
+
99
+ ``` ruby
100
+ raise("this is a concatenated" + "message")
101
+ ```
102
+
103
+ ##### Good
104
+
105
+ ``` ruby
106
+ raise(_("this is a concatenated message"))
107
+ ```
108
+
109
+ #### Interpolated message
110
+
111
+ The message should be formated in this particular style. Otherwise it causes issues during translation and with the gettext gem.
112
+
113
+ ##### Error message thrown
114
+
115
+ ```
116
+ 'raise' function, message should use correctly formatted interpolation
117
+ ```
118
+
119
+ ##### Bad
120
+
121
+ ``` ruby
122
+ raise("this is an interpolated message IE #{variable}")
123
+ ```
124
+
125
+ ##### Good
126
+
127
+ ``` ruby
128
+ raise(_("this is an interpolated message IE %{value0}") % {value0: var,})
129
+ ```
130
+
131
+ #### No decoration and no string detected
132
+
133
+ The raise or fail function does not contain any decoration, or a simple string
134
+
135
+ ##### Error message thrown
136
+
137
+ ```
138
+ 'raise' function, message should be decorated
139
+ ```
140
+
141
+ ##### Bad
142
+
143
+ ``` ruby
144
+ raise(someOtherFuntioncall(foo, "bar"))
145
+ ```
146
+
147
+ ##### Good
148
+
149
+ In this raise or fail function, the message does not contain any decoration at all and the message is not a simple string. It may make sense to convert the message to a simple string. eg [Simple decoration of a message](#Simple-decoration-of-a-message).
150
+ Or ignore this raise or fail function following this [How to ignore rules in code](#How-to-ignore-rules-in-code) section.
151
+
152
+ ## How to ignore rules in code
153
+
154
+ It may be necessary to ignore a cop for a particular piece of code. We follow standard rubocop idioms.
155
+ ``` ruby
156
+ raise("We don't want this translated") # rubocop:disable GetText/DecorateFunctionMessage
157
+ ```
158
+
159
+ ## Known Issues
160
+
161
+ Rubocop currently does not detect Heredoc style messages in functions correctly, which in turn prevents this plugin from detecting them correctly.
162
+
34
163
  ## Development
35
164
 
36
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
165
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that allows you to experiment.
37
166
 
38
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
167
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which creates a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
39
168
 
40
169
  ## Contributing
41
170
 
42
171
  Bug reports and pull requests are welcome on GitHub at https://github.com/highb/rubocop-i18n. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
172
+
@@ -3,92 +3,152 @@ module RuboCop
3
3
  module I18n
4
4
  module GetText
5
5
  class DecorateFunctionMessage < Cop
6
+ SUPPORTED_METHODS = ['raise', 'fail']
7
+ SUPPORTED_DECORATORS = ['_', 'n_', 'N_']
6
8
 
7
9
  def on_send(node)
8
10
  method_name = node.loc.selector.source
9
- return if !/raise|fail/.match(method_name)
10
- if method_name == "raise"
11
- receiver_node, method_name, *arg_nodes = *node
12
- if !arg_nodes.empty? && arg_nodes[0].type == :const && arg_nodes[1]
13
- how_bad_is_it(node, method_name, arg_nodes[1])
14
- end
15
- elsif method_name == "fail"
16
- receiver_node, method_name, *arg_nodes = *node
17
- if !arg_nodes.empty?
18
- how_bad_is_it(node, method_name, arg_nodes[0])
11
+ return if !supported_method_name?(method_name)
12
+ _, method_name, *arg_nodes = *node
13
+ if !arg_nodes.empty? && !already_decorated?(node) && (contains_string?(arg_nodes) || string_constant?(arg_nodes))
14
+ if string_constant?(arg_nodes)
15
+ message_section = arg_nodes[1]
16
+ else
17
+ message_section = arg_nodes[0]
19
18
  end
19
+
20
+ detect_and_report(node, message_section, method_name)
20
21
  end
21
22
  end
22
23
 
23
24
  private
24
25
 
25
- def how_bad_is_it(node, method_name, message)
26
- if message.str_type?
27
- add_offense(message, :expression, "'#{method_name}' should have a decorator around the message")
28
- elsif multiline_offense?(message)
29
- add_offense(message, :expression, "'#{method_name}' should not use a multi-line string")
30
- elsif concatination_offense?(message)
31
- add_offense(message, :expression, "'#{method_name}' should not use a concatenated string")
32
- elsif interpolation_offense?(message)
33
- add_offense(message, :expression, "'#{method_name}' interpolation is a sin")
26
+ def supported_method_name?(method_name)
27
+ SUPPORTED_METHODS.include?(method_name)
28
+ end
29
+
30
+ def already_decorated?(node, parent = nil)
31
+ parent ||= node
32
+
33
+ if node.respond_to?(:loc) && node.loc.respond_to?(:selector)
34
+ return true if SUPPORTED_DECORATORS.include?(node.loc.selector.source)
34
35
  end
36
+
37
+ return false unless node.respond_to?(:children)
38
+
39
+ node.children.any? { |child| already_decorated?(child, parent) }
35
40
  end
36
41
 
37
- def multiline_offense?(message)
38
- found_multiline = false
39
- found_strings = false
40
- message.children.each { |child|
41
- if child == :/
42
- found_multiline = true
43
- elsif ( (!child.nil? && child.class != Symbol) && ( child.str_type? || child.dstr_type? ) )
44
- found_strings = true
45
- end
46
- }
47
- found_multiline && found_strings
42
+ def string_constant?(nodes)
43
+ nodes[0].type == :const && nodes[1]
48
44
  end
49
45
 
50
- def concatination_offense?(message)
51
- found_concat = false
52
- found_strings = false
53
- message.children.each { |child|
54
- if child == :+
55
- found_concat = true
56
- elsif ( (!child.nil? && child.class != Symbol) && ( child.str_type? || child.dstr_type? ) )
57
- found_strings = true
58
- end
59
- }
60
- found_concat && found_strings
46
+ def contains_string?(nodes)
47
+ nodes[0].inspect.include?(":str") || nodes[0].inspect.include?(":dstr")
61
48
  end
62
49
 
63
- def interpolation_offense?(message)
64
- found_funct = false
65
- message.children.each { |child|
66
- if !child.nil? && child.class != Symbol
67
- if child.begin_type? || child.send_type?
68
- found_funct = true
69
- elsif child.dstr_type?
70
- found_funct = true if child.inspect.include?(":send") || child.inspect.include?(":begin")
71
- end
72
- end
73
- }
74
- found_funct
50
+ def detect_and_report(node, message_section, method_name)
51
+ errors = how_bad_is_it(message_section)
52
+ return if errors.empty?
53
+ error_message = "'#{method_name}' function, "
54
+ errors.each do |error|
55
+ error_message << 'message string should be decorated. ' if error == :simple
56
+ error_message << 'message should not be a concatenated string. ' if error == :concatenation
57
+ error_message << 'message should not be a multi-line string. ' if error == :multiline
58
+ error_message << 'message should use correctly formatted interpolation. ' if error == :interpolation
59
+ error_message << 'message should be decorated. ' if error == :no_decoration
60
+ end
61
+ add_offense(message_section, :expression, error_message)
62
+ end
63
+
64
+ def how_bad_is_it(message_section)
65
+ errors = []
66
+
67
+ errors.push :simple if message_section.str_type?
68
+ errors.push :multiline if message_section.multiline?
69
+ errors.push :concatenation if concatenation_offense?(message_section)
70
+ errors.push :interpolation if interpolation_offense?(message_section)
71
+ errors.push :no_decoration if !already_decorated?(message_section)
72
+
73
+ # only display no_decoration, if that is the only problem.
74
+ if errors.size > 1 && errors.include?(:no_decoration)
75
+ errors.delete(:no_decoration)
76
+ end
77
+ errors
78
+ end
79
+
80
+ def concatenation_offense?(node, parent = nil)
81
+ parent ||= node
82
+
83
+ if node.respond_to?(:loc) && node.loc.respond_to?(:selector)
84
+ return true if node.loc.selector.source == '+'
85
+ end
86
+
87
+ return false unless node.respond_to?(:children)
88
+
89
+ node.children.any? { |child| concatenation_offense?(child, parent) }
90
+ end
91
+
92
+ def interpolation_offense?(node, parent = nil)
93
+ parent ||= node
94
+
95
+ return true if node.class == RuboCop::AST::Node && node.dstr_type?
96
+
97
+ return false unless node.respond_to?(:children)
98
+
99
+ node.children.any? { |child| interpolation_offense?(child, parent) }
75
100
  end
76
101
 
77
102
  def autocorrect(node)
78
103
  if node.str_type?
79
104
  single_string_correct(node)
80
- else
81
- multiline_string_correct(node)
105
+ elsif interpolation_offense?(node)
106
+ # interpolation_correct(node)
82
107
  end
83
108
  end
84
109
 
85
110
  def single_string_correct(node)
86
- ->(corrector) { corrector.insert_before(node.source_range , "_(")
111
+ ->(corrector) {
112
+ corrector.insert_before(node.source_range , "_(")
87
113
  corrector.insert_after(node.source_range , ")") }
88
114
  end
89
115
 
90
- def multiline_string_correct(node)
116
+ def interpolation_correct(node)
117
+ interpolated_values_string = ""
118
+ count = 0
119
+ ->(corrector) {
120
+ node.children.each do |child|
121
+ # dstrs are split into "str" segments and other segments.
122
+ # The "other" segments are the interpolated values.
123
+ if child.type == :begin
124
+ value = child.children[0]
125
+ hash_key = "value"
126
+ if value.type == :lvar
127
+ # Use the variable's name as the format key
128
+ hash_key = value.loc.name.source
129
+ else
130
+ # These are placeholders that will manually need to be given
131
+ # a descriptive name
132
+ hash_key << "#{count}"
133
+ count += 1
134
+ end
135
+ if interpolated_values_string.empty?
136
+ interpolated_values_string << "{ "
137
+ end
138
+ interpolated_values_string << "#{hash_key}: #{value.loc.expression.source}, "
139
+
140
+ # Replace interpolation with format string
141
+ corrector.replace(child.loc.expression, "%{#{hash_key}}")
142
+ end
143
+ end
144
+ if !interpolated_values_string.empty?
145
+ interpolated_values_string << "}"
146
+ end
147
+ corrector.insert_before(node.source_range, '_(')
148
+ corrector.insert_after(node.source_range, ") % #{interpolated_values_string}")
149
+ }
91
150
  end
151
+
92
152
  end
93
153
  end
94
154
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+
5
+ # This module provides methods that make it easier to test Cops.
6
+ module CopHelper
7
+ extend RSpec::SharedContext
8
+
9
+ let(:ruby_version) { 2.2 }
10
+ let(:enabled_rails) { false }
11
+ let(:rails_version) { false }
12
+
13
+ def inspect_source_file(source)
14
+ Tempfile.open('tmp') { |f| inspect_source(source, f) }
15
+ end
16
+
17
+ def inspect_gemfile(source)
18
+ inspect_source(source, 'Gemfile')
19
+ end
20
+
21
+ def inspect_source(source, file = nil)
22
+ if source.is_a?(Array) && source.size == 1
23
+ raise "Don't use an array for a single line of code: #{source}"
24
+ end
25
+ RuboCop::Formatter::DisabledConfigFormatter.config_to_allow_offenses = {}
26
+ RuboCop::Formatter::DisabledConfigFormatter.detected_styles = {}
27
+ processed_source = parse_source(source, file)
28
+ raise 'Error parsing example code' unless processed_source.valid_syntax?
29
+ _investigate(cop, processed_source)
30
+ end
31
+
32
+ def parse_source(source, file = nil)
33
+ source = source.join($RS) if source.is_a?(Array)
34
+
35
+ if file && file.respond_to?(:write)
36
+ file.write(source)
37
+ file.rewind
38
+ file = file.path
39
+ end
40
+
41
+ RuboCop::ProcessedSource.new(source, ruby_version, file)
42
+ end
43
+
44
+ def autocorrect_source_file(source)
45
+ Tempfile.open('tmp') { |f| autocorrect_source(source, f) }
46
+ end
47
+
48
+ def autocorrect_source(source, file = nil)
49
+ cop.instance_variable_get(:@options)[:auto_correct] = true
50
+ processed_source = parse_source(source, file)
51
+ _investigate(cop, processed_source)
52
+
53
+ corrector =
54
+ RuboCop::Cop::Corrector.new(processed_source.buffer, cop.corrections)
55
+ corrector.rewrite
56
+ end
57
+
58
+ def autocorrect_source_with_loop(source, file = nil)
59
+ loop do
60
+ cop.instance_variable_set(:@corrections, [])
61
+ new_source = autocorrect_source(source, file)
62
+ return new_source if new_source == source
63
+ source = new_source
64
+ end
65
+ end
66
+
67
+ def _investigate(cop, processed_source)
68
+ forces = RuboCop::Cop::Force.all.each_with_object([]) do |klass, instances|
69
+ next unless cop.join_force?(klass)
70
+ instances << klass.new([cop])
71
+ end
72
+
73
+ commissioner =
74
+ RuboCop::Cop::Commissioner.new([cop], forces, raise_error: true)
75
+ commissioner.investigate(processed_source)
76
+ commissioner
77
+ end
78
+ end
79
+
80
+ module RuboCop
81
+ module Cop
82
+ # Monkey-patch Cop for tests to provide easy access to messages and
83
+ # highlights.
84
+ class Cop
85
+ def messages
86
+ offenses.sort.map(&:message)
87
+ end
88
+
89
+ def highlights
90
+ offenses.sort.map { |o| o.location.source }
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ RSpec.configure do |config|
97
+ config.include CopHelper
98
+ end
@@ -4,13 +4,13 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "rubocop-i18n"
7
- spec.version = '0.0.1'
8
- spec.authors = ["Brandon High"]
9
- spec.email = ["brandon.high@puppet.com"]
7
+ spec.version = '1.0.0'
8
+ spec.authors = ["Brandon High", "TP Honey", "Helen Campbell"]
9
+ spec.email = ["brandon.high@puppet.com", "tp@puppet.com", "helen@puppet.com"]
10
10
 
11
11
  spec.summary = %q{RuboCop rules for i18n}
12
12
  spec.description = %q{RuboCop rules for detecting and autocorrecting undecorated strings for i18n}
13
- spec.homepage = "https://github.com/highb/rubocop-i18n"
13
+ spec.homepage = "https://github.com/puppetlabs/rubocop-i18n"
14
14
  spec.license = 'Apache-2'
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
@@ -23,6 +23,7 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency "bundler", "~> 1.14"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
25
  spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency "rb-readline"
26
27
  spec.add_development_dependency "pry"
27
28
  spec.add_runtime_dependency "rubocop", "~> 0.49"
28
29
  end
metadata CHANGED
@@ -1,14 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-i18n
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon High
8
+ - TP Honey
9
+ - Helen Campbell
8
10
  autorequire:
9
11
  bindir: exe
10
12
  cert_chain: []
11
- date: 2017-08-11 00:00:00.000000000 Z
13
+ date: 2017-09-06 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
16
  name: bundler
@@ -52,6 +54,20 @@ dependencies:
52
54
  - - "~>"
53
55
  - !ruby/object:Gem::Version
54
56
  version: '3.0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rb-readline
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
55
71
  - !ruby/object:Gem::Dependency
56
72
  name: pry
57
73
  requirement: !ruby/object:Gem::Requirement
@@ -84,14 +100,16 @@ description: RuboCop rules for detecting and autocorrecting undecorated strings
84
100
  i18n
85
101
  email:
86
102
  - brandon.high@puppet.com
103
+ - tp@puppet.com
104
+ - helen@puppet.com
87
105
  executables: []
88
106
  extensions: []
89
107
  extra_rdoc_files: []
90
108
  files:
91
109
  - ".gitignore"
110
+ - ".travis.yml"
92
111
  - CODE_OF_CONDUCT.md
93
112
  - Gemfile
94
- - Gemfile.lock
95
113
  - LICENSE
96
114
  - README.md
97
115
  - Rakefile
@@ -102,8 +120,9 @@ files:
102
120
  - lib/rubocop/cop/i18n/gettext.rb
103
121
  - lib/rubocop/cop/i18n/gettext/decorate_function_message.rb
104
122
  - lib/rubocop/cop/i18n/gettext/decorate_string.rb
123
+ - lib/rubocop/rspec/cop_helper.rb
105
124
  - rubocop-i18n.gemspec
106
- homepage: https://github.com/highb/rubocop-i18n
125
+ homepage: https://github.com/puppetlabs/rubocop-i18n
107
126
  licenses:
108
127
  - Apache-2
109
128
  metadata: {}
@@ -1,60 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- rubocop-i18n (0.0.1)
5
- rubocop (~> 0.49)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- ast (2.3.0)
11
- coderay (1.1.1)
12
- diff-lcs (1.3)
13
- method_source (0.8.2)
14
- parallel (1.12.0)
15
- parser (2.4.0.0)
16
- ast (~> 2.2)
17
- powerpack (0.1.1)
18
- pry (0.10.4)
19
- coderay (~> 1.1.0)
20
- method_source (~> 0.8.1)
21
- slop (~> 3.4)
22
- rainbow (2.2.2)
23
- rake
24
- rake (10.5.0)
25
- rspec (3.6.0)
26
- rspec-core (~> 3.6.0)
27
- rspec-expectations (~> 3.6.0)
28
- rspec-mocks (~> 3.6.0)
29
- rspec-core (3.6.0)
30
- rspec-support (~> 3.6.0)
31
- rspec-expectations (3.6.0)
32
- diff-lcs (>= 1.2.0, < 2.0)
33
- rspec-support (~> 3.6.0)
34
- rspec-mocks (3.6.0)
35
- diff-lcs (>= 1.2.0, < 2.0)
36
- rspec-support (~> 3.6.0)
37
- rspec-support (3.6.0)
38
- rubocop (0.49.1)
39
- parallel (~> 1.10)
40
- parser (>= 2.3.3.1, < 3.0)
41
- powerpack (~> 0.1)
42
- rainbow (>= 1.99.1, < 3.0)
43
- ruby-progressbar (~> 1.7)
44
- unicode-display_width (~> 1.0, >= 1.0.1)
45
- ruby-progressbar (1.8.1)
46
- slop (3.6.0)
47
- unicode-display_width (1.3.0)
48
-
49
- PLATFORMS
50
- ruby
51
-
52
- DEPENDENCIES
53
- bundler (~> 1.14)
54
- pry
55
- rake (~> 10.0)
56
- rspec (~> 3.0)
57
- rubocop-i18n!
58
-
59
- BUNDLED WITH
60
- 1.14.6