i18n-hygiene 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f906fef080e1dbaa668f50b353fc861f709f1ca1
4
- data.tar.gz: c79a6e97927517a08ac025ac2d8570a481de6b74
3
+ metadata.gz: 8a0ed252e0cc76261202f3e8c8224a9d2ba89d82
4
+ data.tar.gz: 70f696d78272fc56810a9243d8e12471865ec54f
5
5
  SHA512:
6
- metadata.gz: 854a7ec8e067f52e39dd77a45f649af2a9f036208c25db7e81b0a34a786bcc8fea2e986aad6d3f32c7cc678f918aa3297f69a0aea090d605c752d96e11a21bff
7
- data.tar.gz: 50bc730b81a6fd89a9ea58547548f5148c96c8ed8118d780271edb07df8cd540fa8a4d4ff45b6adb3b4b7a10c5e32ce3c12da3f1c46fa7278db0754b82764548
6
+ metadata.gz: b8226fec6814080e9240c23e6ad77ced8a38e7e301385f40c9f6cc43f5b7c707cfc7447f0fded226b67957c49f0c03ba42700b1e9eb75e4624c26c214cfd24f2
7
+ data.tar.gz: a49070ce84895f31cb18ce08936643aa7fcfaea784a79b578654181141e910fc7a3de3d90cc3a3098311bc2d4fda29871dbb07db092aea08b7a3fc85b5b35f8c
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # i18n-hygiene [![Build Status](https://travis-ci.org/conversation/i18n-hygiene.svg?branch=master)](https://travis-ci.org/conversation/i18n-hygiene)
2
+
3
+ Provides a configurable rake task to help maintain your translations.
4
+
5
+ Over the lifetime of a project there tends to be a lot of churn in the translation files. Contexts and meanings will change, features will be added and removed. You'll find that soon enough, translations fall out of use, mistakes by developers or translators will creep in. The longer this goes on, the harder it is to keep them neat and tidy.
6
+
7
+ This tool is intended to be used as part of your continuous integration pipeline to keep your translations healthy and prevent issues from ever making it to production.
8
+
9
+ ## Usage
10
+
11
+ Include the gem in your gemfile and bundle:
12
+
13
+ `gem 'i18n-hygiene'`
14
+
15
+ ## Integrating with rake
16
+
17
+ Create a rake task with the desired configuration. For example, this will create a rake task called `i18n:hygiene`:
18
+
19
+ ```ruby
20
+ namespace :i18n do
21
+ I18n::Hygiene::RakeTask.new do |config|
22
+ config.directories = ["app", "lib"]
23
+ end
24
+ end
25
+
26
+ ```
27
+
28
+ You can then run the rake task with:
29
+ ```
30
+ bundle exec rake i18n:hygiene
31
+ ```
32
+
33
+ You could also create separate rake tasks with different names and configurations, this may be useful if you are in the middle of rolling out a new locale:
34
+ ```ruby
35
+ namespace :i18n do
36
+ I18n::Hygiene::RakeTask.new(:hygiene_live) do |config|
37
+ config.locales = [:fr]
38
+ end
39
+
40
+ I18n::Hygiene::RakeTask.new(:hygiene_wip) do |config|
41
+ config.locales = [:es]
42
+ end
43
+ end
44
+ ```
45
+
46
+ Which could be run like:
47
+
48
+ ```
49
+ bundle exec rake i18n:hygiene_live
50
+ bundle exec rake i18n:hygiene_wip
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ | Configuration | Default | Description |
56
+ |---|---|---|
57
+ | `concurrency` | Number of CPU cores | How many threads to use for key usage check |
58
+ | `directories` | All | Directories to search for key usage |
59
+ | `exclude_files` | None | Excludes files from key usage check |
60
+ | `exclude_keys` | None | Exclude individual keys |
61
+ | `exclude_scopes` | None | Exclude groups of keys |
62
+ | `file_extensions` | `rb, erb, coffee, js, jsx` | Only look in files with these extensions for key usage |
63
+ | `primary_locale` | `I18n.default_locale` | Translations from other locales are checked against this |
64
+ | `locales` | `I18n.available_locales` | Translations from these are checked against primary |
65
+
66
+ Example using all configuration options:
67
+
68
+ ```ruby
69
+ I18n::Hygiene::RakeTask.new do |config|
70
+ config.concurrency = 16
71
+ config.directories = ["app", "lib"]
72
+ config.exclude_files = ["README.md"]
73
+ config.file_extensions = ["rb", "jsx"]
74
+ config.primary_locale = :en
75
+ config.locales = [:ja, :kr]
76
+ config.exclude_keys = [
77
+ "my.dynamically.used.key",
78
+ "another.dynamically.used.key"
79
+ ]
80
+ config.exclude_scopes = [
81
+ "activerecord",
82
+ "countries"
83
+ ]
84
+ end
85
+
86
+ ```
87
+
88
+ #### Without Rails
89
+
90
+ Using this gem without Rails is possible, but you'll need to load the translations manually first.
91
+
92
+ ```ruby
93
+ namespace :i18n do
94
+ require 'i18n'
95
+ require 'i18n/hygiene'
96
+
97
+ I18n.load_path = Dir["locales/*.yml"]
98
+ I18n.backend.load_translations
99
+
100
+ I18n::Hygiene::RakeTask.new(:hygiene_live) do |config|
101
+ config.directories = ["src"]
102
+ config.locales = [:fr]
103
+ end
104
+ end
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
data/lib/i18n/hygiene.rb CHANGED
@@ -1,9 +1,6 @@
1
- require 'i18n/hygiene/railtie' if defined?(Rails)
2
1
  require 'i18n/hygiene/key_usage_checker'
