diff --git a/src/Controller/ApiLabelController.php b/src/Controller/ApiLabelController.php
index 6c628c50d9fd812daad813ea982ae0cd5daecc12..af573a85c15fb184e7ff64bc6c9ad3768a5ba135 100644
--- a/src/Controller/ApiLabelController.php
+++ b/src/Controller/ApiLabelController.php
@@ -4,9 +4,11 @@ namespace App\Controller;
 
 use App\Entity\Entry;
 use App\Entity\Graphy;
+use App\Entity\Group;
 use App\Entity\Headword;
 use App\Entity\Label;
 use App\Entity\Lexicon;
+use App\Entity\User;
 use App\Manager\WiktionaryManager;
 use App\Repository\LabelRepository;
 use Doctrine\Persistence\ManagerRegistry;
@@ -71,9 +73,11 @@ class ApiLabelController extends ApiBaseController
      * @OA\RequestBody(
      *     required=true,
      *     @OA\JsonContent(
-     *       required={"name", "category"},
+     *       required={"name", "category", "masters_type"},
      *       @OA\Property(property="name", type="string"),
-     *       @OA\Property(property="category", type="string", example="morphological OR thematic OR list OR milestone"),
+     *       @OA\Property(property="category", type="string", example="morphological OR general OR list OR milestone OR institutional"),
+     *       @OA\Property(property="masters_type", type="string", example="user OR group OR public"),
+     *       @OA\Property(property="master_id", type="integer"),
      *       @OA\Property(property="milestone", type="date")
      *     )
      * )
@@ -86,12 +90,16 @@ class ApiLabelController extends ApiBaseController
         if (!$data) {
             return $this->createJsonResponse(401, ['error' => sprintf("Json non valide")]);
         }
-        if ($missingFields = $this->getMissingFields($data, ['name', 'category'])) {
+        if ($missingFields = $this->getMissingFields($data, ['name', 'category', 'masters_type'])) {
             return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour: %s", implode(', ', $missingFields))]);
         }
         if (!in_array($data['category'], Label::LABEL_LIST_CATEGORIES)) {
             return $this->createJsonResponse(401, ['error' => sprintf("La catégorie de label %s n'existe pas", $data['category'])]);
         }
+        if (!in_array($data['masters_type'], Label::MASTERS_TYPES)) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Le type de masters de label %s n'existe pas", $data['masters_type'])]);
+        }
+
         $milestone = $data['milestone'] ?? null;
         if ($data['category'] === Label::LABEL_CATEGORY_MILESTONE &&
             (!$milestone || !date_create_from_format('Y-m-d', $milestone))) {
@@ -99,7 +107,23 @@ class ApiLabelController extends ApiBaseController
         }
 
         $label = new Label($data['category']);
-        $label->setUser($this->getUser());
+
+        $mastersType = $data['mastersType'];
+        $masterId = $data['master_id'] ?? null;
+        if ($mastersType !== Label::MASTER_PUBLIC && !$masterId) {
+            return $this->createJsonResponse(401, ['error' => sprintf("Veuillez fournir une valeur pour « master_id »")]);
+        }
+        if ($data['masters_type'] === Label::MASTER_GROUP) {
+            $group = $this->doctrine->getRepository(Group::class)->find($masterId);
+            if (!$group) return $this->createJsonResponse(401, ['error' => sprintf("Impossible de trouver le groupe pour l'id «%s»", $masterId)]);
+            $label->setGroup($group);
+        }
+        if ($data['masters_type'] === Label::MASTER_PERSONAL) {
+            $user = $this->doctrine->getRepository(User::class)->find($masterId);
+            if (!$user) return $this->createJsonResponse(401, ['error' => sprintf("Impossible de trouver l'utilisateur pour l'id «%s»", $masterId)]);
+            $label->setUser($user);
+        }
+
         $label->setName($data['name']);
         if ($label->isMilestone()) {
             $label->setMilestone(date_create_from_format('Y-m-d', $milestone));
diff --git a/src/Entity/Headword.php b/src/Entity/Headword.php
index 0a901efec6da879c4863bbca8e6d6a77d5a438b1..022fde791d9e053ae75c51a3e7e0b48a2c85109e 100644
--- a/src/Entity/Headword.php
+++ b/src/Entity/Headword.php
@@ -72,6 +72,20 @@ class Headword
         $this->labels = new ArrayCollection();
     }
 
+    /**
+     * @return Label[]
+     */
+    public function getInstitutionalLabels()
+    {
+        $result = [];
+        foreach ($this->getLabels() as $label) {
+            if ($label->isInstitutional()) {
+                $result[] = $label;
+            }
+        }
+        return $result;
+    }
+
     /**
      * @return Label[]
      */
@@ -89,11 +103,11 @@ class Headword
     /**
      * @return Label[]
      */
