Instrument calls to I18n.t in Ruby and JavaScript in your Rails app.
Add it to your Gemfile:
gem 'i18n-instrument', require: 'i18n/instrument'
Platforms like iOS and Android make it easy to tell which of your localization strings are currently in use - just do a bit of grepping or run a script and voilà! Once you've identified them, you can remove any unused, crufty strings from your translation files and move on with your life.
Due to Ruby's dynamic nature and the fact that you can never tell what Rails is actually doing, identifying unused strings is much more difficult. The static analysis that worked so well with iOS and Android projects won't work for your Rails app.
Consider this ERB template. It lives in app/views/products/index.html.erb:
<div class="description">
<%= I18n.t('.description') %>: <%= @product.description %>
</div>
Under the hood, the i18n gem (which provides Rails' localization implementation) fully qualifies .description
into products.index.description
. Because this qualification is done at runtime, static analysis (i.e. grepping) won't be able to identify all the strings your app is currently using.
The problem is compounded by the fact that the key you pass to I18n.t
is just a string, and can therefore be generated in any way the Ruby language allows, for example:
<div class="fields">
<% @product.attributes.each_pair do |attr, value|
<div class="<%= attr %>">
<%= I18n.t(".#{attr}") %>: <%= value %>
</div>
<% end %>
</div>
First of all, I sincerely hope you never write code like this - it's probably bad practice to loop over all the attributes in your model and print them out (security blah blah blah). Hopefully my example illustrates the problem however - namely that the I18n.t
method can accept any string, including string variables, interpolated strings, and the rest. Unless your static analyzer is very clever, it won't be able to tell you which of your localization strings are currently being used.
So glad you asked.
The only foolproof way to collect information about the localization strings your app is using is during runtime. This gem, i18n-instrument, is capable of annotating calls to I18n.t
in Ruby/Rails and Javascript (provided you're using the capable i18n-js gem). Whenever one of these methods is called, i18n-instrument will fire the on_record
callback and pass you some useful information. From there, the possibilities are endless. You could log the information to the console, write it down somewhere useful, save it to a database, you name it.
-
on_lookup(key : String, value : String)
: Fired wheneverI18n.t
is called. Yields the localization key (i.e.es.products.index.description
) and the corresponding translation string. -
on_record(params : Hash)
: Similar toon_lookup
but comes with a bunch of useful information.The
params
hash contains the following keys (all values are strings):url
: the request URL.trace
: the filename and line number from the first application stack frame, i.e. the place in your codeI18n.t
was called.key
: the localization key.source
: either "ruby" or "javascript".locale
: the value ofI18n.locale
in Ruby or Javascript.
-
on_error(e : Exception)
: Fired whenever an error occurs. The default behavior is to re-raise the exception. You may want to add your own exception handling callback so your app doesn't crash. See the configuration section below for details. -
on_check_enabled() : Boolean
: Fired on every lookup. The return value must betrue
if the lookup should be recorded andfalse
otherwise. The default behavior is to look for the existence of a file named config/enable_i18n_instrumentation. If the file exists,I18n.t
calls are recorded. If not, calls are not recorded.
i18n-instrument is a piece of Rack middleware. It sits in between the Internet and your Rails app. You can configure it any time before your app is finished booting. I'd suggest doing it in a Rails initializer, for example config/initializers/i18n_instrument.rb:
I18n::Instrument.configure do |config|
# The first stack trace line that begins with this string will be passed to
# `on_record` as the `trace` value. Defaults to `Rails.root`.
config.stack_trace_prefix = 'custom/path/to/app'
# The URL you want your app to send js instrumentation requests to. Defaults
# to '/i18n/instrument.json'.
config.js_endpoint = '/my_i18n/foo.json'
# all of the callback methods are available here
config.on_record do |params|
# print params to the rails log
Rails.logger.info(params.inspect)
end
config.on_lookup do |key, value|
puts "Looked up i18n key #{key} and got value #{value}"
end
config.on_error do |e|
# report errors to our error aggregation service
Rollbar.error(e)
end
config.on_check_enabled do
# always enable
true
end
end
Be careful not to call I18n::Instrument.configure
more than once - doing so will replace any existing configuration you may have already done. Instead, try this:
# replace any existing on_record behavior, but leave all other configuration intact
I18n::Instrument.config.on_record do |params|
...
end
Using i18n-instrument in Javascript is pretty straightforward - just include it in your application.js file:
//= require i18n
//= require i18n/instrument
Make sure to include i18n/instrument after i18n. This is critical since i18n-instrument overrides i18n's t
method.
Also, don't forget to enable Javascript instrumentation by adding this line, probably at the bottom of application.js:
I18n.instrumentation_enabled = true
Alternatively, you could enable or disable Javascript instrumentation per request by adding something like this to your application layout:
<% if I18n::Instrument.config.enabled? %>
<script src="javascript">
I18n.instrumentation_enabled = true
</script>
<% end %>
i18n-instrument is designed to work with Rails 4 and up, although it has not been tested under Rails 5 yet.
Run the test suite with bundle exec rspec
.
- Cameron C. Dutro (@camertron) on behalf of Lumos Labs, Inc.
Licensed under the MIT license.