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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile.lock +102 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +46 -0
  5. data/Rakefile +12 -0
  6. data/bin/i18n-processes +28 -0
  7. data/bin/i18n-processes.cmd +2 -0
  8. data/config/locales/en.yml +2 -0
  9. data/config/locales/zh-CN.yml +2 -0
  10. data/i18n-processes.gemspec +64 -0
  11. data/lib/i18n/processes/base_process.rb +47 -0
  12. data/lib/i18n/processes/cli.rb +208 -0
  13. data/lib/i18n/processes/command/collection.rb +21 -0
  14. data/lib/i18n/processes/command/commander.rb +43 -0
  15. data/lib/i18n/processes/command/commands/data.rb +107 -0
  16. data/lib/i18n/processes/command/commands/eq_base.rb +21 -0
  17. data/lib/i18n/processes/command/commands/health.rb +26 -0
  18. data/lib/i18n/processes/command/commands/meta.rb +38 -0
  19. data/lib/i18n/processes/command/commands/missing.rb +86 -0
  20. data/lib/i18n/processes/command/commands/preprocessing.rb +90 -0
  21. data/lib/i18n/processes/command/commands/tree.rb +119 -0
  22. data/lib/i18n/processes/command/commands/usages.rb +69 -0
  23. data/lib/i18n/processes/command/commands/xlsx.rb +29 -0
  24. data/lib/i18n/processes/command/dsl.rb +56 -0
  25. data/lib/i18n/processes/command/option_parsers/enum.rb +55 -0
  26. data/lib/i18n/processes/command/option_parsers/locale.rb +60 -0
  27. data/lib/i18n/processes/command/options/common.rb +41 -0
  28. data/lib/i18n/processes/command/options/data.rb +95 -0
  29. data/lib/i18n/processes/command/options/locales.rb +36 -0
  30. data/lib/i18n/processes/command_error.rb +13 -0
  31. data/lib/i18n/processes/commands.rb +31 -0
  32. data/lib/i18n/processes/configuration.rb +132 -0
  33. data/lib/i18n/processes/console_context.rb +76 -0
  34. data/lib/i18n/processes/data/adapter/json_adapter.rb +29 -0
  35. data/lib/i18n/processes/data/adapter/yaml_adapter.rb +27 -0
  36. data/lib/i18n/processes/data/file_formats.rb +111 -0
  37. data/lib/i18n/processes/data/file_system.rb +14 -0
  38. data/lib/i18n/processes/data/file_system_base.rb +205 -0
  39. data/lib/i18n/processes/data/router/conservative_router.rb +66 -0
  40. data/lib/i18n/processes/data/router/pattern_router.rb +60 -0
  41. data/lib/i18n/processes/data/tree/node.rb +204 -0
  42. data/lib/i18n/processes/data/tree/nodes.rb +97 -0
  43. data/lib/i18n/processes/data/tree/siblings.rb +333 -0
  44. data/lib/i18n/processes/data/tree/traversal.rb +190 -0
  45. data/lib/i18n/processes/data.rb +87 -0
  46. data/lib/i18n/processes/google_translation.rb +125 -0
  47. data/lib/i18n/processes/html_keys.rb +16 -0
  48. data/lib/i18n/processes/ignore_keys.rb +30 -0
  49. data/lib/i18n/processes/key_pattern_matching.rb +37 -0
  50. data/lib/i18n/processes/locale_list.rb +19 -0
  51. data/lib/i18n/processes/locale_pathname.rb +17 -0
  52. data/lib/i18n/processes/logging.rb +37 -0
  53. data/lib/i18n/processes/missing_keys.rb +122 -0
  54. data/lib/i18n/processes/path.rb +42 -0
  55. data/lib/i18n/processes/plural_keys.rb +41 -0
  56. data/lib/i18n/processes/rainbow_utils.rb +13 -0
  57. data/lib/i18n/processes/references.rb +101 -0
  58. data/lib/i18n/processes/reports/base.rb +71 -0
  59. data/lib/i18n/processes/reports/spreadsheet.rb +72 -0
  60. data/lib/i18n/processes/reports/terminal.rb +252 -0
  61. data/lib/i18n/processes/scanners/file_scanner.rb +65 -0
  62. data/lib/i18n/processes/scanners/files/caching_file_finder.rb +34 -0
  63. data/lib/i18n/processes/scanners/files/caching_file_finder_provider.rb +33 -0
  64. data/lib/i18n/processes/scanners/files/caching_file_reader.rb +28 -0
  65. data/lib/i18n/processes/scanners/files/file_finder.rb +60 -0
  66. data/lib/i18n/processes/scanners/files/file_reader.rb +19 -0
  67. data/lib/i18n/processes/scanners/occurrence_from_position.rb +27 -0
  68. data/lib/i18n/processes/scanners/pattern_mapper.rb +60 -0
  69. data/lib/i18n/processes/scanners/pattern_scanner.rb +103 -0
  70. data/lib/i18n/processes/scanners/pattern_with_scope_scanner.rb +98 -0
  71. data/lib/i18n/processes/scanners/relative_keys.rb +53 -0
  72. data/lib/i18n/processes/scanners/results/key_occurrences.rb +54 -0
  73. data/lib/i18n/processes/scanners/results/occurrence.rb +69 -0
  74. data/lib/i18n/processes/scanners/ruby_ast_call_finder.rb +62 -0
  75. data/lib/i18n/processes/scanners/ruby_ast_scanner.rb +206 -0
  76. data/lib/i18n/processes/scanners/ruby_key_literals.rb +30 -0
  77. data/lib/i18n/processes/scanners/scanner.rb +17 -0
  78. data/lib/i18n/processes/scanners/scanner_multiplexer.rb +41 -0
  79. data/lib/i18n/processes/split_key.rb +68 -0
  80. data/lib/i18n/processes/stats.rb +24 -0
  81. data/lib/i18n/processes/string_interpolation.rb +16 -0
  82. data/lib/i18n/processes/unused_keys.rb +23 -0
  83. data/lib/i18n/processes/used_keys.rb +177 -0
  84. data/lib/i18n/processes/version.rb +7 -0
  85. data/lib/i18n/processes.rb +69 -0
  86. data/source/p1/_messages/zh/article.properties +9 -0
  87. data/source/p1/_messages/zh/company.properties +62 -0
  88. data/source/p1/_messages/zh/devices.properties +40 -0
  89. data/source/p1/_messages/zh/meeting-rooms.properties +99 -0
  90. data/source/p1/_messages/zh/meetingBooking.properties +18 -0
  91. data/source/p1/_messages/zh/office-areas.properties +64 -0
  92. data/source/p1/_messages/zh/orders.properties +25 -0
  93. data/source/p1/_messages/zh/schedulings.properties +7 -0
  94. data/source/p1/_messages/zh/tag.properties +2 -0
  95. data/source/p1/_messages/zh/ticket.properties +9 -0
  96. data/source/p1/_messages/zh/visitor.properties +5 -0
  97. data/source/p1/messages +586 -0
  98. data/source/p2/orders.properties +25 -0
  99. data/source/p2/schedulings.properties +7 -0
  100. data/source/p2/tag.properties +2 -0
  101. data/source/p2/ticket.properties +9 -0
  102. data/source/p2/visitor.properties +5 -0
  103. data/source/zh.messages.ts +30 -0
  104. data/translated/en/p1/_messages/zh/article.properties +9 -0
  105. data/translated/en/p1/_messages/zh/company.properties +62 -0
  106. data/translated/en/p1/_messages/zh/devices.properties +40 -0
  107. data/translated/en/p1/_messages/zh/meeting-rooms.properties +99 -0
  108. data/translated/en/p1/_messages/zh/meetingBooking.properties +18 -0
  109. data/translated/en/p1/_messages/zh/office-areas.properties +64 -0
  110. data/translated/en/p1/_messages/zh/orders.properties +25 -0
  111. data/translated/en/p1/_messages/zh/schedulings.properties +7 -0
  112. data/translated/en/p1/_messages/zh/tag.properties +2 -0
  113. data/translated/en/p1/_messages/zh/ticket.properties +9 -0
  114. data/translated/en/p1/_messages/zh/visitor.properties +5 -0
  115. data/translated/en/p1/messages +586 -0
  116. data/translated/en/p2/orders.properties +25 -0
  117. data/translated/en/p2/schedulings.properties +7 -0
  118. data/translated/en/p2/tag.properties +2 -0
  119. data/translated/en/p2/ticket.properties +9 -0
  120. data/translated/en/p2/visitor.properties +5 -0
  121. data/translated/en/zh.messages.ts +30 -0
  122. data/translation/en/article.properties +9 -0
  123. data/translation/en/company.properties +56 -0
  124. data/translation/en/meeting-rooms.properties +87 -0
  125. data/translation/en/meetingBooking.properties +14 -0
  126. data/translation/en/messages.en +164 -0
  127. data/translation/en/office-areas.properties +51 -0
  128. data/translation/en/orders.properties +26 -0
  129. data/translation/en/tag.properties +2 -0
  130. data/translation/en/translated +1263 -0
  131. data/translation/en/visitor.properties +4 -0
  132. metadata +408 -0
@@ -0,0 +1,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