An app to allow a single Django application to serve multiple domains and country sites and adjust the content based on that. Similar to the Django Sites Framework, but without the Site
hardcoded in the settings of the app. Instead it uses a CountrySite
object (similar to Site
) which can be set dynamically in the middleware based on the request domain, session, url parameter or visitor location.
pip install django-international-sites
- Add "international" to
INSTALLED_APPS
insettings.py
- Run
python manage.py migrate international
- Add
CountrySite
objects in admin/admin/international/countrysite/
# Fallback country code
DEFAULT_COUNTRY_CODE = "NL"
# Optional: Set the below if you want the middleware to set the current site based
# on the location/country deduced from the visitor IP
# When using international.middleware.InternationalSiteMiddleware obtain
# geoip license key (for free) at xx and set path were geoip2 country library is
# to be installed here
GEOIP_PATH = os.path.join("geoip")
GEOIP_LICENSE = "asecretkeybymaxmind"
# Map domains uniquely to a single country code (optional)
UNIQUE_DOMAINS = {"example.nl": "nl", "example.co.uk": "uk"}
# Directory for site icons to be displayed in admin (optional)
SITE_ICON_DIR = "static/site_icons/"
How is the country code detected from the request?
- If a unique domain name set in
settings.UNIQUE_DOMAINS
(e.g. example.nl), use country data of related country code - If country code is forced as url parameter (i.e. example.com/c=fr), use that country code
- If a cookie with location preference is used, use that country code
- Check location based on visitor IP address, use that country code
- If nothing could be detected, use default country code
Add the middleware to settings after the Django LocaleMiddleware
:
MIDDLEWARE = [
...
'django.middleware.locale.LocaleMiddleware',
'international.middleware.InternationalSiteMiddleware',
'vinaigrette.middleware.VinaigretteAdminLanguageMiddleware'
]
This makes the current CountrySite
object available through the request object. E.g., in views:
def index(request):
country_site = request.country_site
All models in a project can be made international, i.e. associated to countries and/or languages, by inheriting the InternationalModel
base class.
# models.py
from international.models import InternationalModel
class Product(InternationalModel):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
This will add the country_sites
and object_language
field and extend the model managers.
To filter all models associated with CountrySite nl
or with language "en":
# Get all items associated to a country code
products = Product.objects.by_country("nl")
# Get all items associated to a language code
products = Product.objects.by_language("en")
# Get all items associated to either a country code or language code
products = Product.by_country_or_language(country_code="nl", language_code="en")
When using in combination with Django's i18n translation, add the InternationalSiteMiddleware
before the Django LocaleMiddleware
in your project's settings.
If you want to force using the CountrySite.default_language
language for a given CountrySite, set FORCE_COUNTRY_LANGUAGE
to True. This will make sure that for e.g. the German country site, i18n
will use the German language that has been associated to the CountrySite.
# settings.py
FORCE_COUNTRY_LANGUAGE = True
The international.views.get_country_from_request
is included that will return a JSON response with the detected visitor location based on their IP address when the MaxMind GeoIP2 library is installed. To use it, include international.urls
in your project urls.py
. This will include the localize/
endpoint that only allows GET requests, with example response:
{
"country": "NL", # country code or null
"detected": true # false when the country could not be detected from the visitor IP
}
Use the International extension to the Django Sites Sitemap to create dynamic sitemaps based on the current request domain rather than a single fixed site domain. First, use the Django Sitemaps like usual but instead of using the out-of-the-box django.contrib.sites.sitemaps.views
import the same views from international.sitemaps.views
, this will change the domain of the urls shown in the sitemap to that of the current request CountrySite instead of the hardcoded Site domain (which can only be one per application).
from international.sitemaps import views as international_sitemap_views
# Register sitemap views
urlpatterns += (
path(
r"sitemap.xml", international_sitemap_views.index, {"sitemaps": sitemap_sections}
),
path(
"sitemap-<section>.xml",
international_sitemap_views.sitemap,
{"sitemaps": sitemap_sections},
name="international.sitemaps.views.sitemap",
),
)
In order to limit/adjust the items shown in a sitemap based on the current request domain/current CountrySite. Inherit the InternationalSitemap
extension in your Sitemap class (this is available for models that use InternationalModel
), this wel make the country_code of the current request available inside the class methods. For example, here the blog post items shown in the sitemap are filtered by country code:
from international.sitemaps import InternationalSitemap
class BlogSitemap(InternationalSitemap):
changefreq = "weekly"
priority = 0.5
def items(self):
return Post.objects.by_country(self.country_code)
def lastmod(self, obj):
return obj.published_date
For models inheriting the InternationalModel class
For models using Vinaigrette translated fields - not InternationalModel
Extend model admin's with fields that use django-vinaigrette to add translations (using gettext
instead of adding more fields in the db). In order to use this mixin the translated fields must be registerred to Vinaigrette in the app's config like below. Specifically, the translated_fields
dictionary must be available in the AppConfig.
class TestappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'testapp'
translated_fields = {
'Certificate': ['description']
}
def ready(self):
for modelname in self.translated_fields.keys():
model = self.get_model(modelname)
# Register fields to translate
vinaigrette.register(model, self.translated_fields[modelname])
This will show the translated field indicator with all fields that can be translated in the translation files. It will also add the current translations to the bottom of the admin page (note: these are for reference and cannot be edited through the admin since they do not come from the database but the translation files)