3
- require 'i18n/hygiene/keys_with_entities'
4
2
  require 'i18n/hygiene/keys_with_matched_value'
5
- require 'i18n/hygiene/keys_with_return_symbol'
6
- require 'i18n/hygiene/keys_with_script_tags'
7
3
  require 'i18n/hygiene/locale_translations'
8
4
  require 'i18n/hygiene/variable_checker'
9
5
  require 'i18n/hygiene/wrapper'
6
+ require 'i18n/hygiene/rake_task'
@@ -0,0 +1,26 @@
1
+ module I18n
2
+ module Hygiene
3
+ module Checks
4
+ class Base
5
+ def initialize(config)
6
+ raise "Must pass an instance of Config" unless config.is_a?(I18n::Hygiene::Config)
7
+ @config = config
8
+ end
9
+
10
+ def run
11
+ raise "#run must be implemented by subclass"
12
+ end
13
+
14
+ protected
15
+
16
+ def config
17
+ @config
18
+ end
19
+
20
+ def all_locales
21
+ config.all_locales
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ require 'i18n/hygiene/checks/base'
2
+ require 'i18n/hygiene/keys_with_matched_value'
3
+ require 'i18n/hygiene/result'
4
+ require 'i18n/hygiene/wrapper'
5
+ require 'i18n/hygiene/error_message_builder'
6
+
7
+ module I18n
8
+ module Hygiene
9
+ module Checks
10
+ ##
11
+ # Looks for unexpected HTML entities (`&`, `!`) in translations.
12
+ class HtmlEntity < Base
13
+ ENTITY_REGEX = /&\w+;/
14
+
15
+ def run
16
+ wrapper = I18n::Hygiene::Wrapper.new(locales: all_locales, exclude_scopes: config.exclude_scopes)
17
+
18
+ keys_with_entities = I18n::Hygiene::KeysWithMatchedValue.new(ENTITY_REGEX, wrapper, reject_keys: reject_keys)
19
+
20
+ keys_with_entities.each do |locale, key|
21
+ message = ErrorMessageBuilder.new
22
+ .title("Unexpected HTML entity")
23
+ .locale(locale)
24
+ .key(key)
25
+ .translation(wrapper.value(locale, key))
26
+ .create
27
+
28
+ yield Result.new(:failure, message: message)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def reject_keys
35
+ Proc.new { |key| key.end_with?("_html") || key.end_with?("_markdown") }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,42 @@
1
+ require 'parallel'
2
+ require 'i18n/hygiene/wrapper'
3
+ require 'i18n/hygiene/checks/base'
4
+ require 'i18n/hygiene/key_usage_checker'
5
+ require 'i18n/hygiene/result'
6
+ require 'i18n/hygiene/error_message_builder'
7
+
8
+ module I18n
9
+ module Hygiene
10
+ module Checks
11
+ ##
12
+ # Ensures that existing translations are actually used.
13
+ class KeyUsage < Base
14
+ def run
15
+ key_usage_checker = I18n::Hygiene::KeyUsageChecker.new(
16
+ directories: config.directories,
17
+ exclude_files: config.exclude_files,
18
+ file_extensions: config.file_extensions
19
+ )
20
+
21
+ wrapper = I18n::Hygiene::Wrapper.new(
22
+ exclude_keys: config.exclude_keys,
23
+ exclude_scopes: config.exclude_scopes
24
+ )
25
+
26
+ Parallel.each(wrapper.keys_to_check(config.primary_locale), in_threads: config.concurrency) do |key|
27
+ if key_usage_checker.used?(key)
28
+ yield Result.new(:pass, message: ".")
29
+ else
30
+ message = ErrorMessageBuilder.new
31
+ .title("Unused translation")
32
+ .key(key)
33
+ .create
34
+
35
+ yield Result.new(:failure, message: message)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ require 'i18n/hygiene/wrapper'
2
+ require 'i18n/hygiene/checks/base'
3
+ require 'i18n/hygiene/variable_checker'
4
+ require 'i18n/hygiene/result'
5
+ require 'i18n/hygiene/error_message_builder'
6
+
7
+ module I18n
8
+ module Hygiene
9
+ module Checks
10
+ ##
11
+ # Looks for translations which are missing interpolation variables.
12
+ class MissingInterpolationVariable < Base
13
+ def run
14
+ wrapper = I18n::Hygiene::Wrapper.new(exclude_scopes: config.exclude_scopes)
15
+
16
+ wrapper.keys_to_check(config.primary_locale).select do |key|
17
+ checker = I18n::Hygiene::VariableChecker.new(key, wrapper, config.primary_locale, config.locales)
18
+
19
+ checker.mismatched_variables do |locale, key, missing_variables|
20
+ if missing_variables.any?
21
+ message = ErrorMessageBuilder.new
22
+ .title("Missing interpolation variable(s)")
23
+ .locale(locale)
24
+ .key(key)
25
+ .translation(wrapper.value(locale, key))
26
+ .expected(missing_variables.join(", "))
27
+ .create
28
+
29
+ yield Result.new(:failure, message: message)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ require 'i18n/hygiene/checks/base'
2
+ require 'i18n/hygiene/keys_with_matched_value'
3
+ require 'i18n/hygiene/result'
4
+ require 'i18n/hygiene/wrapper'
5
+ require 'i18n/hygiene/error_message_builder'
6
+
7
+ module I18n
8
+ module Hygiene
9
+ module Checks
10
+ ##
11
+ # Looks for unexpected script tags in translations.
12
+ class ScriptTag < Base
13
+ SCRIPT_TAG_REGEX = /<script.*/
14
+
15
+ def run
16
+ wrapper = I18n::Hygiene::Wrapper.new(locales: all_locales, exclude_scopes: config.exclude_scopes)
17
+
18
+ keys_with_script_tags = I18n::Hygiene::KeysWithMatchedValue.new(SCRIPT_TAG_REGEX, wrapper)
19
+
20
+ keys_with_script_tags.each do |locale, key|
21
+ message = ErrorMessageBuilder.new
22
+ .title("Unexpected script tag")
23
+ .locale(locale)
24
+ .key(key)
25
+ .translation(wrapper.value(locale, key))
26
+ .create
27
+
28
+ yield Result.new(:failure, message: message)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ require 'i18n/hygiene/checks/base'
2
+ require 'i18n/hygiene/keys_with_matched_value'
3
+ require 'i18n/hygiene/result'
4
+ require 'i18n/hygiene/error_message_builder'
5
+
6
+ module I18n
7
+ module Hygiene
8
+ module Checks
9
+ ##
10
+ # Looks for unexpected return symbols (U+23CE) in translations.
11
+ #
12
+ # This check is fairly specific to PhraseApp, where U+23CE has special meaning.
13
+ class UnexpectedReturnSymbol < Base
14
+ RETURN_SYMBOL_REGEX = /\u23ce/
15
+
16
+ def run
17
+ wrapper = I18n::Hygiene::Wrapper.new(locales: all_locales, exclude_scopes: config.exclude_scopes)
18
+ keys_with_return_symbols = I18n::Hygiene::KeysWithMatchedValue.new(RETURN_SYMBOL_REGEX, wrapper)
19
+
20
+ keys_with_return_symbols.each do |locale, key|
21
+ message = ErrorMessageBuilder.new
22
+ .title("Unexpected return symbol (U+23CE)")
23
+ .locale(locale)
24
+ .key(key)
25
+ .translation(wrapper.value(locale, key))
26
+ .create
27
+
28
+ yield Result.new(:failure, message: message)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,52 @@
1
+ require 'parallel'
2
+
3
+ module I18n
4
+ module Hygiene
5
+ class Config
6
+ attr_writer :exclude_files
7
+ attr_writer :directories
8
+ attr_writer :file_extensions
9
+ attr_writer :primary_locale
10
+ attr_writer :locales
11
+ attr_writer :exclude_keys
12
+ attr_writer :concurrency
13
+ attr_writer :exclude_scopes
14
+
15
+ def exclude_files
16
+ @exclude_files ||= []
17
+ end
18
+
19
+ def directories
20
+ @directories ||= []
21
+ end
22
+
23
+ def file_extensions
24
+ @file_extensions ||= ["rb", "erb", "coffee", "js", "jsx"]
25
+ end
26
+
27
+ def primary_locale
28
+ @primary_locale ||= ::I18n.default_locale
29
+ end
30
+
31
+ def locales
32
+ @locales ||= ::I18n.available_locales
33
+ end
34
+
35
+ def all_locales
36
+ [primary_locale] + locales
37
+ end
38
+
39
+ def exclude_keys
40
+ @exclude_keys ||= []
41
+ end
42
+
43
+ def concurrency
44
+ @concurrency || Parallel.processor_count
45
+ end
46
+
47
+ def exclude_scopes
48
+ @exclude_scopes ||= []
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,82 @@
1
+ require 'rainbow'
2
+
3
+ module I18n
4
+ module Hygiene
5
+ class ErrorMessageBuilder
6
+ LEFT_PAD = " " * 2
7
+ TRUNCATE_LIMIT = 48
8
+
9
+ def initialize
10
+ @title = "Unspecified Error"
11
+ @key = "unknown_key"
12
+ @locale = nil
13
+ @translation = nil
14
+ @location = nil
15
+ end
16
+
17
+ def title(title)
18
+ @title = title
19
+ self
20
+ end
21
+
22
+ def locale(locale)
23
+ @locale = locale
24
+ self
25
+ end
26
+
27
+ def key(key)
28
+ @key = key
29
+ self
30
+ end
31
+
32
+ def expected(expected)
33
+ @expected = expected
34
+ self
35
+ end
36
+
37
+ def translation(translation)
38
+ @translation = translation
39
+ self
40
+ end
41
+
42
+ def create
43
+ s = StringIO.new
44
+ s << "\n"
45
+ s << Rainbow("i18n-hygiene/#{@title}:").red
46
+ s << "\n"
47
+ s << LEFT_PAD
48
+
49
+ if @locale
50
+ s << "#{@locale}."
51
+ end
52
+
53
+ s << @key
54
+
55
+ if @translation
56
+ s << ": "
57
+ s << Rainbow("\"#{truncated_translation}\"").yellow
58
+ end
59
+
60
+ if @expected
61
+ s << "\n"
62
+ s << LEFT_PAD * 2
63
+ s << "Expected: "
64
+ s << Rainbow(@expected).color(:orange)
65
+ end
66
+
67
+ s << "\n"
68
+ s.string
69
+ end
70
+
71
+ private
72
+
73
+ def truncated_translation
74
+ if @translation.length > TRUNCATE_LIMIT
75
+ @translation[0..TRUNCATE_LIMIT] + "..."
76
+ else
77
+ @translation
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -1,50 +1,61 @@
1
1
  module I18n
