
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.
| Couche | Composant | Rôle |
|---|---|---|
| Framework | Python, Django 6.0.4 | Modèles, vues, templates, administration |
| API | Django REST Framework, SimpleJWT | Points d’accès JSON, authentification par jeton |
| Base de données | PostgreSQL | Articles, catégories et données du site |
| Serveur applicatif | Gunicorn (3 workers) | Exécute le code Python, piloté par systemd |
| Serveur web | Apache | Relaie les requêtes vers Gunicorn |
| Reverse proxy | Nginx externe | Terminaison TLS, certificats Let’s Encrypt |
| Interface | Bootstrap 5.3.3, Bootstrap Icons | Mise en page, sans framework JavaScript |
| Configuration | python-decouple | Secrets et réglages lus depuis l’environnement |
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 :
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
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 :
- La modification est préparée et relue.
- Contrôle de syntaxe avant envoi : un fichier qui ne passe pas la vérification n’est pas copié.
- Copie du ou des fichiers concernés sur le serveur.
- Rechargement de Gunicorn par
SIGHUP. - 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.
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 active | Adresse 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"))
# ...
Les deux approches côte à côte
| Interface statique | Contenu dynamique | |
|---|---|---|
| Ce qui est traduit | Textes des templates et du code, segments d’URL | Titres et contenus d’articles, noms de catégories |
| Où se trouve la traduction | Fichiers .po et .mo dans locale/ | Colonnes dédiées dans PostgreSQL (titre_en, contenu_it…) |
| Mécanisme | gettext, natif dans Django | Propriétés Python écrites pour le site |
| Moment de la traduction | Pendant le développement | À la saisie du contenu |
| Traduction absente | Le texte source français s’affiche | Repli sur le champ français |
| Mise en ligne | Compilation, puis rechargement de Gunicorn | Immé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.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…


