Formats a number with grouped thousands using a delimiter. You can customize the format in the options hash.
:locale Sets the locale to be used for formatting. Defaults to current locale.
:delimiter Sets the thousands delimiter. Defaults to ","
.
:separator Sets the separator between the units. Defaults to "."
.
:raise Setting to true
raises InvalidNumberError
when the number is invalid.
1 number_with_delimiter(12345678) # => "12,345,678"
2 number_with_delimiter(12345678.05) # => "12,345,678.05"
3 number_with_delimiter(12345678, delimiter: ".") # => "12.345.678"
Formats a number with the specified level of precision. You can customize the format in the options hash.
:locale Sets the locale to be used for formatting. Defaults to current locale.
:precision Sets the level of precision. Defaults to 3.
:significant If true, precision will be the number of significant_digits; otherwise, the number of fractional digits are used. Defaults to false.
:separator Sets the separator between the units. Defaults to "."
:delimiter Sets the thousands delimiter. Defaults to ""
.
:strip_insignificant_zeros Setting to true removes insignificant zeros after the decimal separator. Defaults to false.
:raise Setting to true
raises InvalidNumberError
when the number is invalid.
1 number_with_precision(111.2345) # => "111.235"
2 number_with_precision(111.2345, precision: 2) # => "111.23"
This is an extremely simple helper module, barely worth mentioning.
Bypasses HTML sanitization by calling to_s
and then html_safe
on the argument passed to it.
Returns an HTML safe string by first escaping all array items and joining them by calling Array#join
using the supplied separator. The returned string is also called with html_safe
for good measure.
safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
# => "<p>foo</p><br /><p>bar</p>"
This module assists in creation of HTML markup code that follows good, clean naming conventions.
This helper method creates an HTML element with id
and class
parameters that relate to the specified Active Record object. For instance, assuming @person
is an instance of a Person
class with an id
value of 123
, the template code
= content_tag_for(:tr, @person) do
%td= @person.first_name
%td= @person.last_name
will produce the following HTML:
<tr id="person_123" class="person">
...
</tr>
If you require the HTML id
attribute to have a prefix, you can specify it as a third argument:
content_tag_for(:tr, @person, :foo) do ...
# => "<tr id="foo_person_123" class="person">..."
The content_tag_for
helper also accepts a hash of options, which will be converted to additional HTML attributes on the tag. If you specify a :class
value, it will be combined with the default class name for your object instead of replacing it (since replacing it would defeat the purpose of the method!).
content_tag_for(:tr, @person, :foo, class: 'highlight') do ...
# => "<tr id="foo_person_123" class="person highlight">..."
Produces a wrapper div
element with id
and class
parameters that relate to the specified Active Record object. This method is exactly like content_tag_for
except that it’s hard-coded to output div
elements.
This module contains helper methods related to rendering from a view context to be used with an ActionView::Renderer
object. Development of an Action View renderer is outside the scope of this book, but for those who are interested, investigating the source code for ActionView::TemplateRenderer
and ActionView::PartialRenderer
would be a good starting point.5
5. https://github.com/rails/rails/tree/4-0-stable/actionpack/lib/action_view/renderer
The SanitizeHelper
module provides a set of methods for scrubbing text of undesired HTML elements. Rails sanitizes and escapes HTML content by default, so this helper is really intended to assist with the inclusion of dynamic content into your views.
Encodes all tags and strip all attributes (not specifically allowed) from the html
string passed to it. Also strips href
and src
tags with invalid protocols, particularly in an effort to prevent abuse of javascript
attribute values.
= sanitize @article.body
With its default settings, the sanitize
method does its best to counter known hacker tricks such as using Unicode/ASCII/hex values to get past the JavaScript filters.
You can customize the behavior of sanitize
by adding or removing allowable tags and attributes using the :attributes
or :tags
options.
= sanitize @article.body, tags: %w(table tr td),
attributes: %w(id class style)
It’s possible to add tags to the default allowed tags in your application by altering the value of config.action_view.sanitized_allowed_tags
in an initializer. For instance, the following code adds support for basic HTML tables.
1 class Application < Rails::Application
2 config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
3 end
You can also remove some of the tags that are allowed by default.
1 class Application < Rails::Application
2 config.after_initialize do
3 ActionView::Base.sanitized_allowed_tags.delete 'div'
4 end
5 end
Or you can change them altogether.
1 class Application < Rails::Application
2 config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
3 end
Sanitizing user-provided text does not guarantee that the resulting markup will be valid (conforming to a document type) or even well-formed. The output may still contain unescaped <
, >
, and &
characters that confuse browsers and adversely affect rendering.
Sanitizes a block of CSS code. Used by sanitize
when it comes across a style attribute in HTML being sanitized.
Strips all link tags from text leaving just the link text.
1 strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
2 # => Ruby on Rails
3
4 strip_links('Please email me at <a href="mailto:[email protected]">[email protected]</a>.')
5 # => Please email me at [email protected].
6
7 strip_links('Blog: <a href="http://www.myblog.com/" class="nav">Visit</a>.')
8 # => Blog: Visit
Strips all tags from the supplied HTML string, including comments. Its HTML parsing ability is limited by that of the HTML scanner tokenizer built into Rails.6
6. You can examine the source code of the html scanner yourself by opening up https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb
1 strip_tags("Strip <i>these</i> tags!")
2 # => Strip these tags!
3
4 strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
5 # => Bold no more! See more here...
6
7 strip_tags("<div id='top-bar'>Welcome to my website!</div>")
8 # => Welcome to my website!
This module provides helper methods for generating HTML tags programmatically.
Returns a CDATA section wrapping the given content
. CDATA sections are used to escape blocks of text containing characters that would otherwise be recognized as markup. CDATA sections begin with the string <![CDATA[
and end with (and may not contain) the string ]]>
.
Returns an HTML block tag of type name
surrounding the content. Add HTML attributes by passing an attributes hash as options
. Instead of passing the content as an argument, you can also use a block to hold additional markup (and/or additional calls to content_tag
), in which case you pass your options
as the second parameter. Set escape
to false to disable attribute value escaping.
Here are some simple examples of using content_tag
without a block:
1 content_tag(:p, "Hello world!")
2 # => <p>Hello world!</p>
3
4 content_tag(:div, content_tag(:p, "Hello!"), class: "message")
5 # => <div class="message"><p>Hello!</p></div>
6
7 content_tag("select", options, multiple: true)
8 # => <select multiple="multiple">...options...</select>
Here it is with content in a block (shown as template code rather than in the console):
= content_tag :div, class: "strong" do
Hello world!
The preceding code produces the following HTML:
<div class="strong">Hello world!</div>
Returns an escaped version of HTML without affecting existing escaped entities.
1 escape_once("1 > 2 & 3")
2 # => "1 < 2 & 3"
3
4 escape_once("<< Accept & Checkout")
5 # => "<< Accept & Checkout"
Returns an empty HTML tag of type name
, which by default is XHTML compliant. Setting open to true
will create an open tag compatible with HTML 4.0 and below. Add HTML attributes by passing an attributes hash to options. Set escape to false to disable attribute value escaping.
The options hash is used with attributes with no value (e.g., disabled
and readonly
), which you can give a value of true
in the options hash. You can use symbols or strings for the attribute names.
1 tag("br")
2 # => <br />
3
4 tag("br", nil, true)
5 # => <br>
6
7 tag("input", type: 'text', disabled: true)
8 # => <input type="text" disabled="disabled" />
9
10 tag("img", src: "open.png")
11 # => <img src="open.png" />
The methods in this module provide filtering, formatting, and string transformation capabilities.
The preferred method of outputting text in your views is to use the = expression
in Haml syntax or the <%= expression %>
in eRuby syntax. The regular puts and print methods do not operate as expected in an eRuby code block—that is, if you expected them to output to the browser. If you absolutely must output text within a nonoutput code block like - expression
in Haml or <% expression %>
in eRuby, you can use the concat
method. I’ve found that this method can be especially useful when combined with capture
in your own custom helper method implementations.
The following example code defines a helper method that wraps its block content in a div with a particular CSS class.
1 def wrap(&block)
2 concat(content_tag(:div, capture(&block), class: "wrapped_content"))
3 end
You would use it in your template as follows:
1 - wrap do
2 My wrapped content
Returns the current cycle string after a cycle has been started. Useful for complex table highlighting or any other design need that requires the current cycle string in more than one place.
1 - # Alternate background colors with coordinating text color.
2 - [1,2,3,4].each do |item|
3 %div(style="background-color:#{cycle('red', 'green', 'blue')}")
4 %span(style="color:dark#{current_cycle}")= item
Creates a Cycle
object whose to_s
method cycles through elements of the array of values passed to it every time it is called. This can be used, for example, to alternate classes for table rows. Here’s an example that alternates CSS classes for even and odd numbers, assuming that the @items
variable holds an array with 1 through 4:
1 %table
2 - @items.each do |item|
3 %tr{ class: cycle('even', 'odd') }
4 %td= item
As you can tell from the example, you don’t have to store the reference to the cycle in a local variable or anything like that; you just call the cycle
method repeatedly. That’s convenient, but it means that nested cycles need an identifier. The solution is to pass cycle a name: cycle_name
option as its last parameter. Also, you can manually reset a cycle by calling reset_cycle
and passing it the name of the cycle to reset. For example, here is some data to iterate over:
1 # Cycle CSS classes for rows and text colors for values within each row.
2 @items = [{first: 'Robert', middle: 'Daniel', last: 'James'},
3 {first: 'Emily', last: 'Hicks'},
4 {first: 'June', middle: 'Dae', last: 'Jones'}]
And here is the template code. Since the number of cells rendered varies, we want to make sure to reset the colors cycle before looping:
1 - @items.each do |item|
2 %tr{ class: cycle('even', 'odd', name: 'row_class') }
3 - item.values.each do |value|
4 %td{ class: cycle('red', 'green', name: 'colors') }
5 = value
6 - reset_cycle 'colors'
Extracts an excerpt from text that matches the first instance of phrase
. The :radius
option expands the excerpt on each side of the first occurrence of phrase
by the number of characters defined in :radius
(which defaults to 100). If the excerpt radius overflows the beginning or end of the text, the :omission
option will be prepended/appended accordingly. Use the :separator
option to set the delimitation. If the phrase isn’t found, nil is returned.
1 excerpt('This is an example', 'an', radius: 5)
2 # => "...s is an examp..."
3
4 excerpt('This is an example', 'is', radius: 5)
5 # => "This is an..."
6
7 excerpt('This is an example', 'is')
8 # => "This is an example"
9
10 excerpt('This next thing is an example', 'ex', radius: 2)
11 # => "...next..."
12
13 excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
14 # => "<chop> is also an example"
Highlights one or more phrases everywhere in text by inserting into a highlighter template. The highlighter can be specialized by passing the option :highlighter
as a single-quoted string with 1
where the phrase is to be inserted.
1 highlight('You searched for: rails', 'rails')
2 # => You searched for: <mark>rails</mark>
3
4 highlight('You searched for: ruby, rails, dhh', 'actionpack')
5 # => You searched for: ruby, rails, dhh
6
7 highlight('You searched for: rails', ['for', 'rails'],
8 highlighter: '<em>1</em>')
9 # => You searched <em>for</em>: <em>rails</em>
10
11 highlight('You searched for: rails', 'rails',
12 highlighter: '<a href="search?q=1">1</a>')
13 # => You searched for: <a href="search?q=rails">rails</a>
Note that as of Rails 4, the highlight
helper now uses the HTML5 mark
tag by default.
Attempts to pluralize the singular word unless count is 1. If the plural is supplied, it will use that when count is > 1. If the ActiveSupport Inflector
is loaded, it will use the Inflector
to determine the plural form; otherwise, it will just add an “s” to the singular word.
1 pluralize(1, 'person')
2 # => 1 person
3
4 pluralize(2, 'person')
5 # => 2 people
6
7 pluralize(3, 'person', 'users')
8 # => 3 users
9
10 pluralize(0, 'person')
11 # => 0 people
Resets a cycle (see the cycle
method in this section) so that it starts cycling from its first element the next time it is called. Pass in a name
to reset a named cycle.
Returns text transformed into HTML using simple formatting rules. Two or more consecutive newlines (
) are considered to denote a paragraph and thus are wrapped in p
tags. One newline (
) is considered to be a line break and a br
tag is appended. This method does not remove the newlines from the text.
Any attributes set in html_options
will be added to all outputted paragraphs. The following options are also available:
:sanitize Setting this option to false will not sanitize any text.
:wrapper_tag A string representing the wrapper tag. Defaults to "p"
.
If text
is longer than the :length
option (defaults to 30), text
will be truncated to the length specified and the last three characters will be replaced with the :omission
(defaults to "..."
). The :separator
option allows defining the delimitation. Finally, to not escape the output, set :escape
to false.
1 truncate("Once upon a time in a world far far away", length: 7)
2 => "Once..."
3
4 truncate("Once upon a time in a world far far away")
5 # => "Once upon a time in a world..."
6
7 truncate("And they found that many people were sleeping better.",
8 length: 25, omission: '... (continued)')
9 # => "And they f... (continued)"
Wraps the text into lines no longer than the :line_width
option. This method breaks on the first whitespace character that does not exceed :line_width
(which is 80 by default).
1 word_wrap('Once upon a time')
2 # => Once upon a time
3
4 word_wrap('Once upon a time', line_width: 8)
5 # => Once
upon a
time
6
7 word_wrap('Once upon a time', line_width: 1)
8 # => Once
upon
a
time
I18n stands for internationalization and the I18n gem that ships with Rails makes it easy to support multiple languages other than English in your Rails applications. When you internationalize your app, you do a sweep of all the textual content in your models and views that needs to be translated, as well as demarking data like currency and dates, which should be subject to localization.7
7. This section is an authorized remix of the complete guide to using I18n in Rails by Sven Fuchs and Karel Minarik, available at http://guides.rubyonrails.org/i18n.html
Rails provides an easy-to-use and extensible framework for translating your application to a single custom language other than English or for providing multilanguage support in your application.
The process of internationalization in Rails involves the abstraction of strings and other locale-specific parts of your application (such as dates and currency formats) out of the codebase and into a locale file.
The process of localization means to provide translations and localized formats for the abstractions created during internationalization. In the process of localizing your application, you’ll probably want to do following three things:
• Replace or add to Rails’ default locale.
• Add abstract strings used in your application to keyed dictionaries—for example, flash messages, static text in your views, and so on.
• Store the resulting dictionaries somewhere.
Internationalization is a complex problem. Natural languages differ in so many ways (e.g., in pluralization rules) that it is hard to provide tools for solving all problems at once. For that reason, the Rails I18n API focuses on the following:
• Providing support for English and similar languages by default
• Making it easy to customize and extend everything for other languages
As part of this solution, every static string in the Rails framework—for example, Active Record validation messages, time and date formats, and so on—has been internationalized, so localization of a Rails application means overriding Rails defaults.
Before diving into the more complicated localization techniques, let’s briefly cover a simple way to translate views that is useful for content-heavy pages. Assume you have a BooksController
in your application. Your index
action renders content in app/views/books/index.html.haml
template. When you put a localized variant of that template such as index.es.html.haml
in the same directory, Rails will recognize it as the appropriate template to use when the locale is set to :es
. If the locale is set to the default, the generic index.html.haml
view will be used normally.
You can make use of this feature when working with a large amount of static content that would be clumsy to maintain inside locale dictionaries. Just bear in mind that any changes to a template must be kept in sync with all its translations.
The following two methods are provided for use in your views and assume that I18n support is set up in your application.
Delegates to Active Support’s I18n#translate
method with no additional functionality. Normally, you would want to use translate
instead.
Delegates to Active Support’s I18n#translate
method while performing three additional functions. First, it’ll catch MissingTranslationData
exceptions and turn them into inline spans that contain the missing key so that you can see them within your views when keys are missing.
Second, it’ll automatically scope the key provided by the current partial if the key starts with a period. So if you call translate(".foo")
from the people/index.html.haml
template, you’ll be calling I18n.translate("people.index.foo")
. This makes it less repetitive to translate many keys within the same partials and gives you a simple framework for scoping them consistently. If you don’t prepend the key with a period, nothing is converted.
Third, it’ll mark the translation as safe HTML if the key has the suffix “_html” or the last element of the key is the word “html.” For example, calling translate (“header.html”) will return a safe HTML string that won’t be escaped.
There are just a few simple steps to get up and running with I18n support for your application.
Following the convention over configuration philosophy, Rails will set up your application with reasonable defaults. If you need different settings, you can overwrite them easily.
Rails adds all .rb
and .yml
files from the config/locales
directory to your translations load path automatically.8 The default en.yml
locale in this directory contains a sample pair of translation strings:
8. The translations load path is just an array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you.
1 en:
2 hello: "Hello world"
This means that in the :en
locale, the key hello
will map to the “Hello world” string.9
9. Every string inside Rails is internationalized in this way; see, for instance, Active Record validation messages in the file or time and date formats in the file.
You can use YAML or standard Ruby hashes to store translations in the default (Simple
) backend.
Unless you change it, the I18n library will use English (:en
) as its default locale for looking up translations. Change the default in using code similar to the following:
config.i18n.default_locale = :de
Note
The I18n library takes a pragmatic approach to locale keys after some discussion,10 including only the locale (“language”) part, like :en
, :pl
, not the region part, like :en-US
or :en-UK
, which are traditionally used for separating “languages” and “regional setting” or “dialects.” Many international applications use only the “language” element of a locale such as :cz
, :th
, or :es
(for Czech, Thai, and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the :en-US
locale, you would have $ as a currency symbol, while in :en-UK
, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full “English–United Kingdom” locale in a :en-UK
dictionary. Rails I18n plugins such as Globalize311 may help you implement it.
10. https://groups.google.com/forum/?hl=en#!topic/rails-i18n/FN7eLH2-lHA
11. https://github.com/svenfuchs/globalize3
If you want to translate your Rails application to a single language other than English, you can just set default_locale
to your locale in application.rb
as shown earlier, and it will persist through the requests. However, you probably want to provide support for more locales in your application, depending on the user’s preference. In such case, you need to set and pass the locale between requests.
Warning
You may be tempted to store the chosen locale in a session or a cookie. Do not do so. The locale should be transparent and a part of the URL. This way, you don’t break people’s basic assumptions about the web itself: If you send a URL of some page to a friend, she should see the same page and the same content.
You can set the locale in a before_action
in your ApplicationController
like the following:
1 before_action :set_locale
2
3 def set_locale
4 # If params[:locale] is nil, then I18n.default_locale will be used.
5 I18n.locale = params[:locale]
6 end
This approach requires you to pass the locale as a URL query parameter, as in http://example.com/books?locale=pt
. (This is, for example, Google’s approach.)
Getting the locale from params
and setting it accordingly is not the hard part of this technique. Including the locale parameter in every URL generated by your application is the hard part. To include an explicit option in every URL, as illustrated by
= link_to books_url(locale: I18n.locale)
would be tedious at best and impossible to maintain at worst.
A default_url_options
method in ApplicationController
is useful precisely in this scenario. It enables us to set defaults for url_for
and helper methods dependent on it.
1 def default_url_options(options={})
2 logger.debug "default_url_options is passed options: #{options.inspect}
"
3 { locale: I18n.locale }
4 end
Every helper method dependent on url_for
(e.g., helpers for named routes like root_path
or root_url
, resource routes like books_path
or books_url
, etc.) will now automatically include the locale in the query string, like this:
http://localhost:3000/?locale=ja
Having the locale hang at the end of every path in your application can negatively impact readability of your URLs. Moreover, from an architectural standpoint, locales are a concept that live above other parts of your application domain, and your URLs should probably reflect that.
You might want your URLs to look more like www.example.com/en/books
(which loads the English locale) and www.example.com/nl/books
(which loads the Netherlands locale). This is achievable with the same default_url_options
strategy we just reviewed. You just have to set up your routes with a scope
option in this way:
1 # config/routes.rb
2 scope "/:locale" do
3 resources :books
4 end
Even with this approach, you still need to take special care of the root URL of your application. A URL like http://localhost:3000/nl
will not work automatically, because the root "books#index"
declaration in your routes.rb
doesn’t take locale into account. After all, there should only be one “root” of your website.
A possible solution is to map a URL like this:
# config/routes.rb
get '/:locale' => "dashboard#index"
Do take special care about the order of your routes, so this route declaration does not break other ones. It would be most wise to add it directly before the root
declaration at the end of your routes file.
Warning
This solution has currently one rather big downside. Due to the default_url_options
implementation, you have to pass the :id
option explicitly, like link_to 'Show', book_url(id: book)
, and not depend on Rails’ magic in code like link_to 'Show', book
. If this should be a problem, have a look at Sven Fuchs’s routing_filter12 plugin, which simplifies work with routes in this way.
12. https://github.com/svenfuchs/routing-filter
Another option you have is to set the locale from the domain name where your application runs. For example, we want www.example.com
to load the English (or default) locale and www.example.es
to load the Spanish locale. Thus the top-level domain name is used for locale setting. This has several advantages:
• The locale is a very obvious part of the URL.
• People intuitively grasp in which language the content will be displayed.
• It is very trivial to implement in Rails.
• Search engines seem to like that content in different languages lives at different, interlinked domains.
You can implement it like this in your ApplicationController
:
1 before_action :set_locale
2
3 def set_locale
4 I18n.locale = extract_locale_from_uri
5 end
6
7 # Get locale from top-level domain or return nil.
8 def extract_locale_from_tld
9 parsed_locale = request.host.split('.').last
10 (available_locales.include? parsed_locale) ? parsed_locale : nil
11 end
Try adding localhost aliases to your file to test this technique.
127.0.0.1 application.com
127.0.0.1 application.it
127.0.0.1 application.pl
We can also set the locale from the subdomain in a very similar way inside of ApplicationController
.
1 before_action :set_locale
2
3 def set_locale
4 I18n.locale = extract_locale_from_uri
5 end
6
7 def extract_locale_from_subdomain
8 parsed_locale = request.subdomains.first
9 (available_locales.include? parsed_locale) ? parsed_locale : nil
10 end
In specific cases, it would make sense to set the locale from client-supplied information—that is, not from the URL. This information may come from the users’ preferred language (set in their browser), it can be based on the users’ geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface, saving it to their profile. This approach is more suitable for web-based applications or services, not for websites.
One source of client-supplied information would be an Accept-Language
HTTP header. People may set this in their browser13 or other clients (such as curl
).
13. http://www.w3.org/International/questions/qa-lang-priorities
A trivial implementation of setting locale based on the Accept-Language
header in ApplicationController
might be the following:
1 before_action :set_locale
2
3 def set_locale
4 I18n.locale = extract_locale_from_accept_language_header
5 logger.debug "* Locale set to '#{I18n.locale}'"
6 end
7
8 private
9
10 def extract_locale_from_accept_language_header
11 request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
12 end
In real production environments, you should use much more robust code than the previous example. Try plugins such as Iain Hecker’s http_accept_language14 or even Rack middleware such as locale.15
14. https://github.com/iain/http_accept_language
15. https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb
Yet another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as GeoIP Lite Country.16 The mechanics of the code would be very similar to the previous code—you would need to query the database for the user’s IP and look up your preferred locale for the country/region/city returned.
16. http://dev.maxmind.com/geoip/legacy/geolite/
You can also provide users of your application with means to set (and possibly override) the locale in your application interface. Again, mechanics for this approach would be very similar to the previous code—you’d probably let users choose a locale from a drop-down list and save it to their profile in the database. Then you’d set the locale to this value using a before_action
in ApplicationController
.
After you’ve set up I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests, you’re ready for the really interesting part of the process: actually internationalizing your application.
First of all, you should be acquainted with the I18n API. The two most important methods of the I18n API are the following:
translate # Look up text translations.
localize # Localize Date and Time objects to local formats.
These have the aliases #t
and #l
so you can use them like the following:
I18n.t 'store.title'
I18n.l Time.now
Take the following basic pieces of a simple Rails application as an example for describing the process.
1 # config/routes.rb
2 Rails.application.routes.draw do
3 root "home#index"
4 end
5
6 # app/controllers/home_controller.rb
7 class HomeController < ApplicationController
8 def index
9 flash[:notice] = "Welcome"
10 end
11 end
12
13 # app/views/home/index.html.haml
14 %h1 Hello world!
15 %p.notice= flash[:notice]
The example has two strings that are currently hard-coded in English. To internationalize this code, we must replace those strings with calls to Rails’ #t
helper with a key that makes sense for the translation.
1 # app/controllers/home_controller.rb
2 class HomeController < ApplicationController
3 def index
4 flash[:notice] = t(:welcome_flash)
5 end
6 end
7
8 # app/views/home/index.html.haml
9 %h1= t(:hello_world)
10 %p.notice= flash[:notice]
Now when you render this view, it will show an error message that tells you that the translations for the keys :hello_world
and :welcome_flash
are missing.
Rails adds a t
(translate
) helper method to your views so that you do not need to spell out I18n.t
all the time. Additionally, this helper will catch missing translations and wrap the resulting error message into a <span class="translation_missing">
.
To make the example work, you would add the missing translations into the dictionary files (thereby doing the localization part of the work):
1 # config/locale/en.yml
2 en:
3 hello_world: Hello World
4 welcome_flash: Welcome
5
6 # config/locale/pirate.yml
7 pirate:
8 hello_world: Ahoy World
9 welcome_flash: All aboard!
Note
You need to restart the server when you add or edit locale files.
You may use YAML (.yml
) or plain Ruby (.rb
) files for storing your translations. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you can easily find what’s wrong. (If you encounter any “weird issues” with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.)
Okay! Now let’s add a timestamp to the view so we can demo the date/time localization feature as well. To localize the time format, you pass the time object to I18n.l
or use Rails’ #l
helper method in your views.
1 # app/views/home/index.html.haml
2 %h1= t(:hello_world)
3 %p.notice= flash[:notice]
4 %p= l(Time.now, format: :short)
And in our pirate translations file, let’s add a time format (it’s already there in Rails’ defaults for English):
1 # config/locale/pirate.yml
2 pirate:
3 time:
4 formats:
5 short: "arrrround %H'ish"
The rails-i18n
repository
There’s a great chance that somebody has already done much of the hard work of translating Rails’ defaults for your locale. See the Rails-I18n repository at GitHub17 for an archive of various locale files. When you put such file(s) in config/locale/
directory, they will automatically be ready for use.
17. https://github.com/svenfuchs/rails-i18n
Putting translations for all parts of your application in one file per locale could be hard to manage. You can store these files in a hierarchy that makes sense to you.
For example, your config/locale
directory could look like this:
|-defaults
|---es.rb
|---en.rb
|-models
|---book
|-----es.rb
|-----en.rb
|-views
|---defaults
|-----es.rb
|-----en.rb
|---books
|-----es.rb
|-----en.rb
|---users
|-----es.rb
|-----en.rb
|---navigation
|-----es.rb
|-----en.rb
This way, you can separate model and model attribute names from text inside views and all of those values from the “defaults” (e.g., date and time formats). Other stores for the I18n library could provide different means of such separation.
Note
The default locale loading mechanism in Rails does not load locale files in nested dictionaries, like we have here. So for this to work, we must explicitly tell Rails to look further through settings in the following:
1 # config/application.rb
2 config.i18n.load_path += Dir[File.join(Rails.root, 'config',
3 'locales', '**', '*.{rb,yml}')]
Translations are looked up by keys that can be both symbols or strings, so these calls are equivalent:
I18n.t :message
I18n.t 'message'
The translate
method also takes a :scope
option that can contain one or more additional keys that will be used to specify a “namespace” or scope for a translation key:
I18n.t :invalid, scope: [:activerecord, :errors, :messages]
This looks up the :invalid
message in the Active Record error messages.
Additionally, both the key and scopes can be specified as dot-separated keys as in the following:
I18n.translate :"activerecord.errors.messages.invalid"
Thus the following four calls are equivalent:
I18n.t 'activerecord.errors.messages.invalid'
I18n.t 'errors.messages.invalid', scope: :activerecord
I18n.t :invalid, scope: 'activerecord.errors.messages'
I18n.t :invalid, scope: [:activerecord, :errors, :messages]
When a :default
option is given, its value will be returned if the translation is missing:
I18n.t :missing, default: 'Not here'
# => 'Not here'
If the :default
value is a symbol, it will be used as a key and translated. One can provide multiple values as default. The first one that results in a value will be returned.
For example, the following are first tries to translate the key :missing
and then the key :also_missing.
As both do not yield a result, the string “Not here” will be returned:
I18n.t :missing, default: [:also_missing, 'Not here']
# => 'Not here'
To look up multiple translations at once, an array of keys can be passed:
I18n.t [:odd, :even], scope: 'activerecord.errors.messages'
# => ["must be odd", "must be even"]
Also, a key can translate to a (potentially nested) hash of grouped translations. For instance, one can receive all Active Record error messages as a hash with the following:
I18n.t 'activerecord.errors.messages'
# => { inclusion: "is not included in the list", exclusion: ... }
Rails implements a convenient way to reference keys inside of views. Assume you have the following local file:
1 es:
2 books:
3 index:
4 title: "Título"
You can reference the value of books.index.title
inside of the app/views/books/index.html.haml
template by prefixing the key name with a dot. Rails will automatically fill in the scope based on the identity of the view.
In many cases you want to abstract your translations in such a way that variables can be interpolated into the translation. For this reason, the I18n API provides an interpolation feature.
All options besides :default
and :scope
that are passed to translate
will be interpolated to the translation:
I18n.backend.store_translations :en, thanks: 'Thanks, %{name}!
I18n.translate :thanks, name: 'Jeremy'
# => 'Thanks, Jeremy!'
If a translation uses :default
or :scope
as an interpolation variable, an I18n::ReservedInterpolationKey
exception is raised. If a translation expects an interpolation variable but this has not been passed to translate
, an I18n::MissingInterpolationArgument
exception is raised.
In English there are only one singular and one plural form for a given string—for example, “1 message” and “2 messages”—but other languages have different grammars with additional or fewer plural forms.18 Thus the I18n API provides a flexible pluralization feature.
18. http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html
The :count
interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by Unicode:
1 I18n.backend.store_translations :en, inbox: {
2 one: '1 message',
3 other: '%{count} messages'
4 }
5
6 I18n.translate :inbox, count: 2
7 # => '2 messages'
8
9 I18n.translate :inbox, count: 1
10 # => 'one message'
The algorithm for pluralizations in :en
is as simple as the following:
1 entry[count == 1 ? 0 : 1]
The translation denoted as :one
is regarded as singular versus any other value regarded as plural (including the count being zero).
If the lookup for the key does not return a hash suitable for pluralization, an I18n::InvalidPluralizationData
exception is raised.
The Simple
backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format. A Ruby hash locale file would look like this:
1 {
2 pt: {
3 foo: {
4 bar: "baz"
5 }
6 }
7 }
The equivalent YAML file would look like this:
1 pt:
2 foo:
3 bar: baz
In both cases, the top-level key is the locale. :foo
is a namespace key and :bar
is the key for the translation “baz.”
Here is a real example from the Active Support en.yml
translations YAML file:
1 en:
2 date:
3 formats:
4 default: "%Y-%m-%d"
5 short: "%b %d"
6 long: "%B %d, %Y"
So all the following equivalent lookups will return the :short
date format "%B %d"
:
1 I18n.t 'date.formats.short'
2 I18n.t 'formats.short', scope: :date
3 I18n.t :short, scope: 'date.formats'
4 I18n.t :short, scope: [:date, :formats]
Generally, we recommend using YAML as a format for storing translations.
You can use the methods Model.human_name
and Model.human_attribute_name(attribute)
to transparently look up translations for your model and attribute names.
For example, when you add the following translations
1 en:
2 activerecord:
3 models:
4 user: Dude
5 attributes:
6 user:
7 login: "Handle"
8 # will translate User attribute "login" as "Handle"
User.human_name
will return “Dude” and User.human_attribute_name(:login)
will return “Handle.”
Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account.
This gives you powerful means to flexibly adjust your messages to your application’s needs.
Consider a User
model with a validates_presence_of
validation for the name attribute like this:
1 class User < ActiveRecord::Base
2 validates_presence_of :name
3 end
The key for the error message in this case is :blank
. Active Record will look up this key in the namespaces:
1 activerecord.errors.models.[model_name].attributes.[attribute_name]
2 activerecord.errors.models.[model_name]
3 activerecord.errors.messages
Thus in our example it will try the following keys in this order and return the first result:
1 activerecord.errors.models.user.attributes.name.blank
2 activerecord.errors.models.user.blank
3 activerecord.errors.messages.blank
When your models are additionally using inheritance, then the messages are looked up in the inheritance chain.
For example, you might have an Admin
model inheriting from User
:
1 class Admin < User
2 validates_presence_of :name
3 end
Then Active Record will look for messages in this order:
1 activerecord.errors.models.admin.attributes.title.blank
2 activerecord.errors.models.admin.blank
3 activerecord.errors.models.user.attributes.title.blank
4 activerecord.errors.models.user.blank
5 activerecord.errors.messages.blank
This way, you can provide special translations for various error messages at different points in your models’ inheritance chain and in the attributes, models, or default scopes.
The translated model name, translated attribute name, and value are always available for interpolation.
So, for example, instead of the default error message "cannot be blank"
, you could use the attribute name like "Please fill in your %{attribute}"
.
Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here’s a brief overview.
• distance_of_time_in_words
translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See datetime.distance_in_words
19 translations.
19. https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_view/locale/en.yml#L4
• datetime_select
and select_month
use translated month names for populating the resulting select tag. See date.month_names
20 for translations. datetime_select
also looks up the order option from date.order
21 (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the datetime.prompts22 scope if applicable. Note the number_to_curr
ency, number_with_precision
, number_to_percentage
, number_with_delimiter
, and number_to_human_size
helpers use the number format settings located in the number23 scope.
20. https://github.com/rails/rails/blob/4-0-stable/activesupport/lib/active_support/locale/en.yml#L155
21. https://github.com/rails/rails/blob/4-0-stable/activesupport/lib/active_support/locale/en.yml#L18
22. https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_view/locale/en.yml#L39
23. https://github.com/rails/rails/blob/4-0-stable/activesupport/lib/active_support/locale/en.yml#L37
• human_name
and human_attribute_name
use translations for model names and attribute names if available in the activerecord.models24 scope. They also support translations for inherited class names (e.g., for use with STI) as explained in “Error message scopes.”
24. https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/locale/en.yml#L37
• ActiveRecord::Errors#generate_message
(which is used by Active Record validations but may also be used manually) uses human_name
and human_attribute_name
. It also translates the error message and supports translations for inherited class names as explained in “Error message scopes.”
• ActiveRecord::Errors#full_messages
prepends the attribute name to the error message using a separator that will be looked up from activerecord.errors.format
(and that defaults to "%{attribute} %{message}"
).
Array#to_sentence
uses format settings as given in the support.array
scope.
In some contexts you might want to extend I18n’s default exception handling behavior. For instance, the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose, a different exception handler can be specified. The specified exception handler must be a method on the I18n module. You would add code similar to the following to your file or other kind of initializer.
1 module I18n
2 def just_raise_that_exception(*args)
3 raise args.first
4 end
5 end
6
7 I18n.exception_handler = :just_raise_that_exception
This would reraise all caught exceptions, including MissingTranslationData
.
This module provides a set of methods for making links and getting URLs that depend on the routing subsystem, covered extensively in Chapter 2, “Routing,” and Chapter 3, “REST, Resources, and Rails.”
Generates a form containing a single button that submits to the URL created by the set of options. This is the safest method to ensure that links that cause changes to your data are not triggered by search bots or accelerators. If the HTML button does not work with your layout, you can also consider using the link_to
method (also in this module) with the :method
modifier.
The options hash accepts the same options as the url_for
method.
The generated form element has a class name of button-to
to allow styling of the form itself and its children. This class name can be overridden by setting :form_class
in :html_options
. The :method
option works just like the link_to
helper. If no :method
modifier is given, it defaults to performing a POST operation.
1 button_to("New", action: "new")
2 # => "<form method="post" action="/controller/new" class="button-to">
3 # <div><input value="New" type="submit" /></div>
4 # </form>"
5
6 button_to "Delete Image", { action: "delete", id: @image.id },
7 method: :delete, data: { confirm: "Are you sure?" }
8 # => "<form method="post" action="/images/delete/1" class="button_to">
9 # <div>
10 # <input type="hidden" name="_method" value="delete" />
11 # <input data-confirm='Are you sure?'
12 # value="Delete Image" type="submit" />
13 # <input name="authenticity_token" type="hidden"
14 # value="10f2163b45388899..."/>
15 # </div>
16 # </form>"
Returns true
if the current request URI was generated by the given options. For example, let’s assume that we’re currently rendering the /shop/checkout
action:
1 current_page?(action: 'process')
2 # => false
3
4 current_page?(action: 'checkout') # controller is implied
5 # => true
6
7 current_page?(controller: 'shop', action: 'checkout')
8 # => true
One of the fundamental helper methods. Creates a link tag of the given name using a URL created by the set of options. The valid options are covered in the description of this module’s url_for
method. It’s also possible to pass a string instead of an options hash to get a link tag that uses the value of the string as the href for the link. If nil
is passed as a name, the link itself will become the name.
:data Adds custom data attributes.
method: symbol Specify an alternative HTTP verb for this request (other than GET). This modifier will dynamically create an HTML form and immediately submit the form for processing using the HTTP verb specified (:post
, :patch
, or :delete
).
remote: true Allows the unobtrusive JavaScript driver to make an Ajax request to the URL instead of following the link.
The following data attributes work alongside the unobtrusive JavaScript driver:
confirm: 'question?' The unobtrusive JavaScript driver will display a JavaScript confirmation prompt with the question specified. If the user accepts, the link is processed normally; otherwise, no action is taken.
:disable_with Used by the unobtrusive JavaScript driver to provide a name for disabled versions.
Generally speaking, GET requests should be idempotent—that is, they do not modify the state of any resource on the server and can be called one or many times without a problem. Requests that modify server-side resources or trigger dangerous actions like deleting a record should not usually be linked with a normal hyperlink, since search bots and so-called browser accelerators can follow those links while spidering your site, leaving a trail of chaos.
If the user has JavaScript disabled, the request will always fall back to using GET, no matter what :method
you have specified. This is accomplished by including a valid href
attribute. If you are relying on the POST behavior, your controller code should check for it using the post?
, delete?
, or patch?
methods of request
.
As usual, the html_options
will accept a hash of HTML attributes for the link tag.
1 = link_to "Help", help_widgets_path
2
3 = link_to "Rails", "http://rubyonrails.org/",
4 data: { confirm: "Are you sure?" }
5
6 = link_to "Delete", widget_path(@widget), method: :delete,
7 data: { confirm: "Are you sure?" }
8
9 [Renders in the browser as...]
10
11 <a href="/widgets/help">Help</a>
12
13 <a href="http://rubyonrails.org/" data-confirm="Are you sure?">Rails</a>
14
15 <a href="/widgets/42" rel="nofollow" data-method="delete"
16 data-confirm="Are you sure?">View</a>
Creates a link tag using the same options as link_to
if the condition is true
; otherwise, only the name
is output (or block
is evaluated for an alternative value, if one is supplied).
Creates a link tag using the same options as link_to
unless the condition is true
, in which case only the name
is output (or block is evaluated for an alternative value, if one is supplied).
Creates a link tag using the same options as link_to
unless the condition is true
, in which case only the name
is output (or block
is evaluated for an alternative value, if one is supplied).
This method is pretty useful sometimes. Remember that the block given to link_to_unless_current
is evaluated if the current action is the action given. So if we had a comments page and wanted to render a “Go Back” link instead of a link to the comments page, we could do something like the following:
1 link_to_unless_current("Comment", { controller: 'comments', action: 'new}) do
2 link_to("Go back", posts_path)
3 end
Creates a mailto
link tag to the specified email_address
, which is also used as the name of the link unless name
is specified. Additional HTML attributes for the link can be passed in html_options
.
The mail_to
helper has several methods for customizing the email address itself by passing special keys to html_options
:
:subject The subject line of the email.
:body The body of the email.
:cc Add cc recipients to the email.
:bcc Add bcc recipients to the email.
Here are some examples of usages:
1 mail_to "[email protected]"
2 # => <a href="mailto:[email protected]">[email protected]</a>
3
4 mail_to "[email protected]", "My email"
5 # => <a href="mailto:[email protected]">My email</a>
6
7 mail_to "[email protected]", "My email", cc: "[email protected]",
8 subject: "This is an email"
9 # => <a href="mailto:[email protected][email protected]&
10 subject=This%20is%20an%20email">My email</a>
Note
In previous versions of Rails, the mail_to
helper provided options for encoding the email address to hinder email harvesters. If your application is still dependent on these options, add the actionview-encoded_mail_to
gem to your Gemfile
.
If you pass the magic symbol :back
to any method that uses url_for
under the covers (redirect_to
, etc.), the contents of the HTTP_REFERRER
request header will be returned. (If a referrer is not set for the current request, it will return javascript:history.back()
to try to make the browser go back one page.)
url_for(:back)
# => "javascript:history.back()"
As you develop an application in Rails, you should be on the lookout for opportunities to refactor duplicated view code into your own helper methods. As you think of these helpers, you add them to one of the helper modules defined in the app/helpers
folder of your application.
There is an art to effectively writing helper methods, similar in nature to what it takes to write effective APIs. Helper methods are basically a custom, application-level API for your view code. It is difficult to teach API design in a book form. It’s the sort of knowledge that you gain by apprenticing with more experienced programmers and lots of trial and error. Nevertheless, in this section, we’ll review some varied use cases and implementation styles that we hope will inspire you in your application design.
Here is a simple helper method that has been of use to me on many projects now. It’s called page_title
, and it combines two simple functions essential to a good HTML document:
• Setting the title
of the page in the document’s head
• Setting the content of the page’s h1
element
This helper assumes that you want the title
and h1
elements of the page to be the same and has a dependency on your application template. The code for the helper is in Listing 11.3 and would be added to app/helpers/application_helper.rb
, since it is applicable to all views.
1 def page_title(name)
2 content_for(:title) { name }
3 content_tag("h1", name)
4 end
First it sets content to be yielded in the layout as :title
and then it outputs an h1
element containing the same text. I could have used string interpolation on the second line, such as "<h1>#{name}</h1>"
, but it would have been sloppier than using the built-in Rails helper method content_tag
.
My application template is now written to yield :title
so that it gets the page title.
1 %html
2 %head
3 %title= yield :title
As should be obvious, you call the page_title
method in your view template where you want to have an h1
element:
1 - page_title "New User"
2 = form_for(user) do |f|
3 ...
Here’s another relatively simple helper. This time, instead of simply outputting data, we are encapsulating some view logic that decides whether to display a user’s profile photo or a placeholder image. It’s logic that you would otherwise have to repeat over and over again throughout your application.
The dependency (or contract) for this particular helper is that the user object being passed in has a profile_photo
associated to it, which is an attachment model based on Rick Olson’s old attachment_fu Rails plugin. The code in Listing 11.4 should be easy enough to understand without delving into the details of attachment_fu
. Since this is an example, I broke out the logic for setting src
into an if/else
structure; otherwise, this would be a perfect place to use Ruby’s ternary operator.
1 def photo_for(user, size=:thumb)
2 if user.profile_photo
3 src = user.profile_photo.public_filename(size)
4 else
5 src = 'user_placeholder.png'
6 end
7 link_to(image_tag(src), user_path(user))
8 end
Tim Says ...
Luckily, the latest generation of attachment plugins such as Paperclip and CarrierWave use a NullObject pattern to alleviate the need for you to do this sort of thing.
Lots of web applications feature user-interface concepts called breadcrumbs. They are made by creating a list of links, positioned near the top of the page, that display how far the user has navigated into a hierarchically organized application. I think it makes sense to extract breadcrumb
logic into its own helper method instead of leaving it in a layout template.
The trick to our example implementation (shown in Listing 11.5) is to use the presence of helper methods exposed by the controller, on a convention specific to your application, to determine whether to add elements to an array of breadcrumb links.
1 def breadcrumbs
2 return if controller.controller_name == 'home'
3
4 html = [link_to('Home', root_path)]
5
6 # first level
7 html << link_to(company.name, company) if respond_to? :company
8
9 # second level
10 html << link_to(department.name, department) if respond_to? :department
11
12 # third and final level
13 html << link_to(employee.name, employee) if respond_to? :employee
14
15 html.join(' > ').html_safe
16 end
Here’s the line-by-line explanation of the code, noting where certain application-design assumptions are made:
On line 2, we abort execution if we’re in the context of the application’s homepage
controller, since its pages don’t ever need breadcrumbs. A simple return with no value implicitly returns nil
, which is fine for our purposes. Nothing will be output to the layout template.
On line 4, we are starting to build an array of HTML links, held in the html
local variable, which will ultimately hold the contents of our breadcrumb trail. The first link of the breadcrumb trail always points to the home page of the application, which of course will vary, but since it’s always there, we use it to initialize the array. In this example, it uses a named route called root_path
.
After the html
array is initialized, all we have to do is check for the presence of the methods returning objects that make up the hierarchy (lines 7 to 13). It is assumed that if a department is being displayed, its parent company will also be in scope. If an employee is being displayed, both department and company will be in scope as well. This is not just an arbitrary design choice. It is a common pattern in Rails applications that are modeled on REST principles and use nested resource routes.
Finally, on line 15, the array of HTML links is joined with the > character to give the entire string the traditional breadcrumb appearance. The call to html_safe
tells the rendering system that this is HTML code and we’re cool with that—don’t sanitize it!
I don’t think that partials (by themselves) lead to particularly elegant or concise template code. Whenever there’s a shared partial template that gets used over and over again in my application, I will take the time to wrap it up in a custom helper method that conveys its purpose and formalizes its parameters. If appropriate, I might even generalize its implementation to make it more of a lightweight, reusable component. (Gasp!)
Let’s trace the steps to writing a helper method that wraps what I consider to be a general-purpose partial. Listing 11.6 contains code for a partial for a piece of a user interface that is common to many applications and generally referred to as a tile. It pairs a small thumbnail photo of something on the left side of the widget with a linked name and description on the right.
Tiles can also represent other models in your application, such as users and files. As I mentioned, tiles are a very common construct in modern user interfaces and operating systems. So let’s take the cities tiles partial and transform it into something that can be used to display other types of data.
Note
I realize that it has become passé to use HTML tables, and I happen to agree that div
-based layouts plus CSS are a lot more fun and flexible to work with. However, for the sake of simplicity in this example, and since the UI structure we’re describing is tabular, I’ve decided to structure it using a table.
1 %table.cities.tiles
2 - cities.in_groups_of(columns) do |row|
3 %tr
4 - row.each do |city|
5 %td[city]
6 .left
7 = image_tag(city.photo.url(:thumb))
8 .right
9 .title
10 = city.name
11 .description
12 = city.description
Since we’re going to transform this city-specific partial into a generalized UI component, I want to make sure that the code we start with makes absolute sense to you first. Before proceeding, I’m going through the implementation line by line and explaining what everything in Listing 11.6 does.
Line 1 opens up the partial with a table element and gives it semantically significant CSS classes so that the table and its contents can be properly styled.
Line 2 leverages a useful Array
extension method provided by Active Support called in_groups_of
. It uses both of the local variables: cities
and columns
. Both will need to be passed into this partial using the :locals
option of the render :partial
method. The cities
variable will hold the list of cities to be displayed, and columns
is an integer representing how many city tiles each row should contain. A loop iterates over the number of rows that will be displayed in this table.
Line 3 begins a table row using the tr
element.
Line 4 begins a loop over the tiles for each row to be displayed, yielding a city
for each.
Line 5 opens a td
element and uses Haml’s object reference notation to autogenerate a dom_id
attribute for the table cell in the style of city_98
, city_99
, and so on.
Line 6 opens a div
element for the left side of the tile and has the CSS class name needed so that it can be styled properly.
Line 7 calls the image_tag
helper to insert a thumbnail photo of the city.
Skipping along, lines 9–10 insert the content for the .title div
element—in this case, the name and state of the city.
Line 12 directly invokes the description
method.
In order to use this partial, we have to call render :partial
with the two required parameters specified in the :locals
hash:
1 = render "cities/tiles", cities: @user.cities, columns: 3
I’m guessing that most experienced Rails developers have written some partial code similar to this and tried to figure out a way to include default values for some of the parameters. In this case, it would be really nice to not have to specify :columns
all the time, since in most cases we want there to be three.
The problem is that since the parameters are passed via the :locals
hash and become local variables, there isn’t an easy way to insert a default value in the partial itself. If you left off the columns: n
part of your partial call, Rails would bomb with an exception about columns
not being a local variable or method. It’s not the same as an instance variable, which defaults to nil
and can be used willy-nilly.
Experienced Rubyists probably know that you can use the defined?
method to figure out whether a local variable is in scope or not, but the resulting code would be very ugly. The following code might be considered elegant, but it doesn’t work!25
25. If you want to know why it doesn’t work, you’ll have to buy the first book in this series: The Ruby Way (ISBN: 0-6723288-4-4).
columns = 3 unless defined? columns
Instead of teaching you how to jump through annoying Ruby idiom hoops, I’ll show you how to tackle this challenge the Rails way, and that is where we can start discussing the helper wrapping technique.
Tim Says ...
Obie might not want to make you jump through Ruby idiom hoops, but I don’t mind.
First, I’ll add a new helper method to the CitiesHelper
module of my application, like in Listing 11.7. It’s going to be fairly simple at first. In thinking about the name of the method, it occurs to me that I like the way that tiled(cities)
will read instead of tiles(cities)
, so I name it that way.
1 module CitiesHelper
2 def tiled(cities, columns=3)
3 render "cities/tiles", cities: cities, columns: columns
4 end
5 end
Right from the start, I can take care of that default columns
parameter by giving the helper method parameter for columns a default value. That’s just a normal feature of Ruby. Now instead of specifying the render :partial
call in my view template, I can simply write = tiled(cities)
, which is considerably more elegant and terse. It also serves to decouple the implementation of the tiled city table from the view. If I need to change the way that the tiled table is rendered in the future, I just have to do it in one place: the helper method.
Now that we’ve set the stage, the fun can begin. The first thing we’ll do is move the helper method to the ApplicationHelper
module so that it’s available to all view templates. We’ll also move the partial template file to app/views/shared/_tiled_table.html.haml
to denote that it isn’t associated with a particular kind of view and to more accurately convey its use. As a matter of good code style, I also do a sweep through the implementation and generalize the identifiers appropriately. The reference to cities
on line 2 becomes collection
. The block variable city
on line 4 becomes item
. Listing 11.8 has the new partial code.
1 %table.tiles
2 - collection.in_groups_of(columns) do |row|
3 %tr
4 - row.each do |item|
5 %td[item]
6 .left
7 = image_tag(item.photo.public_filename(:thumb))
8 .right
9 .title
10 = item.name
11 .description
12 = item.description
There’s still the matter of a contract between this partial code and the objects that it is rendering. Namely, they must respond to the following messages: photo
, name
, and description
. A survey of other models in my application reveals that I need more flexibility. Some things have names, but others have titles. Sometimes I want the description to appear under the name of the object represented, but other times I want to be able to insert additional data about the object plus some links.
Ruby allows you to store references to anonymous methods (also known as procs or lambdas) and call them whenever you want.26 Knowing this capability is there, what becomes possible? For starters, we can use lambdas to pass in blocks of code that will fill in parts of our partial dynamically.
26. If you’re familiar with Ruby already, you might know that Proc.new
is an alternate way to create anonymous blocks of code. I prefer lambda, at least in Ruby 1.9, because of subtle behavior differences. Lambda blocks check the arity of the argument list passed to them when call is invoked, and explicitly calling return in a lambda block works correctly.
For example, the current code for showing the thumbnail is a big problem. Since the code varies greatly depending on the object being handled, I want to be able to pass in instructions for how to get a thumbnail image without having to resort to big if/else
statements or putting view logic in my model classes. Please take a moment to understand the problem I’m describing, and then take a look at how we solve it in Listing 11.9. Hint: The thumbnail
, link
, title
, and description
variables hold lambdas!
1 .left
2 = link_to thumbnail.call(item), link.call(item)
3 .right
4 .title
5 = link_to title.call(item), link.call(item)
6 .description
7 = description.call(item)
Notice that in Listing 11.9, the contents of the left and right div
elements come from variables containing lambdas. On line 2, we make a call to link_to
, and both of its arguments are dynamic. A similar construct on line 5 takes care of generating the title link. In both cases, the first lambda should return the output of a call to image_tag
and the second should return a URL. In all of these lambda usages, the item
currently being rendered is passed to the lambdas as a block variable.
Wilson Says ...
Things like link.call(item)
could potentially look even sassier as link[item]
, except that you’ll shoot your eye out doing it. (Proc#[]
is an alias for Proc#call
.)
If you now direct your attention to Listing 11.10, you’ll notice that the tiled
method is changed considerably. In order to keep my positional argument list down to a manageable size, I’ve switched over to taking a hash of options as the last parameter to the tiled
method. This approach is useful, and it mimics the way that almost all helper methods take options in Rails.
Default values are provided for all parameters, and they are all passed along to the partial via the :locals
hash given to render
.
1 module ApplicationHelper
2
3 def tiled(collection, opts={})
4 opts[:columns] ||= 3
5
6 opts[:thumbnail] ||= lambda do |item|
7 image_tag(item.photo.url(:thumb))
8 end
9
10 opts[:title] ||= lambda { |item| item.to_s }
11
12 opts[:description] ||= lambda { |item| item.description }
13
14 opts[:link] ||= lambda { |item| item }
15
16 render "shared/tiled_table",
17 collection: collection,
18 columns: opts[:columns],
19 link: opts[:link],
20 thumbnail: opts[:thumbnail],
21 title: opts[:title],
22 description: opts[:description]
23 end
24 end
Finally, to wrap up this example, here’s a snippet showing how to invoke our new tiled
helper method from a template, overriding the default behavior for links:
1 tiled(cities, link: lambda { |city| showcase_city_path(city) })
The showcase_city_path
method is available to the lambda block since it is a closure, meaning that it inherits the execution context in which it is created.
This very long chapter served as a thorough reference of helper methods, both those provided by Rails and ideas for ones that you will write yourself. Effective use of helper methods leads to more elegant and maintainable view templates. At this point you should also have a good overview about how I18n support in Ruby on Rails works, and you should be ready to start translating your project.
Before we fully conclude our coverage of Action View, we’ll jump into the world of Ajax and JavaScript. Arguably, one of the main reasons for Rails’ continued popularity is its support for those two crucial technologies of Web 2.0.
This chapter is published under the Creative Commons Attribution-ShareAlike 4.0 license, http://creativecommons.org/licenses/by-sa/4.0/.
18.119.128.113