Hook into Rails' I18n - ruby-on-rails

I've written a little library to hold translated content in model attributes. All you have to do is add the following to a model:
class Page < ActiveRecord::Base
i18n_attributes :title, :content
end
By convention, the data is written to the real attributes i18n_title and i18n_content as a hash (or hstore hash for Postgres). And a number of getters and setters give you access to "localized virtual attributes":
page = Page.new
page.title_en = 'Hello'
page.title_es = 'Hola'
page.i18n_title # => { en: "Hello", es: "Hola" }
I18n.locale = :es
page.title # => "Hola"
page.title_en # => "Hello"
You can use these virtual attributes in forms as well, but there's a downside: The form builder uses I18n to get the label and translate attribute validation errors. And it does of course look for keys such as activerecord.attributes.page.title_en if you use title_enin the form.
It would be very cumbersome to replicate the same translation for every available_locale in the locales/en.yml etc files:
activerecord:
attributes:
page:
title_en: "Title"
title_es: "Title"
...
What I'd like to do is execute some code after Rails has loaded all locales in the boot process and then clone translations for these keys. Is there a way to do this? Maybe a hook which gets called after the translations have been loaded from the YAML files? (The translations are not yet loaded when my lib loads.)
Or do you see another way to tackle this problem? (I've tried to alias I18n.translate, but I'm afraid this might cause major headache in the future.)
Thanks for your hints!

Although you dropped this approach, please let me share my thoughts:
I don't think it is incredible useful to add other locale strings into a translation file for a specific localization. Since a config/locales/$locale.yml usually starts (at least in my case) with
$locale:
...
there is no need for activerecord.attributes.page.title_es in an English localization file. I'd just put it in es.yml as activerecord.attributes.page.title.
I mean: isn't that the whole purpose of separate localization files? (Or from the developer/translator point of view: In which file should I search for .title_es, in en.yml, es.yml or both?)

Related

Intercept and handle missing translations in helper

