How to change human readable attributes depending on context in Rails? - ruby-on-rails

I know I can change the ActiveRecord names in a locale file, but is there a way to change them based on context rather than locale (while still leveraging locale files in the application)?
For a simple example (I have multiple circumstances where I need to accomplish this), if I have an address form in a wizard and a user selects a country, how could I change the label/error messages for a :zipcode attribute to display "Zipcode" to those who have selected United States, and "Postcode" to those who have selected United Kingdom?
Edit: What I mean is when a model attribute (country) changes, how to change the human readable attributes for (zipcode) based on the country selection. The users locale won't change (I am already making use of locale files for translations).

Best way to localize is to use I18n, check this post: https://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization
Basic I18n
First add locales in you application.rb controller
config.i18n.available_locales = ["en-GB", "en-US"]
config.i18n.default_locale = "en-US"
Then create 2 files en-US.yml and en-GB.yml under config/locales
# en-GB.yml
en-Gb:
zipcode: "PostCode"
# en-US.yml
en-US:
zipcode: "ZipCode"
Then in your controller, you will need to set dictionary that will be used for translation. It is defined with the I18n.locale variable.
# example when locale is passing through params
before_action :set_locale
def set_locale
I18n.locale = I18n.available_locales.include?(params[:locale]) ? params[:locale] : I18n.default_locale
end
And finaly in your views:
<%= t('zipcode') %>
Or if you need in you ruby files:
I18n.t('zipcode')
Localize ActiveRecord Attributes
Same as above, you can create a active_record.en-US.yml under config/locales
# active_record.en-US.yml
en-US:
activerecord:
attributes:
your_model_name:
zipcode: 'ZipCode'

I accepted the other answer as I did end up using locale files as part of my solution.
What I did was a bit hacky, however:
Set locale to {language}-{selected_country} before the necessary request
Have specific overrides in those YAML files where necessary, fall back to standard attributes in base language file whenever possible.
Make use of alias_attributes in the FormObject to minimise the need for the above hack.

Related

Localization of custom validations

I'm working on a project in which I'd like to move all strings used in the app to a single file, so they can be easily changed and updated. However I'm having trouble with the custom validation. I have validations in my app as follows:
validate :thing_is_correct
def thing_is_correct
unless thing.is_correct
errors[:base] << "Thing must be correct"
end
end
I'm not sure how to move "Thing must be correct" into my en.yml file and out of the model. Any help would be greatly appreciated.
The Rails Way to do that would be to use the mechanism described in the Guides.
errors is an instance of ActiveModel::Errors. New messages can be added by calling ActiveModel::Errors#add. As you can see in the docs, you can not only pass a message but also a symbol representing the error:
def thing_is_correct
unless thing.is_correct?
errors.add(:thing, :thing_incorrect)
end
end
Active Model will automatically try fetching the message from the namespaces described in the Guides (see the link above). The actual message is generated using ActiveModel::Errors#generate_message.
To sum up:
Use errors.add(:think, :thing_incorrect)
Add thing_incorrect under one of the YAML keys listed in the Guides.
You can access the I18n inside the model.
validate :thing_is_correct
def thing_is_correct
unless thing.is_correct
errors[:base] << I18n.t('mymodel.mymessage')
end
end
Inside config/locales/en.yml
en:
mymodel:
mymessage: "Thing must be correct"
Inside another locale: (config/locales/es.yml)
es:
mymodel:
mymessage: "Esto debe ser correcto"
If you set I18n.locale = :en, the message inside en.yml will be displayed. If you set it to :es, the one inside es.yml will be used instead.

Rails model using only default locale

