How to Let Google Know of Other Languages in Your Django Site

Using hreflang to link to other languages


If you have a public facing Django site in multiple languages, you probably want to let Google and other search engines know about it.

Linguistic map of the world (<a href="https://en.wikipedia.org/wiki/Linguistic_map">source</a>)
Linguistic map of the world (source)

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!

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:

  1. Use absolute URLs, including the schema.
  2. Link must be valid, and the linked page should be in the specified language.
  3. List all languages, including the current one.
  4. 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:
        to_be_reversed = "%s:%s" % (match.namespace, match.url_name) if match.namespace else 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.



Similar articles