33

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.

Danilo Bargen
  • 18,626
  • 15
  • 91
  • 127

14 Answers14

18

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.

Tobit
  • 406
  • 7
  • 19
Philipp Zedler
  • 1,660
  • 1
  • 17
  • 36
  • 4
    Thanks, your solution works for me with little improvement. I am using simple tag within forloop ... `@register.simple_tag(name='translate_url') def do_translate_url(language): return TranslatedURL(language)` ... Then in template ... `{% get_language_info_list for LANGUAGES as languages %} {% for language in languages %} {{ language.name_local }} {% endfor %}` – pista329 Jan 13 '14 at 13:53
  • This seems to fail if the kwargs are language-dependent - for example with a language-dependent `slug` parameter in the url this renders the same slug for all languages. – Flash Feb 28 '17 at 17:38
  • @Flash did you figured out a way to work with slug parameters? I'm having that problem too :( – Luis Palacios Mar 29 '17 at 23:50
  • @LuisPalacios As a temporary hack, after `translation.activate` I'm manually fixing the slug with `view.kwargs['slug'] = MyModel.objects.get(pk=view.kwargs['id']).slug` - I am sure there is a better way. – Flash Mar 31 '17 at 13:32
  • @LuisPalacios, I've developed my solution for a project with [translated url patterns](https://docs.djangoproject.com/en/1.10/topics/i18n/translation/#translating-url-patterns) - and it worked. Anyway, when the URL is related to a model, we can define a method on the model that returns the URL in the desired language. – Philipp Zedler Mar 31 '17 at 13:44
  • @PhilippZedler thanks, I am also doing something similar to that with .get_abosolute_url(self, language) – Luis Palacios Mar 31 '17 at 16:26
  • I got: 'translate_url' did not receive value(s) for the argument(s): 'token' when use this solution for django 2.2 – panjianom Jul 11 '20 at 06:52
  • @pije76, also in Django 2.2, there should be a token argument when registering a tag: https://docs.djangoproject.com/en/2.2/howto/custom-template-tags/#registering-the-tag – Philipp Zedler Jul 11 '20 at 17:59
8

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)
Paweł Mucha
  • 226
  • 2
  • 3
7

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>

seddonym
  • 16,304
  • 6
  • 66
  • 71
  • The downside of this solution is: it doesn't preserve a selected language in contrast to `django.conf.urls.i18n.set_language`, therefore you should do it on your own, for example, by adding an extra middleware or a special view that will handle it. – potar Oct 24 '15 at 08:22
6

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.

Charlesthk
  • 9,394
  • 5
  • 43
  • 45
4

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>
Max Malysh
  • 29,384
  • 19
  • 111
  • 115
3

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 */
}
Nico
  • 39
  • 2
  • `{{ language.local_name }}` should be `{{ language.name_local }}`. – Danilo Bargen Aug 13 '12 at 21:32
  • 7
    Also, that solution doesn't really play nice together with language prefixes. We decided against using only the session or cookies to set and remember language and went for unique URLs instead. Therefore that approach doesn't really work for me... (But thanks anyways for contributing it.) – Danilo Bargen Aug 13 '12 at 21:35
  • 3
    I have similar issue and It's not helpful to use a form to solve that. I want to use "[alternate language meta tags](http://support.google.com/webmasters/bin/answer.py?hl=en&answer=189077)" to dictate to client(or bot) that "this url is current page's x language version". Any suggestion about that? – Murat Çorlu Nov 13 '12 at 23:03
  • You can also use a get parameter. I sometimes use it like this: `
    `
    – didierCH Apr 22 '21 at 08:26
3

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.

FeNUMe
  • 91
  • 2
  • Removing lang prefix works for me in Django 3.2 . I had to use this function https://stackoverflow.com/a/14482123/329395 on `window.location.pathname` because i had prefix like this one: `es-mx` – jartaud Jun 25 '21 at 02:17
3

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>
Daniel W.
  • 31,164
  • 13
  • 93
  • 151
2

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>
Stephen Kennedy
  • 20,585
  • 22
  • 95
  • 108
Roman
  • 21
  • 2
1

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

marcanuy
  • 23,118
  • 9
  • 64
  • 113
1

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.

Damaged Organic
  • 8,175
  • 6
  • 58
  • 84
1

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
K.H.
  • 1,383
  • 13
  • 33
1

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>
Jonhatan Fajardo
  • 348
  • 6
  • 11
0

Using translate_url() seems the simplest approach. The other upvoted answers to this question created other problems for me using Django 4 - the default langauge would always be seen by the browser, and the messages framework gave some buggy behavior. Using translate_url() doesn't have these drawbacks.

from django.urls.base import translate_url, seems to be undocumented.

Create a custom template tag like this:

class TranslatedURL(template.Node):
    def __init__(self, language):
        self.language = language

    def render(self, context):
        path = resolve(context["request"].path)
        url = reverse(path.view_name, args=path.args, kwargs=path.kwargs)
        new_url = translate_url(url, self.language)
        return new_url


@register.tag(name="translate_url")
def do_translate_url_django(parser, token):
    language_code = token.split_contents()[1]
    return TranslatedURL(language_code)

Once you've loaded your new tag, use it like:

{% translate_url_django nl %}

John
  • 949
  • 1
  • 9
  • 20
  • Completely wrong approach! if you use i18n_patterns in urls, this works: {% load i18n %}{% language lang.iso %}{% url 'my_url' %}{% endlanguage %}, right now i check it again. – Maxim Danilov Jul 21 '22 at 13:37
  • 1
    It is a valid approach, especially as your suggested solution does not work if the url isnt static. For example the following does not work: {% load i18n %} {% get_current_language as CURRENT_LANGUAGE %} {% get_available_languages as AVAILABLE_LANGUAGES %} {% get_language_info_list for AVAILABLE_LANGUAGES as languages %} {% for language in languages %} {% if language.code != CURRENT_LANGUAGE %} {% language language.code|safe %} {{ language.code|safe }} {% endlanguage %} {% endif %} {% endfor %} – John Jul 21 '22 at 19:56
  • Again not the right approach: This is a template, you can send any translated url_list in context. For example: trans_urls = ((translate_url(request.path, lang), title) for lang, title in settings.LANGIUAGES if lang != get_language()). This is the generator. It's simple and works faster than the 5 template tags. Of course, it depends on the logic: "you want a slow and big solution with many lines of code in the template" or a faster one-line solution in the view. – Maxim Danilov Jul 21 '22 at 20:45
  • 1
    I think this is a valid approach for dynamic urls (like in the navbar). I don't want to be passing trans_urls in context — a custom template tag is the perfect use case to avoid that. – greenie-beans Apr 26 '23 at 19:37