31
votes

I'm using i18n_patterns to create language prefixes in a Django app.

My URLs look like this:

/de/contact/
/fr/contact/
/it/contact/

In my base template, I'm looping over all available languages to show the language switch links.

{% get_available_languages as languages %}
<nav id="language_chooser">
    <ul>
        {% for lang_code, lang_name in languages %}
            {% language lang_code %}
            <li><a href="{% url 'home' %}" alt="{{ lang_name }}" title="{{ lang_name }}">{{ lang_code }}</a></li
            {% endlanguage %}
        {% endfor %}
    </ul>
</nav>

In this case, I'm reversing the "home" URL. Is there a way to get a translated URL of the current page instead?

If I'm on the German version of my "contact" page, I want the "fr" link to point to the French version of the "contact" page, not to the "home" page.

13

13 Answers

17
votes

I'm not using language prefixes, but translated urls instead. However, this template tag should also help you:

# This Python file uses the following encoding: utf-8

from django import template
from django.core.urlresolvers import reverse # from django.urls for Django >= 2.0
from django.core.urlresolvers import resolve # from django.urls for Django >= 2.0
from django.utils import translation

register = template.Library()

class TranslatedURL(template.Node):
    def __init__(self, language):
        self.language = language
    def render(self, context):
        view = resolve(context['request'].path)
        request_language = translation.get_language()
        translation.activate(self.language)
        url = reverse(view.url_name, args=view.args, kwargs=view.kwargs)
        translation.activate(request_language)
        return url

@register.tag(name='translate_url')
def do_translate_url(parser, token):
    language = token.split_contents()[1]
    return TranslatedURL(language)

It returns the current url in the desired language. Use it like this: {% translate_url de %}

Comments and suggestions for improvements are welcome.

7
votes

This snippet should do it:

https://djangosnippets.org/snippets/2875/

Once you've added that as a custom template tag, then you can do something like:

<a href='{% change_lang 'fr' %}'>View this page in French</a>

7
votes

I think it is worth mentioning that there is a built-in function called translate_url.

from django.urls import translate_url
translate_url(url, lang_code)
4
votes

Use django_hreflang:

{% load hreflang %}

<ul>
    <li><a href="{% translate_url 'en' %}" hreflang="en">English</a></li>
    <li><a href="{% translate_url 'ru' %}" hreflang="ru">Russian</a></li>
</ul>
3
votes

I think you are adding unneccessary complication to the problem. What you are looking for is a simple language selector. Django provides that functionality out of the box, and it always redirects to the current page (in another language).

This is documented here:

https://docs.djangoproject.com/en/dev/topics/i18n/translation/#django.conf.urls.i18n.set_language

The only thing is that the set_language view expects a POST parameter, so you need to use a <form> element; you cannot use a simple <a href="..."> link. However, sometimes you want the language selector to look like a link, not like a form with a select widget. My proposal is to use a form, but style it to look like a link.

Your template might look like this:

<nav id="language_chooser">
    <ul>
        {% get_language_info_list for LANGUAGES as languages %}
        {% for language in languages %}
            <form action="{% url 'set_language' %}" method="post">
                {% csrf_token %}
                <input name="next" type="hidden" value="{{ redirect_to }}" />
                <input name="language" type="hidden" value="{{ language.code }}" />
                <button type="submit">{{ language.local_name }}"</button>
            </form>
        {% endfor %}
    </ul>
</nav>

And then you use CSS to style the forms and submit buttons to look like normal links:

ul#language_chooser form {
    display: inline; 
    margin: 0;
    padding: 0;
}

ul#language_chooser button {
    margin: 0;
    padding: 0;
    border: none;
    background: none;
    color: blue; /* whatever you like */
    text-decoration: underline; /* if you like */
}
3
votes

I use standart language form from docs

