Nov

30

URLs on multi-lingual sites: A solution

Let me tell you about two problems:

  1. Your site is localized into multiple languages, but the words in its URLs can only be in one language. For example, the URL for the "About Us" page will always be "/about-us/" no matter what language you're viewing the site in.

  2. Django's language switching is kind of crappy. You have to post to a view to change the language, and it's hard to expose all the site's translations to search engines for indexing.

Ladies and gentlemen, the solution is at hand: Transurlvania!

Transurlvania is a small suite of URL-related tools for multi-lingual sites with an incredibly hilarious name.

It's up on github here: http://github.com/trapeze/transurlvania

Transurlvania has two major components: url translation, and language-in-URL

URL Translation

URL Translation lets you localize your URLs without a lot of headache. It means the natural language words in your URLs can always be legible to your visitors. How easy is it? Well, here's a regular urlconf module:

from django.conf.urls.defaults import *
 
urlpatterns = patterns('',
url(r'^about-us/$', 'about_us', name='about_us'),
)

To make that URL translatable, you would transform the module like so:

from django.utils.translation import ugettext_noop as 
 
from transurlvania.defaults import *
 
urlpatterns = patterns('',
url(
(r'^about-us/$'), 'about_us', name='about_us'),
)

Now run make messages and open up your po file. Find the URL and provide a translation the same way you would for any other translatable string. Here's the French po file, for example:

msgid "^about-us/$"
msgstr "^a-propos-de-nous/$"

Now, when the French language is active, the URL for the About Us page will be "/a-propos-de-nous/". It's what will resolve and it's also what will be returned in a reverse look-up. Not too shabby.

Language-in-URL

The other half of transurlvania is easy to explain and hard to implement. It's localeurl (but nicer?). Instead of setting the active language via a cookie, transurlvania and localeurl set it via a language code at the beginning of the URL path (ex. "/en/about-us/", "/fr/a-propos-de-nous/"). Transurlvania also lets you map languages to domains (ex. "englishdomain.com/about-us/", "domainfrancais.com/a-propos-de-nous/").

For some people, this will be the big news: unlike localeurl, transurlvania doesn't do monkey patching. It also works better with regular reverse-lookups and provides for a nicer way to add "language agnostic" URLs. Here's an example root URL conf:

from django.contrib import admin
from django.utils.translation import ugettext_noop as
 
from transurlvania.defaults import *
 
admin.autodiscover()
 
urlpatterns = lang_prefixed_patterns('myapp.views',
url(r'^$', 'home'),
url(r'^admin/', include(admin.site.urls)),
url(
(r'^about-us/$'), 'about_us', name='about_us'),
)
 

urlpatterns += patterns('',
url(r'^$', 'transurlvania.views.detect_language_and_redirect'),
url(r'^xml/$', 'myapp.views.xml', name='xml_interface'),
)

You can see a nice bonus feature of transurlvania in there: detect_language_and_redirect. This view calls Django's language detection code and redirects the user to the home page URL for that language.

The language-in-URL functionality is implemented in a collection of middleware, a context processor, some decorators and even some template tags. It's not so much compicated as it is spread out. The code needs to live in several different places because transurlvania doesn't just let you set active language using the URL, it also provides for something new:

Contextual Language Switching

If you're using translatable URLs or language-in-URL, the URL that works in one language probably won't work in another. To resolve this, transurlvania provides a tool for determining what the equivalent URL for the current page will be in another language.

The end result is this: If you're on the English About Us page ("/en/about-us/"), the "Fran├žais" link in the footer will point to "/fr/a-propos-de-nous/".

The language switching link should work correctly for almost all static URLs. For URLs that don't work out of the box, such as URLs with slugs or ids in them, transurlvania provides a set of simple decorators. For example, you could use a decorator to support a detail view for a translatable object from django-multilingual. About the only place where a contextual language switching link can't be generated is in generic views.

If you want to see transurlvania in action, you can check it out in several websites that Trapeze has built, including http://thinsations.ca, http://olympic.ca, and http://kraftpeanutbutter.ca.

Once again, the github link: http://github.com/trapeze/transurlvania

Now Comes The Caveats

It probably doesn't work with URL namespaces. That's because I don't use them and I don't know how they work. If you use the app_name/namespace thing, I would love to hear from you on whether or not transurlvania works with them and if not, how I can fix it.