Wednesday, 10 February 2016

Internationalization and Locale-Based URLs with Ruby on Rails 4.x

In this post we will learn how to produce the following locale-based URLs:

Deutsch: http://example.com/de/books
English: http://example.com/en/books

Introduction

No matter how small your application is, it is never too early to think of internationalization, even if you have no immediate plans to broadcast your website in multiple languages. Planning for internationalization early on will save countless hours and headache in the future. Even if your website is only using one language, the process of internationalization will force you to refactor all of your user-specific messages into one place so that they are easy to find and maintain.
This post, however, is not about the benefits of internationalization (you can find many yourself with a quick Google search) or how to display your website in multiple languages. I assume that you already know how to do that and will instead delve deeper into how to serve language-based content to your users with intuitive and user-friendly URLs.


Different locale-based URL options

Although I am only discussing one specific method of producing locale-based URLs, it is good to know other alternatives.
Locale as a URL variable: http://example.com/books?locale=en
With this method every URL with have the locale=en segment to indicate the language. While, it is clear what language is being used, having the same parameter in every URL is a little verbose, not to mention aesthetically unappealing (if you happen to be a pedant). Another disadvantage of this method is that search engines may not realize that a page uses a different language and, as a result, might not index it correctly.
Locale as a subdomain: http://en.example.com/books
This URL certainly looks much more pleasing to the eye and addresses the search engine shortcoming addressed above; however, this method requires extra configuration for every different language. That is, you will have manually add every subdomain to your website and point it correctly to your website, and, depending on how you configure these subdomains, you may have to repoint them all if the IP address of your website happens to change (which, of course, can be addresses by using canonical names). Another possible problem arises from using third-party hosting services, which may not allow you to add subdomains for free.
Locale as a host suffix: http://example.com/en/books
This, in my opinion, is the absolute winner, as it is trivial to configure, does not require any extra configuration if another language needs to be added in the future. Search engines are well aware of this technique, and your URLs stay short and easy to understand.
Session-based locale: http://example.com/books
In this case, there is no way to tell from the URL what language is being used. This method is typically reserved for websites where users have to log in and can then change their language of preference in website settings. Since a user specifically opts for a particular language, there is no reason to make URLs locale-based, and, therefore, this method certainly has its place and should not be discounted.


Creating a small application and setting up some translations

To learn to how to create pretty locale-based URLs, we will build together a tiny book inventory application. Follow my lead.

Let's create our application and a books resource with a title, one author and a price.

rails new bookapp && cd bookapp
rails generate scaffold book title author price:decimal
rake db:migrate

If we start our application with rails server and navigate to http://localhost:3000 we should be able to see an empty list of books. Before we start customizing URLs, let's add some translations to the application so that when the URLs are ready, we will be able to tell the difference.
Create the following internationalization files in /config/locales:
de.yml

de:
  button:
    back: 'Zurück'
    destroy: 'Zerstören'
    edit: 'Bearbeiten'
    show: 'Zeigen'

  listing: "%{model}liste"
  editing: "%{model} bearbeiten"

  helpers:
    submit:
      create: "%{model} erstellen"
      submit: "%{model} speichern"
      update: "%{model} aktualisieren"

  activerecord:
    models:
      book:
        one: 'Buch'
        other: 'Bücher'
        new: "Neues Buch"
    attributes:
      book:
        title: 'Titel'
        author: 'Autor'
        price: 'Preis'

en.yml

en:
  button:
    back: 'Back'
    destroy: 'Destroy'
    edit: 'Edit'
    show: 'Show'

  listing: "Listing %{model}"
  editing: "Editing %{model}"

  helpers:
    submit:
      create: "Create %{model}"
      submit: "Save %{model}"
      update: "Update %{model}"

  activerecord:
    models:
      book:
        one: 'Book'
        other: 'Books'
        new: "New Book"
    attributes:
      book:
        title: 'Title'
        author: 'Author'
        price: 'Price'
Modify the following Book views as follows:
_form.html.erb

