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.
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