5 March 2023

Identify I18n translation keys

Looking up and updating I18n keys can be time consuming, but with the help of a little module we can make this task easier. As a bonus, this module will also help us identify untranslated bits on our pages.

The module

Here is the module. For Rails apps, create an initializer file config/initializers/i18n.rb and add this code. Other Ruby frameworks should be able to use the module by switching the logger during initialization and remove the active support exception case.

# config/initializers/i18n.rb

module TranslationKeys
  def translate(key, throw: false, raise: false, locale: nil, **options)
    # Log key and default translations
    Rails.logger.info([key, locale, options])

    # Exceptions
    result = super
    return result if result.is_a?(Hash) # ignore hashes from active support format
    return result if options[:scope].to_s == "simple_form.options" # ignore simple_form collection options

    # Display translation  key
    scope = key.to_s
    if options[:scope]
      scope = (Array(options[:scope]) << scope).join('.')
    end
    scope
  end
  # Override aliases and similar methods.
  alias :t :translate
  alias :transliterate :translate
end

I18n.extend(TranslationKeys)

Note that we're also aliasing #t and #transliterate in the process before extending I18n library. Finally, this module will only display keys for server rendered pages. See below for a front-end equivalent using i18n-js library.

Demo

Here is the result of the TranslationKeys module applied on the Mastodon Admin Dashboard.

Click the previous slide or next slide links to see all the Mastodon admin pages displayed with i18n keys.

previous slide / 42 next slide

Default keys

Translation keys displayed on the pages are the first key definitions looked up, but I18n also relies on defaults. We can now discover key defaults via our server logs:

[:"nomination.edit.instructions", nil, {:scope=>:"simple_form.hints", :default=>[:"nomination.instructions", :"defaults.edit.instructions", :"defaults.instructions", ""]}]
[:"nomination.instructions", :en, {:scope=>:"simple_form.hints"}]
[:"defaults.edit.instructions", :en, {:scope=>:"simple_form.hints"}]
[:"defaults.instructions", :en, {:scope=>:"simple_form.hints"}]

These logs shows that I18n tries to intially find translation keys in this order:

Sequence Key Type
1 simple_form.hints.nomination.edit.instructions key 1
2 simple_form.hints.nomination.instructions default 1
3 simple_form.hints.defaults.edit.instructions default 2
4 simple_form.hints.defaults.instructions default 3

Here is the resulting YML structure for this translation. I18n will check the keys in the order above until it finds one.

en:
  simple_form:
    hints:
      nomination:
        edit:
          instructions: key 1 
        instructions: default 1 
      defaults:
        edit:
          instructions: default 2 
        instructions: default 3 

Exceptions

The solution isn't perfect, and I couldn't find an elegant way to fix two exceptions encountered so far: ActiveSupport::NumberHelper::NumberConverter & SimpleForm options.

Feel free to contact me if you encounter other exceptions or have a better fix for these. I would love to improve this little module and make it more reliable.

NumberConverter format

ActiveSupport provides methods to format numbers via the abstract class ActiveSupport::NumberHelper::NumberConverter. This class can expect a Hash from I18n.translate, which raises as our new module only returns strings. Source code here.

This is why we do this.

return result if result.is_a?(Hash) # ignore hashes from active support format

SimpleForm options

Creating a select box with SimpleForm assumes that I18n can return an empty string preventing buliding a translated collection. The module TranslationKeys always returns the scoped key looked up and triggers the collection mapping, which raises. Source code here and here.

This is why we do this.

return result if options[:scope].to_s == "simple_form.options" # ignore simple_form collection options

Front-end translations

I'm not using front-end translations much, but the logic to display keys should remain the same. An override in development of the I18n.translation function can achieve similar results.

Here is a quick override of the i18n-js functions following a similar template than the Ruby module.

import I18n from "i18n-js"

I18n.oldTranslate = I18n.translate;

I18n.translate = function(scope, options){
  // Log key and default translations
  console.log(scope, options);

  // Exceptions
  const result = I18n.oldTranslate(scope, options);
  if (typeof result !== "string") {
    return result;
  }

  // Display translation key
  return scope;
};

// Override aliases and similar functions.
I18n.t = I18n.translate;