Blog Posts tagged with: i18n

Sep 09 2009

Comments

Keep Models out of Your Views' Business! (Part 2)

Last week, I wrote about using Rails’ I18N facilities to break dependencies between your models and views, by changing how model and attribute names are displayed. But there’s another place where the implementation of models sometimes peeks through into views: validation error messages.

Depending on how you use them, Rails’ error message helpers may insert attribute names into messages, and the changes from last week will take care of that automatically. But it goes farther than that. Often, validation error messages express things in a way that is not appropriate for end users. The usual solution is to override the default messages with `:message => '...'` options on the validation declarations. But again, that means the model is actively involved in presentation issues, and we’d like to avoid that if possible.

Whole plugins have been written to try to solve this problem, but Rails internationalization support has made it much easier. When validation error messages have to refer to the name of a model class or attribute, they will use the locale definitions we’ve already supplied. And it turns out that they look in the locale for error message text, too.

Overriding Validation Error Messages

Last week, we used a typical Rails example: a blogging app with a model called `Post` with attributes `title` and `body`. (And we used I18N to relabel `Post` as `Article`, and `title` as `headline`.) Let’s continue with that example, assuming this declaration in `Post`:

  validates_presence_of :title

The default error message (with the re-labeling we’ve already done) is “Headline can’t be blank.” How would we change that to something friendlier, like “Headline should not be empty; please supply one”?

Let’s add a little more to `en.yml`—an “errors” section that includes customized validation error messages:

  en:
    activerecord:
      # models and attributes sections carried over from
      # previous article
      models:
        post: "Article"
      attributes:
        post:
          title: "Headline"
      # errors section is new
      errors:
        models:
          post:
            attributes:
              title:
                blank: "should not be empty; please supply one"

We’re now several levels deep in this YAML structure, but it should be fairly easy to follow. Under `activerecord.errors` there’s a section for `models`, within which we find our model (`post`). Of its `attributes` we’ve supplied a custom error message for the `title` attribute; in particular, we’ve customized the `blank` message.

With that change in place, the error messages will read as we’d like them to, with no change to the model or the views.

Note that we restricted this change to just the `title` attribute of `Post`. If we wanted to change all “can’t be blank” messages for all attributes of `Post`, we could do that as well:

  en:
  activerecord:
    errors:
      models:
        post:
          blank: "should not be empty; please supply one"

Finally, if we wanted to make the change across all the models, here’s how:

en:
  activerecord:
    messages:
      blank: "should not be empty; please supply one"

Which Messages Can I Override?

You can see the current default messages in the source code for the activerecord gem; just consult the file `lib/active_record/locale/en.yml`. But here’s the entire list, mapped to the validation methods that use them. (Some validations use different messages depending on the circumstances.)

validates_acceptance_of
`:accepted` (“must be accepted”)
validates_associated
`:invalid` (“is invalid”)
validates_confirmation_of
`:confirmation` (“doesn’t match confirmation”)
validates_exclusion_of
`:exclusion` (“is reserved”)
validates_format_of
`:invalid` (“is invalid”)
validates_inclusion_of
`:inclusion`(“is not included in the list”)
validates_length_of
`:too_short` (“is too short (minimum is {{count}} characters)”)
`:too_long` (“is too long (maximum is {{count}} characters)”)
validates_length_of (with :is option)
`:wrong_length` (“is the wrong length (should be {{count}} characters)”)
validates_numericality_of
`:not_a_number` (“is not a number”)
validates_numericality_of (with :odd option)
`:odd` (“must be odd”)
validates_numericality_of (with :even option)
`:even` (“must be even”)
validates_numericality_of (with :greater_than option)
`:greater_than` (“must be greater than {{count}}”)
validates_numericality_of (with :greater_than_or_equal_to option)
`:greater_than_or_equal_to` (“must be greater than or equal to {{count}}”)
validates_numericality_of (with :equal_to option)
`:equal_to` (“must be equal to {{count}}”)
validates_numericality_of (with :less_than option)
`:less_than` (“must be less than {{count}}”)
validates_numericality_of (with :less_than_or_equal_to option)
`:less_than_or_equal_to` (“must be less than or equal to {{count}}”)
validates_presence_of
`:blank` (“can’t be blank”)
validates_uniqueness_of
`:taken` (“has already been taken”)

Interpolating values

You probably noticed that some of the default messages contain the string `{{count}}`. Validations that use some threshold value pass that value into the I18n library as the option `count`, and I18n interpolates the count value into the message string.

In addition to `count`, all of the validations supply three other values that can be interpolated: `model` and `attribute` (the model and attribute names, already humanized as discussed in part 1) and `value` (the erronenous value of the attribute). You can write your error messages to make use of any of those values, using the same double-curly-brace syntax.

Writing custom validations

If you write your own validation methods, whether just for your project or in a plugin, you should make use of the same mechanisms.

When a validation detects an error, it reports that error by calling the `#add` method on the `errors` object. Here’s an example (taken from `validates_inclusion_of`, and slightly modified to make more sense out of context):

  record.errors.add(attr_name, :inclusion, 
                               :value => value, 
                               :default => options[:message]) 

The first parameter is the name of the attribute being validated. Second is the symbol that’s used to look up the default message text from the I18n message repositories. Finally there’s an options hash, which includes the current attribute value (for possible interpolation into messages) and any new message text that may have been supplied directly on the call to `validates_inclusion_of`.

