Aller au contenu

Microcopy

Status: Implemented Last reviewed: 2026-05-01 Sources of truth: scripts/audit_i18n_source.py (curated EN→FR dictionary — the binding canonical translations); templates/skeletons/skeleton_{form,list,detail}.html (reference button verbs, empty states, audit-row copy); templates/components/messages.html (banner / toast surface — receives the strings written here); apps/imports/views.py (canonical module-level message constants); apps/register/templates/register/assembly_confirm_delete.html (canonical destructive-confirmation copy); apps/sync/views.py:bulk_sync_practices (canonical bulk summary copy).

Scope

Tone and word choice for every short string the user reads: button verbs, link labels, table column headers, form labels and help text, empty-state copy, error messages, success / warning / info messages, confirmation-dialog copy, page titles, breadcrumbs, tooltip titles, placeholder text. The goal is that a contributor (or Claude) writes the exact word without grepping four pages for precedent.

This file is the content layer that sits on top of guidelines/i18n/translation-rules.md. The mechanics — which function (_() vs gettext_lazy vs {% trans %}), the nl-be trick, the _(f"…") antipattern, the percent-format placeholder convention — live there. This file only picks the words.

Out of scope (cross-refs)

  • i18n mechanicsguidelines/i18n/translation-rules.md. Wrapping, lazy vs eager, placeholder syntax, the .po workflow.
  • Status badge palette (which colour for Actif / Inactif / En attente) — guidelines/ui/badges.md. Microcopy decides the word; badges decide the colour.
  • Channel choice (toast vs banner vs hx-confirm vs _confirm_delete.html vs modal) — guidelines/ux/feedback-and-confirmation.md. Microcopy fills the channels; that file picks them.
  • Form layout, validation rendering, char-counter wiringguidelines/ui/forms.md. This file gives the text of the validation message; that file gives the placement.
  • List table structure, action column, two empty statesguidelines/ui/lists.md. This file gives the empty-state sentence; that file gives the layout.

