While working with one of our Ruby on Rails applications, I wanted to Internationalize the application with full support to Unicode data to be displayed and stored in database.
Here are some Highlights:
Following Gems are good to be used/referred:
- rails-i18n (https://github.com/svenfuchs/rails-i18n)
Collection of locale data
- i18n-js (https://github.com/fnando/i18n-js)
For translations in javascript files
- rails-i18nterface (https://github.com/mose/rails-i18nterface)
For managing locales and their translations from interface
- i18n-recursive-lookup (https://github.com/annkissam/i18n-recursive-lookup)
Allow translation definitions to reference other definitions
- i18n_data (https://github.com/grosser/i18n_data)
List of 2-letter-code/name pairs for all countries/languages in all languages
- i18n-tasks (https://github.com/glebm/i18n-tasks)
Tasks for checking usage of translations, add/remove unused translations
Some points to be considered while starting internationalizing:
- Use dictionaries to maintain commonly used translations so as to avoid repetition of translations in locale files.
- Categorize translations as following
en: dictionary: views: helpers: activerecord: models: user: one: Dude other: Dudes attributes: user: name: Username models: controllers: # gems translations
- Follow alphabetic order for keys
- If we are avoiding repetition of keys in locales then it is good not to have nesting of keys deeper than 3 levels.
- Check for the for I18n supported messages for gems used in application. Special care should be taken while overriding messages from gems.
- If there is no support for internationalized messages from a gem then we may need to provide translated messages using blocks or by overriding the methods in gem.
- Need to consider data that is stored in database and will be displayed as it is. For example. the status of a person stored in database.
- Searching data must be handled for data where the displayed translated data is actually not present in the database as translated. These cases are where we only show database value by translating on views.
- Writing full keys so that if the part is views is extracted into partials then also the I18n will remain intact.
- While using third party services or APIs there should be compatibility check on the data accepted by the service provider.
- UI may get distorted because of the changes in length of translated strings. Use of responsive design is good.
Challenges faced in internationalizing application:
- Breadcrumbs were not translated:
In application we are using ‘breadcrumbs_on_rails’ in our application.
When breadcrumbs are set in controller outside action definition as shown below:
class UsersController < ApplicationController
add_breadcrumb I18n.t('general.users'), :users_path, :title => I18n.t('general.users')
def index
# Something
end
end
then they did not get translated on changing locales.
If the breadcrumbs are set inside action then they are translated correctly.
If same breadcrumb is required to be set for all the action then to follow DRY we cannot set breadcrumbs in each action separately.
class UsersController < ApplicationController
def index
add_breadcrumb I18n.t('general.users'), :users_path, :title => I18n.t('general.users')
# Something
end
end
Reason: This happens because the add_breadcrumb method that is invoked outside action is class_method and is initialized with `en` locale value at the time of initializing app.
Solution:
- Use of proc
class UsersController < ApplicationController
add_breadcrumb proc{I18n.t('general.users')}, :users_path, :title => proc{I18n.t('general.users')}
def index
# Something
end
end
- You can add a before_filter
class UsersController < ApplicationController
before_filter :set_breadcrumbs
def index
# Something
end
private
def set_breadcrumbs
add_breadcrumb I18n.t('general.users'), :users_path, :title => I18n.t('general.users')
end
end
- Issue with Paperclip
Validation Error messages if directly provided to Paperclip are not translated.
For example:
class User < ActiveRecord::Base
validates_attachment_content_type :document,
:content_type => %w(image/jpeg image/png image/gif),
:message => I18n.t('document_messages.invalid_content_type')
end
Workaround is to use `proc`
class User < ActiveRecord::Base
validates_attachment_content_type :document,
:content_type => %w(image/jpeg image/png image/gif),
:message => proc { I18n.t('document_messages.invalid_content_type') }
end
- Issue with ChronicDuration
In case of ChronicDuration we were using only `output` method
In this method the translation part was coming from following method
def humanize_time_unit(number, unit, pluralize, keep_zero)
return nil if number == 0 && !keep_zero
res = "#{number}#{unit}"
# A poor man's pluralizer
res << 's' if !(number == 1) && pluralize
res
end
This method was overriden as following
def humanize_time_unit(number, unit, pluralize, keep_zero)
return nil if number == 0 && !keep_zero
res = "#{number}" + I18n.t("chronic_duration.#{(number != 1 && pluralize) ? unit + 's' : unit }")
end
The required translations were added to locales as following
en: chronic_duration: year: ' year' years: ' years' month: ' month' months: ' months' week: ' week' weeks: ' weeks' day: ' day' days: ' days' hour: ' hour' hours: ' hours' minute: ' minute' minutes: ' minutes' second: ' second' seconds: ' seconds'
- Issue with Kaminari
Setting entry_name as per model is removed as we are using ‘entry’ word by default
def page_entries_info(collection, options = {})
entry_name = collection.total_count == 1 ? t('general.entry') : t('general.entries')
if collection.total_pages < 2
t('helpers.page_entries_info.one_page.display_entries', :entry_name => entry_name, :count => collection.total_count)
else
first = collection.offset_value + 1
last = collection.last_page? ? collection.total_count : collection.offset_value + collection.limit_value
t('helpers.page_entries_info.more_pages.display_entries', :entry_name => entry_name, :first => first, :last => last, :total => collection.total_count)
end.html_safe
end
- Issue with overriding validation error message
Providing error message as a message key to validation does not work correctly.
For example:
class User < ActiveRecord::Base
validates :name, presence: true, message: I18n.t('some.translation.keys.to.custom.validation.message')
end
Validation error message are looked up as following
activerecord.errors.models.[model_name].attributes.[attribute_name] activerecord.errors.models.[model_name] activerecord.errors.messages errors.attributes.[attribute_name] errors.messages
For above given example it is not required to specify message instead specify it in locale.
Error message will be looked up as following:
activerecord.errors.models.user.attributes.name.blank activerecord.errors.models.user.blank activerecord.errors.messages.blank errors.attributes.name.blank errors.messages.blank
- Issue with datepicker
Following keys used in Datepicker were coming from jquery-ui.min.js are translated after using gem i18n-js
prevText:I18n.t('helpers.submit.prev'),nextText:I18n.t('helpers.submit.next'),currentText:I18n.t('trading.tabs.today'),
monthNames:[I18n.t('month_names.january'),I18n.t('month_names.february'),I18n.t('month_names.march'),I18n.t('month_names.april'),I18n.t('month_names.may'),I18n.t('month_names.june'),I18n.t('month_names.july'),I18n.t('month_names.august'),I18n.t('month_names.september'),I18n.t('month_names.october'),I18n.t('month_names.november'),I18n.t('month_names.december')],
monthNamesShort:[I18n.t('month_names_short.january'),I18n.t('month_names_short.february'),I18n.t('month_names_short.march'),I18n.t('month_names_short.april'),I18n.t('month_names_short.may'),I18n.t('month_names_short.june'),I18n.t('month_names_short.july'),I18n.t('month_names_short.august'),I18n.t('month_names_short.september'),I18n.t('month_names_short.october'),I18n.t('month_names_short.november'),I18n.t('month_names_short.december')],
dayNames:[I18n.t('day_names.sunday'),I18n.t('day_names.monday'),I18n.t('day_names.tuesday'),I18n.t('day_names.wednesday'),I18n.t('day_names.thursday'),I18n.t('day_names.friday'),I18n.t('day_names.saturday')],
dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],
dayNamesMin:[I18n.t('day_names_min.sunday'),I18n.t('day_names_min.monday'),I18n.t('day_names_min.tuesday'),I18n.t('day_names_min.wednesday'),I18n.t('day_names_min.thursday'),I18n.t('day_names_min.friday'),I18n.t('day_names_min.saturday')],
- Issue with Uniform
I had to override the texts from default options of jquery.uniform.js
fileDefaultText: I18n.t('js.no_file_chosen'),
fileBtnText: I18n.t('js.choose_file'),
- Issue with prevent_destroy_if_any
Translation message issue of prevent_destroy_if_any was solved as following
def self.prevent_destroy_if_any(*association_names)
before_destroy do |model|
associations_present = []
association_names.each do |association_name|
association = model.send association_name
if association.class == Array
associations_present << association_name if association.any?
else
associations_present << association_name if association
end
end
if associations_present.any?
errors.add :base, "Cannot delete #{model.class.model_name.human.downcase} while #{associations_present.join ', '} exist"
return false
end
end
end
As
def prevent_destroy_if_any(*association_names)
before_destroy do |model|
associations_present = []
association_names.each do |association_name|
association = model.send association_name
if association.class == Array
associations_present << association.first.class.model_name.human(:count => 2).downcase if association.any?
else
associations_present << association.class.model_name.human.downcase if association
end
end
if associations_present.any?
errors.add(
:base,
I18n.t(
'messages.cannot_delete_parent_object',
:parent_object => model.class.model_name.human.downcase,
:associated_objects => associations_present.join(', ')
)
)
return false
end
end
end
Even related to to few other gems problems were faced that were tackled successfully.