5 March 2023
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.
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.
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
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
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.
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
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
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;