Implementing localization with i18n gem

Hi all, I’m struggling a bit with the implementation of i18n gem and its functionalities. I have it in my 1.3 app as per this post: Using I18n with Hanami - Bounga’s Home.

With 2.1 I simply wanted this as a component, but it’s not available in the view. How would I have to implement it to be available on the app level?

Also, in 1.3 I haven’t actually separated locales per individual app, even though I have several, because of the yml key names pattern. In 2.1 in might be useful to consider separation by slices as well.

Edit:
What I’ve also added was an error message translator module to accommodate for Slovenian specifics :slight_smile:

# coding: utf-8
module Translator
  private

  def translate_err_mess(incomming_error_messages)
    parsed_messages = {}
    translated_messages = []

    # Main_key would ussualy be an entity
    # Example {:contact=>{:name=>["must be filled", "size cannot be less than 3"]}
    incomming_error_messages.each do |main_key, main_key_errors|
      @entity = main_key
      main_key_errors.each do |field, messages|
        parsed_messages[field] = messages
      end
    end

    parsed_messages.each do |form_field, err_messages|
      subject = I18n.t ("#{@entity}_#{form_field}")
      field_element = I18n.t :err_field

      err_messages.each do |err_message|
        translated_messages << "#{field_element.capitalize} \"#{subject.capitalize}\" #{err_message}."
      end
    end
    return translated_messages
  end

end

::Hanami::Controller.configure do
  prepare do
    include Translator
  end
end

Hey @sebastjan_hribar, thanks for this question!

I did a simple i18n integration in a Hanami-style app quite a while ago. I set it up via a provider:

# config/providers/i18n.rb

Hanami.app.register_provider :i18n, namespace: true do
  prepare do
    require "i18n"
  end

  start do
    # TODO: ensure these match your actual paths (e.g. if you use app/ instead)
    load_paths = Dir["#{target.root}/slices/**/config/locales/**/*.yml"]

    I18n.load_path += load_paths
    I18n.backend.load_translations

    register :t, I18n.method(:t)
  end
end

This registers an "i18n.t" component into the app container.

You can then use that as a dep for any class to do translations:

module MyApp
  class MyClass
    include Deps["i18n.t"]

    def some_method
      t.call("i18n.key.here")
    end
  end
end

If the callable t isn’t your jam, you can of course register the I18n class itself in the container (giving you an "i18n" component name and i18n.t("...") for your call sites), or some other kind of wrapper object built to your liking.

But let’s stick with "i18n.t" for now and see how we can get this into your views. To do this, you’ll want to add it as a dep for your view context class. Create a views/context.rb in your app or slice (if you don’t have one already), and then include the dep:

# app/views/context.rb

module MyApp
  module Views
    class Context < Hanami::View::Context
      include Deps["i18n.t"]
    end
  end
end

With this done, you should now be able to call t.call("...") in your templates, and context.t.call("...") in your parts.

See this guide for more details on the Hanami view context.

I hope this helps! Let me know how you go and if you have any other questions along the way.

Hi @timriley,

thank you very much for the code. I’ve implemented the provider and context as shown and it works as expected. By including the dependency in the action, I have access to t.call there as well.

I’m setting the default locale in the provider and later on I’ll have the default locale read from the DB like so:

Hanami.app.register_provider :i18n, namespace: true do
  prepare do
    require "i18n"
  end

  start do
    lang_setting = 1 # Change later to read from settings repo.

    I18n.available_locales = :en, :sl
    I18n.default_locale = :sl if lang_setting == 1

    load_paths = Dir["#{target.root}/app/translations/locale/*.yml"]

    I18n.load_path += load_paths
    I18n.backend.load_translations

    register :t, I18n.method(:t)
  end
end

I’m sure it’s clear by now based on my questions that I stumble about with the new concepts, so here is another question regarding i18n as a provider.

This is in my initializer in my 1.3 app:

existing_language = SettingRepository.new.get_default_language
  if existing_language
    I18n.default_locale = SettingRepository.new.get_default_language.value.to_sym
  else
    I18n.default_locale = :en
  end

If I read the guides right I’ll be able to do the same in the provider, right? Instead of current placeholder line lang_setting = 1 # Change later to read from settings repo.

The idea is, the user selects the UI language as they use the app.

Thanks for the extra info, @sebastjan_hribar :slight_smile:

Personally, I wouldn’t recommend doing a database lookup in the i18n provider. This couples it to your persistence layer in a way that (to me) doesn’t feel right for an otherwise simple subsystem.

Are you looking to set a locale per user based on preferences they have saved into the database?

In this case, I think doing this in your base action class might make more sense. Something like this:

module MyApp
  class Action < Hanami::Action
    include Deps["setting_repo"]

    before :set_locale

    private

    def set_locale
      # TODO: make `current_user` exist
      # TODO: replace setting_repo logic with your own

      I18n.locale = setting_repo.locale_for_user(current_user)
    end
  end
end

And if you wanted to make the i18n subsystem more encapsulated, I think that registering the whole I18n class as your "i18n" component would make that action callback a little cleaner, i.e. with it only needing to interact with the registered component, rather than multiple places in your app needing to be aware of the I18n constant:

module MyApp
  class Action < Hanami::Action
    include Deps["i18n", "setting_repo"]

    before :set_locale

    private

    def set_locale
      # Now using an i18n dep, rather than referencing the class
      i18n.locale = setting_repo.locale_for_user(current_user)
    end
  end
end

Thank you again, this is a cleaner approach. I might have overcomplicated in my existing 1.3 app. I have a rake task with all initial settings the app requires that run when the app boots with docker-compose. One of those is also the default language setting. That is why I have the initializer coupled with the corresponding repo. However, I see now it’s a redundant step anyway. :slight_smile:

1 Like