2
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.
3
+ # Checks the usage of i18n keys in the codebase.
13
4
  class KeyUsageChecker
14
5
 
15
- attr_reader :key
6
+ def initialize(directories:, exclude_files: [], file_extensions: [])
7
+ @directories = directories
8
+ @exclude_files = exclude_files
9
+ @file_extensions = file_extensions
16
10
 
17
- def initialize(key)
18
- @key = key
11
+ raise "Must have git installed!" unless system("which git > /dev/null")
19
12
  end
20
13
 
21
- def used_in_codebase?
22
- fully_qualified_key_used?
14
+ def used?(key)
15
+ i18n_config_key?(key) || fully_qualified_key_used?(key)
23
16
  end
24
17
 
25
- def fully_qualified_key_used?(given_key = key)
26
- if pluralized_key_used?(given_key)
27
- fully_qualified_key_used?(without_last_part)
18
+ private
19
+
20
+ def fully_qualified_key_used?(key)
21
+ if pluralized_key_used?(key)
22
+ fully_qualified_key_used?(without_last_part(key))
28
23
  else
29
- %x<#{ag_or_ack} #{given_key} app lib | wc -l>.strip.to_i > 0
24
+ %x<git grep #{key} #{git_grep_options} | wc -l>.strip.to_i > 0
30
25
  end
