# Django Internationalization and Localization

## Overview

Django provides full support for translation of text, formatting of dates/times/numbers, and timezone handling. The framework allows a single web application to serve content in multiple languages and formats tailored to different audiences. Internationalization (i18n) is the process of preparing software for localization, while localization (l10n) is the process of writing translations and local formats.

## Core Concepts

### Key Settings

```python
# settings.py

# Enable translation machinery
USE_I18N = True

# Default language
LANGUAGE_CODE = "en-us"

# Available languages
from django.utils.translation import gettext_lazy as _

LANGUAGES = [
    ("de", _("German")),
    ("en", _("English")),
    ("fr", _("French")),
]

# Translation file locations
LOCALE_PATHS = [
    BASE_DIR / "locale",
]

# Enable timezone support
USE_TZ = True
TIME_ZONE = "UTC"

# Thousand separator in number formatting
USE_THOUSAND_SEPARATOR = True
```

### Required Middleware

```python
MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",  # Must be after Session, before Common
    "django.middleware.common.CommonMiddleware",
]
```

## Translation Functions

### gettext() and gettext_lazy()

```python
from django.utils.translation import gettext as _

def my_view(request):
    output = _("Welcome to my site.")
    return HttpResponse(output)
```

Use `gettext_lazy()` for module-level strings evaluated at import time:

```python
from django.db import models
from django.utils.translation import gettext_lazy as _

class MyThing(models.Model):
    name = models.CharField(_("name"), max_length=100)

    class Meta:
        verbose_name = _("my thing")
        verbose_name_plural = _("my things")
```

### String Interpolation

```python
# Use named interpolation (allows translators to reorder placeholders)
output = _("Today is %(month)s %(day)s.") % {"month": m, "day": d}

# format() also works
_("Welcome {name}").format(name=name)

# NEVER use f-strings (variable substituted before translation)
_(f"Welcome {name}")  # WRONG
```

### ngettext() - Pluralization

```python
from django.utils.translation import ngettext

page = ngettext(
    "there is %(count)d object",
    "there are %(count)d objects",
    count,
) % {"count": count}
```

Lazy variant for class-level usage:

```python
from django.utils.translation import ngettext_lazy

class MyForm(forms.Form):
    error_message = ngettext_lazy(
        "You only provided %(num)d argument",
        "You only provided %(num)d arguments",
        "num",  # Pass key name instead of integer
    )
```

### pgettext() - Contextual Translation

For words with multiple meanings (e.g., "May" as month or verb):

```python
from django.utils.translation import pgettext, pgettext_lazy

month = pgettext("month name", "May")

# In models
class MyThing(models.Model):
    name = models.CharField(
        help_text=pgettext_lazy("help text for MyThing", "This is the help text")
    )
```

### format_lazy()

```python
from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy

name = gettext_lazy("John Lennon")
instrument = gettext_lazy("guitar")
result = format_lazy("{name}: {instrument}", name=name, instrument=instrument)
```

## Template Translation

### Basic Tags

```django
{% load i18n %}

{# Simple translation #}
<title>{% translate "This is the title." %}</title>

{# Store in variable #}
{% translate "This is the title" as the_title %}
<title>{{ the_title }}</title>

{# With context #}
{% translate "May" context "month name" %}
```

### blocktranslate Tag

```django
{% load i18n %}

{# Variable interpolation #}
{% blocktranslate with amount=article.price %}
That will cost $ {{ amount }}.
{% endblocktranslate %}

{# Pluralization #}
{% blocktranslate count counter=list|length %}
There is {{ counter }} object.
{% plural %}
There are {{ counter }} objects.
{% endblocktranslate %}

{# Trimmed (normalizes whitespace for .po files) #}
{% blocktranslate trimmed %}
  First sentence.
  Second paragraph.
{% endblocktranslate %}
```

### Language Switching in Templates

```django
{% load i18n %}

{% get_current_language as LANGUAGE_CODE %}
<p>Current: {{ LANGUAGE_CODE }}</p>

{% language "en" %}
    <p>{% translate "Welcome" %}</p>
{% endlanguage %}
```

### Language Info Tags and Filters

```django
{% get_available_languages as LANGUAGES %}
{% get_language_info for "de" as lang %}
{{ lang.name }} / {{ lang.name_local }} / {{ lang.bidi }}

{{ LANGUAGE_CODE|language_name }}
{{ LANGUAGE_CODE|language_name_local }}
{{ LANGUAGE_CODE|language_bidi }}
```

## Language Detection

The `LocaleMiddleware` detects language in this order (first match wins):

1. URL language prefix (from `i18n_patterns()`)
2. Language cookie (`django_language` by default)
3. `Accept-Language` HTTP header
4. `LANGUAGE_CODE` setting (fallback)

## URL Internationalization

### Language Prefix in URLs

