i18n_flow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +3 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +13 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +45 -0
  8. data/LICENSE +22 -0
  9. data/README.md +103 -0
  10. data/Rakefile +2 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/doc/rules.md +316 -0
  14. data/doc/tags.md +488 -0
  15. data/example/example.en.yml +14 -0
  16. data/example/example.ja.yml +9 -0
  17. data/exe/i18n_flow +11 -0
  18. data/i18n_flow.gemspec +28 -0
  19. data/i18n_flow.yml +8 -0
  20. data/lib/i18n_flow/cli/color.rb +18 -0
  21. data/lib/i18n_flow/cli/command_base.rb +33 -0
  22. data/lib/i18n_flow/cli/copy_command.rb +69 -0
  23. data/lib/i18n_flow/cli/help_command.rb +29 -0
  24. data/lib/i18n_flow/cli/lint_command/ascii.erb +45 -0
  25. data/lib/i18n_flow/cli/lint_command/ascii_renderer.rb +58 -0
  26. data/lib/i18n_flow/cli/lint_command/markdown.erb +49 -0
  27. data/lib/i18n_flow/cli/lint_command/markdown_renderer.rb +55 -0
  28. data/lib/i18n_flow/cli/lint_command.rb +55 -0
  29. data/lib/i18n_flow/cli/read_config_command.rb +20 -0
  30. data/lib/i18n_flow/cli/search_command/default.erb +11 -0
  31. data/lib/i18n_flow/cli/search_command/default_renderer.rb +67 -0
  32. data/lib/i18n_flow/cli/search_command/oneline.erb +5 -0
  33. data/lib/i18n_flow/cli/search_command/oneline_renderer.rb +39 -0
  34. data/lib/i18n_flow/cli/search_command.rb +59 -0
  35. data/lib/i18n_flow/cli/split_command.rb +20 -0
  36. data/lib/i18n_flow/cli/version_command.rb +9 -0
  37. data/lib/i18n_flow/cli.rb +42 -0
  38. data/lib/i18n_flow/configuration.rb +205 -0
  39. data/lib/i18n_flow/parser.rb +34 -0
  40. data/lib/i18n_flow/repository.rb +39 -0
  41. data/lib/i18n_flow/search.rb +176 -0
  42. data/lib/i18n_flow/splitter/merger.rb +60 -0
  43. data/lib/i18n_flow/splitter/strategy.rb +66 -0
  44. data/lib/i18n_flow/splitter.rb +5 -0
  45. data/lib/i18n_flow/util.rb +57 -0
  46. data/lib/i18n_flow/validator/errors.rb +99 -0
  47. data/lib/i18n_flow/validator/file_scope.rb +58 -0
  48. data/lib/i18n_flow/validator/multiplexer.rb +58 -0
  49. data/lib/i18n_flow/validator/symmetry.rb +154 -0
  50. data/lib/i18n_flow/validator.rb +4 -0
  51. data/lib/i18n_flow/version.rb +7 -0
  52. data/lib/i18n_flow/yaml_ast_proxy/mapping.rb +72 -0
  53. data/lib/i18n_flow/yaml_ast_proxy/node.rb +128 -0
  54. data/lib/i18n_flow/yaml_ast_proxy/node_meta_data.rb +86 -0
  55. data/lib/i18n_flow/yaml_ast_proxy/sequence.rb +29 -0
  56. data/lib/i18n_flow/yaml_ast_proxy.rb +57 -0
  57. data/lib/i18n_flow.rb +15 -0
  58. data/spec/lib/i18n_flow/cli/command_base_spec.rb +46 -0
  59. data/spec/lib/i18n_flow/cli/help_command_spec.rb +13 -0
  60. data/spec/lib/i18n_flow/cli/version_command_spec.rb +13 -0
  61. data/spec/lib/i18n_flow/configuration_spec.rb +334 -0
  62. data/spec/lib/i18n_flow/repository_spec.rb +40 -0
  63. data/spec/lib/i18n_flow/splitter/merger_spec.rb +149 -0
  64. data/spec/lib/i18n_flow/util_spec.rb +194 -0
  65. data/spec/lib/i18n_flow/validator/file_scope_spec.rb +74 -0
  66. data/spec/lib/i18n_flow/validator/multiplexer_spec.rb +68 -0
  67. data/spec/lib/i18n_flow/validator/symmetry_spec.rb +511 -0
  68. data/spec/lib/i18n_flow/yaml_ast_proxy/node_spec.rb +151 -0
  69. data/spec/lib/i18n_flow_spec.rb +21 -0
  70. data/spec/spec_helper.rb +16 -0
  71. data/spec/support/repository_examples.rb +60 -0
  72. data/spec/support/util_macro.rb +14 -0
  73. metadata +214 -0
