i18n-hygiene 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f906fef080e1dbaa668f50b353fc861f709f1ca1
4
+ data.tar.gz: c79a6e97927517a08ac025ac2d8570a481de6b74
5
+ SHA512:
6
+ metadata.gz: 854a7ec8e067f52e39dd77a45f649af2a9f036208c25db7e81b0a34a786bcc8fea2e986aad6d3f32c7cc678f918aa3297f69a0aea090d605c752d96e11a21bff
7
+ data.tar.gz: 50bc730b81a6fd89a9ea58547548f5148c96c8ed8118d780271edb07df8cd540fa8a4d4ff45b6adb3b4b7a10c5e32ce3c12da3f1c46fa7278db0754b82764548
@@ -0,0 +1,9 @@
1
+ require 'i18n/hygiene/railtie' if defined?(Rails)
2
+ require 'i18n/hygiene/key_usage_checker'
3
+ require 'i18n/hygiene/keys_with_entities'
4
+ require 'i18n/hygiene/keys_with_matched_value'
5
+ require 'i18n/hygiene/keys_with_return_symbol'
6
+ require 'i18n/hygiene/keys_with_script_tags'
7
+ require 'i18n/hygiene/locale_translations'
8
+ require 'i18n/hygiene/variable_checker'
9
+ require 'i18n/hygiene/wrapper'
@@ -0,0 +1,54 @@
1
+ module I18n
2
+ module Hygiene
3
+ # Checks the usage of i18n keys in the TC codebase.
4
+ #
5
+ # TODO: This class needs some work, I had to strip out a bunch of functionality around
6
+ # the dynamically used keys because it was all specific to TC.
7
+ #
8
+ # We need to add a way to configure the hygiene checks to "whitelist" certain dynamic
9
+ # keys and so on.
10
+ #
11
+ # The other issue is that we hard code the folders we scan for fully qualified keys
12
+ # which should also be configurable.
13
+ class KeyUsageChecker
14
+
15
+ attr_reader :key
16
+
17
+ def initialize(key)
18
+ @key = key
19
+ end
20
+
21
+ def used_in_codebase?
22
+ fully_qualified_key_used?
23
+ end
24
+
25
+ def fully_qualified_key_used?(given_key = key)
26
+ if pluralized_key_used?(given_key)
27
+ fully_qualified_key_used?(without_last_part)
28
+ else
29
+ %x<#{ag_or_ack} #{given_key} app lib | wc -l>.strip.to_i > 0
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def ag_or_ack
36
+ if %x<which ag | wc -l>.strip.to_i == 1
37
+ return "ag"
38
+ else
39
+ return "ack --type-add=js=.coffee"
40
+ end
41
+ end
42
+
43
+ def pluralized_key_used?(key)
44
+ [ "zero", "one", "other" ].include?(key.split(".").last)
45
+ end
46
+
47
+ def without_last_part
48
+ key.split(".")[0..-2].join(".")
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ require 'enumerator'
2
+
3
+ module I18n
4
+ module Hygiene
5
+ # A collection of Strings that indicate the i18n keys in any locale that contain
6
+ # entities which are likely to be rendered incorrectly. Only keys ending in _html
7
+ # or _markdown may contain entities.
8
+ #
9
+ class KeysWithEntities
10
+ include Enumerable
11
+
12
+ ENTITY_REGEX = /&\w+;/
13
+
14
+ def initialize(i18nwrapper: nil)
15
+ @matcher = I18n::Hygiene::KeysWithMatchedValue.new(ENTITY_REGEX, i18nwrapper, reject_keys: reject_keys)
16
+ end
17
+
18
+ def each(&block)
19
+ @matcher.each(&block)
20
+ end
21
+
22
+ private
23
+
24
+ def reject_keys
25
+ Proc.new { |key| key.end_with?("_html") || key.end_with?("_markdown") }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,52 @@
1
+ module I18n
2
+ module Hygiene
3
+ # Checks to see if any i18n values match a given regex.
4
+ class KeysWithMatchedValue
5
+ include Enumerable
6
+
7
+ def initialize(regex, i18n_wrapper = nil, reject_keys: nil)
8
+ @regex = regex
9
+ @i18n = i18n_wrapper || I18n::Hygiene::Wrapper.new
10
+ @reject_keys = reject_keys
11
+ @matching_keys = load_matching_keys
12
+ end
13
+
14
+ def each(&block)
15
+ @matching_keys.each(&block)
16
+ end
17
+
18
+ private
19
+
20
+ def load_matching_keys
21
+ locales.inject([]) do |results, locale|
22
+ results + matching_keys(locale).map { |key| "#{locale}: #{key}" }
23
+ end
24
+ end
25
+
26
+ def matching_keys(locale)
27
+ keys_to_check(locale).select { |key| i18n.value(locale, key).to_s.match(regex) }
28
+ end
29
+
30
+ def regex
31
+ @regex
32
+ end
33
+
34
+ def keys_to_check(locale)
35
+ reject_keys ? i18n.keys_to_check(locale).reject(&reject_keys) : i18n.keys_to_check(locale)
36
+ end
37
+
38
+ def reject_keys
39
+ @reject_keys
40
+ end
41
+
42
+ def i18n
43
+ @i18n
44
+ end
45
+
46
+ def locales
47
+ i18n.locales
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,21 @@
1
+ require 'enumerator'
2
+
3
+ module I18n
4
+ module Hygiene
5
+ # Checks to see if any i18n values contain the U+23CE character.
6
+ # It has been included in PhraseApp translations but is unwanted.
7
+ class KeysWithReturnSymbol
8
+ include Enumerable
9
+
10
+ RETURN_SYMBOL_REGEX = /\u23ce/
11
+
12
+ def initialize(i18n_wrapper: nil)
13
+ @matcher = I18n::Hygiene::KeysWithMatchedValue.new(RETURN_SYMBOL_REGEX, i18n_wrapper)
14
+ end
15
+
16
+ def each(&block)
17
+ @matcher.each(&block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ require 'enumerator'
2
+
3
+ module I18n
4
+ module Hygiene
5
+ # Checks to see if any i18n values contain script tags.
6
+ # We should never allow script tags!
7
+ class KeysWithScriptTags
8
+ include Enumerable
9
+
10
+ SCRIPT_TAG_REGEX = /<script>.*<\/script>/
11
+
12
+ def initialize(i18n_wrapper: nil)
13
+ @matcher = I18n::Hygiene::KeysWithMatchedValue.new(SCRIPT_TAG_REGEX, i18n_wrapper)
14
+ end
15
+
16
+ def each(&block)
17
+ @matcher.each(&block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,74 @@
1
+ module I18n
2
+ module Hygiene
3
+ # Wrap all translations for a single locale, with knowledge of the keys that
4
+ # aren't in our control. Can return the i18n keys that **are** in our control,
5
+ # and therefore are interesting for a variety of reasons.
6
+ class LocaleTranslations
7
+ # This is a default example key in the locale files that is not actually used.
8
+ EXAMPLE_KEY = "common.greeting"
9
+
10
+ # These are i18n keys provided by Rails. We cannot exclude them at the :helpers
11
+ # scope level because we do have some TC i18n keys scoped within :helpers.
12
+
13
+ # TODO: make this configurable
14
+ KEYS_TO_SKIP = [
15
+ "helpers.select.prompt",
16
+ "helpers.submit.create",
17
+ "helpers.submit.submit",
18
+ "helpers.submit.update"
19
+ ]
20
+
21
+ def initialize(translations)
22
+ @translations = translations
23
+ end
24
+
25
+ def keys_to_check
26
+ fully_qualified_keys(translations_to_check).reject { |key|
27
+ KEYS_TO_SKIP.include?(key) || EXAMPLE_KEY == key
28
+ }.sort
29
+ end
30
+
31
+ private
32
+
33
+ def translations_to_check
34
+ @translations.reject { |k, _v| non_tc_scopes.include? k }
35
+ end
36
+
37
+ def non_tc_scopes
38
+ scopes_from_rails + scopes_from_devise + scopes_from_kaminari + scopes_from_i18n_country_select + scopes_from_faker
39
+ end
40
+
41
+ def scopes_from_rails
42
+ [ :activerecord, :date, :datetime, :errors, :number, :support, :time ]
43
+ end
44
+
45
+ def scopes_from_devise
46
+ [ :devise ]
47
+ end
48
+
49
+ def scopes_from_kaminari
50
+ [ :views ]
51
+ end
52
+
53
+ def scopes_from_i18n_country_select
54
+ [ :countries ]
55
+ end
56
+
57
+ def scopes_from_faker
58
+ [ :faker ]
59
+ end
60
+
61
+ def fully_qualified_keys(hash)
62
+ hash.inject([]) do |accum, (key, value)|
63
+ if value.is_a?(Hash)
64
+ accum + fully_qualified_keys(value).map do |sub_keys|
65
+ "#{key}.#{sub_keys}"
66
+ end
67
+ else
68
+ accum + [key.to_s]
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,9 @@
1
+ module I18n
2
+ module Hygiene
3
+ class Railtie < Rails::Railtie
4
+ rake_tasks do
5
+ load "tasks/i18n_hygiene.rake"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,80 @@
1
+ module I18n
2
+ module Hygiene
3
+ # Checks for mismatching interpolation variables. For example, if the value for an i18n key
4
+ # as defined in :en contains an interpolation variable, the value for that key as defined
5
+ # in any other locale must have a matching variable name.
6
+ class VariableChecker
7
+
8
+ NON_ENGLISH_LOCALES_TO_CHECK = [ :fr_fr ]
9
+
10
+ def initialize(key, i18n_wrapper)
11
+ @key = key
12
+ @i18n_wrapper = i18n_wrapper
13
+ end
14
+
15
+ def mismatched_variables_found?
16
+ NON_ENGLISH_LOCALES_TO_CHECK.each do |locale|
17
+ if key_defined?(locale)
18
+ return true unless variables_match?(locale)
19
+ end
20
+ end
21
+ false
22
+ end
23
+
24
+ def mismatch_details
25
+ if mismatched_variables_found?
26
+ details_array = []
27
+ NON_ENGLISH_LOCALES_TO_CHECK.each do |locale|
28
+ if key_defined?(locale)
29
+ details_array << mismatch_details_for_locale(locale) unless variables_match?(locale)
30
+ end
31
+ end
32
+ return details_array.join("\n")
33
+ else
34
+ return "#{@key}: no missing interpolation variables found."
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def mismatch_details_for_locale(locale)
41
+ "#{@key} for locale #{locale} is missing interpolation variable(s): #{missing_variables(locale)}"
42
+ end
43
+
44
+ def missing_variables(locale)
45
+ variables(:en).reject { |v| variables(locale).include?(v) }.join(', ')
46
+ end
47
+
48
+ def key_defined?(locale)
49
+ @i18n_wrapper.key_found?(locale, @key)
50
+ end
51
+
52
+ def variables_match?(locale)
53
+ variables(locale) == variables(:en)
54
+ end
55
+
56
+ def variables(locale)
57
+ collect_variables(@i18n_wrapper.value(locale, @key))
58
+ end
59
+
60
+ def collect_variables(string)
61
+ return [] unless string.is_a?(String)
62
+ (rails_variables(string) + js_variables(string)).uniq.sort
63
+ end
64
+
65
+ def rails_variables(string)
66
+ string.scan(/%{\S+}/).map { |var_string| var_string.gsub(/[%{}]/, '').to_sym }
67
+ end
68
+
69
+ def js_variables(string)
70
+ without_markdown_italics(string.scan(/__\S+__/).map { |var_string| var_string.gsub("__", "").to_sym })
71
+ end
72
+
73
+ def without_markdown_italics(array)
74
+ @key.end_with?("_markdown") ? [] : array
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,41 @@
1
+ module I18n
2
+ module Hygiene
3
+ # Utility class for interacting with i18n definitions. This is not intended to be used
4
+ # in production code - it's focus is on making the i18n data easily enumerable and
5
+ # queryable.
6
+ class Wrapper
7
+
8
+ def keys_to_check(locale = :en)
9
+ I18n::Hygiene::LocaleTranslations.new(translations[locale]).keys_to_check
10
+ end
11
+
12
+ def locales
13
+ translations.keys
14
+ end
15
+
16
+ def key_found?(locale, key)
17
+ I18n.with_locale(locale) do
18
+ I18n.exists?(key)
19
+ end
20
+ end
21
+
22
+ def value(locale, key)
23
+ I18n.with_locale(locale) do
24
+ I18n.t(key)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def translations
31
+ load_translations unless @translations
32
+ @translations ||= ::I18n.backend.send(:translations)
33
+ end
34
+
35
+ def load_translations
36
+ ::I18n.backend.send(:init_translations)
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ namespace :i18n do
2
+ namespace :hygiene do
3
+
4
+ desc 'run all the i18n hygiene checks'
5
+ task all: [:check_key_usage, :check_variables, :check_entities, :check_return_symbols, :check_script_tags]
6
+
7
+ desc "check usage of all EN keys"
8
+ task check_key_usage: :environment do
9
+ require 'parallel'
10
+
11
+ puts "Checking usage of EN keys..."
12
+ puts "(Please be patient while the codebase is searched for key usage)"
13
+
14
+ unused_keys = Parallel.map(I18n::Hygiene::Wrapper.new.keys_to_check) { |key|
15
+ key unless I18n::Hygiene::KeyUsageChecker.new(key).used_in_codebase?
16
+ }.compact
17
+
18
+ unused_keys.each do |key|
19
+ puts "#{key} is unused."
20
+ end
21
+
22
+ puts "Finished checking.\n\n"
23
+
24
+ exit(1) if unused_keys.any?
25
+ end
26
+
27
+ desc "check for mismatching interpolation variables"
28
+ task check_variables: :environment do
29
+ puts "Checking for mismatching interpolation variables..."
30
+
31
+ wrapper = I18n::Hygiene::Wrapper.new
32
+
33
+ mismatched_variables = wrapper.keys_to_check.select do |key|
34
+ checker = I18n::Hygiene::VariableChecker.new(key, wrapper)
35
+ checker.mismatch_details if checker.mismatched_variables_found?
36
+ end
37
+
38
+ mismatched_variables.each { |details| puts details }
39
+
40
+ puts "Finished checking.\n\n"
41
+
42
+ exit(1) if mismatched_variables.any?
43
+ end
44
+
45
+ desc "check for i18n phrases that contain entities"
46
+ task check_entities: :environment do
47
+ puts "Checking for phrases that contain entities but probably shouldn't..."
48
+
49
+ keys_with_entities = I18n::Hygiene::KeysWithEntities.new
50
+
51
+ keys_with_entities.each do |key|
52
+ puts "- #{key}"
53
+ end
54
+
55
+ puts "Finished checking.\n\n"
56
+
57
+ exit(1) if keys_with_entities.any?
58
+ end
59
+
60
+ desc "Check there are no values containing return symbols"
61
+ task check_return_symbols: :environment do
62
+ puts "Checking that no values contain return symbols i.e. U+23CE ..."
63
+
64
+ keys_with_return_symbols = I18n::Hygiene::KeysWithReturnSymbol.new
65
+
66
+ keys_with_return_symbols.each { |key| puts "- #{key}" }
67
+
68
+ puts "Finished checking.\n\n"
69
+
70
+ exit(1) if keys_with_return_symbols.any?
71
+ end
72
+
73
+ desc "Check there are no values containing scripts"
74
+ task check_script_tags: :environment do
75
+ puts "Checking that no values contain script tags ..."
76
+
77
+ keys_with_script_tags = I18n::Hygiene::KeysWithScriptTags.new
78
+
79
+ keys_with_script_tags.each { |key| puts " - #{key}" }
80
+
81
+ puts "Finished checking.\n\n"
82
+
83
+ exit(1) if keys_with_script_tags.any?
84
+ end
85
+
86
+ end
87
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: i18n-hygiene
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Browne
8
+ - " Keith Pitty"
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-11-26 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: i18n
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '0.6'
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 0.6.9
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '0.6'
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.9
34
+ - !ruby/object:Gem::Dependency
35
+ name: parallel
36
+ requirement: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ type: :runtime
42
+ prerelease: false
43
+ version_requirements: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ - !ruby/object:Gem::Dependency
49
+ name: rspec
50
+ requirement: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ description: Provides rake tasks to help maintain translations.
63
+ email: dev@theconversation.edu.au
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - lib/i18n/hygiene.rb
69
+ - lib/i18n/hygiene/key_usage_checker.rb
70
+ - lib/i18n/hygiene/keys_with_entities.rb
71
+ - lib/i18n/hygiene/keys_with_matched_value.rb
72
+ - lib/i18n/hygiene/keys_with_return_symbol.rb
73
+ - lib/i18n/hygiene/keys_with_script_tags.rb
74
+ - lib/i18n/hygiene/locale_translations.rb
75
+ - lib/i18n/hygiene/railtie.rb
76
+ - lib/i18n/hygiene/variable_checker.rb
77
+ - lib/i18n/hygiene/wrapper.rb
78
+ - lib/tasks/i18n_hygiene.rake
79
+ homepage: https://github.com/conversation/i18n-hygiene
80
+ licenses:
81
+ - MIT
82
+ metadata: {}
83
+ post_install_message:
84
+ rdoc_options: []
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubyforge_project:
99
+ rubygems_version: 2.4.5.1
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Helps maintain translations.
103
+ test_files: []