Simple Localization in Rails 2.2

July 21st, 2008 by Trevor

I've been staying on the sidelines when it comes to localization in Rails for a while now, but I couldn't help getting excited about the upcoming native support in Rails 2.2. So, with some guidance from the Rails i18n team, I decided to give things a try.

I've been extremely pleased with the results so far, but I'm all ears if anyone would like to offer suggestions on how to better achieve basic localization for a Rails app. Here's where I'm at so far in a kind of how-to format. This is all plugin-free, using only what's available in core. I expect that plugins will be coming out to add features and functionality, but you can accomplish quite a bit without any extras.

You can try to follow along, or just get the gist be reading through the steps. As noted in the comments, this is just a proof of concept, is not secure, and shouldn't be used in production as-is.

1. Make a new Rails app and freeze edge:

 
~ $ rails i18n
~ $ cd i18n
~ $ rake rails:freeze:edge
 

2. Make a couple of translation stores (files) in lib/locale directory:

 
# lib/locale/en-US.rb
I18n.store_translations 'en-US',
:hello_world => "Hello World",
:hello_flash => "Hello Flash"
 
#lib/locale/pirate.rb
I18n.store_translations 'pirate',
:hello_world => "Ahoy World",
:hello_flash => "Ahoy Flash"
 

3. Set I18n.locale with a before_filter:

 
# app/controllers/application.rb
class ApplicationController < ActionController::Base
 
  before_filter :set_locale
 
  def set_locale
    locale = params[:locale] || 'en-US'
    I18n.locale = locale
    I18n.populate do
      require "lib/locale/#{locale}.rb"
    end
  end
 
end
 

4. Create a "t" convenience method (I thought this was going to be there automagically?):

 
# app/helpers/application_helper.rb
module ApplicationHelper
  def t(*args)
    translate(*args)
  end
end
 

7. Make a controller and route to test things out, using symbols from your translation for user messages:

 
# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.root :controller => 'home', :action => 'index'
end
 
# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    flash[:notice] = :hello_flash
  end
end
 

6. Create a view using symbols for user messages and use the "t" helper to translate:

 
# app/views/home/index.html.erb
<h1><%=t :hello_world %></h1>
 
<%=t flash[:notice] %>
 
<%= link_to 'en-US', root_path(:locale => 'en-US') %> or
<%= link_to 'pirate', root_path(:locale => 'pirate') %>
 

6. Fire up the old script/server and check it out:

 
~ $ script/server
 

I think that about covers it. Of course, this is a very simple example, but it should cover the basics well enough to get started. Please let me know if you have any ideas about how to simplify/improve this, and thanks again to the Rails i18n team for all of their work - everything looks great so far!

 

Migrate to the Rails Default Time Zones

June 3rd, 2008 by Trevor

I was recently upgrading my open-source app, El Dorado, up to Rails 2.1 and decided to switch over to the default time zones provided by the Rails. I'm not sure where I picked up the time zone definitions I was using before, but they were things like "US/Central" as opposed to "Central Time (US & Canada)".

Switching to the new time zone definitions provided by Rails means not having to rely on the TZInfo gem, because a stripped-down version is now packaged with Rails 2.1.

Here's the migration used to achieve this, which was made with some help from Geoff Buesing himself:

 
# db/migrate/20080603023415_use_rails_new_default_time_zones.rb
 
class UseRailsNewDefaultTimeZones < ActiveRecord::Migration
  def self.up
    @users = User.all
    @users.each do |user|
      user.time_zone = 'UTC' if user.time_zone.blank?
      tz = TZInfo::Timezone.get(user.time_zone) rescue TimeZone[user.time_zone] || TimeZone['UTC']
      time_zone = if tz.is_a?(TZInfo::Timezone)
        linked_timezone = tz.instance_variable_get('@linked_timezone')
        name = linked_timezone ? linked_timezone.name : tz.name
        TimeZone::MAPPING.index(name)
      else
        tz.name
      end
      user.update_attribute(:time_zone, time_zone) unless time_zone == user.time_zone
    end unless @users.empty?
  end
 
  def self.down
  end
end
 

If you haven't played with the new time zone stuff in Rails 2.1, make sure to check it out. It makes dealing with time zones so easy, it's almost unbelievable. Thanks, Geoff!

 

El Dorado 0.9.2 (Group Chat Edition)

May 5th, 2008 by Trevor

