ionecum
ionecum

Reputation: 121

Problem detecting a language outside views.py in Django

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

Answers (1)

ionecum
ionecum

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

Related Questions