@@ -0,0 +1,99 @@
1
+ module I18nFlow::Validator
2
+ class Error
3
+ attr_reader :key
4
+ attr_reader :file
5
+ attr_reader :line
6
+
7
+ def initialize(key)
8
+ @key = key
9
+ end
10
+
11
+ def ==(other)
12
+ return false unless other.is_a?(self.class)
13
+ data == other.data
14
+ end
15
+
16
+ def data
17
+ [key]
18
+ end
19
+
20
+ def single?
21
+ !!@single
22
+ end
23
+
24
+ def set_location(node)
25
+ @file = node.file_path
26
+ @line = node.start_line
27
+ self
28
+ end
29
+ end
30
+
31
+ class InvalidTypeError < Error
32
+ def initialize(key, single: false)
33
+ super(key)
34
+ @single = single
35
+ end
36
+ end
37
+
38
+ class MissingKeyError < Error
39
+ def initialize(key, single: false)
40
+ super(key)
41
+ @single = single
42
+ end
43
+ end
44
+
45
+ class ExtraKeyError < Error
46
+ def initialize(key, single: false)
47
+ super(key)
48
+ @single = single
49
+ end
50
+ end
51
+
52
+ class InvalidTodoError < Error
53
+ end
54
+
55
+ class TodoContentError < Error
56
+ attr_reader :expect
57
+ attr_reader :actual
58
+
59
+ def initialize(key, expect:, actual:)
60
+ super(key)
61
+ @expect = expect
62
+ @actual = actual
63
+ end
64
+
65
+ def data
66
+ super + [expect, actual]
67
+ end
68
+ end
69
+
70
+ class InvalidLocaleError < Error
71
+ attr_reader :expect
72
+ attr_reader :actual
73
+
74
+ def initialize(key, expect:, actual:)
75
+ super(key)
76
+ @expect = expect
77
+ @actual = actual
78
+ end
79
+
80
+ def data
81
+ super + [expect, actual]
82
+ end
83
+ end
84
+
85
+ class AsymmetricArgsError < Error
86
+ attr_reader :expect
87
+ attr_reader :actual
88
+
89
+ def initialize(key, expect:, actual:)
90
+ super(key)
91
+ @expect = expect
92
+ @actual = actual
93
+ end
94
+
95
+ def data
96
+ super + [expect, actual]
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,58 @@
1
+ require_relative 'errors'
2
+ require_relative '../util'
3
+
4
+ module I18nFlow::Validator
5
+ class FileScope
6
+ attr_reader :ast
7
+ attr_reader :filepath
8
+
9
+ def initialize(ast, filepath:)
10
+ @ast = ast
11
+ @filepath = filepath
12
+ end
13
+
14
+ def filepath_scopes
15
+ @filepath_scopes ||= I18nFlow::Util.filepath_to_scope(filepath)
16
+ end
17
+
18
+ def validate!
19
+ @errors = nil
20
+ validate_scope(ast, scopes: filepath_scopes)
21
+ end
22
+
23
+ def errors
24
+ @errors ||= []
25
+ end
26
+
27
+ private
28
+
29
+ def validate_scope(tree, scopes:)
30
+ scopes.each_with_index do |scope, i|
31
+ node = tree[scope]
32
+
33
+ if node.nil?
34
+ full_key = scopes[0..i].join('.')
35
+ errors << MissingKeyError.new(full_key, single: true).set_location(tree)
36
+ break
37
+ end
38
+
39
+ if tree.mapping? && tree.size > 1
40
+ parent_scopes = scopes[0...i]
41
+ (tree.keys - [scope]).each do |key|
42
+ full_key = [*parent_scopes, key].join('.')
43
+ errors << ExtraKeyError.new(full_key, single: true).set_location(node)
44
+ end
45
+ break
46
+ end
47
+
48
+ if node.scalar?
49
+ full_key = scopes[0..i].join('.')
50
+ errors << InvalidTypeError.new(full_key, single: true).set_location(node)
51
+ break
52
+ end
53
+
54
+ tree = node
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ require_relative 'file_scope'
2
+ require_relative 'symmetry'
3
+ require_relative '../parser'
4
+
5
+ module I18nFlow::Validator
6
+ class Multiplexer
7
+ attr_reader :repository
8
+ attr_reader :valid_locales
9
+ attr_reader :locale_pairs
10
+ attr_reader :linters
11
+
12
+ def initialize(
13
+ repository:,
14
+ valid_locales:,
15
+ locale_pairs:,
16
+ linters: %i[file_scope symmetry]
17
+ )
18
+ @repository = repository
19
+ @valid_locales = valid_locales
20
+ @locale_pairs = locale_pairs
21
+ @linters = linters
22
+ end
23
+
24
+ def validate!
25
+ @errors = nil
26
+
27
+ if linters.include?(:file_scope)
28
+ repository.asts_by_path.each do |path, tree|
29
+ validator = FileScope.new(tree, filepath: path)
30
+ validator.validate!
31
+ validator.errors.each do |err|
32
+ errors[err.file][err.key] = err
33
+ end
34
+ end
35
+ end
36
+
37
+ if linters.include?(:symmetry)
38
+ repository.asts_by_scope.each do |scope, locale_trees|
39
+ locale_pairs.each do |(master, slave)|
40
+ master_tree = locale_trees[master]
41
+ slave_tree = locale_trees[slave]
42
+ next unless master_tree && slave_tree
43
+
44
+ validator = Symmetry.new(master_tree[master], slave_tree[slave])
45
+ validator.validate!
46
+ validator.errors.each do |err|
47
+ errors[err.file][err.key] = err
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def errors
55
+ @errors ||= Hash.new { |h, k| h[k] = {} }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,154 @@
1
+ require_relative 'errors'
2
+ require_relative '../util'
3
+
4
+ module I18nFlow::Validator
5
+ class Symmetry
6
+ attr_reader :ast_1
7
+ attr_reader :ast_2
8
+
9
+ def initialize(ast_1, ast_2)
10
+ @ast_1 = ast_1
11
+ @ast_2 = ast_2
12
+ end
13
+
14
+ def validate!
15
+ @errors = nil
16
+ validate_content(ast_1, ast_2)
17
+ end
18
+
19
+ def errors
20
+ @errors ||= []
21
+ end
22
+
23
+ private
24
+
25
+ def validate_content(t1, t2)
26
+ keys = t1.keys | t2.keys
27
+
28
+ keys.each do |k|
29
+ validate_node(t1, t2, k)
30
+ end
31
+ end
32
+
33
+ def validate_node(t1, t2, key)
34
+ n1 = t1[key]
35
+ n2 = t2[key]
36
+
37
+ check_only_tag(n1, n2)&.tap do |err|
38
+ errors << err if err
39
+ return
40
+ end
41
+
42
+ check_asymmetric_key(n1, n2, t2)&.tap do |err|
43
+ errors << err if err
44
+ return
45
+ end
46
+
47
+ check_type(n1, n2)&.tap do |err|
48
+ errors << err
49
+ return
50
+ end
51
+
52
+ check_todo_tag(n1, n2)&.tap do |err|
53
+ errors << err
54
+ return
55
+ end
56
+
57
+ if n1.scalar? || n1.alias?
58
+ check_args(n1, n2)&.tap do |err|
59
+ errors << err
60
+ end
61
+ else
62
+ validate_content(n1, n2)
63
+ end
64
+ end
65
+
66
+ def check_only_tag(n1, n2)
67
+ return unless n1&.marked_as_only? || n2&.marked_as_only?
68
+
69
+ if n1 && !n1.valid_locale?
70
+ return InvalidLocaleError.new(n1.full_key,
71
+ expect: n1.valid_locales,
72
+ actual: n1.locale,
73
+ ).set_location(n1)
74
+ end
75
+
76
+ if n2 && !n2.valid_locale?
77
+ return InvalidLocaleError.new(n2.full_key,
78
+ expect: n2.valid_locales,
79
+ actual: n2.locale,
80
+ ).set_location(n2)
81
+ end
82
+
83
+ if n1 && !n2 && n1.marked_as_only?
84
+ return false
85
+ end
86
+
87
+ if !n1 && n2 && n2.marked_as_only?
88
+ return false
89
+ end
90
+
91
+ if n1 && n2 && n1.valid_locales.any? && !n1.valid_locales.include?(n2.locale)
92
+ return InvalidLocaleError.new(n2.full_key,
93
+ expect: n1.valid_locales,
94
+ actual: n2.locale,
95
+ ).set_location(n2)
96
+ end
97
+
98
+ if n1 && n2 && n2.valid_locales.any? && !n2.valid_locales.include?(n1.locale)
99
+ return InvalidLocaleError.new(n1.full_key,
100
+ expect: n2.valid_locales,
101
+ actual: n1.locale,
102
+ ).set_location(n1)
103
+ end
104
+
105
+ false
106
+ end
107
+
108
+ def check_type(n1, n2)
109
+ return unless n1 && n2
110
+ return if n1.scalar? == n2.scalar?
111
+
112
+ InvalidTypeError.new(n2.full_key).set_location(n2)
113
+ end
114
+
115
+ def check_asymmetric_key(n1, n2, t2)
116
+ return false if n1&.ignored_violation == :key || n2&.ignored_violation == :key
117
+ return if n1 && n2
118
+
119
+ if n1
120
+ full_key = [t2.locale, *n1.scopes.drop(1)].join('.')
121
+ MissingKeyError.new(full_key).set_location(t2)
122
+ else
123
+ ExtraKeyError.new(n2.full_key).set_location(n2)
124
+ end
125
+ end
126
+
127
+ def check_todo_tag(n1, n2)
128
+ return unless n2.marked_as_todo?
129
+
130
+ if !n2.scalar?
131
+ InvalidTodoError.new(n2.full_key).set_location(n2)
132
+ elsif n2.value != n1.value
133
+ TodoContentError.new(n2.full_key,
134
+ expect: n1.value,
135
+ actual: n2.value,
136
+ ).set_location(n2)
137
+ end
138
+ end
139
+
140
+ def check_args(n1, n2)
141
+ return if n1.ignored_violation == :args || n2.ignored_violation == :args
142
+
143
+ args_1 = I18nFlow::Util.extract_args(n1.value).uniq
144
+ args_2 = I18nFlow::Util.extract_args(n2.value).uniq
145
+
146
+ return if args_1 == args_2
147
+
148
+ AsymmetricArgsError.new(n2.full_key,
149
+ expect: args_1,
150
+ actual: args_2,
151
+ ).set_location(n2)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,4 @@
1
+ module I18nFlow::Validator
2
+ end
3
+
4
+ require_relative 'validator/multiplexer'
@@ -0,0 +1,7 @@
1
+ module I18nFlow
2
+ MAJOR = 0
3
+ MINOR = 1
4
+ REVISION = 0
5
+
6
+ VERSION = [MAJOR, MINOR, REVISION].join('.')
7
+ end
@@ -0,0 +1,72 @@
1
+ require 'psych'
2
+ require_relative 'node'
3
+
4
+ module I18nFlow::YamlAstProxy
5
+ class Mapping < Node
6
+ extend Forwardable
7
+
8
+ def_delegators :indexed_object, :==, :keys, :size
9
+
10
+ def each
11
+ indexed_object.each do |k, _|
12
+ yield k, cache[k]
13
+ end
14
+ end
15
+
16
+ def values
17
+ indexed_object.map { |k, _| cache[k] }
18
+ end
19
+
20
+ def set(key, value)
21
+ super.tap do
22
+ cache.delete(key)
23
+ synchronize!
24
+ end
25
+ end
26
+ alias []= set
27
+
28
+ def batch
29
+ @locked = true
30
+ yield
31
+ ensure
32
+ @locked = false
33
+ synchronize!
34
+ end
35
+
36
+ def merge!(other)
37
+ return unless other&.is_a?(Mapping)
38
+
39
+ batch do
40
+ other.batch do
41
+ other.each do |k, rhs|
42
+ if (lhs = self[k])
43
+ lhs.merge!(rhs)
44
+ else
45
+ self[k] = rhs.node
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def cache
55
+ @cache ||= Hash.new { |h, k| h[k] = wrap(indexed_object[k], key: k) }
56
+ end
57
+
58
+ def indexed_object
59
+ @indexed_object ||= node.children
60
+ .each_slice(2)
61
+ .map { |k, v| [k.value, v] }
62
+ .to_h
63
+ end
64
+
65
+ def synchronize!
66
+ return if @locked
67
+
68
+ children = indexed_object.flat_map { |k, v| [Psych::Nodes::Scalar.new(k), v] }
69
+ node.children.replace(children)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,128 @@
1
+ require_relative 'node_meta_data'
2
+ require 'forwardable'
3
+
4
+ module I18nFlow::YamlAstProxy
5
+ class Node
6
+ extend Forwardable
7
+ include NodeMetaData
8
+
9
+ TAG_IGNORE = /^!ignore:(args|key)$/
10
+ TAG_TODO = /^!todo(?::([,a-zA-Z_-]+))?$/
11
+ TAG_ONLY = /^!only(?::([,a-zA-Z_-]+))?$/
12
+
13
+ attr_reader :node
14
+ attr_reader :parent
15
+ attr_reader :scopes
16
+ attr_reader :file_path
17
+ attr_reader :ignored_violation
18
+
19
+ def_delegators :indexed_object, :each
20
+
21
+ def initialize(
22
+ node,
23
+ parent: nil,
24
+ scopes: [],
25
+ file_path: nil
26
+ )
27
+ @node = node
28
+ @parent = parent
29
+ @scopes = scopes
30
+ @file_path = file_path
31
+
32
+ parse_tag!(node.tag)
33
+ end
34
+
35
+ def get(key)
36
+ wrap(indexed_object[key], key: key)
37
+ end
38
+ alias [] get
39
+
40
+ def set(key, value)
41
+ indexed_object[key] = value
42
+ end
43
+ alias []= set
44
+
45
+ def value
46
+ node.value if node.respond_to?(:value)
47
+ end
48
+
49
+ def merge!(other)
50
+ return unless other&.is_a?(Node)
51
+
52
+ if scalar? && other.scalar?
53
+ node.value = other.value
54
+ return
55
+ end
56
+
57
+ if !scalar? && !other.scalar?
58
+ batch do
59
+ other.batch do
60
+ other.each do |k, rhs|
61
+ if (lhs = self[k])
62
+ lhs.merge!(rhs)
63
+ else
64
+ self[k] = rhs.node
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ def batch
73
+ yield
74
+ end
75
+
76
+ def ==(other)
77
+ return false unless other.is_a?(self.class)
78
+ identity_data == other.identity_data
79
+ end
80
+
81
+ def to_yaml
82
+ parent.to_yaml(nil, line_width: -1)
83
+ end
84
+
85
+ def keys
86
+ return [] if scalar? || alias?
87
+ indexed_object.keys
88
+ end
89
+
90
+ private
91
+
92
+ def identity_data
93
+ (scalar? || alias?) ? value : indexed_object
94
+ end
95
+
96
+ def indexed_object
97
+ @indexed_object ||= I18nFlow::YamlAstProxy.create(node,
98
+ parent: parent,
99
+ scopes: scopes,
100
+ file_path: file_path,
101
+ )
102
+ end
103
+
104
+ def wrap(value, key:)
105
+ I18nFlow::YamlAstProxy.create(value,
106
+ parent: node,
107
+ scopes: [*scopes, key],
108
+ file_path: file_path,
109
+ )
110
+ end
111
+
112
+ def parse_tag!(tag)
113
+ return unless tag
114
+
115
+ case tag
116
+ when TAG_TODO
117
+ @tag = :todo
118
+ @todo_locales = $1.to_s.split(',').freeze
119
+ when TAG_ONLY
120
+ @tag = :only
121
+ @valid_locales = $1.to_s.split(',').freeze
122
+ when TAG_IGNORE
123
+ @tag = :ignore
124
+ @ignored_violation = $1.freeze.to_sym
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,86 @@
1
+ module I18nFlow::YamlAstProxy
2
+ module NodeMetaData
3
+ def num_lines
4
+ return 1 unless end_line
5
+ end_line - start_line + 1
6
+ end
7
+
8
+ def key
9
+ scopes.last
10
+ end
11
+
12
+ def locale
13
+ scopes.first
14
+ end
15
+
16
+ def full_key
17
+ scopes.join('.')
18
+ end
19
+
20
+ def start_line
21
+ node.start_line + line_correction
22
+ end
23
+
24
+ def end_line
25
+ node.end_line + line_correction
26
+ end
27
+
28
+ def start_column
29
+ node.start_column
30
+ end
31
+
32
+ def end_column
33
+ node.end_column
34
+ end
35
+
36
+ def anchor
37
+ node.anchor
38
+ end
39
+
40
+ def sequence?
41
+ is_a?(Sequence)
42
+ end
43
+
44
+ def mapping?
45
+ is_a?(Mapping)
46
+ end
47
+
48
+ def scalar?
49
+ node.is_a?(Psych::Nodes::Scalar)
50
+ end
51
+
52
+ def alias?
53
+ node.is_a?(Psych::Nodes::Alias)
54
+ end
55
+
56
+ def has_anchor?
57
+ !!anchor
58
+ end
59
+
60
+ def marked_as_todo?
61
+ @tag == :todo
62
+ end
63
+
64
+ def marked_as_only?
65
+ @tag == :only
66
+ end
67
+
68
+ def todo_locales
69
+ @todo_locales ||= []
70
+ end
71
+
72
+ def valid_locales
73
+ @valid_locales ||= []
74
+ end
75
+
76
+ def valid_locale?
77
+ valid_locales.empty? || valid_locales.include?(locale)
78
+ end
79
+
80
+ private
81
+
82
+ def line_correction
83
+ node.is_a?(Psych::Nodes::Scalar) ? 1 : 0
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'node'
2
+
3
+ module I18nFlow::YamlAstProxy
4
+ class Sequence < Node
5
+ def_delegators :indexed_object, :==, :<<, :size
6
+
7
+ def each
8
+ indexed_object.each.with_index do |o, i|
9
+ yield i, wrap(o, key: i)
10
+ end
11
+ end
12
+
13
+ def merge!(other)
14
+ return unless other&.is_a?(Sequence)
15
+
16
+ indexed_object.concat(other.send(:indexed_object))
17
+ end
18
+
19
+ def keys
20
+ (0...size).to_a
21
+ end
22
+
23
+ private
24
+
25
+ def indexed_object
26
+ node.children
27
+ end
28
+ end
29
+ end