Skip to main content

django-i18n-noprefix

Tests Code Quality Release codecov PyPI Version Python Support Django Support License

Clean URLs for multilingual Django sites without language prefixes.

Seamlessly integrate Django's i18n framework while keeping your URLs clean. No more /en/about/ or /ko/about/ — just /about/ that works in any language.

🎯 Why django-i18n-noprefix?

Django's built-in i18n system automatically adds language codes to your URLs (/en/, /ko/, /ja/). While this works, it has drawbacks:

The Problem with URL Prefixes

Issue With Prefixes With django-i18n-noprefix
URLs /en/products/
/ko/products/
/products/
SEO Duplicate content issues Single URL per content
Sharing Language-specific links Universal links
User Experience Exposes technical details Clean, professional URLs

Real-World Example

# Before: Django default i18n
https://mysite.com/en/products/laptop/    # English
https://mysite.com/ko/products/laptop/    # Korean
https://mysite.com/ja/products/laptop/    # Japanese

# After: With django-i18n-noprefix
https://mysite.com/products/laptop/        # Any language!

✨ Features

  • 🚫 No URL prefixes — Keep URLs clean and professional
  • 🔄 Full Django i18n compatibility — Use all Django's i18n features
  • 🍪 Smart language detection — Session, cookie, and Accept-Language header support
  • 🎨 3 language selector styles — Dropdown, list, or inline
  • 🎯 Framework agnostic — Bootstrap, Tailwind, or vanilla CSS
  • ⚡ Zero configuration — Works out of the box
  • 🔒 Production ready — 93% test coverage
  • 📦 Lightweight — No external dependencies except Django

🚀 Quick Start (5 minutes)

1. Install

pip install django-i18n-noprefix

2. Update Settings

# settings.py

INSTALLED_APPS = [
    # ...
    'django_i18n_noprefix',  # Add this
]

MIDDLEWARE = [
    # ...
    'django.contrib.sessions.middleware.SessionMiddleware',
    # 'django.middleware.locale.LocaleMiddleware',  # Remove this
    'django_i18n_noprefix.middleware.NoPrefixLocaleMiddleware',  # Add this
    'django.middleware.common.CommonMiddleware',
    # ...
]

# Configure languages
LANGUAGES = [
    ('en', 'English'),
    ('ko', '한국어'),
    ('ja', '日本語'),
]
LANGUAGE_CODE = 'en'

3. Add URL Pattern

# urls.py
from django.urls import path, include

urlpatterns = [
    # ...
    path('i18n/', include('django_i18n_noprefix.urls')),
]

4. Add Language Selector

<!-- base.html -->
{% load i18n_noprefix %}

<!DOCTYPE html>
<html>
<body>
    <!-- Add anywhere in your template -->
    {% language_selector %}

    <!-- Your content -->
    {% block content %}{% endblock %}
</body>
</html>

That's it! Your site now supports multiple languages without URL prefixes.

📋 Detailed Installation

Using pip

pip install django-i18n-noprefix

Using uv

uv pip install django-i18n-noprefix

Development Installation

git clone https://github.com/jinto/django-i18n-noprefix.git
cd django-i18n-noprefix
pip install -e ".[dev]"

🔧 Configuration

Basic Configuration

The minimal configuration shown in Quick Start is often sufficient. Here are all available options:

# settings.py

# Required: Enable Django i18n
USE_I18N = True
USE_L10N = True  # Django < 5.0
USE_TZ = True

# Language Configuration
LANGUAGES = [
    ('en', 'English'),
    ('ko', '한국어'),
    ('ja', '日本語'),
    ('zh', '中文'),
    ('es', 'Español'),
]

LANGUAGE_CODE = 'en'  # Default language

# Optional: Customize cookie name (default: 'django_language')
LANGUAGE_COOKIE_NAME = 'django_language'
LANGUAGE_COOKIE_AGE = 365 * 24 * 60 * 60  # 1 year
LANGUAGE_COOKIE_PATH = '/'
LANGUAGE_COOKIE_DOMAIN = None
LANGUAGE_COOKIE_SECURE = False
LANGUAGE_COOKIE_HTTPONLY = False
LANGUAGE_COOKIE_SAMESITE = 'Lax'

Language Detection Priority

Languages are detected in this order:

  1. URL parameter (when switching languages)
  2. Session (for logged-in users)
  3. Cookie (for anonymous users)
  4. Accept-Language header (browser preference)
  5. LANGUAGE_CODE setting (fallback)

📖 Usage Examples

Basic Language Selector

{% load i18n_noprefix %}

<!-- Dropdown style (default) -->
{% language_selector %}

<!-- List style -->
{% language_selector style='list' %}

<!-- Inline style -->
{% language_selector style='inline' %}

Custom Language Selector

{% load i18n i18n_noprefix %}
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as LANGUAGES %}

