i18n_flow 0.1.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 +7 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +45 -0
- data/LICENSE +22 -0
- data/README.md +103 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/rules.md +316 -0
- data/doc/tags.md +488 -0
- data/example/example.en.yml +14 -0
- data/example/example.ja.yml +9 -0
- data/exe/i18n_flow +11 -0
- data/i18n_flow.gemspec +28 -0
- data/i18n_flow.yml +8 -0
- data/lib/i18n_flow/cli/color.rb +18 -0
- data/lib/i18n_flow/cli/command_base.rb +33 -0
- data/lib/i18n_flow/cli/copy_command.rb +69 -0
- data/lib/i18n_flow/cli/help_command.rb +29 -0
- data/lib/i18n_flow/cli/lint_command/ascii.erb +45 -0
- data/lib/i18n_flow/cli/lint_command/ascii_renderer.rb +58 -0
- data/lib/i18n_flow/cli/lint_command/markdown.erb +49 -0
- data/lib/i18n_flow/cli/lint_command/markdown_renderer.rb +55 -0
- data/lib/i18n_flow/cli/lint_command.rb +55 -0
- data/lib/i18n_flow/cli/read_config_command.rb +20 -0
- data/lib/i18n_flow/cli/search_command/default.erb +11 -0
- data/lib/i18n_flow/cli/search_command/default_renderer.rb +67 -0
- data/lib/i18n_flow/cli/search_command/oneline.erb +5 -0
- data/lib/i18n_flow/cli/search_command/oneline_renderer.rb +39 -0
- data/lib/i18n_flow/cli/search_command.rb +59 -0
- data/lib/i18n_flow/cli/split_command.rb +20 -0
- data/lib/i18n_flow/cli/version_command.rb +9 -0
- data/lib/i18n_flow/cli.rb +42 -0
- data/lib/i18n_flow/configuration.rb +205 -0
- data/lib/i18n_flow/parser.rb +34 -0
- data/lib/i18n_flow/repository.rb +39 -0
- data/lib/i18n_flow/search.rb +176 -0
- data/lib/i18n_flow/splitter/merger.rb +60 -0
- data/lib/i18n_flow/splitter/strategy.rb +66 -0
- data/lib/i18n_flow/splitter.rb +5 -0
- data/lib/i18n_flow/util.rb +57 -0
- data/lib/i18n_flow/validator/errors.rb +99 -0
- data/lib/i18n_flow/validator/file_scope.rb +58 -0
- data/lib/i18n_flow/validator/multiplexer.rb +58 -0
- data/lib/i18n_flow/validator/symmetry.rb +154 -0
- data/lib/i18n_flow/validator.rb +4 -0
- data/lib/i18n_flow/version.rb +7 -0
- data/lib/i18n_flow/yaml_ast_proxy/mapping.rb +72 -0
- data/lib/i18n_flow/yaml_ast_proxy/node.rb +128 -0
- data/lib/i18n_flow/yaml_ast_proxy/node_meta_data.rb +86 -0
- data/lib/i18n_flow/yaml_ast_proxy/sequence.rb +29 -0
- data/lib/i18n_flow/yaml_ast_proxy.rb +57 -0
- data/lib/i18n_flow.rb +15 -0
- data/spec/lib/i18n_flow/cli/command_base_spec.rb +46 -0
- data/spec/lib/i18n_flow/cli/help_command_spec.rb +13 -0
- data/spec/lib/i18n_flow/cli/version_command_spec.rb +13 -0
- data/spec/lib/i18n_flow/configuration_spec.rb +334 -0
- data/spec/lib/i18n_flow/repository_spec.rb +40 -0
- data/spec/lib/i18n_flow/splitter/merger_spec.rb +149 -0
- data/spec/lib/i18n_flow/util_spec.rb +194 -0
- data/spec/lib/i18n_flow/validator/file_scope_spec.rb +74 -0
- data/spec/lib/i18n_flow/validator/multiplexer_spec.rb +68 -0
- data/spec/lib/i18n_flow/validator/symmetry_spec.rb +511 -0
- data/spec/lib/i18n_flow/yaml_ast_proxy/node_spec.rb +151 -0
- data/spec/lib/i18n_flow_spec.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/repository_examples.rb +60 -0
- data/spec/support/util_macro.rb +14 -0
- metadata +214 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'pathname'
|
3
|
+
require_relative 'util'
|
4
|
+
|
5
|
+
class I18nFlow::Configuration
|
6
|
+
module Validation
|
7
|
+
def self.included(base)
|
8
|
+
base.include(InstanceMethods)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def _validators
|
14
|
+
@_validators ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(attr, message, &block)
|
18
|
+
_validators << [attr, message, block]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def validate!
|
24
|
+
self.class._validators.each do |attr, message, block|
|
25
|
+
next if instance_eval(&block)
|
26
|
+
raise ValueError.new(attr, message)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class NoConfigurationFileFoundError < ::IOError
|
33
|
+
def message
|
34
|
+
'no configuration file found'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class ValueError < ::StandardError
|
39
|
+
def initialize(attr, message)
|
40
|
+
@attr = attr
|
41
|
+
@message = message
|
42
|
+
end
|
43
|
+
|
44
|
+
def message
|
45
|
+
'%s: %s' % [@attr, @message]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class I18nFlow::Configuration
|
51
|
+
include Validation
|
52
|
+
|
53
|
+
CONFIG_FILES = [
|
54
|
+
'i18n_flow.yml',
|
55
|
+
'i18n_flow.yaml',
|
56
|
+
].freeze
|
57
|
+
|
58
|
+
LINTERS = %i[
|
59
|
+
file_scope
|
60
|
+
symmetry
|
61
|
+
].freeze
|
62
|
+
|
63
|
+
attr_reader(*%i[
|
64
|
+
base_path
|
65
|
+
glob_patterns
|
66
|
+
valid_locales
|
67
|
+
locale_pairs
|
68
|
+
linters
|
69
|
+
split_max_level
|
70
|
+
split_line_threshold
|
71
|
+
])
|
72
|
+
|
73
|
+
validate :base_path, 'need to be an absolute path' do
|
74
|
+
!!base_path&.absolute?
|
75
|
+
end
|
76
|
+
|
77
|
+
validate :glob_patterns, 'should be an array' do
|
78
|
+
!glob_patterns.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
validate :glob_patterns, 'should contain at least one pattern' do
|
82
|
+
glob_patterns.any?
|
83
|
+
end
|
84
|
+
|
85
|
+
validate :valid_locales, 'should be an array' do
|
86
|
+
!valid_locales.nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
validate :valid_locales, 'should contain at least one pattern' do
|
90
|
+
valid_locales.any?
|
91
|
+
end
|
92
|
+
|
93
|
+
validate :locale_pairs, 'should be an array' do
|
94
|
+
!locale_pairs.nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
validate :linters, "should be an array" do
|
98
|
+
!linters.nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
validate :linters, "should contain any of [#{LINTERS.join(', ')}]" do
|
102
|
+
(linters - LINTERS).empty?
|
103
|
+
end
|
104
|
+
|
105
|
+
validate :split_max_level, 'must be set' do
|
106
|
+
!split_max_level.nil?
|
107
|
+
end
|
108
|
+
|
109
|
+
validate :split_line_threshold, 'must be set' do
|
110
|
+
!split_line_threshold.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
def initialize
|
114
|
+
update(validate: false) do |c|
|
115
|
+
c.base_path = File.expand_path('.')
|
116
|
+
c.glob_patterns = ['*.en.yml']
|
117
|
+
c.valid_locales = %w[en]
|
118
|
+
c.locale_pairs = []
|
119
|
+
c.linters = LINTERS
|
120
|
+
c.split_max_level = 3
|
121
|
+
c.split_line_threshold = 50
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def base_path=(path)
|
126
|
+
@base_path = path&.tap do |v|
|
127
|
+
break Pathname.new(v)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def glob_patterns=(patterns)
|
132
|
+
@glob_patterns = patterns&.tap do |v|
|
133
|
+
break unless v.is_a?(Array)
|
134
|
+
break v.map(&:to_s)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def locale_pairs=(pairs)
|
139
|
+
@locale_pairs = pairs&.tap do |v|
|
140
|
+
break unless v.is_a?(Array)
|
141
|
+
break unless v.all? { |e| e.size == 2 }
|
142
|
+
break v.map { |e| e.map(&:to_s) }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def valid_locales=(locales)
|
147
|
+
@valid_locales = locales&.tap do |v|
|
148
|
+
break unless v.is_a?(Array)
|
149
|
+
break v.map(&:to_s)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def linters=(linters)
|
154
|
+
@linters = linters&.tap do |v|
|
155
|
+
break unless v.is_a?(Array)
|
156
|
+
break v.map(&:to_sym)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def split_max_level=(level)
|
161
|
+
@split_max_level = level&.tap do |v|
|
162
|
+
break unless v.is_a?(Integer)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def split_line_threshold=(threshold)
|
167
|
+
@split_line_threshold = threshold&.tap do |v|
|
168
|
+
break unless v.is_a?(Integer)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def update(validate: true)
|
173
|
+
yield self if block_given?
|
174
|
+
validate! if validate
|
175
|
+
end
|
176
|
+
|
177
|
+
def auto_configure!
|
178
|
+
load_from_file!
|
179
|
+
update
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def load_from_file!
|
185
|
+
config_file = I18nFlow::Util.find_file_upward(*CONFIG_FILES)
|
186
|
+
|
187
|
+
unless config_file
|
188
|
+
raise NoConfigurationFileFoundError
|
189
|
+
end
|
190
|
+
|
191
|
+
yaml = YAML.load_file(config_file)
|
192
|
+
yaml_dir = File.dirname(config_file)
|
193
|
+
|
194
|
+
_base_path = yaml.delete('base_path')
|
195
|
+
self.base_path = _base_path ? File.absolute_path(_base_path, yaml_dir) : yaml_dir
|
196
|
+
|
197
|
+
yaml.each do |k, v|
|
198
|
+
if respond_to?("#{k}=")
|
199
|
+
send("#{k}=", v)
|
200
|
+
else
|
201
|
+
raise KeyError.new('invalid option: %s' % [k])
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'psych'
|
2
|
+
require_relative 'yaml_ast_proxy'
|
3
|
+
|
4
|
+
class I18nFlow::Parser
|
5
|
+
attr_reader :buffer
|
6
|
+
attr_reader :file_path
|
7
|
+
|
8
|
+
def initialize(buffer, file_path: nil)
|
9
|
+
@buffer = buffer
|
10
|
+
@file_path = file_path
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse!
|
14
|
+
parser.parse(buffer)
|
15
|
+
end
|
16
|
+
|
17
|
+
def root
|
18
|
+
builder.root
|
19
|
+
end
|
20
|
+
|
21
|
+
def root_proxy
|
22
|
+
@root_proxy ||= I18nFlow::YamlAstProxy.create(root, file_path: file_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def builder
|
28
|
+
@builder ||= Psych::TreeBuilder.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def parser
|
32
|
+
@parser ||= Psych::Parser.new(builder)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require_relative 'parser'
|
3
|
+
require_relative 'util'
|
4
|
+
|
5
|
+
class I18nFlow::Repository
|
6
|
+
def initialize(
|
7
|
+
base_path:,
|
8
|
+
glob_patterns:
|
9
|
+
)
|
10
|
+
@base_path = Pathname.new(base_path)
|
11
|
+
@glob_patterns = glob_patterns.to_a
|
12
|
+
end
|
13
|
+
|
14
|
+
def file_paths
|
15
|
+
@file_paths ||= @glob_patterns
|
16
|
+
.flat_map { |pattern| Dir.glob(@base_path.join(pattern)) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def asts_by_path
|
20
|
+
@asts ||= file_paths
|
21
|
+
.map { |path|
|
22
|
+
rel_path = Pathname.new(path).relative_path_from(@base_path).to_s
|
23
|
+
parser = I18nFlow::Parser.new(File.read(path), file_path: rel_path)
|
24
|
+
parser.parse!
|
25
|
+
[rel_path, parser.root_proxy]
|
26
|
+
}
|
27
|
+
.to_h
|
28
|
+
end
|
29
|
+
|
30
|
+
def asts_by_scope
|
31
|
+
@asts_by_scope ||= Hash.new { |h, k| h[k] = {} }
|
32
|
+
.tap { |h|
|
33
|
+
asts_by_path.each { |path, tree|
|
34
|
+
locale, *scopes = I18nFlow::Util.filepath_to_scope(path)
|
35
|
+
h[scopes.join('.')][locale] = tree
|
36
|
+
}
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
class I18nFlow::Search
|
2
|
+
class Item
|
3
|
+
attr_reader :locale
|
4
|
+
attr_reader :file
|
5
|
+
attr_reader :line
|
6
|
+
attr_reader :column
|
7
|
+
attr_reader :value
|
8
|
+
attr_reader :score
|
9
|
+
|
10
|
+
def initialize(
|
11
|
+
locale:,
|
12
|
+
file:,
|
13
|
+
line:,
|
14
|
+
column:,
|
15
|
+
value:,
|
16
|
+
score:
|
17
|
+
)
|
18
|
+
@locale = locale
|
19
|
+
@file = file
|
20
|
+
@line = line
|
21
|
+
@column = column
|
22
|
+
@value = value
|
23
|
+
@score = score
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
class I18nFlow::Search
|
30
|
+
attr_reader :repository
|
31
|
+
attr_reader :pattern
|
32
|
+
|
33
|
+
SCORE_KEY_CS_MATCH = 10
|
34
|
+
SCORE_KEY_CI_MATCH = 9
|
35
|
+
SCORE_CONTENT_CS_MATCH = 8
|
36
|
+
SCORE_CONTENT_CI_MATCH = 6
|
37
|
+
|
38
|
+
def initialize(
|
39
|
+
repository:,
|
40
|
+
pattern:,
|
41
|
+
include_all: false
|
42
|
+
)
|
43
|
+
@repository = repository
|
44
|
+
@pattern = pattern
|
45
|
+
@include_all = include_all
|
46
|
+
end
|
47
|
+
|
48
|
+
def search!
|
49
|
+
repository.asts_by_scope.each do |scope, locale_trees|
|
50
|
+
asts = locale_trees
|
51
|
+
.map { |locale, tree| tree[locale] }
|
52
|
+
.compact
|
53
|
+
|
54
|
+
search_on(asts)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def results
|
59
|
+
@results ||= indexed_results
|
60
|
+
.sort_by { |k, rs| -rs.map(&:score).max }
|
61
|
+
.to_h
|
62
|
+
end
|
63
|
+
|
64
|
+
def include_all?
|
65
|
+
!!@include_all
|
66
|
+
end
|
67
|
+
|
68
|
+
def indexed_results
|
69
|
+
@indexed_results ||= Hash.new { |h, k| h[k] = [] }
|
70
|
+
end
|
71
|
+
|
72
|
+
def pattern_downcase
|
73
|
+
@pattern_downcase ||= pattern.downcase
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def search_on(asts)
|
79
|
+
if include_all?
|
80
|
+
score = asts.map { |a| score_for(a) }.max
|
81
|
+
if score > 0
|
82
|
+
asts.each do |ast|
|
83
|
+
add_result(ast, score: score)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
else
|
87
|
+
asts.each do |ast|
|
88
|
+
score = score_for(ast)
|
89
|
+
next unless score > 0
|
90
|
+
add_result(ast, score: score)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
recursive_asts = asts.reject { |a| a.scalar? || a.alias? }
|
95
|
+
keys = recursive_asts.flat_map(&:keys).uniq
|
96
|
+
|
97
|
+
keys.each do |k|
|
98
|
+
search_on(recursive_asts.map { |a| a[k] }.compact)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_result(node, score:)
|
103
|
+
locale, *scopes = node.scopes
|
104
|
+
key = scopes.join('.')
|
105
|
+
|
106
|
+
indexed_results[key] << Item.new(
|
107
|
+
locale: locale,
|
108
|
+
file: node.file_path,
|
109
|
+
line: node.start_line,
|
110
|
+
column: node.start_column,
|
111
|
+
value: node.value,
|
112
|
+
score: score,
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
def score_for(node)
|
117
|
+
key = node.scopes[1..-1].join('.')
|
118
|
+
|
119
|
+
key_match_score(key).tap do |score|
|
120
|
+
return score if score > 0
|
121
|
+
end
|
122
|
+
|
123
|
+
if node.scalar?
|
124
|
+
return content_match_score(node.value)
|
125
|
+
end
|
126
|
+
|
127
|
+
0
|
128
|
+
end
|
129
|
+
|
130
|
+
def key_match_score(key)
|
131
|
+
if key == pattern
|
132
|
+
return SCORE_KEY_CS_MATCH
|
133
|
+
end
|
134
|
+
if key.downcase == pattern_downcase
|
135
|
+
return SCORE_KEY_CI_MATCH
|
136
|
+
end
|
137
|
+
|
138
|
+
0
|
139
|
+
end
|
140
|
+
|
141
|
+
def content_match_score(str)
|
142
|
+
if str.include?(pattern)
|
143
|
+
return SCORE_CONTENT_CS_MATCH * lcs_score(str, pattern)
|
144
|
+
end
|
145
|
+
|
146
|
+
str = str.downcase
|
147
|
+
|
148
|
+
if str.include?(pattern_downcase)
|
149
|
+
return SCORE_CONTENT_CI_MATCH * lcs_score(str, pattern_downcase)
|
150
|
+
end
|
151
|
+
|
152
|
+
0
|
153
|
+
end
|
154
|
+
|
155
|
+
def lcs_score(a, b)
|
156
|
+
a_size = a.size
|
157
|
+
b_size = b.size
|
158
|
+
|
159
|
+
m = Array.new(a_size) { 0 }
|
160
|
+
|
161
|
+
result = 0
|
162
|
+
|
163
|
+
b_size.times do |i|
|
164
|
+
result = d = 0
|
165
|
+
|
166
|
+
a_size.times do |j|
|
167
|
+
t = (b[i] == a[j]) ? d + 1 : d
|
168
|
+
d = m[j]
|
169
|
+
|
170
|
+
m[j] = result = [d, t, result].max
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
result * 2.0 / (a_size + b_size)
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module I18nFlow::Splitter
|
2
|
+
class Merger
|
3
|
+
attr_reader :chunks
|
4
|
+
|
5
|
+
def initialize(chunks)
|
6
|
+
@chunks = chunks
|
7
|
+
end
|
8
|
+
|
9
|
+
def root
|
10
|
+
@root ||= I18nFlow::YamlAstProxy.new_root
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform_merge!
|
14
|
+
chunks.each do |chunk|
|
15
|
+
append_chunk(chunk)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_yaml
|
20
|
+
root.parent.to_yaml
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def append_chunk(chunk)
|
26
|
+
parent = root
|
27
|
+
|
28
|
+
chunk.scopes[0..(chunk.scalar? ? -2 : -1)]
|
29
|
+
.each
|
30
|
+
.with_index do |scope, i|
|
31
|
+
next_scope = chunk.scopes[i + 1]
|
32
|
+
is_seq = next_scope ? next_scope&.is_a?(Integer) : chunk.sequence?
|
33
|
+
|
34
|
+
node = parent[scope]
|
35
|
+
|
36
|
+
if node && (!is_seq && !node.mapping? || is_seq && !node.sequence?)
|
37
|
+
# TODO: should raise?
|
38
|
+
return
|
39
|
+
end
|
40
|
+
|
41
|
+
unless node
|
42
|
+
parent[scope] = if is_seq
|
43
|
+
Psych::Nodes::Sequence.new
|
44
|
+
else
|
45
|
+
Psych::Nodes::Mapping.new
|
46
|
+
end
|
47
|
+
node = parent[scope]
|
48
|
+
end
|
49
|
+
|
50
|
+
parent = node
|
51
|
+
end
|
52
|
+
|
53
|
+
if chunk.scalar?
|
54
|
+
parent[chunk.scopes[-1]] = chunk.node
|
55
|
+
else
|
56
|
+
parent.merge!(chunk)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module I18nFlow::Splitter
|
2
|
+
class Strategy
|
3
|
+
DEFAULT_MAX_LEVEL = 3
|
4
|
+
DEFAULT_LINE_THRESHOLD = 50
|
5
|
+
|
6
|
+
def initialize(
|
7
|
+
ast,
|
8
|
+
max_level: DEFAULT_MAX_LEVEL,
|
9
|
+
line_threshold: DEFAULT_LINE_THRESHOLD
|
10
|
+
)
|
11
|
+
@ast = ast
|
12
|
+
@max_level = max_level
|
13
|
+
@line_threshold = line_threshold
|
14
|
+
end
|
15
|
+
|
16
|
+
def split!
|
17
|
+
@chunks = nil
|
18
|
+
traverse(@ast, level: 0)
|
19
|
+
end
|
20
|
+
|
21
|
+
def chunks
|
22
|
+
@chunks ||= Hash.new { |h, k| h[k] = [] }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def traverse(node, level:)
|
28
|
+
return if node.scalar?
|
29
|
+
|
30
|
+
if level > 0 && node.mapping?
|
31
|
+
others, mappings = node.values.partition(&:scalar?)
|
32
|
+
|
33
|
+
others.each do |n|
|
34
|
+
add_chunk(n, delta_level: -2)
|
35
|
+
end
|
36
|
+
|
37
|
+
if mappings.sum(&:num_lines) < @line_threshold
|
38
|
+
mappings.each do |n|
|
39
|
+
add_chunk(n, delta_level: -2)
|
40
|
+
end
|
41
|
+
return
|
42
|
+
end
|
43
|
+
|
44
|
+
if level >= @max_level
|
45
|
+
add_chunk(node)
|
46
|
+
return
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
if node.sequence?
|
51
|
+
add_chunk(node, delta_level: -2)
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
node.each do |_, v|
|
56
|
+
traverse(v, level: level + 1)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_chunk(node, delta_level: 0)
|
61
|
+
level = [node.scopes.size + delta_level, 1].max
|
62
|
+
file_scopes = node.scopes[0...level]
|
63
|
+
chunks[file_scopes] << node
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module I18nFlow::Util
|
2
|
+
extend self
|
3
|
+
|
4
|
+
def extract_args(text)
|
5
|
+
text.to_s
|
6
|
+
.gsub(/%%/, '')
|
7
|
+
.scan(/%\{([^\}]+)\}/)
|
8
|
+
.flatten
|
9
|
+
.sort
|
10
|
+
end
|
11
|
+
|
12
|
+
def filepath_to_scope(filepath)
|
13
|
+
*scopes, filename = filepath.split('/')
|
14
|
+
*basename, locale, _ = filename.split('.')
|
15
|
+
|
16
|
+
([locale] + scopes + basename).compact.reject(&:empty?)
|
17
|
+
end
|
18
|
+
|
19
|
+
def scope_to_filepath(scopes)
|
20
|
+
locale, *components = scopes
|
21
|
+
[components.join('/'), locale, 'yml'].compact.reject(&:empty?).join('.')
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_file_upward(*file_names)
|
25
|
+
pwd = Dir.pwd
|
26
|
+
base = Hash.new { |h, k| h[k] = pwd }
|
27
|
+
file = {}
|
28
|
+
|
29
|
+
while base.values.all? { |b| '.' != b && '/' != b }
|
30
|
+
file_names.each do |name|
|
31
|
+
file[name] = File.join(base[name], name)
|
32
|
+
base[name] = File.dirname(base[name])
|
33
|
+
|
34
|
+
return file[name] if File.exists?(file[name])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_options(args)
|
42
|
+
options = {}
|
43
|
+
|
44
|
+
args.reject! do |arg|
|
45
|
+
case arg
|
46
|
+
when /^-([a-z0-9])$/i, /^--([a-z0-9][a-z0-9-]*)$/i
|
47
|
+
options[$1] = true
|
48
|
+
when /^--([a-z0-9][a-z0-9-]*)=(.+)$/i
|
49
|
+
options[$1] = $2
|
50
|
+
else
|
51
|
+
break
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
options
|
56
|
+
end
|
57
|
+
end
|