<%= form_for(@book) do |f| %>
  <div class="field">
    <%= f.label :title, Book.human_attribute_name(:title) %><br>
    <%= f.text_field :title %>
  </div>
  <div class="field">
    <%= f.label :author, Book.human_attribute_name(:author) %><br>
    <%= f.text_field :author %>
  </div>
  <div class="field">
    <%= f.label :price, Book.human_attribute_name(:price) %><br>
    <%= f.text_field :price %>
  </div>
  <div class="actions">
    <%= f.submit class: 'button' %>
  </div>
<% end %>

edit.html.erb

<h1><%= t('editing', model: Book.model_name.human) %></h1>

<%= render 'form' %>

<%= link_to t('button.back'), books_path, class: 'hollow secondary button' %>
<%= link_to t('button.show'), @book, class: 'button' %>

index.html.erb

<h1><%= t('listing', model: Book.model_name.human(:count => 2)) %></h1>

<table>
  <thead>
    <tr>
      <th><%= Book.human_attribute_name(:title) %></th>
      <th><%= Book.human_attribute_name(:author) %></th>
      <th><%= Book.human_attribute_name(:price) %></th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <% @books.each do |book| %>
      <tr>
        <td><%= book.title %></td>
        <td><%= book.author %></td>
        <td><%= book.price %></td>
        <td><%= link_to t('button.show'), book %></td>
        <td><%= link_to t('button.edit'), edit_book_path(book) %></td>
        <td><%= link_to t('button.destroy'), book, method: :delete %></td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= link_to t('activerecord.models.book.new'), new_book_path, class: 'button' %>

new.html.erb

<h1><%= t('activerecord.models.book.new') %></h1>

<%= render 'form' %>

<%= link_to t('button.back'), books_path, class: 'hollow secondary button' %>

show.html.erb

<p>
  <strong><%= Book.human_attribute_name(:title) %>:</strong>
  <%= @book.title %>
</p>

<p>
  <strong><%= Book.human_attribute_name(:author) %>:</strong>
  <%= @book.author %>
</p>

<p>
  <strong><%= Book.human_attribute_name(:price) %>:</strong>
  <%= @book.price %>
</p>

<%= link_to t('button.back'), books_path, class: 'hollow secondary button' %>
<%= link_to t('button.edit'), edit_book_path(@book), class: 'button' %>

Now that we've created some translations, we can change the default locale to German in /config/application.rb by uncommenting the line:

config.i18n.default_locale = :de

If we navigate to http://localhost:3000/books, we will see the list of books in German (remember to restart the application to see the changes).


Now we're finally ready to add locales to our URLs.


Setting up locale-based URLs

The first we need to do is enclose all routes that should have the locale prefix inside scope inside routes.rb.

Rails.application.routes.draw do
  scope '(:locale)', locale: Rails.configuration.x.locale do
    resources :books
    # other locale-based routes defined here
  end
  # non-locale-based routes defined here
end

The variable Rails.configuration.x.locale is currently undefined, but we will use it to keep a list of all supported locales. The best place to define it is in application.rb:

class Application < Rails::Application
  ...
  config.x.locale = /de|en/
  ...
end

Now we need to create a before_action in application_controller.rb. We also need to override the default default_url_options method so that all URLs always contain the locale (note that this will not affect any non-locale-based routes in case you defined some).

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :set_locale

  private

  def default_url_options
    {locale: params[:locale]}
  end

  def set_locale
    I18n.locale = params[:locale] || I18n.default_locale
  end
end

Now we can navigate to http://localhost:3000/de/books and http://localhost:3000/en/books and see the list of books in German and English, respectively.


Detecting default browser language

An even better idea is to serve website content in a user's preferred language by default instead of making users change the language to the one they like best. To achieve this goal, we need only change the default application locale to the user's default browser locale. The resulting application_controller.rb is given below:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :set_locale

  private

  def default_url_options
    {locale: params[:locale]}
  end

  def preferred_locale
    request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first if request.env['HTTP_ACCEPT_LANGUAGE'].present?
  end

  def set_locale
    new_locale = params[:locale] || preferred_locale
    I18n.locale = if I18n.locale_available? new_locale
                    new_locale
                  else
                    I18n.default_locale
                  end
  end
