========================================== Internationalisation (django-parler) ========================================== Le framework intègre **django-parler** pour la traduction des modèles. Les champs traduisibles sont stockés dans des tables ``*Translation`` séparées, ce qui permet un support multi-langue sans modifier le schéma principal. Configuration ============= Module : ``updo.settings`` .. code-block:: python INSTALLED_APPS = [ ... 'parler', ... ] LANGUAGES = [ ('en', 'English'), ('fr', 'Français'), ('ar', 'العربية'), ] PARLER_LANGUAGES = { None: ( {'code': 'en'}, {'code': 'fr'}, {'code': 'ar'}, ), 'default': { 'fallbacks': ['en'], 'hide_untranslated': False, }, } - ``fallbacks`` : si une traduction n'existe pas dans la langue active, django-parler utilise l'anglais comme fallback. - ``hide_untranslated`` : ``False`` signifie que les objets sans traduction dans la langue courante restent visibles (avec le contenu fallback). Modèles de base traduisibles ============================= Module : ``src.module.tenant.infrastructure.base_models`` .. module:: src.module.tenant.infrastructure.base_models :synopsis: Modèles abstraits partagés et tenant-specific, avec variantes traduisibles .. class:: TranslatableSharedModel(TranslatableModel) Modèle abstrait pour les entités traduisibles dans le **schéma public**. Combine ``TranslatableModel`` de django-parler avec ``TenantMeta.tenant_type = "shared"`` pour le routage du database router. Utilisé par les données de référence (Country, Currency, etc.) et les modèles globaux (Role, Department du Tenant Context, Policy). .. code-block:: python from parler.models import TranslatedFields from src.module.tenant.infrastructure.base_models import TranslatableSharedModel class Country(TranslatableSharedModel, ERPBaseModel): code = models.CharField(max_length=2, unique=True) translations = TranslatedFields( name=models.CharField(max_length=100), ) .. class:: TranslatableTenantSpecificModel(TranslatableModel) Modèle abstrait pour les entités traduisibles dans le **schéma tenant**. Combine ``TranslatableModel`` avec ``TenantMeta.tenant_type = "tenant_specific"`` et le même garde-fou ``save()`` que ``TenantSpecificModel`` (auto-population de ``tenant_id``, vérification du contexte tenant). .. code-block:: python from parler.models import TranslatedFields from src.module.tenant.infrastructure.base_models import TranslatableTenantSpecificModel class Product(TranslatableTenantSpecificModel, ERPBaseModel): sku = models.CharField(max_length=50, unique=True) translations = TranslatedFields( name=models.CharField(max_length=255), description=models.TextField(blank=True), ) Modèles traduits — Données de référence ======================================== Ces modèles héritent de ``TranslatableSharedModel`` et stockent leurs champs traduisibles dans des tables ``*Translation`` distinctes. .. list-table:: :header-rows: 1 :widths: 25 25 50 * - Modèle - Champs traduits - Table de traduction * - ``Country`` - ``name`` - ``ref_countries_translation`` * - ``Currency`` - ``name`` - ``ref_currencies_translation`` * - ``UnitOfMeasureCategory`` - ``name`` - ``ref_uom_categories_translation`` * - ``UnitOfMeasure`` - ``name`` - ``ref_units_of_measure_translation`` * - ``Department`` (ref) - ``name`` - ``ref_departments_translation`` Modèles traduits — Tenant Context ================================== .. list-table:: :header-rows: 1 :widths: 25 25 50 * - Modèle - Champs traduits - Table de traduction * - ``Role`` - ``display_name``, ``description`` - ``mt_tenant_role_translation`` * - ``Department`` (tenant) - ``display_name``, ``description`` - ``mt_department_translation`` .. note:: ``Role`` et ``Department`` conservent un champ ``name`` non traduit comme identifiant programmatique (utilisé dans les JSON de rôles, les conditions ABAC, etc.). Le champ ``display_name`` est la version traduisible destinée à l'affichage. Modèles traduits — Authorization Context ========================================== .. list-table:: :header-rows: 1 :widths: 25 25 50 * - Modèle - Champs traduits - Table de traduction * - ``Policy`` - ``display_name``, ``description`` - ``mt_policy_translation`` * - ``AttributeDefinition`` - ``display_name``, ``description`` - ``mt_attribute_definition_translation`` .. note:: ``Policy.name`` et ``AttributeDefinition.name`` restent des champs non traduits. Ils servent d'identifiant technique (logs, évaluation ABAC). Les champs ``display_name`` et ``description`` sont traduits pour l'UI. Utilisation dans le code ======================== Créer un objet avec traduction ------------------------------- .. code-block:: python from src.share_kernel.models import Country country = Country(code="FR", code_alpha3="FRA") country.set_current_language("en") country.name = "France" country.set_current_language("fr") country.name = "France" country.set_current_language("ar") country.name = "فرنسا" country.save() Ou avec ``create_translation()`` : .. code-block:: python country = Country.objects.create(code="MA", code_alpha3="MAR") country.create_translation("en", name="Morocco") country.create_translation("fr", name="Maroc") country.create_translation("ar", name="المغرب") Lire une traduction -------------------- .. code-block:: python from django.utils.translation import activate activate("fr") country = Country.objects.language("fr").get(code="MA") print(country.name) # "Maroc" # Avec fallback sécurisé name = country.safe_translation_getter("name", any_language=True) Requêtes filtrées par langue ------------------------------ .. code-block:: python # Toutes les devises dont le nom contient "dollar" en anglais currencies = Currency.objects.language("en").filter( translations__name__icontains="dollar", ) # Toutes les devises avec traductions pré-chargées currencies = Currency.objects.prefetch_related("translations").all() Sérialisation DRF ------------------ Pour exposer les traductions via l'API REST, django-parler fournit ``TranslatedModelSerializer`` : .. code-block:: python from parler_rest.serializers import TranslatableModelSerializer, TranslatedFieldsField class CountrySerializer(TranslatableModelSerializer): translations = TranslatedFieldsField(shared_model=Country) class Meta: model = Country fields = ["id", "code", "code_alpha3", "translations", "is_active"] Cela produit un JSON de la forme : .. code-block:: json { "id": "...", "code": "MA", "translations": { "en": {"name": "Morocco"}, "fr": {"name": "Maroc"}, "ar": {"name": "المغرب"} } } Architecture des tables ======================== Chaque modèle traduisible crée une table ``*Translation`` qui contient : - ``id`` : clé primaire auto-incrémentée - ``language_code`` : code de langue (``en``, ``fr``, ``ar``) - ``master_id`` : FK vers le modèle parent - Les champs traduits (``name``, ``display_name``, ``description``, etc.) - Contrainte unique : ``(language_code, master_id)`` .. code-block:: text ref_countries ref_countries_translation ┌──────────┐ ┌──────────────────────┐ │ id (PK) │◄──────────────│ master_id (FK) │ │ code │ │ language_code │ │ ... │ │ name │ └──────────┘ └──────────────────────┘ Cette architecture permet d'ajouter de nouvelles langues sans modifier la table principale.