F4HXN

django.jerko.fr

Publié le 15 septembre 2026 Par F4HXN

django

django.jerko.fr est un blog technique et un laboratoire personnel Ă©crit en Django. Ce n’est pas une dĂ©monstration : le site est en production, publie des articles et met Ă  disposition une trentaine d’outils destinĂ©s aux radioamateurs.

Cet article décrit son architecture, ses fonctionnalités et la façon dont il est mis à jour. La dernière partie détaille la gestion des trois langues du site (français, anglais, italien), qui repose sur deux mécanismes bien distincts.

La pile technique

Le site suit un schĂ©ma classique pour une application Python en production, avec une particularitĂ© : deux serveurs web se succèdent devant l’application.

CoucheComposantRĂ´le
FrameworkPython, Django 6.0.4Modèles, vues, templates, administration
APIDjango REST Framework, SimpleJWTPoints d’accès JSON, authentification par jeton
Base de donnéesPostgreSQLArticles, catégories et données du site
Serveur applicatifGunicorn (3 workers)Exécute le code Python, piloté par systemd
Serveur webApacheRelaie les requĂŞtes vers Gunicorn
Reverse proxyNginx externeTerminaison TLS, certificats Let’s Encrypt
InterfaceBootstrap 5.3.3, Bootstrap IconsMise en page, sans framework JavaScript
Configurationpython-decoupleSecrets et rĂ©glages lus depuis l’environnement
Chemin d’une requĂŞte : navigateur, Nginx, Apache, Gunicorn, Django, puis PostgreSQL et sources temps rĂ©el Navigateur du visiteur HTTPS Nginx, reverse proxy externe Terminaison TLS, certificats Let’s Encrypt HTTP Apache Relais vers l’application Gunicorn 3 workers, service systemd Django 6.0.4 Templates, Markdown, DRF et SimpleJWT Middlewares : langue, suivi des visites PostgreSQL Articles, catĂ©gories, donnĂ©es Sources temps rĂ©el ADS-B (SDR), DX cluster, WSPR, propagation
Chemin d’une requĂŞte jusqu’Ă  l’application, et les deux sources de donnĂ©es du site.

Le chiffrement s’arrĂŞte au Nginx externe, qui gère les certificats Let’s Encrypt. Apache reçoit ensuite la requĂŞte et la transmet Ă  Gunicorn, dont les trois workers exĂ©cutent le code Django en parallèle. Le rendu se fait cĂ´tĂ© serveur : templates Django, articles Markdown convertis en HTML, Bootstrap pour la prĂ©sentation. Aucun framework JavaScript de type React ou Vue n’est utilisĂ©.

Ce que propose le site

Blog

Articles rédigés en Markdown, classés par catégories, avec une recherche plein texte.

API REST

Points d’accès JSON construits avec Django REST Framework. L’accès authentifiĂ© passe par un jeton JWT dĂ©livrĂ© par SimpleJWT.

Administration

Interface d’administration Django personnalisĂ©e, servie Ă  une adresse propre au site plutĂ´t qu’Ă  l’habituel /admin/.

Tableau de bord temps réel

Trafic aérien ADS-B capté localement par un récepteur SDR, spots du DX cluster, réceptions WSPR, indices de propagation solaire et HF, état des services et statistiques de visites.

Widgets embarqués

Carte APRS en direct et horloge planétaire, développées pour le site.

Suivi des visites

Pas de Google Analytics : un middleware écrit pour le site compte les visites côté serveur.

Une trentaine d’outils radioamateur

Les calculateurs couvrent l’Ă©lectronique, les antennes, les modes de transmission et le calcul de positions. Quelques exemples :

  • DipĂ´le
  • Loi d’Ohm
  • Filtres RC
  • Circuits LC
  • Code Morse
  • RTTY / Baudot
  • CoordonnĂ©es GPS et locator Maidenhead
  • Distance Terre-Lune et bruit galactique (EME)

Déploiement et mises à jour

