right now when if I have in my app some code like
<%= t :test %>
and there is no translation available for :test I get something like this in my view:
<span class="translation_missing" title="translation missing: en.test">Test</span>
What I would like to add is to include a similar span even for existing translations, so if the translation exists I would like to get something like:
<span class="translation_existing" title="translation existing: en.test">Translated string</span>
note the different class name in the span. And the string should be really translated.
How can I achieve this result by overriding the translate method?
Thank you in advance
Gnagno
"t" is a view helper :
http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-t
it is easy to view its source code and to create your own, you can also override the helper by redefining it in your ApplicationHelper.
But IMHO, you should not redefine it, but create your own one with a different name. There are so many cases where you'll want to use the "t" helper in parts of your views that would screw up if you insert additional HTML markup (ie. : I use the "t" helper in the section of my pages to translate the title, meta tags, etc)...
I created a helper that overwrites 't' method, which is just shortcut for 'translate':
def t(*a) # I don't care what params go in
key = a.first # just want to override behaviour based on the key
#if my overrides contain the key
if value = #i18n_overrides["#{I18n.locale}.#{key}"]
return value # return the overrided value
end
translate(*a) # otherwise letting i18n do its thing
end
More clean approaches would be these, but they didn't work for me:
Tried using alias but realised that t or translate dont' exist in that scope at the point of execution of class body.
Tried calling ActionView::Helpers::TranslationHelper.translate instead of just translate but that didn't work either.
Related
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.
A Rails project of mine uses rails-i18n for localization purposes. From within views (ActionView) and controllers (ActionController) one can call the (I18n.)t method to get values from the appropriate locale YAML.
If you start the localization-key with a period, like t(".title"), Rails will add a prefix path for the file you're currently in, using a feature called lazy lookup. So the ".title"-key get's a prefix from within your users/show.html.erb file to become "users.show.title".
Works like a charm, but now I have a couple of classes that are neither views, nor controllers, and I want to use the t-method from there as well. Calling I18n.t works fine, but because my custom class doesn't inherit from any Rails classes, it doesn't get a prefix. I can work around this quite easily, but all my workarounds look ugly, and I have the feeling there's a method in one of the super classes that's used to determine the prefix — but I can't find it in the documentation.
Is there a (class-)method one can override, that I18n.t uses for the lazy lookup?
I guess if it's good enough for ActionPack's AbstractController::Translation, it's good enough for me to copy in my custom class scenario:
def translate(*args)
key = args.first
if key.is_a?(String) && (key[0] == '.')
key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }"
args[0] = key
end
I18n.translate(*args)
end
alias :t :translate
(wherein I'll replace the controller_path part)
Rails fills the id and class of an element generated with various input tag helpers (text_field for example) by itself. For example, in a form_for(#athlete), adding a text_field :height generates
<input name="athlete_height" ... />
I am a strong advocate of the use of hyphens in HTML ids and classes (and in CSS selectors consequently) (by reading MDN, because I like to write using the conventions dictated by the language - like in data-stuff - and because it's just better looking).
I read here and there that in Rails you can't fix this. This would be quite disappointing.
Do you know a way around this problem?
I'm afraid, underscores are hardcoded. From https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_helper.rb, lines 446-450:
options[:html].reverse_merge!(
class: as ? "#{action}_#{as}" : dom_class(object, action),
id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
method: method
)
you can always provide your own id and class, or patch code of ActionView, which may be hard, tedious and error-prone
Recently ran into this very same situation and #lobanovadik has a good solution: just write your own helper method.
# app/helpers/dom_helper.rb
module DomHelper
dom_id_helper(model)
dom_id(model).dasherize
end
end
# view.html.erb
<%= link_to "Text", path, id: dom_id_helper(#model) %>
#=> Text
This has the benefit of not monkey-patching Rails or messing with any default methods/configuration. Thus, you won't "break" any updates to Rails.
It also gives you greater flexibility because you can now use both dashes or underscores depending on the situation. For instance, let's say there's a gem that expects IDs to have underscores...you won't break it.
As a personal preference, I always append _helper to all my own helper methods, so that I know it's a helper method and that it came from me and not from Rails (easier to debug).
I have a template that users can upload that generates a report. They can put special tags into the html template and it will replace with data from the db. Quick example:
<div class="customer-info">
<h1>{customer_name}</h1>
<h2>{customer_address_line1}</h2>
<h2>{customer_address_line2}</h2>
<h2>{customer_address_city}, {customer_address_state} {customer_address_zip}</h2>
</div>
I have a controller that looks up the customer and then parses the template and replaces the tokens.
Right now I have the parse code in the controller creating a fat controller. Not good.
But where should I move the code? Model folder? Create a Util folder and put it there?
Just not sure what the Rails Way would be.
I was curious about this too, and found a very similar discussion here. Honestly, I think it depends on how much parse code there is. If there are only a very few lines, then the model is a safe place. If it's going to be a large package, especially a re-usable one, the /lib/ folder may be a better bet for the parsing itself. However, you definitely should remove it from the controller, as you suggested.
I agree that the logic shouldn't be in the controller, but let's get a
little more specific about how you'd go about implementing this.
First, where are you storing your templates in the database? They
should be stored in their own model, let's call it
CustomerTemplate and give an attribute :template of type Text.
So now we have two types of objects, Customers and
CustomerTemplates. How to render a customer given a template? It
honestly wouldn't be terrible to just have a render function in
the CustomerTemplate model that takes a customer and renders it, but
it is putting some logic inside your app that doesn't strictly belong
there. You should separate out the "customer specific rendering logic"
from the "rendering my simple custom template language".
So, let's create a simple template handler for your custom language,
which I'm going to nickname Curly. This handler should know nothing about
customers. All it does is take a string and interpolate values inside
{}'s. This way if you want to add new template types in the future —
say, to render another model like an invoice — you can use the same
template type.
Templates in Rails are classes which respond to call and are
registered with ActionView::Template. The simplest example is Builder.
Here's a quickly written Template handler which renders Curly. The
call function returns a string which is eval'd, so the string has to
be valid ruby code. The string eval in scoped by the render call, so
it has access to any variables passed in via the { locals: {} }
option to render.
# In lib/curly_template_handler.rb
class CurlyTemplateHandler
def self.call(template)
src = template.source
"""
r = '#{src}'.gsub(/{([^}]*)}/) { |s|
local_assigns[$1.to_sym] || s
}
raw r
"""
end
end
Make sure the handler is initialized, and let's set it to listen for
the :curly type.
# In config/initializers/register_curly_template.rb
ActionView::Template.register_template_handler(:curly, CurlyTemplateHandler)
We need to add lib/ to autoload_paths so the class is loaded:
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
Finally, we can render our template in our view! I'm embedding the string here, but you'd really get it from a CustomerTemplate object:
<%= render(inline: "<h2>{customer_name}</h2><p>{customer_address}</p>",
type: :curly,
locals: { customer_name: #customer.name,
customer_address: #customer.address }) %>
DO NOT USE MY EXAMPLE CODE IN PRODUCTION! I left out a bunch of corner
cases which you'll need to handle, like sanitizing user input.
I'd like to, for all helpers, across my entire Rails application, replace this syntax:
time_ago_in_words(#from_time)
with this:
#from_time.time_ago_in_words
Ideally this change would also make the helpers available anywhere in my application (the same way, say, 5.times is).
Does anyone know of a plugin that does this? Would it be difficult to roll my own?
If you create a new model you can make it inherit the methods from an existing class.
Example (app/models/mykindofstring.rb):
class Mykindofstring < String
def all_caps_and_reverse
self.upcase.reverse
end
end
Then from the controller:
#my_string = Mykindofstring.new('This is my string')
and finally calling it from the view:
<%= #my_string %>
displays as This is my string, while:
<%= #my_string.all_caps_and_reverse %>
displays as GNIRTS YM SI SIHT.
The class inherits from the string class, so all the methods that apply to a string also apply to a mykindofstring object. The method in it is just a new name for a combination of two existing string methods, but you can make it do whatever you want, such as a method to convert a time into a formatted string.
Basically it's just extending a current Ruby class with your own methods instead of using functions/helpers.
Major Edit
Based on Ians comment to this answer and his own answer to the above question, I played around a bit in Rails and came up with adding this to the application_helper.rb:
module ApplicationHelper
String.class_eval do
def all_caps_and_reverse
self.upcase.reverse
end
end
end
Now just call the method on any string in the app:
#string = 'This is a string.'
#string.all_caps_and_reverse
.gnirts a si sihT
You won't find any easy way to do this across your whole application for all (or even most) helpers.
Helpers are structured as modules meant to be included in view rendering classes. To leverage them, you'd have to keep a copy of an ActionView around (not a HUGE deal I suppose).
You can always open up the classes and specify each helper you want, though:
class Time
def time_ago_in_words
ActionView::Base.new.time_ago_in_words self
end
end
>> t = Time.now
=> Tue May 12 10:54:07 -0400 2009
>> t.time_ago_in_words
=> "less than a minute"
(wait a minute)
>> t.time_ago_in_words
=> "1 minute"
If you take that approach, I recommend caching the instance of ActionView::Base that you use. Or, if you don't want to go so heavyweight, you can create your own class to just include the helpers you want (you don't want to include the helpers directly in each Time, Date, String, etc, class, though, as that will clutter up the method namespace pretty fierce -- and conflict with the natural names you want).
If you are using Ruby 2.1 you can use Refinements. They are an alternative to monkey-patching Ruby's classes. to borrow #Jarrod's monkey-patch example:
module MyString
refine String do
def all_caps_and_reverse
upcase.reverse
end
end
end
You can now use this in any object you want:
class Post
using MyString
end
#post.title.all_caps_and_reverse
You can read more about Refinements here.
Make a model, TimeAgo or something similar. Implement following this idea here:
a code snippet for "wordifying" numbers
Then, in your controller, create the instance variables using this class. Then, in your view, call #from_time.in_words