i18n-processes 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +12 -0
- data/bin/i18n-processes +28 -0
- data/bin/i18n-processes.cmd +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/zh-CN.yml +2 -0
- data/i18n-processes.gemspec +64 -0
- data/lib/i18n/processes/base_process.rb +47 -0
- data/lib/i18n/processes/cli.rb +208 -0
- data/lib/i18n/processes/command/collection.rb +21 -0
- data/lib/i18n/processes/command/commander.rb +43 -0
- data/lib/i18n/processes/command/commands/data.rb +107 -0
- data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
- data/lib/i18n/processes/command/commands/health.rb +26 -0
- data/lib/i18n/processes/command/commands/meta.rb +38 -0
- data/lib/i18n/processes/command/commands/missing.rb +86 -0
- data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
- data/lib/i18n/processes/command/commands/tree.rb +119 -0
- data/lib/i18n/processes/command/commands/usages.rb +69 -0
- data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
- data/lib/i18n/processes/command/dsl.rb +56 -0
- data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
- data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
- data/lib/i18n/processes/command/options/common.rb +41 -0
- data/lib/i18n/processes/command/options/data.rb +95 -0
- data/lib/i18n/processes/command/options/locales.rb +36 -0
- data/lib/i18n/processes/command_error.rb +13 -0
- data/lib/i18n/processes/commands.rb +31 -0
- data/lib/i18n/processes/configuration.rb +132 -0
- data/lib/i18n/processes/console_context.rb +76 -0
- data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
- data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
- data/lib/i18n/processes/data/file_formats.rb +111 -0
- data/lib/i18n/processes/data/file_system.rb +14 -0
- data/lib/i18n/processes/data/file_system_base.rb +205 -0
- data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
- data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
- data/lib/i18n/processes/data/tree/node.rb +204 -0
- data/lib/i18n/processes/data/tree/nodes.rb +97 -0
- data/lib/i18n/processes/data/tree/siblings.rb +333 -0
- data/lib/i18n/processes/data/tree/traversal.rb +190 -0
- data/lib/i18n/processes/data.rb +87 -0
- data/lib/i18n/processes/google_translation.rb +125 -0
- data/lib/i18n/processes/html_keys.rb +16 -0
- data/lib/i18n/processes/ignore_keys.rb +30 -0
- data/lib/i18n/processes/key_pattern_matching.rb +37 -0
- data/lib/i18n/processes/locale_list.rb +19 -0
- data/lib/i18n/processes/locale_pathname.rb +17 -0
- data/lib/i18n/processes/logging.rb +37 -0
- data/lib/i18n/processes/missing_keys.rb +122 -0
- data/lib/i18n/processes/path.rb +42 -0
- data/lib/i18n/processes/plural_keys.rb +41 -0
- data/lib/i18n/processes/rainbow_utils.rb +13 -0
- data/lib/i18n/processes/references.rb +101 -0
- data/lib/i18n/processes/reports/base.rb +71 -0
- data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
- data/lib/i18n/processes/reports/terminal.rb +252 -0
- data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
- data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
- data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
- data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
- data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
- data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
- data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
- data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
- data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
- data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
- data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
- data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
- data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
- data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
- data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
- data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
- data/lib/i18n/processes/scanners/scanner.rb +17 -0
- data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
- data/lib/i18n/processes/split_key.rb +68 -0
- data/lib/i18n/processes/stats.rb +24 -0
- data/lib/i18n/processes/string_interpolation.rb +16 -0
- data/lib/i18n/processes/unused_keys.rb +23 -0
- data/lib/i18n/processes/used_keys.rb +177 -0
- data/lib/i18n/processes/version.rb +7 -0
- data/lib/i18n/processes.rb +69 -0
- data/source/p1/_messages/zh/article.properties +9 -0
- data/source/p1/_messages/zh/company.properties +62 -0
- data/source/p1/_messages/zh/devices.properties +40 -0
- data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
- data/source/p1/_messages/zh/meetingBooking.properties +18 -0
- data/source/p1/_messages/zh/office-areas.properties +64 -0
- data/source/p1/_messages/zh/orders.properties +25 -0
- data/source/p1/_messages/zh/schedulings.properties +7 -0
- data/source/p1/_messages/zh/tag.properties +2 -0
- data/source/p1/_messages/zh/ticket.properties +9 -0
- data/source/p1/_messages/zh/visitor.properties +5 -0
- data/source/p1/messages +586 -0
- data/source/p2/orders.properties +25 -0
- data/source/p2/schedulings.properties +7 -0
- data/source/p2/tag.properties +2 -0
- data/source/p2/ticket.properties +9 -0
- data/source/p2/visitor.properties +5 -0
- data/source/zh.messages.ts +30 -0
- data/translated/en/p1/_messages/zh/article.properties +9 -0
- data/translated/en/p1/_messages/zh/company.properties +62 -0
- data/translated/en/p1/_messages/zh/devices.properties +40 -0
- data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
- data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
- data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
- data/translated/en/p1/_messages/zh/orders.properties +25 -0
- data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
- data/translated/en/p1/_messages/zh/tag.properties +2 -0
- data/translated/en/p1/_messages/zh/ticket.properties +9 -0
- data/translated/en/p1/_messages/zh/visitor.properties +5 -0
- data/translated/en/p1/messages +586 -0
- data/translated/en/p2/orders.properties +25 -0
- data/translated/en/p2/schedulings.properties +7 -0
- data/translated/en/p2/tag.properties +2 -0
- data/translated/en/p2/ticket.properties +9 -0
- data/translated/en/p2/visitor.properties +5 -0
- data/translated/en/zh.messages.ts +30 -0
- data/translation/en/article.properties +9 -0
- data/translation/en/company.properties +56 -0
- data/translation/en/meeting-rooms.properties +87 -0
- data/translation/en/meetingBooking.properties +14 -0
- data/translation/en/messages.en +164 -0
- data/translation/en/office-areas.properties +51 -0
- data/translation/en/orders.properties +26 -0
- data/translation/en/tag.properties +2 -0
- data/translation/en/translated +1263 -0
- data/translation/en/visitor.properties +4 -0
- 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
|