<form action="{% url 'set_language' %}" method="post" id="lang_changer">
{% csrf_token %}
<input name="next" type="hidden" value="{{ redirect_to }}" />
<select name="language">
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
    {{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
<input type="submit" value="Go" />
</form>

and jquery fix to work with url lang prefixes:

$('#lang_changer input[name="next"]').attr('value', '/'+window.location.pathname.substring(4));

run when page ready.

3
votes

A straightforward solution would be to use Django's translate_url function with a template tag:

# utils/templatetags/utils.py
from django.template import Library
from django.urls import translate_url as django_translate_url

register = Library()

@register.simple_tag(takes_context=True)
def translate_url(context, lang_code):
    path = context.get('request').get_full_path()
    return django_translate_url(path, lang_code)

Then use it this way in your html for language selection :

{% load i18n utils %}
{% get_available_languages as languages %}

<ul>
{% for lang_code, lang_name in languages %}
     <li><a href="{% translate_url lang_code %}">{{ lang_code }}</a></li>
{% endfor %}
</ul>

And for hreflangs :

{% get_available_languages as languages %}
{% for lang_code, lang_name in languages %}
    <link rel="alternate" hreflang="{{lang_code}}" href="{% translate_url lang_code %}" />
{% endfor %}

Hope this helps.

2
votes

For Django 2.0 (based on Philipp Zedler's answer)

Custom template:

from django import template
from django.urls import reverse
from django.urls import resolve
from django.utils import translation
register = template.Library()

@register.simple_tag(takes_context=True)
def translate_url(context, language):
  view = resolve(context['request'].path)
  request_language = translation.get_language()
  translation.activate(language)
  url = reverse(view.app_name+":"+view.url_name, args=view.args, kwargs=view.kwargs, )
  translation.activate(request_language)
  return url

In Template:

{% get_available_languages as LANGUAGES %}
<ul>
  {% for lang_code, lang_name in LANGUAGES %}
    <li><a href="{% translate_url lang_code %}">{{ lang_name }}</a></li>
  {% endfor %}
</ul>
1
votes

The problem I had with the custom template tag is that the function calculates the other language equivalent based on the current url, as I am using modeltranslation package then the slug was always the same between urls. e.g.:

example.com/en/article/english-slug
example.com/es/articulo/english-slug

To fix that I took a slightly different approach, calculating the alternate urls at view level and have them available in the template context.

For this to work:

1- Create a utils.py file with the following helper function

from django.utils.translation import activate, get_language
from django.conf import settings

def getAlternateUrls(object):
    #iterate through all translated languages and get its url for alt lang meta tag                      
    cur_language = get_language()
    altUrls = {}
    for language in settings.LANGUAGES:
        try:
            code = language[0]
            activate(code)
            url = object.get_absolute_url()
            altUrls[code] = url
        finally:
            activate(cur_language)
    return altUrls;

2- Have your models define the reverse url: get_absolute_url

3- Add a context variable that will hold the urls dictionary in your views.py

from .utils import getAlternateUrls
...
def MyView(DetailView):
    def get_context_data(self, **kwargs):
        context['alt_urls'] = getAlternateUrls(self.object)

4- Generate the alternate urls meta tags at the head section of the template

<!-- alternate lang -->
{% for key, value in alt_urls.items %}
<link rel="alternate" hreflang="{{ key }}" href="http://{{ request.get_host }}{{ value}}">
{% endfor %}
{% endblock %}

Tested in Django 1.8

1
votes

I tried to make it as simple as possible - to use dynamic reverse() with any number of kwargs, so that language switch (or any other similar stuff) will redirect to the current view.

Added simple template tag in templatetags dir file (for example, templatetags/helpers.py):

from django.core.urlresolvers import reverse

register = template.Library()


@register.simple_tag
def get_url_with_kwargs(request):
    url_name = ''.join([
        request.resolver_match.app_name,
        ':',
        request.resolver_match.url_name,
    ])

    url_kwargs = request.resolver_match.kwargs

    return reverse(url_name, None, None, url_kwargs)

Which could be used in language switch template like this:

{% load helpers %}

{% get_available_languages as available_languages %}
{% get_language_info_list for available_languages as language_info_list %}

{% for language in language_info_list %}

    {% language language.code %}

        {% get_url_with_kwargs request as url_with_kwargs %}
        <a href="{{ url_with_kwargs }}">{{ language.code }}</a>

    {% endlanguage %}

{% endfor %}

Works for me quite well.

1
votes

I'd rather comment on accepted answer, but can't so I am posting my own. I am using fairly similar solution based on: https://djangosnippets.org/snippets/2875/

There is problem that both resolve and reverse methods can crash:

  • resolve can raise Resolver404 exception, especially when you are already displaying 404 page (causing 500 error instead, very annoying and hard to detect especially with DEBUG=True not displaying real 404)
  • reverse can crash when you are trying to get page with different language that actually doesn't have translation.

Maybe reverse depends more on what kind of translations method you use or whatever, but resolve crash within 404 page is pretty obvious.

In case of exception you may want to either return same url or maybe url to index page rather than raising exception in template. Code may look like this:

from django.core.urlresolvers import resolve, reverse
from django.utils.translation import activate, get_language


@register.simple_tag(takes_context=True, name="change_lang")
def change_lang(context, lang=None, *args, **kwargs):
    url = context['request'].path
    cur_language = get_language()
    try:
        url_parts = resolve(url)
        activate(lang)
        url = reverse(url_parts.view_name, kwargs=url_parts.kwargs)
    except:
        url = reverse("index") #or whatever page you want to link to
        # or just pass if you want to return same url
    finally:
        activate(cur_language)
    return "%s" % url
1
votes

Works for me in Django 2.2

Create a custom template tag

from django import template
from django.urls import translate_url

register = template.Library()


@register.simple_tag(takes_context=True)
def change_lang(context, lang=None, *args, **kwargs):
    path = context['request'].path
    return translate_url(path, lang)

In the template

{% load change_lang %}
<a href="{% change_lang 'en' %}">English</a>
<a href="{% change_lang 'es' %}">Español</a>
1
votes

Django 3 solution, based on Jonhatan's answer:

File: App/templatetags.py or App/templatetags/change_lang.py:

from django.template.defaultfilters import register
from django.urls import translate_url


@register.simple_tag(takes_context=True)
def change_lang(context, lang=None, *args, **kwargs):
    path = context['request'].path
    return translate_url(path, lang)

Template:

{% load trans change_lang %}
{% trans 'View page in other languages:' %}
<a href="{% change_lang 'en' %}">English</a>
| <a href="{% change_lang 'de' %}">Deutsch</a>