From 38fbc451f59fbd69c35df6a2bdfe97beadcee715 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Thu, 21 Aug 2025 16:50:17 +0200 Subject: [PATCH] add wiki compare --- src/Controller/WikiController.php | 282 + .../admin/followup_theme_graph.html.twig | 28 +- templates/admin/wiki.html.twig | 101 +- templates/admin/wiki_compare.html.twig | 577 + templates/public/nav.html.twig | 1 + wiki_compare/outdated_pages.json | 79851 +++++++++++++++- wiki_compare/top_keys.json | 40 + wiki_compare/wiki_compare.py | 336 +- wiki_compare/wiki_pages.csv | 61 +- 9 files changed, 81151 insertions(+), 126 deletions(-) create mode 100644 templates/admin/wiki_compare.html.twig diff --git a/src/Controller/WikiController.php b/src/Controller/WikiController.php index 660b221..b6c04b3 100644 --- a/src/Controller/WikiController.php +++ b/src/Controller/WikiController.php @@ -5,6 +5,7 @@ namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpFoundation\Request; class WikiController extends AbstractController { @@ -23,13 +24,294 @@ class WikiController extends AbstractController $headers = array_shift($csvData); $wikiPages = []; + $missingTranslations = []; + + // First pass: collect all staleness scores to find min and max + $stalenessScores = []; foreach ($csvData as $row) { $page = array_combine($headers, $row); + if (isset($page['staleness_score']) && is_numeric($page['staleness_score'])) { + $stalenessScores[] = (float)$page['staleness_score']; + } + } + + // Find min and max scores for normalization + $minScore = !empty($stalenessScores) ? min($stalenessScores) : 0; + $maxScore = !empty($stalenessScores) ? max($stalenessScores) : 100; + + // Second pass: process pages and normalize scores + foreach ($csvData as $row) { + $page = array_combine($headers, $row); + + // Normalize staleness score to 0-100 range (0 = best, 100 = worst) + if (isset($page['staleness_score']) && is_numeric($page['staleness_score'])) { + $originalScore = (float)$page['staleness_score']; + + // Avoid division by zero + if ($maxScore > $minScore) { + $normalizedScore = ($originalScore - $minScore) / ($maxScore - $minScore) * 100; + } else { + $normalizedScore = 50; // Default to middle value if all scores are the same + } + + // Round to 2 decimal places + $page['staleness_score'] = round($normalizedScore, 2); + } + $wikiPages[$page['key']][$page['language']] = $page; } + // Identify pages missing French translations + foreach ($wikiPages as $key => $languages) { + if (isset($languages['en']) && !isset($languages['fr'])) { + $missingTranslations[$key] = $languages['en']; + } + } + + // Sort wiki pages by staleness score (descending) + uasort($wikiPages, function($a, $b) { + $scoreA = isset($a['en']) && isset($a['fr']) && isset($a['en']['staleness_score']) ? (float)$a['en']['staleness_score'] : 0; + $scoreB = isset($b['en']) && isset($b['fr']) && isset($b['en']['staleness_score']) ? (float)$b['en']['staleness_score'] : 0; + return $scoreB <=> $scoreA; + }); + return $this->render('admin/wiki.html.twig', [ 'wiki_pages' => $wikiPages, + 'missing_translations' => $missingTranslations, + ]); + } + + #[Route('/admin/wiki/compare/{key}', name: 'app_admin_wiki_compare')] + public function compare(string $key): Response + { + $csvFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/wiki_pages.csv'; + $jsonFile = $this->getParameter('kernel.project_dir') . '/wiki_compare/outdated_pages.json'; + + if (!file_exists($csvFile)) { + $this->addFlash('error', 'Le fichier wiki_pages.csv n\'existe pas.'); + return $this->redirectToRoute('app_admin_index'); + } + + $csvData = array_map('str_getcsv', file($csvFile)); + $headers = array_shift($csvData); + + // Process CSV data to find the requested key + $enPage = null; + $frPage = null; + + foreach ($csvData as $row) { + $page = array_combine($headers, $row); + if ($page['key'] === $key) { + if ($page['language'] === 'en') { + $enPage = $page; + } elseif ($page['language'] === 'fr') { + $frPage = $page; + } + } + } + + // If English page doesn't exist, redirect back with error + if (!$enPage) { + $this->addFlash('error', 'La page wiki pour la clé "' . $key . '" n\'existe pas.'); + return $this->redirectToRoute('app_admin_wiki'); + } + + // Get detailed content comparison from JSON file + $detailedComparison = null; + $mediaDiff = 0; + + if (file_exists($jsonFile)) { + $jsonData = json_decode(file_get_contents($jsonFile), true); + + foreach ($jsonData as $page) { + if ($page['key'] === $key) { + $detailedComparison = [ + 'section_comparison' => $page['section_comparison'] ?? null, + 'link_comparison' => $page['link_comparison'] ?? null, + 'media_comparison' => $page['media_comparison'] ?? null + ]; + + $mediaDiff = $page['media_diff'] ?? 0; + break; + } + } + } + + // Calculate staleness score components + $scoreComponents = []; + + if ($frPage) { + // Calculate date difference in days + $dateDiff = 0; + if ($enPage['last_modified'] && $frPage['last_modified']) { + $enDate = \DateTime::createFromFormat('Y-m-d', $enPage['last_modified']); + $frDate = \DateTime::createFromFormat('Y-m-d', $frPage['last_modified']); + if ($enDate && $frDate) { + $dateDiff = ($enDate->getTimestamp() - $frDate->getTimestamp()) / (60 * 60 * 24); + } + } + + // Calculate content differences + $wordDiff = $enPage['word_count'] - $frPage['word_count']; + $sectionDiff = $enPage['sections'] - $frPage['sections']; + $linkDiff = $enPage['link_count'] - $frPage['link_count']; + + // Calculate score components + $dateComponent = abs($dateDiff) * 0.2; + $wordComponent = abs($wordDiff) / 100 * 0.5; + $sectionComponent = abs($sectionDiff) * 0.15; + $linkComponent = abs($linkDiff) / 10 * 0.15; + + $scoreComponents = [ + 'date' => [ + 'value' => $dateDiff, + 'weight' => 0.2, + 'component' => $dateComponent, + 'description' => 'Différence de date (en jours)' + ], + 'word' => [ + 'value' => $wordDiff, + 'weight' => 0.5, + 'component' => $wordComponent, + 'description' => 'Différence de nombre de mots' + ], + 'section' => [ + 'value' => $sectionDiff, + 'weight' => 0.15, + 'component' => $sectionComponent, + 'description' => 'Différence de nombre de sections' + ], + 'link' => [ + 'value' => $linkDiff, + 'weight' => 0.15, + 'component' => $linkComponent, + 'description' => 'Différence de nombre de liens' + ] + ]; + + // Add media component if available + if (isset($enPage['media_count']) && isset($frPage['media_count'])) { + $mediaComponent = abs($mediaDiff) / 5 * 0.1; + $scoreComponents['media'] = [ + 'value' => $mediaDiff, + 'weight' => 0.1, + 'component' => $mediaComponent, + 'description' => 'Différence de nombre d\'images' + ]; + + // Adjust other weights to maintain total of 1.0 + $scoreComponents['date']['weight'] = 0.2; + $scoreComponents['word']['weight'] = 0.45; + $scoreComponents['section']['weight'] = 0.15; + $scoreComponents['link']['weight'] = 0.1; + } + } + + // Create URL for new French page if it doesn't exist + $createFrUrl = null; + if (!$frPage) { + $createFrUrl = 'https://wiki.openstreetmap.org/wiki/FR:Key:' . $key; + } + + // Format section titles for copy functionality + $enSections = ''; + $frSections = ''; + + if ($detailedComparison && $detailedComparison['section_comparison']) { + // English sections + if ($enPage) { + $enSectionsList = []; + + // Add common sections + foreach ($detailedComparison['section_comparison']['common'] as $section) { + $enSectionsList[] = str_repeat('=', $section['en']['level']) . ' ' . + $section['en']['title'] . ' ' . + str_repeat('=', $section['en']['level']); + } + + // Add English-only sections + foreach ($detailedComparison['section_comparison']['en_only'] as $section) { + $enSectionsList[] = str_repeat('=', $section['level']) . ' ' . + $section['title'] . ' ' . + str_repeat('=', $section['level']) . ' (EN only)'; + } + + $enSections = implode("\n", $enSectionsList); + } + + // French sections + if ($frPage) { + $frSectionsList = []; + + // Add common sections + foreach ($detailedComparison['section_comparison']['common'] as $section) { + $frSectionsList[] = str_repeat('=', $section['fr']['level']) . ' ' . + $section['fr']['title'] . ' ' . + str_repeat('=', $section['fr']['level']); + } + + // Add French-only sections + foreach ($detailedComparison['section_comparison']['fr_only'] as $section) { + $frSectionsList[] = str_repeat('=', $section['level']) . ' ' . + $section['title'] . ' ' . + str_repeat('=', $section['level']) . ' (FR only)'; + } + + $frSections = implode("\n", $frSectionsList); + } + } + + // Format links for copy functionality + $enLinks = ''; + $frLinks = ''; + + if ($detailedComparison && $detailedComparison['link_comparison']) { + // English links + if ($enPage) { + $enLinksList = []; + + // Add common links + foreach ($detailedComparison['link_comparison']['common'] as $link) { + $enLinksList[] = $link['en']['text'] . ' - ' . $link['en']['href']; + } + + // Add English-only links + foreach ($detailedComparison['link_comparison']['en_only'] as $link) { + $enLinksList[] = $link['text'] . ' - ' . $link['href'] . ' (EN only)'; + } + + $enLinks = implode("\n", $enLinksList); + } + + // French links + if ($frPage) { + $frLinksList = []; + + // Add common links + foreach ($detailedComparison['link_comparison']['common'] as $link) { + $frLinksList[] = $link['fr']['text'] . ' - ' . $link['fr']['href']; + } + + // Add French-only links + foreach ($detailedComparison['link_comparison']['fr_only'] as $link) { + $frLinksList[] = $link['text'] . ' - ' . $link['href'] . ' (FR only)'; + } + + $frLinks = implode("\n", $frLinksList); + } + } + + return $this->render('admin/wiki_compare.html.twig', [ + 'key' => $key, + 'en_page' => $enPage, + 'fr_page' => $frPage, + 'score_components' => $scoreComponents, + 'create_fr_url' => $createFrUrl, + 'detailed_comparison' => $detailedComparison, + 'en_sections' => $enSections, + 'fr_sections' => $frSections, + 'en_links' => $enLinks, + 'fr_links' => $frLinks ]); } } \ No newline at end of file diff --git a/templates/admin/followup_theme_graph.html.twig b/templates/admin/followup_theme_graph.html.twig index c4833df..4a3907b 100644 --- a/templates/admin/followup_theme_graph.html.twig +++ b/templates/admin/followup_theme_graph.html.twig @@ -7,6 +7,13 @@ +
+

Comparaison Wiki OpenStreetMap - {{ key }}

+

Comparaison détaillée des pages wiki en français et en anglais pour la clé OSM "{{ key }}".

+ + {% if fr_page %} +
+
+

Comparaison des versions

+
+
+
+
+
+
+

Version anglaise

+

+ Dernière modification: {{ en_page.last_modified }} +

+
+
+
    +
  • + Sections + {{ en_page.sections }} +
  • +
  • + Mots + {{ en_page.word_count }} +
  • +
  • + Liens + {{ en_page.link_count }} +
  • +
+
+ + Voir la page + + + +
+
+
+
+
+
+
+

Version française

+

+ Dernière modification: {{ fr_page.last_modified }} +

+
+
+
    +
  • + Sections + {{ fr_page.sections }} +
  • +
  • + Mots + {{ fr_page.word_count }} +
  • +
  • + Liens + {{ fr_page.link_count }} +
  • +
+
+ + Voir la page + + + +
+
+
+
+
+
+
+ + {% if detailed_comparison and detailed_comparison.section_comparison %} +
+
+

Comparaison des sections

+
+
+
+
+
+
+

Sections en anglais

+ {{ en_page.sections }} sections +
+
+{#

Sections communes ({{ detailed_comparison.section_comparison.common|length }})

#} +{#
    #} +{# {% for section in detailed_comparison.section_comparison.common %}#} +{#
  • #} +{# h{{ section.en.level }}#} +{# {{ section.en.title }}#} +{#
  • #} +{# {% endfor %}#} +{#
#} + +

Sections uniquement en anglais ({{ detailed_comparison.section_comparison.en_only|length }})

+
    + {% for section in detailed_comparison.section_comparison.en_only %} +
  • + h{{ section.level }} + {{ section.title }} +
  • + {% endfor %} +
+
+
+
+
+
+
+

Sections en français

+ {{ fr_page.sections }} sections +
+
+{#

Sections communes ({{ detailed_comparison.section_comparison.common|length }})

#} +{#
    #} +{# {% for section in detailed_comparison.section_comparison.common %}#} +{#
  • #} +{# h{{ section.fr.level }}#} +{# {{ section.fr.title }}#} +{#
  • #} +{# {% endfor %}#} +{#
#} + +

Sections uniquement en français ({{ detailed_comparison.section_comparison.fr_only|length }})

+
    + {% for section in detailed_comparison.section_comparison.fr_only %} +
  • + h{{ section.level }} + {{ section.title }} +
  • + {% endfor %} +
+
+
+
+
+
+
+ {% endif %} + + {% if detailed_comparison and detailed_comparison.media_comparison %} +
+
+

Comparaison des médias

+
+
+
+
+
+
+

Images en anglais

+ {{ en_page.media_count|default(0) }} images +
+
+{#

Images communes ({{ detailed_comparison.media_comparison.common|length }})

#} +{#
#} +{# {% for media in detailed_comparison.media_comparison.common %}#} +{#
#} +{#
#} +{# {{ media.en.alt }}#} +{#
#} +{#

{{ media.en.alt }}

#} +{#
#} +{#
#} +{#
#} +{# {% endfor %}#} +{#
#} + +

Images uniquement en anglais ({{ detailed_comparison.media_comparison.en_only|length }})

+
+ {% for media in detailed_comparison.media_comparison.en_only %} +
+
+ {{ media.alt }} +
+

{{ media.alt }}

+
+
+
+ {% endfor %} +
+
+
+
+
+
+
+

Images en français

+ {{ fr_page.media_count|default(0) }} images +
+
+{#

Images communes ({{ detailed_comparison.media_comparison.common|length }})

#} +{#
#} +{# {% for media in detailed_comparison.media_comparison.common %}#} +{#
#} +{#
#} +{# {{ media.fr.alt }}#} +{#
#} +{#

{{ media.fr.alt }}

#} +{#
#} +{#
#} +{#
#} +{# {% endfor %}#} +{#
#} + +

Images uniquement en français ({{ detailed_comparison.media_comparison.fr_only|length }})

+
+ {% for media in detailed_comparison.media_comparison.fr_only %} +
+
+ {{ media.alt }} +
+

{{ media.alt }}

+
+
+
+ {% endfor %} +
+
+
+
+
+
+
+ {% endif %} + + {% if detailed_comparison and detailed_comparison.link_comparison %} +
+
+

Comparaison des liens

+
+
+
+
+
+
+

Liens en anglais

+ {{ en_page.link_count }} liens +
+
+

Liens communs ({{ detailed_comparison.link_comparison.common|length }})

+
+ + + + + + + + + {% for link in detailed_comparison.link_comparison.common|slice(0, 10) %} + + + + + {% endfor %} + {% if detailed_comparison.link_comparison.common|length > 10 %} + + + + {% endif %} + +
TexteURL
{{ link.en.text }}{{ link.en.href|slice(0, 30) }}...
+ {{ detailed_comparison.link_comparison.common|length - 10 }} liens supplémentaires... +
+
+ +

Liens uniquement en anglais ({{ detailed_comparison.link_comparison.en_only|length }})

+
+ + + + + + + + + {% for link in detailed_comparison.link_comparison.en_only|slice(0, 10) %} + + + + + {% endfor %} + {% if detailed_comparison.link_comparison.en_only|length > 10 %} + + + + {% endif %} + +
TexteURL
{{ link.text }}{{ link.href|slice(0, 30) }}...
+ {{ detailed_comparison.link_comparison.en_only|length - 10 }} liens supplémentaires... +
+
+
+
+
+
+
+
+

Liens en français

+ {{ fr_page.link_count }} liens +
+
+

Liens communs ({{ detailed_comparison.link_comparison.common|length }})

+
+ + + + + + + + + {% for link in detailed_comparison.link_comparison.common|slice(0, 10) %} + + + + + {% endfor %} + {% if detailed_comparison.link_comparison.common|length > 10 %} + + + + {% endif %} + +
TexteURL
{{ link.fr.text }}{{ link.fr.href|slice(0, 30) }}...
+ {{ detailed_comparison.link_comparison.common|length - 10 }} liens supplémentaires... +
+
+ +

Liens uniquement en français ({{ detailed_comparison.link_comparison.fr_only|length }})

+
+ + + + + + + + + {% for link in detailed_comparison.link_comparison.fr_only|slice(0, 10) %} + + + + + {% endfor %} + {% if detailed_comparison.link_comparison.fr_only|length > 10 %} + + + + {% endif %} + +
TexteURL
{{ link.text }}{{ link.href|slice(0, 30) }}...
+ {{ detailed_comparison.link_comparison.fr_only|length - 10 }} liens supplémentaires... +
+
+
+
+
+
+
+
+ {% endif %} + {% else %} +
+
+

Traduction française manquante

+
+
+
+

La page wiki pour la clé "{{ key }}" n'existe pas en français.

+

Vous pouvez contribuer en créant cette page sur le wiki OpenStreetMap.

+
+ +
+
+
+
+

Version anglaise

+

+ Dernière modification: {{ en_page.last_modified }} +

+
+
+
    +
  • + Sections + {{ en_page.sections }} +
  • +
  • + Mots + {{ en_page.word_count }} +
  • +
  • + Liens + {{ en_page.link_count }} +
  • +
+
+ + Voir la page anglaise + + + +
+
+
+
+
+
+
+

Créer la version française

+
+
+

Vous pouvez créer la page française en suivant ces étapes :

+
    +
  1. Consultez la version anglaise pour comprendre le contenu
  2. +
  3. Créez une nouvelle page avec le préfixe "FR:" sur le wiki OSM
  4. +
  5. Traduisez le contenu en respectant la structure de la page anglaise
  6. +
  7. Ajoutez des exemples pertinents pour le contexte français
  8. +
+ +
+
+
+
+
+
+ {% endif %} + + +
+
+

Score de décrépitude

+
+
+

Le score de décrépitude est calculé en prenant en compte plusieurs facteurs, avec une pondération plus importante pour la différence de nombre de mots :

+ +
+ + + + + + + + + + + {% for key, component in score_components %} + + + + + + + {% endfor %} + + + + + +
FacteurValeurPoidsContribution
{{ component.description }}{{ component.value }}{{ component.weight * 100 }}%{{ component.component|round(2) }}
Score total + {% set total_score = 0 %} + {% for key, component in score_components %} + {% set total_score = total_score + component.component %} + {% endfor %} + {{ total_score|round(2) }} +
+
+ +
+

Comment interpréter ce score :

+
    +
  • Plus le score est élevé, plus la page française est considérée comme "décrépite" par rapport à la version anglaise.
  • +
  • La différence de nombre de mots compte pour 50% du score, car c'est l'indicateur le plus important de la complétude de la traduction.
  • +
  • Les différences de sections (15%), de liens (15%) et de date de modification (20%) complètent le score.
  • +
+
+
+
+ + +
+ + + + +{% if fr_page %} + + +{% endif %} + +{% block javascripts %} + +{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/public/nav.html.twig b/templates/public/nav.html.twig index 93ce833..d83ca6c 100644 --- a/templates/public/nav.html.twig +++ b/templates/public/nav.html.twig @@ -41,6 +41,7 @@ Admin