I have a helper function that generates useful HTML markup. My functions are usually called with some parameters, including some text translated with I18n my_helper( t(:translation_symbol) ).
I use I18n t translation helper for that, which (if left untouched) outputs some <span class="translation missing">...</span> for texts without translation
I was wondering how to best "intercept" this translation missing so
I can render them appropriately in my helpers
Normal behavior outside my helpers
In my views I should be able to call both
<%= my_helper(t(:my_text)) %> # My helper needs to handle missing translations
<%= t(:my_text) %> # I want default I18n markup (or a custom one) '<span class="translation missing">fr.my_text</span>
My helper
def my_helper(text=nil)
# Suppose my_text doesn't have a translation, text will have value
# => '<span class="translation-missing">fr.my_text</span>'
if translation_missing?(text)
# Code to handle missing translation
doSomething(remove_translation_missing_markup(text))
notify_translation_missing
else
doSomething(text)
end
end
def translation_missing?(text)
# Uses some regex pattern to detect '<span class="translation-missing">'
end
def remove_translation_missing_markup(text)
# Uses some regex pattern to extract my_text from '<span class="translation-missing">fr.my_text</span>'
end
Is there a better way around this ? I feel bad about using a dirty regex solution.
EDIT : extra requirements
No additional markup on views : I don't want to look at my files individually to add raise: true or default: xxx for every translation. If there is a need to change the behavior everywhere I can override the t method.
In my helpers, I need a convenient way to manipulate, just the translated text if the translation was found, but for missing translations, I need to easily be able to extract the full path of the translation (fr.namespace.translation_symbol), and the original translation symbol (translation_symbol), so I can add myself translation_missing in my custom markup.
EDIT2 : I am thinking of something like that
Override t helper to rescue I18n::MissingTranslationData => e
If the exception is raised, create and return a custom object that
If rendered in a html.erb, will output the usual <span class="translation..."
Has useful fields (translation path, translation_string) that I can reuse in my helpers
I think you can leverage the fact that I18n can be configured to raise an exception when translation missing. You can either set ActionView::Base.raise_on_missing_translations = true to globally raise the exception upon missing translations or you can pass :raise => true option to the t translation helper.
Update: since you are already using t helpers in your views and you don't want to change them, I think the only option for you is overriding the t helper in your ApplicationHelper:
# ApplicationHelper
def t(key, options = {})
# try to translate as usual
I18n.t(key, options.merge(raise: true)
rescue I18n::MissingTranslationData => e
# do whatever you want on translation missing, e.g. send notification to someone
# ...
"Oh-oh!"
end
This helper either returns the translated text or "oh-oh" if translation missing. And it should work as-is in your current views.
Since missing translations seems to be a pain point in your app, rather than have your app code be constantly on the lookout for whether a translation exists or not, if possible I would advocate ensuring that you always have translations for every potential call to t, regardless of locale (this is all on the assumption that the :translation_symbol keys in your calls to my_helper(t(:translation_symbol)) are all static, kept in your config/locales directory, and aren't generated dynamically).
You can do this using the I18n-tasks gem to:
Ensure your app does not have missing or unused keys, so you should be able to delete the missing translation handling parts of your helper methods
Make your test suite/build process fail if there are any missing or unused translations, so no one else that touches the codebase accidentally sneaks any through. See instructions on copying over their RSpec test.
Why not set the default option to the translate method as follows:
<%= t(:my_text, default: "not here") %>
See http://guides.rubyonrails.org/i18n.html#defaults for details about setting defaults for the I18n.translate method.

Add translation to I18N dynamically

I have added humanized-money-accessors as described here: Decimals and commas when entering a number into a Ruby on Rails form
Now I have two attributes in my model for the same type of data: the original version and the human-readable version. The problem: Since I am using activerecord-translation-yml-files, I have to put in the same translation for original attribute and the humanized_attribute, because my forms show the name of thie humanized_attribute, but on validation errors, the name of the original attribute is shown.
Is it possible to add translations dynamically? This way I could add the translation for the humanized-version of the field when the humanized_accessor-class-method is called, getting the original translation string from the yml file, instead of writing both of them (with the same value) into the yml-file, just to have more DRY.
This is dependent on the I18n gem's internal API not changing but it is possible using I18n.backend.store_translations.
This contrived example demonstrates:
I18n.with_locale(:fake_locale) { I18n.t('some_word') }
=> "translation missing: fake_locale.some_word"
I18n.backend.store_translations(:fake_locale, some_word: 'fake translation')
I18n.with_locale(:fake_locale) { I18n.t('some_word') }
=> "fake translation"
Important: This is only done in memory. Some persistence or re-generation mechanism is necessary to prevent these from disappearing when you redeploy/restart the server.
You might want to check out globalize3 gem. You have railscast tutorial http://railscasts.com/episodes/338-globalize3?view=asciicast.

Keeping track of changes using rails - "changed?"

I am building a multi lingual website, using ruby on rails, where part of the content is supposed to be user generated and they are supposed to be able to create different versions of it for all languages. The language support is handled by i18n gem.
Part of their content is created using Markdown through http://daringfireball.net/projects/markdown/basics .
In my database I save: object.content_markdown_en, object.content_html_en, object.content_markdown_sv, object.content_html_sv and so on for the different locales.
Now if a user changes the content, new html is supposed to be generated. But it seems unnecessary to regenerate the html for all locales if he only made changes in one of the languages.
I thought there might be some way to use something like
if object.content_markdown_[locale]_changed?
generate_new_html
end
that can be run in a loop for all possible locales. But I can't find any nice ways of doing this.
How about:
[:en, :sv].each do |locale|
if object.send("content_markdown_#{ locale }_changed?".to_sym)
send("generate_new_#{ locale }_html".to_sym)
end
end
You can use send to call methods by name:
object.send("content_markdown_#{locale}_changed?".to_sym)
Your loop would look like this:
%w(en sv).each { |locale|
if object.send("content_markdown_#{locale}_changed?".to_sym)
generate_new_html
end
}
However, using a separate translation table might be a better approach.

set constant values for cuisine like Chinese,Indian in ruby on rails

I want to use Cuisines like (Chinese, Indian, US) as constant values in my application which are defined in a config file. How can I set as constants and how can access in controllers?
This is explicitly not an answer to your question, but a suggestion that you look for alternatives. I think you would be far better off creating a database table with your cuisine names in it than to use constants. Leverage rails associations so that you can write nice readable code.
The problem with using constants is that under many circumstances, they aren't really constant. What happens if you want to add Japanese? What happens if you want to add Thai, but then 6 months later decide to drop it? What happens if you decide that Indian is too broad, and you want "Northern Indian" and "Southern Indian"?
With a database table, you can ensure that the class that are associated with those constants are always in a consistent state. When you need to get them all, they are just a line of code away with
my_cuisines = Cuisine.all
with nice built in iterators.
You can use gem 'settingslogic'
model settings.rb:
class Settings < Settingslogic
source "#{Rails.root}/config/settings.yml"
namespace Rails.env
end
then, use in controller:
Settings.cousines
First, consider what Marc Talbot said. Make sure that you really don't want a normal database model. If you're sure you want to use constants then continue on:
My preferred way to do this is with a pseudo-model.
In app/models/cuisine.rb
class Cuisine
# Should come before the constant declarations
def initialize(name)
#name = name
end
Mexican = new('Mexican')
Chinese = new('Chinese')
Indian = new('Indian')
def to_s
name
end
# other related methods
# like translations, descriptions, etc.
end
Then in the everywhere else in the app you can just reference Cuisine::Mexican or Cuisine::Indian
Also depending on how you are using it you might need a list of the cuisines.
class Cuisine
...
def self.all
[Mexican, Indian, Chinese, ...]
end
end
This technique keeps the code organized and keeps you from writing yet another initializer file.

translate database fields with rails

I know about the built-in I18n in Rails, but how can I select a database field per locale?
My model structure is something like this:
title #default (englisch)
title_de #(german)
title_it #(italian)
In my template I want to be able to write only
<%= #model.title %>
and should get the value in the right language.
Is there a plugin or a solution to use different fields per different locale settings with a structure like mine?
Although your db architecture (different locales hardcoded as table columns) seems wrong to me, I think you can achieve what you want by adding a pseudo-field to your model, something along:
# example not tested
class MyModel < ActiveRecord::Base
def localized_title(locale)
locale = locale == 'en' ? '' : '_' + locale
read_attribute("title#{locale}".to_sym")
end
end
Or, provided that you somehow make your current locale visible to your models, you can similarly overwrite the default title accessor method.
Edit: You can take a look at http://github.com/iain/translatable_columns, it seems pretty much compatible with your architecture....
Try using:
http://github.com/joshmh/globalize2
It may require renaming your columns (to a different standard).
Nowadays the best way to translate active record models fields is using https://github.com/shioyama/mobility

Resources