31
26
  end
32
27
 
33
- private
28
+ def git_grep_options
29
+ [git_grep_include, git_grep_exclude].reject(&:empty?).join(" ")
30
+ end
34
31
 
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
32
+ def git_grep_include
33
+ @directories.map { |dir|
34
+ if @file_extensions.empty?
35
+ dir
36
+ else
37
+ @file_extensions.map { |ext|
38
+ "'#{dir}/*.#{ext}'"
39
+ }
40
+ end
41
+ }.flatten.join(" ")
42
+ end
43
+
44
+ def git_grep_exclude
45
+ @exclude_files.map { |file|
46
+ "':(exclude)*#{file}'"
47
+ }.join(" ")
48
+ end
49
+
50
+ def i18n_config_key?(key)
51
+ key.start_with?("i18n.")
41
52
  end
42
53
 
43
54
  def pluralized_key_used?(key)
44
55
  [ "zero", "one", "other" ].include?(key.split(".").last)
45
56
  end
46
57
 
47
- def without_last_part
58
+ def without_last_part(key)
48
59
  key.split(".")[0..-2].join(".")
49
60
  end
50
61
 
@@ -6,23 +6,20 @@ module I18n
6
6
 
7
7
  def initialize(regex, i18n_wrapper = nil, reject_keys: nil)