end

To test whether this worked, change your default browser language from English to German or vice versa, restart the browser and navigate to http://localhost:3000/books. You should now see content in two different languages. Note that we've also added a check that verifies that the preferred user language is indeed among the languages available in the application. If that is not the case, we fall back to the default locale.


Setting page language in HTML

Search engines and web crawlers need to know which language is used by a particular page of your website. To do that simply make sure that the lang attribute on the <html> tag is set to change dynamically:

<html lang="<%= I18n.locale %>">


Creating buttons that change the locale

The only thing left to do is create links that change the language of a website. Fortunately, that is the easiest part.

<%= link_to 'Deutsch', params.permit(:locale).merge(locale: :de) %>
<%= link_to 'English', params.permit(:locale).merge(locale: :en) %>

These links can be placed on any page of the website (and should probably be present on all pages). They will redirect users to the same page that they are currently on, but the language will be changed.


Conclusion

You are now well on your way to creating user-friendly locale-based URLs that are generic and easily maintainable. The only thing to remember is that when adding new languages it is necessary to add the new locale in the application.rb and restart the application. Everything else should work as is.

Happy internationalizing!

Friday, 5 September 2014

HTML5 Pattern Regex Date Validator for 1900-2099 using YYYY-MM-DD

HTML5 pattern is a wonderful tool for validating user input, and date validation is a common problem if one doesn't want to use a date plugin.

This date validator has the following features:
  • It validates dates between 1999 and 2099 in the format YYYY-MM-DD
  • It takes leap years into account (i.e. Feb 29, 2014 is not a valid date)
  • It knows which months have 30 days and which have 31
  • It allows the following separators: period(.), comma(,), forward slash(/), hyphen(-), space( )

Use the following code to implement your date validator:

<input type="text" pattern="^(?:((?:19|20)[0-9]{2})[\/\-. ]?(?:(0[1-9]|1[0-2])[\/\-. ]?([0-2][1-8]|[12]0|09|19)|(0[13-9]|1[0-2])[\/\-. ]?(29|30)|(0[13578]|1[02])[\/\-. ]?(31))|(19(?:[0][48]|[2468][048]|[13579][26])|20(?:[02468][048]|[13579][26]))[\/\-. ]?(02)[\/\-. ]?(29))$">

If you would like to restrict your dates to particular separators, simply remove the ones you do not want from the [\/\-. ] portion of the regular expression.

If you would to like to allow partial dates, such as YYYY-MM-00, use the following regular expression instead (note that the only change was to add a 0 within [12]):

<input type="text" pattern="^(?:((?:19|20)[0-9]{2})[\/\-. ]?(?:(0[1-9]|1[0-2])[\/\-. ]?([0-2][1-8]|[012]0|09|19)|(0[13-9]|1[0-2])[\/\-. ]?(29|30)|(0[13578]|1[02])[\/\-. ]?(31))|(19(?:[0][48]|[2468][048]|[13579][26])|20(?:[02468][048]|[13579][26]))[\/\-. ]?(02)[\/\-. ]?(29))$">

Warning: Remember that your browser may not support the pattern attribute. Always have another option for validating user input, such as javascript that reads the pattern attribute and validates it using RegExp or other alternatives.

Attribution: This validator was originally found here.

Thursday, 4 September 2014

Expand and Collapse Textarea Automatically When Typing

I was searching one day for ways to expand the textarea automatically, but couldn't find any solutions that were quite what I wanted. Some required hacks to CSS, others to HTML. As a result, I decided to write my own JQuery based script that would do the job.

Here are some of its features:
  • Textarea expands and collapses automatically when typing
  • Textarea expands automatically when pasting text into it
  • If updated dynamically, it will not expand on change but it will expand the moment it gets focus
  • Works with textareas that are added using AJAX
  • Very lightweight, as it does NOT use JQuery
  • Fully cross-browser compatible

