Reputation: 121
I have a Django app that loads a list of countries and states from a json file and translate them using a custom translaction dictonary little tool I created for this purpose. I don't use gettext for this specific task because gettext doesn't work with data coming from databases or files.
First the file that handles the json file
from django.utils.translation import gettext_lazy as _, get_language
import importlib
current_lng = get_language()
# importing the appropriate translation file based on current_lng
imp = importlib.import_module("countries.translations.countries_%s" % current_lng)
import json
def readJson(filename):
with open(filename, 'r', encoding="utf8") as fp:
return json.load(fp)
def get_country():
filepath = 'myproj/static/data/countries_states_cities.json'
all_data = readJson(filepath)
all_countries = [('----', _("--- Select a Country ---"))]
for x in all_data:
y = (x['name'], imp.t_countries(x['name']))
all_countries.append(y)
return all_countries
# the files continues with other function but they are not relevant
arr_country = get_country()
I use countries_hangler.py in forms.py
from django import forms
from .models import Address
from django.conf import settings
from .countries_handler import arr_country
from django.utils.translation import gettext_lazy as _
class AddressForm(forms.ModelForm):
# data = []
#def __init__(self, data):
# self.data = data
country = forms.ChoiceField(
choices = arr_country,
required = False,
label=_('Company Country Location'),
widget=forms.Select(attrs={'class':'form-control', 'id': 'id_country'}),
)
def get_state_by_country(self, country):
return return_state_by_country(country)
def get_city_by_state(self, state):
return return_city_by_state(state)
class Meta:
model = Address
fields = ['country']
In views.py I have these lines
from django.shortcuts import render
from django.http import HttpResponseRedirect, JsonResponse
from .forms import AddressForm
import json
from django.utils import translation
from django.utils.translation import gettext_lazy as _, get_language
def load_form(request):
form = AddressForm
# the lang variable is only passed for testing
return render(request, 'countries/country_form.html', {'form':form, 'lang':get_language()})
settings.py is properly configured, here's the relevant parts
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# ...
LANGUAGE_CODE = 'en'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LANGUAGES = [
('en', 'English'),
('fr', 'French'),
]
Also my urls.py is defined correctly
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.i18n import i18n_patterns
urlpatterns = i18n_patterns(
path('admin/', admin.site.urls),
path('countries/', include('countries.urls')),
prefix_default_language=False
)
Now when I change the language like that:
http://127.0.0.1:8000/fr/countries/
the get_language function in countries_handler.py still returns 'en' instead of 'fr'. The result is that the translation file for French is never loaded:
# countries/translations/countries_fr.py
# here's my little custom translation tool
def t_countries(c):
country_dict = {
"Albania" : "Albanie",
"Algeria" : "Algérie",
"Antigua and Barbuda" : "Antigua-et-Barbuda",
"Argentina" : "Argentine",
"Armenia" : "Arménie",
# ...
}
if c in country_dict:
return country_dict[c]
return c
By the way if I put the following lines in forms.py or countries_handler.py:
from django.utils import translation
and then
translation.activate('fr')
Then the language change would be very well detected.
Now I actually know the cause of the problem, the only issue that I can't get a solution. THE CAUSE OF THE PROBLEM IS:
In forms.py (throught languages_handler.py) get_language() doesn't detect the language changed from the url, because forms.py builds the form when the development server is being started and not after the url is requested.
The result is that get_language only returns the correct language in views.py and not in forms.py. Sure I tried to load the form data in the view instead than in the form in the following way
# views.py
from .countries_handler import arr_country
# .....
def load_form(request):
form = AddressForm(arr_country)
return render(request, 'countries/country_form.html', {'form':form, 'lang':get_language()})
and then in the form:
class AddressForm(forms.ModelForm):
data = []
def __init__(self, data):
self.data = data
country = forms.ChoiceField(
choices = data,
required = False,
label=_('Company Country Location'),
widget=forms.Select(attrs={'class':'form-control', 'id': 'id_country'}),
)
But this doesn't work. In this case the form is not even displayed!!!
Then, what I have to do to solve this problem? Is there a way to detect the user's current language from the url or form a cookie and then, send the data to the form and build it dynamically at runtime?
I need that the list of countries loads properly translated when I change the language in the url.
Upvotes: 0
Views: 324
Reputation: 121
I finally found a solution myself and I will share it. First of all notice that:
Forms.py and countries_handler.py are ran when the server starts, not when a http request occurs. Therefore, any change coming from the browser, like the user change the language in the url, must be handled in views.py, in the exact place where the route in urls.py hits it. Here the correct views.py:
# ...
from .countries_handler import get_country, return_state_by_country
from django.utils.translation import gettext_lazy as _, get_language
# ...
def load_form(request):
# load_form is called by urls.py, therefore it respond to a change made in the url, at run time.
# get_language must be called here to get a correct result.
arr_country = get_country(get_language())
form = AddressForm(arr_country)
return render(request, 'countries/country_form.html', {'form':form, 'lang':get_language()})
Here, get_country from countries_handler.py is called. get_language provides to it the current selected language correctly.
Here's the forms.py:
class AddressForm(forms.ModelForm):
""" Using an empty list as a default argument is a common error.
It may lead to unwanted behavior. The correct way to do it is to
initialize the list to None """
def __init__(self, data=None, *args, **kwargs):
super(AddressForm, self).__init__(*args, **kwargs)
if data is not None:
self.fields['country'].choices = data
country = forms.ChoiceField(
choices = (),
required = False,
label=_('Company Country Location'),
widget=forms.Select(attrs={'class':'form-control', 'id': 'id_country'}),
)
def get_state_by_country(self, country):
return return_state_by_country(country)
def get_city_by_state(self, state):
return return_city_by_state(state)
class Meta:
model = Address
fields = ['country']
Notice how I fill the choices fields from the constructor directly. data is declared as optional because when I use the same class to call return_state_by_country(country), for example, I don't have any data to pass.
That is. I don't post all the files of the project to avoid verbosity. But the essential ones are in this answer. Where I learnt how to pass data dynamically to a form constructor at run time. The rest remains unchanged.
Upvotes: 1