i18n-hygiene 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []