I18nLint
Lint your i18n for common problems, so you can be sure your copy doesn't get broken.
Installation
Install the gem and add to the application's Gemfile by executing:
bundle add i18nlint --git='https://github.com/henrahmagix/i18nlint' --ref='0.1.0'If bundler is not being used to manage dependencies, install the gem by executing:
git clone https://github.com/henrahmagix/i18nlint
cd i18nlint
gem build i18nlint.gemspec
gem install i18nlint i18nlint-0.1.0.gemUsage
Rails
bin/rails i18nlintThis task depends on the Rails task :environment, so your application will be loaded, including your I18n configuration, to detect your I18n.default_locale.
If there's a .i18nlint.yml config file in the root folder, it'll get loaded automatically.
CLI
As a command-line tool with arguments:
i18nlint --source=en --config=lint.yml config/locales/*.ymlRuby
As a Ruby script:
require "i18nlint"
class MyRule < I18nLint::Rule
# Define at least one method of :on_file, :on_segment, or :on_segment_comparison.
end
I18nLint.register_rule(MyRule)
offences = I18nLint.lint("path/to/locales/*.{yml,rb,json}", source_locale: "en") # array of offences
# Each offence has the `rule` that added it, the `filepath`, `lineno`, and `text` of the segment or file, and an optional `message` string.
# Segment offences also have `locale` and their full `key`.
# It may also contain a `highlight` array of match offsets pointing to the exact location of the offence in `text`.
# Also if `source_offence` is not nil, it holds the same attributes but about the segment of the same key defined under the source locale.
offences.each do |o|
puts "#{o.rule} offence at #{o.filepath}:#{o.lineno}"
puts "#{o.key} in locale: #{o.locale}" unless o.locale && o.key
puts "compared to #{o.source_offence.key} in source #{o.source_offence.locale}" if o.source_offence
puts o.text, o.message
endConfiguration
A YAML file named .i18nlint.yml is looked for automatically in the current directory and your home directory.
It can be used to add your own rules and configure them.
require:
- ./path/to/my/rule
- ./path/to/other/rule
BuiltIn/Interpolations:
Enabled: false # disable a built-in that always runs; all rules are enabled by default
MyRules/Amazing: # this matches a class named MyRules::Amazing, defined in any of the `require:` paths above
Enabled: true
Exclude: # every rule config can have a list of Pathname#fnmatch to exclude
- '*.exclude.yml'
match-segment:
# A list of `Pattern:` configurations to search for in each individual segment, where each match is an offence.
- Pattern: 'https://.*\.example.org'
- Pattern: 'https://.*\.bad.url'
Exclude:
- urls-*.yml
- Pattern: 'Word'
CaseSensitive: false # makes the Regexp made from Pattern case-insensitive with Regexp::IGNORECASE
match-file:
# Same as match-segment but passes in a whole file
- Pattern: something
mismatch-to-source:
# Same as match-segment but it matches on both translated segments and their
# source, and then compares the matches: if there are any that are in the
# source locale but not the translation, or vice versa, an offence is marked.
- Pattern: '\[[A-Z_]+\]' # some custom tags used in your renderer like `[NAME]`, `[EMAIL_ADDRESS]`, etc.Custom rules
You can define your own custom rules by defining classes extending I18nLint::Rule, and requiring them before I18nLint runs if you're using it in ruby, or linking them under require: in the configuration file if you're calling the binary. I18nLint will automatically register all of the subclasses of I18nLint::Rule that exist at runtime.
You must define at least one of on_file, on_segment, or on_segment_comparison instance methods. Your rule will get a noop definition for any that is missing.
-
on_filereceives aI18nLint::Filethat has:-
filepathstring -
parsedRuby value of the contents as loaded by I18n -
rawstring contents of the file.
-
-
on_segmentreceives aI18nLint::Segmentthat has:-
file(theI18nLint::Fileit's in) -
filepathdelegated tofile.filepath -
linenointeger of the start of the segment definition within the file -
keystring of the full segment that you would use inI18n.t() -
textstring content of the segment - if
textis nil,valuewill be the Ruby value of the segment (it could be an array... if you're putting arrays in your I18n 🤷) -
localesymbol of the segment (or string if i18n gem is < 1.9.1) -
source_localestring of the project source locale -
source?true if the locale is the project source
-
-
on_segment_comparisonreceives twoI18nLint::Segments: the first is a translation segment, the second is the same segment in the source locale.
To register an offence, use:
- for
on_filecalladd_file_offence(file, msg = nil, lineno: nil, source: nil, highlight: nil)where:-
fileis the input argument toon_file -
msgwill be displayed alongside the offence -
linenois helpful to pass if the issue is isolated to or starts at a certain line in the file -
sourceshould be the string on which the offence occurred, if any -
highlightcan be passed as an array of tuples to indicate a range of the file content; if you pass this, you probably don't need to pass thesource
-
- for
on_segmentcalladd_segment_offence(segment, msg = nil, highlight: nil)where:-
segmentis the input argument toon_segment -
msgandhighlightare the same as above
-
- for
on_segment_comparisoncalladd_segment_compare_offence(segment, source_segment, msg = nil, src_msg = nil, highlight: nil, source_highlight: nil)where:-
segmentandsource_segmentare the input arguments toon_segment_comparison -
msgandhighlightare the same as above -
src_msgandsource_highlightare the same but attached to the source segment
-
If you're using Regexp in your rule, more often than not you can pass highlight: Regexp.last_match.offset(0) to the add-offence method.
There is a singleton method on_init available to add a block that gets evaluated on the rule instance after initialisation, so you can do some one-time setup of configuration values, available as config.
A rule can define a description instance or class method to describe the rule. Without it, the class name will be used with :: replaced by /.
A message can be passed in per offence, or it will be taken automatically from a Message property on the rule configuration.
Example
# lib/linters/i18n.rb
module MyI18nRules # the module namespace doesn't matter and is entirely optional
class DontAllowHelloAndGoodbyeInSameSentence < I18nLint::Rule
def on_segment(segment)
segment.text.downcase.scan(/[^.?!]*?[.?!]\s*/) do |sentence|
if sentence.include?('hello') && sentence.include?('goodbye')
add_segment_offence(segment, 'Thou Shalt Not greet and leave in the same sentence', highlight: Regexp.last_match.offset(0))
end
end
end
end
end# content.yml
en:
welcome: "One two three. Hello and goodbye! One more finally."
fr:
welcome: "Un deux trois. Bonjour et au revoir! Encore."# .i18nlint.yml
require:
- ./lib/linters/i18n.rb$ i18nlint --source=en content.yml
Inspecting 1 files
F.
Comparing segments to source EN
.
Offences:
content.yml:2 in en.welcome: MyI18nRules/DontAllowHelloAndGoodbyeInSameSentence: Thou Shalt Not greet and leave in the same sentence
One two three. Hello and goodbye! One more finally.
^^^^^^^^^^^^^^^^^^^Development
After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec appraisal rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
bundle exec rake also runs an example of the CLI tool, and RuboCop.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/henrahmagix/i18nlint. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the I18nLint project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.