-    public function getThematicLabels()
+    public function getGeneralLabels()
     {
         $result = [];
         foreach ($this->getLabels() as $label) {
-            if ($label->isThematic()) {
+            if ($label->isGeneral()) {
                 $result[] = $label;
             }
         }
diff --git a/src/Entity/Label.php b/src/Entity/Label.php
index d05111f5b0df6c778e378338264c82be75fd56e3..1a91d00fb6c745a3425a57186b3f38aa89e2f02a 100644
--- a/src/Entity/Label.php
+++ b/src/Entity/Label.php
@@ -7,6 +7,7 @@ use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
 
 /**
  * @ORM\Entity(repositoryClass=LabelRepository::class)
@@ -14,19 +15,31 @@ use Symfony\Component\Serializer\Annotation\Groups;
  */
 class Label
 {
+    const LABEL_CATEGORY_INSTITUTIONAL = 'institutional';
     const LABEL_CATEGORY_MORPHOLOGICAL = 'morphological';
-    const LABEL_CATEGORY_THEMATIC      = 'thematic';
-    const LABEL_CATEGORY_LIST          = 'list';
     const LABEL_CATEGORY_MILESTONE     = 'milestone';
+    const LABEL_CATEGORY_GENERAL       = 'general';
+    const LABEL_CATEGORY_LIST          = 'list';
     const LABEL_CATEGORY_SYSTEM        = 'system';
 
     const LABEL_LIST_CATEGORIES = [
+        self::LABEL_CATEGORY_INSTITUTIONAL => self::LABEL_CATEGORY_INSTITUTIONAL,
         self::LABEL_CATEGORY_MORPHOLOGICAL => self::LABEL_CATEGORY_MORPHOLOGICAL,
-        self::LABEL_CATEGORY_THEMATIC      => self::LABEL_CATEGORY_THEMATIC,
+        self::LABEL_CATEGORY_GENERAL       => self::LABEL_CATEGORY_GENERAL,
         self::LABEL_CATEGORY_LIST          => self::LABEL_CATEGORY_LIST,
         self::LABEL_CATEGORY_MILESTONE     => self::LABEL_CATEGORY_MILESTONE,
     ];
 
+    const MASTER_PERSONAL        = 'personal';
+    const MASTER_GROUP           = 'group';
+    const MASTER_PUBLIC          = 'public';
+
+    const MASTERS_TYPES = [
+        self::MASTER_GROUP    => self::MASTER_GROUP,
+        self::MASTER_PERSONAL => self::MASTER_PERSONAL,
+        self::MASTER_PUBLIC   => self::MASTER_PUBLIC,
+    ];
+
     const LABEL_MERGED = 'merged';
 
     /**
@@ -48,6 +61,13 @@ class Label
      */
     private $category;
 
+    /**
+     * @ORM\Column(type="string", length=255)
+     * @Groups({"label:read"})
+     * @Assert\Length(max=255)
+     */
+    private $source;
+
     /**
      * @ORM\Column(type="datetime", nullable=true)
      * @Groups({"label:read"})
@@ -87,10 +107,22 @@ class Label
      */
     private $graphyList;
 
+    /**
+     * @ORM\OneToMany(targetEntity=UpdateRequest::class, mappedBy="label")
+     */
+    private $updateRequests;
+
+    /**
+     * @ORM\OneToMany(targetEntity=LabelVisibility::class, mappedBy="label")
+     */
+    private $labelVisibilities;
+
     public function __construct($category)
     {
         $this->setCategory($category);
         $this->headwords = new ArrayCollection();
+        $this->updateRequests = new ArrayCollection();
+        $this->labelVisibilities = new ArrayCollection();
     }
 
     public function __toString()
@@ -98,6 +130,15 @@ class Label
         return $this->getName();
     }
 
+    public function getMasters()
+    {
+        if ($this->getUser() && ! $this->getGroup())     return self::MASTER_PERSONAL;
+        if ($this->getGroup() &&  $this->getUser())      return self::MASTER_GROUP;
+        if (! $this->getGroup() && ! $this->getUser())   return self::MASTER_PUBLIC;
+
+        throw new \LogicException("Cannot determine label masters type");
+    }
+
     public function compareWith(Label $label)
     {
         $diffs = [];
@@ -123,13 +164,17 @@ class Label
         return $diffs;
     }
 
+    public function isInstitutional()
+    {
+        return $this->getCategory() === self::LABEL_CATEGORY_INSTITUTIONAL;
+    }
     public function isMorphological()
     {
         return $this->getCategory() === self::LABEL_CATEGORY_MORPHOLOGICAL;
     }
-    public function isThematic()
+    public function isGeneral()
     {
-        return $this->getCategory() === self::LABEL_CATEGORY_THEMATIC;
+        return $this->getCategory() === self::LABEL_CATEGORY_GENERAL;
     }
     public function isList()
     {
@@ -280,4 +325,76 @@ class Label
 
         return $this;
     }
+
+    public function getSource(): ?string
+    {
+        return $this->source;
+    }
+
+    public function setSource(string $source): self
+    {
+        $this->source = $source;
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, UpdateRequest>
+     */
+    public function getUpdateRequests(): Collection
+    {
+        return $this->updateRequests;
+    }
+
+    public function addUpdateRequest(UpdateRequest $updateRequest): self
+    {
+        if (!$this->updateRequests->contains($updateRequest)) {
+            $this->updateRequests[] = $updateRequest;
+            $updateRequest->setLabel($this);
+        }
+
+        return $this;
+    }
+
+    public function removeUpdateRequest(UpdateRequest $updateRequest): self
+    {
+        if ($this->updateRequests->removeElement($updateRequest)) {
+            // set the owning side to null (unless already changed)
+            if ($updateRequest->getLabel() === $this) {
+                $updateRequest->setLabel(null);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, LabelVisibility>
+     */
+    public function getLabelVisibilities(): Collection
+    {
+        return $this->labelVisibilities;
+    }
+
+    public function addLabelVisibility(LabelVisibility $labelVisibility): self
+    {
+        if (!$this->labelVisibilities->contains($labelVisibility)) {
+            $this->labelVisibilities[] = $labelVisibility;
+            $labelVisibility->setLabel($this);
+        }
+
+        return $this;
+    }
+
+    public function removeLabelVisibility(LabelVisibility $labelVisibility): self
+    {
+        if ($this->labelVisibilities->removeElement($labelVisibility)) {
+            // set the owning side to null (unless already changed)
+            if ($labelVisibility->getLabel() === $this) {
+                $labelVisibility->setLabel(null);
+            }
+        }
+
+        return $this;
+    }
 }
diff --git a/src/Entity/LabelVisibility.php b/src/Entity/LabelVisibility.php
new file mode 100644
index 0000000000000000000000000000000000000000..c0dfad8743f64b956e9d20b156947a8b173e00f0
--- /dev/null
+++ b/src/Entity/LabelVisibility.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\LabelRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * @ORM\Entity(repositoryClass=VoteRepository::class)
+ * @ORM\HasLifecycleCallbacks
+ */
+class LabelVisibility
+{
+    /**
+     * @ORM\Id
+     * @ORM\GeneratedValue
+     * @ORM\Column(type="integer")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column(type="datetime_immutable")
+     */
+    private $createdAt;
+
+    /**
+     * @ORM\Column(type="datetime_immutable", nullable=true)
+     */
+    private $updatedAt;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="labelVisibilities")
+     * @Groups({"labelVisibility:read"})
+     */
+    private $user;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=Label::class, inversedBy="labelVisibilities")
+     * @Groups({"labelVisibility:read"})
+     */
+    private $label;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=Lexicon::class, inversedBy="labelVisibilities")
+     * @Groups({"labelVisibility:read"})
+     */
+    private $lexicon;
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getCreatedAt(): ?\DateTimeImmutable
+    {
+        return $this->createdAt;
+    }
+
+    public function setCreatedAt(\DateTimeImmutable $createdAt): self
+    {
+        $this->createdAt = $createdAt;
+
+        return $this;
+    }
+
+    public function getUpdatedAt(): ?\DateTimeImmutable
+    {
+        return $this->updatedAt;
+    }
+
+    public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self
+    {
+        $this->updatedAt = $updatedAt;
+
+        return $this;
+    }
+
+    public function getUser(): ?User
+    {
+        return $this->user;
+    }
+
+    public function setUser(?User $user): self
+    {
+        $this->user = $user;
+
+        return $this;
+    }
+
+    public function getLabel(): ?Label
+    {
+        return $this->label;
+    }
+
+    public function setLabel(?Label $label): self
+    {
+        $this->label = $label;
+
+        return $this;
+    }
+
+    public function getLexicon(): ?Lexicon
+    {
+        return $this->lexicon;
+    }
+
+    public function setLexicon(?Lexicon $lexicon): self
+    {
+        $this->lexicon = $lexicon;
+
+        return $this;
+    }
+
+}
diff --git a/src/Entity/LabelVisibilityRepository.php b/src/Entity/LabelVisibilityRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ef59849072581391ff84f62234e8a14b04672a9
--- /dev/null
+++ b/src/Entity/LabelVisibilityRepository.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Entity;
+
+use App\Entity\LabelVisibility;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<LabelVisibility>
+ *
+ * @method LabelVisibility|null find($id, $lockMode = null, $lockVersion = null)
+ * @method LabelVisibility|null findOneBy(array $criteria, array $orderBy = null)
+ * @method LabelVisibility[]    findAll()
+ * @method LabelVisibility[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class LabelVisibilityRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, LabelVisibility::class);
+    }
+
+    public function add(LabelVisibility $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->persist($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+    public function remove(LabelVisibility $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->remove($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+//    /**
+//     * @return LabelVisibility[] Returns an array of LabelVisibility objects
+//     */
+//    public function findByExampleField($value): array
+//    {
+//        return $this->createQueryBuilder('v')
+//            ->andWhere('v.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->orderBy('v.id', 'ASC')
+//            ->setMaxResults(10)
+//            ->getQuery()
+//            ->getResult()
+//        ;
+//    }
+
+//    public function findOneBySomeField($value): ?LabelVisibility
+//    {
+//        return $this->createQueryBuilder('v')
+//            ->andWhere('v.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}
diff --git a/src/Entity/Lexicon.php b/src/Entity/Lexicon.php
index ab29022759cf715ba54b185cd979ac48ed576677..0625fd2b3c84aa754b1bcf86cba005c933f47a44 100644
--- a/src/Entity/Lexicon.php
+++ b/src/Entity/Lexicon.php
@@ -106,6 +106,11 @@ class Lexicon
      */
     private $logs;
 
+    /**
+     * @ORM\OneToMany(targetEntity=LabelVisibility::class, mappedBy="lexicon")
+     */
+    private $labelVisibilities;
+
     public function __toString()
     {
         switch ($this->getCategory()) {
@@ -121,6 +126,7 @@ class Lexicon
         $this->entries = new ArrayCollection();
         $this->graphyLists = new ArrayCollection();
         $this->logs = new ArrayCollection();
+        $this->labelVisibilities = new ArrayCollection();
     }
 
     public function isZero()
@@ -341,4 +347,34 @@ class Lexicon
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, LabelVisibility>
+     */
+    public function getLabelVisibilities(): Collection
+    {
+        return $this->labelVisibilities;
+    }
+
+    public function addLabelVisibility(LabelVisibility $labelVisibility): self
+    {
+        if (!$this->labelVisibilities->contains($labelVisibility)) {
+            $this->labelVisibilities[] = $labelVisibility;
+            $labelVisibility->setLexicon($this);
+        }
+
+        return $this;
+    }
+
+    public function removeLabelVisibility(LabelVisibility $labelVisibility): self
+    {
+        if ($this->labelVisibilities->removeElement($labelVisibility)) {
+            // set the owning side to null (unless already changed)
+            if ($labelVisibility->getLexicon() === $this) {
+                $labelVisibility->setLexicon(null);
+            }
+        }
+
+        return $this;
+    }
 }
diff --git a/src/Entity/UpdateRequest.php b/src/Entity/UpdateRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b8ad52b9cff220af493e25bcb932c6af5d467536
--- /dev/null
+++ b/src/Entity/UpdateRequest.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\LabelRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * @ORM\Entity(repositoryClass=UpdateRequestRepository::class)
+ * @ORM\HasLifecycleCallbacks
+ */
+class UpdateRequest
+{
+    const CATEGORY_RENAME = 'rename';
+    const CATEGORY_DELETE = 'delete';
+
+    const CATEGORY_LIST = [
+        self::CATEGORY_RENAME => self::CATEGORY_RENAME,
+        self::CATEGORY_DELETE => self::CATEGORY_DELETE,
+    ];
+
+    /**
+     * @ORM\Id
+     * @ORM\GeneratedValue
+     * @ORM\Column(type="integer")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column(type="string", length=255)
+     * @Groups({"updateRequest:read"})
+     */
+    private $suggestedName;
+
+    /**
+     * @ORM\Column(type="string", length=80)
+     * @Groups({"updateRequest:read"})
+     */
+    private $category;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=Label::class, inversedBy="updateRequests")
+     */
+    private $label;
+
+    /**
+     * @ORM\Column(type="datetime_immutable")
+     */
+    private $createdAt;
+
+    /**
+     * @ORM\Column(type="datetime_immutable", nullable=true)
+     */
+    private $updatedAt;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="updateRequests")
+     * @Groups({"updateRequest:read"})
+     */
+    private $user;
+
+    /**
+     * @ORM\OneToMany(targetEntity=Vote::class, mappedBy="updateRequest")
+     * @Groups({"updateRequest:read"})
+     */
+    private $votes;
+
+    public function __construct($category)
+    {
+        $this->setCategory($category);
+        $this->votes = new ArrayCollection();
+    }
+
+    public function __toString()
+    {
+        return sprintf("update request du %s label «%s» par %s", $this->getCreatedAt()->format('d/m/Y'), $this->getLabel(), $this->getUser());
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getSuggestedName(): ?string
+    {
+        return $this->suggestedName;
+    }
+
+    public function setSuggestedName(string $suggestedName): self
+    {
+        $this->suggestedName = $suggestedName;
+
+        return $this;
+    }
+
+    public function getCategory(): ?string
+    {
+        return $this->category;
+    }
+
+    public function setCategory(string $category): self
+    {
+        $this->category = $category;
+
+        return $this;
+    }
+
+    public function getCreatedAt(): ?\DateTimeImmutable
+    {
+        return $this->createdAt;
+    }
+
+    public function setCreatedAt(\DateTimeImmutable $createdAt): self
+    {
+        $this->createdAt = $createdAt;
+
+        return $this;
+    }
+
+    public function getUpdatedAt(): ?\DateTimeImmutable
+    {
+        return $this->updatedAt;
+    }
+
+    public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self
+    {
+        $this->updatedAt = $updatedAt;
+
+        return $this;
+    }
+
+    public function getLabel(): ?Label
+    {
+        return $this->label;
+    }
+
+    public function setLabel(?Label $label): self
+    {
+        $this->label = $label;
+
+        return $this;
+    }
+
+    public function getUser(): ?User
+    {
+        return $this->user;
+    }
+
+    public function setUser(?User $user): self
+    {
+        $this->user = $user;
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, Vote>
+     */
+    public function getVotes(): Collection
+    {
+        return $this->votes;
+    }
+
+    public function addVote(Vote $vote): self
+    {
+        if (!$this->votes->contains($vote)) {
+            $this->votes[] = $vote;
+            $vote->setUpdateRequest($this);
+        }
+
+        return $this;
+    }
+
+    public function removeVote(Vote $vote): self
+    {
+        if ($this->votes->removeElement($vote)) {
+            // set the owning side to null (unless already changed)
+            if ($vote->getUpdateRequest() === $this) {
+                $vote->setUpdateRequest(null);
+            }
+        }
+
+        return $this;
+    }
+
+}
diff --git a/src/Entity/UpdateRequestRepository.php b/src/Entity/UpdateRequestRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7b5182443791adf4be20126326796740391f25d
--- /dev/null
+++ b/src/Entity/UpdateRequestRepository.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Entity;
+
+use App\Entity\UpdateRequest;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<UpdateRequest>
+ *
+ * @method UpdateRequest|null find($id, $lockMode = null, $lockVersion = null)
+ * @method UpdateRequest|null findOneBy(array $criteria, array $orderBy = null)
+ * @method UpdateRequest[]    findAll()
+ * @method UpdateRequest[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class UpdateRequestRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, UpdateRequest::class);
+    }
+
+    public function add(UpdateRequest $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->persist($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+    public function remove(UpdateRequest $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->remove($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+//    /**
+//     * @return UpdateRequest[] Returns an array of UpdateRequest objects
+//     */
+//    public function findByExampleField($value): array
+//    {
+//        return $this->createQueryBuilder('u')
+//            ->andWhere('u.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->orderBy('u.id', 'ASC')
+//            ->setMaxResults(10)
+//            ->getQuery()
+//            ->getResult()
+//        ;
+//    }
+
+//    public function findOneBySomeField($value): ?UpdateRequest
+//    {
+//        return $this->createQueryBuilder('u')
+//            ->andWhere('u.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}
diff --git a/src/Entity/User.php b/src/Entity/User.php
index 3d844f9c6889a577b10bd9b177e3083bf9f80c77..66f3d0ab72aea157180fb7ada9164505c17a71cc 100644
--- a/src/Entity/User.php
+++ b/src/Entity/User.php
@@ -201,6 +201,21 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
      */
     private $metaInfos;
 
+    /**
+     * @ORM\OneToMany(targetEntity=UpdateRequest::class, mappedBy="user")
+     */
+    private $updateRequests;
+
+    /**
+     * @ORM\OneToMany(targetEntity=Vote::class, mappedBy="user")
+     */
+    private $votes;
+
+    /**
+     * @ORM\OneToMany(targetEntity=LabelVisibility::class, mappedBy="user")
+     */
+    private $labelVisibilities;
+
     public function __toString()
     {
         return $this->getPseudo();
@@ -222,6 +237,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
         $this->labels = new ArrayCollection();
         $this->createdGraphyLists = new ArrayCollection();
         $this->logs = new ArrayCollection();
+        $this->traces = new ArrayCollection();
+        $this->metaInfos = new ArrayCollection();
+        $this->updateRequests = new ArrayCollection();
+        $this->votes = new ArrayCollection();
+        $this->labelVisibilities = new ArrayCollection();
     }
 
     /**
@@ -808,4 +828,154 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
 
         return $this;
     }
+
+    /**
+     * @return Collection<int, Trace>
+     */
+    public function getTraces(): Collection
+    {
+        return $this->traces;
+    }
+
+    public function addTrace(Trace $trace): self
+    {
+        if (!$this->traces->contains($trace)) {
+            $this->traces[] = $trace;
+            $trace->setCreatedBy($this);
+        }
+
+        return $this;
+    }
+
+    public function removeTrace(Trace $trace): self
+    {
+        if ($this->traces->removeElement($trace)) {
+            // set the owning side to null (unless already changed)
+            if ($trace->getCreatedBy() === $this) {
+                $trace->setCreatedBy(null);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, MetaInfo>
+     */
+    public function getMetaInfos(): Collection
+    {
+        return $this->metaInfos;
+    }
+
+    public function addMetaInfo(MetaInfo $metaInfo): self
+    {
+        if (!$this->metaInfos->contains($metaInfo)) {
+            $this->metaInfos[] = $metaInfo;
+            $metaInfo->setCreatedBy($this);
+        }
+
+        return $this;
+    }
+
+    public function removeMetaInfo(MetaInfo $metaInfo): self
+    {
+        if ($this->metaInfos->removeElement($metaInfo)) {
+            // set the owning side to null (unless already changed)
+            if ($metaInfo->getCreatedBy() === $this) {
+                $metaInfo->setCreatedBy(null);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, UpdateRequest>
+     */
+    public function getUpdateRequests(): Collection
+    {
+        return $this->updateRequests;
+    }
+
+    public function addUpdateRequest(UpdateRequest $updateRequest): self
+    {
+        if (!$this->updateRequests->contains($updateRequest)) {
+            $this->updateRequests[] = $updateRequest;
+            $updateRequest->setUser($this);
+        }
+
+        return $this;
+    }
+
+    public function removeUpdateRequest(UpdateRequest $updateRequest): self
+    {
+        if ($this->updateRequests->removeElement($updateRequest)) {
+            // set the owning side to null (unless already changed)
+            if ($updateRequest->getUser() === $this) {
+                $updateRequest->setUser(null);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, Vote>
+     */
+    public function getVotes(): Collection
+    {
+        return $this->votes;
+    }
+
+    public function addVote(Vote $vote): self
+    {
+        if (!$this->votes->contains($vote)) {
+            $this->votes[] = $vote;
+            $vote->setUser($this);
+        }
+
+        return $this;
+    }
+
+    public function removeVote(Vote $vote): self
+    {
+        if ($this->votes->removeElement($vote)) {
+            // set the owning side to null (unless already changed)
+            if ($vote->getUser() === $this) {
+                $vote->setUser(null);
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return Collection<int, LabelVisibility>
+     */
+    public function getLabelVisibilities(): Collection
+    {
+        return $this->labelVisibilities;
+    }
+
+    public function addLabelVisibility(LabelVisibility $labelVisibility): self
+    {
+        if (!$this->labelVisibilities->contains($labelVisibility)) {
+            $this->labelVisibilities[] = $labelVisibility;
+            $labelVisibility->setUser($this);
+        }
+
+        return $this;
+    }
+
+    public function removeLabelVisibility(LabelVisibility $labelVisibility): self
+    {
+        if ($this->labelVisibilities->removeElement($labelVisibility)) {
+            // set the owning side to null (unless already changed)
+            if ($labelVisibility->getUser() === $this) {
+                $labelVisibility->setUser(null);
+            }
+        }
+
+        return $this;
+    }
 }
diff --git a/src/Entity/Vote.php b/src/Entity/Vote.php
new file mode 100644
index 0000000000000000000000000000000000000000..11b944edc6a70f748cc7070b8384c40b6fb64a14
--- /dev/null
+++ b/src/Entity/Vote.php
@@ -0,0 +1,141 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\LabelRepository;
+use Doctrine\Common\Collections\ArrayCollection;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * @ORM\Entity(repositoryClass=VoteRepository::class)
+ * @ORM\HasLifecycleCallbacks
+ */
+class Vote
+{
+    const CATEGORY_YES = 'yes';
+    const CATEGORY_NO = 'no';
+
+    const CATEGORY_LIST = [
+        self::CATEGORY_YES => self::CATEGORY_YES,
+        self::CATEGORY_NO  => self::CATEGORY_NO,
+    ];
+
+    /**
+     * @ORM\Id
+     * @ORM\GeneratedValue
+     * @ORM\Column(type="integer")
+     */
+    private $id;
+
+    /**
+     * @ORM\Column(type="string", length=80)
+     * @Groups({"vote:read"})
+     */
+    private $category;
+
+    /**
+     * @ORM\Column(type="datetime_immutable")
+     */
+    private $createdAt;
+
+    /**
+     * @ORM\Column(type="datetime_immutable", nullable=true)
+     */
+    private $updatedAt;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=User::class, inversedBy="votes")
+     * @Groups({"vote:read"})
+     */
+    private $user;
+
+    /**
+     * @ORM\ManyToOne(targetEntity=UpdateRequest::class, inversedBy="votes")
+     * @Groups({"vote:read"})
+     */
+    private $updateRequest;
+
+    public function __construct($category)
+    {
+        $this->setCategory($category);
+    }
+
+    public function __toString()
+    {
+        return sprintf("vote %s du %s  pour label «%s» par %s", $this->getCategory(), $this->getCreatedAt()->format('d/m/Y'), $this->getLabel(), $this->getUser());
+    }
+
+    public function getLabel()
+    {
+        return $this->getUpdateRequest()->getLabel();
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getCategory(): ?string
+    {
+        return $this->category;
+    }
+
+    public function setCategory(string $category): self
+    {
+        $this->category = $category;
+
+        return $this;
+    }
+
+    public function getCreatedAt(): ?\DateTimeImmutable
+    {
+        return $this->createdAt;
+    }
+
+    public function setCreatedAt(\DateTimeImmutable $createdAt): self
+    {
+        $this->createdAt = $createdAt;
+
+        return $this;
+    }
+
+    public function getUpdatedAt(): ?\DateTimeImmutable
+    {
+        return $this->updatedAt;
+    }
+
+    public function setUpdatedAt(?\DateTimeImmutable $updatedAt): self
+    {
+        $this->updatedAt = $updatedAt;
+
+        return $this;
+    }
+
+    public function getUser(): ?User
+    {
+        return $this->user;
+    }
+
+    public function setUser(?User $user): self
+    {
+        $this->user = $user;
+
+        return $this;
+    }
+
+    public function getUpdateRequest(): ?UpdateRequest
+    {
+        return $this->updateRequest;
+    }
+
+    public function setUpdateRequest(?UpdateRequest $updateRequest): self
+    {
+        $this->updateRequest = $updateRequest;
+
+        return $this;
+    }
+
+}
diff --git a/src/Entity/VoteRepository.php b/src/Entity/VoteRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..dbcd977c496c5b1176d5435b1e7157cba33e013f
--- /dev/null
+++ b/src/Entity/VoteRepository.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Entity;
+
+use App\Entity\Vote;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Vote>
+ *
+ * @method Vote|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Vote|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Vote[]    findAll()
+ * @method Vote[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class VoteRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Vote::class);
+    }
+
+    public function add(Vote $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->persist($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+    public function remove(Vote $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->remove($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+//    /**
+//     * @return Vote[] Returns an array of Vote objects
+//     */
+//    public function findByExampleField($value): array
+//    {
+//        return $this->createQueryBuilder('v')
+//            ->andWhere('v.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->orderBy('v.id', 'ASC')
+//            ->setMaxResults(10)
+//            ->getQuery()
+//            ->getResult()
+//        ;
+//    }
+
+//    public function findOneBySomeField($value): ?Vote
+//    {
+//        return $this->createQueryBuilder('v')
+//            ->andWhere('v.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}
diff --git a/src/Repository/UpdateRequestRepository.php b/src/Repository/UpdateRequestRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c819c1b143f6b1a0c556dc90f1d69515398537b
--- /dev/null
+++ b/src/Repository/UpdateRequestRepository.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\UpdateRequest;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<UpdateRequest>
+ *
+ * @method UpdateRequest|null find($id, $lockMode = null, $lockVersion = null)
+ * @method UpdateRequest|null findOneBy(array $criteria, array $orderBy = null)
+ * @method UpdateRequest[]    findAll()
+ * @method UpdateRequest[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class UpdateRequestRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, UpdateRequest::class);
+    }
+
+    public function add(UpdateRequest $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->persist($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+    public function remove(UpdateRequest $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->remove($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+//    /**
+//     * @return UpdateRequest[] Returns an array of UpdateRequest objects
+//     */
+//    public function findByExampleField($value): array
+//    {
+//        return $this->createQueryBuilder('l')
+//            ->andWhere('l.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->orderBy('l.id', 'ASC')
+//            ->setMaxResults(10)
+//            ->getQuery()
+//            ->getResult()
+//        ;
+//    }
+
+//    public function findOneBySomeField($value): ?UpdateRequest
+//    {
+//        return $this->createQueryBuilder('l')
+//            ->andWhere('l.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}
diff --git a/src/Repository/VoteRepository.php b/src/Repository/VoteRepository.php
new file mode 100644
index 0000000000000000000000000000000000000000..b1a9518ba7472dd9b52fa1285bcdfe9d7ae2a410
--- /dev/null
+++ b/src/Repository/VoteRepository.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\Vote;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+
+/**
+ * @extends ServiceEntityRepository<Vote>
+ *
+ * @method Vote|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Vote|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Vote[]    findAll()
+ * @method Vote[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class VoteRepository extends ServiceEntityRepository
+{
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, Vote::class);
+    }
+
+    public function add(Vote $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->persist($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+    public function remove(Vote $entity, bool $flush = false): void
+    {
+        $this->getEntityManager()->remove($entity);
+
+        if ($flush) {
+            $this->getEntityManager()->flush();
+        }
+    }
+
+//    /**
+//     * @return Vote[] Returns an array of Vote objects
+//     */
+//    public function findByExampleField($value): array
+//    {
+//        return $this->createQueryBuilder('l')
+//            ->andWhere('l.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->orderBy('l.id', 'ASC')
+//            ->setMaxResults(10)
+//            ->getQuery()
+//            ->getResult()
+//        ;
+//    }
+
+//    public function findOneBySomeField($value): ?Vote
+//    {
+//        return $this->createQueryBuilder('l')
+//            ->andWhere('l.exampleField = :val')
+//            ->setParameter('val', $value)
+//            ->getQuery()
+//            ->getOneOrNullResult()
+//        ;
+//    }
+}
diff --git a/src/Wikstraktor/wikstraktor_new.py b/src/Wikstraktor/wikstraktor_new.py
new file mode 100644
index 0000000000000000000000000000000000000000..fec1efb131518ec026be156fa3cdd4f42538cf62
--- /dev/null
+++ b/src/Wikstraktor/wikstraktor_new.py
@@ -0,0 +1,668 @@
+#!/usr/bin/env python3
+import pywikibot
+import wikitextparser
+import importlib
+import json
+from wikstraktor_version import version as the_version
+from wikstraklog import Wikstraklog
+
+#ICITE : fr marche pas, en prend des trucs vides à virer (cf. yellow… def & example)
+
+
+class SubInfo:
+	next_id = 1
+	prfx = "err"
+
+	@classmethod
+	def inc_n_id(cls):
+		cls.next_id += 1
+
+	@classmethod
+	def reset(cls):
+		cls.next_id = 0
+
+	def __init__(self, prefix = None):
+		self.id = None
+		self.set_id(prefix)
+
+	def set_id(self, prefix):
+		if self.id == None and prefix != None:
+			self.id = f"{prefix}_{self.__class__.prfx}{self.__class__.next_id}"
+			self.__class__.inc_n_id()
+		return self.id
+
+	def serializable(self, prefix = None):
+		res = {}
+		if self.set_id(prefix) != None:
+			res["id"] = self.id
+		return res
+
+
+
+#######
+# Oral
+#######
+class Sound:
+	def __init__(self, url, accent):
+		self.url = url
+		self.accent = accent
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and self.url == other.url and self.accent == other.accent
+
+	def serializable(self):
+		if self.accent == None:
+			res = {"url":self.url}
+		else:
+			res = {"accent":self.accent, "url":self.url}
+		return res
+
+class Pronunciation(SubInfo):
+	prfx = "prn"
+
+	def __init__(self, prefix = None):
+		super().__init__(prefix)
+		self.ipa = None
+		self.sounds = []
+		self.accent = None
+
+	def set_transcription(self, tscpt):
+		self.ipa = tscpt
+
+	def set_accent(self, accent):
+		self.accent = accent
+
+	def add_sound(self, url, accent=None):
+		self.sounds.append(Sound(url,accent))
+
+	def serializable(self, prefix = None):
+		snds = []
+		for s in self.sounds:
+			snds.append(s.serializable())
+		res = super().serializable(prefix)
+		res['transcript'] = self.ipa
+		if self.accent != None:
+			res['accent'] = self.accent
+		res['sounds'] = snds
+		return res
+
+	def __str__(self):
+		return json.dumps(self.serializable(''))
+
+	def __eq__(self, other):
+		res = isinstance(other, self.__class__) and self.ipa == other.ipa and self.accent == other.accent and len(self.sounds)==len(other.sounds)
+		i = 0
+		while res and i<len(self.sounds):
+			res = self.sounds[i] == other.sounds[i]
+			i += 1
+		return res
+
+#######
+# Metadata
+## TODO:
+#  * POS : créer une classe POS avec les traits dépendants (ex: masc en fr)
+#######
+
+#######
+# Senses
+# TODO: créer une classe Translations
+#######
+
+class Definition(SubInfo):
+	prfx = "def"
+	key = "definition"
+
+	def __init__(self, lang, text, prefix=None):
+		super().__init__(prefix)
+		if text != "":
+			self.lang = lang
+			self.text = text
+		else:
+			raise ValueError(f"Definition.__init__: “{text}” empty definition.")
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and self.lang == other.lang and self.text == other.text
+
+	def serializable(self, prefix = None):
+		res = super().serializable(prefix)
+		res["lang"] = self.lang
+		res[self.__class__.key] = self.text
+		return res
+
+class Translation(Definition):
+	prfx = "trad"
+	key = "translation"
+
+class Example(SubInfo):
+	prfx = "ex"
+
+	def __init__(self, transcript, source=None, url=None, prefix=None):
+		super().__init__(prefix)
+		if transcript != "":
+			self.text = transcript
+			self.source = source
+			self.url = url
+		else:
+			raise ValueError(f"Example.__init__: “{transcript}” empty example.")
+
+
+	def __eq__(self, other):
+		return isinstance(other, self.__class__) and self.text==other.text and self.source==other.source and self.url==other.url
+
+	def serializable(self, prefix = None):
+		res = super().serializable(prefix)
+		res["example"]=self.text
+		if self.source != None:
+			res["source"] = self.source
+		if self.url != None:
+			res["url"] = self.url
+		return res
+
+class Sense(SubInfo):
+	prfx = ""
+
+	def __init__(self, lang=None, definition=None, wiki_lang=None, prefix=None):
+		self.lang = lang
+		self.label = None
+		self.set_id(prefix)
+		#On réinitialise les identifiants des sous-éléments
+		if not isinstance(self, SubSense):
+			Definition.reset()
+			Example.reset()
+			Translation.reset()
+			SubSense.reset()
+
+		self.definitions = [] #liste des définitions (elles auront une langue et un texte)
+		self.subsenses = [] #liste des sous-définitions (récursif…)
+		self.examples = [] #liste des exemples (un texte obligatoire, source et url sont optionnels)
+		self.translations = [] #liste des traductions dans d'autres langues
+		self.domain = None #domaine d'usage du mot dans ce sens
+		if definition != None:
+			try:
+				self.add_def(wiki_lang, definition)
+			except ValueError as err:
+				raise ValueError(f"Sense.__init__() with empty definition\n{err}")
+
+	def set_id(self, prefix=None):
+		if prefix != None and self.label == None:
+			self.label = f"{prefix}_{self.__class__.next_id}"  #l'identifiant du sens
+			self.__class__.inc_n_id()
+		return self.label
+
+	def get_id(self):
+		return f"{self.lang}.{self.label}"
+
+	def set_domain(self, d):
+		self.domain = d
+
+	def add_def(self, lang, definition):
+		theDef = Definition(lang, definition)
+		if theDef != None and theDef not in self.definitions:
+			theDef.set_id(self.set_id())
+			self.definitions.append(theDef)
+
+	def add_example(self, transcript, src=None, url=None, prefix=None):
+		try:
+			theEx = Example(transcript, src, url, prefix)
+			if theEx != None and theEx not in self.examples:
+				theEx.set_id(self.set_id())
+				self.examples.append(theEx)
+		except ValueError as e:
+			print(f"Skipped empty example")
+
+	def add_translation(self, lang=None, translation=None):
+		theTranslation = Translation(lang, translation)
+		if theTranslation != None and theTranslation not in self.translations:
+			theTranslation.set_id(self.set_id())
+			self.translations.append(theTranslation)
+
+	def add_subsense(self, subsense):
+		if self.label!=None:
+			subsense.set_id(self.set_id())
+		if subsense not in self.subsenses:
+			self.subsenses.append(subsense)
+
+	def __eq__(self, other):
+		res = isinstance(other, self.__class__) and self.label == other.label and len(self.definitions) == len(other.definitions) and len(self.examples) == len(other.examples) and len(self.translations) == len(other.translations) and self.domain == other.domain
+		i = 0
+		while res and i < len(self.examples):
+			res = self.examples[i] in other.examples
+			i+=1
+		i = 0
+		while res and i < len(self.translations):
+			res = self.translations[i] in other.translations
+			i+=1
+		i = 0
+		while res and i < len(self.definitions):
+			res = self.definitions[i] in other.definitions
+			i+=1
+		i = 0
+		while res and i < len(self.subsenses):
+			res = self.subsenses[i] in other.subsenses
+			i+=1
+		return res
+
+	def serializable(self, prefix = None):
+		res = {}
+		if self.domain != None:
+			res["Domain"] = self.domain
+		if len(self.definitions) > 0:
+			res["Definitions"] = []
+			for d in self.definitions:
+				res["Definitions"].append(d.serializable(prefix))
+		if len(self.subsenses) > 0:
+			res["Subsenses"] = {}
+			for t in self.subsenses:
+				res["Subsenses"][t.set_id(self.label)]= t.serializable(prefix)
+		if len(self.examples) > 0 :
+			res["Examples"] = []
+			for e in self.examples:
+				res["Examples"].append(e.serializable(prefix))
+		if len(self.translations) > 0:
+			res["Translations"] = []
+			for t in self.translations:
+				res["Translations"].append(t.serializable(prefix))
+		return res
+
+	def __str__(self):
+		return json.dumps(self.serializable())
+
+class SubSense(Sense):
+	def set_id(self, prefix=None):
+		if prefix != None and self.label == None:
+			self.label = f"{prefix}.{self.__class__.next_id}"  #l'identifiant du sens
+			self.__class__.inc_n_id()
+		return self.label
+
+class Entry:
+	#version_id : l'identifiant unique de la vesion de la page du wiktionnaire (pywikibot.Page.latest_revision_id)
+	def __init__(self, lemma, lang, wiki_lang, version_id, wkskt_version):
+		self.lemma = lemma
+		self.lang = lang
+		#Si un jour on mixe +ieurs données de plusieurs wiktionnaires, ce sera utile
+		self.sources = []
+		self.sources.append({"wiktionary_language":wiki_lang,"permanentId":version_id,"wikstraktor_version":wkskt_version})
+		self.current_source = 0
+		self.pronunciations = []
+		self.pos = None
+		self.senses = []
+ #l'identifiant unique de la version de la page du wiktionnaire
+		Sense.reset()
+
+	def set_pos(self, pos):
+		self.pos = pos
+
+	def get_id(self, source_id=0):
+		#TODO: remplacer un jour le source id par la bonne source
+		if self.pos != None:
+			pos = self.pos
+		else:
+			pos = ""
+		return f"{self.lang}-{source_id}.{self.lemma}{pos}"
+
+	def set_pronunciations(self, pron):
+		if isinstance(pron, Pronunciation):
+			self.add_pronunciation(pron)
+		elif type(pron) == list:
+			for p in pron:
+				if isinstance(p, Pronunciation):
+					self.add_pronunciation(p)
+				else:
+					raise ValueError(f"Entry.set_pronunciations: {p} is not a Pronunciation object ({p.__class__.__name__}).")
+		else:
+			raise ValueError(f"Entry.set_pronunciations: {pron} is not a Pronunciation object ({pron.__class__.__name__}).")
+
+	def add_pronunciation(self, p):
+		if p not in self.pronunciations:
+			p.set_id(self.get_id())
+			self.pronunciations.append(p)
+
+	def set_senses(self, senses):
+		for s in senses:
+			if isinstance(s, Sense):
+				self.add_sense(s)
+			else:
+				raise ValueError(f"Entry.set_senses: {s} is not a Sense object ({p.__class__.__name__}).")
+
+	def add_sense(self, s):
+		if s not in self.senses:
+			s.set_id(self.get_id())
+			self.senses.append(s)
+
+	def is_valid(self):
+		return self.lemma != None and len(self.pronunciations) > 0 and self.pos != None and len(self.senses) > 0
+
+	def __eq__(self, other):
+		res = isinstance(other, self.__class__) and self.lemma == other.lemma and self.lang == other.lang and self.pos ==other.pos and len(self.pronunciations) == len(other.pronunciations) and len(self.senses) == len(other.senses)
+		i = 0
+		while res and i < len(self.senses):
+			res = self.senses[i] == other.senses[i]
+			i += 1
+		i = 0
+		while res and i < len(self.pronunciations):
+			res = self.pronunciations[i] == other.pronunciations[i]
+			i += 1
+		return res
+
+	def serializable(self, id=True):
+		res = {}
+		res['sources'] = self.sources
+		if id:
+			id = self.get_id()
+			res['id'] = id
+		else:
+			id == None
+		res[self.lemma] = {"pos":self.pos}
+		res[self.lemma]["pronunciations"] = []
+		for p in self.pronunciations:
+			res[self.lemma]["pronunciations"].append(p.serializable(id))
+		res[self.lemma]["senses"] = {}
+		for s in self.senses:
+			res[self.lemma]["senses"][s.get_id()]=s.serializable(id)
+		return res
+
+	def __str__(self):
+		res = f"{self.lemma}_{self.lang} ({self.pos})\n"
+		for p in self.pronunciations:
+			res += f"{str(p)}\n"
+		for s in self.senses:
+			res += f"{str(s)}\n"
+		return res
+
+class ParserContext:
+	def __init__(self, entry, lang, wiki_lang, wversion_id, version_id):
+		self.lemma = entry
+		self.lang = lang
+		self.wiki_lang = wiki_lang
+		self.page_version_id = wversion_id
+		self.wikstraktor_version = version_id
+		self.context = []
+		self.entries = []
+
+	def get_level(self):
+		if len(self.context) == 0:
+			res = -1
+		else:
+			res = self.context[-1]["wiki"].level
+		return res
+
+	def push(self, wiki_context):
+		self.context.append({"wiki":wiki_context})
+
+	def pop(self, testNewEntry = True):
+		if testNewEntry:
+			self.create_entries()
+		return self.context.pop()
+
+	def flush(self):
+		while len(self.context) > 0:
+			self.pop(True)
+
+	def set_top_wiki(self, wiki_context):
+		if len(self.context) == 0:
+			self.push(wiki_context)
+		else:
+			self.context[-1]['wiki'] = wiki_context
+
+	def set_top_entry_info(self, key, entry_context, testNewEntry=True):
+		if len(self.context) == 0:
+			raise ValueError(f"Trying to set up entry info ({entry_context}), in an empty parserContext.")
+		else:
+			self.context[-1][key] = entry_context
+			if testNewEntry:
+				self.create_entries()
+
+	def create_entries(self):
+		#In the key dict there are traits that describe every thing (ety, pro) and different entities (POS:senses)
+		tmp = {}
+		res = 0
+		pro = None
+		for l in self.context:
+			for k,v in l.items():
+				if k == "pro":
+					pro = v
+				elif k == "ety" or k == "wiki":
+					#wiki context is not necessary
+					pass #On ignore l'étymologie pour le moment
+				else:
+					tmp[k]=v
+		if(pro!=None and len(tmp)>0):
+			for pos,senses in tmp.items():
+				e = Entry(self.lemma, self.lang, self.wiki_lang, self.page_version_id, self.wikstraktor_version)
+				e.set_pronunciations(pro)
+				e.set_pos(pos)
+				e.set_senses(senses)
+				#an improvement would be to remove that sense from context, but we test not to add doubles
+				if e.is_valid() and e not in self.entries:
+					res += 1
+					self.entries.append(e)
+		return res
+
+	def debug_top(self):
+		res = "Context: "
+		if len(self.context) == 0 :
+			res += "0"
+		else:
+			info = ""
+			for k,v in self.context[-1].items():
+				if k != 'wiki':
+					if info != "":
+						info += "\n\t\t\t"
+					info += f"{k} → {str(v)}"
+			res += f"{len(self.context)*'='} {self.context[-1]['wiki'].level*'#'} {self.context[-1]['wiki'].title} / {info}"
+		return res
+
+	def __str__(self):
+		res = ""
+		i=0
+		for c in self.context:
+			res += f"====={i}======\n"
+			for k,v in c.items():
+				if k!= "wiki":
+					res+=f"  {k}→{v}\n"
+				else:
+					res+=f"  {k}→{len(v)}\n"
+			i+=1
+		return res+f"nb of entries: {len(self.entries)}"
+
+
+
+class Wikstraktor:
+	@classmethod
+	def get_instance(cls, wiki_language, entry_language):
+		try:
+			m_name = f"{wiki_language}_{entry_language}".capitalize()
+			instance = getattr(importlib.import_module(f"parsers.{m_name.lower()}"), f"{m_name}_straktor")()
+			instance.version = the_version
+			instance.log = Wikstraklog(the_version, entry_language, wiki_language)
+		except ModuleNotFoundError:
+			print(f"parsers.{m_name.lower()} module not found or {m_name}_straktor not found in module")
+			instance = None
+		return instance
+
+	def __init__(self):
+		self.entries = []
+		self.pwb = pywikibot
+		self.wtp = wikitextparser
+		self.parserContext = None
+
+	def get_file_url(self, file_page_name):
+		res = None
+		try:
+			f = self.pwb.FilePage(self.site, file_page_name)
+			res = f.get_file_url()
+		except pywikibot.exceptions.NoPageError:
+			print(f"{file_page_name} does not exist in {self.site}.")
+		return res
+
+	#retrieves the content of a page and processes it (adding the entries to the list of entries)
+	#returns the number of entries added
+	def fetch(self, graphy):
+		nb_entries_added = 0
+		page = self.pwb.Page(self.site, graphy)
+		to_parse = []
+		if page.text != "":
+			sections = self.wtp.parse(page.text).sections
+			found = False
+			i = 0
+			### find language
+			while i < len(sections) and not found:
+				found = sections[i].title != None and sections[i].title.capitalize() == self.constants[self.entry_language]
+				if not found:
+					i += 1
+			if found:
+				nb_entries_added = self.parse(page.title(), page.latest_revision_id, sections[i].sections)#self.wtp.parse(s.contents).sections)
+		return nb_entries_added
+
+	def parse(self, entry, v_id, sections):
+		self.parserContext = ParserContext(entry, self.entry_language, self.wiki_language, v_id, self.version)
+		self.log.set_context(entry, v_id)
+		for s in sections:
+			if s.title != None :
+				#handle wiki context
+				if self.parserContext.get_level() < s.level:
+					self.parserContext.push(s)
+				else:
+					while self.parserContext.get_level() > s.level:
+						self.parserContext.pop(True)
+					self.parserContext.set_top_wiki(s)
+				#get section title
+				stitle = self.wtp.parse(s.title).templates
+				if stitle == []:
+					stitle = s.title
+				else:
+					stitle = stitle[0].arguments[0].value
+				if self.isPro(stitle):
+					self.parserContext.set_top_entry_info('pro', self.process_pronunciation(self.wtp.parse(s.contents)))
+				elif self.isEty(stitle):
+					self.parserContext.set_top_entry_info('ety', self.process_etymology(self.wtp.parse(s.contents)))
+				else:
+					#Edit to process other types of sections
+					pos = self.process_POS(stitle)
+					if pos != None :
+						self.parserContext.set_top_entry_info(pos, self.process_senses(self.wtp.parse(s.contents)))
+		self.parserContext.flush()
+		res = len(self.parserContext.entries)
+		if res > 0:
+			for e in self.parserContext.entries:
+				self.entries.append(e)
+		return res
+
+	def isPro(self, title):
+		if type(self.constants['pro']) == str:
+			res = title == self.constants['pro']
+		else:
+			res = title in self.constants['pro']
+		return res
+
+	def isEty(self, title):
+		if type(self.constants['ety']) == str:
+			res = title == self.constants['ety']
+		else:
+			res = title in self.constants['ety']
+		return res
+
+	#recognizes POS and returns None if it can't
+	def process_POS(self, parsedwikitext):
+		pass#in subclass
+
+	def process_pronunciation(self, parsedwikitext):
+		pass#in subclass
+
+	def process_etymology(self, parsedwikitext):
+		pass#in subclass
+
+	#can be overloaded
+	def process_example(self, example_wiki_text):
+		k = 0
+		isEx = 0
+		res = None
+		#process templates
+		while k < len(self.wtp.parse(example_wiki_text).templates) and isEx == 0 :
+			if (self.wtp.parse(example_wiki_text).templates[k].normal_name() in self.constants['t_ex']):
+				res = self.wtp.parse(example_wiki_text).templates[0].arguments[-1].value
+				isEx = 1
+			k += 1
+		if isEx == 0:
+			res = self.wtp.parse(example_wiki_text).plain_text().strip()
+		return res
+
+	#can be overloaded
+	def process_definition(self, definition, sub_items, def_level = True):
+		if def_level:
+			newSense = Sense(self.entry_language, self.wtp.parse(definition).plain_text().strip(), self.wiki_language)
+			pattern_ex = self.constants['sense_pattern'][0]["ex"]
+			pattern_subdef = self.constants['sense_pattern'][0]["add_subdef"] + self.constants['sense_pattern'][0]["def"]
+		else:
+			newSense = SubSense(self.entry_language, self.wtp.parse(definition).plain_text().strip(), self.wiki_language)
+			pattern_subdef = None
+			pattern_ex = self.constants['sense_pattern'][0]["add_subdef"] + self.constants['sense_pattern'][0]["ex"]
+		#Process examples
+		a = 0
+		#print(newSense, sub_items)# DEBUG:
+		for item_list in sub_items:
+			if item_list.pattern == pattern_ex:
+				for item in item_list.items:
+					newSense.add_example(self.process_example(item))
+					#Si on veut traiter les sous items (ex traductions), on peut utiliser
+					#item_list.sublists(a)
+			if def_level and item_list.pattern == pattern_subdef:
+				for item in item_list.items:
+					newSense.add_subsense(self.process_definition(item, item_list.sublists(a), False))
+			a += 1
+		return newSense
+
+	def process_senses(self, sensesContent):
+		l = sensesContent.get_lists((self.constants['sense_pattern'][0]["def"]))
+		senses = []
+		if len(l) > 1:
+			self.log.add_log("Wikstraktor.process_senses", f"===== WARNING ======\nmore than one sense list, make sure we don't forget anything\nignored lists : \n{l[1:]}\n===================")
+		l = l[0] #l now contains a list of list items
+		if l.pattern == self.constants['sense_pattern'][0]["def"]:
+			i = 0
+			for item in l.items:
+				senses.append(self.process_definition(item, l.sublists(i)))
+				i += 1
+		return senses
+
+	def __str__(self):
+		return self.export()
+
+	def export(self, id=True, ascii=False, compact=False):
+		res = []
+		for e in self.entries:
+			res.append(e.serializable(id))
+		if compact:
+			return json.dumps(res, ensure_ascii=ascii)
+		else:
+			return json.dumps(res, ensure_ascii=ascii, indent=4)
+
+if __name__ == "__main__":
+	import argparse
+	from argparse import RawTextHelpFormatter #pour le formattage de l'aide
+	parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, description="""Interroger un wiktionnaire
+	\033[1m\033[32mex :\033[0m
+	‣\033[0m\033[32m./wikstraktor.py -m blue\033[0m
+	‣\033[0m\033[32m./wikstraktor.py -m blue -f blue.json -A -C\033[0m
+	‣\033[0m\033[32m./wikstraktor.py -l en -w fr -m blue -f blue.json -n -A -C\033[0m""")
+	parser.add_argument("-l", "--language",  help="la langue du mot", type=str, default = "en")
+	parser.add_argument("-w", "--wiki_language",  help="la langue du wiki", type=str, default = "en")
+	parser.add_argument("-m", "--mot",  help="le mot à chercher", type=str, default=None)
+	parser.add_argument("-f", "--destination_file", help="le fichier dans lequel stocker le résultat", type=str, default=None)
+	parser.add_argument("-A", "--force_ascii", help="json avec que des caractères ascii", action="store_true")
+	parser.add_argument("-C", "--compact", help="json sans indentation", action="store_true")
+	parser.add_argument("-n", "--no_id", help="json sans id", action="store_true")
+	args = parser.parse_args()
+	if args.mot != None:
+		w = Wikstraktor.get_instance(args.wiki_language, args.language)
+		resp = None
+		if w.fetch(args.mot) > 0:
+			resp = w.export(not args.no_id, args.force_ascii, args.compact)
+		if args.destination_file != None:
+			f = open(args.destination_file, "w")
+			f.write(resp)
+			f.close
+		else:
+			print(resp)
+	else:
+		raise NameError("Pas de mot demandé")
diff --git a/templates/lexicon/show.html.twig b/templates/lexicon/show.html.twig
index 8df5ddd900cb37d38dd221d6a74fe4dc53742e6b..f208143758d94ac1a3c50ba87e2a837e5a07fa83 100644
--- a/templates/lexicon/show.html.twig
+++ b/templates/lexicon/show.html.twig
@@ -35,7 +35,7 @@
                                 <tr>
                                     <td>{{ entry }}</td>
                                     <td>{% for label in entry.headword.morphologicalLabels %}<span class="badge bg-primary">{{ label }}</span> {% endfor %}</td>
-                                    <td>{% for label in entry.headword.thematicLabels %}<span class="badge bg-secondary">{{ label }}</span> {% endfor %}</td>
+                                    <td>{% for label in entry.headword.generalLabels %}<span class="badge bg-secondary">{{ label }}</span> {% endfor %}</td>
                                     <td>{% for label in entry.headword.listLabels %}<span class="badge bg-info">{{ label }}</span> {% endfor %}</td>
                                     <td>{% for label in entry.headword.milestoneLabels %}<span class="badge bg-warning">{{ label }}</span> {% endfor %}</td>
                                     <td>