i18n-processes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile.lock +102 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +46 -0
  5. data/Rakefile +12 -0
  6. data/bin/i18n-processes +28 -0
  7. data/bin/i18n-processes.cmd +2 -0
  8. data/config/locales/en.yml +2 -0
  9. data/config/locales/zh-CN.yml +2 -0
  10. data/i18n-processes.gemspec +64 -0
  11. data/lib/i18n/processes/base_process.rb +47 -0
  12. data/lib/i18n/processes/cli.rb +208 -0
  13. data/lib/i18n/processes/command/collection.rb +21 -0
  14. data/lib/i18n/processes/command/commander.rb +43 -0
  15. data/lib/i18n/processes/command/commands/data.rb +107 -0
  16. data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
  17. data/lib/i18n/processes/command/commands/health.rb +26 -0
  18. data/lib/i18n/processes/command/commands/meta.rb +38 -0
  19. data/lib/i18n/processes/command/commands/missing.rb +86 -0
  20. data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
  21. data/lib/i18n/processes/command/commands/tree.rb +119 -0
  22. data/lib/i18n/processes/command/commands/usages.rb +69 -0
  23. data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
  24. data/lib/i18n/processes/command/dsl.rb +56 -0
  25. data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
  26. data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
  27. data/lib/i18n/processes/command/options/common.rb +41 -0
  28. data/lib/i18n/processes/command/options/data.rb +95 -0
  29. data/lib/i18n/processes/command/options/locales.rb +36 -0
  30. data/lib/i18n/processes/command_error.rb +13 -0
  31. data/lib/i18n/processes/commands.rb +31 -0
  32. data/lib/i18n/processes/configuration.rb +132 -0
  33. data/lib/i18n/processes/console_context.rb +76 -0
  34. data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
  35. data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
  36. data/lib/i18n/processes/data/file_formats.rb +111 -0
  37. data/lib/i18n/processes/data/file_system.rb +14 -0
  38. data/lib/i18n/processes/data/file_system_base.rb +205 -0
  39. data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
  40. data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
  41. data/lib/i18n/processes/data/tree/node.rb +204 -0
  42. data/lib/i18n/processes/data/tree/nodes.rb +97 -0
  43. data/lib/i18n/processes/data/tree/siblings.rb +333 -0
  44. data/lib/i18n/processes/data/tree/traversal.rb +190 -0
  45. data/lib/i18n/processes/data.rb +87 -0
  46. data/lib/i18n/processes/google_translation.rb +125 -0
  47. data/lib/i18n/processes/html_keys.rb +16 -0
  48. data/lib/i18n/processes/ignore_keys.rb +30 -0
  49. data/lib/i18n/processes/key_pattern_matching.rb +37 -0
  50. data/lib/i18n/processes/locale_list.rb +19 -0
  51. data/lib/i18n/processes/locale_pathname.rb +17 -0
  52. data/lib/i18n/processes/logging.rb +37 -0
  53. data/lib/i18n/processes/missing_keys.rb +122 -0
  54. data/lib/i18n/processes/path.rb +42 -0
  55. data/lib/i18n/processes/plural_keys.rb +41 -0
  56. data/lib/i18n/processes/rainbow_utils.rb +13 -0
  57. data/lib/i18n/processes/references.rb +101 -0
  58. data/lib/i18n/processes/reports/base.rb +71 -0
  59. data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
  60. data/lib/i18n/processes/reports/terminal.rb +252 -0
  61. data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
  62. data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
  63. data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
  64. data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
  65. data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
  66. data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
  67. data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
  68. data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
  69. data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
  70. data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
  71. data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
  72. data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
  73. data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
  74. data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
  75. data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
  76. data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
  77. data/lib/i18n/processes/scanners/scanner.rb +17 -0
  78. data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
  79. data/lib/i18n/processes/split_key.rb +68 -0
  80. data/lib/i18n/processes/stats.rb +24 -0
  81. data/lib/i18n/processes/string_interpolation.rb +16 -0
  82. data/lib/i18n/processes/unused_keys.rb +23 -0
  83. data/lib/i18n/processes/used_keys.rb +177 -0
  84. data/lib/i18n/processes/version.rb +7 -0
  85. data/lib/i18n/processes.rb +69 -0
  86. data/source/p1/_messages/zh/article.properties +9 -0
  87. data/source/p1/_messages/zh/company.properties +62 -0
  88. data/source/p1/_messages/zh/devices.properties +40 -0
  89. data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
  90. data/source/p1/_messages/zh/meetingBooking.properties +18 -0
  91. data/source/p1/_messages/zh/office-areas.properties +64 -0
  92. data/source/p1/_messages/zh/orders.properties +25 -0
  93. data/source/p1/_messages/zh/schedulings.properties +7 -0
  94. data/source/p1/_messages/zh/tag.properties +2 -0
  95. data/source/p1/_messages/zh/ticket.properties +9 -0
  96. data/source/p1/_messages/zh/visitor.properties +5 -0
  97. data/source/p1/messages +586 -0
  98. data/source/p2/orders.properties +25 -0
  99. data/source/p2/schedulings.properties +7 -0
  100. data/source/p2/tag.properties +2 -0
  101. data/source/p2/ticket.properties +9 -0
  102. data/source/p2/visitor.properties +5 -0
  103. data/source/zh.messages.ts +30 -0
  104. data/translated/en/p1/_messages/zh/article.properties +9 -0
  105. data/translated/en/p1/_messages/zh/company.properties +62 -0
  106. data/translated/en/p1/_messages/zh/devices.properties +40 -0
  107. data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
  108. data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
  109. data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
  110. data/translated/en/p1/_messages/zh/orders.properties +25 -0
  111. data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
  112. data/translated/en/p1/_messages/zh/tag.properties +2 -0
  113. data/translated/en/p1/_messages/zh/ticket.properties +9 -0
  114. data/translated/en/p1/_messages/zh/visitor.properties +5 -0
  115. data/translated/en/p1/messages +586 -0
  116. data/translated/en/p2/orders.properties +25 -0
  117. data/translated/en/p2/schedulings.properties +7 -0
  118. data/translated/en/p2/tag.properties +2 -0
  119. data/translated/en/p2/ticket.properties +9 -0
  120. data/translated/en/p2/visitor.properties +5 -0
  121. data/translated/en/zh.messages.ts +30 -0
  122. data/translation/en/article.properties +9 -0
  123. data/translation/en/company.properties +56 -0
  124. data/translation/en/meeting-rooms.properties +87 -0
  125. data/translation/en/meetingBooking.properties +14 -0
  126. data/translation/en/messages.en +164 -0
  127. data/translation/en/office-areas.properties +51 -0
  128. data/translation/en/orders.properties +26 -0
  129. data/translation/en/tag.properties +2 -0
  130. data/translation/en/translated +1263 -0
  131. data/translation/en/visitor.properties +4 -0
  132. metadata +408 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+ module I18n