Le site n’utilise pas de chaĂ®ne CI/CD. Le dĂ©ploiement est manuel et volontairement limitĂ© : seuls les fichiers modifiĂ©s sont copiĂ©s sur le serveur, un par un.

Gunicorn tourne comme service systemd. Pour prendre en compte le nouveau code, le processus principal reçoit le signal SIGHUP : il dĂ©marre de nouveaux workers avec le code Ă  jour, puis arrĂŞte les anciens une fois leurs requĂŞtes en cours terminĂ©es. Le site reste disponible pendant toute l’opĂ©ration.

Exemple simplifiĂ© d’unitĂ© systemd

[Service]
ExecStart=/chemin/vers/venv/bin/gunicorn --workers 3 projet.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
sudo systemctl reload gunicorn
Ce rechargement Ă  chaud recharge bien le code Python tant que l’option preload_app de Gunicorn n’est pas activĂ©e. Avec cette option, un redĂ©marrage complet du service devient nĂ©cessaire.

Les modifications sont prĂ©parĂ©es avec l’assistant de dĂ©veloppement Claude Code, selon une procĂ©dure Ă  double vĂ©rification :

  1. La modification est préparée et relue.
  2. ContrĂ´le de syntaxe avant envoi : un fichier qui ne passe pas la vĂ©rification n’est pas copiĂ©.
  3. Copie du ou des fichiers concernés sur le serveur.
  4. Rechargement de Gunicorn par SIGHUP.
  5. Contrôle en direct : la page modifiée est vérifiée sur le site en production.

Le site détaille lui-même ce fonctionnement sur sa page « Comment ce site est mis à jour ».

Trois langues, deux mécanismes

Le français est la langue source ; l’anglais et l’italien sont les langues de traduction. Django sait traduire nativement les textes Ă©crits dans le code et les templates. En revanche, il ne traduit pas ce qui est stockĂ© en base de donnĂ©es. Le site traite donc deux problèmes diffĂ©rents, avec deux solutions diffĂ©rentes.

Deux niveaux de traduction : interface statique par gettext, contenu dynamique par champs par langue avec repli sur le français 1. Interface statique gettext, mĂ©canisme natif de Django 2. Contenu dynamique Champs par langue et repli sur le français Template {% trans « Outils » %} Catalogue locale/en/LC_MESSAGES/django.mo msgid « Outils » → msgstr « Tools » Affichage : Tools ChaĂ®ne absente du catalogue : le texte source français s’affiche Modèle Article (PostgreSQL) titre · titre_en · titre_it PropriĂ©tĂ© titre_tr get_language() renvoie en titre_en renseignĂ© affiche titre_en titre_en vide repli sur titre (français) Textes connus au moment du dĂ©veloppement Contenu saisi après la mise en ligne
Les deux niveaux de traduction, pour une page affichée en anglais.

Niveau 1 : les textes fixes de l’interface

Menus, boutons, libellĂ©s et messages sont Ă©crits en français directement dans les templates, entourĂ©s des balises {% trans %} ou {% blocktrans %}. C’est le système d’internationalisation standard de Django, basĂ© sur gettext.

{% load i18n %}
<a href="{% url 'circuit_lc' %}">{% trans "Circuit LC" %}</a>
{% blocktrans with nb=outils|length %}{{ nb }} outils disponibles{% endblocktrans %}

Des URL prĂ©fixĂ©es par la langue. Les routes sont dĂ©clarĂ©es dans i18n_patterns() avec prefix_default_language=True. Chaque page porte donc un prĂ©fixe, y compris en français : /fr/, /en/ ou /it/. Le français n’est pas traitĂ© comme une exception sans prĂ©fixe.

Le middleware LocaleMiddleware lit ce prĂ©fixe et active la langue correspondante pour toute la durĂ©e de la requĂŞte. Par dĂ©faut dans Django, une adresse sans prĂ©fixe est redirigĂ©e vers la version prĂ©fixĂ©e, la langue Ă©tant choisie d’après le cookie de langue, puis l’en-tĂŞte Accept-Language du navigateur, puis la langue par dĂ©faut.

settings.py (extrait)

