Commit 28180403 authored by Jordan Grossemy's avatar Jordan Grossemy
Browse files

Lot 5 : le texte qui ne vit pas dans le rendu, et deux fois où la mesure a triché

287 chaînes enveloppées, 217 traductions neuves. Ce sont les messages d'erreur,
les libellés de colonnes, les catalogues et les états vides — du texte qui vit
dans une expression, pas dans un nœud JSX, et que l'enveloppeur du lot précédent
ne pouvait pas voir.

── Deux fardeaux de l'outillage, corrigés

« quoi="Aucun arbitrage n'attend." » est une valeur d'attribut JSX : l'envelopper
en « quoi=t(…) » ne compile pas, il faut « quoi={t(…)} ». Cent quarante-trois
erreurs de compilation, reprises après avoir appris la différence à l'outil.

Et une ligne abîmée : « {" · "}dossier de préparation gelé en version{" "} ». Le
texte français y était pris en sandwich entre deux expressions de chaîne, et le
motif a coupé du mauvais côté des guillemets. C'est la seule du dépôt — cherchée
partout ailleurs.

── La mesure ne comptait que ce qui portait un accent

« Hors cellule », « Arbitrages en attente », « Message direct », « Actions en
retard » : ni accent, ni cédille, donc invisibles. Cent quatre-vingt-dix chaînes
hors du compte, et un pourcentage qui se remettait à flatter — pour la troisième
fois de ce chantier, et toujours de la même manière : le compteur mesurait ce
qu'on lui avait donné.

La seconde passe les cherche par leur forme : sept caractères au moins, des
lettres et de la ponctuation française, un mot-outil ou une espace. Les noms de
classes de la feuille de style — « discret petit pousse », « pastille alerte » —
sont écartés nommément, faute de quoi ils pèseraient plus lourd que le texte.

La couverture retombe donc de quatre-vingt-seize à quatre-vingt-huit pour cent.
C'est un progrès, pas un recul : les huit points manquants existaient déjà, ils
n'étaient pas comptés.

── Ce qui reste

Environ cent quatre-vingt-dix chaînes sans accent à envelopper, cinquante-quatre
libellés déclarés au niveau du module — qui demandent un marquage explicite,
« t » étant un crochet React et ne pouvant pas s'appeler hors d'un composant —
et cent quatre-vingt-trois côté serveur, qui ne relèvent pas de cette méthode.

