i18n-processes 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/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
|