<!-- Custom dropdown -->
<select onchange="location.href=this.value">
    {% for lang_code, lang_name in LANGUAGES %}
        <option value="{% switch_language_url lang_code %}"
                {% if lang_code == CURRENT_LANGUAGE %}selected{% endif %}>
            {{ lang_name }}
        </option>
    {% endfor %}
</select>

<!-- Custom buttons -->
<div class="language-buttons">
    {% for lang_code, lang_name in LANGUAGES %}
        <a href="{% switch_language_url lang_code %}"
           class="btn {% if lang_code|is_current_language %}active{% endif %}">
            {{ lang_code|upper }}
        </a>
    {% endfor %}
</div>

Programmatic Language Change

# views.py
from django.shortcuts import redirect
from django_i18n_noprefix.utils import activate_language

def my_view(request):
    # Change language programmatically
    activate_language(request, 'ko')
    return redirect('home')

# Or in class-based views
from django.views import View

class LanguagePreferenceView(View):
    def post(self, request):
        language = request.POST.get('language')
        if language:
            activate_language(request, language)
        return redirect(request.META.get('HTTP_REFERER', '/'))

AJAX Language Switching

// JavaScript
function changeLanguage(langCode) {
    fetch('/i18n/set-language-ajax/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken')
        },
        body: JSON.stringify({language: langCode})
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            location.reload();
        }
    });
}

Using with Django Forms

# forms.py
from django import forms
from django.conf import settings

class LanguageForm(forms.Form):
    language = forms.ChoiceField(
        choices=settings.LANGUAGES,
        widget=forms.Select(attrs={'class': 'form-control'})
    )

# views.py
from django.shortcuts import redirect
from django_i18n_noprefix.utils import activate_language

def change_language_view(request):
    if request.method == 'POST':
        form = LanguageForm(request.POST)
        if form.is_valid():
            activate_language(request, form.cleaned_data['language'])
            return redirect('home')
    else:
        form = LanguageForm()
    return render(request, 'change_language.html', {'form': form})

🔄 Integration with Django i18n

This package complements Django's i18n system. You can use all Django i18n features:

Using Django's Translation Features

{% load i18n %}

<!-- Translation tags work normally -->
{% trans "Welcome" %}
{% blocktrans %}Hello {{ name }}{% endblocktrans %}

<!-- Get current language -->
{% get_current_language as LANGUAGE_CODE %}
Current language: {{ LANGUAGE_CODE }}

<!-- Get available languages -->
{% get_available_languages as LANGUAGES %}

In Python Code

from django.utils.translation import gettext as _
from django.utils.translation import get_language

def my_view(request):
    # Django's translation functions work normally
    message = _("Welcome to our site")
    current_language = get_language()

    return render(request, 'template.html', {
        'message': message,
        'language': current_language
    })

Migration from Standard Django i18n

Migrating from Django's default i18n is straightforward:

  1. Remove URL prefixes:

    # Old: urls.py
    from django.conf.urls.i18n import i18n_patterns
    
    urlpatterns = i18n_patterns(
        path('about/', views.about),
        # ...
    )
    
    # New: urls.py
    urlpatterns = [
        path('about/', views.about),
        # ...
    ]
    
  2. Replace middleware (as shown in Quick Start)

  3. Update language switcher (use our template tags)

That's it! All your translations and language files remain unchanged.

🎨 Styling Options

Bootstrap 5

{% load static %}
<link href="{% static 'i18n_noprefix/css/bootstrap5.css' %}" rel="stylesheet">

{% language_selector style='dropdown' %}

Tailwind CSS

{% load static %}
<link href="{% static 'i18n_noprefix/css/tailwind.css' %}" rel="stylesheet">

{% language_selector style='inline' %}

Vanilla CSS

{% load static %}
<link href="{% static 'i18n_noprefix/css/vanilla.css' %}" rel="stylesheet">

{% language_selector style='list' %}

Custom Styling

/* Override CSS variables */
:root {
    --i18n-primary: #007bff;
    --i18n-primary-hover: #0056b3;
    --i18n-text: #333;
    --i18n-bg: #fff;
    --i18n-border: #ddd;
    --i18n-radius: 4px;
}

/* Or write custom CSS */
.language-selector-dropdown {
    /* Your styles */
}

📊 Django/Python Compatibility Matrix

Django Version Python 3.8 Python 3.9 Python 3.10 Python 3.11 Python 3.12
4.2 LTS ✅ ✅ ✅ ✅ ✅
5.0 ❌ ❌ ✅ ✅ ✅
5.1 ❌ ❌ ✅ ✅ ✅
5.2 ❌ ❌ ✅ ✅ ✅

🎮 Example Project

See the package in action with our complete example project:

# Clone the repository
git clone https://github.com/jinto/django-i18n-noprefix.git
cd django-i18n-noprefix/example_project

