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,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'easy_translate'
|
|
4
|
+
require 'i18n/processes/html_keys'
|
|
5
|
+
|
|
6
|
+
module I18n::Processes
|
|
7
|
+
module GoogleTranslation
|
|
8
|
+
# @param [I18n::Processes::Tree::Siblings] forest to translate to the locales of its root nodes
|
|
9
|
+
# @param [String] from locale
|
|
10
|
+
# @return [I18n::Processes::Tree::Siblings] translated forest
|
|
11
|
+
def google_translate_forest(forest, from)
|
|
12
|
+
forest.inject empty_forest do |result, root|
|
|
13
|
+
translated = google_translate_list(root.key_values(root: true), to: root.key, from: from)
|
|
14
|
+
result.merge! Data::Tree::Siblings.from_flat_pairs(translated)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param [Array<[String, Object]>] list of key-value pairs
|
|
19
|
+
# @return [Array<[String, Object]>] translated list
|
|
20
|
+
def google_translate_list(list, opts) # rubocop:disable Metrics/AbcSize
|
|
21
|
+
return [] if list.empty?
|
|
22
|
+
opts = opts.dup
|
|
23
|
+
opts[:key] ||= translation_config[:api_key]
|
|
24
|
+
validate_google_translate_api_key! opts[:key]
|
|
25
|
+
key_pos = list.each_with_index.inject({}) { |idx, ((k, _v), i)| idx.update(k => i) }
|
|
26
|
+
# copy reference keys as is, instead of translating
|
|
27
|
+
reference_key_vals = list.select { |_k, v| v.is_a? Symbol } || []
|
|
28
|
+
list -= reference_key_vals
|
|
29
|
+
result = list.group_by { |k_v| html_key? k_v[0], opts[:from] }.map do |is_html, list_slice|
|
|
30
|
+
fetch_google_translations list_slice, opts.merge(is_html ? { html: true } : { format: 'text' })
|
|
31
|
+
end.reduce(:+) || []
|
|
32
|
+
result.concat(reference_key_vals)
|
|
33
|
+
result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] }
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param [Array<[String, Object]>] list of key-value pairs
|
|
38
|
+
# @return [Array<[String, Object]>] translated list
|
|
39
|
+
def fetch_google_translations(list, opts)
|
|
40
|
+
from_values(list, EasyTranslate.translate(to_values(list), opts)).tap do |result|
|
|
41
|
+
fail CommandError, 'Google Translate returned no results. Make sure billing information is set at https://code.google.com/apis/console.' if result.blank?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def validate_google_translate_api_key!(key)
|
|
48
|
+
fail CommandError, 'Set Google API key via GOOGLE_TRANSLATE_API_KEY environment variable or translation.api_key in config/i18n-processes.yml. Get the key at https://code.google.com/apis/console.' if key.blank?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [Array<[String, Object]>] list of key-value pairs
|
|
52
|
+
# @return [Array<String>] values for translation extracted from list
|
|
53
|
+
def to_values(list)
|
|
54
|
+
list.map { |l| dump_value l[1] }.flatten.compact
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param [Array<[String, Object]>] list
|
|
58
|
+
# @param [Array<String>] translated_values
|
|
59
|
+
# @return [Array<[String, Object]>] translated key-value pairs
|
|
60
|
+
def from_values(list, translated_values)
|
|
61
|
+
keys = list.map(&:first)
|
|
62
|
+
untranslated_values = list.map(&:last)
|
|
63
|
+
keys.zip parse_value(untranslated_values, translated_values.to_enum)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Prepare value for translation.
|
|
67
|
+
# @return [String, Array<String, nil>, nil] value for Google Translate or nil for non-string values
|
|
68
|
+
def dump_value(value)
|
|
69
|
+
case value
|
|
70
|
+
when Array
|
|
71
|
+
# dump recursively
|
|
72
|
+
value.map { |v| dump_value v }
|
|
73
|
+
when String
|
|
74
|
+
replace_interpolations value
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parse translated value from the each_translated enumerator
|
|
79
|
+
# @param [Object] untranslated
|
|
80
|
+
# @param [Enumerator] each_translated
|
|
81
|
+
# @return [Object] final translated value
|
|
82
|
+
def parse_value(untranslated, each_translated)
|
|
83
|
+
case untranslated
|
|
84
|
+
when Array
|
|
85
|
+
# implode array
|
|
86
|
+
untranslated.map { |from| parse_value(from, each_translated) }
|
|
87
|
+
when String
|
|
88
|
+
restore_interpolations untranslated, each_translated.next
|
|
89
|
+
else
|
|
90
|
+
untranslated
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
INTERPOLATION_KEY_RE = /%\{[^}]+}/
|
|
95
|
+
UNTRANSLATABLE_STRING = 'zxzxzx'
|
|
96
|
+
|
|
97
|
+
# @param [String] value
|
|
98
|
+
# @return [String] 'hello, %{name}' => 'hello, <round-trippable string>'
|
|
99
|
+
def replace_interpolations(value)
|
|
100
|
+
i = -1
|
|
101
|
+
value.gsub INTERPOLATION_KEY_RE do
|
|
102
|
+
i += 1
|
|
103
|
+
"#{UNTRANSLATABLE_STRING}#{i}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @param [String] untranslated
|
|
108
|
+
# @param [String] translated
|
|
109
|
+
# @return [String] 'hello, <round-trippable string>' => 'hello, %{name}'
|
|
110
|
+
def restore_interpolations(untranslated, translated)
|
|
111
|
+
return translated if untranslated !~ INTERPOLATION_KEY_RE
|
|
112
|
+
values = untranslated.scan(INTERPOLATION_KEY_RE)
|
|
113
|
+
translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/i) do |m|
|
|
114
|
+
values[m[UNTRANSLATABLE_STRING.length..-1].to_i]
|
|
115
|
+
end
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
raise CommandError.new(e, <<-TEXT.strip)
|
|
118
|
+
Error when restoring interpolations:
|
|
119
|
+
original: "#{untranslated}"
|
|
120
|
+
response: "#{translated}"
|
|
121
|
+
error: #{e.message} (#{e.class.name})
|
|
122
|
+
TEXT
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module HtmlKeys
|
|
5
|
+
HTML_KEY_PATTERN = /[.\-_]html\z/
|
|
6
|
+
MAYBE_PLURAL_HTML_KEY_PATTERN = /[.\-_]html\.[^.]+\z/
|
|
7
|
+
|
|
8
|
+
def html_key?(full_key, locale)
|
|
9
|
+
# rubocop:disable Style/DoubleNegation
|
|
10
|
+
!!(full_key =~ HTML_KEY_PATTERN ||
|
|
11
|
+
full_key =~ MAYBE_PLURAL_HTML_KEY_PATTERN &&
|
|
12
|
+
depluralize_key(split_key(full_key, 2)[1], locale) =~ HTML_KEY_PATTERN)
|
|
13
|
+
# rubocop:enable Style/DoubleNegation
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes::IgnoreKeys
|
|
4
|
+
# whether to ignore the key
|
|
5
|
+
# will also apply global ignore rules
|
|
6
|
+
# @param [:missing, :unused, :eq_base] ignore_type
|
|
7
|
+
def ignore_key?(key, ignore_type, locale = nil)
|
|
8
|
+
key =~ ignore_pattern(ignore_type, locale)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @param type [nil, :missing, :unused, :eq_base] type
|
|
12
|
+
# @param locale [String] only when type is :eq_base
|
|
13
|
+
# @return [Regexp] a regexp that matches all the keys ignored for the type (and locale)
|
|
14
|
+
def ignore_pattern(type, locale = nil)
|
|
15
|
+
@ignore_patterns ||= HashWithIndifferentAccess.new
|
|
16
|
+
@ignore_patterns[type] ||= {}
|
|
17
|
+
@ignore_patterns[type][locale] ||= begin
|
|
18
|
+
global = ignore_config.presence || []
|
|
19
|
+
type_ignore = ignore_config(type).presence || []
|
|
20
|
+
patterns = if type_ignore.is_a?(Array)
|
|
21
|
+
global + type_ignore
|
|
22
|
+
elsif type_ignore.is_a?(Hash)
|
|
23
|
+
# ignore per locale
|
|
24
|
+
global + (type_ignore['all'] || []) +
|
|
25
|
+
type_ignore.select { |k, _v| k.to_s =~ /\b#{locale}\b/ }.values.flatten(1).compact
|
|
26
|
+
end
|
|
27
|
+
compile_patterns_re patterns
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'strscan'
|
|
4
|
+
|
|
5
|
+
module I18n::Processes::KeyPatternMatching
|
|
6
|
+
module_function # rubocop:disable Style/ModuleFunction
|
|
7
|
+
|
|
8
|
+
MATCH_NOTHING = /\z\A/
|
|
9
|
+
|
|
10
|
+
# one regex to match any
|
|
11
|
+
def compile_patterns_re(key_patterns)
|
|
12
|
+
if key_patterns.blank?
|
|
13
|
+
# match nothing
|
|
14
|
+
MATCH_NOTHING
|
|
15
|
+
else
|
|
16
|
+
/(?:#{ key_patterns.map { |p| compile_key_pattern p } * '|' })/m
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# convert pattern to regex
|
|
21
|
+
# In patterns:
|
|
22
|
+
# * is like .* in regexs
|
|
23
|
+
# : matches a single key
|
|
24
|
+
# { a, b.c } match any in set, can use : and *, match is captured
|
|
25
|
+
def compile_key_pattern(key_pattern)
|
|
26
|
+
return key_pattern if key_pattern.is_a?(Regexp)
|
|
27
|
+
/\A#{key_pattern_re_body(key_pattern)}\z/
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def key_pattern_re_body(key_pattern)
|
|
31
|
+
key_pattern
|
|
32
|
+
.gsub(/\./, '\.')
|
|
33
|
+
.gsub(/\*/, '.*')
|
|
34
|
+
.gsub(/:/, '(?<=^|\.)[^.]+?(?=\.|$)')
|
|
35
|
+
.gsub(/\{(.*?)}/) { "(#{Regexp.last_match(1).strip.gsub(/\s*,\s*/, '|')})" }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module LocaleList
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# @return locales converted to strings, with base locale first, the rest sorted alphabetically
|
|
8
|
+
def normalize_locale_list(locales, base_locale, include_base = false)
|
|
9
|
+
locales = Array(locales).map(&:to_s).sort
|
|
10
|
+
if locales.include?(base_locale)
|
|
11
|
+
[base_locale] + (locales - [base_locale])
|
|
12
|
+
elsif include_base
|
|
13
|
+
[base_locale] + locales
|
|
14
|
+
else
|
|
15
|
+
locales
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module LocalePathname
|
|
5
|
+
class << self
|
|
6
|
+
def replace_locale(path, from, to)
|
|
7
|
+
path && path.gsub(path_locale_re(from), to)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def path_locale_re(locale)
|
|
13
|
+
(@path_locale_res ||= {})[locale] ||= %r{(?<=^|[/.])#{locale}(?=[/.])}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes::Logging
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
MUTEX = Mutex.new
|
|
7
|
+
PROGRAM_NAME = File.basename($PROGRAM_NAME)
|
|
8
|
+
|
|
9
|
+
def warn_deprecated(message)
|
|
10
|
+
log_stderr Rainbow("#{program_name}: [DEPRECATED] #{message}").yellow.bright
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def log_verbose(message = nil)
|
|
14
|
+
log_stderr Rainbow(message || yield).blue.bright if ::I18n::Processes.verbose?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def log_warn(message)
|
|
18
|
+
log_stderr Rainbow("#{program_name}: [WARN] #{message}").yellow
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def log_error(message)
|
|
22
|
+
log_stderr Rainbow("#{program_name}: #{message}").red.bright
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def log_stderr(*args)
|
|
26
|
+
MUTEX.synchronize do
|
|
27
|
+
# 1. We don't want output from different threads to get intermixed.
|
|
28
|
+
# 2. StringIO is currently not thread-safe (blows up) on JRuby:
|
|
29
|
+
# https://github.com/jruby/jruby/issues/4417
|
|
30
|
+
$stderr.puts(*args)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def program_name
|
|
35
|
+
PROGRAM_NAME
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
module I18n::Processes
|
|
5
|
+
module MissingKeys
|
|
6
|
+
MISSING_TYPES = {
|
|
7
|
+
used: { glyph: '✗', summary: 'used in code but missing from base locale' },
|
|
8
|
+
diff: { glyph: '∅', summary: 'translated in one locale but not in the other' }
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def self.missing_keys_types
|
|
12
|
+
@missing_keys_types ||= MISSING_TYPES.keys
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def missing_keys_types
|
|
16
|
+
MissingKeys.missing_keys_types
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param types [:used, :diff] all if `nil`.
|
|
20
|
+
# @return [Siblings]
|
|
21
|
+
def missing_keys(locales: nil, types: nil, base_locale: nil)
|
|
22
|
+
locales ||= self.locales
|
|
23
|
+
types ||= missing_keys_types
|
|
24
|
+
base = base_locale || self.base_locale
|
|
25
|
+
types.inject(empty_forest) do |f, type|
|
|
26
|
+
f.merge! send(:"missing_#{type}_forest", locales, base)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def eq_base_keys(opts = {})
|
|
31
|
+
locales = Array(opts[:locales]).presence || self.locales
|
|
32
|
+
(locales - [base_locale]).inject(empty_forest) do |tree, locale|
|
|
33
|
+
tree.merge! equal_values_tree(locale, base_locale)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def missing_diff_forest(locales, base = base_locale)
|
|
38
|
+
tree = empty_forest
|
|
39
|
+
# present in base but not locale
|
|
40
|
+
(locales - [base]).each do |locale|
|
|
41
|
+
tree.merge! missing_diff_tree(locale, base)
|
|
42
|
+
end
|
|
43
|
+
if locales.include?(base)
|
|
44
|
+
# present in locale but not base
|
|
45
|
+
(self.locales - [base]).each do |locale|
|
|
46
|
+
tree.merge! missing_diff_tree(base, locale)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
tree
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def missing_used_forest(locales, _base = base_locale)
|
|
53
|
+
locales.inject(empty_forest) do |forest, locale|
|
|
54
|
+
forest.merge! missing_used_tree(locale)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# keys present in compared_to, but not in locale
|
|
59
|
+
def missing_diff_tree(locale, compared_to = base_locale)
|
|
60
|
+
data[compared_to].select_keys do |key, _node|
|
|
61
|
+
locale_key_missing? locale, depluralize_key(key, compared_to)
|
|
62
|
+
end.set_root_key!(locale, type: :missing_diff).keys do |_key, node|
|
|
63
|
+
# change path and locale to base
|
|
64
|
+
data = { locale: locale, missing_diff_locale: node.data[:locale] }
|
|
65
|
+
# $stderr.puts Rainbow("data: #{data}").green
|
|
66
|
+
if node.data.key?(:path)
|
|
67
|
+
data[:path] = LocalePathname.replace_locale(node.data[:path], node.data[:locale], locale)
|
|
68
|
+
end
|
|
69
|
+
# $stderr.puts Rainbow("data: #{node.data}").green
|
|
70
|
+
node.data.update data
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# keys used in the code missing translations in locale
|
|
75
|
+
def missing_used_tree(locale)
|
|
76
|
+
used_tree(strict: true).select_keys do |key, _node|
|
|
77
|
+
locale_key_missing?(locale, key)
|
|
78
|
+
end.set_root_key!(locale, type: :missing_used)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def equal_values_tree(locale, compare_to = base_locale)
|
|
82
|
+
base = data[compare_to].first.children
|
|
83
|
+
data[locale].select_keys(root: false) do |key, node|
|
|
84
|
+
other_node = base[key]
|
|
85
|
+
other_node && !node.reference? && node.value == other_node.value && !ignore_key?(key, :eq_base, locale)
|
|
86
|
+
end.set_root_key!(locale, type: :eq_base)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def locale_key_missing?(locale, key)
|
|
90
|
+
!key_value?(key, locale) && !external_key?(key) && !ignore_key?(key, :missing)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @param [::I18n::Processes::Data::Tree::Siblings] forest
|
|
94
|
+
# @yield [::I18n::Processes::Data::Tree::Node]
|
|
95
|
+
# @yieldreturn [Boolean] whether to collapse the node
|
|
96
|
+
def collapse_same_key_in_locales!(forest)
|
|
97
|
+
locales_and_node_by_key = {}
|
|
98
|
+
to_remove = []
|
|
99
|
+
forest.each do |root|
|
|
100
|
+
locale = root.key
|
|
101
|
+
root.keys do |key, node|
|
|
102
|
+
next unless yield node
|
|
103
|
+
if locales_and_node_by_key.key?(key)
|
|
104
|
+
locales_and_node_by_key[key][0] << locale
|
|
105
|
+
else
|
|
106
|
+
locales_and_node_by_key[key] = [[locale], node]
|
|
107
|
+
end
|
|
108
|
+
to_remove << node
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
forest.remove_nodes_and_emptied_ancestors! to_remove
|
|
112
|
+
locales_and_node_by_key.each_with_object({}) do |(key, (locales, node)), inv|
|
|
113
|
+
(inv[locales.sort.join('+')] ||= []) << [key, node]
|
|
114
|
+
end.map do |locales, keys_nodes|
|
|
115
|
+
keys_nodes.each do |(key, node)|
|
|
116
|
+
forest["#{locales}.#{key}"] = node
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
forest
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes:: Path
|
|
4
|
+
|
|
5
|
+
def origin_files(locale)
|
|
6
|
+
source = locale == base_locale ? source_path : translation_path[locale.to_sym]
|
|
7
|
+
[].tap do |file|
|
|
8
|
+
source.map do |path|
|
|
9
|
+
path = path[-1] == '/' ? path : path + '/'
|
|
10
|
+
group = Dir.glob("#{path}**/**")
|
|
11
|
+
file << group.reject { |x| File.directory?(x) }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def get_dic(path)
|
|
17
|
+
{}.tap do |dic|
|
|
18
|
+
File.open(path).each_line do |line|
|
|
19
|
+
key = line.split('=').first
|
|
20
|
+
value = line.split('=').last
|
|
21
|
+
dic[key] = value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def source_path
|
|
27
|
+
config_file[:data][:source]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def translation_path
|
|
31
|
+
config_file[:data][:translation]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def translated_path
|
|
35
|
+
config_file[:data][:translated]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def config_file
|
|
39
|
+
file = Dir.glob(File.join('**', '*.yml')).select{ |x| x.include?'i18n-processes' }
|
|
40
|
+
YAML.load_file(file.first).deep_symbolize_keys unless file.empty?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
module I18n::Processes::PluralKeys
|
|
5
|
+
PLURAL_KEY_SUFFIXES = Set.new %w[zero one two few many other]
|
|
6
|
+
PLURAL_KEY_RE = /\.(?:#{PLURAL_KEY_SUFFIXES.to_a * '|'})$/
|
|
7
|
+
|
|
8
|
+
def collapse_plural_nodes!(tree)
|
|
9
|
+
tree.leaves.map(&:parent).compact.uniq.each do |node|
|
|
10
|
+
children = node.children
|
|
11
|
+
next unless plural_forms?(children)
|
|
12
|
+
node.value = children.to_hash
|
|
13
|
+
node.children = nil
|
|
14
|
+
node.data.merge! children.first.data
|
|
15
|
+
end
|
|
16
|
+
tree
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param [String] key i18n key
|
|
20
|
+
# @param [String] locale to pull key data from
|
|
21
|
+
# @return [String] the base form if the key is a specific plural form (e.g. apple for apple.many), the key otherwise.
|
|
22
|
+
def depluralize_key(key, locale = base_locale)
|
|
23
|
+
return key if key !~ PLURAL_KEY_RE
|
|
24
|
+
key_name = last_key_part(key)
|
|
25
|
+
parent_key = key[0..- (key_name.length + 2)]
|
|
26
|
+
nodes = tree("#{locale}.#{parent_key}").presence || (locale != base_locale && tree("#{base_locale}.#{parent_key}"))
|
|
27
|
+
if nodes && plural_forms?(nodes)
|
|
28
|
+
parent_key
|
|
29
|
+
else
|
|
30
|
+
key
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def plural_forms?(s)
|
|
35
|
+
s.present? && s.all? { |node| node.leaf? && plural_suffix?(node.key) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def plural_suffix?(key)
|
|
39
|
+
PLURAL_KEY_SUFFIXES.include?(key)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module RainbowUtils
|
|
5
|
+
# TODO: This method can be removed after below PR is released.
|
|
6
|
+
# https://github.com/sickill/rainbow/pull/53
|
|
7
|
+
def self.faint_color(str)
|
|
8
|
+
presenter = Rainbow(str)
|
|
9
|
+
return presenter unless Rainbow.enabled
|
|
10
|
+
Rainbow::Presenter.new(Rainbow::StringUtils.wrap_with_sgr(presenter, [2]))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes
|
|
4
|
+
module References
|
|
5
|
+
# Given a raw usage tree and a tree of reference keys in the data, return 3 trees:
|
|
6
|
+
# 1. Raw references -- a subset of the usages tree with keys that are reference key usages.
|
|
7
|
+
# 2. Resolved references -- all the used references in their fully resolved form.
|
|
8
|
+
# 3. Reference keys -- all the used reference keys.
|
|
9
|
+
def process_references(usages,
|
|
10
|
+
data_refs = merge_reference_trees(data_forest.select_keys { |_, node| node.reference? }))
|
|
11
|
+
fail ArgumentError, 'usages must be a Data::Tree::Instance' unless usages.is_a?(Data::Tree::Siblings)
|
|
12
|
+
fail ArgumentError, 'all_references must be a Data::Tree::Instance' unless data_refs.is_a?(Data::Tree::Siblings)
|
|
13
|
+
raw_refs = empty_forest
|
|
14
|
+
resolved_refs = empty_forest
|
|
15
|
+
refs = empty_forest
|
|
16
|
+
data_refs.key_to_node.each do |ref_key_part, ref_node|
|
|
17
|
+
usages.each do |usage_node|
|
|
18
|
+
next unless usage_node.key == ref_key_part
|
|
19
|
+
if ref_node.leaf?
|
|
20
|
+
process_leaf!(ref_node, usage_node, raw_refs, resolved_refs, refs)
|
|
21
|
+
else
|
|
22
|
+
process_non_leaf!(ref_node, usage_node, raw_refs, resolved_refs, refs)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
[raw_refs, resolved_refs, refs]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @param [I18n::Processes::Data::Tree::Node] ref
|
|
32
|
+
# @param [I18n::Processes::Data::Tree::Node] usage
|
|
33
|
+
# @param [I18n::Processes::Data::Tree::Siblings] raw_refs
|
|
34
|
+
# @param [I18n::Processes::Data::Tree::Siblings] resolved_refs
|
|
35
|
+
# @param [I18n::Processes::Data::Tree::Siblings] refs
|
|
36
|
+
def process_leaf!(ref, usage, raw_refs, resolved_refs, refs)
|
|
37
|
+
refs.merge_node!(Data::Tree::Node.new(key: ref.key, data: usage.data)) unless refs.key_to_node.key?(ref.key)
|
|
38
|
+
new_resolved_refs = Data::Tree::Siblings.from_key_names([ref.value.to_s]) do |_, resolved_node|
|
|
39
|
+
raw_refs.merge_node!(usage)
|
|
40
|
+
if usage.leaf?
|
|
41
|
+
resolved_node.data.merge!(usage.data)
|
|
42
|
+
else
|
|
43
|
+
resolved_node.children = usage.children
|
|
44
|
+
end
|
|
45
|
+
resolved_node.leaves { |node| node.data[:ref_info] = [ref.full_key, ref.value.to_s] }
|
|
46
|
+
end
|
|
47
|
+
add_occurences! refs.key_to_node[ref.key].data, new_resolved_refs
|
|
48
|
+
resolved_refs.merge! new_resolved_refs
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param [Hash] ref_data
|
|
52
|
+
# @param [I18n::Processes::Data::Tree::Siblings] new_resolved_refs
|
|
53
|
+
def add_occurences!(ref_data, new_resolved_refs)
|
|
54
|
+
ref_data[:occurrences] ||= []
|
|
55
|
+
new_resolved_refs.leaves do |leaf|
|
|
56
|
+
ref_data[:occurrences].concat(leaf.data[:occurrences] || [])
|
|
57
|
+
end
|
|
58
|
+
ref_data[:occurrences].sort_by!(&:path)
|
|
59
|
+
ref_data[:occurrences].uniq!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param [I18n::Processes::Data::Tree::Node] ref
|
|
63
|
+
# @param [I18n::Processes::Data::Tree::Node] usage
|
|
64
|
+
# @param [I18n::Processes::Data::Tree::Siblings] raw_refs
|
|
65
|
+
# @param [I18n::Processes::Data::Tree::Siblings] resolved_refs
|
|
66
|
+
# @param [I18n::Processes::Data::Tree::Siblings] refs
|
|
67
|
+
def process_non_leaf!(ref, usage, raw_refs, resolved_refs, refs)
|
|
68
|
+
child_raw_refs, child_resolved_refs, child_refs = process_references(usage.children, ref.children)
|
|
69
|
+
raw_refs.merge_node! Data::Tree::Node.new(key: ref.key, children: child_raw_refs) unless child_raw_refs.empty?
|
|
70
|
+
resolved_refs.merge! child_resolved_refs
|
|
71
|
+
refs.merge_node! Data::Tree::Node.new(key: ref.key, children: child_refs) unless child_refs.empty?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Given a forest of references, merge trees into one tree, ensuring there are no conflicting references.
|
|
75
|
+
# @param roots [I18n::Processes::Data::Tree::Siblings]
|
|
76
|
+
# @return [I18n::Processes::Data::Tree::Siblings]
|
|
77
|
+
def merge_reference_trees(roots)
|
|
78
|
+
roots.inject(empty_forest) do |forest, root|
|
|
79
|
+
root.keys do |full_key, node|
|
|
80
|
+
if full_key == node.value.to_s
|
|
81
|
+
log_warn(
|
|
82
|
+
"Self-referencing key #{node.full_key(root: false).inspect} in #{node.data[:locale].inspect}"
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
forest.merge!(
|
|
87
|
+
root.children,
|
|
88
|
+
on_leaves_merge: lambda do |node, other|
|
|
89
|
+
if node.value != other.value
|
|
90
|
+
log_warn(
|
|
91
|
+
'Conflicting references: '\
|
|
92
|
+
"#{node.full_key(root: false)} ⮕ #{node.value} in #{node.data[:locale]},"\
|
|
93
|
+
" but ⮕ #{other.value} in #{other.data[:locale]}"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module I18n::Processes::Reports
|
|
4
|
+
class Base
|
|
5
|
+
include I18n::Processes::Logging
|
|
6
|
+
|
|
7
|
+
def initialize(task = I18n::Processes::BaseProcess.new)
|
|
8
|
+
@task = task
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :task
|
|
12
|
+
delegate :base_locale, :locales, to: :task
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def missing_type_info(type)
|
|
17
|
+
::I18n::Processes::MissingKeys::MISSING_TYPES[type.to_s.sub(/\Amissing_/, '').to_sym]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def missing_title(forest)
|
|
21
|
+
"Missing translations (#{forest.leaves.count || '∅'})"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def unused_title(key_values)
|
|
25
|
+
"All keys (#{key_values.count || '∅'})"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def eq_base_title(key_values, locale = base_locale)
|
|
29
|
+
"Same value as #{locale} (#{key_values.count || '∅'})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def used_title(keys_nodes, filter)
|
|
33
|
+
used_n = keys_nodes.map { |_k, node| node.data[:occurrences].size }.reduce(:+).to_i
|
|
34
|
+
"#{keys_nodes.size} key#{'s' if keys_nodes.size != 1}#{" matching '#{filter}'" if filter}"\
|
|
35
|
+
"#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sort keys by their attributes in order
|
|
39
|
+
# @param [Hash] order e.g. {locale: :asc, type: :desc, key: :asc}
|
|
40
|
+
def sort_by_attr!(objects, order = { locale: :asc, key: :asc })
|
|
41
|
+
order_keys = order.keys
|
|
42
|
+
objects.sort! do |a, b|
|
|
43
|
+
by = order_keys.detect { |k| a[k] != b[k] }
|
|
44
|
+
order[by] == :desc ? b[by] <=> a[by] : a[by] <=> b[by]
|
|
45
|
+
end
|
|
46
|
+
objects
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def forest_to_attr(forest)
|
|
50
|
+
forest.keys(root: false).map do |key, node|
|
|
51
|
+
{ key: key, value: node.value, type: node.data[:type], locale: node.root.key, data: node.data }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def format_locale(locale)
|
|
56
|
+
return '' unless locale
|
|
57
|
+
if locale.split('+') == task.locales.sort
|
|
58
|
+
'all'
|
|
59
|
+
else
|
|
60
|
+
locale.tr '+', ' '
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def collapse_missing_tree!(forest)
|
|
65
|
+
forest = task.collapse_plural_nodes!(forest)
|
|
66
|
+
forest = task.collapse_same_key_in_locales!(forest) { |node| node.data[:type] == :missing_used }
|
|
67
|
+
forest
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
end
|
|
71
|
+
end
|