Compare commits
6 Commits
feature/ge
...
main
Author | SHA1 | Date | |
---|---|---|---|
11bcd9a83d | |||
71ddec420c | |||
4f19bac707 | |||
ba82630d8a | |||
![]() |
b0507a44ea | ||
![]() |
beca5f92da |
@@ -30,7 +30,3 @@ services:
|
|||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
# please note that last definitions always *replace* previous ones
|
# please note that last definitions always *replace* previous ones
|
||||||
|
|
||||||
when@prod:
|
|
||||||
services:
|
|
||||||
App\Service\ImageSafetyServiceInterface: '@App\Service\SightEngineImageSafetyService'
|
|
||||||
|
24
public/css/app.css
Normal file
24
public/css/app.css
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
.no-style {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-style:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-4 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.grid-4 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
27
public/js/like_toggle.js
Normal file
27
public/js/like_toggle.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
document.querySelectorAll('.like-toggle').forEach(button => {
|
||||||
|
button.addEventListener('click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let isLiked = this.classList.contains('liked');
|
||||||
|
let url = isLiked ? this.dataset.unlikeUrl : this.dataset.likeUrl;
|
||||||
|
|
||||||
|
fetch(url, { method: 'POST' })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let likesCountElement = this.parentElement.querySelector('.likes-count');
|
||||||
|
likesCountElement.textContent = data.likesCount;
|
||||||
|
this.classList.toggle('liked');
|
||||||
|
this.classList.toggle('not-liked');
|
||||||
|
this.innerHTML = isLiked ? '♡' : '❤️';
|
||||||
|
} else {
|
||||||
|
console.error('Erreur lors du traitement du like/unlike.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Erreur lors de la requête fetch:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -16,9 +16,16 @@ use Symfony\Component\Routing\Attribute\Route;
|
|||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||||
use Symfony\UX\Turbo\TurboBundle;
|
use Symfony\UX\Turbo\TurboBundle;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRUD on posts and comments.
|
||||||
|
*/
|
||||||
class PostController extends AbstractController
|
class PostController extends AbstractController
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The number of item on a page for the pagination.
|
||||||
|
*/
|
||||||
private const POSTS_PER_PAGE = 10;
|
private const POSTS_PER_PAGE = 10;
|
||||||
|
|
||||||
#[Route('/', name: 'app_posts')]
|
#[Route('/', name: 'app_posts')]
|
||||||
@@ -35,7 +42,7 @@ class PostController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/posts', name: 'app_post_index', methods: ['GET'])]
|
#[Route('/posts', name: 'app_post_index', methods: ['GET'])]
|
||||||
public function table(PostRepository $repository): Response
|
public function table(): Response
|
||||||
{
|
{
|
||||||
return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER);
|
return $this->redirectToRoute('app_posts', [], Response::HTTP_SEE_OTHER);
|
||||||
}
|
}
|
||||||
@@ -170,4 +177,29 @@ class PostController extends AbstractController
|
|||||||
}
|
}
|
||||||
return $this->redirectToRoute('app_post_show', ['id' => $comment->getRelatedPost()->getId()]);
|
return $this->redirectToRoute('app_post_show', ['id' => $comment->getRelatedPost()->getId()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/post/{id}/like', name: 'app_posts_like', methods: ['POST'])]
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
public function addLike(#[CurrentUser] User $user, Post $post, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$user->addLikedPost($post);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$likesCount = $post->getLikes()->count();
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true, 'likesCount' => $likesCount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/post/{id}/unlike', name: 'app_posts_unlike', methods: ['POST'])]
|
||||||
|
#[IsGranted('ROLE_USER')]
|
||||||
|
public function deleteLike(#[CurrentUser] User $user, Post $post, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$user->removeLikedPost($post);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$likesCount = $post->getLikes()->count();
|
||||||
|
|
||||||
|
return new JsonResponse(['success' => true, 'likesCount' => $likesCount]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -10,6 +10,9 @@ use Doctrine\Bundle\FixturesBundle\Fixture;
|
|||||||
use Doctrine\Persistence\ObjectManager;
|
use Doctrine\Persistence\ObjectManager;
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates fake data for testing purposes.
|
||||||
|
*/
|
||||||
class AppFixtures extends Fixture
|
class AppFixtures extends Fixture
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
@@ -20,10 +23,12 @@ class AppFixtures extends Fixture
|
|||||||
|
|
||||||
public function load(ObjectManager $manager): void
|
public function load(ObjectManager $manager): void
|
||||||
{
|
{
|
||||||
|
// Dummy user
|
||||||
$user = (new User())->setEmail('test@test.fr');
|
$user = (new User())->setEmail('test@test.fr');
|
||||||
$user->setPassword($this->passwordHasher->hashPassword($user, 'password'));
|
$user->setPassword($this->passwordHasher->hashPassword($user, 'password'));
|
||||||
$manager->persist($user);
|
$manager->persist($user);
|
||||||
|
|
||||||
|
// Posts and their species
|
||||||
$faker = \Faker\Factory::create();
|
$faker = \Faker\Factory::create();
|
||||||
for ($i = 0; $i < 20; ++$i) {
|
for ($i = 0; $i < 20; ++$i) {
|
||||||
$name = $faker->name();
|
$name = $faker->name();
|
||||||
|
@@ -84,9 +84,16 @@ class Post
|
|||||||
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'related_post', fetch: 'EXTRA_LAZY')]
|
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'related_post', fetch: 'EXTRA_LAZY')]
|
||||||
private Collection $comments;
|
private Collection $comments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, User>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'liked_post')]
|
||||||
|
private Collection $likes;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->comments = new ArrayCollection();
|
$this->comments = new ArrayCollection();
|
||||||
|
$this->likes = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -256,4 +263,28 @@ class Post
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, User>
|
||||||
|
*/
|
||||||
|
public function getLikes(): Collection
|
||||||
|
{
|
||||||
|
return $this->likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addLike(User $user): static
|
||||||
|
{
|
||||||
|
if (!$this->likes->contains($user)) {
|
||||||
|
$this->likes->add($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeLike(User $user): static
|
||||||
|
{
|
||||||
|
$this->likes->removeElement($user);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -68,9 +68,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'author')]
|
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'author')]
|
||||||
private Collection $comments;
|
private Collection $comments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Post>
|
||||||
|
*/
|
||||||
|
#[ORM\ManyToMany(targetEntity: Post::class, mappedBy: 'likes')]
|
||||||
|
private Collection $liked_post;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->comments = new ArrayCollection();
|
$this->comments = new ArrayCollection();
|
||||||
|
$this->liked_post = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -189,4 +196,31 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, Post>
|
||||||
|
*/
|
||||||
|
public function getLikedPost(): Collection
|
||||||
|
{
|
||||||
|
return $this->liked_post;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addLikedPost(Post $likedPost): static
|
||||||
|
{
|
||||||
|
if (!$this->liked_post->contains($likedPost)) {
|
||||||
|
$this->liked_post->add($likedPost);
|
||||||
|
$likedPost->addLike($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeLikedPost(Post $likedPost): static
|
||||||
|
{
|
||||||
|
if ($this->liked_post->removeElement($likedPost)) {
|
||||||
|
$likedPost->removeLike($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,9 @@ use Symfony\Component\HttpKernel\Event\RequestEvent;
|
|||||||
use Symfony\Component\HttpKernel\KernelEvents;
|
use Symfony\Component\HttpKernel\KernelEvents;
|
||||||
use Symfony\Component\Translation\LocaleSwitcher;
|
use Symfony\Component\Translation\LocaleSwitcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the locale from the user session and change it for every request.
|
||||||
|
*/
|
||||||
final readonly class LocaleListener
|
final readonly class LocaleListener
|
||||||
{
|
{
|
||||||
public function __construct(private LocaleSwitcher $localeSwitcher)
|
public function __construct(private LocaleSwitcher $localeSwitcher)
|
||||||
|
@@ -6,6 +6,9 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
|||||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only allow admins or comment owners to edit their comments.
|
||||||
|
*/
|
||||||
class CommentVoter extends Voter
|
class CommentVoter extends Voter
|
||||||
{
|
{
|
||||||
public const EDIT = 'COMMENT_EDIT';
|
public const EDIT = 'COMMENT_EDIT';
|
||||||
|
@@ -4,6 +4,9 @@ namespace App\Service;
|
|||||||
|
|
||||||
use Symfony\Component\HttpFoundation\File\File;
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that an image is safe.
|
||||||
|
*/
|
||||||
interface ImageSafetyServiceInterface
|
interface ImageSafetyServiceInterface
|
||||||
{
|
{
|
||||||
public function isValid(File $file): bool;
|
public function isValid(File $file): bool;
|
||||||
|
@@ -8,6 +8,8 @@ use App\Entity\User;
|
|||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Hashes plain text password in the API.
|
||||||
|
*
|
||||||
* @implements ProcessorInterface<User, User>
|
* @implements ProcessorInterface<User, User>
|
||||||
*/
|
*/
|
||||||
final readonly class UserPasswordHasher implements ProcessorInterface
|
final readonly class UserPasswordHasher implements ProcessorInterface
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{% set route = app.request.attributes.get('_route') %}
|
{% set route = app.request.attributes.get('_route') %}
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="pagination">
|
<ul class="pagination justify-content-center mt-4">
|
||||||
<li class="page-item {{ page < 2 ? 'disabled' }}">
|
<li class="page-item {{ page < 2 ? 'disabled' }}">
|
||||||
<a class="page-link" href="{{ path(route, {'page': page - 1}) }}">{{ 'previous'|trans }}</a>
|
<a class="page-link" href="{{ path(route, {'page': page - 1}) }}">{{ 'previous'|trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
@@ -37,9 +38,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="navbar-nav ms-auto">
|
<div class="d-flex">
|
||||||
<a class="nav-link" href="{{ path('app_setting_locale', { 'locale': 'en' }) }}">English</a>
|
<a class="nav-link me-2" href="{{ path('app_setting_locale', { 'locale': 'en' }) }}">English</a>
|
||||||
<a class="nav-link" href="{{ path('app_setting_locale', { 'locale': 'fr' }) }}">Français</a>
|
<a class="nav-link me-2" href="{{ path('app_setting_locale', { 'locale': 'fr' }) }}">Français</a>
|
||||||
{% if app.user %}
|
{% if app.user %}
|
||||||
<a class="nav-link" href="{{ path('app_logout') }}">{{ app.user.email }} - {{ 'log_out'|trans }}</a>
|
<a class="nav-link" href="{{ path('app_logout') }}">{{ app.user.email }} - {{ 'log_out'|trans }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@@ -2,9 +2,9 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
{{ comment.author.email }} le {{ comment.createdAt | date }}
|
{{ 'commented_at'|trans({ email: comment.author.email, date: comment.createdAt|date }) }}
|
||||||
{% if comment.createdAt != comment.editedAt %}
|
{% if comment.createdAt != comment.editedAt %}
|
||||||
(modifié le {{ comment.editedAt | date }})
|
{{ 'edited_at'|trans({ date: comment.editedAt|date }) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<p class="card-text">{{ comment.content }}</p>
|
<p class="card-text">{{ comment.content }}</p>
|
||||||
|
@@ -3,19 +3,44 @@
|
|||||||
{% block title %}{{ 'posts'|trans }}{% endblock %}
|
{% block title %}{{ 'posts'|trans }}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% if app.user %}
|
||||||
|
<div class="d-grid gap-2 d-md-flex justify-content-md-end mb-2">
|
||||||
|
<a href="{{ path('app_post_new') }}" class="btn btn-primary">{{ 'create_new_post'|trans }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="grid-4">
|
||||||
{% for post in posts.iterator %}
|
{% for post in posts.iterator %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
{% if post.image %}
|
||||||
|
<img src="{{ app.request.baseUrl }}{{ vich_uploader_asset(post, 'imageFile') }}" class="card-img-top" alt="">
|
||||||
|
{% endif %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title"><a href="{{ path('app_post_show', {id: post.id}) }}">{{ post.species ? post.species.vernacularName : 'post_undefined'|trans }}</a></h5>
|
<h5 class="card-title"><a href="{{ path('app_post_show', {id: post.id}) }}">{{ post.species ? post.species.vernacularName : 'post_undefined'|trans }}</a></h5>
|
||||||
<h6 class="card-subtitle mb-2 text-muted">{{ post.foundDate | date("d/m/Y \\à H \\h") }}</h6>
|
<h6 class="card-subtitle mb-2 text-muted">{{ post.foundDate | date("d/m/Y \\à H \\h") }}</h6>
|
||||||
<p class="card-subtitle mb-2 text-muted">{{ post.latitude }}, {{ post.longitude }}, {{ post.altitude }}m</p>
|
<p class="card-subtitle mb-2 text-muted">{{ post.latitude ?? 0 }}, {{ post.longitude ?? 0 }}, {{ post.altitude ?? 0 }}m</p>
|
||||||
<p class="card-text">{{ post.commentary }}</p>
|
<p class="card-text">{{ post.commentary }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
28 ❤️
|
<span class="likes-count">{{ post.likes.count() }}</span>
|
||||||
|
{% if app.user %}
|
||||||
|
<button class="like-toggle btn no-style {% if post.likes.contains(app.user) %}liked{% else %}not-liked{% endif %}"
|
||||||
|
data-post-id="{{ post.id }}"
|
||||||
|
data-like-url="{{ path('app_posts_like', {id: post.id}) }}"
|
||||||
|
data-unlike-url="{{ path('app_posts_unlike', {id: post.id}) }}">
|
||||||
|
{% if post.likes.contains(app.user) %}❤️{% else %}♡{% endif %}
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="like-toggle no-style not-liked">♡</span>
|
||||||
|
{% endif %}
|
||||||
{{ post.comments.count() }} 💬
|
{{ post.comments.count() }} 💬
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% include '_pagination.html.twig' %}
|
{% include '_pagination.html.twig' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{{ parent() }}
|
||||||
|
<script src="{{ asset('js/like_toggle.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
@@ -34,7 +34,8 @@
|
|||||||
{% if post.image %}
|
{% if post.image %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Image</th>
|
<th>Image</th>
|
||||||
<td><img src="{{ vich_uploader_asset(post, 'imageFile') }}" class="img-thumbnail" width="200" alt=""></td>
|
{# Vich doesn't prefix the path, as asset() would. #}
|
||||||
|
<td><img src="{{ app.request.baseUrl }}{{ vich_uploader_asset(post, 'imageFile') }}" class="img-thumbnail" width="200" alt=""></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
|
@@ -161,6 +161,14 @@
|
|||||||
<source>save</source>
|
<source>save</source>
|
||||||
<target>Save</target>
|
<target>Save</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="zdhzvdha" resname="commented_at">
|
||||||
|
<source>commented_at</source>
|
||||||
|
<target>email on date</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="zdhzvdht" resname="edited_at">
|
||||||
|
<source>edited_at</source>
|
||||||
|
<target>edited on date</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
@@ -161,6 +161,14 @@
|
|||||||
<source>save</source>
|
<source>save</source>
|
||||||
<target>Sauvegarder</target>
|
<target>Sauvegarder</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="zdhzvdha" resname="commented_at">
|
||||||
|
<source>commented_at</source>
|
||||||
|
<target>email le date</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="zdhzvdht" resname="edited_at">
|
||||||
|
<source>edited_at</source>
|
||||||
|
<target>édité le date</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
Reference in New Issue
Block a user