(That last bit is somewhat confusing; the option to `add` is called `default`, but it’s actually a specific override. That bit of weirdness is there to maintain some internal compatibility for the benefit of plugins that were written for older versions of Rails. I wouldn’t be surprised to see that cleaned up in Rails 3, although for now it still works the same way.)

All you need to do in your custom validation is to use the `#add` method in the same way, and supply default error message text in a message repository, under the `en.activerecord.messages` key. You can do it directly in `config/locales/en.yml`, like this:

  en:
    activerecord:
      messages:
        bozo: "can't be ridiculous"

Plugins and gems can supply their own YAML file. They just need to tell Rails about it by pushing the file path onto `I18n.load_path`.

Conclusion

As I said last week, the fact that Rails uses model and attribute names in views isn’t a bad thing; it’s a useful convention that helps developers move quickly. The test of a framework like Rails is how easy it is to break those links when you need to. Rails’ internationalization support makes it easy to solve this particular problem, with the added benefit that it makes your application easier to localize, should you need to go down that path.

I’ve completely stopped overriding validation error messages directly in the model code. You should, too!

Sep 02 2009

Comments

Keep Models out of Your Views' Business!

One of the core goals of the MVC architecture is to separate the presentation of a model from its implementation and semantics. And yet Rails, out of the box, takes the representation of models---even the names of tables and columns in the database---right through to the user. The default behavior, for form labels and other places where models and attributes are referred to, is simply to use the name of the model or attribute.

Does this mean Rails' implementation of MVC is broken? Not at all! My favorite definition of "architecture" in software is "the set of decisions that will be hard to change later." And the best architectures are the ones that keep that set small, allowing developers to move quickly without fear of being boxed-in later in the project. Rails uses its default behavior to let you move quickly, but leaves your options open for changing that behavior later.

I think Rails' defaults work really well in most cases. After all, we should be working with a domain language that we share with customers and domain experts, so it's likely that our schema and models will use names that are appropriate for presentation. Nevertheless, it's common to reach a point where the user interface needs to use different terms for some of the domain concepts.

So let's see how Rails can help. Assume we've developed the "canonical" Rails application, a blogging engine. The primary model is called Post, and each post has a title and body. And then, the customer decides that the users will prefer writing "articles" rather than "posts", and "headlines" instead of "titles".

What can we do?

Breaking the Link: The Obvious Ways

The most obvious, brute-force solution is to avoid the defaults, instead supplying literal names in all of our views. While there are places where it will be appropriate to supply literal labels, this option should be reserved for specific places where the context requires a different label. Using this approach to change labels throughout the application would be way too much work, and create a maintenance nightmare.

You could actually change your model---rename the table, or column, along with every place that name appears in your Ruby code. But that approach would be tedious and error-prone if the name were central to your model. More importantly, it would be a mistake to change the domain model understood by those involved in the project just to satisfy presentation constraints. It's at this point that the MVC architecture needs to pull its weight by helping you deal with this problem.

Digging a little deeper, you realize that the places in ActionView that use model and attribute names (e.g., the label helper) transform them to "human" form first, removing underscores, adding spaces, and capitalizing appropriately. They do that by calling the human_name method (for model names) and human_attribute_name (for attributes). Every ActiveRecord model inherits those methods from ActiveRecord::Base. So one answer is to override those methods. That's much better than either of the other options, but it's still not very good. If you solve your problem this way, the model will be actively involved in presentation issues. We want to work with the MVC architecture, not against it.

Breaking the Link: The Right Way

The answer to our problem can be found in the documentation for ActiveRecord::Base#human_attribute_name:

This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n module now.

We can use Rails' internationalization support to supply the new names we want to present to users. Think about your current problem as specifying the English localization for your application.

If it's not already there, create the file config/locales/en.yml, with the following contents:

  en:
    activerecord:
      models:
        post: "Article"
      attributes:
        post:
          title: "Headline"

Under the en.activerecord key there are two sections that define presentation names for the locale: models contains names for models, and attributes contains names for ... well, I think you get the idea. So in our example, the new name for a post is "Article", and the new name for a post's title is "Headline". Note that you specify attribute names in the context of a model class, so if there were another title attribute in your system on a different model class, it would not be affected.

Suddenly, throughout your application, those changes will take effect. You can still supply different labels in particular contexts as needed, but the default is to use the names in the locale file.

Update: As commenter Tim Watson pointed out, it's not currently automatic everywhere. It does automatically happen in error messages, but not in labels (making labels handle this automatically is planned for an upcoming Rails point release). You can supply the proper label text on your own (using the human_attribute_name method). If you're using custom FormBuilders, you can easily encapsulate this change; otherwise, see commenter Priit Tamboom's solution for doing a quick monkeypatch of the label helper until Rails handles it properly.

As you may have gathered from the documentation excerpt above, you should use Post.human_attribute_name('title') instead of calling humanize on an attribute name when writing views. Likewise, you should use post.class.human_name rather than post.class.name.humanize or the literal 'Post' for model class names.

Also, associations are treated as attributes, rather than inheriting the new name of their target model. So if some model had an association to multiple posts, you would need to define an attribute renaming in the appropriate model; it would not automatically be called "Articles".

Finally, this approach doesn't deal with controller names in URLs; for that, you should modify the routes in config/routes.rb.

More To Come

There's more to this story, because there's another place where your models peek through into the user interface in undesirable ways. I'll address that in a follow-up post.

Popular Tags