-
Notifications
You must be signed in to change notification settings - Fork 260
/
file_system_base.rb
205 lines (181 loc) · 6.56 KB
/
file_system_base.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# frozen_string_literal: true
require 'i18n/tasks/data/tree/node'
require 'i18n/tasks/data/router/pattern_router'
require 'i18n/tasks/data/router/conservative_router'
require 'i18n/tasks/data/router/isolating_router'
require 'i18n/tasks/data/file_formats'
require 'i18n/tasks/key_pattern_matching'
module I18n::Tasks
module Data
class FileSystemBase # rubocop:disable Metrics/ClassLength
include ::I18n::Tasks::Data::FileFormats
include ::I18n::Tasks::Logging
attr_accessor :locales
attr_reader :config, :base_locale
attr_writer :router
DEFAULTS = {
read: ['config/locales/%{locale}.yml'],
write: ['config/locales/%{locale}.yml']
}.freeze
def initialize(config = {})
self.config = config.except(:base_locale, :locales)
self.config[:sort] = !config[:keep_order]
@base_locale = config[:base_locale]
locales = config[:locales].presence
@locales = LocaleList.normalize_locale_list(locales || available_locales, base_locale, true)
if locales.present?
log_verbose "locales read from config #{@locales * ', '}"
else
log_verbose "locales inferred from data: #{@locales * ', '}"
end
end
# @param [String, Symbol] locale
# @return [::I18n::Tasks::Data::Siblings]
def get(locale)
locale = locale.to_s
@trees ||= {}
@trees[locale] ||= read_locale(locale)
end
alias [] get
# @param [String, Symbol] locale
# @return [::I18n::Tasks::Data::Siblings]
def external(locale)
locale = locale.to_s
@external ||= {}
@external[locale] ||= read_locale(locale, paths: config[:external])
end
# set locale tree
# @param [String, Symbol] locale
# @param [::I18n::Tasks::Data::Siblings] tree
def set(locale, tree)
locale = locale.to_s
@trees&.delete(locale)
paths_before = Set.new(get(locale)[locale].leaves.map { |node| node.data[:path] })
paths_after = Set.new([])
router.route locale, tree do |path, tree_slice|
paths_after << path
write_tree path, tree_slice, config[:sort]
end
(paths_before - paths_after).each do |path|
FileUtils.remove_file(path) if File.exist?(path)
end
@trees&.delete(locale)
@available_locales = nil
end
alias []= set
# @param [String] locale
# @return [Array<String>] paths to files that are not normalized
def non_normalized_paths(locale)
router.route(locale, get(locale))
.reject { |path, tree_slice| normalized?(path, tree_slice) }
.map(&:first)
end
def write(forest)
forest.each { |root| set(root.key, root.to_siblings) }
end
def merge!(forest)
forest.inject(Tree::Siblings.new) do |result, root|
locale = root.key
merged = get(locale).merge(root)
set locale, merged
result.merge! merged
end
end
def remove_by_key!(forest)
forest.inject(Tree::Siblings.new) do |removed, root|
locale = root.key
locale_data = get(locale)
subtracted = locale_data.subtract_by_key(forest)
set locale, subtracted
removed.merge! locale_data.subtract_by_key(subtracted)
end
end
# @return self
def reload
@trees = nil
@available_locales = nil
self
end
# Get available locales from the list of file names to read
def available_locales
@available_locales ||= begin
locales = Set.new
Array(config[:read]).map do |pattern|
[pattern, Dir.glob(format(pattern, locale: '*'))] if pattern.include?('%{locale}')
end.compact.each do |pattern, paths|
p = pattern.gsub('\\', '\\\\').gsub('/', '\/').gsub('.', '\.')
p = p.gsub(/(\*+)/) { Regexp.last_match(1) == '**' ? '.*' : '[^/]*?' }.gsub('%{locale}', '([^/.]+)')
re = /\A#{p}\z/
paths.each do |path|
locales << Regexp.last_match(1) if re =~ path
end
end
locales
end
end
def t(key, locale)
tree = self[locale.to_s]
return unless tree
tree[locale][key].try(:value_or_children_hash)
end
def config=(config)
@config = DEFAULTS.deep_merge((config || {}).compact)
reload
end
def with_router(router)
router_was = self.router
self.router = router
yield
ensure
self.router = router_was
end
ROUTER_NAME_ALIASES = {
'conservative_router' => 'I18n::Tasks::Data::Router::ConservativeRouter',
'isolating_router' => 'I18n::Tasks::Data::Router::IsolatingRouter',
'pattern_router' => 'I18n::Tasks::Data::Router::PatternRouter'
}.freeze
def router
@router ||= begin
name = @config[:router].presence || 'conservative_router'
name = ROUTER_NAME_ALIASES[name] || name
router_class = ActiveSupport::Inflector.constantize(name)
router_class.new(self, @config.merge(base_locale: base_locale, locales: locales))
end
end
protected
def read_locale(locale, paths: config[:read])
Array(paths).flat_map do |path|
Dir.glob format(path, locale: locale)
end.map do |path|
[path.freeze, load_file(path) || {}]
end.map do |path, data|
if router.is_a?(I18n::Tasks::Data::Router::IsolatingRouter)
data.transform_values! { |tree| { "<#{router.alternate_path_for(path, base_locale)}>" => tree } }
end
filter_nil_keys! path, data
Data::Tree::Siblings.from_nested_hash(data).tap do |s|
s.leaves { |x| x.data.update(path: path, locale: locale) }
end
end.reduce(Tree::Siblings[locale => {}], :merge!)
end
def filter_nil_keys!(path, data, suffix = [])
data.each do |key, value|
if key.nil?
data.delete(key)
log_warn <<~TEXT
Skipping a nil key found in #{path.inspect}:
key: #{suffix.join('.')}.`nil`
value: #{value.inspect}
Nil keys are not supported by i18n.
The following unquoted YAML keys result in a nil key:
#{%w[null Null NULL ~].join(', ')}
See http://yaml.org/type/null.html
TEXT
elsif value.is_a?(Hash)
filter_nil_keys! path, value, suffix + [key]
end
end
end
end
end
end