Technology

Challenges faced in Internationalizing a rails Application

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:

  1. rails-i18n (https://github.com/svenfuchs/rails-i18n)

Collection of locale data

  1. i18n-js (https://github.com/fnando/i18n-js)

For translations in javascript files

  1. rails-i18nterface (https://github.com/mose/rails-i18nterface)

For managing locales and their translations from interface

  1. i18n-recursive-lookup (https://github.com/annkissam/i18n-recursive-lookup)

Allow translation definitions to reference other definitions

  1. i18n_data (https://github.com/grosser/i18n_data)

List of 2-letter-code/name pairs for all countries/languages in all languages

  1. 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:

  1. Use dictionaries to maintain commonly used translations so as to avoid repetition of translations in locale files.

 

  1. Categorize translations as following
en:

dictionary:

views:

helpers:

activerecord:

models:

user:

one: Dude

other: Dudes

attributes:

user:

name: Username

models:

controllers:

# gems translations

 

  1. Follow alphabetic order for keys

 

  1. If we are avoiding repetition of keys in locales then it is good not to have nesting of keys deeper than 3 levels.

 

  1. Check for the for I18n supported messages for gems used in application. Special care should be taken while overriding messages from gems.

 

  1. 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.

 

  1. 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.

 

  1. 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.

 

  1. Writing full keys so that if the part is views is extracted into partials then also the I18n will remain intact.

 

  1. While using third party services or APIs there should be compatibility check on the data accepted by the service provider.

 

  1. UI may get distorted because of the changes in length of translated strings. Use of responsive design is good.

 

Challenges faced in internationalizing application:

 

  1. 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:

  1. 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

  1. 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

  1. 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

  1. 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'

  1. 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

  1. 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

  1. 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')],

  1. 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'),

  1. 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

*