I'm pleased to announce the release of El Dorado version 0.9.2 - the "group chat" edition. You can download it right away, or check out the demo, testing, and support site here:

http://eldorado.almosteffortless.com/

Since we last spoke at the end of January, there's been a goodly amount of activity with El Dorado. The highlight, of course, is the new group chat feature:

eldo-chat-2

This is a very simple and lightweight implementation that's perfect for small groups. It works solely with the tools provided by Rails and doesn't add anything to the overall requirements for running El Dorado. I've been using it with my group of friends for a few weeks now, and it's been working great. I'm sure that I'll have some more features to announce with the next release, but my favorite little touch so far is the "who's in the chat room and when was the chat last active" indicator on the home page:

eldo-chat-1

In addition to the addition of group chat, there have been a number of improvements to other parts of the app and quite a bit of back-end optimization/refactoring. Here's a quick list of the new features:

  • Make entire site private with a simple admin setting (great for small companies)
  • View all posts across the entire forum in reverse chronological order
  • View all posts by individual user
  • Upload new files quickly and easily via URL
  • Ability to "sticky" and "lock" forum threads
  • Move threads to different forums, and forums to different categories
  • Allow users to stay logged in between different browsers/machines
  • New calendar interface for setting the date/time of an event
  • Admin interface for viewing/editing/creating user rankings
  • Admin interface to grant/revoke admin privileges
  • Various BBCode additions (Flickr video, Slideshare, FunnyOrDie, etc) and some bug fixes

Of course, you can follow along with all of the activity in El Dorado on the commit log at github. You see, being the lemming that I am, I've followed Rails to github and Lighthouse already. I haven't had time to settle into Lighthouse just yet, but I've been having a blast with git and github.

In fact, I gave a presentation at Chicago Ruby and spent a fair amount of time talking about making the move to git. Definitely flip through the PDF to learn all about the origin of El Dorado (where the name came from, etc) but take my word for it about one thing: git/github is the way to go:

eldo-chicagoruby

If you believe the PDF, then the upcoming of releases should go something like this:

  • Chat
  • Localization (see note below)
  • Blog
  • Themes
  • Photos
  • Wiki
  • Polls
  • Link Sharing
  • Mobile Messaging

Thanks to git/github, jxl has been working on a localized version of El Dorado using Gibberish, and he's already completed a Dutch translation. I'll be working with him to pull his changes into my repo for the next release. If you're interested in translating El Dorado into another language, please do get in touch!

I think that about covers it... so... Welcome to El Dorado v.0.9.2!

Learn more and/or download El Dorado here: http://almosteffortless.com/eldorado/

 

Automatically Creating, Loading, and Migrating your Database (with magic!)

April 10th, 2008 by Trevor

So, let's say you're working on this open source Rails app, and you're doing your best to keep the requirements light and the installation as easy as possible. You'd love to have that Famous 5-Minute Install that WordPress has going for it, and you start to wonder...

Why should I even have to worry about the database?

Why can't my app take care of itself? Why can't it automatically create and maintain the database for me?

Well friend, I just wrapped up a little initializer that will automatically create the db, load the schema, and/or migrate as necessary. It all seems fine so far, but I still have this funny feeling that I'm doing something wrong...

 
/config/initializers/db_create_load_or_migrate.rb
 

I dunno... Have I gone too far this time?

 
unless defined?(Rake)
  load "#{RAILS_ROOT}/Rakefile"
  begin
    current_version = ActiveRecord::Migrator.current_version
    highest_version = Dir.glob("#{RAILS_ROOT}/db/migrate/*.rb").map { |f| f.match(/(\d+)_\w*\.rb$/) ? $1.to_i : 0 }.max
    Rake::Task["db:migrate"].invoke if current_version != highest_version
  rescue
    Rake::Task["db:create"].invoke
    abort 'ERROR: Database has no schema version and is not empty' unless ActiveRecord::Base.connection.tables.blank?
    Rake::Task["db:schema:load"].invoke
    retry
  end
end
 

Addendum

Thanks to the Fail Early chapter of the Advanced Rails Recipes book for turning me onto the idea of (at least) checking for an up-to-date schema with an initializer.

Also, make sure to check out this nifty trick that lets you run Rake tasks from within a Rails app. It's pretty easy - all you need to do is load the Rails Rakefile and use .invoke:

 
load "#{RAILS_ROOT}/Rakefile"
Rake::Task["db:create"].invoke
 