8
8
  @regex = regex
9
- @i18n = i18n_wrapper || I18n::Hygiene::Wrapper.new
9
+ @i18n = i18n_wrapper || I18n::Hygiene::Wrapper.new(exclude_keys: [])
10
10
  @reject_keys = reject_keys
11
- @matching_keys = load_matching_keys
12
11
  end
13
12
 
14
13
  def each(&block)
15
- @matching_keys.each(&block)
14
+ locales.each do |locale|
15
+ matching_keys(locale).each do |key|
16
+ block.call(locale, key)
17
+ end
18
+ end
16
19
  end
17
20
 
18
21
  private
19
22
 
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
23
  def matching_keys(locale)
27
24
  keys_to_check(locale).select { |key| i18n.value(locale, key).to_s.match(regex) }
28
25
  end
@@ -10,52 +10,30 @@ module I18n
10
10
  # These are i18n keys provided by Rails. We cannot exclude them at the :helpers
11
11
  # scope level because we do have some TC i18n keys scoped within :helpers.
12
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)
13
+ def initialize(translations:, exclude_keys:, exclude_scopes:)
22
14
  @translations = translations
15
+ @exclude_keys = exclude_keys || []
16
+ @exclude_scopes = exclude_scopes || []
23
17
  end
24
18
 
