templates/moderation/translations.html.twig line 1

Open in your IDE?
  1. {% extends 'base.html.twig' %}
  2. {# Les nouveaux en premier #}
  3. {% set translations = translations|sort((a, b) => (a.redirectUrl == redirectUrl ? -1 : 1) <=> (b.redirectUrl == redirectUrl ? -1 : 1)) %}
  4. {% set locales_icons = {
  5.    'FR_FR': '🇫🇷',
  6.    'EN_GB': '🇬🇧',
  7.    'ES_ES': '🇪🇸',
  8.    'DE_DE': '🇩🇪',
  9.    'PT_PT': '🇵🇹',
  10.    'IT_IT': '🇮🇹',
  11.    'NL_NL': '🇳🇱',
  12.    'PL_PL': '🇵🇱',
  13.    'RU_RU': '🇷🇺',
  14.    'ZH_ZH': '🇨🇳',
  15.    'JA_JA': '🇯🇵',
  16.    'AR_AR': '🇸🇦'
  17.   } %}
  18. {% set locales_names = {
  19.   'FR_FR': 'Français',
  20.   'EN_GB': 'Anglais',
  21.   'ES_ES': 'Espagnol',
  22.   'DE_DE': 'Allemand',
  23.   'PT_PT': 'Portugais',
  24.   'IT_IT': 'Italien',
  25.   'NL_NL': 'Néerlandais',
  26.   'PL_PL': 'Polonais',
  27.   'RU_RU': 'Russe',
  28.   'ZH_ZH': 'Chinois',
  29.   'JA_JA': 'Japonais',
  30.   'AR_AR': 'Arabe'
  31. } %}
  32. {% block title %}Modération des Traductions{% endblock %}
  33. {% block body %}
  34. <style>
  35. .same-width-col {
  36.   width: 35%; /* ou 30%, selon ton besoin */
  37.   min-width: 200px; /* facultatif, pour les petits écrans */
  38. }
  39. td.source-cell,
  40. td.translation-cell {
  41.   vertical-align: middle;
  42. }
  43. td.translation-cell {
  44.   display: flex;
  45.   align-items: center;
  46. }
  47. td.translation-cell .translation-input {
  48.   flex: 1;
  49. }
  50. </style>
  51. <div class="container mt-5">
  52.     <h2 class="mb-4 text-center">Modération des Traductions</h2>
  53.     <div class="card shadow-sm">
  54.         <div class="card-header">
  55.             <h5 class="mb-0">Traductions en attente de validation</h5>
  56.             <div class="form-check mb-3 d-none">
  57.                 <input class="form-check-input" type="checkbox" value="" id="showAll" checked>
  58.                 <label class="form-check-label" for="showAll">
  59.                     Afficher toutes les traductions en attente
  60.                 </label>
  61.             </div>
  62.         </div>
  63.         <div class="card-body">
  64.             {% if translations|length > 0 %}
  65.               <div class="d-flex justify-content-end gap-2 mb-2">
  66.                 <input type="text" id="search-input" class="form-control form-control-sm w-auto mr-2"
  67.                        placeholder="🔍 Rechercher dans texte source..." />
  68.                 <button type="button"
  69.                         class="btn btn-outline-success mr-2 btn-sm filter-toggle active"
  70.                         data-status="nouveau">
  71.                   Nouveaux :
  72.                   {{ translations|filter(t => t.redirectUrl == redirectUrl)|length }}
  73.                 </button>
  74.                 <button type="button"
  75.                         class="btn btn-outline-warning btn-sm filter-toggle active"
  76.                         data-status="attente">
  77.                   En attente :
  78.                   {{ translations|filter(t => t.redirectUrl != redirectUrl)|length }}
  79.                 </button>
  80.               </div>
  81.                 <div class="table-responsive">
  82.                     <table class="table table-striped table-hover">
  83.                         <thead class="table-dark">
  84.                             <tr>
  85.                                 <th>#</th>
  86.                                 <th>Statut</th>
  87.                                 <th>Langue</th>
  88.                                 <th class="same-width-col">Texte Source</th>
  89.                                 <th class="same-width-col">Traduction Proposée</th>
  90.                                 <th>Actions</th>
  91.                             </tr>
  92.                         </thead>
  93.                         <tbody>
  94.                             {% for translation in translations %}
  95.                             <tr class="translation-row" data-status="{{ translation.redirectUrl == redirectUrl ? 'new' : 'pending' }}">
  96.                                 <td>{{ loop.index }}</td>
  97.                                 <td class="text-center statut-cell">
  98.                                     <span class="badge badge-status 
  99.                                                  bg-{% if translation.redirectUrl == redirectUrl %}success{% else %}warning{% endif %}">
  100.                                       {% if translation.redirectUrl == redirectUrl %}
  101.                                         ✔️ 
  102.                                       {% else %}
  103.                                         ⏳ 
  104.                                       {% endif %}
  105.                                     </span>
  106.                                 </td>
  107.                                 <td>
  108.                                     <span class="badge bg-light text-dark border">
  109.                                       {{ locales_icons[translation.locale|upper] ?? '' }}
  110.                                       {{ locales_names[translation.locale|upper] ?? translation.locale|upper }}
  111.                                     </span>
  112.                                 </td>
  113.                                 <td class="source-cell">{{ translation.keyName }}</td>
  114.                                 <td>
  115.                                     <input type="text" class="form-control translation-input" value="{{ translation.translatedText }}" />
  116.                                 </td>
  117.                                 <td class="translation2-cell">
  118.                                     <button style="padding: 2px !important;" type="button"
  119.                                             class="btn btn-sm btn-outline-secondary copy-source"
  120.                                             title="Copier le texte source"
  121.                                             data-source="{{ translation.keyName }}">
  122.                                         <i class="la la-refresh"></i>
  123.                                     </button>
  124.                                     <button style="padding: 2px !important;" class="btn btn-sm btn-success validate-translation" data-id="{{ translation.id }}" title="Valider la traduction">
  125.                                         <i class="la la-check"></i>
  126.                                     </button>
  127.                                     <button style="padding: 2px !important;" class="btn btn-sm btn-danger reject-translation" data-id="{{ translation.id }}" title="Rejeter la traduction">
  128.                                         <i class="la la-eraser"></i>
  129.                                     </button>
  130.                                 </td>
  131.                             </tr>
  132.                             {% endfor %}
  133.                         </tbody>
  134.                     </table>
  135.                 </div>
  136.             {% else %}
  137.                 <div class="text-center">
  138.                     Aucune traduction en attente de validation.
  139.                 </div>
  140.             {% endif %}
  141.             {% if redirectUrl != "/symfony/public/" %}
  142.                 <div class="text-center mt-3">
  143.                     <a href="{{ redirectUrl }}" class="btn btn-primary">Retour à la page précédente</a>
  144.                 </div>
  145.             {% endif %}
  146.         </div>
  147.     </div>
  148. </div>
  149. {% endblock %}
  150. {% block javascripts %}
  151. <script>
  152. document.addEventListener("DOMContentLoaded", function () {
  153.     document.querySelectorAll(".validate-translation").forEach(button => {
  154.         button.addEventListener("click", function () {
  155.             let row = this.closest("tr");
  156.             let translationId = this.dataset.id;
  157.             let newTranslation = row.querySelector(".translation-input").value;
  158.             fetch(`/symfony/public/moderation/translation/validate/${translationId}`, {
  159.                 method: "POST",
  160.                 headers: {
  161.                     "Content-Type": "application/json",
  162.                     "X-Requested-With": "XMLHttpRequest"
  163.                 },
  164.                 body: JSON.stringify({ translation: newTranslation, redirectUrl: "{{ redirectUrl }}" })
  165.             })
  166.             .then(response => response.json())
  167.             .then(data => {
  168.                 if (data.status === "success") {
  169.                     //row.querySelector(".status-badge").classList.replace("bg-warning", "bg-success");
  170.                     //row.querySelector(".status-badge").textContent = "Validée";
  171.                     row.remove(); // Supprime la ligne du tableau
  172.                 }
  173.             });
  174.         });
  175.     });
  176.     document.querySelectorAll(".reject-translation").forEach(button => {
  177.         button.addEventListener("click", function () {
  178.             let row = this.closest("tr");
  179.             let translationId = this.dataset.id;
  180.             fetch(`/symfony/public/moderation/translation/reject/${translationId}`, {
  181.                 method: "POST",
  182.                 headers: {
  183.                     "Content-Type": "application/json",
  184.                     "X-Requested-With": "XMLHttpRequest"
  185.                 }
  186.             })
  187.             .then(response => response.json())
  188.             .then(data => {
  189.                 if (data.status === "success") {
  190.                     row.remove(); // Supprime la ligne du tableau
  191.                 }
  192.             });
  193.         });
  194.     });
  195. });
  196. </script>
  197. <script>
  198. document.getElementById('showAll').addEventListener('change', function () {
  199.     const showAll = this.checked;
  200.     document.querySelectorAll('.translation-row').forEach(function (row) {
  201.         const isNew = row.dataset.status === 'new';
  202.         if (showAll || isNew) {
  203.             row.style.display = '';
  204.         } else {
  205.             row.style.display = 'none';
  206.         }
  207.     });
  208. });
  209. // Facultatif : déclencher une fois au chargement
  210. document.getElementById('showAll').dispatchEvent(new Event('change'));
  211. </script>
  212. <script>
  213. document.addEventListener('DOMContentLoaded', function () {
  214.   const rows = document.querySelectorAll('tbody tr');
  215.   const buttons = document.querySelectorAll('.filter-toggle');
  216.   const redirectUrl = '{{ redirectUrl }}';
  217.   // Renvoie les statuts actifs sélectionnés
  218.   function getActiveStatuses() {
  219.     return Array.from(buttons)
  220.       .filter(btn => btn.classList.contains('active'))
  221.       .map(btn => btn.dataset.status);
  222.   }
  223.   // Met à jour la visibilité des lignes selon les statuts actifs
  224.   function filterRows() {
  225.     const activeStatuses = getActiveStatuses();
  226.     rows.forEach(row => {
  227.       const isNew = row.querySelector('td:nth-child(5) .badge')?.classList.contains('bg-success');
  228.       const status = isNew ? 'nouveau' : 'attente';
  229.       row.style.display = activeStatuses.includes(status) ? '' : 'none';
  230.     });
  231.   }
  232.   // Toggle bouton + mise à jour affichage
  233.   buttons.forEach(btn => {
  234.     btn.addEventListener('click', () => {
  235.       btn.classList.toggle('active');
  236.       filterRows();
  237.     });
  238.   });
  239.   // Filtrage initial
  240.   filterRows();
  241. });
  242. </script>
  243. <script>
  244. document.addEventListener('DOMContentLoaded', function () {
  245.   document.querySelectorAll('.copy-source').forEach(button => {
  246.     button.addEventListener('click', function () {
  247.       const row = this.closest('tr');
  248.       const sourceText = this.dataset.source;
  249.       const input = row.querySelector('.translation-input');
  250.       if (input) {
  251.         input.value = sourceText;
  252.         input.classList.add('border-warning');
  253.         setTimeout(() => input.classList.remove('border-warning'), 1000);
  254.       }
  255.     });
  256.   });
  257. });
  258. </script>
  259. <script>
  260. document.addEventListener('DOMContentLoaded', function () {
  261.   const searchInput = document.getElementById('search-input');
  262.   const rows = document.querySelectorAll('tbody tr');
  263.   searchInput.addEventListener('input', function () {
  264.     const filter = this.value.trim().toLowerCase();
  265.     rows.forEach(row => {
  266.       const sourceText = row.querySelector('td.source-cell')?.textContent.toLowerCase() || '';
  267.       if (sourceText.includes(filter)) {
  268.         row.style.display = '';
  269.       } else {
  270.         row.style.display = 'none';
  271.       }
  272.     });
  273.   });
  274. });
  275. </script>
  276. {% endblock %}