P.S. I spent the last hour re-enabling comments on this site, just so somebody like you can tell me that I'm not allowed to do this and why. So, please, have at it!

Update: I updated the regex thanks to a patch that Ben sent to deal with more kinds of directory names.

Update 2: After living with this for a while, I decided to take it out of El Dorado for the time being. I still think it's an interesting idea, but it's too risky to play with production data like this, and I think it's better to be on the safe side. Plus, the Rails developers I've spoken to about it seem to think that it's introducing unexpected behavior into the app (e.g. auto-migrations are not the norm).

 

Sharing Code Between ActiveRecord Models

January 29th, 2008 by Trevor

I was frustrated with the amount of duplicated code sprinkled in my models that are using the attachment_fu plugin to deal with file uploads. It's not like I need to write a plugin or anything fancy - I just want to be a little more DRY.

So, how does one simply share code between ActiveRecord models? It's actually quite easy.

First, drop some code into a module in a .rb file in your lib directory. For example:

 
# lib/attachment_fu_extensions.rb
 
module AttachmentFuExtensions
 
  # include some standard attachment_fu stuff
  def self.included(klass)
    klass.send :validates_as_attachment
    klass.send :validates_uniqueness_of, :filename
    klass.send :validates_presence_of, :user_id
    klass.send :attr_protected, :id, :parent_id, :user_id, :created_at, :updated_at
  end
 
  # upload into a single directory (e.g. public/uploads) instead of subdirectories based on the id
  def full_filename(thumbnail = nil)
    file_system_path = (thumbnail ? thumbnail_class : self).attachment_options[:path_prefix].to_s
    File.join(RAILS_ROOT, file_system_path, thumbnail_name_for(thumbnail))
  end
 
  # prevent users from uploading index files that would be served instead of the index template/action
  def validate
    errors.add("filename", "is invalid") if filename? && %w(index.html index.htm).include?(filename.downcase)
  end
 
end
 

That code will automatically be loaded when your application starts, provided that the filename and module follow the standard Rails naming conventions.

Then, simply include the relevant module in the relevant models. For example:

 
# app/models/upload.rb
 
class Upload < ActiveRecord::Base
 
  has_attachment :storage => :file_system, :path_prefix => 'public/uploads', :max_size => 10.megabytes
  include AttachmentFuExtensions
 
  # ...
 
end
 

That's it. Extending ActiveRecord models and DRYing out your app is pretty easy, eh?

Update: Walter pointed out that I should put modules like this into lib/ instead of config/initializers/, so I've updated this post to reflect that. I'm not sure why that didn't occur to me. He also told me about the trick for sharing calls to validates_as_attachment and so forth - I didn't even know that was possible!

 

Welcome to El Dorado!

January 24th, 2008 by Trevor

eldorado

I'm pleased to announce the first public release of El Dorado: a full-stack community web application written in Ruby/Rails. This is a stable beta version of the app, which is being released in anticipation of the 1.0 release scheduled for this spring.

The app features a forum, community event calendar, shared file storage, and a randomized header image gallery. The forum is somewhat modeled on PunBB (one of the more popular PHP forums) and can import from an existing site using PunBB.

The app has been used in production since late July '07. The setup/administration process still leave something to be desired, but the end-user functionality is quite well tested.

HomeForumTopicsFilesEvents

You can get the download or more information on the El Dorado homepage, or check out the demo, testing, and support site here: eldorado.almosteffortless.com. Also, consider subscribing to the almost effortless RSS feed to stay informed about upcoming releases and the occasional Ruby/Rails-related post.

Enjoy!

 

Simplifying and sharing code with Rails conventions

January 7th, 2008 by Trevor

One of the benefits of convention over configuration in Rails is that you can make assumptions about a lot of stuff. A good example of this is the user_id column found in most Rails database tables.

If you see a user_id column in a schema, you can typically infer that it represents the unique identifier (id) of the user that created that record. This leads to all kinds of interesting opportunities for code simplification.

I'll take you through a basic example found in my open-source app, El Dorado to demonstrate. This example will show you how to share code related to authentication throughout your app. We'll start by looking at the user, topic, and post database objects defined in schema.rb:

 
create_table "users", :force => true do |t|
  t.string   "login"
  t.boolean  "admin", :default => false
  # ...
end
 
create_table "topics", :force => true do |t|
  t.integer  "user_id"
  t.string   "title"
  # ...
end
 
create_table "posts", :force => true do |t|
  t.integer  "user_id"
  t.text     "body"
  # ...