# Install dependencies
pip install django

# Run the demo
./run_demo.sh

# Visit http://localhost:8000

The example project includes:

  • Multi-language support (English, Korean, Japanese)
  • All three language selector styles
  • Bootstrap, Tailwind, and Vanilla CSS examples
  • Complete translation files
  • Production-ready configuration

🔍 API Reference

Middleware

class NoPrefixLocaleMiddleware:
    """
    Replacement for Django's LocaleMiddleware that doesn't use URL prefixes.

    Methods:
        get_language(request) -> str: Detect language from request
        save_language(request, response, language) -> None: Persist language choice
    """

Template Tags

{% load i18n_noprefix %}

<!-- Render language selector -->
{% language_selector [style='dropdown|list|inline'] [next_url='/custom/'] %}

<!-- Get URL for language switch -->
{% switch_language_url 'ko' [next_url='/custom/'] %}

<!-- Check if language is current -->
{{ 'ko'|is_current_language }} → True/False

Views

# URL: /i18n/set-language/<lang_code>/
def change_language(request, lang_code):
    """Change language and redirect."""

# URL: /i18n/set-language-ajax/
def set_language_ajax(request):
    """AJAX endpoint for language change."""

Utility Functions

from django_i18n_noprefix.utils import (
    activate_language,       # Activate language for request
    get_supported_languages, # Get list of language codes
    get_language_choices,    # Get language choices for forms
    is_valid_language,      # Validate language code
)

🐛 Troubleshooting

Common Issues and Solutions

Language not persisting across requests

  • Ensure sessions are enabled in INSTALLED_APPS and MIDDLEWARE
  • Check that cookies are not blocked in the browser
  • Verify NoPrefixLocaleMiddleware is after SessionMiddleware

Translations not working

# Check these settings
USE_I18N = True
LOCALE_PATHS = [BASE_DIR / 'locale']

# Run these commands
python manage.py makemessages -l ko
python manage.py compilemessages

Static files not loading

python manage.py collectstatic

Middleware order issues

# Correct order
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',  # ← First
    'django_i18n_noprefix.middleware.NoPrefixLocaleMiddleware',  # ← Second
    'django.middleware.common.CommonMiddleware',
    # ...
]

Debug Mode

# Enable debug logging
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'django_i18n_noprefix': {
            'handlers': ['console'],
            'level': 'DEBUG',
        },
    },
}

🚀 Performance

  • Zero overhead: Middleware adds < 0.1ms per request
  • Smart caching: Language preference cached in session/cookie
  • No database queries: Pure middleware solution
  • CDN friendly: No URL prefixes mean better cache utilization

🔒 Security Considerations

  • CSRF protection on language change endpoints
  • XSS safe: All outputs are properly escaped
  • Cookie security: Supports Secure, HttpOnly, and SameSite flags
  • No user input in URL paths

🤝 Contributing

We welcome contributions! See CONTRIBUTING.md for guidelines.

# Setup development environment
git clone https://github.com/jinto/django-i18n-noprefix.git
cd django-i18n-noprefix
pip install -e ".[dev]"

# Run tests
pytest

# Run with coverage
pytest --cov=django_i18n_noprefix

# Code formatting
black .
ruff check .

# Type checking
mypy django_i18n_noprefix

📝 Changelog

See CHANGELOG.md for release history.

📄 License

MIT License - see LICENSE for details.

🙏 Credits

Built with ❤️ by jinto and contributors.

Inspired by Django's excellent i18n framework and the needs of real-world multilingual applications.

📚 Resources


Need help? Open an issue or check our example project.

Like this project? Give it a ⭐ on GitHub!

Metadata

Release files for django-i18n-noprefix 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-i18n-noprefix 0.1.1
File Size Uploaded Provenance
django_i18n_noprefix-0.1.1.tar.gz 31.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for django-i18n-noprefix 0.1.1
File Interpreter ABI Platform
django_i18n_noprefix-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 69.6 kB

Release files / django_i18n_noprefix-0.1.1.tar.gz

Download URL django_i18n_noprefix-0.1.1.tar.gz
Size 31.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a982afa7ac0621690b4f51964b6c90b5f9256f25e917af2c0eb5f9ed2cc5a951
BLAKE2b-256 checksum
How to use checksums
5461bf6825d32be833902f56c1b4c613e991d90d8b95ae4bd164985303118cfe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.18

Release files / django_i18n_noprefix-0.1.1-py3-none-any.whl

Download URL django_i18n_noprefix-0.1.1-py3-none-any.whl
Size 38.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
92a4d9bc448dfd0dff16a5927324901690fd0bd997540d92cb0233cbfa932b59
BLAKE2b-256 checksum
How to use checksums
dddd7a39a2d61be9209c675587c210d384afbfbcfeefb6f01707f1c260ddef52
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.18

Security for django-i18n-noprefix 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page