11.13.0.6 number_with_delimiter(number, options = {})

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"

11.13.0.7 number_with_precision(number, options = {})

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"

11.14 OutputSafetyHelper

This is an extremely simple helper module, barely worth mentioning.

11.14.0.1 raw(stringish)

Bypasses HTML sanitization by calling to_s and then html_safe on the argument passed to it.

11.14.0.2 safe_join(array, sep=$,)

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>"

11.15 RecordTagHelper

This module assists in creation of HTML markup code that follows good, clean naming conventions.

11.15.0.1 content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block)

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">..."

11.15.0.2 div_for(record, *args, &block)

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.

11.16 RenderingHelper

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

11.17 SanitizeHelper

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.

11.17.0.1 sanitize(html, options = {})

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.

11.17.0.2 sanitize_css(style)

Sanitizes a block of CSS code. Used by sanitize when it comes across a style attribute in HTML being sanitized.

11.17.0.3 strip_links(html)

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

11.17.0.4 strip_tags(html)

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!

11.18 TagHelper

This module provides helper methods for generating HTML tags programmatically.

11.18.0.1 cdata_section(content)

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 ]]>.

11.18.0.2 content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)

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>

11.18.0.3 escape_once(html)

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"

11.18.0.4 tag(name, options = nil, open = false, escape = true)

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" />

11.19 TextHelper

The methods in this module provide filtering, formatting, and string transformation capabilities.

11.19.0.1 concat(string)

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

11.19.0.2 current_cycle(name = "default")

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

11.19.0.3 cycle(first_value, *values)

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'

11.19.0.4 excerpt(text, phrase, options = {})

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"

11.19.0.5 highlight(text, phrases, options = {})

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.


11.19.0.6 pluralize(count, singular, plural = nil)

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

11.19.0.7 reset_cycle(name = "default")

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.

11.19.0.8 simple_format(text, html_options = {}, options = {})

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".

11.19.0.9 truncate(text, options = {}, &block)

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)"

11.19.0.10 word_wrap(text, options = {})

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

11.20 TranslationHelper and the I18n API

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.

11.20.1 Localized Views

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.

11.20.2 TranslationHelper Methods

The following two methods are provided for use in your views and assume that I18n support is set up in your application.

11.20.2.1 localize(*args) Aliased to l

Delegates to Active Support’s I18n#translate method with no additional functionality. Normally, you would want to use translate instead.

11.20.2.2 translate(key, options = {}) Aliased to t

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.

11.20.3 I18n Setup

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


11.20.4 Setting and Passing the Locale

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


11.20.4.1 Setting the Locale from the Domain Name

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

11.20.4.2 Setting the Locale from the Host Name

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

11.20.5 Setting Locale from Client-Supplied Information

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.

11.20.5.1 Using Accept-Language

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

11.20.5.2 Using GeoIP (or Similar) Database

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/

11.20.5.3 User Profile

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.

11.20.6 Internationalizing Your Application

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.

11.20.6.1 The Public I18n API

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

11.20.6.2 The Process

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.)

11.20.6.3 Adding Date/Time Formats

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


11.20.7 Organization of Locale Files

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}')]


11.20.8 Looking Up Translations

11.20.8.1 Basic Lookup, Scopes, and Nested Keys

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]

11.20.8.2 Default Values

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'

11.20.8.3 Bulk and Namespace Lookup

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: ... }

11.20.8.4 View Scoped Keys

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.

= t '.title'

11.20.8.5 Interpolation

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.

11.20.8.6 Pluralization

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.

11.20.9 How to Store Your Custom Translations

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.

11.20.9.1 Translations for Active Record Models

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.”

11.20.9.2 Error Message Scopes

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.

11.20.9.3 Error Message Interpolation

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}".

Image
Image

Table 11.1 Error message interpolation

11.20.10 Overview of Other Built-In Methods That Provide I18n Support

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.

11.20.10.1 Action View Helper Methods

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_words19 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_names20 for translations. datetime_select also looks up the order option from date.order21 (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_currency, 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

11.20.10.2 Active Record Methods

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}").

11.20.10.3 Active Support Methods

Array#to_sentence uses format settings as given in the support.array scope.

11.20.11 Exception Handling

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.

11.21 UrlHelper

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.”

11.21.0.1 button_to(name = nil, options = nil, html_options = nil, &block)

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>"

11.21.0.2 current_page?(options)

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

11.21.0.3 link_to(name = nil, options = nil, html_options = nil, &block)

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>

11.21.0.4 link_to_if(condition, name, options = {}, html_options = {}, &block)

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).

11.21.0.5 link_to_unless(condition, name, options = {}, html_options = {}, &block)

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).

11.21.0.6 link_to_unless_current(name, options = {}, html_options = {}, &block)

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

11.21.0.7 mail_to(email_address, name = nil, html_options = {}, &block)

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.


11.21.0.8 Redirecting Back

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()"

11.22 Writing Your Own View Helpers

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.

11.22.1 Small Optimizations: The Title Helper

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.

Listing 11.3 The page_title Helper

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   ...

11.22.2 Encapsulating View Logic: The photo_for Helper

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.

Listing 11.4 The photo_for Helper Encapsulating Common View Logic

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.


11.22.3 Smart View: The breadcrumbs Helper

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.

Listing 11.5 breadcrumbs Helper Method for a Corporate Directory Application

 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!

11.23 Wrapping and Generalizing Partials

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!)

11.23.1 A tiles Helper

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.


Listing 11.6 A Tiles Partial Prior to Wrapping and Generalization

 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

11.23.1.1 Explanation of the Tiles Partial Code

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.

11.23.1.2 Calling the Tiles Partial Code

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.


11.23.1.3 Write the Helper Method

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.

Listing 11.7 The CitiesHelper Tiled Method

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.

11.23.2 Generalizing Partials

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.

Listing 11.8 Tiles Partial Code with Revised Naming

 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.

11.23.2.1 Lambda: The Ultimate Flexibility

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!

Listing 11.9 Tiles Partial Code Refactored to Use 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.)


11.23.2.2 The New Tiled Helper Method

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.

Listing 11.10 The Tiled Collection Helper Method with Lambda Parameters

 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.

11.24 Conclusion

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/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.128.113