25
19
  def keys_to_check
26
20
  fully_qualified_keys(translations_to_check).reject { |key|
27
- KEYS_TO_SKIP.include?(key) || EXAMPLE_KEY == key
21
+ exclude_keys.include?(key) || EXAMPLE_KEY == key
28
22
  }.sort
29
23
  end
30
24
 
31
25
  private
32
26
 
33
27
  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 ]
28
+ @translations.reject { |k, _v| exclude_scopes.include? k }
51
29
  end
52
30
 
53
- def scopes_from_i18n_country_select
54
- [ :countries ]
31
+ def exclude_keys
32
+ @exclude_keys
55
33
  end
56
34
 
57
- def scopes_from_faker
58
- [ :faker ]
35
+ def exclude_scopes
36
+ @exclude_scopes
59
37
  end
60
38
 
61
39
  def fully_qualified_keys(hash)
@@ -0,0 +1,72 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+ require 'i18n/hygiene/config'
4
+ require 'i18n/hygiene/reporter'
5
+ require 'i18n/hygiene/checks/html_entity'
6
+ require 'i18n/hygiene/checks/key_usage'
7
+ require 'i18n/hygiene/checks/missing_interpolation_variable'
8
+ require 'i18n/hygiene/checks/script_tag'
9
+ require 'i18n/hygiene/checks/unexpected_return_symbol'
10
+
11
+ module I18n
12
+ module Hygiene
13
+ class RakeTask < ::Rake::TaskLib
14
+ CHECKS = [
15
+ I18n::Hygiene::Checks::KeyUsage,
16
+ I18n::Hygiene::Checks::MissingInterpolationVariable,
17
+ I18n::Hygiene::Checks::HtmlEntity,
18
+ I18n::Hygiene::Checks::ScriptTag,
19
+ I18n::Hygiene::Checks::UnexpectedReturnSymbol,
20
+ ]
21
+
22
+ def initialize(task_name = :hygiene, &block)
23
+ config = Config.new
24
+
25
+ if block
26
+ block.call(config)
27
+
28
+ # We always want to exclude the file that is configuring this rake task
29
+ config.exclude_files = config.exclude_files + [relative_path_for(block.source_location)]
30
+ end
31
+
32
+ unless ::Rake.application.last_description
33
+ desc %(Check i18n hygiene)
34
+ end
35
+
36
+ task(task_name => dependencies) do
37
+ checks = configure_checks(config)
38
+
39
+ checks.each do |check|
40
+ check.run do |result|
41
+ reporter.concat(result)
42
+ end
43
+ end
44
+
45
+ reporter.report
46
+
47
+ exit(1) unless reporter.passed?
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def relative_path_for(source_location)
54
+ source_location[0].gsub("#{pwd}/", "")
55
+ end
56
+
57
+ def reporter
58
+ @reporter ||= Reporter.new
59
+ end
60
+
61
+ def configure_checks(config)
62
+ CHECKS.map do |check|
63
+ check.new(config)
64
+ end
65
+ end
66
+
67
+ def dependencies
68
+ [:environment] if defined?(Rails)
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ require 'rainbow'
2
+
3
+ module I18n
4
+ module Hygiene
5
+ class Reporter
6
+ def concat(result)
7
+ print_progress(result)
8
+
9
+ results.push(result)
10
+ end
11
+
12
+ def results
13
+ @results ||= []
14
+ end
15
+
16
+ def passed?
17
+ results.all? { |result| result.passed? }
18
+ end
19
+
20
+ def report
21
+ if passed?
22
+ puts Rainbow("\ni18n hygiene checks passed.").green
23
+ else
24
+ puts Rainbow("\ni18n hygiene checks failed.").red
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def result_color(result)
31
+ if result.passed?
32
+ :green
33
+ else
34
+ :red
35
+ end
36
+ end
37
+
38
+ def print_progress(result)
39
+ print Rainbow(result.message).color(result_color(result))
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module I18n
2
+ module Hygiene
3
+ class Result
4
+ attr_reader :message
5
+
6
+ def initialize(status, message: "")
7
+ @status = status
8
+ @message = message
9
+ end
10
+
11
+ def passed?
12
+ case @status
13
+ when :pass
14
+ true
15
+ when :failure
16
+ false
17
+ else
18
+ raise "Unsupported status"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -4,55 +4,29 @@ module I18n
4
4
  # as defined in :en contains an interpolation variable, the value for that key as defined