end
 

According to Rails conventions, the user_id field in the topics and posts tables should reference the id field in the users table. (The id field is assumed and not mentioned in the schema.rb file.) This convention is what allows you to do things like:

 
@user = User.find(1)
@user.topics # the topics the user has created
@user.posts # the posts the user has created
 

This also lets you share code amongst all of the controllers and views in an application. Let's use a basic concept for authentication: checking for permission to edit an item. Using some of the conventions introduced in the restful_authentication plugin, we'll be able to add some basic permission checking that will work for any model that has a user_id field, and for users that have an id field. We'll assume that a user has permission to edit an item if (a) they're an administrator, or (b) they've created the item they're trying to edit.

We'll start by adding the following to application.rb:

 
class ApplicationController < ActionController::Base
 
  helper_method :current_user, :logged_in?, :admin?, :can_edit?
 
  def current_user
    @current_user ||= ((session[:user_id] && User.find_by_id(session[:user_id])) || 0)
  end
 
  def logged_in?()
    current_user != 0
  end
 
  def admin?()
    logged_in? && (current_user.admin == true)
  end
 
  def can_edit?(current_item)
    return false unless logged_in?
    if request.path_parameters['controller'] == "users"
      return current_user.admin? || (current_user == current_item)
    else
      return current_user.admin? || (current_user.id == current_item.user_id)
    end
  end
 
  def can_edit
    redirect_to root_path and return false unless logged_in?
    klass = request.path_parameters['controller'].singularize.classify.constantize
    @item = klass.find(params[:id])
    if request.path_parameters['controller'] == "users"
      redirect_to root_path and return false unless admin? || (current_user == @item)
    else
      redirect_to root_path and return false unless admin? || (current_user == @item.user)
    end
  end
 
  # ...
 
end
 

The current_user bit assumes that you're setting the session[:user_id] value to the currently logged-in user's id. (If you're unsure how to accomplish this and/or aren't doing it already, you should give the restful_authentication plugin a gander.) The logged_in? helper will return true if the user is logged in, and the admin? helper will return true if the logged-in user has their admin attribute set to true.

The can_edit? helper and the can_edit action are where the real action is. We'll start with the can_edit action, which allows you to do the following in any controller in your application:

 
class TopicsController < ApplicationController
 
  before_filter :can_edit, :only => [:edit, :update, :destroy]
 
end
 
# ...
 
class PostsController < ApplicationController 
 
  before_filter :can_edit, :only => [:edit, :update, :destroy]
 
end
 
# ...
 
class UsersController < ApplicationController
 
  before_filter :can_edit, :only => [:edit, :update, :destroy]
 
end
 

The can_edit action is accessible to any controller, and it can be used to check the editing permissions for any item without any additional code overhead. If the user is logged in and is an administrator, they'll be allowed to proceed. Otherwise, the action will check to see if the user is trying to edit an item that they created, or their own user account. If so, they'll be allowed to proceed; if not, they'll be redirected to the root_path. This, of course, requires the defining of a root_path in your routes.rb file:

 
map.root :controller => 'home'
 

That's pretty nice, and there's also the can_edit? helper that can also be shared across all views. Here's a couple of examples. The first would be useful in, say, the user/show view. The second, perhaps in posts/show...?

 
<%= link_to 'Edit User', edit_user_path(@user) if can_edit?(@user) %>
<%= link_to 'Edit Post', edit_post_path(@post) if can_edit?(@post) %>
 

And there you have a great example of the benefits of convention in Rails app. You can do a lot of cool stuff with very little code. Plus, DRY code is always nice. Of course, you can dig further into the whole kit and kaboodle behind this by checking out the El Dorado source...

 

Configuring Cookie-Based Sessions in Rails 2.0

December 27th, 2007 by Trevor

As of Changeset 6184 and the release of Rails 2.0, the default session store for Rails apps is cookie-based.

[This] means sessions are no longer stored on the file system or in the database, but kept by the client in a hashed form that can’t be forged. This makes it not only a lot faster than traditional session stores, but also makes it zero maintenance. There’s no cron job needed to clear out the sessions and your server won’t crash because you forgot and suddenly had 500K files in tmp/session.

Configuring your application to use this speedy new session store is easy. Adding the following to your config/environment.rb file would do the trick:

config.action_controller.session = {
  :session_key => '_my_app_session',
  :secret      => 'some_really_long_and_hashed_key'
}

