If you have a public facing Django site in multiple languages, you probably want to let Google and other search engines know about it.
Multi-Language Django Site
Django has a very extensive framework to serve sites in multiple languages. The least amount of setup necessary to add additional languages to a Django site are these.
Activate the i18n framework in settings.py
:
# settings.py
USE_I18N = True
Define the supported languages:
# settings.py
from django.utils.translation import gettext_lazy as _
LANGUAGES = [
('en', _('English')),
('he', _('Hebrew')),
]
Set the default language:
# settings.py
LANGUAGE_CODE = 'en'
Add LocaleMiddleware
:
# settings.py
MIDDLEWARE = [
# ...
'django.middleware.locale.LocaleMiddleware',
# ...
]
Use gettext
to mark texts for translation:
# app/views.py
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponse
def about(request) -> HttpResponse:
return HttpResponse(_('Hello!'))
Generate the translation files:
$ python manage.py makemessages
Translate the text:
msgid "Hello!"
msgstr "שלום!"
Compile the translation files:
$ python manage.py compilemessages
Serve views in multiple languages using i18n_patterns
:
# urls.py
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls import url
from . import views
urlpatterns = i18n_patterns(
url(r'^about$', views.about, name='about'),
)
Make sure that it works:
$ curl http://localhost:8000/en/about
Hello!
$ curl http://localhost:8000/he/about
שלום!
This is it!
There are a few additional steps like adding a view to switch the language, but all in all, your multi-language Django site is ready to go!
Link to Other Languages Using hreflang
To let search engines know a page is available in a different language, you can use a special link tag:
<link rel="alternate" hreflang="en" href="https://example.com/en" />
The tag has the following attributes:
hreflang
: Language code of the linked page.href
: Link to the page in the specified language.
According to Google's guidelines, and the information in Wikipedia, these are the rules we need to follow:
- Use absolute URLs, including the schema.
- Link must be valid, and the linked page should be in the specified language.
- List all languages, including the current one.
- If language X links to language Y, language Y should link back to language X.
To implement the following in Django, start by listing the available languages in a template, and set the language code in the hreflang
attribute:
{% load i18n %}
{% get_available_languages as LANGUAGES %}
{% for language_code, language_name in LANGUAGES %}
<link
rel="alternate"
hreflang="{{ language_code }}"
href="TODO" />
{% endfor %}
The next step is to add localized links for each language. It took some digging, but it turns out Django already has a function called translate_url
we can use:
>>> from django import urls
>>> from django.utils import translation
>>> translation.activate('en')
>>> reverse('about')
'/en/about'
>>> urls.translate_url('/en/about', 'he')
'/he/about'
The function translate_url
accepts a URL and a language, and returns the URL in that language. In the example above, we activated the English language and got the a URL prefixed with /en
.
The guidelines require absolute URLs.
Let's make sure translate_url
can handle absolute URLs as well:
>>> urls.translate_url('https://example.com/en/about', 'he')
'https://example.com/he/about'
Great! translate_url
can "translate" absolute URLs.
How about URLs with query parameters or hash?
>>> urls.translate_url('https://example.com/en/about?utm_source=search#top', 'he')
'https://example.com/he/about?utm_source=search#top'
Cool, it worked too!
NOTE: It doesn't make much sense to have a page URL with query params and hashes in a place like a link tag (or canonical for that matter). The reason I mention it is because it might be useful for deep linking into other pages.
This is basically all that we need. But, translate_url
has some limitations that are worth knowing.
Translate a non-localized URL:
>>> urls.translate_url('/about', 'en')
'/about'
If you use the built-in LocaleMiddleware
and try to navigate to /about
, Django will redirect you to the page in the current language. translate_url
is unable to do the same.
good to know
translate_url
cannot "translate" a non-localized URL (even though it might exist).
What about translating a URL already in a language which is not the current language?
>>> translation.activate('en')
>>> urls.translate_url('/he/about', 'en')
'/he/about'
Nope, can't do that either.
good to know
translate_url
can only translate localized urls in the current language.
If you look at the implementation of translate_url
this restriction becomes clear:
# django/urls/base.py
def translate_url(url, lang_code):
"""
Given a URL (absolute or relative), try to get its translated version in
the `lang_code` language (either by i18n_patterns or by translated regex).
Return the original URL if no translated version is found.
"""
parsed = urlsplit(url)
try:
match = resolve(parsed.path)
except Resolver404:
pass
else:
if match.namespace
to_be_reversed = "%s:%s" % (match.namespace, match.url_name)
else
to_be_reversed = match.url_name
with override(lang_code):
try:
url = reverse(to_be_reversed, args=match.args, kwargs=match.kwargs)
except NoReverseMatch:
pass
else:
url = urlunsplit((parsed.scheme, parsed.netloc, url, parsed.query, parsed.fragment))
return url
Django first tries to resolve
the URL path. This is Django's way of checking if the URL is valid. Only if the URL is valid, it is split into parts, and reversed in the desired language.
translate_url
template tag
Now that we know how to "translate" URLs to different languages, we need to be able to use it in a template. Django provides us with a way to define custom template tags and filters.
Let's add a custom template tag for translate_url
:
# app/templatetags/urls.py
from typing import Optional, Any
from django import urls
register = template.Library()
@register.simple_tag(takes_context=True)
def translate_url(context: Dict[str, Any], language: Optional[str]) -> str:
"""Get the absolute URL of the current page for the specified language.
Usage:
{% translate_url 'en' %}
"""
url = context['request'].build_absolute_uri()
return urls.translate_url(url, language)
Our translate_url
template tag takes context. This is necessary if we want to provide an absolute URL. We use build_absolute_uri
to grab the absolute URL from the request.
The tag also accepts the target language code to translate the URL to, and uses translate_url
to generate the translated URL.
With our new template tag, we can fill in the blanks in the previous implementation:
{% load i18n urls %}
{% get_available_languages as LANGUAGES %}
{% for language_code, language_name in LANGUAGES %}
<link
rel="alternate"
hreflang="{{ language_code }}"
href="{% translate_url language_code %}" />
{% endfor %}
Using x-default
for the default language
The guidelines include another recommendation:
The reserved value hreflang="x-default" is used when no other language/region matches the user's browser setting. This value is optional, but recommended, as a way for you to control the page when no languages match. A good use is to target your site's homepage where there is a clickable map that enables the user to select their country.
So it's also a good idea to add a link to some default language. If, for example, we want to make our default language English, we can add the following to the snippet above:
{% load i18n urls %}
{% get_available_languages as LANGUAGES %}
{% for language_code, language_name in LANGUAGES %}
<link
rel="alternate"
hreflang="{{ language_code }}"
href="{% translate_url language_code %}" />
{% endfor %}
<link
rel="alternate"
hreflang="x-default"
href="{% translate_url en %}" />
When we setup our Django project, we already defined a default language. Instead of hard-coding English (or any other language for that matter), we want to use the LANGUAGE_CODE
defined in settings.py
.
To use values from settings.py
in templates, we can use an old trick we used in the past to visually distinguish between environments in Django admin. It's a simple context processor that exposes specific values from settings.py
to templates through the request context:
# app/context_processor.py
from typing import Dict, Any
from django.conf import settings
def from_settings(request) -> Dict[str, Any]:
return {
attr: getattr(settings, attr, None)
for attr in (
'LANGUAGE_CODE',
)
}
To register the context processor, add the following in settings.py
:
# settings.py
TEMPLATES = [{
# ...
'OPTIONS': {
'context_processors': [
#...
'app.context_processors.from_settings',
],
#...
}
]}
Now that we have access to LANGUAGE_CODE
in the template, we can really complete our snippet:
{% load i18n urls %}
{% get_available_languages as LANGUAGES %}
{% for language_code, language_name in LANGUAGES %}
<link
rel="alternate"
hreflang="{{ language_code }}"
href="{% translate_url language_code %}" />
{% endfor %}
<link
rel="alternate"
hreflang="x-default"
href="{% translate_url LANGUAGE_CODE %}" />
The rendered markup for the about page looks like this:
<link
rel="alternate"
hreflang="en"
href="https://example.com/en/about" />
<link
rel="alternate"
hreflang="he"
href="https://example.com/he/about" />
<link
rel="alternate"
hreflang="x-default"
href="https://example.com/en/about" />
Final Words
Hopefully this short article helped you gain a better understanding of how search engines can identify different languages in your Django site. In the process, you might have also picked up on some little tricks to manipulate localized URLs in Django.