mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
List duplicate persons
Signed-off-by: Mathieu Jaumotte <mathieu.jaumotte@champs-libres.coop> Signed-off-by: Mathieu Jaumotte <mathieu.jaumotte@champs-libres.coop>
This commit is contained in:
parent
0cc951b296
commit
728ea73bdf
@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\PersonNotDuplicate;
|
||||
use Chill\PersonBundle\Form\PersonConfimDuplicateType;
|
||||
use Chill\PersonBundle\Repository\PersonRepository;
|
||||
use Chill\PersonBundle\Search\SimilarPersonMatcher;
|
||||
use http\Exception\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
class PersonDuplicateController extends Controller
|
||||
{
|
||||
/**
|
||||
* @var \Chill\PersonBundle\Search\SimilarPersonMatcher
|
||||
*/
|
||||
private $similarPersonMatcher;
|
||||
|
||||
/**
|
||||
* @var \Symfony\Component\Translation\TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* @var \Chill\PersonBundle\Repository\PersonRepository
|
||||
*/
|
||||
private $personRepository;
|
||||
|
||||
public function __construct(
|
||||
SimilarPersonMatcher $similarPersonMatcher,
|
||||
TranslatorInterface $translator,
|
||||
PersonRepository $personRepository
|
||||
) {
|
||||
$this->similarPersonMatcher = $similarPersonMatcher;
|
||||
$this->translator = $translator;
|
||||
$this->personRepository = $personRepository;
|
||||
}
|
||||
|
||||
public function viewAction($person_id)
|
||||
{
|
||||
$person = $this->_getPerson($person_id);
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("Person with id $person_id not"
|
||||
. " found on this server");
|
||||
}
|
||||
|
||||
$duplicatePersons = $this->similarPersonMatcher->matchPerson($person, 0.5);
|
||||
|
||||
return $this->render('ChillPersonBundle:PersonDuplicate:view.html.twig', [
|
||||
"person" => $person,
|
||||
'duplicatePersons' => $duplicatePersons
|
||||
]);
|
||||
}
|
||||
|
||||
public function confirmAction($person_id, $person2_id, Request $request)
|
||||
{
|
||||
if ($person_id === $person2_id) {
|
||||
throw new InvalidArgumentException('Can not merge same person');
|
||||
}
|
||||
|
||||
if ($person_id > $person2_id) {
|
||||
$tmpId = $person2_id;
|
||||
$person2_id = $person_id;
|
||||
$person_id = $tmpId;
|
||||
unset($tmpId);
|
||||
}
|
||||
|
||||
$person = $this->_getPerson($person_id);
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("Person with id $person_id not"
|
||||
. " found on this server");
|
||||
}
|
||||
|
||||
$person2 = $this->_getPerson($person2_id);
|
||||
if ($person2 === null) {
|
||||
throw $this->createNotFoundException("Person with id $person2_id not"
|
||||
. " found on this server");
|
||||
}
|
||||
|
||||
$form = $this->createForm(PersonConfimDuplicateType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
dd('todo');
|
||||
}
|
||||
|
||||
return $this->render('ChillPersonBundle:PersonDuplicate:confirm.html.twig', [
|
||||
'person' => $person,
|
||||
'person2' => $person2,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function notDuplicateAction($person_id, $person2_id)
|
||||
{
|
||||
if ($person_id === $person2_id) {
|
||||
throw new InvalidArgumentException('Can not merge same person');
|
||||
}
|
||||
|
||||
if ($person_id > $person2_id) {
|
||||
$tmpId = $person2_id;
|
||||
$person2_id = $person_id;
|
||||
$person_id = $tmpId;
|
||||
unset($tmpId);
|
||||
}
|
||||
|
||||
$person = $this->_getPerson($person_id);
|
||||
if ($person === null) {
|
||||
throw $this->createNotFoundException("Person with id $person_id not"
|
||||
. " found on this server");
|
||||
}
|
||||
|
||||
$person2 = $this->_getPerson($person2_id);
|
||||
if ($person2 === null) {
|
||||
throw $this->createNotFoundException("Person with id $person2_id not"
|
||||
. " found on this server");
|
||||
}
|
||||
|
||||
$personNotDuplicate = new PersonNotDuplicate();
|
||||
$personNotDuplicate->setPerson1($person);
|
||||
$personNotDuplicate->setPerson2($person2);
|
||||
$personNotDuplicate->setUser($this->getUser());
|
||||
|
||||
$this->getDoctrine()->getManager()->persist($personNotDuplicate);
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
return $this->redirectToRoute('chill_person_duplicate_view', ['person_id' => $person->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* easy getting a person by his id
|
||||
*/
|
||||
private function _getPerson($id): ?Person
|
||||
{
|
||||
return $this->personRepository->find($id);
|
||||
}
|
||||
}
|
107
src/Bundle/ChillPersonBundle/Entity/PersonNotDuplicate.php
Normal file
107
src/Bundle/ChillPersonBundle/Entity/PersonNotDuplicate.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* PersonNotDuplicate
|
||||
*
|
||||
* @ORM\Table(name="chill_person_not_duplicate")
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class PersonNotDuplicate
|
||||
{
|
||||
/**
|
||||
* The person's id
|
||||
* @var integer
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Person
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
|
||||
*/
|
||||
private $person1;
|
||||
|
||||
/**
|
||||
* @var Person
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\Person")
|
||||
*/
|
||||
private $person2;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(type="datetime")
|
||||
*/
|
||||
private $date;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->date = new \DateTime();
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getPerson1()
|
||||
{
|
||||
return $this->person1;
|
||||
}
|
||||
|
||||
public function setPerson1(Person $person1)
|
||||
{
|
||||
$this->person1 = $person1;
|
||||
}
|
||||
|
||||
public function getPerson2()
|
||||
{
|
||||
return $this->person2;
|
||||
}
|
||||
|
||||
public function setPerson2(Person $person2)
|
||||
{
|
||||
$this->person2 = $person2;
|
||||
}
|
||||
|
||||
public function getDate()
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function setDate(\DateTime $date)
|
||||
{
|
||||
$this->date = $date;
|
||||
}
|
||||
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function setUser(User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class PersonConfimDuplicateType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
* @param array $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('confirm', CheckboxType::class, [
|
||||
'label' => 'Je confirme la fusion de ces 2 personnes',
|
||||
'mapped' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'chill_personbundle_person_confirm_duplicate';
|
||||
}
|
||||
}
|
@ -63,6 +63,16 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||
->setExtras([
|
||||
'order' => 50
|
||||
]);
|
||||
|
||||
$menu->addChild($this->translator->trans('Person duplicate'), [
|
||||
'route' => 'chill_person_duplicate_view',
|
||||
'routeParameters' => [
|
||||
'person_id' => $parameters['person']->getId()
|
||||
]
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 51
|
||||
]);
|
||||
|
||||
if ($this->showAccompanyingPeriod === 'visible') {
|
||||
$menu->addChild($this->translator->trans('Accompanying period list'), [
|
||||
|
@ -0,0 +1,36 @@
|
||||
{% extends "@ChillPerson/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_person_duplicate' %}
|
||||
|
||||
{% block title %}{{ 'Person duplicate'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~
|
||||
' ' ~ person.lastName }}{% endblock %}
|
||||
|
||||
{% block personcontent %}
|
||||
<h2>Ancien dossier</h2>
|
||||
{{ person2 }}
|
||||
<a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : person2.id }) }}"></a>
|
||||
|
||||
|
||||
<h2>Nouveau Dossier</h2>
|
||||
{{ person }}
|
||||
<a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : person.id }) }}"></a>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_rest(form) }}
|
||||
|
||||
<ul class="grid-12 sticky-form-buttons record_actions ">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_person_duplicate_view', {'person_id' : person.id}) }}" class="sc-button grey center margin-5">
|
||||
<i class="fa fa-arrow-left"></i>
|
||||
{{ 'Return'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="sc-button bt-save" type="submit">{{ 'Confirm'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,65 @@
|
||||
{% extends "@ChillPerson/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_person_duplicate' %}
|
||||
|
||||
{% block title %}{{ 'Person duplicate'|trans|capitalize ~ ' ' ~ person.firstName|capitalize ~
|
||||
' ' ~ person.lastName }}{% endblock %}
|
||||
|
||||
|
||||
{% block personcontent %}
|
||||
|
||||
<h2>{{ title|default('Person duplicate')|trans }}</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="chill-red">{% trans %}Name{% endtrans %}</th>
|
||||
<th class="chill-green">{% trans %}Date of birth{% endtrans %}</th>
|
||||
<th class="chill-orange">{% trans %}Nationality{% endtrans %}</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for duplicatePerson in duplicatePersons %}
|
||||
<tr>
|
||||
<td>
|
||||
{% set is_open = duplicatePerson.isOpen() %}
|
||||
<a href="{{ path('chill_person_view', { person_id : duplicatePerson.getId }) }}" {% if chill_person.fields.accompanying_period == 'visible' %}{% if is_open %} alt="{{ 'An accompanying period is open'|trans|e('html_attr') }}"{% else %} alt="{{ 'Any accompanying periods are open'|trans|e('html_attr') }}" {% endif %}{% endif %}>
|
||||
{{ duplicatePerson|chill_entity_render_box }}
|
||||
{% apply spaceless %}
|
||||
{% if chill_person.fields.accompanying_period == 'visible' %}
|
||||
{% if is_open == false %}
|
||||
<i class="fa fa-lock" ></i>
|
||||
{% else %}
|
||||
<i class="fa fa-unlock" ></i>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endapply %}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if duplicatePerson.birthdate is not null %}
|
||||
{{ duplicatePerson.birthdate|format_date('long') }}
|
||||
{% else %}
|
||||
{{ 'Unknown date of birth'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if duplicatePerson.nationality is not null %}
|
||||
{{ duplicatePerson.nationality.name|localize_translatable_string }}
|
||||
{% else %}
|
||||
{{ 'Without nationality'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li><a class="sc-button bt-show" target="_blank" href="{{ path('chill_person_view', { person_id : duplicatePerson.id }) }}"></a></li>
|
||||
<li><a class="sc-button bt-duplicate" href="{{ path('chill_person_duplicate_confirm', { person_id : person.id, person2_id : duplicatePerson.id }) }}"></a></li>
|
||||
<li><a class="sc-button bt-not-duplicate" href="{{ path('chill_person_duplicate_not_duplicate', {person_id : person.id, person2_id : duplicatePerson.id}) }}"></a></li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endblock %}
|
@ -61,35 +61,32 @@ class SimilarPersonMatcher
|
||||
}
|
||||
|
||||
|
||||
public function matchPerson(Person $person)
|
||||
public function matchPerson(Person $person, $precision = 0.15)
|
||||
{
|
||||
$centers = $this->authorizationHelper
|
||||
->getReachableCenters(
|
||||
$this->tokenStorage->getToken()->getUser(),
|
||||
new Role(PersonVoter::SEE)
|
||||
);
|
||||
$centers = $this->authorizationHelper->getReachableCenters(
|
||||
$this->tokenStorage->getToken()->getUser(),
|
||||
new Role(PersonVoter::SEE)
|
||||
);
|
||||
|
||||
$dql = 'SELECT p from ChillPersonBundle:Person p WHERE'
|
||||
. ' ('
|
||||
. ' UNACCENT(LOWER(p.firstName)) LIKE UNACCENT(LOWER(:firstName)) '
|
||||
. ' OR UNACCENT(LOWER(p.lastName)) LIKE UNACCENT(LOWER(:lastName)) '
|
||||
. ' OR UNACCENT(LOWER(p.firstName)) LIKE UNACCENT(LOWER(:lastName)) '
|
||||
. ' OR UNACCENT(LOWER(p.lastName)) LIKE UNACCENT(LOWER(:firstName)) '
|
||||
. ' OR SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) >= 0.15 '
|
||||
$dql = 'SELECT p from ChillPersonBundle:Person p '
|
||||
. ' WHERE ('
|
||||
. ' SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) >= :precision '
|
||||
. ' OR SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullNameInverted))) >= :precision '
|
||||
. ' ) '
|
||||
. ' AND p.center IN (:centers)'
|
||||
. ' AND p.id != :personId'
|
||||
. ' ORDER BY SIMILARITY(p.fullnameCanonical, UNACCENT(LOWER(:fullName))) DESC '
|
||||
;
|
||||
;
|
||||
|
||||
$query =
|
||||
$this->em
|
||||
$query = $this->em
|
||||
->createQuery($dql)
|
||||
->setParameter('firstName', $person->getFirstName())
|
||||
->setParameter('lastName', $person->getLastName())
|
||||
->setParameter('fullName', $person->getFirstName() . ' ' . $person->getLastName())
|
||||
->setParameter('fullNameInverted', $person->getLastName() . ' ' . $person->getFirstName())
|
||||
->setParameter('centers', $centers)
|
||||
;
|
||||
|
||||
->setParameter('personId', $person->getId())
|
||||
->setParameter('precision', $precision)
|
||||
;
|
||||
|
||||
return $query->getResult();
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,10 @@ chill_person_admin_redirect_to_admin_index:
|
||||
order: 0
|
||||
label: Main admin menu
|
||||
|
||||
chill_person_duplicate_view:
|
||||
path: /{_locale}/person/{person_id}/duplicate/view
|
||||
controller: Chill\PersonBundle\Controller\PersonDuplicateController::viewAction
|
||||
|
||||
chill_person_closingmotive_admin:
|
||||
path: /{_locale}/admin/closing-motive
|
||||
controller: cscrud_closing_motive_controller:index
|
||||
@ -107,6 +111,10 @@ chill_person_closingmotive_admin:
|
||||
order: 90
|
||||
label: 'person_admin.closing motives'
|
||||
|
||||
chill_person_duplicate_confirm:
|
||||
path: /{_locale}/person/{person_id}/duplicate/{person2_id}/confirm
|
||||
controller: Chill\PersonBundle\Controller\PersonDuplicateController::confirmAction
|
||||
|
||||
chill_person_maritalstatus_admin:
|
||||
path: /{_locale}/admin/marital-status
|
||||
controller: cscrud_marital_status_controller:index
|
||||
@ -114,4 +122,8 @@ chill_person_maritalstatus_admin:
|
||||
menus:
|
||||
admin_person:
|
||||
order: 120
|
||||
label: 'person_admin.marital status'
|
||||
label: 'person_admin.marital status'
|
||||
|
||||
chill_person_duplicate_not_duplicate:
|
||||
path: /{_locale}/person/{person_id}/duplicate/{person2_id}/not-duplicate
|
||||
controller: Chill\PersonBundle\Controller\PersonDuplicateController::notDuplicateAction
|
||||
|
@ -20,5 +20,12 @@ services:
|
||||
arguments:
|
||||
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
|
||||
Chill\PersonBundle\Controller\AdminController: ~
|
||||
|
||||
Chill\PersonBundle\Controller\PersonDuplicateController:
|
||||
arguments:
|
||||
$similarPersonMatcher: '@Chill\PersonBundle\Search\SimilarPersonMatcher'
|
||||
$translator: '@Symfony\Component\Translation\TranslatorInterface'
|
||||
$personRepository: '@Chill\PersonBundle\Repository\PersonRepository'
|
||||
tags: ['controller.service_arguments']
|
||||
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20210128152747 extends AbstractMigration
|
||||
{
|
||||
public function getDescription() : string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE SEQUENCE chill_person_not_duplicate_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||
$this->addSql('CREATE TABLE chill_person_not_duplicate (id INT NOT NULL, person1_id INT DEFAULT NULL, person2_id INT DEFAULT NULL, user_id INT DEFAULT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('CREATE INDEX IDX_BD211EE23EF5821B ON chill_person_not_duplicate (person1_id)');
|
||||
$this->addSql('CREATE INDEX IDX_BD211EE22C402DF5 ON chill_person_not_duplicate (person2_id)');
|
||||
$this->addSql('CREATE INDEX IDX_BD211EE2A76ED395 ON chill_person_not_duplicate (user_id)');
|
||||
$this->addSql('ALTER TABLE chill_person_not_duplicate ADD CONSTRAINT FK_BD211EE23EF5821B FOREIGN KEY (person1_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_person_not_duplicate ADD CONSTRAINT FK_BD211EE22C402DF5 FOREIGN KEY (person2_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('ALTER TABLE chill_person_not_duplicate ADD CONSTRAINT FK_BD211EE2A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
}
|
||||
|
||||
public function down(Schema $schema) : void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP SEQUENCE chill_person_not_duplicate_id_seq CASCADE');
|
||||
$this->addSql('DROP TABLE chill_person_not_duplicate');
|
||||
}
|
||||
}
|
@ -66,6 +66,7 @@ Reset: 'Remise à zéro'
|
||||
'Create accompanying period': 'Create accompanying period'
|
||||
'Closing motive': 'Motif de clôture'
|
||||
'Person details': 'Détails de la personne'
|
||||
'Person duplicate': 'Find duplicate'
|
||||
|
||||
Create an accompanying period: Create an accompanying period
|
||||
'Create': Create
|
||||
|
@ -119,6 +119,7 @@ Update accompanying period: Mettre à jour une période d'accompagnement
|
||||
"Period not opened : form is invalid": "La période n'a pas été ouverte: le formulaire est invalide."
|
||||
'Closing motive': 'Motif de clôture'
|
||||
'Person details': 'Détails de la personne'
|
||||
'Person duplicate': 'Trouver des doublons'
|
||||
'Update details for %name%': 'Modifier détails de %name%'
|
||||
Any accompanying periods are open: Aucune période d'accompagnement ouverte
|
||||
An accompanying period is open: Une période d'accompagnement est ouverte
|
||||
|
Loading…
x
Reference in New Issue
Block a user