{
  "docs": {
    "gettingStarted": {
      "hero": {
        "title": "Documentation",
        "subtitle": "Tout ce dont vous avez besoin pour créer des applications web réactives avec No.JS"
      },
      "introduction": {
        "title": "Introduction",
        "text": "No.JS est un framework réactif HTML-first. Construisez des applications web dynamiques et orientées données en utilisant uniquement des attributs HTML — pas de build, pas de DOM virtuel, pas de JSX.",
        "callout": "Zéro dépendance · Fonctionne dans tous les navigateurs modernes · Aucune étape de build requise"
      },
      "installation": {
        "title": "Installation",
        "cdnSubtitle": "CDN (recommandé)",
        "selfHostedSubtitle": "Auto-hébergé",
        "selfHostedText": "Téléchargez dist/iife/no.js et incluez-le avec une balise <script>. C'est un fichier unique, sans dépendances."
      },
      "quickStart": {
        "title": "Démarrage rapide",
        "text": "Créez un fichier index.html : incluez le script, ajoutez quelques attributs, et c'est tout. Pas de app.mount(), pas de createApp(), pas de NgModule. Ça fonctionne, tout simplement."
      },
      "howItWorks": {
        "title": "Comment ça marche",
        "text": "Au DOMContentLoaded, No.JS parcourt le DOM à la recherche d'éléments possédant des attributs reconnus. Chaque attribut correspond à une directive exécutée par priorité.",
        "card1Title": "1. Analyse",
        "card1Desc": "Parcourt le DOM à la recherche d'éléments possédant des attributs reconnus.",
        "card2Title": "2. Résolution",
        "card2Desc": "Chaque attribut correspond à une directive exécutée par priorité (récupération de données d'abord, puis conditions, puis rendu).",
        "card3Title": "3. Réaction",
        "card3Desc": "Toutes les données résident dans des contextes réactifs (basés sur Proxy). Quand les données changent, chaque élément lié se met à jour automatiquement.",
        "card4Title": "4. Portée",
        "card4Desc": "Les contextes héritent des éléments parents, comme la portée lexicale. Un bind à l'intérieur d'une boucle each peut accéder à la fois à l'élément de la boucle et aux données des ancêtres."
      },
      "coreConcepts": {
        "title": "Concepts clés",
        "reactiveContextSubtitle": "Contexte réactif",
        "reactiveContextText": "Chaque élément peut avoir un contexte — un objet de données réactif. Les contextes sont créés par state, get, store, etc. Les éléments enfants héritent automatiquement du contexte de leur parent.",
        "directivePrioritySubtitle": "Priorité des directives",
        "tableCol1": "Priorité",
        "tableCol2": "Directives",
        "tableCol3": "Description",
        "tableRow1": "Initialiser l'état local/global",
        "tableRow4": "Structurel (ajouter/supprimer du DOM)",
        "tableRow5": "Rendu (mettre à jour le DOM existant)",
        "tableRow7": "Effets de bord",
        "expressionSubtitle": "Syntaxe des expressions",
        "expressionText": "La plupart des valeurs de directives acceptent des expressions JavaScript évaluées par rapport au contexte courant :",
        "tableRow1b": "Récupérer des données, error boundaries, namespace i18n",
        "tableRow2b": "Valeurs dérivées et watchers d'effets secondaires",
        "tableRow5b": "Références aux éléments",
        "tableRow15": "Configuration du drag and drop (Maintenant dans NoJS Elements)"
      }
    },
    "cheatsheet": {
      "data": {
        "title": "Données",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "base": "Définir l'URL de base de l'API pour les descendants",
        "get": "Récupérer des données (GET)",
        "post": "Soumettre des données (POST)",
        "put": "Mettre à jour des données (PUT)",
        "patch": "Mise à jour partielle (PATCH)",
        "delete": "Supprimer des données (DELETE)",
        "query": "Lecture avec corps (QUERY, RFC 10008)",
        "as": "Nom des données récupérées dans le contexte",
        "body": "Corps de la requête",
        "headers": "En-têtes de la requête",
        "params": "Paramètres de requête",
        "cached": "Mettre en cache les réponses (memory/local/session)",
        "into": "Écrire la réponse dans un store global nommé",
        "debounce": "Debounce des refetchs d'URL réactives (ms)",
        "retry": "Nombre de tentatives par élément",
        "refresh": "Intervalle de polling en ms",
        "success": "ID du template à afficher en cas de succès",
        "then": "Expression à exécuter en cas de succès",
        "redirect": "Chemin de navigation après succès",
        "confirm": "Message de confirmation avant la requête",
        "skeleton": "Afficher/masquer un placeholder pendant le chargement",
        "retryDelay": "Délai entre les tentatives en ms",
        "sse": "Ouvre un flux SSE (EventSource)",
        "sseEvent": "Type d'événement nommé à écouter",
        "sseInsert": "Mode de mise à jour de la variable (replace, append, prepend)",
        "sseLimit": "Longueur maximale du tableau en mode append/prepend",
        "sseCredentials": "Envoie les cookies vers les endpoints cross-origin"
      },
      "state": {
        "title": "État",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "state": "Créer un état réactif local",
        "store": "Définir/accéder à un store global",
        "computed": "Valeur dérivée réactive",
        "watch": "Réagir aux changements de valeur",
        "persist": "Attribut de la directive state — persiste l'état dans le stockage",
        "model": "Liaison bidirectionnelle pour les champs de saisie",
        "persistKey": "Clé de stockage pour la persistance",
        "persistFields": "Champs séparés par virgule à persister",
        "persistSchema": "Valider les clés restaurées par rapport à l'état initial"
      },
      "rendering": {
        "title": "Rendu",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "bind": "Définir le contenu texte",
        "bindHtml": "Définir le innerHTML (nettoyé)",
        "bindStar": "Lier n'importe quel attribut",
        "if": "Rendu conditionnel",
        "elseIf": "Condition chaînée",
        "then": "Template pour le cas vrai",
        "else": "Template pour le cas faux",
        "show": "Basculer la visibilité (CSS)",
        "hide": "Inverse de show",
        "switch": "Rendu switch/case",
        "case": "Correspondance de cas",
        "default": "Cas par défaut"
      },
      "loops": {
        "title": "Boucles",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "foreach": "Directive de boucle primaire",
        "each": "Alias de foreach",
        "for": "Alias de foreach",
        "from": "Tableau source (DEPRECATED — utilisez la syntaxe \"item in array\")",
        "template": "Template à cloner",
        "index": "Nom de la variable d'index",
        "key": "Clé unique pour le diffing",
        "filter": "Expression de filtrage",
        "sort": "Propriété de tri (nom seul, sans préfixe de variable d'élément)",
        "limit": "Nombre maximum d'éléments",
        "offset": "Éléments à ignorer",
        "else": "Template rendu quand le tableau est vide ou null/undefined"
      },
      "events": {
        "title": "Événements",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "onClick": "Gestionnaire de clic",
        "onSubmit": "Gestionnaire de soumission",
        "onInput": "Gestionnaire de saisie",
        "onKeydown": "Gestionnaire de touche",
        "onInit": "Se déclenche immédiatement lors de l'initialisation",
        "onMounted": "Cycle de vie : monté",
        "onUnmounted": "Cycle de vie : démonté",
        "throttle": "Limiter l'exécution du gestionnaire (ms)",
        "self": "Se déclenche uniquement si la cible de l'événement est l'élément lui-même",
        "backspace": "Modificateur de touche pour la touche Retour arrière",
        "onUpdated": "Lifecycle : mutation du DOM observée",
        "onError": "Lifecycle : erreur dans le sous-arbre"
      },
      "styling": {
        "title": "Styles",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "classStar": "Basculer une classe CSS",
        "classMap": "Classe depuis un objet",
        "styleStar": "Définir un style inline",
        "styleMap": "Style depuis un objet"
      },
      "forms": {
        "title": "Formulaires",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "validate": "Activer la validation du formulaire/champ (Maintenant dans NoJS Elements)",
        "error": "Template d'erreur pour le champ",
        "success": "Template de succès",
        "loading": "Template de chargement",
        "confirm": "Boîte de dialogue de confirmation",
        "redirect": "Redirection en cas de succès"
      },
      "routing": {
        "title": "Routage",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "route": "Définir une route ou un lien",
        "routeView": "Point de sortie de la route",
        "routeViewNamed": "Point de sortie de route nommé",
        "outlet": "Cibler un outlet nommé",
        "routeActive": "Classe de lien actif",
        "guard": "Condition de garde de route",
        "routeActiveExact": "Classe active en correspondance exacte pour les liens de route",
        "redirect": "Chemin de redirection en cas d'échec de la garde",
        "lazyPriority": "Charger le template distant avant tous les autres (Phase 0)",
        "lazyOnDemand": "Charger le template de route uniquement lors de la première visite (templates de route uniquement)",
        "routerForward": "Naviguer en avant dans l'historique",
        "routerOn": "S'abonner aux changements de route",
        "routeWildcard": "Route wildcard catch-all 404",
        "routerCurrent": "Objet de la route courante",
        "routeMatched": "<code>true</code> si une route explicite a correspondu, <code>false</code> pour wildcard/fallback",
        "i18nNs": "Dérivation automatique du namespace i18n à partir du nom de fichier de route",
        "routeViewSrc": "Point de sortie de routage basé sur les fichiers",
        "routeIndex": "Nom de fichier pour la racine <code>/</code> (défaut <code>\"index\"</code>)",
        "routeExt": "Extension de fichier (défaut <code>\".tpl\"</code>, repli <code>\".html\"</code>)",
        "transitionVT": "Preset View Transition API sur route-view (slide, fade, scale, none)"
      },
      "animation": {
        "title": "Animation",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "animate": "Animation d'entrée",
        "animateEnter": "Animation d'entrée",
        "animateLeave": "Animation de sortie",
        "animateDuration": "Durée en ms",
        "animateStagger": "Délai d'échelonnement",
        "transition": "Transition CSS (basée sur les classes, pour les éléments réguliers)",
        "transitionVT": "Preset View Transition API sur route-view (slide, fade, scale, none)"
      },
      "dnd": {
        "title": "Drag and Drop (Maintenant dans NoJS Elements)",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "drag": "Rendre un élément déplaçable",
        "dragType": "Identifiant de type de données",
        "dragEffect": "Effet autorisé (move/copy/link/all)",
        "dragHandle": "Restreindre le glissement au sélecteur de poignée",
        "dragDisabled": "Désactiver le glissement conditionnellement",
        "dragClass": "Classe ajoutée pendant le glissement",
        "dragGroup": "Limiter le glissement à un groupe nommé",
        "drop": "Définir une zone de dépôt",
        "dropAccept": "Type(s) de glissement accepté(s)",
        "dropEffect": "Effet de retour visuel",
        "dropClass": "Classe ajoutée au survol",
        "dropDisabled": "Désactiver le dépôt conditionnellement",
        "dropMax": "Nombre maximum d'éléments dans la zone",
        "dropSort": "Activer le tri positionnel",
        "dropPlaceholder": "Template placeholder pendant le survol",
        "dropSettleClass": "Classe CSS personnalisée pour l'animation de stabilisation",
        "dropEmptyClass": "Classe CSS personnalisée pour l'état vide sur drag-list",
        "dragList": "Liste triable liée à un tableau d'état",
        "dragListKey": "Clé unique pour chaque élément",
        "dragListItem": "Sélecteur de template d'élément",
        "dragListCopy": "Copier au lieu de déplacer lors du transfert",
        "dragListRemove": "Supprimer les éléments de la source lors du transfert",
        "dragMultiple": "Lasso / sélection multiple sur les enfants",
        "dragMultipleClass": "Classe ajoutée aux éléments sélectionnés",
        "dropRejectClass": "Classe ajoutée lors du rejet du survol"
      },
      "i18n": {
        "title": "i18n",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "t": "Traduire une clé",
        "tStar": "Paramètre de traduction",
        "tHtml": "Rendre la traduction en HTML sanitisé"
      },
      "misc": {
        "title": "Divers",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "ref": "Référence d'élément nommée",
        "call": "Déclencher un appel API",
        "trigger": "Émettre un événement personnalisé",
        "use": "Instancier un template",
        "src": "Template distant (voir aussi : lazy)",
        "loading": "Placeholder affiché pendant le chargement du template distant ; supprimé à l'arrivée",
        "include": "Cloner de manière synchrone un template inline à la position courante",
        "errorBoundary": "Limite d'erreur",
        "var": "Nom de variable du template"
      },
      "headManagement": {
        "title": "Head Management",
        "col1": "Directive",
        "col2": "Exemple",
        "col3": "Description",
        "pageTitle": "Définir document.title de manière réactive",
        "pageDescription": "Définir <meta name=\"description\">",
        "pageCanonical": "Définir <link rel=\"canonical\">",
        "pageJsonld": "Définir <script type=\"application/ld+json\">"
      }
    },
    "stateManagement": {
      "state": {
        "title": "state — État local",
        "text": "Crée un contexte réactif limité à l'élément et à ses enfants.",
        "preview": "Aperçu",
        "helloLabel": "Bonjour,",
        "countLabel": "Compteur :",
        "resetBtn": "Réinitialiser"
      },
      "store": {
        "title": "store — Store global",
        "text": "Un store réactif global accessible depuis n'importe où. Idéal pour l'état d'authentification, le thème, les données partagées."
      },
      "configStores": {
        "title": "Pré-initialisation des Stores via config()",
        "text": "Vous pouvez pré-créer des stores avec des données initiales dans NoJS.config(). Chaque clé de l'objet stores devient un store global nommé, accessible immédiatement via $store.nom.",
        "callout": "Si un store existe déjà (ex. créé via un attribut store), config() ne l'écrasera pas — le store existant est préservé."
      },
      "into": {
        "title": "into — Écrire les résultats de Fetch dans un Store",
        "text": "L'attribut into sur n'importe quelle directive HTTP écrit la réponse directement dans un store global nommé.",
        "callout": "Le store n'a pas besoin d'être prédéfini — into le créera s'il n'existe pas."
      },
      "computed": {
        "title": "computed — État dérivé",
        "text": "Des valeurs automatiquement recalculées lorsque les dépendances changent.",
        "preview": "Aperçu",
        "priceLabel": "Prix :",
        "qtyLabel": "Qté :",
        "subtotalLabel": "Sous-total :",
        "totalLabel": "Total :"
      },
      "watch": {
        "title": "watch — Effets de bord",
        "text": "Exécute une action à chaque fois qu'une valeur change."
      },
      "persistence": {
        "title": "Persistance de l'état",
        "text": "Persistez l'état entre les rechargements de page en utilisant localStorage ou sessionStorage."
      },
      "notify": {
        "title": "NoJS.notify() — Mettre à jour le Store",
        "text": "Lorsque du JavaScript externe modifie un store via NoJS.store, appelez NoJS.notify() pour mettre à jour tous les bindings du DOM.",
        "callout": "Nécessaire uniquement lors de la modification de NoJS.store depuis du JavaScript pur — en dehors des expressions HTML. Les modifications dans on:click ou bind sont gérées automatiquement."
      },
      "persistFields": {
        "title": "persist-fields — Persistance Sélective",
        "text": "Utilisez persist-fields pour contrôler exactement quels champs sont sauvegardés et restaurés. Les champs non listés ne sont jamais écrits dans le stockage, ce qui est utile pour garder les valeurs sensibles (tokens, mots de passe) hors de localStorage/sessionStorage.",
        "text2": "persist-fields accepte une liste de noms de champs séparés par des virgules. Les espaces autour de chaque nom sont supprimés, donc \"theme, sidebar\" et \"theme,sidebar\" sont équivalents.",
        "callout": "Omettez persist-fields pour persister tous les champs. Lorsqu'il est présent, seuls les champs listés sont sauvegardés — tous les autres champs utilisent leurs valeurs initiales lors de la restauration."
      },
      "persistSchema": {
        "title": "persist-schema — Validation de Schéma",
        "text": "Lors de la restauration de l'état persisté, persist-schema valide que les clés restaurées correspondent à la forme de l'état initial. Les clés absentes de l'état initial sont écartées. Cela empêche les données obsolètes ou corrompues de casser votre app après des changements de schéma."
      },
      "scoping": {
        "title": "Portée du Contexte & Shadowing",
        "text": "Les contextes héritent des éléments parents comme la portée lexicale. Un state enfant peut masquer une propriété du parent — l'enfant voit sa propre valeur, tandis que les frères voient celle du parent.",
        "callout": "Le shadowing est intentionnel et suit les règles de portée de JavaScript. Si vous avez besoin d'un état partagé entre frères, utilisez un store."
      }
    },
    "dataBinding": {
      "bind": {
        "title": "bind — Contenu texte",
        "text": "Remplace le textContent de l'élément par l'expression évaluée.",
        "filtersNote": "Les expressions dans <code>bind</code> supportent les <a href=\"/docs/filters\">pipe filters</a> pour transformer les valeurs à l'affichage :"
      },
      "bindHtml": {
        "title": "bind-html — HTML interne",
        "text": "Affiche l'expression évaluée en HTML. Nettoyé par défaut.",
        "callout": "⚠️ Utilise un nettoyage structurel intégré basé sur DOMParser : supprime les balises <script>, bloque les gestionnaires d'événements on*, et retire les URI javascript:.",
        "sanitizerTip": "<strong>Astuce :</strong> Vous pouvez fournir un sanitizer personnalisé via <code>NoJS.config({ sanitizeHtml: fn })</code>. Voir <a href=\"/docs/configuration#security\">Configuration &rarr; Sécurité</a> pour plus de détails."
      },
      "bindAttr": {
        "title": "bind-* — Liaison d'attributs",
        "text": "Liez dynamiquement n'importe quel attribut HTML."
      },
      "model": {
        "title": "model — Liaison bidirectionnelle",
        "text": "Pour les champs de formulaire, model crée automatiquement une liaison de données bidirectionnelle.",
        "preview": "Aperçu",
        "nameLabel": "Nom",
        "placeholder": "Tapez votre nom...",
        "checkbox": "J'accepte",
        "agreedLabel": "Accepté :",
        "helloPrefix": "Bonjour,"
      },
      "radioSelect": {
        "title": "Radio Buttons & Multi-Select",
        "text": "model fonctionne avec tous les types d'input. Pour les radio buttons, liez le même nom de model à un groupe — la valeur se met à jour vers le value du radio sélectionné.",
        "multiSelectWarning": "⚠️ Le binding model avec multi-select n'est pas encore implémenté. La directive model actuelle utilise el.value pour les éléments <select> et ne lit pas selectedOptions ni ne gère l'attribut multiple. Un model basé sur un array renverra une seule valeur string, pas un array des valeurs sélectionnées. Cette fonctionnalité est prévue pour une version future. L'exemple ci-dessous est présenté à titre de référence uniquement."
      },
      "commonMistakes": {
        "title": "Erreurs Courantes",
        "text": "Évitez ces pièges courants du data binding :",
        "mistake1": "Utiliser bind-html sans comprendre la sanitisation — tout contenu est sanitisé par défaut, mais soyez conscient de ce qui est supprimé.",
        "mistake2": "Oublier que bind remplace textContent — tous les éléments enfants dans une cible bind sont supprimés.",
        "mistake3": "Utiliser model sur des éléments non-formulaire — model ne fonctionne que sur input, textarea et select."
      }
    },
    "events": {
      "handlers": {
        "title": "on:* — Gestionnaires d'événements",
        "text": "Liez n'importe quel événement DOM directement en HTML. Accédez à l'état et aux variables de contexte directement dans l'expression du gestionnaire.",
        "preview": "Aperçu",
        "youTyped": "Vous avez tapé :",
        "countLabel": "Compteur :"
      },
      "modifiers": {
        "title": "Modificateurs d'événements",
        "text": "Les modificateurs vous permettent de contrôler le comportement des événements directement dans l'attribut :",
        "col1": "Modificateur",
        "col2": "Description",
        "prevent": "Appelle preventDefault() sur l'événement",
        "stop": "Appelle stopPropagation() sur l'événement",
        "once": "Le handler ne s'exécute qu'une seule fois, puis est automatiquement supprimé",
        "self": "Ne se déclenche que si event.target est l'élément lui-même",
        "debounce": "Applique un debounce à l'exécution du handler (ajouter le délai en ms, ex. .debounce.300)",
        "throttle": "Applique un throttle à l'exécution du handler (ajouter l'intervalle en ms, ex. .throttle.200)"
      },
      "eventAndEl": {
        "title": "$event & $el",
        "text": "$event est l'événement DOM natif. $el fait référence à l'élément courant."
      },
      "lifecycle": {
        "title": "Hooks de cycle de vie",
        "col1": "Hook",
        "col2": "Quand",
        "onInit": "Directive traitée pour la première fois",
        "onMounted": "Élément inséré dans le DOM visible",
        "onUpdated": "Mutation du DOM observée (via MutationObserver)",
        "onUnmounted": "Élément retiré du DOM",
        "onError": "Erreur dans le sous-arbre de cet élément"
      },
      "keyModifiers": {
        "title": "Modificateurs de Touche",
        "text": "Utilisez les modificateurs de touche sur les événements clavier pour filtrer par touches spécifiques. Combinez avec Ctrl, Alt, Shift ou Meta pour les raccourcis.",
        "col1": "Modificateur",
        "col2": "Touche",
        "enter": "Entrée / Return",
        "escape": "Échap",
        "space": "Espace",
        "tab": "Tab",
        "backspace": "Retour arrière",
        "arrowUp": "Flèche Haut",
        "arrowDown": "Flèche Bas",
        "arrowLeft": "Flèche Gauche",
        "arrowRight": "Flèche Droite",
        "ctrl": "Ctrl (Contrôle)",
        "alt": "Alt (Option sur Mac)",
        "shift": "Shift (Maj)",
        "meta": "Meta (Cmd sur Mac, Win sur Windows)",
        "delete": "Supprimer"
      }
    },
    "conditionals": {
      "ifThenElse": {
        "title": "if / then / else",
        "text": "Affichez des éléments ou des templates de manière conditionnelle selon des expressions.",
        "preview": "Aperçu",
        "checkbox": "Connecté",
        "welcome": "✅ Bon retour !",
        "login": "Veuillez vous connecter."
      },
      "elseIf": {
        "title": "else-if — Conditions chaînées"
      },
      "showHide": {
        "title": "show / hide",
        "text": "Bascule display: none sans ajouter/supprimer d'éléments DOM. Préférable pour les éléments fréquemment basculés.",
        "comparisonTitle": "if vs show",
        "colIf": "if",
        "colShow": "show",
        "mechanism": "Mécanisme",
        "mechanismIf": "Ajoute/supprime des éléments DOM",
        "mechanismShow": "Bascule l'affichage CSS",
        "bestFor": "Idéal pour",
        "bestForIf": "Contenu rarement basculé",
        "bestForShow": "Contenu fréquemment basculé",
        "preservesState": "Préserve l'état",
        "preservesIf": "Non (recrée)",
        "preservesShow": "Oui"
      },
      "switchCase": {
        "title": "switch / case",
        "text": "Affichez l'un parmi plusieurs templates selon une valeur.",
        "inlineSubtitle": "Contenu inline (sans templates)",
        "multiValueSubtitle": "Case à valeurs multiples"
      },
      "elseOnLoops": {
        "title": "else sur les elements de boucle",
        "text": "Les directives de boucle (foreach, each, for) supportent else=\"templateId\" pour référencer un <template> comme fallback d'état vide. Le template s'affiche quand la liste est vide ([]) ou null/undefined/non-array — par exemple l'état de l'API avant le premier fetch.",
        "callout": "Le pattern else frère pour les boucles a été supprimé en v1.15. Utilisez else=\"templateId\" sur l'élément de boucle lui-même. Consultez la page Boucles pour plus de détails."
      }
    },
    "loops": {
      "foreach": {
        "title": "foreach — Itérer sur des tableaux",
        "text": "La directive d'itération principale. Placez foreach=\"item in array\" sur l'élément qui se répète (self-repeating pattern). L'élément et ses enfants sont clonés pour chaque item. each et for sont des alias avec un comportement identique.",
        "col1": "Attribut",
        "col2": "Description",
        "foreach": "Expression : item in array",
        "from": "Tableau source (DEPRECATED — utilisez la syntaxe item in array)",
        "index": "Nom de la variable d'index (par défaut : $index)",
        "key": "Expression de clé unique pour l'identification et le suivi des éléments",
        "filter": "Expression pour filtrer les éléments",
        "sort": "Chemin de propriété pour le tri (préfixe - pour l'ordre décroissant)",
        "limit": "Nombre maximum d'éléments à afficher",
        "offset": "Nombre d'éléments à ignorer",
        "preview": "Aperçu",
        "template": "ID de template externe (optionnel — les enfants deviennent le template si omis)",
        "elseTpl": "ID de template rendu quand le tableau est vide ou null/undefined (ex : else=\"noItemsTpl\")"
      },
      "fullExample": {
        "title": "Exemple complet avec tous les attributs",
        "text": "Un exemple complet montrant foreach avec filtrage, tri, pagination et un template else pour le secours d'état vide."
      },
      "aliases": {
        "title": "Alias : each et for",
        "text": "each et for sont des alias de foreach — ils partagent le même handler et supportent tous les mêmes attributs."
      },
      "inline": {
        "title": "Self-Repeating Pattern",
        "text": "Placez la directive de boucle sur l'élément répété lui-même. L'élément est cloné pour chaque item du tableau. Utilisez else=\"templateId\" pour référencer un template pour l'état vide."
      },
      "deprecated": {
        "title": "Déprécié : attribut from",
        "text": "L'attribut from fonctionne encore mais est déprécié. Utilisez la syntaxe \"item in array\" à la place. L'utilisation de from émettra un avertissement dans la console."
      },
      "contextVars": {
        "title": "Variables de contexte de boucle",
        "col1": "Variable",
        "col2": "Description",
        "index": "Index courant (base 0)",
        "count": "Nombre total d'éléments",
        "first": "true si premier élément",
        "last": "true si dernier élément",
        "even": "true si l'index est pair",
        "odd": "true si l'index est impair"
      },
      "nested": {
        "title": "Boucles imbriquées",
        "text": "Les boucles enfants (foreach, each ou for) peuvent accéder aux variables de la portée parente."
      },
      "reactivity": {
        "title": "Réactivité",
        "text": "Les directives de boucle sont entièrement réactives. Quand le tableau source change (éléments ajoutés, supprimés ou réordonnés), le DOM se met à jour automatiquement. Utilisez l'attribut key pour un diffing efficace — sans lui, la liste entière est re-rendue à chaque changement."
      },
      "objectIteration": {
        "title": "Itération d'Objets",
        "text": "Pour itérer sur les entrées d'un objet, utilisez le filtre keys ou values pour le convertir en tableau d'abord.",
        "callout": "L'itération directe d'objets n'est pas supportée — convertissez toujours en tableau avec keys, values ou Object.entries() dans une expression computed."
      },
      "elseTpl": {
        "title": "Else avec Référence de Template",
        "text": "Placez else=\"templateId\" directement sur l'élément de boucle. Quand la liste est vide ([]) ou null/undefined, le contenu du template référencé remplace la sortie de la boucle.",
        "callout": "L'attribut else sur l'élément de boucle s'active pour les tableaux vides ([]) et aussi pour les sources null, undefined ou non-array — par exemple l'état de l'API avant la résolution du premier fetch.",
        "breaking": "Breaking change (v1.15) : le pattern else frère pour les boucles a été supprimé. Utilisez else=\"templateId\" sur l'élément de boucle."
      },
      "keyReconciliation": {
        "title": "Réconciliation par clé",
        "text": "Par défaut, quand le tableau source change, la directive de boucle effectue une reconstruction complète — tous les nœuds enfants sont supprimés et recréés de zéro. Quand vous fournissez un attribut key, la directive passe en réconciliation par clé : les nœuds DOM existants pour les éléments dont la clé est encore dans la liste sont réutilisés et leur contexte est mis à jour sur place. Seuls les éléments qui sont réellement apparus ou disparus déclenchent des mutations du DOM.",
        "callout": "La valeur de key doit être unique et stable entre les rendus — typiquement un ID de base de données ou un UUID. Utiliser une clé non unique (ex : $index) annule la réconciliation puisque les éléments sembleront toujours correspondre.",
        "whenTitle": "Quand utiliser key",
        "colUseCase": "Cas d'utilisation",
        "colRecommendation": "Recommandation",
        "staticLists": "Listes statiques, rendues une fois",
        "smallLists": "Listes avec < ~10 éléments, mises à jour peu fréquentes",
        "noKeyNeeded": "Pas besoin de key",
        "largeLists": "Grandes listes (50+ éléments) avec mises à jour fréquentes",
        "useKey": "Utiliser key",
        "mutatingArrays": "push / splice / sort sur des tableaux réactifs",
        "useKeyPreserve": "Utiliser key pour préserver le focus, le scroll et l'état des champs",
        "embeddedInputs": "Éléments avec des champs, vidéo, canvas intégrés",
        "useKeyReset": "Utiliser key — la reconstruction complète réinitialise l'état",
        "positionalTitle": "Métadonnées positionnelles après réordonnancement",
        "positionalText": "Après un réordonnancement (ex : sort), le réconciliateur appelle $notify() sur le contexte de chaque clone conservé. Cela propage les valeurs mises à jour de $index, $first, $last, $even, $odd et $count à tous les watchers enfants, y compris les bindings imbriqués et les expressions de classe qui dépendent de la position."
      },
      "externalTemplate": {
        "title": "Template externe",
        "text": "Utilisez l'attribut template pour référencer un élément <template> par ID. L'élément de boucle reste l'unité répétée — les clones du template référencé sont insérés comme éléments frères."
      },
      "animationAttrs": {
        "title": "Attributs d'animation de boucle",
        "text": "Toutes les directives de boucle (foreach, each, for) supportent les animations d'entrée/sortie et l'échelonnement via ces attributs :",
        "col1": "Attribut",
        "col2": "Description",
        "enter": "Classe CSS ajoutée à chaque nouvel élément lors de l'insertion",
        "leave": "Classe CSS ajoutée aux éléments avant leur suppression",
        "stagger": "Délai (ms) entre l'animation d'entrée de chaque élément",
        "duration": "Durée maximale (ms) avant la fin forcée de l'animation de sortie"
      }
    },
    "templates": {
      "basic": {
        "title": "Template de base",
        "text": "Les templates sont des fragments HTML réutilisables qui ne sont jamais rendus directement. Ils sont clonés lorsqu'ils sont référencés par des directives comme then, else, template, loading, error, etc."
      },
      "var": {
        "title": "Variables de template (var)",
        "text": "Les templates peuvent déclarer quelle variable ils attendent du contexte appelant."
      },
      "slots": {
        "title": "Slots de template",
        "text": "Permettent aux templates d'accepter du contenu projeté."
      },
      "remote": {
        "title": "Templates distants (src)",
        "text": "Chargez des templates depuis des fichiers HTML externes."
      },
      "recursive": {
        "subtitle": "Chargement récursif",
        "text": "Les templates distants sont chargés de manière récursive — si un template distant contient lui-même des éléments <template src=\"...\">, ceux-ci sont automatiquement résolus aussi :"
      },
      "remoteRoutes": {
        "subtitle": "Templates distants dans les routes",
        "text": "Les templates distants à l'intérieur du contenu des routes sont également résolus automatiquement avant que la route ne s'affiche. Voir Routage pour les détails."
      },
      "lazy": {
        "title": "Chargement différé (lazy)",
        "text": "Contrôlez quand les templates distants sont récupérés en utilisant l'attribut lazy sur les éléments <template src=\"...\">. NoJS charge les templates par phases pour optimiser le premier rendu.",
        "col1": "Valeur",
        "col2": "Comportement",
        "absent": "(absent)",
        "absentDesc": "Priorisation automatique par défaut : les templates d'inclusion de contenu et le template de la route courante se chargent avant le premier rendu ; les autres templates de route sont préchargés en arrière-plan après le premier rendu.",
        "priorityDesc": "Forcer le chargement avant tout le reste — même avant les inclusions de contenu classiques. Utile pour les templates de mise en page critiques et partagés.",
        "ondemandDesc": "Valable uniquement sur les templates de route. Jamais préchargé — récupéré uniquement lors de la première navigation de l'utilisateur vers cette route. Idéal pour les pages lourdes ou rarement visitées."
      },
      "phases": {
        "subtitle": "Phases de chargement",
        "text": "Les templates sont résolus en quatre phases ordonnées : la Phase 0 récupère d'abord les templates lazy=\"priority\" ; la Phase 1 récupère tous les autres templates non-route plus le template de la route active (bloquant avant le premier rendu) ; la Phase 2 précharge les templates de route restants en arrière-plan après le premier rendu ; et le chargement à la demande récupère les templates de route lazy=\"ondemand\" uniquement lorsque l'utilisateur navigue pour la première fois vers ceux-ci."
      },
      "loading": {
        "title": "Placeholder de chargement (loading)",
        "text1": "Affichez un template de placeholder pendant qu'un template distant est en cours de chargement. Le placeholder est inséré de manière synchrone — avant toute requête réseau — et supprimé automatiquement une fois le contenu réel arrivé. Fonctionne pour les inclusions de contenu statiques et les templates imbriqués dans les pages de route.",
        "text2": "Les ID simples et la syntaxe #id sont acceptés. Le template de placeholder est cloné à chaque fois, il peut donc être réutilisé pour plusieurs templates distants :"
      },
      "include": {
        "title": "Inclusion de template inline (include)",
        "text1": "Clone un template inline à la position courante de manière synchrone, avant tout fetch. Utile pour injecter du balisage réutilisable (par ex. jeux d'icônes, fragments communs) sans effectuer de requête réseau.",
        "text2": "include et loading ont des objectifs différents : include clone du contenu inline de manière permanente ; loading insère un placeholder temporaire qui disparaît une fois le chargement du template distant terminé."
      }
    },
    "dataFetching": {
      "baseUrl": {
        "title": "URL de base",
        "text1": "Définie une fois sur n'importe quel élément ancêtre. Tous les descendants get, post, etc. résolvent les URL relatives par rapport à celle-ci.",
        "text2": "Surcharge pour des sections spécifiques :",
        "text3": "Les URL absolues ignorent la résolution de base :"
      },
      "config": {
        "title": "Configuration programmatique"
      },
      "headers": {
        "title": "En-têtes par requête"
      },
      "get": {
        "title": "get — Récupérer et afficher des données",
        "attributesTitle": "Attributs",
        "col1": "Attribut",
        "col2": "Type",
        "col3": "Description",
        "get": "URL à récupérer (requête GET)",
        "as": "Nom à attribuer à la réponse dans le contexte. Par défaut : \"data\"",
        "loading": "ID du template à afficher pendant le chargement (ex. \"#skeleton\")",
        "error": "ID du template à afficher en cas d'erreur de fetch",
        "empty": "ID du template à afficher quand la réponse est un tableau vide/null",
        "refresh": "Intervalle de rafraîchissement automatique en ms (polling)",
        "cached": "Mettre en cache les réponses. cached = memory, cached=\"local\" = localStorage, cached=\"session\" = sessionStorage",
        "into": "Écrire la réponse dans un store global nommé",
        "debounce": "Debounce en ms (utile avec les URL réactives)",
        "headers": "Chaîne JSON d'en-têtes supplémentaires",
        "params": "Expression qui résout en objet de paramètres de requête"
      },
      "fullExample": {
        "title": "Exemple complet"
      },
      "reactiveUrls": {
        "title": "URL réactives",
        "text": "Les URL qui référencent des variables d'état se re-fetchent automatiquement lorsque ces valeurs changent."
      },
      "mutations": {
        "title": "post, put, patch, delete — Requêtes de mutation",
        "text": "Utilisées sur des formulaires ou déclenchées via call.",
        "formSubmissionTitle": "Soumission de formulaire",
        "putPatchDeleteTitle": "PUT / PATCH / DELETE"
      },
      "mutationAttrs": {
        "title": "Attributs de mutation",
        "col1": "Attribut",
        "col2": "Description",
        "method": "URL de la requête",
        "body": "Corps de la requête (chaîne JSON avec interpolation). Pour les formulaires, sérialise automatiquement les champs",
        "success": "ID du template à afficher en cas de succès. Reçoit la réponse comme var",
        "error": "ID du template à afficher en cas d'erreur. Reçoit l'erreur comme var",
        "loading": "ID du template à afficher pendant la requête",
        "confirm": "Afficher une boîte de dialogue confirm() du navigateur avant l'envoi",
        "redirect": "URL vers laquelle naviguer en cas de succès (route SPA)",
        "then": "Expression à exécuter en cas de succès (ex. \"users.push(result)\")",
        "into": "Écrire la réponse dans un store global nommé",
        "cached": "Mettre en cache les réponses (memory/local/session). Note : la mise en cache s'applique uniquement aux requêtes GET."
      },
      "lifecycle": {
        "title": "Cycle de vie de la requête"
      },
      "query": {
        "title": "query — Requêtes de Lecture avec Corps (RFC 10008)",
        "text": "La directive <code>query</code> émet une requête HTTP QUERY — une lecture sûre, idempotente et cacheable qui transporte un corps de requête. Elle rend, met en cache et se déclenche automatiquement comme <code>get</code>, mais envoie un corps comme <code>post</code>. Utilisez-la lorsqu'une recherche ou un filtre est trop complexe pour une chaîne de requête d'URL mais reste une lecture, pas une mutation. Aucun token CSRF n'est injecté, car QUERY est une méthode sûre.",
        "formTitle": "Query via Formulaire",
        "cacheNote": "En cache, la clé de cache inclut le corps de la requête — donc deux requêtes QUERY vers la même URL avec des corps différents sont mises en cache séparément. La pagination (get-cursor / get-page / get-insert) reste réservée à GET."
      },
      "liveDemo": {
        "title": "Démo en direct — Fetch API",
        "label": "Résultat"
      },
      "urlEncoding": {
        "title": "Interpolation d'URL et Encodage",
        "text": "Depuis la v1.10, toutes les valeurs <code>{placeholder}</code> dans les attributs d'URL passent automatiquement par <code>encodeURIComponent()</code>. C'est sûr pour les paramètres de requête et les segments de chemin à un seul niveau, mais cela cassera les chemins multi-segments contenant des barres obliques — car <code>/</code> devient <code>%2F</code>. Si votre valeur contient des séparateurs de chemin, concaténez en dehors des accolades."
      },
      "skeleton": {
        "title": "Skeleton Placeholders",
        "text": "L'attribut <code>skeleton</code> référence un élément DOM (par son <code>id</code>) qui sert de placeholder pendant que la requête est en cours. Contrairement à <code>loading=</code>, l'élément skeleton existe dans le markup de la page dès le départ — le rendant compatible SSG et prévenant le Cumulative Layout Shift (CLS).",
        "hiddenWhen": "Le skeleton est automatiquement masqué (via <code>display: none</code>) quand la réponse arrive, quand une méthode autre que <code>GET</code> est utilisée, ou quand la réponse est servie depuis le cache.",
        "combinedTitle": "Skeleton vs. Loading",
        "combinedText": "Vous pouvez utiliser à la fois <code>skeleton=</code> et <code>loading=</code> sur le même élément. Le skeleton s'affiche en premier (pré-rendu), puis se masque quand la requête commence et le template de loading prend le relais jusqu'à l'arrivée de la réponse.",
        "colContent": "Contenu",
        "skeletonContent": "Élément HTML pré-rendu dans la page",
        "loadingContent": "Template injecté par No.JS au moment de l'exécution",
        "colVisibility": "Visibilité",
        "skeletonVisibility": "Visible au chargement de la page, masqué après la réponse",
        "loadingVisibility": "Affiché pendant la requête, masqué après la réponse",
        "colCLS": "Prévention du CLS",
        "skeletonCLS": "Oui — occupe l'espace avant l'exécution du JS",
        "loadingCLS": "Non — injecté après l'initialisation du JS",
        "colSSG": "Compatible SSG",
        "skeletonSSG": "Oui",
        "loadingSSG": "Non",
        "visibilityTitle": "Règles de Visibilité",
        "visibilityText": "Ne masquez pas le skeleton avec du CSS (<code>display: none</code> ou une classe <code>.hidden</code>). No.JS gère la visibilité en basculant le style inline <code>display</code> — si votre CSS masque l'élément en premier, la logique d'affichage/masquage entrera en conflit."
      },
      "pagination": {
        "title": "Pagination & Défilement Infini",
        "text": "No.JS prend en charge la pagination déclarative et le défilement infini grâce à un ensemble d'attributs <code>get-*</code>. Ajoutez <code>get-trigger=\"scroll\"</code> pour le défilement infini ou <code>get-trigger=\"click\"</code> pour un bouton charger plus, combiné avec <code>get-insert=\"append\"</code> pour accumuler les résultats.",
        "seeAlso": "Pour la référence complète de pagination — incluant la pagination par curseur, les libellés de déclencheur personnalisés, la configuration du seuil, et l'événement <code>fetch:end</code> — consultez la page dédiée <a href=\"#pagination\">Pagination & Défilement Infini</a>."
      },
      "interceptors": {
        "title": "Intercepteurs",
        "text": "Les intercepteurs vous permettent de vous brancher sur chaque requête et réponse HTTP effectuée par No.JS. Utilisez-les pour les en-têtes d'authentification, la gestion globale des erreurs, la mise en cache, le logging ou l'annulation de requêtes.",
        "basicTitle": "Utilisation de Base",
        "asyncTitle": "Intercepteurs Asynchrones",
        "asyncText": "Les intercepteurs peuvent être <code>async</code>. No.JS attend chaque intercepteur avec un timeout de 5 secondes — si l'intercepteur ne se résout pas à temps, il est ignoré et un avertissement est journalisé.",
        "cancelTitle": "Annuler des Requêtes (NoJS.CANCEL)",
        "cancelText": "Retournez un objet avec <code>[NoJS.CANCEL]: true</code> depuis un intercepteur de requête pour annuler la requête entièrement. La directive fetch traite cela comme une <code>AbortError</code>.",
        "respondTitle": "Court-Circuiter les Réponses (NoJS.RESPOND)",
        "respondText": "Retournez un objet avec <code>[NoJS.RESPOND]: data</code> depuis un intercepteur de requête pour ignorer l'appel HTTP et utiliser les données fournies directement comme réponse.",
        "replaceTitle": "Remplacer le Corps de Réponse (NoJS.REPLACE)",
        "replaceText": "Retournez un objet avec <code>[NoJS.REPLACE]: data</code> depuis un intercepteur de réponse pour remplacer le corps parsé avec vos propres données.",
        "redactionTitle": "Rédaction des En-têtes",
        "redactionText": "Pour la sécurité, No.JS rédige automatiquement les en-têtes sensibles (<code>Authorization</code>, <code>X-API-Key</code>, <code>Cookie</code>, etc.) dans les logs de debug et lors du passage des données de requête aux intercepteurs non fiables. Seuls les intercepteurs enregistrés avec <code>{ trusted: true }</code> reçoivent les en-têtes originaux.",
        "sentinelTitle": "Résumé des Sentinelles",
        "colSentinel": "Sentinelle",
        "colType": "Type d'Intercepteur",
        "colEffect": "Effet",
        "typeRequest": "Requête",
        "typeResponse": "Réponse",
        "cancelEffect": "Annule la requête (lance AbortError)",
        "respondEffect": "Ignore l'appel HTTP, retourne les données fournies comme réponse",
        "replaceEffect": "Remplace le corps de réponse parsé avec les données fournies"
      }
    },
    "routing": {
      "definition": {
        "title": "Définition des routes"
      },
      "params": {
        "title": "Paramètres de route & requête"
      },
      "context": {
        "title": "$route — Contexte de route",
        "col1": "Propriété",
        "col2": "Description",
        "path": "Chemin courant (ex. \"/users/42\")",
        "params": "Paramètres de route (ex. { id: \"42\" })",
        "query": "Paramètres de chaîne de requête (ex. { q: \"hello\" })",
        "hash": "Hash de l'URL (ex. \"#section\")",
        "matched": "Si une route explicite a correspondu (true) ou si un wildcard/fallback est en cours de rendu (false)"
      },
      "activeStyle": {
        "title": "Style de route active"
      },
      "guards": {
        "title": "Gardes de route"
      },
      "programmatic": {
        "title": "Navigation programmatique",
        "callout": "$router.push() et $router.replace() retournent des Promises — la navigation (y compris le chargement de templates distants) est entièrement asynchrone. Dans les gestionnaires on:click la valeur de retour est ignorée, mais dans les scripts vous pouvez les attendre avec await :"
      },
      "nested": {
        "title": "Routes imbriquées"
      },
      "remoteTemplates": {
        "title": "Templates distants dans les routes",
        "text1": "Les templates de route peuvent inclure <template src=\"...\"> pour charger du contenu depuis des fichiers externes. Ils sont automatiquement résolus avant que la route ne s'affiche :",
        "text2": "Les templates distants imbriqués (un template distant qui contient lui-même d'autres <template src>) sont chargés de manière récursive."
      },
      "fileBased": {
        "title": "Routage basé sur les fichiers",
        "text": "Au lieu de déclarer chaque template de route manuellement, pointez votre sortie <code>route-view</code> vers un dossier. No.JS résoudra automatiquement les chemins de route vers les fichiers templates dans ce dossier.",
        "howItWorks": "Comment ça fonctionne",
        "list1": "Ajoutez <code>route-view</code> à votre élément de sortie — le routage basé sur les fichiers est activé par défaut (config <code>router.templates: \"pages\"</code>). Remplacez par sortie avec <code>src=\"folder/\"</code>.",
        "list2": "Quand un utilisateur navigue vers <code>/analytics</code>, No.JS le résout en <code>pages/analytics.tpl</code>",
        "list3": "Le template est récupéré, mis en cache et rendu — automatiquement",
        "attributesTitle": "Attributs",
        "colAttr": "Attribut",
        "colDefault": "Par défaut",
        "colDesc": "Description",
        "srcDesc": "Dossier de base pour la résolution des templates (remplacement par sortie ; config : <code>router.templates</code>)",
        "routeIndexDesc": "Nom de fichier pour la route racine <code>/</code>",
        "extDesc": "Extension de fichier ajoutée aux segments de route (fallback : <code>\".html\"</code>)",
        "i18nNsDesc": "Lorsque présent, dérive automatiquement le namespace i18n du nom de fichier",
        "callout": "<strong>Config par défaut :</strong> La valeur par défaut de <code>router.templates</code> est <code>\"pages\"</code>, donc le routage basé sur les fichiers fonctionne immédiatement — ajoutez simplement <code>route-view</code> à votre sortie. Remplacez avec <code>NoJS.config({ router: { templates: 'views' } })</code> ou par sortie via <code>src=\"./custom/\"</code>.",
        "exampleTitle": "Exemple — Dashboard SaaS",
        "exampleText": "C'est tout — <strong>deux lignes</strong> pour une SPA complète avec six routes.",
        "mixingTitle": "Mélanger Routes Explicites et Basées sur les Fichiers",
        "mixingText": "Les déclarations explicites <code>&lt;template route=\"...\"&gt;</code> <strong>ont toujours la priorité</strong>. Cela vous permet de combiner les deux approches — utilisez le routage basé sur les fichiers pour les pages simples et les templates explicites pour les routes qui nécessitent des guards, des paramètres ou des sorties nommées :",
        "autoI18nTitle": "Namespace i18n Automatique",
        "autoI18nText": "Lorsque l'élément <code>route-view</code> a un attribut <code>i18n-ns</code> (même sans valeur), No.JS charge automatiquement le namespace i18n correspondant au nom du fichier :",
        "autoI18nText2": "Cela remplace le besoin d'ajouter <code>i18n-ns=\"...\"</code> sur chaque template de route individuellement."
      },
      "lazyLoading": {
        "title": "Chargement différé de templates",
        "text": "L'attribut lazy sur <template src=\"...\"> contrôle quand un template distant est récupéré par rapport au premier rendu. Utilisez-le pour prioriser les templates critiques et différer les pages lourdes ou rarement visitées.",
        "col1": "Valeur",
        "col2": "Phase",
        "col3": "Comportement",
        "absent": "(absent)",
        "absentPhase": "1 ou 2",
        "absentDesc": "Auto : les templates non-route et le template de la route active se chargent avant le premier rendu (Phase 1) ; les autres templates de route se préchargent en arrière-plan après le premier rendu (Phase 2).",
        "priorityPhase": "0",
        "priorityDesc": "Charger avant tout le reste — même avant les inclusions de contenu classiques. À utiliser pour les templates de mise en page critiques et partagés.",
        "ondemandPhase": "à la demande",
        "ondemandDesc": "Valable uniquement sur les templates de route. Jamais préchargé — récupéré lors de la première navigation de l'utilisateur vers cette route. Idéal pour les pages lourdes ou rarement visitées."
      },
      "anchor": {
        "title": "Liens d'ancrage",
        "text1": "En utilisant useHash: true, le hash de l'URL (#) est utilisé pour le routage (ex. #/docs). Cela entre normalement en conflit avec les liens d'ancrage standard comme <a href=\"#section\"> — mais No.JS le gère automatiquement dans les deux modes (hash et history).",
        "text2": "Les liens d'ancrage qui pointent vers un id d'élément sur la page sont interceptés par le routeur : l'élément cible est scrollé en vue de manière fluide, et le lien cliqué reçoit une classe active. La route elle-même n'est pas affectée.",
        "howItWorks": "Comment ça marche :",
        "list1": "Cliquer sur <a href=\"#introduction\"> fait défiler vers <div id=\"introduction\"> avec un comportement fluide",
        "list2": "La classe .active est basculée sur le lien cliqué (et retirée des éléments frères)",
        "list3": "Le chemin de la route courante est préservé — aucune navigation ne se produit",
        "list4": "Les liens avec un attribut route sont toujours traités comme une navigation de route, pas comme des ancres",
        "tip": "Astuce : Stylisez le lien d'ancrage actif avec .active dans votre CSS — le routeur gère la classe pour vous."
      },
      "namedOutlets": {
        "title": "Outlets nommés (route-view)",
        "text": "Plusieurs outlets route-view peuvent coexister sur la même page. Donnez un nom à chaque outlet via la valeur de l'attribut, et dirigez les templates de route vers des outlets spécifiques en utilisant l'attribut outlet.",
        "callout": "Les outlets sans template correspondant pour la route courante sont toujours vidés lors de la navigation."
      },
      "catchAll": {
        "title": "404 / Routes Catch-All",
        "text": "Utilisez <code>route=\"*\"</code> pour définir un template <strong>wildcard catch-all</strong> qui s'affiche lorsqu'aucune route explicite ne correspond au chemin courant. Le wildcard est toujours évalué en dernier, quel que soit l'ordre dans le DOM.",
        "text2": "Les routes explicites <strong>ont toujours la priorité</strong> — le wildcard ne se déclenche que lorsque <code>matchRoute()</code> ne retourne aucune correspondance.",
        "fallbackTitle": "Fallback 404 Automatique",
        "fallbackText": "Si vous ne définissez pas de template <code>route=\"*\"</code>, No.JS affiche automatiquement une page 404 minimale intégrée lorsqu'aucune route ne correspond. Cela garantit que les utilisateurs voient toujours quelque chose de significatif au lieu d'un outlet vide.",
        "fallbackTip": "Le fallback intégré est volontairement minimal et sans style. Définissez votre propre template <code>route=\"*\"</code> pour les applications en production.",
        "namedTitle": "Wildcards dans les Outlets Nommés",
        "namedText": "Chaque outlet nommé peut avoir son propre fallback wildcard. Lorsqu'aucune route ne correspond pour un outlet, le framework résout les fallbacks dans cet ordre :",
        "namedList1": "<strong>Wildcard local</strong> — <code>&lt;template route=\"*\" outlet=\"{name}\"&gt;</code> pour cet outlet spécifique",
        "namedList2": "<strong>Wildcard global</strong> — <code>&lt;template route=\"*\"&gt;</code> (le wildcard de l'outlet par défaut), utilisé uniquement pour les outlets non par défaut",
        "namedList3": "<strong>404 intégré</strong> — la page de fallback minimale du framework",
        "namedText2": "Si la barre latérale n'a pas de wildcard local, elle recourt au <code>route=\"*\"</code> global. Si aucun n'existe, le 404 intégré est utilisé.",
        "matchedTitle": "$route.matched",
        "matchedText": "Le booléen <code>$route.matched</code> vous indique si le chemin courant a trouvé une route explicite (<code>true</code>) ou un wildcard/fallback (<code>false</code>). Utilisez-le pour le rendu conditionnel dans vos templates :",
        "matchedText2": "<code>$route.matched</code> est défini <strong>avant</strong> le rendu du template, il est donc toujours disponible pendant le traitement.",
        "remoteTitle": "Template 404 Distant",
        "remoteText": "Les routes wildcard supportent tous les mêmes attributs que les templates de route classiques, y compris <code>src</code> pour le chargement distant :",
        "remoteText2": "Le template distant est récupéré, mis en cache et rendu comme tout autre template de route — et il a un accès complet à <code>$route.path</code>, <code>$route.matched</code>, et toutes les autres fonctionnalités du framework.",
        "fileBasedTitle": "404 dans le Routage Basé sur les Fichiers",
        "fileBasedText": "Lors de l'utilisation du routage basé sur les fichiers, naviguer vers un chemin dont le fichier <code>.tpl</code> n'existe pas sur le serveur (HTTP 404 ou autre erreur) déclenche automatiquement la chaîne de fallback wildcard.",
        "fileBasedText2": "La réponse HTTP en échec n'est <strong>pas</strong> mise en cache — les navigations ultérieures vers d'autres chemins ne sont pas affectées."
      },
      "headAttributes": {
        "title": "Attributs Head des Routes",
        "text": "Les templates de route peuvent déclarer <code>page-title</code> et <code>page-description</code> directement sur la balise <code>&lt;template&gt;</code>. Quand la route est activée, les balises <code>&lt;head&gt;</code> correspondantes sont mises à jour automatiquement.",
        "text2": "Les chaînes statiques et les expressions dynamiques sont prises en charge :",
        "colAttr": "Attribut",
        "colDesc": "Description",
        "pageTitleDesc": "Définit <code>document.title</code> quand la route est active",
        "pageDescriptionDesc": "Définit le contenu de <code>&lt;meta name=\"description\"&gt;</code> quand la route est active",
        "pageCanonicalDesc": "Définit le href de <code>&lt;link rel=\"canonical\"&gt;</code> quand la route est active",
        "pageJsonldDesc": "Injecte un bloc <code>&lt;script type=\"application/ld+json\"&gt;</code> dans le head du document",
        "callout": "Pour une gestion complète du head (URLs canoniques, JSON-LD, Open Graph), consultez le guide <a href=\"/docs/head-management\">Gestion du Head</a>."
      },
      "deployment": {
        "title": "Déploiement",
        "text": "No.JS utilise l'API History HTML5 par défaut (<code>useHash: false</code>), ce qui produit des URLs propres comme <code>/about</code> et <code>/products/42</code>. Ces URLs nécessitent que votre serveur serve le même <code>index.html</code> pour <strong>chaque route</strong> — sinon, une visite directe à une route côté client retourne un 404 du serveur.",
        "nginxTitle": "nginx",
        "apacheTitle": "Apache",
        "apacheText": "Créez un fichier <code>.htaccess</code> à la racine de votre application :",
        "netlifyTitle": "Netlify",
        "netlifyText": "Créez un fichier <code>_redirects</code> dans votre répertoire de publication :",
        "netlifyToml": "Ou utilisez <code>netlify.toml</code> :",
        "vercelTitle": "Vercel",
        "vercelText": "Créez un <code>vercel.json</code> à la racine de votre projet :",
        "cloudflareTitle": "Cloudflare Pages",
        "cloudflareText": "Créez un fichier <code>_redirects</code> dans le répertoire de sortie de votre projet :",
        "firebaseTitle": "Firebase Hosting",
        "firebaseText": "Dans <code>firebase.json</code> :",
        "hashTitle": "Mode Hash",
        "hashText": "Si vous ne pouvez pas configurer votre serveur (ex. hébergement de fichiers statiques sans règles de réécriture), utilisez le mode hash. Le mode hash produit des URLs comme <code>https://votre-site.com/#/about</code> — elles fonctionnent sur n'importe quel serveur sans configuration, mais <strong>les moteurs de recherche n'indexent pas les fragments hash comme des pages distinctes</strong>.",
        "hashCallout": "No.JS émet un avertissement dans la console lorsque <code>useHash: true</code> est détecté. Pour le supprimer, ajoutez <code>suppressHashWarning: true</code> à la configuration de votre router."
      },
      "focusBehavior": {
        "title": "Accessibilité — Gestion du Focus",
        "text": "Après chaque navigation, No.JS déplace le focus vers un élément prévisible à l'intérieur du contenu de la nouvelle route. Cela garantit que les lecteurs d'écran annoncent le changement de page et que les utilisateurs au clavier atterrissent à un endroit utile.",
        "text2": "Quand focusBehavior est 'auto' (par défaut), le framework parcourt une liste de priorité :",
        "priority1": "<code>[autofocus]</code> — un élément avec l'attribut <code>autofocus</code> à l'intérieur du template de route",
        "priority2": "<code>h1</code> — le premier <code>&lt;h1&gt;</code> à l'intérieur du contenu de la route",
        "priority3": "<code>[tabindex=\"-1\"]</code> — un conteneur focalisable manuellement",
        "priority4": "L'élément route-view lui-même (dernier recours)",
        "defaultTitle": "Comportement par Défaut",
        "defaultText": "La gestion du focus est <strong>activée par défaut</strong> — vous n'avez rien à configurer. La stratégie <code>'auto'</code> fonctionne immédiatement.",
        "timingTitle": "Timing",
        "timingText": "Le focus est déplacé <strong>après</strong> que le template de route soit entièrement rendu et que tous les templates distants à l'intérieur soient résolus. Cela garantit que l'élément cible existe dans le DOM.",
        "sideEffectsTitle": "Effets Secondaires",
        "sideEffectsText": "L'élément focalisé reçoit <code>tabindex=\"-1\"</code> s'il n'a pas déjà un tabindex, et la page défile pour le rendre visible via <code>scrollIntoView({ block: 'nearest' })</code>.",
        "futureTitle": "Valeurs Futures",
        "futureText": "Actuellement seul <code>'auto'</code> est pris en charge. Les versions futures pourraient ajouter <code>'none'</code> (désactiver) et <code>'target'</code> (focaliser un sélecteur spécifique).",
        "ariaLiveTitle": "Région ARIA Live",
        "ariaLiveText": "Pour les lecteurs d'écran qui ne répondent pas aux changements de focus, ajoutez <code>aria-live=\"polite\"</code> à votre <code>route-view</code>. Le navigateur annoncera le nouveau contenu à son apparition :"
      },
      "viewTransitions": {
        "title": "View Transitions",
        "text": "L'attribut <code>transition</code> sur <code>route-view</code> utilise maintenant la <strong>View Transition API</strong> par défaut. Choisissez simplement un nom de preset et les changements de route sont animés nativement par le navigateur — sans CSS manuel.",
        "presetsTitle": "Presets Intégrés",
        "presetsText": "No.JS inclut quatre presets de transition intégrés :",
        "colPreset": "Preset",
        "colEffect": "Effet",
        "presetSlide": "Slide directionnel — le contenu glisse gauche/droite selon la direction de navigation (forward/backward)",
        "presetFade": "Crossfade — l'ancien contenu s'estompe tandis que le nouveau apparaît",
        "presetScale": "Scale zoom — l'ancien contenu se réduit tandis que le nouveau s'agrandit",
        "presetNone": "Changement instantané — sans animation, le contenu est remplacé immédiatement",
        "configTitle": "Configuration",
        "configText": "La View Transition API est <strong>activée par défaut</strong> via <code>router.viewTransition: true</code>. Réglez sur <code>false</code> pour revenir au système legacy de transitions basé sur les classes CSS.",
        "customCssTitle": "CSS Personnalisé",
        "customCssText": "Pour des animations personnalisées, ciblez les pseudo-éléments <code>::view-transition-old(route-content)</code> et <code>::view-transition-new(route-content)</code>. Le <code>view-transition-name: route-content</code> est défini automatiquement sur tout outlet ayant un attribut <code>transition</code>.",
        "howItWorksTitle": "Comment ça Fonctionne",
        "howItWorksText": "Lors d'un changement de route avec un preset <code>transition</code> :",
        "howStep1": "Le router détecte la direction de navigation (<strong>forward</strong> ou <strong>backward</strong>) depuis la pile d'historique",
        "howStep2": "<code>document.startViewTransition()</code> est appelé, capturant l'état actuel de l'outlet",
        "howStep3": "Le nouveau contenu de la route est rendu à l'intérieur de l'outlet",
        "howStep4": "Le navigateur anime entre les snapshots ancien et nouveau en utilisant les règles CSS du preset",
        "deprecationTitle": "Migration depuis les Transitions Basées sur les Classes",
        "deprecationText": "L'ancien système de transitions basé sur les classes (<code>*-enter</code>, <code>*-enter-active</code>, <code>*-leave</code>, etc.) sur <code>route-view</code> est <strong>deprecated</strong>. Il fonctionne toujours quand <code>router.viewTransition</code> est réglé sur <code>false</code>, mais la View Transition API est l'approche recommandée.",
        "deprecationCallout": "<strong>La migration est pratiquement automatique.</strong> Si vous utilisez déjà <code>transition=\"fade\"</code> sur un <code>route-view</code>, il utilise maintenant la View Transition API par défaut — aucun changement de code nécessaire. Votre CSS personnalisé <code>.fade-enter</code> / <code>.fade-leave</code> est simplement ignoré (sauf si vous définissez <code>viewTransition: false</code>)."
      }
    },
    "formsValidation": {
      "submission": {
        "title": "Soumission déclarative de formulaire"
      },
      "rules": {
        "title": "Règles de validation"
      },
      "perRuleErrors": {
        "title": "Messages d'erreur par règle",
        "text": "Utilisez les attributs error-{règle} pour définir un message personnalisé pour une règle spécifique, ou error comme message générique."
      },
      "errorTemplates": {
        "title": "Modèles d'erreur",
        "text": "Pointez un attribut error vers un <template> en utilisant le préfixe # pour afficher une UI d'erreur enrichie. Dans le modèle, $error contient le message et $rule le nom de la règle échouée."
      },
      "errorClass": {
        "title": "Classe CSS d'erreur",
        "text": "Utilisez error-class sur le formulaire ou sur des champs individuels pour basculer une classe CSS quand un champ est invalide et a été touché."
      },
      "formContext": {
        "title": "$form — Contexte de formulaire",
        "text": "À l'intérieur de tout <form> avec l'attribut validate, $form fournit :",
        "col1": "Propriété",
        "col2": "Type",
        "col3": "Description",
        "valid": "true si tous les champs passent la validation",
        "dirty": "true si un champ a été modifié",
        "touched": "true si un champ a reçu puis perdu le focus",
        "submitting": "true pendant que la requête est en cours",
        "pending": "true pendant que les validateurs asynchrones s'exécutent",
        "errors": "Map des noms de champs → messages d'erreur",
        "values": "Valeurs courantes du formulaire",
        "firstError": "Message d'erreur du premier champ invalide (ordre du DOM)",
        "errorCount": "Nombre de champs actuellement en échec de validation",
        "fields": "Objet d'état par champ (valid, error, dirty, touched)",
        "reset": "Réinitialiser le formulaire aux valeurs initiales, effacer erreurs et classes"
      },
      "formFields": {
        "title": "$form.fields — État par champ",
        "text": "$form.fields expose l'état individuel de chaque champ indexé par nom.",
        "asTitle": "Alias de champ avec as",
        "asText": "Utilisez l'attribut as pour exposer l'état d'un champ sous un nom personnalisé dans le contexte."
      },
      "validateOn": {
        "title": "Déclencheurs de validation (validate-on)",
        "text": "Par défaut, la validation s'exécute sur input et focusout. Utilisez validate-on pour changer quand le feedback visuel apparaît.",
        "note": "En interne, les données de $form sont toujours maintenues à jour indépendamment de validate-on. Le déclencheur contrôle uniquement quand le feedback visuel est affiché."
      },
      "validateIf": {
        "title": "Validation conditionnelle (validate-if)",
        "text": "Ignorez la validation d'un champ en fonction d'une condition. Quand validate-if évalue à false, le champ est traité comme valide."
      },
      "autoDisable": {
        "title": "Désactiver automatiquement les boutons de soumission",
        "text": "Les boutons de soumission sont automatiquement désactivés quand le formulaire est invalide. Les boutons avec type=\"button\" ne sont pas affectés."
      },
      "customValidators": {
        "title": "Validateurs personnalisés"
      },
      "liveDemo": {
        "title": "Démo en direct — Formulaire d'inscription",
        "label": "Résultat",
        "usernameLabel": "Nom d'utilisateur",
        "emailLabel": "E-mail",
        "ageLabel": "Âge",
        "roleLabel": "Rôle",
        "roleDefault": "Sélectionnez un rôle",
        "roleDev": "Développeur",
        "roleDesign": "Designer",
        "roleMgr": "Manager",
        "termsLabel": "J'accepte les conditions",
        "registerButton": "S'inscrire",
        "successMessage": "Inscription terminée ! Bienvenue."
      }
    },
    "styling": {
      "classToggle": {
        "title": "class-* — Basculer les classes",
        "text": "Ajoutez ou supprimez des classes CSS de manière réactive en fonction d'expressions. Utilisez class-{name} pour des toggles individuels ou class-map pour plusieurs classes.",
        "multiObject": "Classes multiples depuis un objet",
        "multiObjectText": "Utilisez class-map avec une expression objet pour basculer plusieurs classes. Les clés sont des noms de classes, les valeurs des expressions booléennes.",
        "fromArray": "Depuis un tableau"
      },
      "inlineStyles": {
        "title": "style-* — Styles inline",
        "text": "Définissez des propriétés CSS inline de manière réactive avec des attributs style-{property}. Les noms de propriétés utilisent le kebab-case (ex : style-font-size, style-background-color).",
        "fromObject": "Depuis un objet",
        "fromObjectText": "Utilisez style-map avec une expression objet pour définir plusieurs styles inline en une seule fois."
      },
      "liveDemo": {
        "title": "Démo en direct — Styles dynamiques",
        "label": "Résultat",
        "toggleButton": "Basculer Actif"
      },
      "classStatic": {
        "title": "Interaction avec class Statique",
        "text": "Les directives class-* fonctionnent avec les attributs class statiques. No.JS ne bascule que les classes gérées par la directive — les classes statiques ne sont jamais supprimées.",
        "callout": "Vous pouvez combiner class=\"card\" avec class-active=\"isActive\" en toute sécurité. La classe card est toujours présente ; active est basculée."
      },
      "cssCustomProperties": {
        "title": "CSS Custom Properties",
        "text": "Utilisez style-* avec les CSS custom properties (variables) pour un style réactif avec thèmes. Les noms de propriétés suivent la même convention kebab-case.",
        "callout": "Les CSS custom properties définies via style-* sont scopées à l'élément et héritées par ses enfants — comme les CSS custom properties classiques."
      }
    },
    "animations": {
      "enterLeave": {
        "title": "animate — Animations d'entrée/sortie",
        "attrsTitle": "Attributs d'animation",
        "col1": "Attribut",
        "col2": "Description",
        "row1": "Classe d'animation CSS ajoutée quand l'élément entre",
        "row2": "Classe d'animation CSS ajoutée quand l'élément sort",
        "row3": "Durée en millisecondes transmise à animationDuration et utilisée comme timeout de secours. Si omis, le secours se déclenche au prochain tick de la boucle d'événements (0 ms)",
        "row4": "Délai en millisecondes entre chaque élément dans une boucle (each / foreach)",
        "fallbackCallout": "Timeout de secours — No.JS écoute animationend / transitionend pour nettoyer les classes et déclencher les re-rendus après les animations de sortie. Si l'événement ne se déclenche jamais (ex. CSS absent, élément détaché) un setTimeout de sécurité garantit que le pipeline n'est jamais bloqué indéfiniment. Si animate-duration est omis, le timeout est de 0 ms — il se déclenche au prochain tick de la boucle d'événements sans attente artificielle. Passer un animate-duration=\"300\" explicite définit à la fois animation-duration sur l'élément cible et le timeout de sécurité à 300 ms."
      },
      "transition": {
        "title": "transition — Classes de transition CSS",
        "viewTransitionNote": "<strong>Note :</strong> Les transitions de route sur <code>route-view</code> utilisent maintenant la <a href=\"/docs/routing#view-transitions\">View Transition API</a> par défaut. L'attribut <code>transition</code> basé sur les classes décrit ci-dessous s'applique toujours aux <strong>éléments réguliers</strong> (ex : <code>if</code>, <code>show</code>).",
        "text1": "Suit une convention similaire au système de transition de Vue.",
        "text2": "No.JS ajoute/supprime des classes pendant la transition :",
        "col1": "Classe",
        "col2": "Quand",
        "row1": "État de départ de l'entrée",
        "row2": "État actif de l'entrée",
        "row3": "État final de l'entrée",
        "row4": "État de départ de la sortie",
        "row5": "État actif de la sortie",
        "row6": "État final de la sortie"
      },
      "loopAnimations": {
        "title": "Animations de boucle",
        "text": "Toutes les directives de boucle (foreach, each, for) supportent les animations d'entrée/sortie et l'échelonnement."
      },
      "builtIn": {
        "title": "Noms d'animations intégrés",
        "text": "No.JS inclut ces animations CSS :"
      },
      "viewTransitions": {
        "title": "View Transitions (Navigation de Routes)",
        "text": "No.JS utilise la View Transition API pour des transitions fluides entre les routes. Ajoutez l'attribut transition a votre outlet route-view pour activer la navigation animee nativement par le navigateur.",
        "configTitle": "Configuration",
        "presetsTitle": "Presets integres",
        "colPreset": "Preset",
        "colEffect": "Effet",
        "presetFade": "Fondu enchaine entre l'ancien et le nouveau contenu",
        "presetSlideLeft": "Fait glisser le contenu vers la gauche",
        "presetSlideRight": "Fait glisser le contenu vers la droite",
        "presetSlideUp": "Fait glisser le contenu vers le haut",
        "routingCallout": "Les View Transitions offrent plus d'options incluant du CSS personnalise, l'accessibilite et des details de configuration. Consultez la section <a href=\"/docs/routing#view-transitions\">Routing — View Transitions</a> pour la reference complete."
      },
      "liveDemo": {
        "title": "Démo en direct — Animation de bascule",
        "label": "Résultat",
        "toggleButton": "Basculer",
        "demoText": "Bonjour, Monde Animé ! ✨"
      },
      "a11y": {
        "title": "Accessibilité — Mouvement Réduit",
        "text": "No.JS respecte automatiquement la media query prefers-reduced-motion. Quand l'utilisateur préfère le mouvement réduit, toutes les animations CSS sont désactivées via une règle CSS intégrée qui définit animation-duration et transition-duration à 0.01ms.",
        "callout": "Cela s'applique globalement — aucune configuration par élément n'est nécessaire. Les utilisateurs qui préfèrent le mouvement réduit voient des changements d'état instantanés sans animation."
      }
    },
    "dnd": {
      "drag": {
        "title": "drag — Rendre un Élément Déplaçable",
        "preview": "Aperçu",
        "demoText": "Glissez les fruits dans le panier :",
        "fruitsLabel": "Fruits",
        "basketLabel": "Panier",
        "col1": "Attribut",
        "col2": "Type",
        "col3": "Défaut",
        "col4": "Description",
        "dragDesc": "La valeur glissée",
        "dragTypeDesc": "Type nommé — seules les zones <code>drop-accept</code> correspondantes répondent",
        "dragEffectDesc": "Correspond à <code>dataTransfer.effectAllowed</code>",
        "dragHandleDesc": "Restreint la zone de saisie à un élément enfant",
        "dragImageDesc": "Élément fantôme de glissement personnalisé",
        "dragImageOffsetDesc": "Décalage en pixels pour l'image de glissement personnalisée",
        "dragDisabledDesc": "Lorsque vrai, désactive le glissement",
        "dragClassDesc": "Classe ajoutée pendant le glissement",
        "dragGhostClassDesc": "Classe ajoutée à l'élément image de glissement",
        "dragGroupDesc": "Nom du groupe pour la sélection multiple"
      },
      "drop": {
        "title": "drop — Définir une Zone de Dépôt",
        "preview": "Aperçu",
        "demoText": "Glissez les éléments entre les zones :",
        "zoneALabel": "Zone A",
        "zoneBLabel": "Zone B",
        "col1": "Attribut",
        "col2": "Type",
        "col3": "Défaut",
        "col4": "Description",
        "dropDesc": "Expression exécutée au dépôt",
        "dropAcceptDesc": "Type(s) <code>drag-type</code> accepté(s). Utilisez <code>\"*\"</code> pour tout",
        "dropEffectDesc": "Correspond à <code>dataTransfer.dropEffect</code>",
        "dropClassDesc": "Classe ajoutée lorsqu'un élément valide survole",
        "dropRejectClassDesc": "Classe ajoutée lorsque l'élément est rejeté (mauvais type ou maximum dépassé)",
        "dropDisabledDesc": "Lorsque vrai, désactive le dépôt",
        "dropMaxDesc": "Nombre maximum d'éléments acceptés par la zone",
        "dropSortDesc": "Active le réordonnancement par position",
        "dropPlaceholderDesc": "Affiche un espace réservé au point d'insertion",
        "dropPlaceholderClassDesc": "Classe pour l'espace réservé",
        "dropSettleClassDesc": "Classe CSS personnalisée pour l'animation de stabilisation",
        "dropEmptyClassDesc": "Classe CSS personnalisée pour l'état vide de la zone de dépôt"
      },
      "dragList": {
        "title": "drag-list — Liste Triable",
        "text": "Une directive de haut niveau qui combine <code>drag</code> et <code>drop</code> en une liste triable liée à un tableau d'état.",
        "preview": "Aperçu",
        "todoLabel": "À Faire",
        "doneLabel": "Terminé",
        "col1": "Attribut",
        "col2": "Type",
        "col3": "Défaut",
        "col4": "Description",
        "dragListDesc": "Chemin vers le tableau dans l'état",
        "templateDesc": "Template pour chaque élément",
        "dragListKeyDesc": "Clé unique par élément pour une identité stable",
        "dragListItemDesc": "Nom de la variable de boucle dans le template",
        "dropSortDesc": "Direction du layout",
        "dropAcceptDesc": "Types acceptés (par défaut : même liste)",
        "dragListCopyDesc": "Copier les éléments au lieu de les déplacer",
        "dragListRemoveDesc": "Supprimer les éléments lorsqu'ils sont glissés",
        "dragDisabledDesc": "Désactive le glissement depuis cette liste",
        "dropDisabledDesc": "Désactive le dépôt dans cette liste",
        "dropMaxDesc": "Nombre maximum d'éléments autorisés",
        "dropSettleClassDesc": "Classe CSS personnalisée pour l'animation de stabilisation",
        "dropEmptyClassDesc": "Classe CSS personnalisée pour l'état vide de la drag-list",
        "dropPlaceholderDesc": "Affiche un espace réservé à l'endroit du dépôt"
      },
      "dragListEvents": {
        "title": "Événements de Drag-List",
        "col1": "Événement",
        "col3": "Description",
        "reorderDesc": "Élément réordonné dans la même liste",
        "receiveDesc": "Élément reçu d'une autre liste",
        "removeDesc": "Élément supprimé (glissé hors)",
        "preview": "Aperçu",
        "inboxLabel": "Boîte de réception",
        "archiveLabel": "Archives",
        "eventLogLabel": "Journal des événements :"
      },
      "dragMultiple": {
        "title": "drag-multiple — Sélection Multiple",
        "text": "Activez le clic pour sélectionner sur les éléments enfants, puis glissez tous les éléments sélectionnés en une fois.",
        "preview": "Aperçu",
        "demoText": "Cliquez pour sélectionner, <kbd>Ctrl</kbd>+clic pour multi-sélection, puis glissez vers la zone de dépôt :",
        "availableLabel": "Disponibles",
        "collectedLabel": "Collectés",
        "col1": "Attribut",
        "col2": "Défaut",
        "col3": "Description",
        "dragMultipleDesc": "Active le clic pour sélectionner",
        "dragMultipleClassDesc": "Classe ajoutée aux éléments sélectionnés",
        "dragGroupDesc": "Nom du groupe — tous les éléments sélectionnés se déplacent ensemble",
        "selectionTitle": "Comportement de sélection :",
        "selCol1": "Action",
        "selCol2": "Résultat",
        "selClick": "Clic",
        "selClickResult": "Sélectionne un seul élément (remplace le précédent)",
        "selCtrlClick": "Ctrl/Cmd + Clic",
        "selCtrlClickResult": "Ajoute à la sélection",
        "selEscape": "Échap",
        "selEscapeResult": "Efface toutes les sélections",
        "selDrag": "Glisser un élément sélectionné",
        "selDragResult": "<code>$drag</code> devient un tableau de tous les éléments sélectionnés"
      },
      "implicitVars": {
        "title": "Variables Implicites",
        "text": "Ces variables sont disponibles dans les expressions <code>drop</code> et les gestionnaires <code>on:drop</code> :",
        "col1": "Variable",
        "col2": "Type",
        "col3": "Description",
        "dragDesc": "La valeur glissée. Tableau si multi-sélection",
        "dragTypeDesc": "Le <code>drag-type</code> de l'élément",
        "dragEffectDesc": "Le <code>drag-effect</code>",
        "dropIndexDesc": "Indice d'insertion dans la zone de dépôt",
        "sourceDesc": "<code>{ list, index, el }</code> — info source",
        "targetDesc": "<code>{ list, index, el }</code> — info cible",
        "preview": "Aperçu",
        "demoText": "Chaque élément a un <code>drag-type</code> différent :",
        "dropHere": "Déposez ici pour inspecter"
      },
      "cssClasses": {
        "title": "Classes CSS",
        "text": "Injectées automatiquement par No.JS :",
        "col1": "Classe",
        "col2": "Quand appliquée",
        "draggingDesc": "Sur l'élément source pendant le glissement",
        "dragOverDesc": "Sur la zone de dépôt lorsqu'un élément valide survole",
        "dropRejectDesc": "Sur la zone de dépôt lorsque l'élément est rejeté (mauvais type ou maximum dépassé)",
        "dropPlaceholderDesc": "Sur l'espace réservé d'insertion",
        "selectedDesc": "Sur les éléments multi-sélectionnés",
        "dropSettleDesc": "Brève animation de stabilisation au dépôt",
        "dragListEmptyDesc": "Sur une <code>drag-list</code> lorsqu'elle est vide",
        "preview": "Aperçu",
        "demoText": "Glissez un élément et observez les classes s'appliquer :",
        "dropZoneHint": "Zone de dépôt (max 2) — observez <code>.nojs-drag-over</code> / <code>.nojs-drop-reject</code>"
      },
      "a11y": {
        "title": "Accessibilité",
        "text": "No.JS ajoute automatiquement les attributs ARIA et le support clavier :",
        "col1": "Fonctionnalité",
        "col2": "Détails",
        "draggableDesc": "Défini sur les sources de glissement",
        "ariaGrabbedDesc": "Reflète l'état de glissement (<code>true</code>/<code>false</code>)",
        "ariaDropeffectDesc": "Défini sur les zones de dépôt",
        "roleListboxDesc": "Sur les conteneurs <code>drag-list</code>",
        "roleOptionDesc": "Sur les éléments <code>drag-list</code>",
        "tabindexDesc": "Pour l'accès au clavier",
        "keyboardTitle": "Raccourcis clavier :",
        "keyCol1": "Touche",
        "keyCol2": "Action",
        "spaceDesc": "Saisir l'élément ciblé",
        "escapeDesc": "Annuler le glissement",
        "arrowDesc": "Naviguer entre les éléments pendant le glissement",
        "enterDesc": "Déposer à la position actuelle",
        "preview": "Aperçu",
        "demoText": "Utilisez <kbd>Tab</kbd> pour cibler, <kbd>Espace</kbd> pour saisir, <kbd>↑↓</kbd> pour déplacer, <kbd>Entrée</kbd> pour déposer :"
      }
    },
    "filters": {
      "text": {
        "title": "Filtres de texte"
      },
      "number": {
        "title": "Filtres numériques"
      },
      "array": {
        "title": "Filtres de tableau"
      },
      "date": {
        "title": "Filtres de date"
      },
      "utility": {
        "title": "Filtres utilitaires"
      },
      "object": {
        "title": "Filtres d'objet"
      },
      "chaining": {
        "title": "Chaînage de filtres"
      },
      "custom": {
        "title": "Filtres personnalisés"
      },
      "liveDemo": {
        "title": "Démo en direct — Filtres",
        "label": "Résultat",
        "uppercaseLabel": "Majuscules :",
        "slugifyLabel": "Slugify :"
      },
      "referenceTable": {
        "title": "Référence Complète des Filtres",
        "text": "Les 32 filtres intégrés en un coup d'œil :",
        "col1": "Filtre",
        "col2": "Catégorie",
        "col3": "Description",
        "uppercase": "Convertit le texte en MAJUSCULES",
        "lowercase": "Convertit le texte en minuscules",
        "capitalize": "Met en majuscule la première lettre de chaque mot",
        "truncate": "Tronque le texte à N caractères avec des points de suspension",
        "slugify": "Convertit en slug compatible URL",
        "trim": "Supprime les espaces en début/fin",
        "encodeUri": "Encode un composant URI",
        "currency": "Formate un nombre en devise",
        "number": "Formate un nombre avec groupement par localité",
        "percent": "Formate un nombre en pourcentage",
        "ordinal": "Ajoute un suffixe ordinal (1er, 2e, 3e)",
        "filesize": "Formate les octets en taille lisible",
        "reverse": "Inverse un tableau ou une chaîne",
        "unique": "Supprime les valeurs en double",
        "pluck": "Extrait une propriété de chaque élément",
        "where": "Filtre un tableau par valeur de propriété",
        "sortBy": "Trie un tableau par propriété",
        "first": "Retourne le premier élément d'un tableau",
        "last": "Retourne le dernier élément d'un tableau",
        "date": "Formate un objet Date",
        "datetime": "Formate une Date avec l'heure",
        "relative": "Temps écoulé depuis la date (ex : 'il y a 5 minutes')",
        "fromNow": "Temps jusqu'à une date future (ex : 'dans 3 heures')",
        "json": "Sérialise une valeur en chaîne JSON",
        "default": "Retourne un fallback si la valeur est null/undefined",
        "keys": "Retourne les clés de l'objet en tableau",
        "values": "Retourne les valeurs de l'objet en tableau",
        "count": "Retourne la longueur d'un tableau",
        "join": "Joint les éléments d'un tableau avec un séparateur",
        "stripHtml": "Supprime les balises HTML d'une chaîne",
        "nl2br": "Convertit les sauts de ligne en balises <br>",
        "debug": "Affiche la valeur dans la console et la retourne"
      }
    },
    "i18n": {
      "setup": {
        "title": "Configuration"
      },
      "externalFiles": {
        "title": "Fichiers de locale externes",
        "text": "Au lieu d'intégrer toutes les traductions dans le JavaScript, vous pouvez les charger depuis des fichiers JSON externes. C'est idéal pour les grandes applications avec de nombreuses locales ou quand les traductions sont gérées par un outil séparé.",
        "flatSubtitle": "Mode plat (un fichier par locale)",
        "flatText": "Structure :",
        "nsSubtitle": "Mode namespace (séparé par fonctionnalité)",
        "nsText": "Divisez les traductions par fonctionnalité pour le code-splitting et le chargement à la demande :",
        "nsRouteSubtitle": "Namespace par route",
        "nsRouteText": "Utilisez i18n-ns sur un template de route pour charger un namespace à la demande lorsque la route est visitée :",
        "nsElementSubtitle": "Namespace sur n'importe quel élément",
        "nsElementText": "Utilisez i18n-ns sur n'importe quel élément pour charger un namespace avant que ses enfants ne soient traités :",
        "cachingSubtitle": "Mise en cache",
        "cachingText": "Les fichiers JSON récupérés sont mis en cache en mémoire par défaut. Désactivez avec cache: false pendant le développement :"
      },
      "usage": {
        "title": "Utilisation"
      },
      "tHtml": {
        "title": "Traductions HTML (t-html)",
        "text": "Ajoutez l'attribut t-html pour rendre une valeur de traduction en HTML assaini au lieu de texte brut. C'est utile quand les traductions contiennent du balisage comme des liens, du texte en gras ou autre HTML inline.",
        "callout": "La sortie HTML est passée par _sanitizeHtml() pour prévenir les attaques XSS. Les balises et attributs dangereux sont supprimés automatiquement."
      },
      "formatting": {
        "title": "Formatage des nombres & dates"
      },
      "liveDemo": {
        "title": "Démo en direct — Sélecteur de locale",
        "label": "Résultat",
        "localeLabel": "Locale :"
      },
      "fallback": {
        "title": "Comportement de Fallback",
        "text": "Quand une clé de traduction est absente de la localité actuelle, No.JS recourt à la fallbackLocale (par défaut : identique à defaultLocale). Si la clé est absente des deux, le chemin brut de la clé est affiché.",
        "callout": "Définissez fallbackLocale explicitement pour garantir que les utilisateurs voient toujours du texte significatif, même pour les localités partiellement traduites."
      },
      "detection": {
        "title": "Détection de la Localité du Navigateur",
        "text": "Activez la détection automatique de localité depuis navigator.language du navigateur avec detectBrowser: true. La localité détectée est comparée à vos localités disponibles.",
        "supportedText": "Lors du chargement différé des localités avec loadPath, listez les codes que vous fournissez dans supportedLocales afin que la détection puisse comparer la langue du navigateur avant qu'aucun fichier ne soit récupéré. Priorité à la première visite : un nojs-locale persisté, puis la langue du navigateur détectée, puis defaultLocale.",
        "callout": "Combiné avec persist: true, la localité détectée est sauvegardée pour que les visites suivantes utilisent la même langue sans re-détection."
      }
    },
    "actionsRefs": {
      "call": {
        "title": "call — Déclencher des requêtes API depuis n'importe quel élément"
      },
      "callAttrs": {
        "title": "Attributs",
        "text": "call supporte les mêmes attributs que les directives HTTP (get, post, etc.), y compris les templates de chargement, la redirection, les en-têtes personnalisés et plus.",
        "col1": "Attribut",
        "col2": "Type",
        "col3": "Description",
        "call": "URL de la requête (supporte l'interpolation {variable})",
        "method": "Méthode HTTP. Par défaut : \"get\"",
        "body": "Corps de la requête (chaîne JSON avec interpolation {variable})",
        "headers": "Chaîne JSON d'en-têtes de requête supplémentaires",
        "as": "Nom à attribuer à la réponse dans le contexte. Par défaut : \"data\"",
        "into": "Écrire la réponse dans un store global nommé",
        "loading": "ID du template à afficher pendant la requête (ex. \"#spinner\")",
        "error": "ID du template à afficher en cas d'erreur. Reçoit l'erreur via var",
        "success": "ID du template à afficher en cas de succès. Reçoit la réponse via var",
        "then": "Expression à exécuter en cas de succès (ex. \"items.push(result)\")",
        "redirect": "Route SPA vers laquelle naviguer en cas de succès",
        "confirm": "Afficher une boîte de dialogue confirm() du navigateur avant l'envoi"
      },
      "loadingTpl": {
        "title": "Template de chargement",
        "text": "Affichez un indicateur de chargement pendant que la requête est en cours. L'élément est désactivé pendant le chargement et son contenu est restauré ensuite."
      },
      "customHeaders": {
        "title": "En-têtes personnalisés",
        "text": "Passez des en-têtes par requête sous forme de chaîne JSON :"
      },
      "redirectAfter": {
        "title": "Redirection après succès",
        "text": "Naviguez vers une route SPA après une requête réussie :"
      },
      "abort": {
        "title": "Abort / SwitchMap",
        "text": "Les clics rapides annulent automatiquement la requête précédente en cours avant d'en démarrer une nouvelle, évitant les conditions de concurrence. Seul le résultat du dernier clic est appliqué."
      },
      "events": {
        "title": "Événements",
        "text": "call émet des événements sur le bus d'événements NoJS (écoutez avec NoJS.on(...)) :",
        "col1": "Événement",
        "col2": "Description",
        "fetchSuccess": "{ url, data } en cas de réponse réussie",
        "fetchError": "{ url, error } en cas d'échec",
        "fetchEnd": "{ url } lorsque la requête se termine (succès ou échec)"
      },
      "lifecycle": {
        "title": "Cycle de vie de la requête"
      },
      "trigger": {
        "title": "trigger — Émettre des événements personnalisés"
      },
      "ref": {
        "title": "ref — Références nommées",
        "text": "Accédez aux éléments DOM sans querySelector :"
      },
      "refsMap": {
        "title": "$refs — Map de références",
        "text": "Tous les éléments avec ref sont accessibles via $refs dans la portée courante :"
      },
      "triggerAttrs": {
        "title": "Attributs du Trigger",
        "text": "Personnalisez le comportement du trigger avec des attributs supplémentaires :",
        "col1": "Attribut",
        "col2": "Description",
        "triggerData": "Payload de données attaché à la propriété detail du CustomEvent"
      }
    },
    "customDirectives": {
      "directive": {
        "title": "NoJS.directive()"
      },
      "usage": {
        "title": "Utilisation"
      },
      "webComponents": {
        "title": "Compatibilité Web Components",
        "text": "Les directives No.JS fonctionnent sur les éléments personnalisés :"
      },
      "componentPatterns": {
        "title": "Patterns de type composant avec les templates"
      },
      "priority": {
        "title": "Niveaux de Priorité",
        "text": "La priorité détermine quand votre directive s'exécute par rapport aux directives intégrées. Les nombres plus bas s'exécutent en premier.",
        "col1": "Plage",
        "col2": "Quand",
        "range0": "Avant tout (initialisation de l'état)",
        "range1": "Après l'état, avec la récupération de données",
        "range10": "Avec les directives structurelles (if, each)",
        "range20": "Avec les directives de rendu (bind, on:*)",
        "range30": "Après tout (validation, effets secondaires)"
      },
      "disposal": {
        "title": "Disposal & Cleanup",
        "text": "Les directives personnalisées doivent nettoyer après elles-mêmes. Utilisez le callback _onDispose pour supprimer les event listeners, nettoyer les timers et annuler les watchers.",
        "callout": "Les directives enregistrées après init (via plugins) ne sont pas gelées — mais ne peuvent pas remplacer les directives intégrées."
      }
    },
    "errorHandling": {
      "perElement": {
        "title": "Gestion des erreurs par élément"
      },
      "globalHandler": {
        "title": "Gestionnaire d'erreurs global"
      },
      "errorBoundary": {
        "title": "error-boundary — Capturer les erreurs dans un sous-arbre",
        "text": "Enveloppez une section de votre page avec error-boundary pour capturer les erreurs qui surviennent dans son sous-arbre. Le boundary intercepte deux types d'erreurs :",
        "type1": "Erreurs d'évaluation d'expression — émises comme CustomEvents nojs:error qui remontent depuis les expressions de handlers (ex : une expression bind ou on:click lance une erreur).",
        "type2": "Erreurs de niveau window — erreurs JS non capturées et échecs de chargement de ressources (ex : un <img> 404) provenant d'éléments à l'intérieur du boundary.",
        "httpCaveat": "error-boundary ne capture PAS les requêtes HTTP échouées faites par les directives get/post/put/patch/delete. Utilisez l'attribut error sur l'élément fetch pour la gestion d'erreurs par requête, ou NoJS.on('fetch:error', ...) pour la gestion globale des erreurs HTTP."
      },
      "retry": {
        "title": "Comportement de Retry",
        "text": "Les requêtes HTTP échouées (erreurs 5xx et pannes réseau) sont automatiquement réessayées. Configurez le nombre de retries par élément avec l'attribut retry, et le délai entre les retries avec retry-delay.",
        "callout": "Les retries ne s'appliquent qu'aux erreurs serveur (5xx) et pannes réseau. Les erreurs client (4xx) ne sont pas réessayées."
      },
      "boundaryEvents": {
        "title": "Événements d'Error Boundary",
        "text": "Quand un error boundary capture une erreur, il émet un CustomEvent nojs:error sur l'élément boundary. Écoutez avec on:error pour enregistrer les erreurs ou afficher des notifications.",
        "callout": "L'objet $event.detail contient : message (string), source (element) et error (objet Error original)."
      },
      "expressionErrors": {
        "title": "Erreurs d'Expression",
        "text": "Quand une expression échoue à s'évaluer (ex : accéder à une propriété d'undefined), No.JS capture l'erreur, enregistre un avertissement via _warn() et retourne undefined. Une expression cassée ne fait jamais planter la page entière.",
        "callout": "Activez NoJS.config({ debug: true }) pour voir les avertissements détaillés d'évaluation d'expression dans la console."
      },
      "commonMistakes": {
        "title": "Erreurs Courantes"
      }
    },
    "configuration": {
      "globalSettings": {
        "title": "Paramètres globaux"
      },
      "configStores": {
        "title": "Pré-initialisation des Stores",
        "text": "Utilisez la clé stores dans NoJS.config() pour pré-créer des stores globaux nommés avec des données initiales. Chaque entrée devient un store réactif accessible via $store.nom partout dans votre HTML.",
        "callout": "Les stores créés via config() n'écraseront pas les stores existants — la première définition prévaut."
      },
      "configOptions": {
        "title": "Détails des options de configuration",
        "sanitizeTitle": "sanitize",
        "sanitizeType": "Type : boolean | Par défaut : true",
        "sanitizeText": "Contrôle si le contenu HTML rendu via bind-html est nettoyé par un sanitizer structurel basé sur DOMParser. Lorsqu'activé, toutes les balises et attributs potentiellement dangereux (ex. <script>, onerror) sont supprimés avant l'insertion dans le DOM.",
        "devtoolsTitle": "devtools",
        "devtoolsType": "Type : boolean | Par défaut : false",
        "devtoolsText": "Active le panneau devtools No.JS, accessible via window.__NOJS_DEVTOOLS__. Lorsqu'actif, il expose l'état réactif, les directives enregistrées, les routes actives et les arborescences de composants pour l'inspection dans la console du navigateur.",
        "templatesCacheTitle": "templates.cache",
        "templatesCacheType": "Type : boolean | Par défaut : true",
        "templatesCacheText1": "Contrôle si le contenu HTML des fichiers .tpl récupérés à distance est stocké dans un Map en mémoire après la première requête. Lors des navigations répétées vers la même route, le HTML en cache est utilisé directement et aucune requête HTTP n'est effectuée. Le cache persiste pendant la durée de la session de page (pas de TTL — les assets de template sont statiques).",
        "templatesCacheText2": "Définir à false pendant le développement local si vous souhaitez que les modifications des fichiers .tpl soient reflétées sans rechargement complet de la page.",
        "loadPathTitle": "i18n.loadPath",
        "loadPathType": "Type : string | null | Par défaut : null",
        "loadPathText": "Modèle d'URL pour charger les fichiers JSON de locale via fetch. Utilisez {locale} et optionnellement {ns} comme placeholders. Quand null, les traductions doivent être fournies en ligne via NoJS.i18n({ locales }).",
        "nsTitle": "i18n.ns",
        "nsType": "Type : string[] | Par défaut : []",
        "nsText": "Tableau d'identifiants de namespace à précharger au init(). Chaque namespace correspond à un fichier JSON séparé par locale. Des namespaces supplémentaires peuvent être chargés à la demande via la directive i18n-ns ou l'attribut de route.",
        "cacheTitle": "i18n.cache",
        "cacheType": "Type : boolean | Par défaut : true",
        "cacheText": "Contrôle si les fichiers JSON de locale récupérés sont stockés dans un Map en mémoire après la première requête. Définir à false pendant le développement pour le rechargement à chaud des fichiers de traduction.",
        "i18nPersistTitle": "i18n.persist",
        "i18nPersistType": "Type : boolean | Par défaut : false",
        "i18nPersistText": "Lorsque true, la locale actuellement sélectionnée est persistée dans le localStorage. Au prochain chargement de page, la locale persistée est restaurée automatiquement — utile pour mémoriser la préférence linguistique de l'utilisateur entre les sessions.",
        "exprCacheSizeTitle": "exprCacheSize",
        "exprCacheSizeType": "Type : number | Par défaut : 500",
        "exprCacheSizeText": "Nombre maximal d'entrées dans chacun des deux caches LRU internes utilisés par l'évaluateur d'expressions : un pour les ASTs d'expressions analysées et un pour les ASTs d'instructions analysées. Lorsque la limite est atteinte, l'entrée la moins récemment utilisée est évincée. Augmentez-le si votre application évalue un grand nombre d'expressions de template distinctes ; diminuez-le pour réduire la pression mémoire dans les environnements contraints.",
        "maxEventListenersTitle": "maxEventListeners",
        "maxEventListenersType": "Type : number | Par défaut : 100",
        "maxEventListenersText": "Nombre maximal de fonctions listener autorisées par nom d'événement sur le bus d'événements NoJS (NoJS.on()). Lorsque la limite est atteinte, un avertissement MaxListenersExceeded est journalisé et le nouveau listener est tout de même enregistré. Augmentez cette valeur si votre application utilise légitimement de nombreux listeners pour le même événement.",
        "appIdTitle": "appId",
        "appIdType": "Type : string | Par défaut : \"\"",
        "appIdText": "Un identifiant d'application optionnel. Exposé via le panneau devtools pour distinguer entre plusieurs instances ou environnements NoJS.",
        "sanitizeHtmlTitle": "sanitizeHtml",
        "sanitizeHtmlType": "Type : function | null | Par défaut : null",
        "sanitizeHtmlText": "Hook de sanitisation personnalisé pour le contenu bind-html. Lorsque défini comme fonction, le sanitizer intégré est entièrement contourné et la fonction fournie est utilisée pour tout le contenu bind-html. Utile pour intégrer des sanitizers externes comme DOMPurify.",
        "dangerouslyDisableSanitizeTitle": "dangerouslyDisableSanitize",
        "dangerouslyDisableSanitizeType": "Type : boolean | Par défaut : false",
        "dangerouslyDisableSanitizeText": "Lorsque true, TOUTE la sanitisation du contenu bind-html est contournée. Ceci n'est pas recommandé — cela expose votre application aux attaques XSS. Utilisez sanitizeHtml pour fournir un sanitizer personnalisé à la place.",
        "sanitizeLockCallout": "Les clés de configuration sanitize, dangerouslyDisableSanitize et sanitizeHtml sont verrouillées après la fin de init(). Appeler NoJS.config() après l'initialisation ne modifiera pas ces valeurs — cela empêche les plugins ou les scripts tardifs d'affaiblir la posture de sécurité.",
        "suppressHashWarningTitle": "router.suppressHashWarning",
        "suppressHashWarningType": "Type : boolean | Par défaut : false",
        "suppressHashWarningText": "En mode history (useHash: false), NoJS journalise un avertissement dans la console s'il détecte un fragment hash dans l'URL qui ressemble à un chemin de route. Définir à true pour supprimer cet avertissement.",
        "focusBehaviorTitle": "router.focusBehavior",
        "focusBehaviorType": "Type : string | Par défaut : \"none\"",
        "focusBehaviorText": "Contrôle la gestion du focus après la navigation de route. Lorsque défini à 'auto', NoJS déplace le focus vers un élément prévisible dans le nouveau contenu de route (autofocus, h1, tabindex, ou l'outlet lui-même). Définir à 'none' pour désactiver.",
        "viewTransitionTitle": "router.viewTransition",
        "viewTransitionType": "Type : boolean | Par défaut : true",
        "viewTransitionText": "Contrôle si les transitions de route utilisent l'API View Transition. Lorsque true (par défaut), les changements de route sont animés nativement par le navigateur en utilisant des presets intégrés (slide, fade, scale, none). Définir à false pour revenir au système de transition basé sur les classes."
      },
      "apiProperties": {
        "title": "Propriétés de l'API",
        "baseApiUrlTitle": "NoJS.baseApiUrl",
        "baseApiUrlText": "Getter/setter pour l'URL de base de l'API utilisée par toutes les directives de fetch et les appels NoJS.http. Peut être lue ou réassignée à l'exécution.",
        "versionTitle": "NoJS.version",
        "versionText": "Propriété en lecture seule qui retourne la chaîne de version actuelle du framework No.JS."
      },
      "interceptors": {
        "title": "Intercepteurs de requêtes"
      },
      "security": {
        "title": "Sécurité",
        "xssTitle": "Protection XSS",
        "xssList1": "bind définit toujours textContent, jamais innerHTML — sûr par défaut.",
        "xssList2": "bind-html nettoie le contenu en utilisant un sanitizer structurel intégré basé sur DOMParser (supprime les balises <script>, bloque les gestionnaires d'événements on*, retire les URI javascript:).",
        "xssList3": "Les expressions de template sont évaluées par un parser sandboxé personnalisé — aucun eval() ni Function() n'est utilisé, et les propriétés dangereuses comme __proto__ et constructor sont bloquées.",
        "csrfTitle": "Protection CSRF",
        "cspSecTitle": "Content Security Policy",
        "cspSecText1": "No.JS utilise un parser d'expressions personnalisé entièrement compatible CSP — aucun eval() ni constructeur Function() n'est utilisé. Aucune directive unsafe-eval n'est requise dans votre Content Security Policy."
      }
    },
    "plugins": {
      "use": {
        "title": "NoJS.use()",
        "text": "Enregistrez un plugin avant ou après NoJS.init(). Si l'application est déjà initialisée, le hook init du plugin s'exécute immédiatement.",
        "objectFormTitle": "Forme Objet",
        "objectFormText": "La manière standard de définir un plugin. Fournissez un nom, une fonction install et des hooks de cycle de vie optionnels.",
        "functionTitle": "Raccourci Fonction",
        "functionText": "Pour les plugins simples, passez une fonction nommée. Le nom de la fonction devient le nom du plugin.",
        "functionCallout": "Les fonctions anonymes et les fonctions fléchées sont rejetées — le plugin doit avoir un nom.",
        "optionsTitle": "Options",
        "optionsText": "Le second argument de NoJS.use() est passé à la fonction install du plugin.",
        "optionsTrustedNote": "L'option trusted est spéciale — voir Intercepteurs de Confiance ci-dessous."
      },
      "interface": {
        "title": "Interface du Plugin",
        "thProperty": "Propriété",
        "thType": "Type",
        "thRequired": "Requis",
        "thDescription": "Description",
        "yes": "Oui",
        "no": "Non",
        "nameDesc": "Identifiant unique. Les noms dupliqués sont rejetés.",
        "versionDesc": "Chaîne semver pour le débogage.",
        "capabilitiesDesc": "Capacités déclarées (enregistrées en mode debug).",
        "installDesc": "Appelé de manière synchrone par NoJS.use().",
        "initDesc": "Appelé après que NoJS.init() se termine (le DOM est prêt).",
        "disposeDesc": "Appelé pendant NoJS.dispose() pour le nettoyage.",
        "lifecycleTitle": "Cycle de vie",
        "lifecycleInstall": "install s'exécute immédiatement et de manière synchrone. Utilisez-le pour enregistrer des intercepteurs, des globales, des directives, des écouteurs d'événements et des stores.",
        "lifecycleInit": "init s'exécute après le traitement du DOM et l'activation du routeur. Utilisez-le pour les tâches qui dépendent d'éléments rendus ou de l'état de la route.",
        "lifecycleDispose": "dispose s'exécute pendant la destruction de l'application. Utilisez-le pour fermer les connexions WebSocket, vider les intervalles, envoyer les analytics en attente, etc.",
        "duplicateTitle": "Détection des doublons",
        "duplicateText": "Un nom de plugin ne peut être enregistré qu'une seule fois. Si NoJS.use() est appelé à nouveau avec le même nom mais un objet différent, un avertissement est enregistré et l'appel est ignoré.",
        "freezingTitle": "Gel du registre des directives",
        "freezingText": "Les directives principales (state, bind, get, on:*, etc.) sont gelées après le chargement du framework. Les plugins peuvent enregistrer de nouvelles directives mais ne peuvent pas remplacer celles intégrées."
      },
      "globals": {
        "title": "NoJS.global()",
        "text": "Injecte une variable réactive accessible en tant que $name dans toute expression de template.",
        "namingTitle": "Conventions de nommage",
        "namingPrefix": "Les noms globaux sont accessibles avec un préfixe $ dans les templates : NoJS.global('foo', ...) devient $foo.",
        "namingNamespace": "Utilisez le nom de votre plugin comme namespace pour éviter les collisions : $analytics, $auth, $featureFlags.",
        "reservedTitle": "Noms réservés",
        "reservedText": "Les noms suivants ne peuvent pas être utilisés avec NoJS.global() :",
        "reservedProto": "Les vecteurs de pollution de prototype (__proto__, constructor, prototype) sont également bloqués.",
        "reactivityTitle": "Réactivité",
        "reactivityText": "Les valeurs objet passées à NoJS.global() sont automatiquement enveloppées dans un contexte réactif. Les mutations déclenchent des mises à jour du DOM comme les changements de store ou state.",
        "ownershipTitle": "Suivi de propriété",
        "ownershipText": "Quand un plugin enregistre une globale pendant sa phase install, il en devient le propriétaire. Si un autre plugin écrase cette globale, un avertissement est enregistré."
      },
      "sentinels": {
        "title": "Sentinelles d'intercepteur",
        "text": "Les intercepteurs de requête peuvent retourner des objets spéciaux avec des clés de Symbols sentinelle pour contrôler le pipeline de fetch.",
        "cancelTitle": "NoJS.CANCEL — Annuler la requête",
        "cancelText": "Retournez un objet avec [NoJS.CANCEL]: true pour empêcher l'envoi de la requête. Le fetch lance une AbortError.",
        "respondTitle": "NoJS.RESPOND — Servir une réponse en cache",
        "respondText": "Retournez un objet avec [NoJS.RESPOND]: data pour court-circuiter la requête et retourner data directement comme réponse. Aucune requête HTTP n'est effectuée.",
        "replaceTitle": "NoJS.REPLACE — Remplacer les données de réponse",
        "replaceText": "Retournez un objet avec [NoJS.REPLACE]: data depuis un intercepteur de réponse pour remplacer le corps de réponse analysé par des données personnalisées.",
        "summaryTitle": "Résumé",
        "thSentinel": "Sentinelle",
        "thUsedIn": "Utilisé dans",
        "thEffect": "Effet",
        "cancelUsedIn": "Intercepteur de requête",
        "respondUsedIn": "Intercepteur de requête",
        "replaceUsedIn": "Intercepteur de réponse",
        "cancelEffect": "Annule la requête (lance AbortError)",
        "respondEffect": "Retourne les données directement, ignore l'appel HTTP",
        "replaceEffect": "Remplace le corps de réponse analysé"
      },
      "trusted": {
        "title": "Intercepteurs de confiance",
        "text": "Par défaut, les intercepteurs reçoivent des copies expurgées des requêtes et réponses — les en-têtes sensibles et les paramètres URL sont supprimés ou remplacés par [REDACTED].",
        "fullAccess": "Les plugins d'authentification qui ont besoin d'accéder aux vrais en-têtes peuvent être installés avec { trusted: true }.",
        "callout": "Un avertissement en console est enregistré quand un plugin est installé avec un accès de confiance. N'accordez trusted qu'aux plugins que vous contrôlez ou avez audités.",
        "redactedTitle": "En-têtes expurgés",
        "requestLabel": "En-têtes de requête supprimés avant transmission aux intercepteurs non fiables :",
        "responseLabel": "En-têtes de réponse supprimés du proxy de réponse :"
      },
      "dispose": {
        "title": "NoJS.dispose()",
        "text": "Détruit toute l'application : dispose les plugins, vide les globales et supprime les intercepteurs.",
        "orderTitle": "Ordre de destruction",
        "orderStep1": "Les plugins sont détruits dans l'ordre inverse d'installation (le dernier installé est détruit en premier).",
        "orderStep2": "La fonction dispose de chaque plugin dispose d'un délai de 3 secondes. En cas de dépassement, une erreur est enregistrée et la destruction continue avec le plugin suivant.",
        "orderStep3": "Après la destruction de tous les plugins, les globales et les intercepteurs sont vidés.",
        "asyncTitle": "Dispose asynchrone",
        "asyncText": "Les fonctions dispose des plugins peuvent être asynchrones. Le framework attend chacune (sous réserve du délai de 3 secondes).",
        "callout": "Les plugins ne peuvent pas être installés pendant la destruction. Les appels à NoJS.use() pendant l'exécution de dispose() sont ignorés avec un avertissement."
      },
      "security": {
        "title": "Directives de sécurité",
        "text": "Lors de la création de plugins, suivez ces pratiques pour garder votre application sécurisée.",
        "namespaceTitle": "Namespacez tout",
        "namespaceText": "Préfixez les globales, stores et noms d'événements avec le nom de votre plugin pour éviter les collisions.",
        "evalTitle": "N'utilisez jamais eval ni Function",
        "evalText": "Les valeurs passées à NoJS.global() sont vérifiées pour les références dangereuses. eval et Function sont bloqués.",
        "cleanupTitle": "Nettoyez dans dispose",
        "cleanupText": "Fournissez toujours un hook dispose qui vide les intervalles, ferme les connexions et supprime les écouteurs d'événements.",
        "overwriteTitle": "Évitez de remplacer les globales des autres",
        "overwriteText": "Si votre plugin détecte qu'une globale appartient déjà à un autre plugin, envisagez d'utiliser un nom différent plutôt que de la remplacer silencieusement.",
        "validateTitle": "Validez les options",
        "validateText": "Vérifiez les options requises dans install et avertissez tôt."
      },
      "example": {
        "title": "Exemple complet",
        "text": "Un plugin d'analytics complet démontrant le cycle de vie du plugin, les globales, les intercepteurs et la destruction."
      }
    },
    "headManagement": {
      "intro": {
        "text": "No.JS vous permet de contrôler les métadonnées du <head> de manière déclarative depuis n'importe où dans votre markup. Utilisez des éléments cachés avec les attributs page-title, page-description, page-canonical et page-jsonld — ils sont évalués de manière réactive et mettent à jour le head du document automatiquement.",
        "routingTip": "Pour la gestion du head au niveau des routes, vous pouvez aussi utiliser <code>page-title</code> et <code>page-description</code> directement sur les balises <code>&lt;template route=\"...\"&gt;</code>. Voir <a href=\"/docs/routing#route-head-attributes\">Attributs Head des Routes</a>."
      },
      "placement": {
        "title": "Placement",
        "text": "Placez les éléments de gestion du head à l'intérieur de n'importe quel scope de state. Ils doivent avoir l'attribut hidden — ils ne sont jamais rendus visuellement.",
        "reactive": "Toutes les valeurs sont réactives — quand l'état sous-jacent change, le head du document est mis à jour immédiatement."
      },
      "pageTitle": {
        "title": "page-title",
        "text": "Définit document.title avec la valeur de l'expression évaluée.",
        "expression": "La valeur est une expression No.JS standard évaluée par rapport au contexte de l'élément."
      },
      "pageDescription": {
        "title": "page-description",
        "text": "Définit l'attribut content de la balise <meta name=\"description\">. Si la balise n'existe pas, elle est créée.",
        "duplicate": "Un seul page-description devrait être actif à la fois. Si plusieurs sont présents, le dernier traité l'emporte."
      },
      "pageCanonical": {
        "title": "page-canonical",
        "text": "Définit l'attribut href de la balise <link rel=\"canonical\">. Si la balise n'existe pas, elle est créée.",
        "existing": "Si un lien canonique existe déjà dans votre HTML, la valeur de l'attribut est mise à jour sur place."
      },
      "pageJsonld": {
        "title": "page-jsonld",
        "text": "Injecte un bloc <script type=\"application/ld+json\"> dans le head du document. Le texte interne de l'élément est utilisé comme payload JSON-LD.",
        "scriptNote": "Le contenu est inséré tel quel — assurez-vous que c'est du JSON valide. Utilisez l'interpolation <code>{variable}</code> pour injecter des valeurs dynamiques.",
        "marker": "La balise script générée est marquée avec <code>data-nojs</code> pour pouvoir être identifiée et nettoyée au démontage.",
        "fullExampleTitle": "Exemple Complet de Page Produit"
      },
      "notes": {
        "title": "Notes et Cas Particuliers",
        "competingTitle": "Directives Multiples en Compétition",
        "competingText": "Si plusieurs éléments définissent le même attribut head (ex. deux éléments page-title), le dernier traité l'emporte. Quand un élément est démonté, sa contribution au head est rétablie à la valeur précédente.",
        "cleanupTitle": "Nettoyage au Démontage",
        "cleanupText": "Quand un élément avec une directive head est retiré du DOM (ex. à l'intérieur d'un bloc if ou lors d'un changement de route), sa contribution au head est automatiquement nettoyée.",
        "captureTitle": "Capture du Template JSON-LD",
        "captureText": "Le innerHTML de l'élément page-jsonld est capturé au moment du traitement. Si vous avez besoin de JSON-LD réactif, placez l'élément à l'intérieur d'un scope de state et utilisez l'interpolation {expression}."
      }
    },
    "pagination": {
      "quickStart": {
        "title": "Démarrage Rapide",
        "text": "Ajoutez get-trigger et get-insert à n'importe quelle directive get pour activer la pagination. Voici un exemple minimal d'infinite scroll :",
        "note": "La directive get récupère la page 1 au chargement. Lorsque l'utilisateur fait défiler près du bas, elle récupère automatiquement la page suivante et ajoute les nouveaux éléments."
      },
      "attributes": {
        "title": "Référence des Attributs",
        "col1": "Attribut",
        "col2": "Valeurs",
        "col3": "Par défaut",
        "col4": "Description",
        "getInsert": "Contrôle où le nouveau contenu est placé par rapport au contenu existant. Sans cet attribut, le contenu récupéré remplace entièrement le contenu actuel.",
        "getTrigger": "Contrôle quand le fetch est déclenché. Sans cet attribut, le fetch se déclenche immédiatement au montage.",
        "getPage": "Numéro de page initial pour la pagination basée sur l'offset. La variable de contexte page s'incrémente automatiquement après chaque fetch réussi.",
        "getCursor": "Active la pagination basée sur un cursor. Le cursor est extrait de l'en-tête de réponse (X-Cursor) ou du champ du corps JSON.",
        "getCursorField": "Chemin personnalisé du champ JSON pour l'extraction du cursor en notation par points (ex. pagination.nextToken). Sans cela, No.JS recherche cursor, next_cursor, nextCursor, next.",
        "getThreshold": "rootMargin de l'IntersectionObserver. Contrôle à quel point le fetch se déclenche tôt avant que l'élément n'entre dans le viewport.",
        "getTriggerLabel": "Texte personnalisé pour le bouton load more généré automatiquement lors de l'utilisation de get-trigger=\"button\"."
      },
      "examples": {
        "title": "Exemples en Direct",
        "infiniteScroll": {
          "title": "Infinite Scroll",
          "desc": "Charge automatiquement la page suivante lorsque l'utilisateur fait défiler près du bas de la liste.",
          "label": "Résultat en Direct"
        },
        "loadMore": {
          "title": "Bouton Load More",
          "desc": "Affiche un bouton sur lequel l'utilisateur clique pour charger la page suivante de résultats.",
          "label": "Résultat en Direct"
        },
        "lazyLoad": {
          "title": "Lazy Load au Défilement",
          "desc": "Le contenu est récupéré uniquement lorsque l'élément entre dans le viewport par défilement. Idéal pour les sections sous la ligne de flottaison.",
          "label": "Résultat en Direct"
        },
        "hoverPrefetch": {
          "title": "Hover Prefetch",
          "desc": "Le contenu est récupéré au premier événement mouseenter. Utile pour les tooltips et les cartes de prévisualisation.",
          "label": "Résultat en Direct",
          "hoverMe": "Survolez cette carte pour charger le contenu de prévisualisation"
        },
        "programmatic": {
          "title": "Fetch Programmatique",
          "desc": "Le fetch ne se déclenche pas automatiquement. Utilisez $refs pour le déclencher manuellement via un bouton ou un autre événement.",
          "label": "Résultat en Direct",
          "clickToLoad": "Cliquez sur le bouton pour déclencher le fetch manuellement."
        },
        "cursorPagination": {
          "title": "Cursor Pagination",
          "desc": "Utilise la pagination basée sur un cursor au lieu des numéros de page. Le cursor est extrait du corps de la réponse JSON.",
          "label": "Résultat en Direct"
        },
        "liveFeed": {
          "title": "Live Feed (Prepend)",
          "desc": "Le nouveau contenu est inséré en haut de la liste, simulant un flux en direct ou un défileur d'actualités.",
          "label": "Résultat en Direct"
        }
      },
      "matrix": {
        "title": "Matrice de Composition",
        "text": "Les attributs get-trigger et get-insert se combinent pour former une matrice 5x3 de comportements. Voici comment chaque combinaison fonctionne :",
        "col1": "Trigger",
        "col2": "Sans insert (replace)",
        "col3": "append",
        "col4": "prepend",
        "immediateReplace": "Comportement par défaut — le fetch remplace le contenu au montage.",
        "immediateAppend": "Fetch au montage, ajoute le résultat sous le contenu existant.",
        "immediatePrepend": "Fetch au montage, insère le résultat au-dessus du contenu existant.",
        "scrollReplace": "Revient au comportement visible (scroll nécessite le mode insert).",
        "scrollAppend": "Infinite scroll — ajoute de nouvelles pages au fur et à mesure que l'utilisateur défile vers le bas.",
        "scrollPrepend": "Infinite scroll — insère de nouvelles pages au fur et à mesure que l'utilisateur défile vers le haut.",
        "buttonReplace": "Revient au comportement immediate (button nécessite le mode insert).",
        "buttonAppend": "Bouton load more — ajoute la page suivante au clic.",
        "buttonPrepend": "Bouton load more — insère la page suivante au clic.",
        "visibleReplace": "Lazy load — récupère et remplace le contenu lorsque l'élément entre dans le viewport.",
        "visibleAppend": "Lazy load — récupère et ajoute le contenu lorsque l'élément entre dans le viewport.",
        "visiblePrepend": "Lazy load — récupère et insère le contenu lorsque l'élément entre dans le viewport.",
        "hoverReplace": "Prefetch — récupère et remplace le contenu au premier mouseenter.",
        "hoverAppend": "Prefetch — récupère et ajoute le contenu au premier mouseenter.",
        "hoverPrepend": "Prefetch — récupère et insère le contenu au premier mouseenter.",
        "noneReplace": "Manuel — pas d'auto-fetch, utilisez $refs.refresh() pour déclencher.",
        "noneAppend": "Manuel — pas d'auto-fetch, $refs.refresh() ajoute.",
        "nonePrepend": "Manuel — pas d'auto-fetch, $refs.refresh() insère."
      },
      "endOfData": {
        "title": "Détection de Fin de Données",
        "text": "No.JS détecte la fin des données paginées de trois manières, selon le format de la réponse :",
        "emptyBody": "Corps de réponse vide",
        "emptyBodyText": "Lorsque le serveur renvoie un corps de réponse vide (Content-Length: 0 ou chaîne vide), No.JS arrête la pagination et masque le bouton load more.",
        "lastPageHeader": "En-tête X-NoJS-Last-Page",
        "lastPageHeaderText": "Le serveur peut envoyer un en-tête de réponse X-NoJS-Last-Page: true pour signaler explicitement qu'il n'y a plus de pages.",
        "nullCursor": "Cursor nul",
        "nullCursorText": "Pour la pagination basée sur un cursor, lorsque le champ du cursor dans la réponse JSON est nul ou absent, No.JS arrête la pagination."
      },
      "templates": {
        "title": "Templates avec Pagination",
        "text": "Les templates de loading, error et empty fonctionnent parfaitement avec la pagination. Le template de loading est affiché pendant chaque fetch de page, et le template d'error est affiché si une page échoue.",
        "loadingNote": "Pendant la pagination (pas lors du premier fetch), un petit indicateur de loading inline est affiché au bord d'insertion au lieu de remplacer tout le contenu.",
        "emptyNote": "Le template empty n'est affiché que lorsque la première page ne renvoie aucune donnée. Les pages suivantes vides arrêtent simplement la pagination."
      },
      "scrollPosition": {
        "title": "Préservation de la Position de Scroll",
        "text": "Lors de l'utilisation de get-insert=\"prepend\", No.JS préserve automatiquement la position de scroll de l'utilisateur. Le nouveau contenu est inséré au-dessus du viewport actuel, et l'offset de scroll est ajusté pour que la vue de l'utilisateur ne saute pas."
      }
    },
    "directiveCompatibility": {
      "overview": {
        "title": "Directive Compatibility",
        "text": "No.JS processes directives by priority order (state 0 > HTTP 1 > computed/watch 2 > ref 5 > structural 10 > rendering/events 20 > validation 30). Most directives compose freely, but certain same-element combinations require awareness of processing order and scope mechanics. This page documents every known interaction.",
        "callout": "Combinations marked \"Defined semantics\" work correctly but follow specific rules you should understand. Combinations marked \"Use workaround\" have a known limitation with a documented alternative pattern."
      },
      "matrix": {
        "title": "Compatibility Matrix",
        "structuralTitle": "Structural + Structural (same element)",
        "conditionalTitle": "Conditional + Other Directives (same element)",
        "loopTitle": "Loop + Other Directives (same element)",
        "bindingTitle": "Binding + Binding (same element)"
      },
      "semantics": {
        "title": "Defined Semantics (v1.18.0)",
        "ifGates": {
          "title": "if gates the entire element",
          "text": "As of v1.18.0, if gates the entire element, not just its children. This means all directives on the same element respect the if condition:",
          "http": "HTTP directives (get, post, etc.) on the same element only fire when the condition is true. Zero network requests while false.",
          "computed": "Computed values and watch side effects on the same element only run when the condition is true. No phantom computations.",
          "head": "Head directives (page-title, page-description, page-canonical, page-jsonld) only apply when the condition is true.",
          "binding": "Binding, model, and event directives on the same element are gated in the same way."
        },
        "perClone": {
          "title": "Per-clone computed/watch in loops",
          "text": "When computed or watch appears on a loop element, each clone gets its own independent instance. A computed creates a per-item derived value scoped to that clone's context. A watch fires independently per clone when the watched property changes."
        },
        "usePriority": {
          "title": "use priority 9",
          "text": "The use directive runs at priority 9 (just below structural directives at 10), ensuring templates are instantiated before if/each processing."
        },
        "httpPlacement": {
          "title": "HTTP verb placement",
          "text": "HTTP directives (get, post, put, patch, delete) should be placed on a parent element, not on the loop element itself. The loop element is used as a clone template — placing an HTTP directive on it would fire one request per clone."
        }
      },
      "limitations": {
        "title": "Known Limitations",
        "switchLoop": {
          "title": "switch + loop on case children",
          "text": "Placing a loop directive (each/foreach/for) on a case child of a switch renders both branches; the switch logic goes inert. Workaround: wrap the loop in a container element inside the case."
        },
        "ifLoop": {
          "title": "if + loop on the same element",
          "text": "if and a loop directive on the same element cannot coordinate — the condition cannot remove items, and empty shells may render. Workaround: wrap the loop in a container element with the if."
        },
        "elseIfLoop": {
          "title": "else-if + loop on the same element",
          "text": "else-if combined with a loop on the same element breaks the conditional chain. Items render regardless of the preceding if condition. Additionally, a sibling else element is orphaned. Workaround: wrap the loop in a container element with the else-if."
        },
        "duplicateRef": {
          "title": "Duplicate ref in loop",
          "text": "Using ref on a loop element means each clone overwrites the same ref name. Only the last clone's element is accessible via $refs. Workaround: use bind-ref with a dynamic expression to create unique ref names per clone."
        },
        "bindValueModel": {
          "title": "bind-value + model on the same input",
          "text": "Both bind-value and model set up two-way binding pipelines with input listeners. The two handlers can fight over the value during typing, especially for number inputs. Workaround: use only model for two-way binding."
        },
        "watchOnChange": {
          "title": "watch + on:change on the same element",
          "text": "Both watch and on:change claim the change event, leading to duplicate handler execution. Workaround: use only one mechanism — watch for reactive side effects or on:change for DOM event handling."
        },
        "tBind": {
          "title": "t + bind double writer",
          "text": "Both t (i18n translation) and bind write to the element's textContent. The attribute processed last wins. Workaround: use t with interpolation parameters (t-name, t-count) or use bind alone."
        },
        "httpOnLoop": {
          "title": "HTTP verb on loop element",
          "text": "Placing an HTTP directive (get, post, etc.) directly on the loop element causes one network request per clone, since clones retain the HTTP attribute. Workaround: place the HTTP directive on a parent element."
        }
      },
      "migration": {
        "title": "Migration Notes (v1.18.0)",
        "ifGates": {
          "title": "if now gates the entire element",
          "before": "Before v1.18.0, if only controlled children visibility. Directives with higher priority than if (such as HTTP at priority 1, computed/watch at priority 2, and head directives at priority 1) would still execute on the element even when the condition was false.",
          "after": "After v1.18.0, if gates the entire element. All directives on the same element respect the if condition.",
          "check": "If you relied on a falsy if branch to still compute values or fire HTTP requests, move those directives to a separate element without an if condition."
        },
        "usePriority": {
          "title": "use priority changed to 9",
          "text": "The use directive now has an explicit priority of 9, placing it just below structural directives (10). This ensures templates are instantiated before if/each processing. This change is backward-compatible for standard usage."
        }
      }
    },
    "sse": {
      "basicTitle": "Utilisation de base",
      "basicText": "La directive sse ouvre une connexion EventSource vers un endpoint serveur et expose les données reçues comme variable de contexte réactive via l'attribut as. Les payloads JSON sont parsés automatiquement ; les données non-JSON sont stockées comme chaîne de caractères.",
      "text": "Lorsque le serveur envoie des données non-JSON (texte brut, lignes CSV, etc.), la variable as reçoit la chaîne brute.",
      "attributesTitle": "Attributs",
      "col1": "Attribut",
      "col2": "Type",
      "col3": "Description",
      "sse": "URL de l'endpoint SSE. Supporte les expressions interpolées (ex : /api/{channel}).",
      "as": "Nom de la variable de contexte réactive qui reçoit les données de chaque message.",
      "sseEvent": "Type d'événement SSE nommé à écouter. Sans cet attribut, seuls les messages sans nom (default) sont capturés.",
      "sseInsert": "Mode de mise à jour de la variable : replace (par défaut), append (tableau du plus ancien au plus récent) ou prepend (tableau du plus récent au plus ancien).",
      "sseLimit": "Longueur maximale du tableau en mode append ou prepend. Les entrées les plus anciennes sont supprimées lorsque la limite est dépassée. N'a aucun effet en mode replace (déclenche un avertissement en console).",
      "sseCredentials": "Attribut booléen. Lorsque présent, l'EventSource est ouvert avec withCredentials: true, envoyant les cookies sur les requêtes cross-origin.",
      "into": "Nom du store. Lorsque défini, chaque message est également écrit dans le store global nommé sous la clé as.",
      "error": "ID d'un élément <template> à afficher lorsque la connexion échoue définitivement (readyState === CLOSED).",
      "then": "Expression exécutée après le traitement de chaque message. Reçoit $event (les données parsées).",
      "insertModesTitle": "Modes d'insertion",
      "insertModesText": "L'attribut sse-insert contrôle comment les messages successifs mettent à jour la variable de contexte. Replace est le mode par défaut, idéal pour les flux à valeur unique comme les cotations. Append et prepend collectent les messages dans un tableau, limité par sse-limit. Utiliser append ou prepend sans sse-limit déclenche un avertissement en console concernant une croissance mémoire potentiellement illimitée. Définir sse-limit en mode replace n'a aucun effet et déclenche également un avertissement.",
      "namedEventsTitle": "Événements nommés",
      "namedEventsText": "Par défaut, la directive écoute les messages SSE sans nom (l'événement message). Définissez sse-event pour écouter un type d'événement nommé spécifique. Plusieurs éléments peuvent s'abonner à différents types d'événements sur le même endpoint — chacun ouvre sa propre connexion EventSource.",
      "connectionStateTitle": "État de connexion ($sse)",
      "connectionStateText": "Chaque élément sse expose un objet réactif $sse dans son contexte avec trois propriétés booléennes reflétant le readyState de l'EventSource.",
      "stateCol1": "Propriété",
      "stateCol2": "Type",
      "stateCol3": "Description",
      "connecting": "true pendant que l'EventSource est en état CONNECTING (connexion initiale ou reconnexion automatique).",
      "open": "true lorsque la connexion est ouverte et reçoit des messages.",
      "errorProp": "true uniquement lorsque la connexion a échoué définitivement (readyState === CLOSED). Pendant la reconnexion automatique, error reste false.",
      "statesCallout": "Pendant la reconnexion automatique, l'EventSource du navigateur passe à l'état CONNECTING. NoJS reflète cela : $sse.connecting devient true, $sse.open devient false et $sse.error reste false. Le flag d'erreur ne se déclenche que lorsque la connexion ne peut pas être rétablie (CLOSED).",
      "liveFeedTitle": "Exemple de flux en temps réel",
      "liveFeedText": "Combinez le mode d'insertion prepend, une limite et des indicateurs d'état de connexion pour un flux d'activité en temps réel complet avec statut de reconnexion automatique.",
      "reactiveUrlTitle": "URLs réactives",
      "reactiveUrlText": "Lorsque l'URL du sse contient des expressions interpolées, la directive surveille les changements. Si l'URL résolue change, l'EventSource actuel est fermé et un nouveau est ouvert vers l'endpoint mis à jour. En mode append/prepend, les données accumulées sont réinitialisées à un tableau vide. En mode replace (par défaut), la valeur précédente persiste jusqu'à l'arrivée du premier message sur la nouvelle connexion.",
      "storeTitle": "Intégration avec le Store",
      "storeText": "Ajoutez l'attribut into pour écrire chaque message dans un store global en plus de la variable de contexte locale. D'autres éléments n'importe où dans le DOM peuvent lire les données via $store.nomDuStore.",
      "errorHandlingTitle": "Gestion des erreurs",
      "errorHandlingText": "Pointez l'attribut error vers un <template> pour afficher un contenu de secours lorsque la connexion se ferme définitivement. Le template reçoit un objet synthétique avec un seul champ message (\"SSE connection closed\") — pas l'objet d'erreur du navigateur. Le template d'erreur ne s'affiche que lorsque readyState atteint CLOSED (fermeture terminale) ; il n'est jamais affiché pendant la reconnexion automatique.",
      "thenTitle": "Callback par message",
      "thenText": "L'attribut then accepte une expression qui s'exécute après le traitement de chaque message et la mise à jour de la variable de contexte. Les données parsées du message sont disponibles comme $event. Utilisez-le pour les effets secondaires comme incrémenter des compteurs ou déclencher des actions.",
      "noLoadingTitle": "Pas de template de chargement",
      "noLoadingText": "Contrairement aux directives HTTP (get, post), la directive sse ne supporte pas d'attribut de template de chargement. Les connexions SSE sont de longue durée — il n'y a pas de cycle unique requête/réponse. Utilisez $sse.connecting pour afficher un indicateur de statut pendant l'établissement de la connexion.",
      "authTitle": "Authentification",
      "authText": "L'API EventSource ne supporte pas les headers personnalisés. Deux stratégies d'authentification sont disponibles : les tokens via query string et l'authentification basée sur les cookies.",
      "authCallout": "L'attribut <code>sse-credentials</code> définit <code>withCredentials: true</code> sur l'EventSource, envoyant les cookies sur les requêtes cross-origin. Le serveur doit répondre avec les headers CORS appropriés (<code>Access-Control-Allow-Credentials: true</code>, <code>Access-Control-Allow-Origin</code> explicite).",
      "interceptorsNote": "Les intercepteurs fetch de NoJS ne s'appliquent pas aux connexions SSE. EventSource est une API navigateur distincte — l'injection de headers, le cache et la logique de retry du pipeline fetch ne sont pas disponibles.",
      "connectionLimitTitle": "Limites de connexion",
      "connectionLimitText": "Les navigateurs imposent une limite d'environ 6 connexions HTTP/1.1 concurrentes par origine. Chaque élément sse ouvre sa propre connexion EventSource — les connexions ne sont jamais partagées ni fusionnées, même lorsque plusieurs éléments pointent vers la même URL. NoJS émet un avertissement lorsqu'une 6e connexion vers la même origine est ouverte. Pour rester dans les limites, utilisez HTTP/2 (qui multiplexe les flux sur une seule connexion TCP), réduisez le nombre d'éléments sse sur la page, ou fermez les connexions inutilisées via le rendu conditionnel (ex : une directive if)."
    }
  }
}
