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,45 @@
1
+ <%- errors.each do |file, errs| -%>
2
+ === <%= file %>
3
+ <%- errs.each do |full_key, err| -%>
4
+ <%= full_key %> #<%= err.line %>
5
+ <%- case err when nil -%>
6
+ <%- when I18nFlow::Validator::InvalidTypeError -%>
7
+ <%- if err.single? -%>
8
+ A file must start with scopes that derive from its file path
9
+ reason: it must not have a scalar value
10
+ <%- else -%>
11
+ Structure mismatches with the master file
12
+ <%- end -%>
13
+ <%- when I18nFlow::Validator::MissingKeyError -%>
14
+ <%- if err.single? -%>
15
+ A file must start with scopes that derive from its file path
16
+ reason: missing key
17
+ <%- else -%>
18
+ The key is missing
19
+ <%- end -%>
20
+ <%- when I18nFlow::Validator::ExtraKeyError -%>
21
+ <%- if err.single? -%>
22
+ A file must start with scopes that derive from its file path
23
+ reason: extra key
24
+ <%- else -%>
25
+ An extra key found
26
+ <%- end -%>
27
+ <%- when I18nFlow::Validator::InvalidTodoError -%>
28
+ Todo cannot be annotated on a mapping/sequence
29
+ <%- when I18nFlow::Validator::TodoContentError -%>
30
+ It has "!todo" but the content diverges from the master file
31
+ master: <%= err.expect %>
32
+ foreign: <%= err.actual %>
33
+ <%- when I18nFlow::Validator::InvalidLocaleError -%>
34
+ It has "!only" but the locale is invalid
35
+ valid: [<%= err.expect.join(', ') %>]
36
+ got: <%= err.actual %>
37
+ <%- when I18nFlow::Validator::AsymmetricArgsError -%>
38
+ Interpolation arguments diverge from the master file
39
+ master: [<%= err.expect.join(', ') %>]
40
+ foreign: [<%= err.actual.join(', ') %>]
41
+ <%- end -%>
42
+ <%- end -%>
43
+
44
+ <%- end -%>
45
+ <%= summary_line %>
@@ -0,0 +1,58 @@
1
+ require 'erb'
2
+ require_relative '../../validator/errors'
3
+ require_relative '../color'
4
+
5
+ class I18nFlow::CLI::LintCommand
6
+ class AsciiRenderer
7
+ include I18nFlow::CLI::Color
8
+
9
+ FILE = __dir__ + '/ascii.erb'
10
+
11
+ attr_reader :errors
12
+
13
+ def initialize(errors, color: true)
14
+ @errors = errors
15
+ @color_enabled = !!color
16
+ end
17
+
18
+ def render
19
+ with_color(erb.result(binding))
20
+ end
21
+
22
+ def color_enabled?
23
+ @color_enabled
24
+ end
25
+
26
+ def file_count
27
+ @file_count ||= errors.size
28
+ end
29
+
30
+ def error_count
31
+ @error_count ||= errors.sum { |_, errs| errs.size }
32
+ end
33
+
34
+ def summary_line
35
+ @summary_line ||= 'Found %d %s in %d %s' % [
36
+ error_count,
37
+ error_count == 1 ? 'violation' : 'violations',
38
+ file_count,
39
+ file_count == 1 ? 'file' : 'files',
40
+ ]
41
+ end
42
+
43
+ private
44
+
45
+ def with_color(str)
46
+ return str unless color_enabled?
47
+
48
+ str = str.gsub(/^(=== )(.+)$/) { $1 + color($2, :yellow) }
49
+ str = str.gsub(/(#\d+)$/) { color($1, :yellow) }
50
+ str = str.gsub(/^([ ]{8})(.+)$/) { $1 + color($2, :red) }
51
+ str
52
+ end
53
+
54
+ def erb
55
+ @erb ||= ERB.new(File.read(FILE), 0, '-')
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ [I18nFlow] <%= summary_line %>
2
+ [→ Lint rules and how to fix](https://github.com/creasty/i18n_flow/blob/master/doc/rules.md)
3
+
4
+ <%- errors.each do |file, errs| -%>
5
+ ## [<%= file %>](@<%= file %>)
6
+ <%- errs.each do |full_key, err| -%>
7
+
8
+ ### <%= full_key %> [L<%= err.line %>](@<%= err.file %>:<%= err.line %>)
9
+
10
+ <%- case err when nil -%>
11
+ <%- when I18nFlow::Validator::InvalidTypeError -%>
12
+ <%- if err.single? -%>
13
+ A file must start with scopes that derive from its file path
14
+ reason: it must not have a scalar value
15
+ <%- else -%>
16
+ Structure mismatches with the master file
17
+ <%- end -%>
18
+ <%- when I18nFlow::Validator::MissingKeyError -%>
19
+ <%- if err.single? -%>
20
+ A file must start with scopes that derive from its file path
21
+ reason: missing key
22
+ <%- else -%>
23
+ The key is missing
24
+ <%- end -%>
25
+ <%- when I18nFlow::Validator::ExtraKeyError -%>
26
+ <%- if err.single? -%>
27
+ A file must start with scopes that derive from its file path
28
+ reason: extra key
29
+ <%- else -%>
30
+ An extra key found
31
+ <%- end -%>
32
+ <%- when I18nFlow::Validator::InvalidTodoError -%>
33
+ Todo cannot be annotated on a mapping/sequence
34
+ <%- when I18nFlow::Validator::TodoContentError -%>
35
+ It has "!todo" but the content diverges from the master file
36
+ master: `<%= err.expect %>`
37
+ foreign: `<%= err.actual %>`
38
+ <%- when I18nFlow::Validator::InvalidLocaleError -%>
39
+ It has "!only" but the locale is invalid
40
+ valid: `[<%= err.expect.join(', ') %>]`
41
+ got: `<%= err.actual %>`
42
+ <%- when I18nFlow::Validator::AsymmetricArgsError -%>
43
+ Interpolation arguments diverge from the master file
44
+ master: `[<%= err.expect.join(', ') %>]`
45
+ foreign: `[<%= err.actual.join(', ') %>]`
46
+ <%- end -%>
47
+ <%- end -%>
48
+
49
+ <%- end -%>
@@ -0,0 +1,55 @@
1
+ require 'erb'
2
+ require_relative '../../validator/errors'
3
+
4
+ class I18nFlow::CLI::LintCommand
5
+ class MarkdownRenderer
6
+ FILE = __dir__ + '/markdown.erb'
7
+
8
+ attr_reader :errors
9
+ attr_reader :url_formatter
10
+
11
+ def initialize(errors, url_formatter:)
12
+ @errors = errors
13
+ @url_formatter = url_formatter
14
+ end
15
+
16
+ def render
17
+ with_link(erb.result(binding))
18
+ end
19
+
20
+ def file_count
21
+ @file_count ||= errors.size
22
+ end
23
+
24
+ def error_count
25
+ @error_count ||= errors.sum { |_, errs| errs.size }
26
+ end
27
+
28
+ def summary_line
29
+ @summary_line ||= 'Found %d %s in %d %s' % [
30
+ error_count,
31
+ error_count == 1 ? 'violation' : 'violations',
32
+ file_count,
33
+ file_count == 1 ? 'file' : 'files',
34
+ ]
35
+ end
36
+
37
+ private
38
+
39
+ def with_link(str)
40
+ str.gsub(/\[([^\]]+)\]\(@([^\):]+)(?::(\d+))?\)/) do
41
+ '[%s](%s)' % [$1, format_link(path: $2, line: $3)]
42
+ end
43
+ end
44
+
45
+ def format_link(path:, line:)
46
+ url_formatter
47
+ .gsub(/%f\b/, path)
48
+ .gsub(/%l\b/, line.to_s)
49
+ end
50
+
51
+ def erb
52
+ @erb ||= ERB.new(File.read(FILE), 0, '-')
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'command_base'
2
+ require_relative '../repository'
3
+ require_relative '../validator/multiplexer'
4
+
5
+ class I18nFlow::CLI
6
+ class LintCommand < CommandBase
7
+ require_relative 'lint_command/ascii_renderer'
8
+ require_relative 'lint_command/markdown_renderer'
9
+
10
+ DEFAULT_FORMAT = 'ascii'
11
+
12
+ def invoke!
13
+ validator.validate!
14
+
15
+ case output_format
16
+ when 'ascii'
17
+ puts AsciiRenderer.new(validator.errors, color: color_enabled?).render
18
+ when 'markdown'
19
+ puts MarkdownRenderer.new(validator.errors, url_formatter: url_formatter).render
20
+ else
21
+ exit_with_message(1, 'Unsupported format: %s' % [output_format])
22
+ end
23
+
24
+ exit validator.errors.size.zero? ? 0 : 1
25
+ end
26
+
27
+ def output_format
28
+ @output_format ||= options['format'] || DEFAULT_FORMAT
29
+ end
30
+
31
+ def url_formatter
32
+ return @url_formatter if @url_formatter
33
+ @url_formatter = options['url-formatter']
34
+ @url_formatter ||= "file://#{I18nFlow.config.base_path}/%f#%l"
35
+ end
36
+
37
+ private
38
+
39
+ def repository
40
+ @repository ||= I18nFlow::Repository.new(
41
+ base_path: I18nFlow.config.base_path,
42
+ glob_patterns: I18nFlow.config.glob_patterns,
43
+ )
44
+ end
45
+
46
+ def validator
47
+ @validator ||= I18nFlow::Validator::Multiplexer.new(
48
+ repository: repository,
49
+ valid_locales: I18nFlow.config.valid_locales,
50
+ locale_pairs: I18nFlow.config.locale_pairs,
51
+ linters: I18nFlow.config.linters,
52
+ )
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'command_base'
2
+
3
+ class I18nFlow::CLI
4
+ class ReadConfigCommand < CommandBase
5
+ def invoke!
6
+ unless key
7
+ exit_with_message(1, 'usage: i18n_flow read_config KEY')
8
+ end
9
+
10
+ case key
11
+ when 'base_path'
12
+ puts I18nFlow.config.base_path
13
+ end
14
+ end
15
+
16
+ def key
17
+ args[0]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ <%- results.each do |key, matches| -%>
2
+ === <%= key %>
3
+ <%- matches.each do |m| -%>
4
+ <%= m.locale %> (<%= m.file %>:<%= m.line %>)
5
+ <%- if m.value -%>
6
+ <%= m.value.gsub(/^/, ' ') %>
7
+ <%- end -%>
8
+ <%- end -%>
9
+
10
+ <%- end -%>
11
+ <%= results.size %> <%= results.size == 1 ? 'hit' : 'hits' %>
@@ -0,0 +1,67 @@
1
+ require 'erb'
2
+ require_relative '../../validator/errors'
3
+ require_relative '../color'
4
+
5
+ class I18nFlow::CLI::SearchCommand
6
+ class DefaultRenderer
7
+ include I18nFlow::CLI::Color
8
+
9
+ FILE = __dir__ + '/default.erb'
10
+
11
+ attr_reader :results
12
+
13
+ def initialize(results, color: true)
14
+ @results = results
15
+ @color_enabled = !!color
16
+ end
17
+
18
+ def render
19
+ with_color(erb.result(binding))
20
+ end
21
+
22
+ def color_enabled?
23
+ @color_enabled
24
+ end
25
+
26
+ private
27
+
28
+ def with_color(str)
29
+ return str unless color_enabled?
30
+
31
+ state = nil
32
+
33
+ str.each_line.map do |l|
34
+ case l
35
+ when /^(=== )(.+)$/
36
+ state = :header if state == nil
37
+ when /^( )([^\s]+)( \(.+:\d+\))$/
38
+ state = :location if %i[header content].include?(state)
39
+ when /^( )(.+)/
40
+ state = :content if %i[location content].include?(state)
41
+ else
42
+ state = nil
43
+ end
44
+
45
+ case state
46
+ when :header
47
+ l = $~[1]
48
+ l << color($~[2], :yellow)
49
+ l << "\n"
50
+ when :location
51
+ l = $~[1]
52
+ l << color($~[2], :blue)
53
+ l << $~[3]
54
+ l << "\n"
55
+ when :content
56
+ l = color(l, :green)
57
+ end
58
+
59
+ l
60
+ end.join
61
+ end
62
+
63
+ def erb
64
+ @erb ||= ERB.new(File.read(FILE), 0, '-')
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,5 @@
1
+ <%- results.each do |key, matches| -%>
2
+ <%- matches.each do |m| -%>
3
+ <%= key %> [<%= m.locale %>] (<%= m.file %>:<%= m.line %>:<%= m.column %>) <%= m.value&.gsub(/\s+/, ' ')&.strip %>
4
+ <%- end -%>
5
+ <%- end -%>
@@ -0,0 +1,39 @@
1
+ require 'erb'
2
+ require_relative '../../validator/errors'
3
+ require_relative '../color'
4
+
5
+ class I18nFlow::CLI::SearchCommand
6
+ class OnelineRenderer
7
+ include I18nFlow::CLI::Color
8
+
9
+ FILE = __dir__ + '/oneline.erb'
10
+
11
+ attr_reader :results
12
+
13
+ def initialize(results, color: true)
14
+ @results = results
15
+ @color_enabled = !!color
16
+ end
17
+
18
+ def render
19
+ with_color(erb.result(binding))
20
+ end
21
+
22
+ def color_enabled?
23
+ @color_enabled
24
+ end
25
+
26
+ private
27
+
28
+ def with_color(str)
29
+ return str unless color_enabled?
30
+
31
+ str.gsub!(/^(\S+)( \[)([^\]]+)(\] \([^\)]+\))(.*)/) { color($1, :yellow) + $2 + color($3, :blue) + $4 + color($5, :green) }
32
+ str
33
+ end
34
+
35
+ def erb
36
+ @erb ||= ERB.new(File.read(FILE), 0, '-')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'command_base'
2
+ require_relative 'color'
3
+ require_relative '../repository'
4
+ require_relative '../search'
5
+
6
+ class I18nFlow::CLI
7
+ class SearchCommand < CommandBase
8
+ require_relative 'search_command/default_renderer'
9
+ require_relative 'search_command/oneline_renderer'
10
+
11
+ DEFAULT_FORMAT = 'default'
12
+
13
+ def invoke!
14
+ unless pattern
15
+ exit_with_message(1, 'usage: i18n_flow search PATTERN')
16
+ end
17
+
18
+ search.search!
19
+
20
+ case output_format
21
+ when 'default'
22
+ puts DefaultRenderer.new(search.results, color: color_enabled?).render
23
+ when 'oneline'
24
+ puts OnelineRenderer.new(search.results, color: color_enabled?).render
25
+ else
26
+ exit_with_message(1, 'Unsupported format: %s' % [output_format])
27
+ end
28
+ end
29
+
30
+ def pattern
31
+ args[0]
32
+ end
33
+
34
+ def output_format
35
+ @output_format ||= options['format'] || DEFAULT_FORMAT
36
+ end
37
+
38
+ def include_all?
39
+ !!(options['all'] || options['a'])
40
+ end
41
+
42
+ private
43
+
44
+ def repository
45
+ @repository ||= I18nFlow::Repository.new(
46
+ base_path: I18nFlow.config.base_path,
47
+ glob_patterns: I18nFlow.config.glob_patterns,
48
+ )
49
+ end
50
+
51
+ def search
52
+ @search ||= I18nFlow::Search.new(
53
+ repository: repository,
54
+ pattern: pattern,
55
+ include_all: include_all?,
56
+ )
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,20 @@
1
+ require_relative 'command_base'
2
+ require_relative '../repository'
3
+ require_relative '../splitter'
4
+
5
+ class I18nFlow::CLI
6
+ class SplitCommand < CommandBase
7
+ def invoke!
8
+ exit_with_message(1, 'not implemented')
9
+ end
10
+
11
+ private
12
+
13
+ def repository
14
+ @repository ||= I18nFlow::Repository.new(
15
+ base_path: I18nFlow.config.base_path,
16
+ glob_patterns: I18nFlow.config.glob_patterns,
17
+ )
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'command_base'
2
+
3
+ class I18nFlow::CLI
4
+ class VersionCommand < CommandBase
5
+ def invoke!
6
+ puts 'i18n_flow v%s' % I18nFlow::VERSION
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ require_relative 'util'
2
+
3
+ class I18nFlow::CLI
4
+ require_relative 'cli/lint_command'
5
+ require_relative 'cli/split_command'
6
+ require_relative 'cli/copy_command'
7
+ require_relative 'cli/search_command'
8
+ require_relative 'cli/version_command'
9
+ require_relative 'cli/help_command'
10
+ require_relative 'cli/read_config_command'
11
+
12
+ COMMANDS = {
13
+ 'lint' => LintCommand,
14
+ 'search' => SearchCommand,
15
+ 'split' => SplitCommand,
16
+ 'copy' => CopyCommand,
17
+ 'version' => VersionCommand,
18
+ 'help' => HelpCommand,
19
+ 'read_config' => ReadConfigCommand,
20
+ }
21
+
22
+ attr_reader :args
23
+ attr_reader :command
24
+ attr_reader :global_options
25
+
26
+ def initialize(args)
27
+ @global_options = I18nFlow::Util.parse_options(args)
28
+ @command, *@args = args
29
+ end
30
+
31
+ def run
32
+ if global_options['v'] || global_options['version']
33
+ @command = 'version'
34
+ end
35
+ if global_options['h']
36
+ @command = 'help'
37
+ end
38
+
39
+ command_class = COMMANDS[command] || COMMANDS['help']
40
+ command_class.new(args).invoke!
41
+ end
42
+ end