5
5
  # in any other locale must have a matching variable name.
6
6
  class VariableChecker
7
-
8
- NON_ENGLISH_LOCALES_TO_CHECK = [ :fr_fr ]
9
-
10
- def initialize(key, i18n_wrapper)
7
+ def initialize(key, i18n_wrapper, primary_locale, locales = [])
11
8
  @key = key
12
9
  @i18n_wrapper = i18n_wrapper
10
+ @primary_locale = primary_locale
11
+ @locales = locales
13
12
  end
14
13
 
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
14
+ def mismatched_variables
15
+ @locales.each { |locale| yield locale, @key, missing_variables(locale) }
36
16
  end
37
17
 
38
18
  private
39
19
 
40
- def mismatch_details_for_locale(locale)
41
- "#{@key} for locale #{locale} is missing interpolation variable(s): #{missing_variables(locale)}"
42
- end
43
-
44
20
  def missing_variables(locale)
45
- variables(:en).reject { |v| variables(locale).include?(v) }.join(', ')
21
+ return [] unless key_defined?(locale)
22
+
23
+ variables(@primary_locale).reject { |v| variables(locale).include?(v) }
46
24
  end
47
25
 
48
26
  def key_defined?(locale)
49
27
  @i18n_wrapper.key_found?(locale, @key)
50
28
  end
51
29
 
52
- def variables_match?(locale)
53
- variables(locale) == variables(:en)
54
- end
55
-
56
30
  def variables(locale)
57
31
  collect_variables(@i18n_wrapper.value(locale, @key))
58
32
  end
@@ -1,3 +1,6 @@
1
+ require 'i18n'
2
+ require 'i18n/hygiene/locale_translations'
3
+
1
4
  module I18n
2
5
  module Hygiene
3
6
  # Utility class for interacting with i18n definitions. This is not intended to be used
@@ -5,12 +8,22 @@ module I18n
5
8
  # queryable.
6
9
  class Wrapper
7
10
 
8
- def keys_to_check(locale = :en)
9
- I18n::Hygiene::LocaleTranslations.new(translations[locale]).keys_to_check
11
+ def initialize(exclude_keys: [], exclude_scopes: [], locales: ::I18n.available_locales)
12
+ @locales = locales
13
+ @exclude_keys = exclude_keys
14
+ @exclude_scopes = exclude_scopes
15
+ end
16
+
17
+ def keys_to_check(locale)
18
+ I18n::Hygiene::LocaleTranslations.new(
19
+ translations: translations[locale],
20
+ exclude_keys: exclude_keys,
21
+ exclude_scopes: exclude_scopes
22
+ ).keys_to_check
10
23
  end
11
24
 
12
25
  def locales
13
- translations.keys
26
+ @locales
14
27
  end
15
28
 
16
29
  def key_found?(locale, key)
@@ -21,7 +34,7 @@ module I18n
21
34
 
22
35
  def value(locale, key)
23
36
  I18n.with_locale(locale) do
24
- I18n.t(key)
37
+ I18n.t(key, resolve: false)
25
38
  end
26
39
  end
27
40
 
@@ -36,6 +49,14 @@ module I18n
36
49
  ::I18n.backend.send(:init_translations)
37
50
  end
38
51
 