LANGUAGE_CODE = "fr"
LANGUAGES = [
    ("fr", "Français"),
    ("en", "English"),
    ("it", "Italiano"),
]
LOCALE_PATHS = [BASE_DIR / "locale"]

MIDDLEWARE = [
    # ...
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    # ...
]

Des segments d’URL traduits. Le prĂ©fixe ne suffit pas : le site traduit aussi le chemin lui-mĂŞme. Chaque motif passĂ© Ă  path() est entourĂ© de gettext_lazy(), et sa traduction figure dans les catalogues comme n’importe quel autre texte.

urls.py (extrait)

from django.conf.urls.i18n import i18n_patterns
from django.urls import path
from django.utils.translation import gettext_lazy as _

urlpatterns = i18n_patterns(
    path(_("circuit-lc/"), views.circuit_lc, name="circuit_lc"),
    # ...
    prefix_default_language=True,
)
Langue activeAdresse produite pour la mĂŞme vue
Français/fr/circuit-lc/
Anglais/en/lc-circuit/
Italien/it/circuito-lc/

La version « lazy » est indispensable : le fichier urls.py est chargĂ© une seule fois au dĂ©marrage, avant qu’une langue soit active. La traduction n’est calculĂ©e qu’au moment oĂą Django rĂ©sout ou construit une adresse. La balise {% url %} et la fonction reverse() produisent ainsi automatiquement la bonne forme selon la langue de la page.

Les catalogues .po et .mo. Les chaĂ®nes sont extraites du code, traduites dans un fichier .po par langue, puis compilĂ©es en .mo, le format binaire lu Ă  l’exĂ©cution. L’ensemble reprĂ©sente environ 900 chaĂ®nes traduites.

locale/en/LC_MESSAGES/django.po (extrait)

msgid "Outils radioamateur"
msgstr "Amateur radio tools"

msgid "circuit-lc/"
msgstr "lc-circuit/"
python manage.py makemessages -l en -l it
python manage.py compilemessages

Chaque worker Gunicorn garde les catalogues en mémoire. Après une compilation, le rechargement par SIGHUP décrit plus haut est donc aussi ce qui met les nouvelles traductions en ligne.

Niveau 2 : le contenu stocké en base

Les articles et les catégories sont saisis après la mise en ligne. Ils ne passent pas par les catalogues gettext, qui ne connaissent que les textes présents dans le code. Chaque modèle possède donc un champ par langue : titre, titre_en, titre_it, et de même contenu, contenu_en, contenu_it. Les catégories suivent le même principe avec nom.

Une propriĂ©tĂ© Python choisit le bon champ selon la langue active, et revient au français lorsque la traduction n’a pas encore Ă©tĂ© saisie.

models.py (exemple simplifié)

from django.db import models
from django.utils.translation import get_language


class Article(models.Model):
    titre = models.CharField(max_length=200)
    titre_en = models.CharField(max_length=200, blank=True)
    titre_it = models.CharField(max_length=200, blank=True)
    contenu = models.TextField()
    contenu_en = models.TextField(blank=True)
    contenu_it = models.TextField(blank=True)

    def _champ_traduit(self, base):
        langue = (get_language() or "fr")[:2]
        if langue != "fr":
            valeur = getattr(self, f"{base}_{langue}", "")
            if valeur:
                return valeur
        return getattr(self, base)  # repli sur le français

    @property
    def titre_tr(self):
        return self._champ_traduit("titre")

    @property
    def contenu_tr(self):
        return self._champ_traduit("contenu")
<h1>{{ article.titre_tr }}</h1>

En pratique, un article publiĂ© en français reste consultable depuis les versions anglaise et italienne, en français, jusqu’Ă  ce que sa traduction soit saisie. Ce choix a une contrepartie : ajouter une quatrième langue impose de nouveaux champs et une migration de la base. Des bibliothèques comme django-modeltranslation automatisent ce schĂ©ma ; ici, il reste Ă©crit Ă  la main et lisible directement dans le modèle.

Les articles qui renvoient vers un outil

