# Acadenice Patches Liste des patches custom appliques sur le fork Acadenice de Docmost. Ce document est maintenu manuellement pour faciliter le rebase upstream. Repo upstream : `github.com/docmost/docmost` Branche fork : `acadenice/main` ## Conventions - Chaque patch est commit isole avec scope `feat(rebrand)` / `feat(custom)` / etc. - Les modifications in-line de fichiers upstream sont documentees ici avec rationale. - Les nouveaux fichiers (extensions Tiptap custom, hooks, etc.) vont dans des emplacements dedies pour minimiser les conflits de rebase. --- ## Patch 015 — R3.7 @user mentions + notifications in-app + email **Date** : 2026-05-08 **Scope** : mention detection bridge (REST API path), Acadenice notification facade API, /notifications page, /settings/notifications preferences page, i18n FR+EN ### Architecture Decision The native Docmost system already provides the complete mention notification pipeline: - `persistence.extension.ts` (collab path) extracts `entityType:"user"` mention nodes via `extractUserMentions`, queues `PAGE_MENTION_NOTIFICATION` jobs. - `PageNotificationService.processPageMention()` handles RBAC checks (space + page access), deduplication (old vs new mentions), `notification.create()` + email send. - `NotificationPopover` + bell icon are already in the header. - Notification preferences are stored in `users.settings.notifications` (native `NotificationPref` component in `/settings/account/preferences`). R3.7 adds: 1. `MentionDetectorService` — pure service that walks Tiptap JSON and extracts user mentions (no DB). Used by the emitter and independently testable. 2. `NotificationEmitterService` — listens to `acadenice.page.content.updated` (REST API save path, not collab) and queues `PAGE_MENTION_NOTIFICATION`. Bridges the gap between the collab-only native detection and pages saved via REST (templates instantiate, import, etc.). 3. `AcadeniceNotificationsController` — facade over native `NotificationService`, prefix `/api/acadenice/notifications`. 4. `NotificationPreferencesController` — GET/PUT `/api/acadenice/notification-preferences` (reads/writes native `users.settings.notifications`). 5. Frontend `/notifications` page — full inbox using native `NotificationItem` component. 6. Frontend `/settings/notifications` preferences page — dedicated toggles via Acadenice API. **No new DB migration** — native `notifications` table covers `page.user_mention` fully. **Poll 30s** for unread count (vs SSE bridge) — sufficient for notification freshness. ### Nouveaux fichiers backend | Fichier | Role | |---------|------| | `apps/server/src/core/acadenice/notifications/notifications.module.ts` | NestJS module | | `apps/server/src/core/acadenice/notifications/dto/notification.dto.ts` | Zod schemas | | `apps/server/src/core/acadenice/notifications/services/mention-detector.service.ts` | Tiptap mention walker (pure) | | `apps/server/src/core/acadenice/notifications/services/notification-emitter.service.ts` | Event listener -> PAGE_MENTION_NOTIFICATION queue | | `apps/server/src/core/acadenice/notifications/services/notification-preferences.service.ts` | Read/write users.settings.notifications | | `apps/server/src/core/acadenice/notifications/controllers/notifications.controller.ts` | REST facade /api/acadenice/notifications | | `apps/server/src/core/acadenice/notifications/controllers/notification-preferences.controller.ts` | REST /api/acadenice/notification-preferences | | `apps/server/src/core/acadenice/notifications/spec/mention-detector.service.spec.ts` | 18 unit tests | | `apps/server/src/core/acadenice/notifications/spec/notifications.controller.spec.ts` | 10 unit tests | | `apps/server/src/core/acadenice/notifications/spec/notification-preferences.spec.ts` | 4 unit tests | ### Nouveaux fichiers frontend | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/notifications/services/notifications-client.ts` | Axios HTTP client | | `apps/client/src/features/acadenice/notifications/queries/notifications-query.ts` | React Query hooks + 30s poll | | `apps/client/src/features/acadenice/notifications/pages/notifications-page.tsx` | Route /notifications | | `apps/client/src/features/acadenice/notifications/pages/notification-preferences-page.tsx` | Route /settings/notifications | | `apps/client/src/features/acadenice/notifications/__tests__/notifications-client.test.ts` | 9 client tests | | `apps/client/src/features/acadenice/notifications/__tests__/notifications-page.test.tsx` | 4 page render tests | ### Fichiers upstream modifies (patches) | Fichier | Modification | |---------|-------------| | `apps/server/src/core/core.module.ts` | +AcadeniceNotificationsModule import + registration | | `apps/client/src/App.tsx` | +import AcadeniceNotificationsPage, NotificationPreferencesPage + routes /notifications, /settings/notifications | | `apps/client/src/components/settings/settings-sidebar.tsx` | +entree "Notifications" (IconBell) dans group Account | | `apps/client/public/locales/en-US/translation.json` | +20 cles acadenice.notifications.* | | `apps/client/public/locales/fr-FR/translation.json` | +20 cles acadenice.notifications.* | ### Endpoints ``` GET /api/acadenice/notifications paginated list GET /api/acadenice/notifications/unread-count unread badge count (polled 30s) POST /api/acadenice/notifications/read-all mark all read POST /api/acadenice/notifications/mark-read bulk mark read POST /api/acadenice/notifications/:id/read single mark read GET /api/acadenice/notification-preferences get prefs PUT /api/acadenice/notification-preferences update prefs ``` ### Tests - Backend : 18 tests MentionDetectorService + 10 tests controller + 4 tests preferences = 32 tests - Frontend : 9 tests client + 4 tests page render = 13 tests - Total R3.7 : 45 tests ### Points a debattre avec Corentin 1. **REST path mention dedup** : `NotificationEmitterService` passe `oldMentionedUserIds: []` — toutes les mentions existantes sont reenvoyees a chaque save REST. La native `processPageMention` filtre les self-mentions mais pas les doubles (si le doc est re-sauvegarde sans changer les mentions, une double notif est creee). Solution : passer l'ancienne content snapshot dans l'event payload `acadenice.page.content.updated`. Patch mineur post-R3.7. 2. **Collab path vs REST path** : en collab (hocuspocus), la detection est deja faite par `PersistenceExtension`. En REST, le `NotificationEmitterService` prend le relais. Les deux peuvent coexister sans conflit car la deduplication native se base sur `oldMentionedUserIds` (diff). Mais si une page est sauvee en collab ET en REST dans la meme session, un doublon est possible. Acceptable pour v1. 3. **SSE optionnel** : le bell count est actuellement poll 30s. Si Corentin veut sub-seconde, brancher le bridge SSE (R3.7+ — note dans SESSION-RESUME). 4. **Notification preferences granularite** : les prefs in-app et email partagent la meme cle dans `users.settings.notifications` (native impl). Pour une granularite fine (ex: email ON, in-app OFF), il faudrait soit etendre le schema JSON soit creer une table `acadenice_notification_preferences` dediee. A evaluer selon le besoin utilisateur. ### Prochaine etape R3.8 — comments inline (threads sur paragraphes Tiptap + sur rows database, resolu/non-resolu, notif R3.7 sur reply via hook dedie). --- ## Patch 014 — R3.6 page templates system **Date** : 2026-05-08 **Scope** : workspace page templates — DB table, backend module, frontend gallery + picker modal, built-in seed, 26 permissions, i18n FR+EN **Rationale** : permet de sauvegarder des pages existantes comme templates, de lister les templates du workspace en gallery filtrable, et d'instancier un nouveau template depuis la sidebar "New page" ou le slash `/template`. ### Architecture - Table DB : `acadenice_template` (workspace-scoped, UNIQUE workspace_id+name, JSONB content, is_built_in, is_workspace_default, usage_count). - Backend `AcadeniceTemplatesModule` (NestJS) : TemplateService + TemplateSeedService + TemplatesController. - 6 endpoints : `GET/POST /api/acadenice/templates`, `GET/PATCH/DELETE /api/acadenice/templates/:id`, `POST /api/acadenice/templates/:id/instantiate`, `PATCH /api/acadenice/templates/:id/default`. - Built-in seed : 5 templates au boot (Meeting Note, Project Brief, Daily Standup, Weekly Review, Empty Page). is_built_in = true, clone-then-edit pattern. - 3 nouvelles permissions : `templates:read`, `templates:create`, `templates:manage` (catalogue passe a 26). - Frontend : gallery grid filtrable par categorie + search, TemplateForm (create/edit), TemplatePicker modal (sidebar New page + slash /template). - i18n : 39 cles EN + 39 cles FR. ### Nouveaux fichiers backend | Fichier | Role | |---------|------| | `apps/server/src/database/migrations/20260508T140000-create-acadenice-template.ts` | Migration up/down table + index | | `apps/server/src/core/acadenice/templates/templates.module.ts` | NestJS module | | `apps/server/src/core/acadenice/templates/dto/template.dto.ts` | Zod schemas + TypeScript types | | `apps/server/src/core/acadenice/templates/services/template.service.ts` | CRUD + instantiate + setDefault | | `apps/server/src/core/acadenice/templates/services/template-seed.service.ts` | 5 built-in templates seed au boot | | `apps/server/src/core/acadenice/templates/controllers/templates.controller.ts` | REST controller | | `apps/server/src/core/acadenice/templates/spec/template.service.spec.ts` | 22 tests service | | `apps/server/src/core/acadenice/templates/spec/templates.controller.spec.ts` | 18 tests controller | ### Nouveaux fichiers frontend | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/templates-admin/services/templates-client.ts` | Axios client HTTP | | `apps/client/src/features/acadenice/templates-admin/queries/templates-query.ts` | React Query hooks | | `apps/client/src/features/acadenice/templates-admin/components/template-card.tsx` | Card single template | | `apps/client/src/features/acadenice/templates-admin/components/template-form.tsx` | Modal create/edit | | `apps/client/src/features/acadenice/templates-admin/components/template-gallery.tsx` | Grid gallery + filtres | | `apps/client/src/features/acadenice/templates-admin/pages/templates-page.tsx` | Page /settings/templates | | `apps/client/src/features/acadenice/templates/components/template-picker-modal.tsx` | Picker modal + hook useInstantiateTemplate | | `apps/client/src/features/acadenice/templates-admin/__tests__/templates-client.test.ts` | 9 tests client | | `apps/client/src/features/acadenice/templates-admin/__tests__/templates-page.test.tsx` | 9 tests page | | `apps/client/src/features/acadenice/templates-admin/__tests__/template-card.test.tsx` | 7 tests card | ### Fichiers upstream modifies (patches) | Fichier | Modification | |---------|-------------| | `apps/server/src/core/core.module.ts` | +AcadeniceTemplatesModule import + registration | | `apps/server/src/core/acadenice/rbac/permissions-catalog.ts` | +3 permissions templates:read/create/manage (26 total) | | `apps/server/src/core/acadenice/rbac/services/seed.service.ts` | +templates:read/create/manage aux roles Admin/Editor/Member/Guest | | `apps/client/src/App.tsx` | +import TemplatesAdminPage + route /settings/templates | | `apps/client/src/components/settings/settings-sidebar.tsx` | +entree "Templates" + import IconTemplate | | `apps/client/src/features/space/components/sidebar/space-sidebar.tsx` | "New page" -> dropdown (New page / From template), import TemplatePickerModal | | `apps/client/src/features/editor/components/slash-menu/menu-items.ts` | +slash /template -> CustomEvent acadenice:open-template-picker | | `apps/client/public/locales/en-US/translation.json` | +39 cles templates.* | | `apps/client/public/locales/fr-FR/translation.json` | +39 cles templates.* | ### Catalogue permissions a jour (26 permissions) ``` pages:read|write|delete|share, space:read|create|write|delete|invite, tables:list|create|write|delete, rows:read|write|delete, attachments:upload|delete, users:invite|write|delete, roles:manage, slash_commands:manage (R3.3), templates:read|create|manage (R3.6 - nouveaux), admin:* ``` ### Tests - Backend : 22 tests service + 18 tests controller = 40 tests (unit, mocked DB) - Frontend : 9 tests client + 9 tests page + 7 tests card = 25 tests - Total R3.6 : 65 tests ### Points a debattre avec Corentin 1. **Positionnement instantiate dans l'arbre** : la methode `getNextPagePosition` utilise une approche simple (append suffixe '0'). Pour une integration propre dans le Tiptap tree, il faudra utiliser `fractional-indexing-jittered` comme le fait `PageService`. Patch mineur post-R3.6. 2. **Instantiate depuis le picker** : le custom event `acadenice:open-template-picker` dispatche sur `document`. Cela fonctionne si le composant ecoutant (ex. Page.tsx) subscribe au mount. Alternativement, utiliser un atom jotai global `templatePickerOpenAtom` pour plus de robustesse. Bonne option pour R3.7 refactor. 3. **Cover image** : `cover_url` est optionnel (URL externe). Pas d'upload prevu dans ce sub-bloc. Si un template a une cover, elle s'affiche en CSS `background-image` — a implementer dans la card si Corentin le souhaite. 4. **Built-in vs custom template** : le pattern "clone-then-edit" n'est pas encore expose en UI (pas de bouton "Clone" sur les built-ins). A ajouter facilement dans la TemplateCard (action "Duplicate as custom"). ### Prochaine etape R3.7 — mentions `@user` + notifs in-app : mention declenche notif + email, center notifs UI + bell icon. --- ## Patch 013 — R3.5.2 frontend graph view (page /graph, react-force-graph-2d) **Date** : 2026-05-08 **Scope** : knowledge graph frontend — page `/graph`, force-directed canvas, controls sidebar, side panel **Rationale** : rend le graphe de liens inter-pages interactif (style Obsidian/AFFiNE). Consomme `GET /api/acadenice/graph` (R3.5.1). Finalise R3.5 entierement. ### Architecture - Lib : `react-force-graph-2d` (Canvas-based, d3-force, jusqu'a 5k nodes interactifs). Peer deps : `d3-force`. A installer : `pnpm add react-force-graph-2d d3-force`. - Chargee en dynamic import (`React.lazy`) avec fallback placeholder si lib absente. - Filter state : jotai atoms (`graphFiltersAtom`, `selectedNodeIdAtom`, `focusNodeIdAtom`, `sidePanelOpenAtom`). - Data : React Query hook avec 300ms debounce sur les filtres. - Side panel : Mantine Drawer ancre a droite. - Navigation : `useNavigate` vers `/p/{slugId}` (slugId map a etendre quand le backend inclura `slugId` dans `GraphNode`). ### Route `/graph` — workspace-level, accessible via sidebar (icone `IconAffiliate`). ### Nouveaux fichiers | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/graph/services/graph-client.ts` | HTTP client fetchGraph | | `apps/client/src/features/acadenice/graph/hooks/use-graph-controls.ts` | Jotai atoms filtres + UI state | | `apps/client/src/features/acadenice/graph/hooks/use-graph-data.ts` | React Query hook + debounce | | `apps/client/src/features/acadenice/graph/components/graph-canvas.tsx` | Canvas force-graph wrapper | | `apps/client/src/features/acadenice/graph/components/graph-controls.tsx` | Sidebar filtres + stats | | `apps/client/src/features/acadenice/graph/components/graph-node-tooltip.tsx` | Card tooltip node hover | | `apps/client/src/features/acadenice/graph/components/graph-side-panel.tsx` | Drawer detail node | | `apps/client/src/features/acadenice/graph/pages/graph-page.tsx` | Page orchestratrice | | `apps/client/src/features/acadenice/graph/__tests__/graph-client.test.ts` | 10 tests | | `apps/client/src/features/acadenice/graph/__tests__/use-graph-controls.test.ts` | 16 tests | | `apps/client/src/features/acadenice/graph/__tests__/use-graph-data.test.ts` | 8 tests | | `apps/client/src/features/acadenice/graph/__tests__/graph-controls.test.tsx` | 11 tests | | `apps/client/src/features/acadenice/graph/__tests__/graph-canvas.smoke.test.tsx` | 5 smoke tests | | `apps/client/src/features/acadenice/graph/__tests__/graph-side-panel.test.tsx` | 8 tests | ### Fichiers upstream modifies (patches) | Fichier | Modification | |---------|-------------| | `apps/client/src/App.tsx` | +import GraphPage + route `/graph` dans `` | | `apps/client/src/components/layouts/global/global-sidebar.tsx` | +import IconAffiliate + entree `graph.page_title` dans mainNavItems | | `apps/client/public/locales/en-US/translation.json` | +24 cles `graph.*` | | `apps/client/public/locales/fr-FR/translation.json` | +24 cles `graph.*` (traductions FR) | ### Tests - 58 tests total (10 graph-client + 16 use-graph-controls + 8 use-graph-data + 11 graph-controls + 5 canvas-smoke + 8 graph-side-panel) - Canvas smoke : verifie le rendu sans crash quand react-force-graph-2d est absent (fallback) - Hooks : debounce behavior, state transitions jotai, React Query staleTime ### Nouvelles deps (a installer) ``` react-force-graph-2d ^1.43.x (peer: d3-force ^3.0.x) d3-force ^3.0.x ``` ### Choix techniques - Dynamic import (`React.lazy`) : isole la dep Canvas du bundle critique. Fallback `` affiche les instructions d'installation si la lib est absente. - Jotai vs useState local : les 4 atoms sont cross-composant (canvas <-> controls <-> side panel). Un Context React aurait necessit un provider supplementaire ; jotai reste hors de l'arbre JSX. - `spaceColorCache` module-level : mapping deterministe spaceId -> couleur Mantine palette. Reset automatique au rechargement de page (pas de persistence necessaire). - slugId map actuellement vide : le backend GraphNode (R3.5.1) n'expose pas `slugId`. Extension possible sans casser le contrat : ajouter `slugId?: string` a GraphNode. ### Point a debattre avec Corentin 1. **slugId dans GraphNode** : faut-il enrichir le backend (R3.5.1) pour inclure `slugId` dans chaque node ? Permettrait l'activation du bouton "Open page" et la navigation double-click. Patch mineur service + DTO cote backend. 2. **react-force-graph-2d version exacte** : pinner a la derniere stable avant install (`pnpm add react-force-graph-2d@latest d3-force@latest --filter docmost-client`). 3. **Context menu right-click** : actuellement le right-click fait "Focus mode" (recenter). Un vrai context menu (Mantine Menu) avec "Open in new tab" / "Focus" / "Copy link" est possible mais necessite un overlay positionne sur le canvas (hors Mantine portals). --- ## Patch 012 — R3.5.1 backend graph endpoint GET /api/acadenice/graph **Date** : 2026-05-08 **Scope** : knowledge graph backend — nodes + edges from acadenice_backlink table **Rationale** : expose workspace link-graph as JSON for the R3.5.2 frontend (Obsidian-style graph view). Reads the `acadenice_backlink` table populated by R3.2. ### Architecture - Source de verite : table `acadenice_backlink` (indexee par R3.2 sur chaque save). - Permission filter : meme join space_members / visibility='public' que BacklinkService. - BFS iteratif en memoire (apres chargement des edges) pour les graphes centres (pageId). - Cache Redis TTL 60s par cle composite. Invalidation sur evenement `acadenice.page.content.updated` (meme event que R3.2). - Truncation a 1000 nodes pour les workspaces larges (top-inDegree + flag `truncated: true`). ### Endpoint `GET /api/acadenice/graph?workspaceId=X&spaceId=Y&pageId=Z&depth=N&types=wikilink,mention,database_embed&includeOrphans=false` - `workspaceId` : ignore — resolu depuis le JWT pour eviter les fuites cross-workspace - `spaceId` : optionnel, filtre les nodes au space - `pageId` : optionnel, centre le graphe + BFS depth hops - `depth` : 1-5, default 2 - `types` : filtre par type de lien (defaut: tous les 3) - `includeOrphans` : default false ### Reponse ```ts { nodes: Array<{ id, label, type, spaceId, spaceName, icon, isOrphan, metrics: { inDegree, outDegree } }>, edges: Array<{ id, source, target, type, weight }>, meta: { totalNodes, totalEdges, workspaceId, rootPageId?, depth?, truncated } } ``` ### Fichiers crees | Fichier | Role | |---------|------| | `apps/server/src/core/acadenice/graph/graph.module.ts` | NestJS module (R3.5.1) | | `apps/server/src/core/acadenice/graph/dto/graph.dto.ts` | Zod schemas + interfaces response | | `apps/server/src/core/acadenice/graph/services/graph.service.ts` | buildGraph, BFS, Redis cache | | `apps/server/src/core/acadenice/graph/controllers/graph.controller.ts` | GET /api/acadenice/graph | | `apps/server/src/core/acadenice/graph/spec/graph.service.spec.ts` | Tests service (21 tests) | | `apps/server/src/core/acadenice/graph/spec/graph.controller.spec.ts` | Tests controller (14 tests) | ### Fichiers modifies (patches upstream) | Fichier | Modification | |---------|-------------| | `apps/server/src/core/core.module.ts` | +AcadeniceGraphModule import + declaration | ### Tests - 35 tests total (21 service + 14 controller) - Service : full graph, edge weight, BFS depth=1/2, spaceId filter, types filter, permission filter, truncation@1000, orphans inclus/exclus, inDegree/outDegree, cache hit, cache invalidation, bfsReachable unit, buildCacheKey, error resilience - Controller : routing, params parsing (depth/spaceId/pageId/types/includeOrphans), validation errors (bad UUID, depth>5, depth<1), workspace isolation ### Strategies techniques - SQL : GROUP BY (source_page_id, target_page_id, link_type) avec COUNT(*) pour weight. Double join pages/spaces (source ET target) avec permission check inline. - BFS : iteratif avec Set visited. Graphe non-oriente (in+out edges). Cap MAX_NODES=1000. - Cache key : `acadenice:graph::::::` - Invalidation : pattern `acadenice:graph::*` via KEYS + DEL. Acceptable pour TTL=60s. --- ## Patch 011 — R3.4 dual editor (WYSIWYG + markdown source) **Date** : 2026-05-08 **Scope** : toggle WYSIWYG <-> raw markdown source, custom-node round-trip **Rationale** : permet aux utilisateurs power-users d'editer le source markdown directement, avec une conversion aller-retour complete preservant les nodes Acadenice custom (database-view, wikilink, mention). ### Architecture - Source de verite : Tiptap JSON (persiste en DB). Le markdown est une vue. - Mode persist : localStorage `acadenice:editor-mode:` par page. - Switch lossy : modal de confirmation listant les elements alteres. - Save : en mode markdown, le doc Tiptap est maintenu sync (setContent) a chaque keystroke pour que le mecanisme save Docmost natif reste fonctionnel. ### Syntaxe custom nodes en markdown | Node | Syntaxe markdown | |------|-----------------| | `database-view` | `[[!db tableId=X viewId=Y viewType=Z]]` | | `wikilink` | `[[Page Title]]` ou `[[Page Title\|alias]]` | | `mention` | `@(displayName)` | Choix : tokens entre `[[...]]` pour etre lisibles et reversibles. Le prefixe `!db` distingue les database-view des wikilinks. Les mentions encodent le userId (UUID) pour eviter la necessite d'une resolution serveur au re-parse. ### Fichiers crees | Fichier | Role | |---------|------| | `apps/client/src/features/acadenice/dual-editor/services/custom-node-serializers.ts` | Registre des serializers custom (databaseView, wikilink, mention) | | `apps/client/src/features/acadenice/dual-editor/services/markdown-converter.ts` | `tiptapToMarkdown` + `markdownToTiptap` — converter custom sans dep externe | | `apps/client/src/features/acadenice/dual-editor/hooks/use-editor-mode.ts` | Jotai atom `editorModeAtom` + `useEditorMode` hook + `initEditorMode` | | `apps/client/src/features/acadenice/dual-editor/components/mode-toggle-button.tsx` | Bouton toggle (IconCode / IconEye) dans la toolbar | | `apps/client/src/features/acadenice/dual-editor/components/markdown-editor.tsx` | Textarea monospace auto-resize (Tab -> 2 espaces) | | `apps/client/src/features/acadenice/dual-editor/components/dual-editor.tsx` | Wrapper WYSIWYG / markdown avec modal warning lossy | | `apps/client/src/features/acadenice/dual-editor/__tests__/markdown-converter.test.ts` | 61 tests round-trip (JSON->MD->JSON et MD->JSON->MD) | | `apps/client/src/features/acadenice/dual-editor/__tests__/custom-node-serializers.test.ts` | 12 tests unitaires serializers | | `apps/client/src/features/acadenice/dual-editor/__tests__/use-editor-mode.test.ts` | 4 tests persistence localStorage | ### Fichiers modifies (patches upstream) | Fichier | Modification | |---------|-------------| | `apps/client/src/features/editor/full-editor.tsx` | +import DualEditor + wrap `` avec `` | | `apps/client/public/locales/en-US/translation.json` | +8 cles `dual_editor.*` | | `apps/client/public/locales/fr-FR/translation.json` | +8 cles `dual_editor.*` traduits | ### Nouvelles dependances requises Aucune. Le converter est custom TypeScript pur. L'editeur markdown utilise une `