
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…