Project Oxygen & Ideo-LabIDEO LAB Dashboard 2026

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 (--template avec closure extends/include)
  • Gestion des langues cibles : --targets en,de,es
  • Respect du workflow i18n :
    • ne traduit que les TranslationRecord non traduits (par défaut),
    • option --overwrite pour forcer la retraduction,
    • versioning dans TranslationRecordVersion, statut & needs_review.
  • Optimisation API : batching par budget de caractères, cache des appels (source+lang → target).
Modèle i18n : la commande ne touche pas aux fichiers .po. Elle travaille exclusivement avec tes modèles 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
    pip install google-cloud-translate

2. Variables d’environnement & settings Django

settings / env
# .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"
Ordre de priorité :
  1. --project / --location via CLI
  2. variables d’environnement (GCP_PROJECT, GOOGLE_CLOUD_PROJECT, etc.)
  3. settings Django (GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION)

3. Vérifier les langues supportées

Terminal
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

Arborescence projet
your_project/
                translation/                       # app i18n
                management/
                __init__.py
                commands/
                __init__.py
                i18n_gtranslate.py           # <= renommer google_translation_2025.py

2. Contenu de i18n_gtranslate.py

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()
Pour la lisibilité, seuls les blocs clés sont montrés. Le fichier réel doit contenir l’intégralité du script tel qu’il a été fourni.

Étape 3 – Utilisation & scénarios types

1. Paramètres principaux

  • --app : code SubProject (ex: accounts).
  • --view : dotted path complet ou court (si --app est fourni).
  • --url : URL à résoudre en view.
  • --template : template root (closure extends/include).
  • --targets : langues cibles, ex en,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

Terminal
python manage.py i18n_gtranslate \
                --project YOUR_GCP_PROJECT \
                --location global \
                --list-languages

B. Traduire une app complète (accounts → EN, DE)

Terminal
python manage.py i18n_gtranslate \
                --app accounts \
                --targets en,de \
                --project YOUR_GCP_PROJECT \
                --location global

C. 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 TranslationKey associé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 --trace est activé.
Tip : lance d’abord la commande avec --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.
Cas classique : si tes statuts sont vides ou incohérents, _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 TranslationServiceClient pour 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 text est 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 en DRAFT,
  • un TranslationRecordVersion est créé avec old_text / new_text + méta (engine, mode, status…).
Workflow conseillé :
  1. Lancer un --dry-run + --trace sur une seule app.
  2. Activer --create-missing-targets pour consolider les cibles.
  3. 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)

translation/models.py (contexte)
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"),)
Rôle :
  • 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)

translation/models.py (i18n core)
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.key
TranslationRecord + TranslationRecordVersion
class 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"])]
Ce que fait le cron avec ce modèle :
  • sélectionne des TranslationRecord non 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)

translation/admin.py (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")
TranslationKey / Record admin
@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)

Dashboards i18n
@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")
Idée d’exploitation :
  • Combiner ce dashboard avec les runs d’audit i18n existants (I18nAuditRun/I18nAuditItem).
  • Utiliser TranslationMissingProxy pour piloter les “gros trous” avant d’appeler Google.
  • Vérifier la progression de la couverture par langue via TranslationDashboardProxy.