I have a model with custom validations. I return the validation
errors to the view as JSON using AJAX.
I need to show the error
messages with the right locale but the messages
show in English (default) for each locale.
When I return I18n.locale
itself instead of the message (for troubleshooting) it shows "en" no matter what is the set locale.
I18n in controllers or views works as expected, this problem
occurs only in the model.
The only configuration that I've made regarding locale is setting the default locale in the application config and setting the locale before each action in the application controller.
Another thing that I've noticed is that when I use root_path without providing the locale, it's using the default locale instead keeping the current locale like it should. I think those issues are connected
Edit:
When I print the locale parameter in controller#create, I get nil. For some reason I don't have the locale parameter at all.
model file:
validate :relevant_date_time
def relevant_date_time
errors.add(:date_error, I18n.t("validations.date_error")) unless date_time_is_relevant?
end
application_controller:
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
Am I forgetting something?
Information I reviewed:
http://guides.rubyonrails.org/i18n.html
Access translation file (i18n) from inside rails model
Rails 3 translations within models in production
http://www.eq8.eu/blogs/10-translating-locales-for-rails-model-errors
https://lingohub.com/frameworks-file-formats/rails5-i18n-ruby-on-rails/
Change locale at runtime in Rails 3
If you set locale at application_controller.rb, it will only have scope in all controllers and views NOT in models.
So you have to set locale inside model too. Same as you did at controller.
At controller
Model.new(params.merge(locale: I18n.locale))
Inside model
attr_accessible :locale
before_validation() do
I18n.local = locale
end
def relevant_date_time
errors.add(:date_error, I18n.t("validations.date_error")) unless date_time_is_relevant?
end
I18n locale in controllers/views only applies to the action's context, it does not change your app's default locale setting, which is where your model is getting its locale from (as you have discovered).
The step you are missing is to pass the locale into your model from your action, which you could do by adding an attr_accessor :locale to your model, and then passing the locale through when creating/updating it in your controller:
# e.g. app/controllers/users_controller.rb
# user_params = { email: test#mail.com, password: 12345678 }
User.new(user_params.merge(locale: I18n.locale))
Then in your validation method you would be able to access the current locale with locale, so all you need to do is pass it to I18n.t:
errors.add(:date_error, I18n.t("validations.date_error", locale: locale)) unless date_time_is_relevant?
Solved by adding the following method in application controller:
def default_url_options(options={})
{ locale: I18n.locale }
end
For anyone who is struggling with I18n in model validation, the solution could be not including any custom message keys in the models but
en:
activerecord:
errors:
models:
model_name:
attributes:
attr_name:
taken: << or whatever error message like blank, too_long, too_short etc.
Credit t:
https://stackoverflow.com/a/4453350/267693

Rails I18n attributes to fallback to default locale

We user I18n gem for translations in our application.
With the translations, it is expected to fallback to default_locale which is :en, if the translation is not available in corresponding locale.
Class Article
...
translates :title
...
end
While accessing as a french user,
article.title => title in english
but
article.attributes(:title) => nil
I guess attributes picks directly from the active_record object (french translation) and since it is not available, it returns nil. Is there a way to make attributes as well fallback to default locale if translation is not available in corresponding locale.
You can use I18n.fallbacks:
I18n.default_locale = :"en-US"
I18n.fallbacks[:fr] # => [:fr, :"en-US", :en]
Take a look how to use Fallbacks on I18n Wiki

Rails Route Scoping to value from database