6
+ module Processes
7
+ module Data
8
+ module FileFormats
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ def adapter_dump(tree, format)
14
+ adapter_op :dump, format, tree, write_config(format)
15
+ end
16
+
17
+ # @return [Hash]
18
+ def adapter_parse(tree, format)
19
+ adapter_op :parse, format, tree, read_config(format)
20
+ end
21
+
22
+ def adapter_op(op, format, tree, config)
23
+ self.class.adapter_by_name(format).send(op, tree, config)
24
+ rescue Exception => e # rubocop:disable Lint/RescueException
25
+ raise CommandError, "#{format} #{op} error: #{e.message}"
26
+ end
27
+
28
+
29
+
30
+ protected
31
+
32
+ def write_config(format)
33
+ (config[format] || {})[:write]
34
+ end
35
+
36
+ def read_config(format)
37
+ (config[format] || {})[:read]
38
+ end
39
+
40
+
41
+
42
+ # def source_config(format)
43
+ # (config[format] || {})[:source]
44
+ # end
45
+ #
46
+ # def translation_config(format)
47
+ # (config[format] || {})[:translation]
48
+ # end
49
+ #
50
+ # def translated_config(format)
51
+ # (config[format] || {})[:translated]
52
+ # end
53
+
54
+ # @return [Hash]
55
+ def load_file(path)
56
+ adapter_parse read_file(path), self.class.adapter_name_for_path(path)
57
+ end
58
+
59
+ # @return [String]
60
+ def read_file(path)
61
+ ::File.read(path, encoding: 'UTF-8')
62
+ end
63
+
64
+ def write_tree(path, tree)
65
+ hash = tree.to_hash(true)
66
+ adapter = self.class.adapter_name_for_path(path)
67
+ content = adapter_dump(hash, adapter)
68
+ # Ignore unchanged data
69
+ return if File.file?(path) && content == read_file(path)
70
+ ::FileUtils.mkpath(File.dirname(path))
71
+ ::File.open(path, 'w') { |f| f.write content }
72
+ end
73
+
74
+ def normalized?(path, tree)
75
+ return false unless File.file?(path)
76
+ read_file(path) == adapter_dump(tree.to_hash(true), self.class.adapter_name_for_path(path))
77
+ end
78
+
79
+ module ClassMethods
80
+ # @param pattern [String] File.fnmatch pattern
81
+ # @param adapter [responds to parse(string)->hash and dump(hash)->string]
82
+ def register_adapter(name, pattern, adapter)
83
+ (@fn_patterns ||= []) << [name, pattern, adapter]
84
+ end
85
+
86
+ def adapter_name_for_path(path)
87
+ @fn_patterns.detect do |(_name, pattern, _adapter)|
88
+ ::File.fnmatch(pattern, path)
89
+ end.try(:first) || fail(
90
+ CommandError, "Adapter not found for #{path}. Registered adapters: #{@fn_patterns.inspect}"
91
+ )
92
+ end
93
+
94
+ def adapter_names
95
+ @fn_patterns.map(&:first)
96
+ end
97
+
98
+ def adapter_by_name(name)
99
+ name = name.to_s
100
+ @fn_patterns.detect do |(adapter_name, _pattern, _adapter)|
101
+ adapter_name.to_s == name
102
+ end.try(:last) || fail(
103
+ CommandError,
104
+ "Adapter with name #{name.inspect} not found. Registered adapters: #{@fn_patterns.inspect}"
105
+ )
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/data/file_system_base'
4
+ require 'i18n/processes/data/adapter/json_adapter'
5
+ require 'i18n/processes/data/adapter/yaml_adapter'
6
+
7
+ module I18n::Processes
8
+ module Data
9
+ class FileSystem < FileSystemBase
10
+ register_adapter :yaml, '*.yml', Adapter::YamlAdapter
11
+ register_adapter :json, '*.json', Adapter::JsonAdapter
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/data/tree/node'
4
+ require 'i18n/processes/data/router/pattern_router'
5
+ require 'i18n/processes/data/router/conservative_router'
6
+ require 'i18n/processes/data/file_formats'
7
+ require 'i18n/processes/key_pattern_matching'
8
+
9
+ module I18n::Processes
10
+ module Data
11
+ class FileSystemBase # rubocop:disable Metrics/ClassLength
12
+ include I18n::Processes::Data::FileFormats
13
+ include I18n::Processes::Logging
14
+
15
+ attr_reader :config, :base_locale, :locales
16
+ attr_writer :locales
17
+
18
+ DEFAULTS = {
19
+ read: ['config/locales/%{locale}.yml'],
20
+ write: ['config/locales/%{locale}.yml']
21
+ }.freeze
22
+
23
+ def initialize(config = {})
24
+ self.config = config.except(:base_locale, :locales)
25
+ @base_locale = config[:base_locale]
26
+ locales = config[:locales].presence
27
+ @locales = LocaleList.normalize_locale_list(locales || available_locales, base_locale, true)
28
+ if locales.present?
29
+ log_verbose "locales read from config #{@locales * ', '}"
30
+ else
31
+ log_verbose "locales inferred from data: #{@locales * ', '}"
32
+ end
33
+ end
34
+
35
+ # @param [String, Symbol] locale
36
+ # @return [::I18n::Processes::Data::Siblings]
37
+ def get(locale)
38
+ locale = locale.to_s
39
+ @trees ||= {}
40
+ @trees[locale] ||= read_locale(locale)
41
+ end
42
+
43
+ alias [] get
44
+
45
+ # @param [String, Symbol] locale
46
+ # @return [::I18n::Processes::Data::Siblings]
47
+ def external(locale)
48
+ locale = locale.to_s
49
+ @external ||= {}
50
+ @external[locale] ||= read_locale(locale, paths: config[:external])
51
+ end
52
+
53
+ # set locale tree
54
+ # @param [String, Symbol] locale
55
+ # @param [::I18n::Processes::Data::Siblings] tree
56
+ def set(locale, tree)
57
+ locale = locale.to_s
58
+ @trees.delete(locale) if @trees
59
+ paths_before = Set.new(get(locale)[locale].leaves.map { |node| node.data[:path] })
60
+ paths_after = Set.new([])
61
+ # $stderr.puts Rainbow("locale: #{locale}").green
62
+ # $stderr.puts Rainbow("tree: #{tree.class}").green
63
+ router.route locale, tree do |path, tree_slice|
64
+ paths_after << path
65
+ write_tree path, tree_slice
66
+ end
67
+ (paths_before - paths_after).each do |path|
68
+ FileUtils.remove_file(path) if File.exist?(path)
69
+ end
70
+ @trees.delete(locale) if @trees
71
+ @available_locales = nil
72
+ end
73
+
74
+ alias []= set
75
+
76
+ # @param [String] locale
77
+ # @return [Array<String>] paths to files that are not normalized
78
+ def non_normalized_paths(locale)
79
+ router.route(locale, get(locale))
80
+ .reject { |path, tree_slice| normalized?(path, tree_slice) }
81
+ .map(&:first)
82
+ end
83
+
84
+ def write(forest)
85
+ forest.each { |root| set(root.key, root.to_siblings) }
86
+ end
87
+
88
+ def merge!(forest)
89
+ ## forest is a sibling
90
+ forest.inject(Tree::Siblings.new) do |result, root|
91
+ # root is a node, merge is a sibling
92
+ locale = root.key
93
+ merged = get(locale).merge(root)
94
+ $stderr.puts Rainbow("root: #{root}").green
95
+ $stderr.puts Rainbow("locale: #{locale}").green
96
+ $stderr.puts Rainbow("merged: #{merged}").green
97
+ set locale, merged
98
+ result.merge! merged
99
+ end
100
+ end
101
+
102
+ def remove_by_key!(forest)
103
+ forest.inject(Tree::Siblings.new) do |removed, root|
104
+ locale = root.key
105
+ locale_data = get(locale)
106
+ subtracted = locale_data.subtract_by_key(forest)
107
+ set locale, subtracted
108
+ removed.merge! locale_data.subtract_by_key(subtracted)
109
+ end
110
+ end
111
+
112
+ # @return self
113
+ def reload
114
+ @trees = nil
115
+ @available_locales = nil
116
+ self
117
+ end
118
+
119
+ # Get available locales from the list of file names to read
120
+ def available_locales
121
+ @available_locales ||= begin
122
+ locales = Set.new
123
+ Array(config[:read]).map do |pattern|
124
+ [pattern, Dir.glob(format(pattern, locale: '*'))] if pattern.include?('%{locale}')
125
+ end.compact.each do |pattern, paths|
126
+ p = pattern.gsub('\\', '\\\\').gsub('/', '\/').gsub('.', '\.')
127
+ p = p.gsub(/(\*+)/) { Regexp.last_match(1) == '**' ? '.*' : '[^/]*?' }.gsub('%{locale}', '([^/.]+)')
128
+ re = /\A#{p}\z/
129
+ paths.each do |path|
130
+ locales << Regexp.last_match(1) if re =~ path
131
+ end
132
+ end
133
+ locales
134
+ end
135
+ end
136
+
137
+ def t(key, locale)
138
+ tree = self[locale.to_s]
139
+ return unless tree
140
+ tree[locale][key].try(:value_or_children_hash)
141
+ end
142
+
143
+ def config=(config)
144
+ @config = DEFAULTS.deep_merge((config || {}).reject { |_k, v| v.nil? })
145
+ reload
146
+ end
147
+
148
+ def with_router(router)
149
+ router_was = self.router
150
+ self.router = router
151
+ yield
152
+ ensure
153
+ self.router = router_was
154
+ end
155
+
156
+ ROUTER_NAME_ALIASES = {
157
+ 'conservative_router' => 'I18n::Processes::Data::Router::ConservativeRouter',
158
+ 'pattern_router' => 'I18n::Processes::Data::Router::PatternRouter'
159
+ }.freeze
160
+ def router
161
+ @router ||= begin
162
+ name = @config[:router].presence || 'conservative_router'
163
+ name = ROUTER_NAME_ALIASES[name] || name
164
+ router_class = ActiveSupport::Inflector.constantize(name)
165
+ router_class.new(self, @config.merge(base_locale: base_locale, locales: locales))
166
+ end
167
+ end
168
+ attr_writer :router
169
+
170
+ protected
171
+
172
+ def read_locale(locale, paths: config[:read])
173
+ Array(paths).flat_map do |path|
174
+ Dir.glob format(path, locale: locale)
175
+ end.map do |path|
176
+ [path.freeze, load_file(path) || {}]
177
+ end.map do |path, data|
178
+ filter_nil_keys! path, data
179
+ Data::Tree::Siblings.from_nested_hash(data).tap do |s|
180
+ s.leaves { |x| x.data.update(path: path, locale: locale) }
181
+ end
182
+ end.reduce(Tree::Siblings[locale => {}], :merge!)
183
+ end
184
+
185
+ def filter_nil_keys!(path, data, suffix = [])
186
+ data.each do |key, value|
187
+ if key.nil?
188
+ data.delete(key)
189
+ log_warn <<-TEXT
190
+ Skipping a nil key found in #{path.inspect}:
191
+ key: #{suffix.join('.')}.`nil`
192
+ value: #{value.inspect}
193
+ Nil keys are not supported by i18n.
194
+ The following unquoted YAML keys result in a nil key:
195
+ #{%w[null Null NULL ~].join(', ')}
196
+ See http://yaml.org/type/null.html
197
+ TEXT
198
+ elsif value.is_a?(Hash)
199
+ filter_nil_keys! path, value, suffix + [key]
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/data/router/pattern_router'
4
+
5
+ module I18n::Processes
6
+ module Data::Router
7
+ # Keep the path, or infer from base locale
8
+ class ConservativeRouter < PatternRouter
9
+ def initialize(adapter, config)
10
+ @adapter = adapter
11
+ @base_locale = config[:base_locale]
12
+ @locales = config[:locales]
13
+ super
14
+ end
15
+
16
+ def route(locale, forest, &block)
17
+ return to_enum(:route, locale, forest) unless block
18
+ out = Hash.new { |hash, key| hash[key] = Set.new }
19
+ not_found = Set.new
20
+ forest.keys do |key, _node|
21
+ path = key_path(locale, key)
22
+ $stderr.puts Rainbow("path in route: #{path}").green
23
+ # infer from another locale
24
+ unless path
25
+ inferred_from = (locales - [locale]).detect { |loc| path = key_path(loc, key) }
26
+ path = LocalePathname.replace_locale(path, inferred_from, locale) if inferred_from
27
+ end
28
+ key_with_locale = "#{locale}.#{key}"
29
+ $stderr.puts Rainbow("key_with_locale in route: #{key_with_locale}").green
30
+ if path
31
+ out[path] << key_with_locale
32
+ else
33
+ not_found << key_with_locale
34
+ end
35
+ end
36
+
37
+
38
+ if not_found.present?
39
+ # fall back to pattern router
40
+ not_found_tree = forest.select_keys(root: true) { |key, _| not_found.include?(key) }
41
+ super(locale, not_found_tree) do |path, tree|
42
+ out[path] += tree.key_names(root: true)
43
+ end
44
+ end
45
+
46
+ out.each do |dest, keys|
47
+ # $stderr.puts Rainbow("dest in route: #{dest}").green
48
+ # $stderr.puts Rainbow("dest in route: #{dest}").green
49
+ block.yield dest, forest.select_keys(root: true) { |key, _| keys.include?(key) }
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def base_tree
56
+ adapter[base_locale]
57
+ end
58
+
59
+ def key_path(locale, key)
60
+ adapter[locale]["#{locale}.#{key}"].try(:data).try(:[], :path)
61
+ end
62
+
63
+ attr_reader :adapter, :base_locale, :locales
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/key_pattern_matching'
4
+ require 'i18n/processes/data/tree/node'
5
+
6
+ module I18n::Processes
7
+ module Data::Router
8
+ # Route based on key name
9
+ class PatternRouter
10
+ include ::I18n::Processes::KeyPatternMatching
11
+
12
+ attr_reader :routes
13
+ # @option data_config write [Array] of routes
14
+ # @example
15
+ # {write:
16
+ # # keys matched top to bottom
17
+ # [['devise.*', 'config/locales/devise.%{locale}.yml'],
18
+ # # default catch-all (same as ['*', 'config/locales/%{locale}.yml'])
19
+ # 'config/locales/%{locale}.yml']}
20
+ def initialize(_adapter, data_config)
21
+ @routes_config = data_config[:write]
22
+ @routes = compile_routes @routes_config
23
+ end
24
+
25
+ # Route keys to destinations
26
+ # @param forest [I18n::Processes::Data::Tree::Siblings] forest roots are locales.
27
+ # @yieldparam [String] dest_path
28
+ # @yieldparam [I18n::Processes::Data::Tree::Siblings] tree_slice
29
+ # @return [Hash] mapping of destination => [ [key, value], ... ]
30
+ def route(locale, forest, &block)
31
+ return to_enum(:route, locale, forest) unless block
32
+ locale = locale.to_s
33
+ out = {}
34
+ forest.keys do |key, _node|
35
+ pattern, path = routes.detect { |route| route[0] =~ key }
36
+ if pattern
37
+ key_match = $~
38
+ path = format(path, locale: locale)
39
+ path.gsub!(/\\\d+/) { |m| key_match[m[1..-1].to_i] }
40
+ (out[path] ||= Set.new) << "#{locale}.#{key}"
41
+ else
42
+ fail CommandError, "Cannot route key #{key}. Routes are #{@routes_config.inspect}"
43
+ end
44
+ end
45
+ out.each do |dest, keys|
46
+ block.yield dest,
47
+ forest.select_keys(root: true) { |key, _| keys.include?(key) }
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def compile_routes(routes)
54
+ routes.map { |x| x.is_a?(String) ? ['*', x] : x }.map do |x|
55
+ [compile_key_pattern(x[0]), x[1]]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'i18n/processes/data/tree/traversal'
4
+ require 'i18n/processes/data/tree/siblings'
5
+ require 'i18n/processes/rainbow_utils'
6
+ module I18n::Processes::Data::Tree
7
+ class Node # rubocop:disable Metrics/ClassLength
8
+ include Enumerable
9
+ include Traversal
10
+
11
+ attr_accessor :value
12
+ attr_reader :key, :children, :parent
13
+
14
+ # rubocop:disable Metrics/ParameterLists
15
+ def initialize(key:, value: nil, data: nil, parent: nil, children: nil, warn_about_add_children_to_leaf: true)
16
+ @key = key
17
+ @key = @key.to_s.freeze if @key
18
+ @value = value
19
+ @data = data
20
+ @parent = parent
21
+ @warn_about_add_children_to_leaf = warn_about_add_children_to_leaf
22
+ self.children = (children if children)
23
+ end
24
+ # rubocop:enable Metrics/ParameterLists
25
+
26
+ def attributes
27
+ { key: @key, value: @value, data: @data.try(:clone), parent: @parent, children: @children }
28
+ end
29
+
30
+ def derive(new_attr = {})
31
+ self.class.new(attributes.merge(new_attr))
32
+ end
33
+
34
+ def children=(children)
35
+ @children = case children
36
+ when Siblings
37
+ children.parent == self ? children : children.derive(parent: self)
38
+ when NilClass
39
+ nil
40
+ else
41
+ Siblings.new(
42
+ nodes: children,
43
+ parent: self,
44
+ warn_about_add_children_to_leaf: @warn_about_add_children_to_leaf
45
+ )
46
+ end
47
+ dirty!
48
+ end
49
+
50
+ def each(&block)
51
+ return to_enum(:each) { 1 } unless block
52
+ block.yield(self)
53
+ self
54
+ end
55
+
56
+ def value_or_children_hash
57
+ leaf? ? value : children.try(:to_hash)
58
+ end
59
+
60
+ def leaf?
61
+ !children
62
+ end
63
+
64
+ # a node with key nil is considered Empty. this is to allow for using these nodes instead of nils
65
+ def root?
66
+ !parent?
67
+ end
68
+
69
+ def parent?
70
+ !parent.nil?
71
+ end
72
+
73
+ def children?
74
+ children && !children.empty?
75
+ end
76
+
77
+ def data
78
+ @data ||= {}
79
+ end
80
+
81
+ def data?
82
+ @data.present?
83
+ end
84
+
85
+ def reference?
86
+ value.is_a?(Symbol)
87
+ end
88
+
89
+ def get(key)
90
+ children.get(key)
91
+ end
92
+
93
+ alias [] get
94
+
95
+ # append and reparent nodes
96
+ def append!(nodes)
97
+ if @children
98
+ @children.merge!(nodes)
99
+ else
100
+ @children = Siblings.new(nodes: nodes, parent: self)
101
+ end
102
+ self
103
+ end
104
+
105
+ def append(nodes)
106
+ derive.append!(nodes)
107
+ end
108
+
109
+ def full_key(root: true)
110
+ @full_key ||= {}
111
+ @full_key[root] ||= "#{"#{parent.full_key(root: root)}." if parent? && (root || parent.parent?)}#{key}"
112
+ end
113
+
114
+ def walk_to_root(&visitor)
115
+ return to_enum(:walk_to_root) unless visitor
116
+ visitor.yield self
117
+ parent.walk_to_root(&visitor) if parent?
118
+ end
119
+
120
+ def root
121
+ p = nil
122
+ walk_to_root { |node| p = node }
123
+ p
124
+ end
125
+
126
+ def walk_from_root(&visitor)
127
+ return to_enum(:walk_from_root) unless visitor
128
+ walk_to_root.reverse_each do |node|
129
+ visitor.yield node
130
+ end
131
+ end
132
+
133
+ def set(full_key, node)
134
+ (@children ||= Siblings.new(parent: self)).set(full_key, node)
135
+ dirty!
136
+ node
137
+ end
138
+
139
+ alias []= set
140
+
141
+ def to_nodes
142
+ Nodes.new([self])
143
+ end
144
+
145
+ def to_siblings
146
+ parent && parent.children || Siblings.new(nodes: [self])
147
+ end
148
+
149
+ def to_hash(sort = false)
150
+ (@hash ||= {})[sort] ||= begin
151
+ children_hash = children ? children.to_hash(sort) : {}
152
+ if key.nil?
153
+ children_hash
154
+ elsif leaf?
155
+ { key => value }
156
+ else
157
+ { key => children_hash }
158
+ end
159
+ end
160
+ end
161
+
162
+ delegate :to_json, to: :to_hash
163
+ delegate :to_yaml, to: :to_hash
164
+
165
+ def inspect(level = 0)
166
+ label = if key.nil?
167
+ I18n::Processes::RainbowUtils.faint_color('∅')
168
+ else
169
+ [Rainbow(key).color(1 + level % 15),
170
+ (": #{format_value_for_inspect(value)}" if leaf?),
171
+ (" #{data}" if data?)].compact.join
172
+ end
173
+ [' ' * level, label, ("\n" + children.map { |c| c.inspect(level + 1) }.join("\n") if children?)].compact.join
174
+ end
175
+
176
+ def format_value_for_inspect(value)
177
+ if value.is_a?(Symbol)
178
+ "#{Rainbow('⮕ ').bright.yellow}#{Rainbow(value).yellow}"
179
+ else
180
+ Rainbow(value).cyan
181
+ end
182
+ end
183
+
184
+ protected
185
+
186
+ def dirty!
187
+ @hash = nil
188
+ @full_key = nil
189
+ end
190
+
191
+ class << self
192
+ # value can be a nested hash
193
+ def from_key_value(key, value)
194
+ Node.new(key: key.try(:to_s)).tap do |node|
195
+ if value.is_a?(Hash)
196
+ node.children = Siblings.from_nested_hash(value)
197
+ else
198
+ node.value = value
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end