But...

I don't like it.

Especially when you're dealing with open-source projects, putting what amounts to installation-specific passwords here doesn't seem appropriate. In the case of my open-source project, El Dorado, I'd like to be able to make changes to environment.rb without troubling the user. Ideally, I think all passwords should be set from a single location. Luckily, it's easy to push this configuration into the already available config/database.yml.

Here's how.

Add the following to config/environment.rb:

require 'yaml'
db = YAML.load_file('config/database.yml')
config.action_controller.session = {
  :session_key => db[RAILS_ENV]['session_key'],
  :secret      => db[RAILS_ENV]['secret']
}

And then you can set everything up in one place: config/database.yml:

development:
  adapter: mysql
  database: eldorado_development
  username: root
  password:
  host: localhost
  session_key: eldorado_development
  secret: YrDOFOmYJyFg2tTZykCbZjWYQUbKBt

test:
  adapter: mysql
  database: eldorado_test
  username: root
  password:
  host: localhost
  session_key: eldorado_test
  secret: Pl8qJNFc8mo1yt1xtHOmfUGHOPEutu

production:
  adapter: mysql
  database: eldorado_production
  username: root
  password:
  host: localhost
  session_key: eldorado_production
  secret:

This seems more... natural. Don't you think?

Anyway, using YAML files for app configuration is the way of the future.

 

Random Records in Rails

December 4th, 2007 by Trevor

There are a number of different ways to retrieve random items from a database in Rails, most of which have been discussed at length on the Rails wiki. According to this article, there are 3 preferred methods:

  1. 1. Select a Record by Random Offset
  2. 2. Randomize with the Database
  3. 3. Randomize with Ruby

For an in-depth look into these options, you can peruse this discussion page, which details some of the pros and cons of different strategies for randomization.

After trying out a few of the techniques that abound in this area, I stumbled across an article from Jamis Buck, where he discusses a RESTful way to approach the creation of custom finders. Although randomization isn't the focus of the article, he does provide a bit of guidance in that regard. His strategy uses Ruby for the randomization, and employs 2 light-weight queries. The first query gathers a list of valid ids. The second simply selects a single item using that (randomized) id.

So, I did a bit of cargo-culting and repurposing to achieve an efficient, database agnostic way to retrieve random items from a database using ActiveRecord. Simply add the following to the model from which you'd like to be able to pull random records:

class Widget < ActiveRecord::Base
 
  # ...
 
  def self.random
    ids = connection.select_all("SELECT id FROM widgets")
    find(ids[rand(ids.length)]["id"].to_i) unless ids.blank?
  end
 
end

Then, you can use the following bit of code in a controller like so:

class SomeController < ApplicationController
 
  # ...
 
  def some_action
    @widget = Widget.random
  end
 
end

And you've got a small and efficient "random finder" for use throughout your app. Lovely.

 

Graceful 404s in Rails 2.0

October 8th, 2007 by Trevor

With the upcoming Rails 2.0: Preview Release starting to get some attention, I thought I'd take a moment to play with some of the new features. One of my favorite additions is the new exception handling stuff. It works just like a before_filter, so you'll pick it up straight away.

Action Pack: Exception handling:

Lots of common exceptions would do better to be rescued at a shared level rather than per action. This has always been possible by overwriting rescue_action_in_public, but then you had to roll out your own case statement and call super. Bah. So now we have a class level macro called rescue_from, which you can use to declaratively point certain exceptions to a given action.

The following is quick example you can use to catch 404 Record Not Found errors. It will catch all 404s on your site and display a nice message, instead of an ugly white page of death:

file-not-found-1.jpeg

Simply dip into the ApplicationController and add the following code:

class ApplicationController < ActionController::Base
 
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
 
  # ...
 
  def record_not_found
    flash[:notice] = "Sorry, the page you requested was not found."
    redirect_to root_path
  end
 
  # ...
 
end

...to display a nice message within your app:

file-not-found-2.jpeg

...and there you have it: Easy as Pie(tm) Record Not Found exception handling.

P.S.

This also takes advantage of my favorite tiny addition to Rails, map.root. I didn't see this mentioned in the release announcement, but it's covered in the video of the Railsconf Europe '07 video.

Instead of this:

map.home '', :controller => 'home'

...you can now do this:

map.root :controller => 'home'

It's not a big change, but it's just... nice, isn't it?

 

Looking for Work?

LifeSnapz is Hiring