Certains articles n’ont pas de contenu propre : ils pointent vers l’un des outils du site. Ce lien est enregistrĂ© en base sous sa forme française, par exemple /fr/circuit-lc/. AffichĂ© tel quel, il renverrait un visiteur anglophone vers la version française.

La propriĂ©tĂ© url_externe_tr corrige cela : elle identifie la vue Ă  partir du chemin français, puis reconstruit l’adresse avec reverse() dans la langue courante.

from django.urls import Resolver404, resolve, reverse
from django.utils import translation

    @property
    def url_externe_tr(self):
        if not self.url_externe:
            return ""
        try:
            with translation.override("fr"):
                match = resolve(self.url_externe)
        except Resolver404:
            return self.url_externe  # lien vers un autre site : inchangé
        return reverse(match.view_name, args=match.args, kwargs=match.kwargs)

La résolution se fait en français, puisque le chemin enregistré est en français ; la reconstruction se fait hors du bloc override, donc dans la langue de la page. Un lien vers /fr/circuit-lc/ devient /en/lc-circuit/ sur la version anglaise.

Le quiz, volontairement en français seulement

Le site propose un quiz de 173 questions techniques. Ce contenu n’existe qu’en français et n’est pas proposĂ© dans les autres langues. Un accès direct Ă  l’adresse anglaise ou italienne est redirigĂ© vers la version française grâce Ă  translate_url(), qui convertit une adresse d’une langue Ă  l’autre en tenant compte des segments traduits.

from django.shortcuts import redirect
from django.urls import translate_url
from django.utils.translation import get_language


def quiz(request):
    if get_language() != "fr":
        return redirect(translate_url(request.get_full_path(), "fr"))
    # ...
Traitement de la requĂŞte /en/lc-circuit/ en cinq Ă©tapes 1 RequĂŞte GET /en/lc-circuit/ Le prĂ©fixe /en/ porte la langue 2 LocaleMiddleware Lit le prĂ©fixe et active l’anglais pour toute la requĂŞte 3 RĂ©solution d’URL Le motif circuit-lc/ vaut lc-circuit/ en anglais : vue circuit_lc 4 Rendu {% trans %} lit le catalogue anglais, titre_tr lit titre_en ou le français 5 RĂ©ponse Page en anglais, liens {% url %} gĂ©nĂ©rĂ©s en /en/
Traitement d’une requĂŞte vers la version anglaise d’un outil.

Les deux approches cĂ´te Ă  cĂ´te

Interface statiqueContenu dynamique
Ce qui est traduitTextes des templates et du code, segments d’URLTitres et contenus d’articles, noms de catĂ©gories
Où se trouve la traductionFichiers .po et .mo dans locale/Colonnes dédiées dans PostgreSQL (titre_en, contenu_it…)
Mécanismegettext, natif dans DjangoPropriétés Python écrites pour le site
Moment de la traductionPendant le développementÀ la saisie du contenu
Traduction absenteLe texte source français s’afficheRepli sur le champ français
Mise en ligneCompilation, puis rechargement de GunicornImmĂ©diate, dès l’enregistrement

La distinction tient en une question : le texte existe-t-il au moment oĂą le code est Ă©crit ? Si oui, il passe par gettext et les catalogues. Sinon, il est stockĂ© en base avec une colonne par langue, et le code choisit la bonne au moment de l’affichage. Les deux cas particuliers, liens vers les outils et quiz rĂ©servĂ© au français, montrent que les deux niveaux doivent rester cohĂ©rents : un contenu traduit ne sert Ă  rien s’il pointe vers une adresse dans la mauvaise langue.

À lire également

jerko

jerko.fr le complémént de f4hxn.fr

jerko.fr — un des domaines du home lab jerko.fr est un des domaines que j'utilise pour héberger et publier les services de mon infrastructure personnelle. Il regroupe des projets radioamateurs, des outils de supervision, des API maison et diverses expérimentations autour de Linux, Proxmox et Docker. L'infrastructure derrière est un home lab construit progressivement : plusieurs nœuds Proxmox, des machines…