```python
from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path

urlpatterns = [
    path("sitemap.xml", sitemap, name="sitemap-xml"),  # No prefix
]

urlpatterns += i18n_patterns(
    path("about/", about_views.main, name="about"),
    path("news/", include(news_patterns, namespace="news")),
)
```

### Translating URL Patterns

```python
from django.utils.translation import gettext_lazy as _

urlpatterns = i18n_patterns(
    path(_("about/"), about_views.main, name="about"),
    path(_("news/"), include(news_patterns, namespace="news")),
)
```

### Set Language View

```python
urlpatterns = [
    path("i18n/", include("django.conf.urls.i18n")),
]
```

Template form for language switching:

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

## Explicit Language Control

```python
from django.utils import translation

# Activate a language
translation.activate("de")

# Get current language
code = translation.get_language()

# Context manager
with translation.override("fr"):
    text = translation.gettext("welcome")  # French translation
# Automatically reverts after block
```

## Message Files

### Creating Message Files

```bash
# Create .po file for German
django-admin makemessages -l de

# All languages
django-admin makemessages -a

# JavaScript translations
django-admin makemessages -d djangojs -l de
```

### .po File Format

```
#. Translators: This message appears on the home page only
#: path/to/python/file.py:123
msgid "Welcome to my site."
msgstr "Willkommen auf meiner Seite."

# Contextual
msgctxt "month name"
msgid "May"
msgstr "Mai"

# Plural forms
msgid "there is %(count)d object"
msgid_plural "there are %(count)d objects"
msgstr[0] "es gibt %(count)d Objekt"
msgstr[1] "es gibt %(count)d Objekte"
```

### Compiling Message Files

```bash
django-admin compilemessages
```

Creates binary `.mo` files from `.po` sources. Fuzzy entries are skipped.

### Discovery Order

1. `LOCALE_PATHS` directories (highest precedence)
2. App-specific `locale/` directories (in `INSTALLED_APPS` order)
3. Django default translations in `django/conf/locale/`

## JavaScript Translation

```python
from django.views.i18n import JavaScriptCatalog

urlpatterns = [
    path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
]
```

```javascript
// In JavaScript
document.write(gettext("this is to be translated"));

// Pluralization
const string = ngettext("one object", "%s objects", count);

// Named interpolation
const string = interpolate(formats, {count: 10, total: 50}, true);

// Contextual
document.write(pgettext("month name", "May"));
```

## Format Localization

### Custom Format Files

```python
# settings.py
FORMAT_MODULE_PATH = ["mysite.formats"]
```

```python
# mysite/formats/en/formats.py
THOUSAND_SEPARATOR = "\xa0"  # Non-breaking space
```

### Template Tags and Filters

```django
{% load l10n %}

{# Enable/disable localization in a block #}
{% localize on %}
    {{ value }}
{% endlocalize %}

{# Per-value filters #}
{{ value|localize }}
{{ value|unlocalize }}
```

### Localized Form Input

```python
class CashRegisterForm(forms.Form):
    revenue = forms.DecimalField(max_digits=4, decimal_places=2, localize=True)
```

## Timezone Support

### Core Utility Functions

```python
from django.utils import timezone
import zoneinfo

# Current time (aware when USE_TZ=True)
now = timezone.now()

# Activate a timezone for rendering
timezone.activate(zoneinfo.ZoneInfo("Europe/Paris"))
timezone.deactivate()

# Convert to current timezone
local_time = timezone.localtime(timezone.now())

# Convert between naive and aware
aware_dt = timezone.make_aware(naive_dt, zoneinfo.ZoneInfo("Europe/Paris"))
naive_dt = timezone.make_naive(aware_dt)

# Check awareness
timezone.is_aware(some_datetime)
timezone.is_naive(some_datetime)
```

### Timezone Middleware

```python
import zoneinfo
from django.utils import timezone

class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = request.session.get("django_timezone")
        if tzname:
            timezone.activate(zoneinfo.ZoneInfo(tzname))
        else:
            timezone.deactivate()
        return self.get_response(request)
```

### Timezone Template Tags

```django
{% load tz %}

{% localtime on %}
    {{ value }}  {# Converted to current timezone #}
{% endlocaltime %}

{% timezone "Europe/Paris" %}
    Paris time: {{ value }}
{% endtimezone %}

{{ value|localtime }}
{{ value|utc }}
{{ value|timezone:"Europe/Paris" }}

{% get_current_timezone as TIME_ZONE %}
```

## Best Practices

- Always use lazy translation (`gettext_lazy`) in models, forms, and class-level attributes
- Use named string interpolation for multi-parameter strings to allow translator reordering
- Never use f-strings with translation functions
- Load `{% load i18n %}` in every template that uses translation tags
- Use context markers (`pgettext`) for ambiguous words
- Always compile messages after editing `.po` files
- Store datetimes in UTC; convert only in templates and forms
- Use `timezone.now()` instead of `datetime.datetime.now()`
- Use `trimmed` option on `blocktranslate` tags to keep `.po` files clean