To install the code, simply include it anywhere on your page (header or body).

(function() {
    function resizeTextArea(event) {
        var e = event || window.event;
        var o = e.target || e.srcElement;
        if (!(o instanceof HTMLTextAreaElement)) return;
        if (o.getAttribute('data-rows') === null)
            o.setAttribute('data-rows', o.rows);
        addEventListener(o, 'blur', restoreTextArea);
        while (o.rows > 1 && o.rows >= o.getAttribute('data-rows') && o.scrollHeight < o.offsetHeight)
            o.rows--;
        for (h = 0; o.scrollHeight > o.offsetHeight && h !== o.offsetHeight; o.rows++)
            h = o.offsetHeight;

    }
    function restoreTextArea(event) {
        var e = event || window.event;
        var o = e.target || e.srcElement;
        if (!(o instanceof HTMLTextAreaElement)) return;
        o.rows = o.getAttribute('data-rows');
    }
    function addEventListener(o, e, f) {
        if (o.addEventListener)
            o.addEventListener(e, f, false);
        else if (o.attachEvent)
            o.attachEvent('on' + e, f);
    }
    addEventListener(document, 'click', resizeTextArea);
    addEventListener(document, 'keydown', resizeTextArea);
    addEventListener(document, 'keyup', resizeTextArea);
}());

Wednesday, 12 February 2014

Chrome Secure Shell App Remove All SSH Hosts

Secure Shell uses strict checking, and if the signature of your host has changed, you will not be able to connect  while receiving this error message:

Welcome to Secure Shell version 0.8.25.
Answers to Frequently Asked Questions: http://goo.gl/TK7876
Verbindung mit vkononov@206.45.90.140, Port ?? wird hergestellt...
Loading NaCl plugin... done.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
76:d5:55:de:65:0d:75:29:2a:7b:0d:fe:46:51:4d:4d.
Please contact your system administrator.
Add correct host key in /.ssh/known_hosts to get rid of this message.
Offending RSA key in /.ssh/known_hosts:1
RSA host key for 206.45.90.140 has changed and you have requested strict checking.
Host key verification failed.
NaCl plugin exited with status code 255.
(R)econnect, (C)hoose another connection, or E(x)it?

The easiest thing to do in this situation is to delete all hosts for Secure Shell.
  1. Open up Secure Shell
  2. Start the Javascript console
  3. Enter this command in the console:
term_.command.removeDirectory('/.ssh/')

You should receive a confirmation "Removed: /.ssh/:"

WARNING: If working on Chrome OS, removing hosts for the OS terminal will have no effect on Secure Shell, as they both use their own respective hosts file.

Sunday, 9 February 2014

Align Text Both Left and Right on the Same Line in Text Editors

Sometimes we would like to align some text on the line to the left and some on the right. Occasionally we even want three types of alignments on one line: left, centre and right. All of this is easy to accomplish in most text editors by utilizing the TAB functionality.

Here's what you need to do to achieve the desired effect:
  1. Place the cursor at the position on the line where you would like to begin aligning the following text differently.
  2. Press the TAB key.
  3. Now place the cursor just before the tab (you can move the cursor with your mouse or press the LEFT key on your keyboard).
  4. If you would like to centre the text, create a TAB that is centre-aligned. If you would like to align the text on the right, create a TAB that is right justified. Then drag the tab in the ruler all the way to the right until it coincides with the right margin.
That's it! Your text should now have multiple alignments. For instructions on how to create a TAB in your particular text editor you may have to do an Internet search.

Sunday, 19 January 2014

How to Completely Log Off Users by Destroying Sessions in PHP

In order to delete a session, two things need to be done:
  • The session variables need to be destroyed: session_unset(). This ensures that all the data associated with the current session is deleted. 
  • The session id needs to be regenerated: session_regenerate_id(). This is necessary so that another session with the same id cannot be created.

It goes without saying that these methods can only be called if the session has already been started. Otherwise, you need to run session_start() first.