Hard rules

  1. Button verbs are imperatives drawn from the canonical glossary. Use Enregistrer / Annuler / Supprimer / Modifier / Ajouter / Retour / Voir / Rechercher / Filtrer / Synchroniser / Importer / Exporter / Téléverser / Envoyer / Approuver / Rejeter / Confirmer. Do not introduce synonyms (no Sauvegarder, Abandonner, Effacer, Éditer, Charger, Soumettre). The full glossary is in Canonical glossary below — it is the binding list.
  2. Address the user as vous. Sentence case for everything except proper nouns and brand names. Headings, button labels, column headers, choice labels: only the first word and proper nouns are capitalised. Tableau de bord, not Tableau De Bord. Ajouter un cabinet, not Ajouter Un Cabinet. Brand / module names that are proper nouns stay capitalised: Doctolib, Pennylane, Logosw, Helios, CCAM, RPPS, Sites Web (the module's UI name).
  3. Error messages name the field and the constraint, never blame the user. Right: Le SIRET doit comporter 14 chiffres. Wrong: Vous avez mal saisi le SIRET. Imperative tone is fine for the fix (Corriger le format du fichier.); accusatory tone (Vous avez oublié X) is not.
  4. Empty-state copy is two parts: the absence and the next step. Aucun X. Modifier les filtres ou ajouter un X. — never just Aucun résultat. on a page where the user can act. The "filtered" empty state and the "no records yet" empty state read differently and live in guidelines/ui/lists.md; the words for both follow this rule.
  5. Destructive confirmations name the verb and the target, and explain the consequence. Right: Supprimer l'assemblée du 10/05/2026 ? followed by an irreversibility alert. Wrong: Êtes-vous sûr ? alone, or with no consequence statement. hx-confirm (one short sentence ending in ?) is the only place a bare confirm-style line is acceptable, and even there it must name the target type (Supprimer cet horaire ?, not Supprimer ?). Full-page confirm pages always carry the consequence — see guidelines/ux/feedback-and-confirmation.md.

Canonical glossary

Bind these EN→FR pairs in code review. The same dictionary lives in scripts/audit_i18n_source.py:load_curated_dictionary and is the audit's "definitely English" lookup — keeping the two in sync means a drift here is caught on the next audit_i18n_source run.

Action / concept Canonical FR Do not use
Save Enregistrer Sauvegarder, Soumettre
Cancel Annuler Abandonner, Quitter
Delete Supprimer Effacer, Détruire, Retirer (unless un-link, see below)
Edit Modifier Éditer, Changer (unless mass-action), Mettre à jour (rare)
Add (new record) Ajouter Créer (use only when the verb is "create from scratch"), Nouveau (adjective only)
Submit (form) Envoyer Soumettre, Valider (reserve for approval flows)
Login / Logout Connexion / Déconnexion Se connecter / Se déconnecter as bare buttons
Search Rechercher Chercher (verb only), Recherche (noun only)
Filter Filtrer Trier (= sort), Affiner
Sort Trier Ordonner
Export Exporter Télécharger (= download a file the server already has — keep separate)
Import Importer Charger, Téléverser (= upload one file, see below)
Upload Téléverser Importer (importer = bulk-load into the model; téléverser = put a file on the server)
Sync Synchroniser Mettre à jour
Approve / Reject Approuver / Rejeter Valider / Refuser (reserve Valider for finalising a state machine, Refuser for hard refusals)
Confirm Confirmer Valider
Back Retour Revenir, Retour à la liste (only when the target page is genuinely the list — be specific)
View / Open Voir (table action) / Ouvrir (modal trigger) Consulter (formal — only in tooltips)
Active / Inactive Actif / Inactif Activé / Désactivé (those are event states, not steady states)
Pending En attente À traiter (use for queues), En cours (= currently running)
Completed / Failed / Skipped Terminé / Échoué / Ignoré
Draft / Published Brouillon / Publié
Settings Paramètres Configuration (reserve for technical config), Réglages
Dashboard Tableau de bord Tableau, Accueil (Accueil = home page)
Profile Profil Compte (only for the account-management page itself)

When introducing a new verb that's not in the table, check the .po catalogue first (grep -i 'msgid "Sauvegard' locale/fr/LC_MESSAGES/django.po etc.) and reuse the existing canonical form. If the action is genuinely new, add it to both the table above and scripts/audit_i18n_source.py.

Tone

  • Vouvoiement, never tutoiement. Veuillez vous connecter, not Connecte-toi. Even internal tools and admin pages stay vous-form — the project has no separate "internal-team-only" tone.
  • Statements over questions, except in confirmations. Le fichier est trop volumineux (max 150 MB). is a statement, not Avez-vous un fichier trop volumineux ?. The only place questions are appropriate is destructive confirmation (Supprimer cette page ?).
  • Veuillez … is acceptable for client-side validation messages and email notifications because the system is asking the user to do something. Avoid it elsewhere — for in-app status copy, the imperative or descriptive form is shorter (Sélectionner un cabinet vs Veuillez sélectionner un cabinet).
  • No exclamation marks. Even success messages are flat: Patient enregistré., not Patient enregistré !. The one exception is the canonical async-launch toast Import lancé avec succès ! Vous pouvez suivre sa progression… — a documented historical exception, kept because the string is shared by four import views via IMPORT_STARTED_MSG.
  • No emoji in user-facing copy. Bootstrap Icons (<i class="bi bi-…">) carry the visual signal; the text stays plain.

Sentence shapes by surface

One imperative verb, optionally + a short noun phrase. No trailing period.

{% trans "Enregistrer" %}                         {# preferred — single verb #}
{% trans "Ajouter un cabinet" %}                  {# verb + scoped target #}
{% trans "Voir tout" %}                           {# canonical for "see all" links #}
{% trans "Retour" %} / {% trans "Retour à la liste" %}

Never {% trans "Cliquer pour ajouter" %} (redundant — buttons are always clicked) or {% trans "Page d'ajout" %} on a button (link label, not a button).

Headings

Sentence case, no trailing punctuation, French verb-headed where the page is action-oriented (Supprimer un patient, Ajouter un dentiste); noun-headed where the page is informational (Patients, Cabinets, Tableau de bord).

Column headers (list pages)

Noun phrases, sentence case, no trailing punctuation, no verb form. Pick one grammatical form per list and stick to it. Date d'envoi, not Envoyé le. Nom du dentiste, not Le dentiste s'appelle. The {% table_header %} tag (per guidelines/ui/search-sort-filter.md) wraps the label — the string still follows this rule.

Form labels and help text

Labels are noun phrases ending in nothing (no colon — colons live in detail-page key/value <p>s, not in form labels). Help text is one sentence, ends in a period, written as a description not as instructions:

forms.CharField(
    label=_("SIRET"),
    help_text=_("14 chiffres, sans espace."),
)

Not Veuillez entrer votre SIRET en 14 chiffres sans espace. (instructional + accusatory).

Placeholders

Rechercher… (or Rechercher une page…) is the canonical search placeholder — three trailing dots, French ellipsis style. Other placeholders should not duplicate the label (label = Email, placeholder ≠ Votre email — leave the placeholder empty or use exemple@cabinet.fr).

Status / choice labels

The canonical four-state status palette is Actif / Inactif / En attente / (neutral). Choice labels are sentence case (En attente, not EN ATTENTE) and the displayed string is _()-wrapped while the code identifier stays English (STATUS_PENDING = 'pending').

Error-message tone

The pattern: what's wrong, then how to fix. Field name + constraint → optional one-clause fix.

raise forms.ValidationError(_("Le SIRET doit comporter 14 chiffres."))
raise forms.ValidationError(_("Le fichier doit être au format texte (.txt)."))
raise forms.ValidationError(_("Le fichier est trop volumineux (max 150 MB)."))
raise forms.ValidationError(_("Le format doit être YYYY-MM (ex. 2025-10)."))

messages.error(request, _("Action inconnue : %(action)s") % {"action": action})
messages.error(request, _("Erreur lors du lancement de l'import : %(error)s") % {"error": str(e)})

Patterns to avoid:

  • Vous avez oublié XLe champ X est obligatoire.
  • Erreur ! Quelque chose ne va pas. → name the failing operation and the cause.
  • Internal error / Server error / 500Une erreur est survenue lors de %(action)s. Réessayer dans quelques minutes. (only when the actual cause is genuinely unknown — otherwise be specific).
  • Bare exception strings (messages.error(request, str(exc))) — wrap in a French preamble: _("Erreur : %(detail)s") % {"detail": str(exc)}.

Success-message tone

Past participle, French, with the subject named, no exclamation, no emoji.

messages.success(request, _("Patient enregistré."))
messages.success(request, _("Statut mis à jour."))
messages.success(request, _("Synchronisation réussie pour %(name)s.") % {"name": practice.display_name})
messages.success(request, _("Documents régénérés."))

Avoid the noun form (Enregistrement réussi.) — past participle of the verb is shorter and matches the button label (EnregistrerEnregistré).

Empty-state copy

Two lines. Line 1 names the absence; line 2 suggests the next step. Per guidelines/ui/lists.md, list pages have two empty states — "no records at all" and "no records for the current filters" — and the copy differs:

{# No records at all #}
<i class="bi bi-people text-muted" style="font-size: 4rem;"></i>
<p class="text-muted mt-3">{% trans "Aucun patient." %}</p>
<a href="{% url 'patients:create' %}" class="btn btn-action-primary mt-2">
    <i class="bi bi-plus-circle"></i> {% trans "Ajouter un patient" %}
</a>

{# No records for the current filters #}
<i class="bi bi-search text-muted" style="font-size: 4rem;"></i>
<p class="text-muted mt-3">{% trans "Aucun patient pour ces filtres." %}</p>
<button class="btn btn-action-secondary mt-2" hx->
    {% trans "Réinitialiser les filtres" %}
</button>

The bare {% trans "Aucun résultat." %} is acceptable only inside a <tbody> row when the page already shows the active filters above the table — otherwise expand to Aucun X. (named target).

Confirmation copy

  • hx-confirm — one French sentence ending in ?, naming the verb and the target type. Supprimer cet horaire ?, Supprimer ce bloc ?. Don't interpolate the record's name (the dialog is a yes/no, not an audit log).
  • _confirm_delete.html — heading Supprimer un X (verb + indefinite article), details card showing the record's identifying fields, irreversibility <div class="alert alert-warning"> explaining downstream consequences, primary button Supprimer définitivement (red), secondary Annuler.
  • Two-step edit confirmation (Alpine confirming flag, see guidelines/ui/forms.md) — the modal-style copy is Confirmer la modification / Confirmer l'enregistrement paired with a one-sentence consequence.

Plurals

Use {% blocktrans %} with count for any sentence whose grammar changes with the count:

{% blocktrans count counter=count %}
{{ counter }} cabinet sélectionné.
{% plural %}
{{ counter }} cabinets sélectionnés.
{% endblocktrans %}

The (s) shortcut (%(count)s cabinet(s)) is acceptable for terse status lines and bulk-summary banners (the canonical bulk summary in apps/sync/views.py:bulk_sync_practices uses it: Synchronisation : %(success)s réussi(s), %(error)s erreur(s).). Don't use (s) in body copy or empty-state lines — those get the blocktrans form.

Punctuation

  • French keeps a non-breaking space before :, ;, ?, !. The codebase does not automate this — write it manually with a literal NBSP (U+00A0) when the rendering matters (legal documents, exports), and accept the regular space everywhere else. The .po extractor preserves whatever you typed.
  • Detail-page key/value rows use <strong>Label :</strong> value with a regular space + colon — that's the canonical render per guidelines/ui/detail-pages.md. Don't try to NBSP-pad it.
  • Ellipsis: (U+2026) for placeholders ending in ellipsis (Rechercher…); three ASCII dots ... are tolerated in legacy strings but new code uses .

Anti-patterns

  • Are you sure? / Êtes-vous sûr ? alone — see Hard rule 5.
  • ❌ Synonym drift — Sauvegarder, Abandonner, Effacer, Éditer instead of the glossary verbs.
  • ❌ Title Case in headings or buttons — Tableau De Bord, Ajouter Un Cabinet. Sentence case only.
  • ❌ Tutoiement — Connecte-toi, Tu n'as pas accès. Vouvoiement only.
  • ❌ Exclamation marks in success / status copy — Patient enregistré ! should be Patient enregistré..
  • ❌ Emoji in _() strings — even when the surrounding code shows status. Use Bootstrap Icons via <i class="bi …"> outside the trans block.
  • _(f"…") — handled by the conformance hook (Rule 5, block-severity), but worth re-stating: f-strings inside _() never resolve. See guidelines/i18n/translation-rules.md.
  • ❌ Concatenating translatable fragments (_("Utilisateur") + " " + name + " " + _("créé")) — French translators can't reorder. Single placeholder string instead.
  • ❌ Bare exception text in messages.error — wrap with a French preamble.
  • Aucun résultat. as the only empty-state copy on a page with actionable filters or an "add new" path.

Known deviations

  • Skeleton templates ship with accent-stripped French strings (Aucun resultat, Aucun element, Coordonnees, Cree le, Modifie le, caracteres, Texte explicatif). Fixed in this graduation — the skeletons are the starting point developers copy, so accent drift here propagates to every new page. See commit message accompanying this guideline.
  • apps/finance_workflow/, apps/websites/, apps/register/admin.py, apps/crm/, apps/core/models.py (ContractDocument file fields) — pockets of English source survive the 2026-04 French-source convergence. scripts/audit_i18n_source.py "suspected-english" bucket is the rolling backlog (per guidelines/i18n/translation-rules.md "Known deviations"). When picked up, apply the glossary above.
  • Êtes-vous sûr de vouloir supprimer … appears in four _confirm_delete.html templates (patients, dentists, accounts, sync/practitioner_review.html). It violates Hard rule 5 (no consequence stated) and uses a question form where the canonical pattern is verb-headed (Supprimer un X). Tracked alongside the existing ux-confirm-delete-template-convergence backlog item — converge the copy in the same sweep.
  • Synchroniser as both button verb and tooltip is fine — but the tooltip should match the button label exactly when the action is the same. Audit pass material; not a per-PR concern.

When to break the rules

  • Brand and proper-noun strings stay capitalised even mid-sentence. Synchronisation Doctolib, Sync Pennylane, Sites Web (the module name in the navbar — kept as-is for brand consistency with the Helios platform).
  • Module-specific verbs allowed when the domain term has no idiomatic French equivalent. Booking, Closing, Funnel survive in CRM/finance UI as accepted French jargon; don't translate to Réservation / Clôture / Tunnel if the team uses the English term in the actual workflow.
  • Bulk-summary (s) plurals stay despite the cleaner blocktrans form — they're terser and the bulk message is a status line, not body copy. Documented in Plurals.
  • Email-notification copy is allowed to be more formal (Veuillez approuver ou rejeter ce mouvement dans Aletheia.) than in-app copy. Email is a standalone surface; the user has had to leave the app to read it, so the in-app brevity rules don't apply.
  • Legal / regulatory strings (assembly minutes, share-register exports, contract documents) follow French legal-writing conventions, including the NBSP before : ; ? !, even where the rest of the codebase doesn't bother.