django.jerko.fr

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…

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *