Django internationalization without URL prefixes
Project description
django-i18n-noprefix
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 |
| API Routes | /api/v1/en/users/ ๐ฑ |
/api/v1/users/ โจ |
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:
- URL parameter (when switching languages)
- Session (for logged-in users)
- Cookie (for anonymous users)
- Accept-Language header (browser preference)
- 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:
-
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), # ... ]
-
Replace middleware (as shown in Quick Start)
-
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_APPSandMIDDLEWARE - Check that cookies are not blocked in the browser
- Verify
NoPrefixLocaleMiddlewareis afterSessionMiddleware
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!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_i18n_noprefix-0.1.0.tar.gz.
File metadata
- Download URL: django_i18n_noprefix-0.1.0.tar.gz
- Upload date:
- Size: 31.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fae95ed93edf4d713a10abc78964a3bf91ae07cabfc3267f52592ca9470bba34
|
|
| MD5 |
5146c7967dc78a4b7c2d8e1cd6126912
|
|
| BLAKE2b-256 |
da19f3dfa429331718573223106418d163ddb40129748692ed1e5e2a3ff47aa8
|
File details
Details for the file django_i18n_noprefix-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_i18n_noprefix-0.1.0-py3-none-any.whl
- Upload date:
- Size: 38.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.10.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a7014c7eeaf03742dd95c5ef1f73d55c39c2b32722f3c9093ed6ebfa5448d6ff
|
|
| MD5 |
f80366c0a95e75d7529b473954c70163
|
|
| BLAKE2b-256 |
9dbd081b080306dbf247aa0e931f683a7cd5af643d84bb7dd2755d695b8bcc27
|