Exploring Django format localization settings
Django supports many languages and locales out of the box. An important aspect of locale-aware applications is number formatting and Django provides quite a few knobs and switches, but using them isn’t entirely straightforward.
The goal and some assumptions
Django ships with the wrong thousand separator for one of the locales I use, so I was interested in finding a way to fix this in my code. Furthermore, I needed a locale-unaware thousand separator while keeping the rest of the localization machinery. This was a good opportunity to dive in.
First, let’s take a look at settings.py
. These are the defaults when starting a new project via startproject
, for Django 1.10:
LANGUAGE_CODE = 'en-us'
USE_I18N = True
USE_L10N = True
Interestingly, the docs say that the default is USE_L10N = False
, but startproject
, for convenience, sets USE_L10N = True
.
Using template filters to format numbers with a custom thousand separator
If you need to format integers only in a couple of places, doing it in the template makes sense. Let’s explore options.
Searching for solutions, one will surely come across django.contrib.humanize
which provides a set of useful template filters, such as intcomma
, which converts an integer to a string containing commas every three digits. Pretty close, but we need a dot, not a comma. As an interesting aside, this filter respects format localization if enabled (by USE_L10N = True
). This means that if the active locale uses a dot as the thousand separator, intcomma
will use a dot when formatting output values.
This point can be illustrated by strategically using unlocalize
:
# In settings.py
# Remember the Germans use a dot
LANGUAGE_CODE = 'de'
USE_THOUSAND_SEPARATOR = True
Quite paradoxical for a template tag named intcomma
. In case you were wondering, using {% localize off %}
is similarly disregarded.
So we’ve reached the conclusion that intcomma
isn’t trully intcomma
unless localization is disabled entirely by USE_L10N = False
in settings.py
. But this is what RTFM is for.
Since the analogous intdot
is nowhere to be seen (and not for lack of effort), we’ll have to do it on our own, while at the same time ignoring localization. Implementing a custom Django template filter is easy, we can just plop something like this into a templatetags
directory inside our app:
import re
from django import template
from django.template.defaultfilters import stringfilter
from django.utils.encoding import force_text
register = template.Library()
@register.filter(is_safe=True)
@stringfilter
def intdot(value):
orig = force_text(value)
new = re.sub("^(-?\d+)(\d{3})", '\g<1>.\g<2>', orig)
if orig == new:
return new
else:
return intdot(new)
Then we can use it in our templates like so:
Drilling into format localization settings
Now, say we have a model field that holds a number like 1000000
. We need to display this value in a template as 1.000.000
and the current locale is en
:
Django template | Rendered HTML |
---|---|
<span>{{ price }}</span> |
<span>1000000</span> |
Ok, so first we need to set USE_THOUSAND_SEPARATOR = True
in settings.py
, to enable thousand separators. Remember that since we’ve instructed Django to be locale-aware and the locale has been set to en
via LANGUAGE_CODE
, the thousand separator is locale-specific, hence a comma:
Django template | Rendered HTML |
---|---|
<span>{{ price }}</span> |
<span>1,000,000</span> |
Django provides a way to override number formatting in settings.py
. Since we’ve set USE_THOUSAND_SEPARATOR = True
, now we can also specify these:
THOUSAND_SEPARATOR = '.'
NUMBER_GROUPING = 3
DECIMAL_SEPARATOR = ','
But does this work? Nope, because of the fineprint:
When USE_L10N is set to True and if this is also set to True, Django will use the values of THOUSAND_SEPARATOR and NUMBER_GROUPING to format numbers unless the locale already has an existing thousands separator. If there is a thousands separator in the locale format, it will have higher precedence and will be applied instead.
This too seems paradoxical, as I would’ve expected my settings file to have the highest precedence, even if it meant I would lose the flexibility of supporting number format localization across locales by doing so. We’ll talk about the proper way to do it further down, using a custom locale format file on an ad-hoc basis. Anyhow, in our case, the en
locale format file (located in django/conf/locale/en/
) of course sets all of these.
Curious about how often a format file doesn’t set THOUSAND_SEPARATOR
, I decided to poke into django/conf/locale/
, where all the locale format files that ship with Django reside. Out of the 70 locales available, only 5 didn’t set THOUSAND_SEPARATOR
:
./ta/formats.py
./fy/formats.py
./te/formats.py
./mn/formats.py
./kn/formats.py
In other words, only in these locales would one be able to specify a THOUSAND_SEPARATOR
(and friends) that works if USE_L10N = True
.
Obviously, if we disable localization entirely, by USE_L10N = False
these settings do take effect no questions asked, but then we lose everything else, like locale-aware date formatting. In light of the above, it seems that creating a custom format file is the only way to selectively override a subset of locale formatting options.
Formatting numbers using a custom locale format file
It’s actually pretty simple. Just follow the instructions. Here we will be overriding a subset of the English format settings. First, we create the directory structure inside our app:
my-app/
formats/
__init__.py
en/
__init__.py
formats.py
These are the contents of formats.py
:
DECIMAL_SEPARATOR = ','
THOUSAND_SEPARATOR = '.'
NUMBER_GROUPING = 3
Then in settings.py
:
FORMAT_MODULE_PATH = [
'my-app.formats',
]
A bit involved, but it works, as long as the locale stays the same, i.e. if the locale changes, perhaps due to browser autodetection, the overridden settings won’t work. Other locales will obey their respective locale format files, unless you override them too, which seems like too much work. But there is a way, if you are so inclined.