I'm working with an internationalized rails app using the locale as part of the url. We only have certain languages translated in the database so it's not meaningful to allow users to access any locale in the url.
For example, we cover english and spanish, so our current routes.rb places pretty much everything inside of a scope "(:locale)", locale: /en|es/ do... block. My understanding is that this forces :locale, if it exists, to be one of "en" or "es", which works fine for now.
My concern is that different clients will want the system to support other languages (and only those languages). They will be responsible for creating the internationalization records which contain locale information. I'm thinking I'd like to automatically allow locale to be any that is already defined in the database, so I added a class method to Translation (the internationalization record)
def self.available_locales
Translation.uniq.pluck(:locale)
end
and changed routes.rb to scope "(:locale)", locale: Translation.available_locales do... however this just made every route go to /en/es/the_rest_of_the_url.
Is it possible to tell rails that routes must use a locale value from the resulting array from available_locales?
I'd use Advanced Constraints for that:
# routes.rb
scope "(:locale)", constraints: LocaleConstraint.new
# i.e. /lib/locale_constraint.rb
class LocaleConstraint
def initialize
# get available locales from DB or so
#locales = Locale.all
end
def matches?(request)
#locales.include?(request.params[:locale])
end
end
This way you could also write a backend to manage available locales, etc.
You could just turn it back into a regex, so it matches your first example:
Regexp.new([:en, :es].join("|")) #=> /en|es/
Or using your class method (ABMagil's edit):
Regexp.new(Translation.available_locales.join("|")) #=> /en|es/

How to make i18n aware of "genders" in gender-sensitive languages like e.g. French

I have to translate a website in French. Now, French, as many other languages, has genders and inflection. Questions are:
How to create yaml files containing messages with variable parts being either male or female...
How to modify the i18n generator to support this?
Are there any gems or plugins supporting this?
This is a common problem in Italian too.
One solution which doesn't require plugins would be to use gender specific keys.
it:
presentation_f: "Sig.ra %{customer_name}"
presentation_m: "Sig. %{customer_name}"
You can create a method in your model that takes a key in input and returns the gender modified key:
module Genderize
def genderize(key)
"#{key}_#{self.gender}"
end
end
class Customer
include Genderize
def gender
gender_field_from_database_or_wherever || 'm'
end
end
In your controller or views you could do something like:
t(#person.genderize('presentation'), customer_name: #person.name)
The example is a bit contrived but you get the picture.
One other thing you can do is to write a tg function (meaning t-genderized) which takes as second argument
the model where to get the gender method:
def tg(key, model, options={})
gender_key = "#{key}_#{model.gender}"
I18n.t(gender_key, options)
end
Which is basically the same as above without polluting the models, using a global function instead.
i18n-inflector-rails looks like an interesting project, but in my experience it's not so rare to pass yaml files to non technical translators, and having that additional complexity to explain:
en:
welcome: "Dear #{f:Lady|m:Sir|n:You|All}"
may be too much for them. While this one:
en:
welcome_f: "Dear Lady"
welcome_m: "Dear Sir"
welcome_n: "Dear You"
It's much easier to read an to explain.
I would also suggest (this is a free advice not related to the question) to keep yaml files as flat as possibile preferring namespaced strings as keys, intstead of nested structure. For instance do this:
it:
home_page_welcome: "Welcome to the home page!"
instead of this:
it:
home_page:
welcome: "Welcome to the home page!"
You pay a little bit more verbosity in controllers and views, but you end up with code you can move around much easier without having to rearrange the tree structure in yaml fies. It's going to be also much easier to find out messed indentation when translated files come back to you.
Have you tried i18n-inflector-rails ?
For files with variable parts depending on gender (in your en.yml):
welcome: "Dear #{f:Lady|m:Sir|n:You|All}"
then you can define f, m and n like this:
en:
i18n:
inflections:
gender:
f: "female"
m: "male"
n: "neuter"
female: #f
male: #m
neuter: #n
man: #male
woman: #female
default: n
Just make sure to define a inflection_method in your controller so it can detect the correct gem for a given case.
This example is for English but I believe a translation to French is straightforward.
EDIT:
I believe you need to specify the gender as a parameter in the translation, this is specially useful if you have a collection, here is how you do this:
t('.welcome', :gender => model.gender)
Remember that the inflection_method defined in the controller is just a helper so you don't need specify the gender as parameter multiple times, but in case you need to change it within the same view you can send it as a parameter like I said.
If you are going to use a lot this structure I recommend that you create a helper like this:
module TranslationHelper
def translate_model(key, model)
gender = model.try(:gender) || 'male'
I18n.t(key, :gender => gender)
end
end

Resources