Google Translation 2025 – Commande i18n_gtranslate
Objectif : automatiser la traduction de ton corpus i18n Ideo-Lab (basé sur l’app translation) en utilisant l’API Google Cloud Translation v3.
- Sélection fine des textes à traduire par:
- app (
--app/SubProject.code) - view Django (
--view) - URL (
--url) - template root (
--templateavec closure extends/include)
- app (
- Gestion des langues cibles :
--targets en,de,es… - Respect du workflow i18n :
- ne traduit que les TranslationRecord non traduits (par défaut),
- option
--overwritepour forcer la retraduction, - versioning dans
TranslationRecordVersion, statut & needs_review.
- Optimisation API : batching par budget de caractères, cache des appels (source+lang → target).
SubProject, DjangoViewRef, HtmlTemplateRef2025, ViewTemplateMap, TranslationKey, TranslationRecord, TranslationRecordVersion.Étape 1 – Pré-requis Google Cloud & projet i18n
1. API Google Cloud Translation v3
- Activer l’API Cloud Translation API dans ton projet GCP.
- Créer un compte de service + clé JSON.
- Installer le client Python :
pip install google-cloud-translate
2. Variables d’environnement & settings Django
# .env (exemple)
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
export GOOGLE_CLOUD_LOCATION="global" # ou europe-west1, etc.
export GOOGLE_APPLICATION_CREDENTIALS="/opt/keys/gcp-translate.json"
# settings.py (fallback possibles)
GOOGLE_CLOUD_PROJECT = "your-gcp-project-id"
GOOGLE_CLOUD_LOCATION = "global"
GOOGLE_CREDENTIALS_FILE = "/opt/keys/gcp-translate.json"--project/--locationvia CLI- variables d’environnement (
GCP_PROJECT,GOOGLE_CLOUD_PROJECT, etc.) - settings Django (
GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION)
3. Vérifier les langues supportées
python manage.py i18n_gtranslate \
--project YOUR_GCP_PROJECT \
--location global \
--list-languagesÉtape 2 – Installation du management command
On installe google_translation_2025.py comme management command Django, sous le nom i18n_gtranslate.
1. Arborescence Django
your_project/
translation/ # app i18n
management/
__init__.py
commands/
__init__.py
i18n_gtranslate.py # <= renommer google_translation_2025.py2. Contenu de i18n_gtranslate.py
# -*- coding: utf-8 -*-
"""
i18n_gtranslate — Batch Google Cloud Translation
- Liste les langues supportées (--list-languages)
- Traduit en masse les TranslationRecord ciblés par app / view / url / template root
- API: google.cloud.translate_v3.TranslationServiceClient
"""
from __future__ import annotations
import itertools
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction, models
from django.urls import resolve, Resolver404
from django.template import engines
from django.conf import settings
from google.api_core.exceptions import GoogleAPICallError
from google.cloud import translate_v3 as translate # pip install google-cloud-translate
from google.oauth2 import service_account
from translation.models import (
SubProject,
DjangoViewRef,
HtmlTemplateRef2025,
ViewTemplateMap,
TranslationKey,
TranslationRecord,
TranslationRecordVersion,
)
# ... helpers (_scope_templates, _diagnose_scope, _ensure_targets_for_scope, _pick_records, etc.)
# ... GClient, _build_client, _chunks_by_budget, _translate_batch
# ... class Command(BaseCommand): add_arguments + handle()Étape 3 – Utilisation & scénarios types
1. Paramètres principaux
--app: code SubProject (ex:accounts).--view: dotted path complet ou court (si--appest fourni).--url: URL à résoudre en view.--template: template root (closure extends/include).--targets: langues cibles, exen,de,es.--status-in: filtre sur les statuts.--only-missing: ne traduit que les records vides/non traduits (défaut).--overwrite: écrase les traductions existantes.--dry-run: ne touche pas à la base.--trace: diagnostic détaillé.--create-missing-targets: crée les records cibles manquants.--commons: cible les keys de scope COMMON.
2. Scénarios CLI (exemples)
A. Lister les langues supportées
python manage.py i18n_gtranslate \
--project YOUR_GCP_PROJECT \
--location global \
--list-languagesB. Traduire une app complète (accounts → EN, DE)
python manage.py i18n_gtranslate \
--app accounts \
--targets en,de \
--project YOUR_GCP_PROJECT \
--location globalC. Par view, par URL, par template, overwrite, dry-run, cron…
Étape 4 – Diagnostic avancé & tuning
1. Diagnostic pivot TranslationKey (_diagnose_scope())
Avant de lancer la traduction, la commande passe par une phase de diagnostic qui sert à comprendre l’état de ton corpus i18n pour le périmètre demandé (--app, --view, --url, --template, --commons…).
- Resolution du périmètre → liste des templates concernés (via
ViewTemplateMap&HtmlTemplateRef2025). - Collecte des
TranslationKeyassociées à ces templates / vues. - Pour chaque langue cible :
- nombre de TranslationRecord existants,
- nombre de records manquants,
- liste détaillée des clés manquantes si
--traceest activé.
--dry-run et --trace pour voir exactement combien de clés / langues seront impactées avant d’appeler l’API Google.2. Création des cibles manquantes (--create-missing-targets)
Si tu actives --create-missing-targets, la commande appelle _ensure_targets_for_scope() pour créer tous les TranslationRecord cibles manquants :
- Un record par couple (
TranslationKey, langue cible) est créé si inexistant. - Texte vide, statut
PENDING,is_translated = False. - Aucun impact sur les originaux (FR) ni sur les traductions existantes.
3. Sélection des records à traduire (_pick_records())
Ensuite, _pick_records() applique les filtres pour ne garder que les records pertinents :
- Filtre sur le périmètre (app / view / url / template_root / commons).
- Langues cibles :
language__in=targets. - Garde-fous :
is_original = False,is_deleted = False,is_active = True. - Filtre
--status-in(ex.pending,draft). - Si
only-missing(défaut) : records vides ou non traduits. - Si
--overwrite: tous les records sélectionnés, même déjà traduits.
_pick_records() peut ne rien retourner. Utilise alors --status-in "" (pour accepter les statuts vides) ou --overwrite pour forcer.4. Batching & appel Google API
Une fois la liste de records prête, la commande :
- regroupe les records par (langue source, langue cible),
- les découpe en paquets via
_chunks_by_budget()(budget caractères contrôlé par--max-chars-per-call), - appelle l’API
TranslationServiceClientpour chaque batch, - utilise un cache mémoire pour éviter les appels dupliqués sur la même phrase.
5. Mise à jour des records & versioning
Pour chaque TranslationRecord mis à jour :
- le champ
textest rempli avec la traduction renvoyée par Google, is_translated = True,is_machine = True,engine = GOOGLE,needs_review = True,- si le statut était
PENDING, il passe enDRAFT, - un
TranslationRecordVersionest créé avecold_text/new_text+ méta (engine, mode, status…).
- Lancer un
--dry-run+--tracesur une seule app. - Activer
--create-missing-targetspour consolider les cibles. - Traduire en vrai (sans
--dry-run), puis valider / corriger dans le Django admin en utilisant les dashboards de couverture.
Étape 5 – Data Model i18n (translation.models)
Le cron i18n_gtranslate s’appuie sur le data model i18n suivant (extraits de translation/models.py) : SubProject, DjangoViewRef, HtmlTemplateRef2025, ViewTemplateMap, TranslationKey, TranslationRecord, TranslationRecordVersion.
1. Contexte fonctionnel (SubProject, vues, templates)
from django.db import models
class SubProject(models.Model):
code = models.SlugField(unique=True) # ex: "accounts"
label = models.CharField(max_length=120)
def __str__(self):
return self.code
class DjangoViewRef(models.Model):
subproject = models.ForeignKey(SubProject, on_delete=models.CASCADE)
dotted_path = models.CharField(max_length=191)
view_name = models.CharField(max_length=120)
view_type = models.CharField(max_length=20)
http_methods = models.CharField(max_length=60, blank=True)
is_login_required = models.BooleanField(default=False)
class Meta:
unique_together = (("subproject", "dotted_path"),)
class HtmlTemplateRef2025(models.Model):
subproject = models.ForeignKey(SubProject, on_delete=models.CASCADE)
template = models.CharField(max_length=191)
is_dynamic = models.BooleanField(default=False)
last_parsed_at = models.DateTimeField(null=True, blank=True)
last_replaced_at = models.DateTimeField(null=True, blank=True)
last_backup_path = models.CharField(max_length=300, blank=True, default="")
has_translation_keys = models.BooleanField(default=False)
last_replace_count = models.PositiveIntegerField(default=0)
path = models.CharField(max_length=192, db_index=True, null=True, blank=True)
class Meta:
unique_together = (("subproject", "template"), ("subproject", "path"))
def __str__(self):
return self.template
class ViewTemplateMap(models.Model):
view = models.ForeignKey(DjangoViewRef, on_delete=models.CASCADE, related_name="templates")
tpl = models.ForeignKey(HtmlTemplateRef2025, on_delete=models.CASCADE, related_name="views")
note = models.CharField(max_length=200, blank=True)
class Meta:
unique_together = (("view", "tpl"),)SubProject= sous-projet / app logique (accounts, toolbox, etc.).DjangoViewRef= référence stable des vues Django (dotted_path, type, login_required…).HtmlTemplateRef2025= inventaire des templates HTML + méta parsing / remplacement.ViewTemplateMap= graphe Vue ↔ Template (inclut les emails, modals, fragments…).
2. Noyau i18n (TranslationKey, TranslationRecord, Versioning)
from django.utils import timezone
from django.core.validators import RegexValidator
class TranslationKey(models.Model):
class Scope(models.TextChoices):
LOCAL = "LOCAL", "Local"
COMMON = "COMMON", "Common"
class PageKind(models.TextChoices):
MAIN = "main", "Fichier principal"
PARTIAL = "partial", "Include/fragment"
MODAL = "modal", "Modal"
AJAX = "ajax", "Ajax callback"
EMAIL = "email", "Email"
WIDGET = "widget", "Widget"
OTHER = "other", "Autre"
class Category(models.TextChoices):
LOGIN = "login", "Login"
THEMATIQUE = "thematique", "Thématique"
AGENDA = "agenda", "Agenda"
FORMATION = "formation", "Formation"
DATA_MODEL = "data_model", "Data Model"
PROBLEMATIQUE = "problematique", "Problématique"
AUDIT_SERVICES = "audit_services", "Audit & Services"
OTHER = "other", "Autre"
key = models.CharField(
max_length=128, unique=True, db_index=True,
validators=[RegexValidator(r"^[a-z0-9][a-z0-9._-]*$")],
help_text="Utilisée dans les templates: {{ translation.<key> }}",
)
subproject = models.ForeignKey(SubProject, on_delete=models.CASCADE, related_name="i18n_keys")
template = models.ForeignKey(HtmlTemplateRef2025, on_delete=models.SET_NULL, null=True, blank=True, related_name="i18n_keys")
view = models.ForeignKey(DjangoViewRef, on_delete=models.SET_NULL, null=True, blank=True, related_name="i18n_keys")
i18n_category = models.ForeignKey("I18nCategory", null=True, blank=True, on_delete=models.SET_NULL, related_name="trans_category_keys")
page_kind = models.CharField(max_length=16, choices=PageKind.choices, default=PageKind.OTHER)
category = models.CharField(max_length=32, choices=Category.choices, default=Category.OTHER)
scope = models.CharField(max_length=10, choices=Scope.choices, default=Scope.LOCAL, db_index=True)
occurrences_count = models.PositiveIntegerField(default=0)
source_hash = models.CharField(max_length=40, db_index=True, blank=True, default="")
is_adhoc = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_deleted = models.BooleanField(default=False)
extra = models.JSONField(default=dict, blank=True)
created_at = models.DateTimeField(default=timezone.now, db_index=True)
updated_at = models.DateTimeField(auto_now=True)
is_materialized = models.BooleanField(default=False)
materialized_at = models.DateTimeField(null=True, blank=True)
class Meta:
indexes = [
models.Index(fields=["subproject", "key"]),
models.Index(fields=["subproject", "source_hash"]),
models.Index(fields=["page_kind", "category"]),
]
def __str__(self):
return self.keyclass TranslationRecord(models.Model):
class Engine(models.TextChoices):
MANUAL = "manual", "Manuelle"
GOOGLE = "google", "Google"
AI = "ai", "IA"
DEEPL = "deepl", "DeepL"
OTHER = "other", "Autre"
class Mode(models.TextChoices):
PLAIN = "plain", "Texte simple"
HTML = "html", "Bloc HTML"
DROPDOWN = "dropdown", "Libellés de liste"
PARAM = "param", "Paramètres"
DATABASE = "database", "Données DB"
SORT = "sort", "Clés de tri"
OTHER = "other", "Autre"
class Status(models.TextChoices):
PENDING = "pending", "À traduire"
DRAFT = "draft", "Brouillon"
APPROVED = "approved", "Validée"
REJECTED = "rejected", "Refusée"
key_ref = models.ForeignKey(TranslationKey, on_delete=models.CASCADE, related_name="records")
language = models.CharField(max_length=8, db_index=True)
text = models.TextField(blank=True)
is_original = models.BooleanField(default=False)
is_translated = models.BooleanField(default=False)
engine = models.CharField(max_length=16, choices=Engine.choices, default=Engine.MANUAL)
mode = models.CharField(max_length=16, choices=Mode.choices, default=Mode.PLAIN)
status = models.CharField(max_length=16, choices=Status.choices, default=Status.PENDING)
is_active = models.BooleanField(default=True)
is_deleted = models.BooleanField(default=False)
needs_review = models.BooleanField(default=True)
is_machine = models.BooleanField(default=False)
is_post_edited = models.BooleanField(default=False)
previous_text = models.TextField(blank=True)
previous_original_text = models.TextField(blank=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = (("key_ref", "language"),)
indexes = [
models.Index(fields=["language", "status"]),
models.Index(fields=["is_original", "is_translated"]),
models.Index(fields=["is_active", "is_deleted"]),
]
class TranslationRecordVersion(models.Model):
record = models.ForeignKey(TranslationRecord, on_delete=models.CASCADE, related_name="versions")
language = models.CharField(max_length=8)
old_text = models.TextField(blank=True)
new_text = models.TextField(blank=True)
was_original = models.BooleanField(default=False)
engine = models.CharField(max_length=16, default="manual")
mode = models.CharField(max_length=16, default="plain")
status = models.CharField(max_length=16, default="pending")
changed_at = models.DateTimeField(default=timezone.now)
changed_by = models.CharField(max_length=120, blank=True)
note = models.CharField(max_length=250, blank=True)
class Meta:
ordering = ["-changed_at"]
indexes = [models.Index(fields=["language", "changed_at"])]- sélectionne des
TranslationRecordnon traduits / à mettre à jour, - envoie les textes à l’API Google,
- remplit
text, met à jour les flags (is_translated,is_machine,status,needs_review), - crée un
TranslationRecordVersionà chaque update.
Étape 6 – Django Admin & dashboards i18n
L’admin de l’app translation sert de “console de pilotage” pour le pipeline i18n + Google Translation 2025 :
- SubProject, DjangoViewRef, HtmlTemplateRef2025, ViewTemplateMap : structure du graphe vue/template.
- TranslationKey, TranslationRecord, TranslationRecordVersion : gestion fine des clés + traductions + historique.
- Proxies & dashboards (I18nAuditRun, TranslationDashboardProxy, etc.) pour la couverture.
1. Admin minimal recommandé (extrait)
from django.contrib import admin
from translation.models import (
SubProject,
DjangoViewRef,
HtmlTemplateRef2025,
ViewTemplateMap,
TranslationKey,
TranslationRecord,
TranslationRecordVersion,
I18nAuditRun,
I18nAuditItem,
TranslationDashboardProxy,
TranslationMissingProxy,
HtmlTemplateDashboardProxy,
)
@admin.register(SubProject)
class SubProjectAdmin(admin.ModelAdmin):
list_display = ("code", "label")
search_fields = ("code", "label")
@admin.register(DjangoViewRef)
class DjangoViewRefAdmin(admin.ModelAdmin):
list_display = ("subproject", "view_name", "dotted_path", "view_type", "is_login_required")
list_filter = ("subproject", "view_type", "is_login_required")
search_fields = ("dotted_path", "view_name")
@admin.register(HtmlTemplateRef2025)
class HtmlTemplateRef2025Admin(admin.ModelAdmin):
list_display = ("subproject", "template", "path", "has_translation_keys", "last_parsed_at")
list_filter = ("subproject", "has_translation_keys")
search_fields = ("template", "path")
@admin.register(ViewTemplateMap)
class ViewTemplateMapAdmin(admin.ModelAdmin):
list_display = ("view", "tpl", "note")
list_filter = ("view__subproject",)
search_fields = ("view__dotted_path", "tpl__template")@admin.register(TranslationKey)
class TranslationKeyAdmin(admin.ModelAdmin):
list_display = ("key", "subproject", "scope", "page_kind", "category",
"occurrences_count", "is_active", "is_deleted")
list_filter = ("subproject", "scope", "page_kind", "category", "is_active", "is_deleted")
search_fields = ("key", "template__template", "view__dotted_path")
autocomplete_fields = ("subproject", "template", "view", "i18n_category")
@admin.register(TranslationRecord)
class TranslationRecordAdmin(admin.ModelAdmin):
list_display = ("key_ref", "language", "is_original", "is_translated",
"engine", "status", "needs_review", "is_machine")
list_filter = ("language", "is_original", "is_translated", "engine", "status",
"needs_review", "is_machine", "is_active", "is_deleted")
search_fields = ("key_ref__key", "text")
autocomplete_fields = ("key_ref",)
@admin.register(TranslationRecordVersion)
class TranslationRecordVersionAdmin(admin.ModelAdmin):
list_display = ("record", "language", "changed_at", "engine", "status", "was_original")
list_filter = ("language", "engine", "status", "was_original")
search_fields = ("record__key_ref__key", "old_text", "new_text")
date_hierarchy = "changed_at"2. Dashboards & diagnostics (proxies)
@admin.register(TranslationDashboardProxy)
class TranslationDashboardAdmin(admin.ModelAdmin):
list_display = ("started_at", "scope", "app", "view", "template_root",
"language", "coverage_pct", "total_keys", "total_missing")
list_filter = ("scope", "language")
date_hierarchy = "started_at"
search_fields = ("app", "view", "template_root")
@admin.register(TranslationMissingProxy)
class TranslationMissingProxyAdmin(admin.ModelAdmin):
list_display = ("key", "subproject", "scope", "category", "page_kind", "occurrences_count")
list_filter = ("subproject", "scope", "category", "page_kind")
search_fields = ("key",)
@admin.register(HtmlTemplateDashboardProxy)
class HtmlTemplateDashboardProxyAdmin(admin.ModelAdmin):
list_display = ("subproject", "template", "path", "has_translation_keys",
"last_parsed_at", "last_replaced_at", "last_replace_count")
list_filter = ("subproject", "has_translation_keys")- Combiner ce dashboard avec les runs d’audit i18n existants (I18nAuditRun/I18nAuditItem).
- Utiliser
TranslationMissingProxypour piloter les “gros trous” avant d’appeler Google. - Vérifier la progression de la couverture par langue via
TranslationDashboardProxy.