Co-Authored-By: Claude (RCA)
parent 4a2c9cff
Loading
Loading
Loading
Loading
Loading
+18 −3
Changes for apps/api/src/verification/interface.ts: 18 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -181,12 +181,22 @@ export async function verifierInterface(r: Rapport): Promise<void> {
  // pas, et les compter à part reviendrait à publier de nouveau un chiffre qui
  // ne dit pas ce qu'il paraît dire. Ils s'affichent, donc ils comptent.
  const FRANCAIS = /"([^"\\\n]*[àâäéèêëîïôöùûü眒][^"\\\n]*)"/g;
  // Un accent n'est pas une preuve, et son absence n'en est pas une non plus :
  // « Hors cellule », « Arbitrages en attente », « Message direct » n'en
  // portent aucun. Sans cette seconde passe, cent quatre-vingt-dix chaînes
  // restaient hors du compte — et le pourcentage se serait remis à flatter.
  const SANS_ACCENT = /"([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ' ,.:;!?()«»–—-]{6,})"/g;
  const MOT_FRANCAIS =
    /\b(le|la|les|un|une|des|de|du|et|ou|pas|ne|se|qui|que|sur|dans|pour|est|sont|aucun|aucune|sans|avec|par|au|aux|ce|cette|il|elle|hors|en)\b/i;
  // Les noms de classes de la feuille de style ne sont pas du texte.
  const CLASSE =
    /\b(discret|petit|pousse|accent|pastille|ligne|carte|etiq|crit|alerte|neutre|sans-point|ok|mono|aide|scene|contenu|champ|pile|grille|vide|item|actif|fait|valeur|nombres|noreferrer|noopener)\b/;
  const APPEL_T = /\bt\(\s*(?:"(?:[^"\\\n]|\\.)*"\s*\+?\s*)+\)?/g;
  for (const f of fichiers) {
    if (f.includes("/langues/")) continue;
    const texte = await readFile(f, "utf8");
    const couverts = [...texte.matchAll(APPEL_T)].map((m) => [m.index!, m.index! + m[0].length]);
    for (const m of texte.matchAll(FRANCAIS)) {
    for (const m of [...texte.matchAll(FRANCAIS), ...texte.matchAll(SANS_ACCENT)]) {
      const i = m.index!;
      if (couverts.some(([a, b]) => a! <= i && i < b!)) continue;
      const debut = texte.lastIndexOf("\n", i) + 1;
@@ -196,7 +206,12 @@ export async function verifierInterface(r: Rapport): Promise<void> {
      if (avant.trimStart().startsWith("*") || avant.includes("//")) continue;
      if (avant.includes("throw new Error(") || avant.includes("console.")) continue;
      const valeur = m[1]!.trim();
      if (valeur.length >= 3) nues.add(valeur);
      if (valeur.length < 3) continue;
      const accentue = /[àâäéèêëîïôöùûü眒]/.test(valeur);
      if (!accentue && (CLASSE.test(valeur)
        || (!MOT_FRANCAIS.test(valeur) && !valeur.includes(" "))
        || /className\s*[=:]\s*$|queryKey|`/.test(avant))) continue;
      nues.add(valeur);
    }
  }

@@ -217,7 +232,7 @@ export async function verifierInterface(r: Rapport): Promise<void> {
  // cinquante-six, puis soixante-dix-neuf. Il ne se baisse jamais. Le jour où
  // il atteindra cent, il deviendra une exigence simple — d'ici là, il dit
  // exactement ce qui reste, et empêche que cela reparte à l'envers.
  const PLANCHER = 79;
  const PLANCHER = 88;
  r.verifie("EX-21",
    `${employees.size} chaînes visibles sur ${visibles} passent par la traduction`
    + ` (${couverture} %, plancher ${PLANCHER} %)`,
+2 −2
Changes for apps/web/src/coque/Coque.tsx: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -384,7 +384,7 @@ export function Coque({ criseId }: { criseId: string }) {
            <button
              disabled={!capacites.visio}
              title={capacites.visio ? undefined
                : "Aucun serveur média n’est configuré sur cette instance."}
                : t("Aucun serveur média n’est configuré sur cette instance.")}
              onClick={() => definirAppel({
                salleId: salleDeLaCellule.id, libelle: salleDeLaCellule.libelle,
              })}>
@@ -468,7 +468,7 @@ export function Coque({ criseId }: { criseId: string }) {
                      <button className="item" style={{ paddingLeft: 22, fontSize: 13 }}
                        disabled={!capacites.visio}
                        title={capacites.visio ? undefined
                          : "Aucun serveur média n’est configuré sur cette instance."}
                          : t("Aucun serveur média n’est configuré sur cette instance.")}
                        onClick={() => definirAppel({ salleId: voix.id, libelle: voix.libelle })}>
                        🔊 {voix.libelle.replace(/^Visio /, "")}
                      </button>
+3 −3
Changes for apps/web/src/coque/DockChat.tsx: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -112,7 +112,7 @@ function Conversation({ criseId, fenetre, basculer, reduire, lue, fermer }: {
      await client.invalidateQueries({
        queryKey: ["direct", criseId, fenetre.conversationId] });
    } catch (e2) {
      definirErreur(e2 instanceof ErreurApi ? e2.message : "Le message n’est pas parti.");
      definirErreur(e2 instanceof ErreurApi ? e2.message : t("Le message n’est pas parti."));
    } finally {
      definirOccupe(false);
    }
@@ -134,8 +134,8 @@ function Conversation({ criseId, fenetre, basculer, reduire, lue, fermer }: {
            portée, fermer l'écarte. Confondre les deux fait perdre le fil
            qu'on voulait seulement mettre de côté. */}
        <button className="reduire"
          aria-label={fenetre.repliee ? "Agrandir la conversation" : "Réduire la conversation"}
          title={fenetre.repliee ? "Agrandir" : "Réduire"}
          aria-label={fenetre.repliee ? "Agrandir la conversation" : t("Réduire la conversation")}
          title={fenetre.repliee ? "Agrandir" : t("Réduire")}
          onClick={() => (fenetre.repliee ? basculer(id) : reduire(id))}>
          {fenetre.repliee ? "▴" : "▾"}
        </button>
+19 −19
Changes for apps/web/src/coque/PanneauVisio.tsx: 19 added lines, 19 removed lines.
Original line number Diff line number Diff line
@@ -122,7 +122,7 @@ function PanneauVisio({
      // confondre fait chercher la panne au mauvais endroit.
      .on(RoomEvent.Disconnected, () => {
        if (!vivant) return;
        definirErreur("La connexion à l’appel a été perdue.");
        definirErreur(t("La connexion à l’appel a été perdue."));
        definirPerdu(true);
        definirEtat("echec");
      });
@@ -164,12 +164,12 @@ function PanneauVisio({
          const nom = (e as { name?: string }).name;
          definirGeneMicro(
            nom === "NotAllowedError"
              ? "Micro refusé par le navigateur. Vous entendez la cellule sans pouvoir "
                + "lui parler : autorisez le micro dans la barre d’adresse, ou appelez."
              ? t("Micro refusé par le navigateur. Vous entendez la cellule sans pouvoir "
                + "lui parler : autorisez le micro dans la barre d’adresse, ou appelez.")
              : nom === "NotFoundError"
                ? "Aucun micro sur ce poste. Vous entendez la cellule sans pouvoir lui "
                  + "parler — le téléphone reste le repli que le dossier prévoit."
                : "Le micro n’a pas pu être activé. Vous restez en écoute.",
                ? t("Aucun micro sur ce poste. Vous entendez la cellule sans pouvoir lui "
                  + "parler — le téléphone reste le repli que le dossier prévoit.")
                : t("Le micro n’a pas pu être activé. Vous restez en écoute."),
          );
        }
      } catch (e) {
@@ -177,8 +177,8 @@ function PanneauVisio({
        definirEtat("echec");
        definirErreur(
          e instanceof ErreurApi ? e.message
            : capacites.visio ? "Le serveur média est injoignable depuis ce réseau."
              : "Aucun serveur média n’est configuré sur cette instance.",
            : capacites.visio ? t("Le serveur média est injoignable depuis ce réseau.")
              : t("Aucun serveur média n’est configuré sur cette instance."),
        );
      }
    })();
@@ -373,7 +373,7 @@ function PanneauVisio({
              ) : (
                <span className="initiales">{initiales(salle.localParticipant.name ?? "Moi")}</span>
              )}
              <span className="nom">Vous{micro ? "" : " · micro coupé"}</span>
              <span className="nom">Vous{micro ? "" : t(" · micro coupé")}</span>
            </div>
            {tuiles.map((tuile) => (
              <div key={tuile.cle} className={`tuile${tuile.ecran ? " ecran" : ""}`}
@@ -391,15 +391,15 @@ function PanneauVisio({
              void salle.localParticipant.setMicrophoneEnabled(!micro)
                .then(() => { definirMicro(!micro); definirGeneMicro(null); })
                .catch(() => definirGeneMicro(
                  "Le micro reste indisponible. Vérifiez l’autorisation du navigateur."));
                  t("Le micro reste indisponible. Vérifiez l’autorisation du navigateur.")));
            }}>
              {micro ? "Micro" : "Micro coupé"}
              {micro ? "Micro" : t("Micro coupé")}
            </button>
            <button className={camera ? "accent" : ""} onClick={() => {
              void salle.localParticipant.setCameraEnabled(!camera);
              definirCamera(!camera);
            }}>
              {camera ? "Caméra" : "Caméra coupée"}
              {camera ? t("Caméra") : t("Caméra coupée")}
            </button>
            {/* Partager son écran vaut souvent mieux que le décrire : une carte,
                un tableau de bord, une main courante qu'on relit ensemble. */}
@@ -407,9 +407,9 @@ function PanneauVisio({
              void salle.localParticipant.setScreenShareEnabled(!ecran)
                .then(() => definirEcran(!ecran))
                .catch(() => definirGeneMicro(
                  "Le partage d’écran a été refusé par le navigateur."));
                  t("Le partage d’écran a été refusé par le navigateur.")));
            }}>
              {ecran ? "Écran partagé" : "Partager l’écran"}
              {ecran ? t("Écran partagé") : t("Partager l’écran")}
            </button>
            {/* EF-806 : l'enregistrement se déclenche par notre API, jamais par
                le client — et seuls l'animateur de la cellule ou le directeur
@@ -417,8 +417,8 @@ function PanneauVisio({
                de rapporter son refus. */}
            <button className={enregistre ? "danger" : ""}
              title={enregistre
                ? "Arrêter l’enregistrement de cette réunion"
                : "Enregistrer cette réunion — le témoin s’allume chez tous les participants"}
                ? t("Arrêter l’enregistrement de cette réunion")
                : t("Enregistrer cette réunion — le témoin s’allume chez tous les participants")}
              onClick={() => {
                definirGeneEnregistrement(null);
                const requete = enregistre
@@ -428,9 +428,9 @@ function PanneauVisio({
                    `/api/crises/${criseId}/salles/${appel.salleId}/enregistrement`)
                    .then((r) => definirEnregistrementId(r.id));
                void requete.catch((e) => definirGeneEnregistrement(
                  e instanceof ErreurApi ? e.message : "L’enregistrement n’a pas pu changer d’état."));
                  e instanceof ErreurApi ? e.message : t("L’enregistrement n’a pas pu changer d’état.")));
              }}>
              {enregistre ? "Arrêter l’enregistrement" : "Enregistrer"}
              {enregistre ? t("Arrêter l’enregistrement") : "Enregistrer"}
            </button>
            <button className={reglages ? "accent" : ""} onClick={() => void ouvrirReglages()}
              aria-expanded={reglages} title={t("Choisir le micro et la caméra")}>⚙</button>
@@ -441,7 +441,7 @@ function PanneauVisio({

          {reglages && (
            <div className="appareils">
              {([["audioinput", "Micro"], ["videoinput", "Caméra"]] as const).map(
              {([["audioinput", "Micro"], ["videoinput", t("Caméra")]] as const).map(
                ([genre, libelle]) => {
                  const liste = parGenre(genre);
                  return (
+26 −22
Changes for apps/web/src/coque/Tuiles.tsx: 26 added lines, 22 removed lines.
Original line number Diff line number Diff line
@@ -56,7 +56,7 @@ function Arbitrages({ cockpit, criseId, aller }: ContexteTuile) {
        </button>
      ) : undefined}
      enfants={liste.length === 0 ? (
        <Rien quoi="Aucun arbitrage n’attend." />
        <Rien quoi={t("Aucun arbitrage n’attend.")} />
      ) : (
        <div className="pile">
          {liste.map((a) => (
@@ -73,16 +73,17 @@ function Arbitrages({ cockpit, criseId, aller }: ContexteTuile) {
}

function ActionsEnRetard({ cockpit }: ContexteTuile) {
  const t = useT();
  const liste = cockpit.actions_en_retard;
  return (
    <Carte titre="Actions en retard" alerte={liste.length > 0} enfants={
      liste.length === 0 ? <Rien quoi="Aucune action n’a dépassé son échéance." /> : (
      liste.length === 0 ? <Rien quoi={t("Aucune action n’a dépassé son échéance.")} /> : (
        <table>
          <tbody>
            {liste.map((a) => (
              <tr key={a.id}>
                <td>{a.intitule}</td>
                <td className="discret">{a.responsable ?? "non attribuée"}</td>
                <td className="discret">{a.responsable ?? t("non attribuée")}</td>
                <td className="nombres" style={{ color: "var(--crit)" }}>
                  {a.echeance ? dateHeure(a.echeance) : "—"}
                </td>
@@ -124,18 +125,19 @@ function Cellules({ cockpit }: ContexteTuile) {
}

function TiersSansRetour({ cockpit }: ContexteTuile) {
  const t = useT();
  const liste = cockpit.tiers_en_attente;
  return (
    <Carte titre="Tiers appelés sans retour" alerte={liste.length > 0} enfants={
    <Carte titre={t("Tiers appelés sans retour")} alerte={liste.length > 0} enfants={
      liste.length === 0
        ? <Rien quoi="Tous les tiers activés ont répondu." />
        ? <Rien quoi={t("Tous les tiers activés ont répondu.")} />
        : (
          <div className="pile">
            {liste.map((tuile) => (
              <div key={tuile.tiers_id} className="ligne">
                <strong>{tuile.nom}</strong>
                <span className="discret" style={{ fontSize: 12.5 }}>
                  {tuile.delai_attendu ? `délai annoncé : ${tuile.delai_attendu}` : "aucun délai annoncé"}
                  {tuile.delai_attendu ? `délai annoncé : ${tuile.delai_attendu}` : t("aucun délai annoncé")}
                </span>
                <span className="pastille crit pousse">{tuile.attente_minutes} min</span>
              </div>
@@ -146,10 +148,11 @@ function TiersSansRetour({ cockpit }: ContexteTuile) {
}

function Presents({ cockpit }: ContexteTuile) {
  const t = useT();
  return (
    <Carte titre="Qui travaille en ce moment" enfants={
      cockpit.actifs.length === 0
        ? <Rien quoi="Personne n’a écrit au journal depuis un quart d’heure." />
        ? <Rien quoi={t("Personne n’a écrit au journal depuis un quart d’heure.")} />
        : (
          <div className="ligne" style={{ flexWrap: "wrap", gap: 6 }}>
            {cockpit.actifs.map((a) => (
@@ -169,6 +172,7 @@ interface PointPublie {
}

function DernierPoint({ criseId, aller }: ContexteTuile) {
  const t = useT();
  const { data } = useQuery({
    queryKey: ["points", criseId],
    queryFn: () => api.lire<{ points: PointPublie[] }>(`/api/crises/${criseId}/points`),
@@ -183,15 +187,15 @@ function DernierPoint({ criseId, aller }: ContexteTuile) {
        </button>
      }
      enfants={!p ? (
        <Rien quoi="Aucun point publié. Le premier fixe la cadence." />
        <Rien quoi={t("Aucun point publié. Le premier fixe la cadence.")} />
      ) : (
        <>
          <div className="discret" style={{ fontSize: 12.5 }}>
            {dateHeure(p.publie_at)} · #{p.seq}
          </div>
          {([["Ce qu’on sait", p.sait], ["Ce qu’on ne sait pas", p.ne_sait_pas],
            ["Ce qu’on fait", p.en_cours],
            ["Ce qu’on demande d’arbitrer", p.arbitrages]] as const)
          {([[t("Ce qu’on sait"), p.sait], [t("Ce qu’on ne sait pas"), p.ne_sait_pas],
            [t("Ce qu’on fait"), p.en_cours],
            [t("Ce qu’on demande d’arbitrer"), p.arbitrages]] as const)
            .filter(([, v]) => v)
            .map(([q, v]) => (
              <div key={q} style={{ marginTop: 8 }}>
@@ -220,14 +224,14 @@ function Continuite({ criseId, aller }: ContexteTuile) {
  const tombees = toutes.filter((a) => a.etat === "arrete" || a.etat === "degrade");

  return (
    <Carte titre="Ce qui est tombé" alerte={tombees.length > 0}
    <Carte titre={t("Ce qui est tombé")} alerte={tombees.length > 0}
      action={
        <button className="petit" onClick={() => aller(`/crises/${criseId}/continuite`)}>
          {t("Constater")}
        </button>
      }
      enfants={toutes.length === 0 ? (
        <Rien quoi="Aucune application au registre : il se constitue à froid." />
        <Rien quoi={t("Aucune application au registre : il se constitue à froid.")} />
      ) : tombees.length === 0 ? (
        <Rien quoi={`${toutes.length} application(s) suivie(s), aucune signalée en défaut.`} />
      ) : (
@@ -244,7 +248,7 @@ function Continuite({ criseId, aller }: ContexteTuile) {
                )}
              </div>
              <div style={{ fontSize: 13, color: a.secours ? "var(--encre-2)" : "var(--alerte)" }}>
                {a.secours ?? "aucun secours décrit au dossier"}
                {a.secours ?? t("aucun secours décrit au dossier")}
              </div>
            </div>
          ))}
@@ -275,7 +279,7 @@ function EcheanceReglementaire({ criseId, aller }: ContexteTuile) {
    : m < 120 ? `${m} min` : `${Math.round(m / 60)} h`;

  return (
    <Carte titre="Prochaine échéance réglementaire"
    <Carte titre={t("Prochaine échéance réglementaire")}
      alerte={proche !== undefined && proche.reste_minutes! < 240}
      action={
        <button className="petit" onClick={() => aller(`/crises/${criseId}/echeances`)}>
@@ -285,8 +289,8 @@ function EcheanceReglementaire({ criseId, aller }: ContexteTuile) {
      }
      enfants={!proche ? (
        <Rien quoi={(data?.parties.length ?? 0) === 0
          ? "Aucune partie prenante au registre : il se constitue à froid."
          : "Toutes les parties dont le délai courait ont été informées."} />
          ? t("Aucune partie prenante au registre : il se constitue à froid.")
          : t("Toutes les parties dont le délai courait ont été informées.")} />
      ) : (
        <>
          <div className="ligne">
@@ -338,7 +342,7 @@ function ActionsCellule({ criseId, celluleId, aller }: ContexteTuile) {
            {ouvertes.map((a) => (
              <tr key={a.id}>
                <td>{a.intitule}</td>
                <td className="discret">{a.responsable_libelle ?? "non attribuée"}</td>
                <td className="discret">{a.responsable_libelle ?? t("non attribuée")}</td>
                <td className="discret">{LIBELLES_STATUT[a.statut]}</td>
              </tr>
            ))}
@@ -363,7 +367,7 @@ function MesActions({ aller }: ContexteTuile) {
  const liste = data?.actions ?? [];

  return (
    <Carte titre="Ce qui m’incombe"
    <Carte titre={t("Ce qui m’incombe")}
      action={<button className="petit" onClick={() => aller("/mes-actions")}>{t("Tout voir")}</button>}
      enfants={liste.length === 0 ? (
        <Rien quoi="Rien ne vous attend sur une crise en cours." />
@@ -407,7 +411,7 @@ function Mobilisation({ criseId, aller }: ContexteTuile) {
                                                                                                        + "suivi")}
        </button>
      }
      enfants={!m ? <Rien quoi="Aucune mobilisation lancée." /> : (
      enfants={!m ? <Rien quoi={t("Aucune mobilisation lancée.")} /> : (
        <>
          <div style={{ fontSize: 13.5 }}>{m.message}</div>
          <div className="ligne" style={{ gap: 14, marginTop: 6, fontSize: 13,
@@ -448,7 +452,7 @@ function Enregistrement({ criseId, aller }: ContexteTuile) {
        </button>
      }
      enfants={enCours.length === 0 ? (
        <Rien quoi="Aucune réunion n’est en cours d’enregistrement." />
        <Rien quoi={t("Aucune réunion n’est en cours d’enregistrement.")} />
      ) : (
        <div className="pile">
          {enCours.map((e) => (
@@ -486,7 +490,7 @@ function Veille({ organisationId, criseId, aller }: ContexteTuile) {
        </button>
      }
      enfants={aTraiter.length === 0 ? (
        <Rien quoi="Rien de nouveau à traiter." />
        <Rien quoi={t("Rien de nouveau à traiter.")} />
      ) : (
        <div className="pile">
          {aTraiter.slice(0, 5).map((s) => (
Loading