52
+ def exclude_keys
53
+ @exclude_keys
54
+ end
55
+
56
+ def exclude_scopes
57
+ @exclude_scopes
58
+ end
59
+
39
60
  end
40
61
  end
41
62
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: i18n-hygiene
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
+ - Eleanor Kiefel Haggerty
8
+ - Keith Pitty
7
9
  - Nick Browne
8
- - " Keith Pitty"
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2015-11-26 00:00:00.000000000 Z
13
+ date: 2017-07-26 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: i18n
@@ -45,6 +46,40 @@ dependencies:
45
46
  - - "~>"
46
47
  - !ruby/object:Gem::Version
47
48
  version: '1.3'
49
+ - !ruby/object:Gem::Dependency
50
+ name: rainbow
51
+ requirement: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 1.99.1
56
+ - - "<"
57
+ - !ruby/object:Gem::Version
58
+ version: '3.0'
59
+ type: :runtime
60
+ prerelease: false
61
+ version_requirements: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 1.99.1
66
+ - - "<"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.7
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.7
48
83
  - !ruby/object:Gem::Dependency
49
84
  name: rspec
50
85
  requirement: !ruby/object:Gem::Requirement
@@ -59,29 +94,57 @@ dependencies:
59
94
  - - "~>"
60
95
  - !ruby/object:Gem::Version
61
96
  version: '3.0'
62
- description: Provides rake tasks to help maintain translations.
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-nav
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Provides a configurable rake task that checks locale data for likely
112
+ issues. Intended to be used in build pipelines to detect problems before they reach
113
+ production
63
114
  email: dev@theconversation.edu.au
64
115
  executables: []
65
116
  extensions: []
66
- extra_rdoc_files: []
117
+ extra_rdoc_files:
118
+ - README.md
67
119
  files:
120
+ - README.md
68
121
  - lib/i18n/hygiene.rb
122
+ - lib/i18n/hygiene/checks/base.rb
123
+ - lib/i18n/hygiene/checks/html_entity.rb
124
+ - lib/i18n/hygiene/checks/key_usage.rb
125
+ - lib/i18n/hygiene/checks/missing_interpolation_variable.rb
126
+ - lib/i18n/hygiene/checks/script_tag.rb
127
+ - lib/i18n/hygiene/checks/unexpected_return_symbol.rb
128
+ - lib/i18n/hygiene/config.rb
129
+ - lib/i18n/hygiene/error_message_builder.rb
69
130
  - lib/i18n/hygiene/key_usage_checker.rb
70
- - lib/i18n/hygiene/keys_with_entities.rb
71
131
  - 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
132
  - lib/i18n/hygiene/locale_translations.rb
75
- - lib/i18n/hygiene/railtie.rb
133
+ - lib/i18n/hygiene/rake_task.rb
134
+ - lib/i18n/hygiene/reporter.rb
135
+ - lib/i18n/hygiene/result.rb
76
136
  - lib/i18n/hygiene/variable_checker.rb
77
137
  - lib/i18n/hygiene/wrapper.rb
78
- - lib/tasks/i18n_hygiene.rake
79
138
  homepage: https://github.com/conversation/i18n-hygiene
80
139
  licenses:
81
140
  - MIT
82
141
  metadata: {}
83
142
  post_install_message:
84
- rdoc_options: []
143
+ rdoc_options:
144
+ - "--title"
145
+ - i18n-hygiene documentation
146
+ - "--main"
147
+ - README.md
85
148
  require_paths:
86
149
  - lib
87
150
  required_ruby_version: !ruby/object:Gem::Requirement
@@ -96,8 +159,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
159
  version: '0'
97
160
  requirements: []
98
161
  rubyforge_project:
99
- rubygems_version: 2.4.5.1
162
+ rubygems_version: 2.5.2
100
163
  signing_key:
101
164
  specification_version: 4
102
- summary: Helps maintain translations.
165
+ summary: A linter for translation data in ruby applications
103
166
  test_files: []
@@ -1,29 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,9 +0,0 @@
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
@@ -1,87 +0,0 @@
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