Plugin Directory

Changeset 3763820


Ignore:
Timestamp:
10/24/2026 11:14:30 PM (10 hours ago)
Author:
lyode
Message:

Release 1.3.0

Location:
filikod
Files:
62 added
17 edited

Legend:

Unmodified
Added
Removed
  • filikod/tags/1.2.1/readme.txt

    r3737019 r3763820  
    3838
    3939* **SEO professionals** who want ALT text treated as a measurable image SEO signal, not an afterthought
    40 * **Accessibility-focused teams** who need a structured audit to meet WCAG 2.1 success criterion 1.1.1 (non-text content)
     40* **Accessibility-focused teams** who need a structured audit to meet WCAG 2.2 success criterion 1.1.1 (non-text content)
    4141* **Content managers** with large media libraries who need to fix issues fast, in bulk
    4242* **Agencies** managing multiple WordPress sites who need a repeatable quality process
    … …  
    5757
    5858This turns ALT text from a vague, invisible problem into a measurable system you can actually improve, like a health score for your media library.
     59
     60---
     61
     62= Accessibility Standards: WCAG 2.2, ADA, Section 508, EAA, RGAA =
     63
     64Every major accessibility standard starts with the same rule for images: WCAG success criterion 1.1.1 (Non-text Content, Level A). An image that carries information needs a text alternative. A purely decorative image needs an empty one (`alt=""`) so screen readers skip it.
     65
     66That rule sits behind the laws and frameworks your clients ask about:
     67
     68* **WCAG 2.2** (and 2.1, 2.0): success criterion 1.1.1, unchanged since 2008
     69* **ADA** (United States): the Title II rule for state and local governments references WCAG 2.1 AA, and WCAG is the usual benchmark in ADA website cases
     70* **Section 508** (US federal websites): WCAG 2.0 AA
     71* **European Accessibility Act (EAA)**: applies since June 28, 2025, through EN 301 549, which references WCAG 2.1 AA
     72* **RGAA** (France): theme 1 "Images", criteria 1.1 to 1.9
     73
     74Filikod checks the image part of these standards across your whole media library: images with no text alternative, generic or filename-like ALT text that a screen reader reads out as noise ("IMG_4832", "photo"), and images you mark as **decorative** so they keep an intentional empty `alt=""`. WooCommerce product images live in the media library too, so they are audited like any other image.
     75
     76Filikod covers this one criterion, not a whole standard. It does not replace a full accessibility audit, and no plugin can make a site compliant on its own. What it gives you is a measured starting point for one of the most frequent failures on the web.
    5977
    6078---
    … …  
    183201
    184202A checker first. Filikod audits the alt text that already exists in your media library and scores it. It never sends your images to an external AI service and never invents descriptions behind your back. An optional, local filename-based setting (off by default) can fill a starting ALT for images that have none, and you then review it in the ALT Audit like any other ALT text.
     203
     204= Does Filikod make my site WCAG, ADA or EAA compliant? =
     205
     206No plugin can do that on its own. Filikod covers the image part of these standards (WCAG success criterion 1.1.1, RGAA theme 1): it finds images without a proper text alternative, lets you fix them in bulk and keeps decorative images with an intentional empty `alt=""`. Color contrast, keyboard navigation, forms and the rest of WCAG need their own checks.
    185207
    186208= Does Filikod generate ALT text automatically? =
  • filikod/trunk/admin/views/alt-audit.php

    r3737001 r3763820  
    9898);
    9999$filikod_save_alt_nonce = wp_create_nonce( 'filikod_save_alt' );
     100
     101// Colonne Langue : seulement quand un plugin multilingue donne une langue aux médias (Polylang, WPML, Bogo)
     102// ou écrit plusieurs langues dans le même ALT (WPGlobus, qTranslate-XT).
     103$filikod_languages       = ( class_exists( 'Filikod_Duplicates' ) && '' !== Filikod_Duplicates::get_multilingual_engine() )
     104        ? Filikod_Duplicates::get_language_map()
     105        : null;
     106$filikod_alt_lang_engine = class_exists( 'Filikod_Alt_Audit' ) ? Filikod_Alt_Audit::get_alt_language_engine() : '';
     107$filikod_show_language   = is_array( $filikod_languages ) || '' !== $filikod_alt_lang_engine;
     108if ( 'missing' === $status && '' !== $filikod_alt_lang_engine ) {
     109        $filikod_issue_descriptions['missing'] .= ' ' . __( 'With WPGlobus or qTranslate-XT, an image is also listed here when its ALT is empty in one of the site languages (see the Language column).', 'filikod' );
     110}
    100111?>
    101112<script type="text/javascript">
    … …  
    152163                                        <th class="column-thumbnail"><?php esc_html_e( 'Thumbnail', 'filikod' ); ?></th>
    153164                                        <th class="column-filename"><?php esc_html_e( 'File name', 'filikod' ); ?></th>
     165                                        <?php if ( $filikod_show_language ) : ?>
     166                                                <th class="column-language"><?php esc_html_e( 'Language', 'filikod' ); ?></th>
     167                                        <?php endif; ?>
    154168                                        <th class="column-uploaded-to"><?php esc_html_e( 'Context', 'filikod' ); ?></th>
    155169                                        <th class="column-alt"><?php esc_html_e( 'ALT', 'filikod' ); ?></th>
    … …  
    204218                                                </td>
    205219                                                <td class="column-filename"><?php echo esc_html( $filikod_file_name ); ?></td>
     220                                                <?php if ( $filikod_show_language ) : ?>
     221                                                        <td class="column-language">
     222                                                                <?php
     223                                                                $filikod_alt_langs = isset( $filikod_item['languages'] ) && is_array( $filikod_item['languages'] ) ? $filikod_item['languages'] : null;
     224                                                                if ( null !== $filikod_alt_langs ) {
     225                                                                        // ALT multilingue (WPGlobus, qTranslate-XT) : langues remplies, puis langues vides.
     226                                                                        echo esc_html( $filikod_alt_langs['present'] ? strtoupper( implode( ' · ', $filikod_alt_langs['present'] ) ) : '–' );
     227                                                                        if ( ! empty( $filikod_alt_langs['missing'] ) ) {
     228                                                                                echo '<span class="filikod-lang-missing">' . esc_html(
     229                                                                                        sprintf(
     230                                                                                                /* translators: %s: language codes, e.g. "EN, DE" */
     231                                                                                                __( 'empty: %s', 'filikod' ),
     232                                                                                                strtoupper( implode( ', ', $filikod_alt_langs['missing'] ) )
     233                                                                                        )
     234                                                                                ) . '</span>';
     235                                                                        }
     236                                                                } elseif ( isset( $filikod_languages[ $filikod_attachment_id ] ) ) {
     237                                                                        echo esc_html( strtoupper( $filikod_languages[ $filikod_attachment_id ] ) );
     238                                                                } elseif ( '' !== $filikod_alt_lang_engine && '' !== trim( $filikod_alt_raw ) ) {
     239                                                                        // ALT sans balise de langue : le même texte sert toutes les langues.
     240                                                                        echo '<span class="filikod-lang-untranslated">' . esc_html__( 'Not translated', 'filikod' ) . '</span>';
     241                                                                } else {
     242                                                                        echo '–';
     243                                                                }
     244                                                                ?>
     245                                                        </td>
     246                                                <?php endif; ?>
    206247                                                <td class="column-uploaded-to"><?php echo wp_kses_post( $filikod_uploaded_to ); ?></td>
    207248                                                <td class="column-alt">
  • filikod/trunk/admin/views/duplicates.php

    r3737001 r3763820  
    1717
    1818$filikod_paged    = isset( $_GET['paged'] ) ? max( 1, (int) $_GET['paged'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only pagination parameter, cast to int and clamped.
    19 $filikod_view     = isset( $_GET['view'] ) && 'missing' === sanitize_text_field( wp_unslash( $_GET['view'] ) ) ? 'missing' : 'duplicates'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab parameter, strictly whitelisted.
     19$filikod_view     = isset( $_GET['view'] ) ? sanitize_key( wp_unslash( $_GET['view'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab parameter, strictly whitelisted below.
     20$filikod_view     = in_array( $filikod_view, array( 'missing', 'similar' ), true ) ? $filikod_view : 'duplicates';
     21$filikod_filter   = isset( $_GET['filter'] ) ? sanitize_key( wp_unslash( $_GET['filter'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only sub-tab parameter, strictly whitelisted below.
     22$filikod_filter   = in_array( $filikod_filter, array( 'languages', 'ignored' ), true ) ? $filikod_filter : 'same';
    2023$filikod_base_url = add_query_arg( array( 'page' => 'filikod-duplicates' ), admin_url( 'admin.php' ) );
    2124$filikod_missing_url = add_query_arg( array( 'view' => 'missing' ), $filikod_base_url );
     25$filikod_similar_url = add_query_arg( array( 'view' => 'similar' ), $filikod_base_url );
     26
     27$filikod_visual_counts     = Filikod_Visual_Duplicates::get_counts();
     28$filikod_visual_signatures = Filikod_Visual_Duplicates::get_signature_counts();
     29$filikod_similar_data      = array( 'groups' => array(), 'total' => 0, 'total_pages' => 0, 'paged' => 1 );
     30if ( 'similar' === $filikod_view ) {
     31        $filikod_similar_data = Filikod_Visual_Duplicates::get_groups_paginated( $filikod_filter, $filikod_paged );
     32}
    2233
    2334$filikod_summary   = Filikod_Duplicates::get_summary();
    … …  
    116127                                        );
    117128                                }
     129                                $filikod_visual_waiting = max( 0, (int) $filikod_summary['total'] - $filikod_visual_signatures['signed'] - $filikod_visual_signatures['none'] );
     130                                if ( $filikod_visual_waiting > 0 && Filikod_Visual_Duplicates::is_supported() ) {
     131                                        echo ' ';
     132                                        printf(
     133                                                /* translators: %s: number of images without a visual signature */
     134                                                esc_html__( '%s images have no visual signature yet (Similar images tab): "Scan library" computes them.', 'filikod' ),
     135                                                esc_html( number_format_i18n( $filikod_visual_waiting, 0 ) )
     136                                        );
     137                                }
    118138                                ?>
    119139                        </p>
    … …  
    146166                                <?php esc_html_e( 'Duplicated files', 'filikod' ); ?> (<span class="filikod-alt-tab-count"><?php echo esc_html( number_format_i18n( (int) $filikod_summary['redundant'], 0 ) ); ?></span>)
    147167                        </a>
     168                        <a href="<?php echo esc_url( $filikod_similar_url ); ?>" class="filikod-tab <?php echo 'similar' === $filikod_view ? 'active' : ''; ?>">
     169                                <?php esc_html_e( 'Similar images', 'filikod' ); ?> (<span class="filikod-alt-tab-count"><?php echo esc_html( number_format_i18n( (int) $filikod_visual_counts['same'], 0 ) ); ?></span>)
     170                        </a>
    148171                        <a href="<?php echo esc_url( $filikod_missing_url ); ?>" class="filikod-tab <?php echo 'missing' === $filikod_view ? 'active' : ''; ?>">
    149172                                <?php esc_html_e( 'Missing files', 'filikod' ); ?> (<span class="filikod-alt-tab-count"><?php echo esc_html( number_format_i18n( $filikod_unhashable, 0 ) ); ?></span>)
    … …  
    153176                <div class="filikod-tabs-content">
    154177
    155                 <?php if ( 'missing' === $filikod_view ) : ?>
     178                <?php if ( 'similar' === $filikod_view ) : ?>
     179                        <?php include FILIKOD_PLUGIN_PATH . 'admin/views/duplicates-similar.php'; ?>
     180                <?php elseif ( 'missing' === $filikod_view ) : ?>
    156181                        <!-- Onglet Missing files -->
    157182                        <div id="tab-missing" class="filikod-tab-content active">
    … …  
    228253                                                                        /* translators: 1: current page, 2: total pages, 3: total items */
    229254                                                                        esc_html__( 'Page %1$d of %2$d (%3$d items)', 'filikod' ),
    230                                                                         $filikod_m_current,
    231                                                                         $filikod_m_pages,
     255                                                                        (int) $filikod_m_current,
     256                                                                        (int) $filikod_m_pages,
    232257                                                                        (int) $filikod_missing_data['total']
    233258                                                                );
    … …  
    251276                        <div id="tab-duplicates" class="filikod-tab-content active">
    252277                                <p class="filikod-alt-audit-description">
    253                                         <?php esc_html_e( 'Exact copies of the same file (same md5 fingerprint) uploaded more than once. Nothing is deleted or merged: check where each copy is used, keep that one, and remove the others from the Media Library once you are sure. Media linked as translations of each other (Polylang, WPML, Bogo) are intentional and never listed here.', 'filikod' ); ?>
     278                                        <?php
     279                                        esc_html_e( 'Exact copies of the same file (same md5 fingerprint) uploaded more than once. Nothing is deleted or merged: check where each copy is used, keep that one, and remove the others from the Media Library once you are sure.', 'filikod' );
     280                                        // Phrase sur les traductions seulement quand un plugin multilingue gère la médiathèque.
     281                                        if ( '' !== Filikod_Duplicates::get_multilingual_engine() ) {
     282                                                echo ' ';
     283                                                esc_html_e( 'Media linked as translations of each other (Polylang, WPML, Bogo) are intentional and never listed here.', 'filikod' );
     284                                        }
     285                                        ?>
    254286                                </p>
    255287
    … …  
    379411                                                                        /* translators: 1: current page, 2: total pages, 3: total groups */
    380412                                                                        esc_html__( 'Page %1$d of %2$d (%3$d groups)', 'filikod' ),
    381                                                                         $filikod_current,
    382                                                                         $filikod_total_pages,
     413                                                                        (int) $filikod_current,
     414                                                                        (int) $filikod_total_pages,
    383415                                                                        (int) $filikod_page_data['total']
    384416                                                                );
  • filikod/trunk/assets/css/admin.css

    r3737001 r3763820  
    33 */
    44
    5 /* Icône du menu - S'assurer que le SVG utilise la couleur du thème WordPress */
    6 #toplevel_page_filikod .wp-menu-image:before {
    7     content: '';
    8     display: none;
    9 }
    10 
    11 #toplevel_page_filikod .wp-menu-image {
    12     display: flex;
    13     align-items: center;
    14     justify-content: center;
    15     width: 20px;
    16     height: 20px;
    17     color: #a7aaad; /* Gris par défaut */
    18 }
    19 
    20 #toplevel_page_filikod .wp-menu-image img,
    21 #toplevel_page_filikod .wp-menu-image svg {
    22     width: 20px;
    23     height: 20px;
    24     display: block;
    25 }
    26 
    27 #toplevel_page_filikod .wp-menu-image svg {
    28     fill: currentColor;
    29 }
    30 
    31 /* Blanc quand actif ou en hover */
    32 #toplevel_page_filikod:hover .wp-menu-image,
    33 #toplevel_page_filikod.current .wp-menu-image,
    34 #toplevel_page_filikod.wp-has-current-submenu .wp-menu-image {
    35     color: #fff;
    36 }
     5/* Icône du menu : SVG passé à add_menu_page() et recoloré par WordPress, aucune règle ici. */
    376
    387/* Dashboard */
    … …  
    12371206}
    12381207
     1208/* Colonne Langue (sites multilingues seulement) : code court, « FR », « EN » */
     1209.filikod-alt-audit-table .column-language {
     1210    width: 90px;
     1211}
     1212
     1213.filikod-alt-audit-table .column-language .filikod-lang-missing {
     1214    display: block;
     1215    margin-top: 2px;
     1216    font-size: 12px;
     1217    color: #b32d2e;
     1218}
     1219
     1220.filikod-alt-audit-table .column-language .filikod-lang-untranslated {
     1221    font-size: 12px;
     1222    color: #646970;
     1223}
     1224
    12391225.filikod-alt-audit-table .column-uploaded-to a {
    12401226    white-space: nowrap;
    … …  
    15901576}
    15911577
     1578/* Images semblables : bouton « C'est voulu » dans l'en-tête de groupe, filtres par langue */
     1579.filikod-alt-audit-table tr.filikod-dup-group-row .filikod-visual-ignore {
     1580    float: right;
     1581    margin: -2px 0;
     1582    font-weight: 400;
     1583}
     1584
     1585.filikod-visual-filters a.current {
     1586    font-weight: 600;
     1587    color: #1d2327;
     1588    text-decoration: none;
     1589}
     1590
    15921591.filikod-duplicates-scan-top {
    15931592    margin: 0 0 20px 0;
  • filikod/trunk/assets/js/admin.js

    r3737001 r3763820  
    2828        // Duplicates : scan par lots (empreinte md5 des fichiers)
    2929        initDuplicatesScan();
     30
     31        // Duplicates > Similar images : « C'est voulu » / « Afficher de nouveau »
     32        initVisualIgnore();
    3033    });
     34
     35    /**
     36     * Images semblables : marque un groupe comme voulu (ou le rétablit), puis recharge la liste.
     37     */
     38    function initVisualIgnore() {
     39        $(document).on('click', '.filikod-visual-ignore', function(e) {
     40            e.preventDefault();
     41            var $button = $(this);
     42            $button.prop('disabled', true);
     43            $.post(filikodAdmin.ajaxUrl, {
     44                action: 'filikod_visual_ignore',
     45                nonce: filikodAdmin.nonce,
     46                key: String($button.data('key')),
     47                ignore: String($button.data('ignore'))
     48            }).done(function(response) {
     49                if (response && response.success) {
     50                    window.location.reload();
     51                    return;
     52                }
     53                $button.prop('disabled', false);
     54                window.alert((response && response.data && response.data.message) || filikodAdmin.strings.errorOccurred);
     55            }).fail(function() {
     56                $button.prop('disabled', false);
     57                window.alert(filikodAdmin.strings.errorOccurred);
     58            });
     59        });
     60    }
    3161
    3262    /**
  • filikod/trunk/filikod.php

    r3737001 r3763820  
    44 * Plugin URI: https://filikod.com
    55 * Description: Free alt text checker for WordPress: find missing, generic or duplicate image alt text, get an ALT Quality Score (0-100%) and fix it all in bulk.
    6  * Version: 1.2.1
     6 * Version: 1.3.0
    77 * Author: Filikod
    88 * Author URI: https://filikod.com
    … …  
    2222
    2323// Définir les constantes du plugin
    24 define('FILIKOD_VERSION', '1.2.1');
     24define('FILIKOD_VERSION', '1.3.0');
    2525define('FILIKOD_PLUGIN_URL', plugin_dir_url(__FILE__));
    2626define('FILIKOD_PLUGIN_PATH', plugin_dir_path(__FILE__));
    … …  
    7676     */
    7777    public $duplicates;
     78
     79    /**
     80     * Instance de la classe Visual Duplicates (images semblables)
     81     */
     82    public $visual_duplicates;
    7883
    7984    /**
    … …  
    121126        require_once FILIKOD_PLUGIN_PATH . 'includes/security/class-filikod-svg-security.php';
    122127        require_once FILIKOD_PLUGIN_PATH . 'includes/duplicates/class-filikod-duplicates.php';
     128        require_once FILIKOD_PLUGIN_PATH . 'includes/duplicates/class-filikod-visual-duplicates.php';
    123129    }
    124130
    … …  
    128134    public function init() {
    129135        // Traductions embarquées (languages/filikod-xx_XX.mo) ; le pack de langue de translate.wordpress.org,
    130         // chargé automatiquement par WordPress, garde la priorité quand il existe.
     136        // chargé automatiquement par WordPress, garde la priorité quand il existe. Sans cet appel, la traduction
     137        // française embarquée ne serait pas chargée tant que le pack fr n'est pas validé sur translate.wordpress.org.
     138        // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Bundled translations until the language packs are validated.
    131139        load_plugin_textdomain('filikod', false, dirname(FILIKOD_PLUGIN_BASENAME) . '/languages');
    132140
    … …  
    140148        $this->svg_security = new Filikod_SVG_Security();
    141149        $this->duplicates = new Filikod_Duplicates();
     150        $this->visual_duplicates = new Filikod_Visual_Duplicates();
    142151        if ( class_exists( 'Filikod_Alt_Audit' ) ) {
    143152            Filikod_Alt_Audit::register_alt_meta_cache_hooks();
    … …  
    180189            'filikod_alt_audit_v5',
    181190            'filikod_alt_audit_v6',
     191            'filikod_alt_audit_v7',
     192            'filikod_alt_audit_v8',
    182193            'filikod_alt_norm_counts_v1',
    183194            'filikod_alt_norm_counts_v2',
    184195            'filikod_alt_norm_counts_v3',
     196            'filikod_alt_norm_counts_v4',
    185197        );
    186198        foreach ($legacy_transients as $legacy) {
  • filikod/trunk/includes/accessibility/class-filikod-accessibility.php

    r3737001 r3763820  
    2323
    2424    /**
     25     * Images reçues pendant la requête, repassées en fin de requête avec leurs copies multilingues
     26     * (voir process_pending_attachments()). Clé « site:ID », valeur array(site, ID) : en multisite,
     27     * une extension peut avoir changé de site entre l'envoi et la fin de requête.
     28     *
     29     * @var array<string, int[]>
     30     */
     31    private static $pending_ids = array();
     32
     33    /**
    2534     * Constructeur - Initialise la classe
    2635     */
    … …  
    3544     * Les hooks utilisés :
    3645     * - 'add_attachment' : Intercepte l'ajout d'une nouvelle image (après insertion dans la base)
     46     * - 'shutdown' (posé à la demande) : repasse les images de la requête et leurs copies multilingues
    3747     * - 'wp_get_attachment_image_attributes' : Filtre les attributs des images dans le contenu
    3848     */
    … …  
    5464     */
    5565    public function process_new_attachment($attachment_id) {
     66        $attachment_id = (int) $attachment_id;
     67        if (!self::has_upload_options()) {
     68            return;
     69        }
     70
     71        // Multilingue : WPML Media Translation crée une copie de l'image par langue (même fichier,
     72        // même seconde, ID suivants). Ces copies ne passaient jamais ici avec leur fichier attaché :
     73        // seule l'image envoyée recevait l'ALT, les copies des autres langues restaient vides
     74        // (constaté avec WPML 5.0). Tout est repassé en fin de requête, quand les copies sont complètes.
     75        if (empty(self::$pending_ids)) {
     76            add_action('shutdown', array($this, 'process_pending_attachments'));
     77        }
     78        $blog_id = get_current_blog_id();
     79        self::$pending_ids[$blog_id . ':' . $attachment_id] = array($blog_id, $attachment_id);
     80
    5681        // Vérifier que c'est une image
    5782        if (!wp_attachment_is_image($attachment_id)) {
    … …  
    5984        }
    6085
     86        $this->apply_upload_options($attachment_id);
     87    }
     88
     89    /**
     90     * Applique à une image les options prévues à l'envoi, dans l'ordre :
     91     * ALT automatique, nettoyage des caractères spéciaux, suppression du titre.
     92     * Chaque étape est sans effet si elle a déjà été faite : l'appeler deux fois ne change rien.
     93     *
     94     * @param int $attachment_id L'ID de l'attachment
     95     */
     96    private function apply_upload_options($attachment_id) {
    6197        // 1) Générer le texte ALT si activé (priorité : titre de l’attachment puis nom de fichier)
    6298        if (get_option('filikod_auto_alt', 'no') === 'yes') {
    … …  
    71107            $this->clear_attachment_title($attachment_id);
    72108        }
     109    }
     110
     111    /**
     112     * Au moins une option d'envoi est active (ALT automatique, nettoyage, suppression du titre).
     113     *
     114     * @return bool
     115     */
     116    private static function has_upload_options() {
     117        return get_option('filikod_auto_alt', 'no') === 'yes'
     118            || get_option('filikod_clean_alt_special_chars', 'no') === 'yes'
     119            || get_option('filikod_remove_title', 'no') === 'yes';
     120    }
     121
     122    /**
     123     * Fin de requête : repasse les images reçues et les copies multilingues créées après elles.
     124     *
     125     * Une image déjà traitée à l'envoi ne bouge plus (chaque option est sans effet la deuxième fois) ;
     126     * une copie WPML reçoit le même traitement qu'une image envoyée seule. Les traductions plus
     127     * anciennes que l'image reçue ne sont jamais touchées : elles relèvent de « Process Existing Images ».
     128     */
     129    public function process_pending_attachments() {
     130        $pending = self::$pending_ids;
     131        self::$pending_ids = array();
     132
     133        foreach ($pending as $item) {
     134            list($blog_id, $attachment_id) = $item;
     135            $switched = is_multisite() && get_current_blog_id() !== $blog_id;
     136            if ($switched) {
     137                switch_to_blog($blog_id);
     138            }
     139
     140            // Options relues ici : en multisite, chaque site a les siennes.
     141            if (self::has_upload_options()) {
     142                $ids = array_merge(array($attachment_id), self::get_later_translation_ids($attachment_id));
     143                foreach ($ids as $id) {
     144                    // Les copies ont pu recevoir leur fichier et leurs métas hors des fonctions de WordPress :
     145                    // relire la base plutôt qu'un cache objet (Redis, Memcached) amorcé avant.
     146                    wp_cache_delete($id, 'posts');
     147                    wp_cache_delete($id, 'post_meta');
     148                    if (get_post_type($id) !== 'attachment' || !wp_attachment_is_image($id)) {
     149                        continue;
     150                    }
     151                    $this->apply_upload_options($id);
     152                }
     153            }
     154
     155            if ($switched) {
     156                restore_current_blog();
     157            }
     158        }
     159    }
     160
     161    /**
     162     * IDs des traductions d'une image créées après elle (copies du même envoi), sans l'image elle-même.
     163     *
     164     * @param int $attachment_id L'ID de l'attachment
     165     * @return int[]
     166     */
     167    private static function get_later_translation_ids($attachment_id) {
     168        if (!class_exists('Filikod_Duplicates')) {
     169            return array();
     170        }
     171        $engine = Filikod_Duplicates::get_multilingual_engine();
     172        if ($engine === '') {
     173            return array();
     174        }
     175
     176        if ($engine === 'wpml') {
     177            // Lecture directe de la table de WPML, sans ses caches, comme Filikod_Duplicates::get_translation_family_map().
     178            global $wpdb;
     179            $table = esc_sql($wpdb->prefix . 'icl_translations');
     180            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPML core table, name derived from $wpdb->prefix; one query per image received in this request.
     181            $ids = $wpdb->get_col(
     182                $wpdb->prepare(
     183                    "SELECT t2.element_id
     184                    FROM {$table} t1
     185                    INNER JOIN {$table} t2 ON t2.trid = t1.trid AND t2.element_type = t1.element_type
     186                    WHERE t1.element_type = 'post_attachment'
     187                    AND t1.element_id = %d",
     188                    $attachment_id
     189                )
     190            );
     191            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
     192        } else {
     193            $ids = Filikod_Duplicates::get_translation_ids($attachment_id);
     194        }
     195
     196        $later = array();
     197        foreach ((array) $ids as $id) {
     198            $id = (int) $id;
     199            if ($id > $attachment_id) {
     200                $later[] = $id;
     201            }
     202        }
     203        return $later;
    73204    }
    74205
    … …  
    240371     */
    241372    public static function clean_special_chars_value($alt) {
    242         $alt     = (string) $alt;
     373        $alt = (string) $alt;
     374
     375        // ALT multilingue WPGlobus / qTranslate-XT : on nettoie le texte de chaque langue, jamais les
     376        // balises « {:fr} » et « [:fr] », dont les accolades et crochets font partie des caractères retirés.
     377        if (is_callable(array('Filikod_Alt_Audit', 'map_multilingual_alt'))) {
     378            $by_language = Filikod_Alt_Audit::map_multilingual_alt($alt, array(__CLASS__, 'clean_special_chars_value'));
     379            if (null !== $by_language) {
     380                return $by_language;
     381            }
     382        }
     383
    243384        $trimmed = trim($alt);
    244385
  • filikod/trunk/includes/admin/class-filikod-admin.php

    r3737001 r3763820  
    3030        add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
    3131        add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
    32         add_action( 'admin_footer', array( $this, 'inject_menu_icon_script' ) );
    3332        if ( class_exists( 'Filikod_Media_Alt_Status' ) ) {
    3433            new Filikod_Media_Alt_Status();
    … …  
    4039     */
    4140    public function add_admin_menu() {
    42         // Menu principal - Utiliser 'none' pour l'icône et l'injecter via CSS.
     41        // Menu principal.
    4342        // Le titre de menu est la marque, volontairement non traduite : WordPress en dérive
    4443        // le hook suffix des sous-pages (sanitize_title), et une translittération le casserait.
    … …  
    5049            'filikod',                                   // Slug du menu
    5150            array( $this->plugin->dashboard, 'display_dashboard_page' ), // Fonction callback
    52             'none',                                      // Pas d'icône par défaut, on l'ajoute via CSS
     51            $this->get_menu_icon_url(),                  // Icône SVG, recolorée par WordPress
    5352            30                                           // Position
    5453        );
    … …  
    103102     */
    104103    public function enqueue_admin_assets( $hook ) {
    105         // Charger le CSS sur toutes les pages admin pour que l'icône soit visible partout
    106         // C'est nécessaire car le menu est visible sur toutes les pages admin
     104        // CSS sur toutes les pages admin : les badges ALT de la médiathèque s'affichent aussi
     105        // dans la fenêtre « Ajouter un média », ouverte depuis n'importe quel écran.
    107106        wp_enqueue_style(
    108107            'filikod-admin',
    … …  
    174173        }
    175174        wp_localize_script( 'filikod-admin', 'filikodAdmin', $localize );
    176 
    177         // Ajouter le script inline pour injecter l'icône SVG du menu (fallback pour les pages du plugin)
    178         $svg = $this->get_svg_icon();
    179         $inline_script = sprintf(
    180             "(function($) {
    181                 $(document).ready(function() {
    182                     var menuIcon = $('#toplevel_page_filikod .wp-menu-image');
    183                     if (menuIcon.length && !menuIcon.find('svg').length) {
    184                         var svgIcon = %s;
    185                         menuIcon.html(svgIcon);
    186                     }
    187                 });
    188             })(jQuery);",
    189             wp_json_encode( $svg, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT )
    190         );
    191         wp_add_inline_script( 'filikod-admin', $inline_script );
    192     }
    193 
    194     /**
    195      * Injecter l'icône SVG du menu via JavaScript
    196      * Utilise admin_footer pour s'exécuter sur toutes les pages admin
    197      * Conforme aux bonnes pratiques WordPress
    198      */
    199     public function inject_menu_icon_script() {
    200         // Vérifier que le menu existe avant d'injecter le script
    201         global $menu;
    202         $menu_exists = false;
    203         if ( is_array( $menu ) ) {
    204             foreach ( $menu as $menu_item ) {
    205                 if ( isset( $menu_item[2] ) && 'filikod' === $menu_item[2] ) {
    206                     $menu_exists = true;
    207                     break;
    208                 }
    209             }
    210         }
    211 
    212         if ( ! $menu_exists ) {
    213             return;
    214         }
    215 
    216         $svg = $this->get_svg_icon();
    217         wp_register_script( 'filikod-admin-menu-icon', false, array(), FILIKOD_VERSION, false );
    218         wp_enqueue_script( 'filikod-admin-menu-icon' );
    219         wp_localize_script( 'filikod-admin-menu-icon', 'filikodMenuIconSvg', array( 'svg' => $svg ) );
    220         $inline_js = "(function() {\n"
    221             . "    function injectFilikodIcon() {\n"
    222             . "        var menuIcon = document.querySelector('#toplevel_page_filikod .wp-menu-image');\n"
    223             . "        if (menuIcon && !menuIcon.querySelector('svg')) {\n"
    224             . "            menuIcon.innerHTML = filikodMenuIconSvg.svg;\n"
    225             . "        }\n"
    226             . "    }\n"
    227             . "    if (document.readyState === 'loading') {\n"
    228             . "        document.addEventListener('DOMContentLoaded', injectFilikodIcon);\n"
    229             . "    } else {\n"
    230             . "        injectFilikodIcon();\n"
    231             . "    }\n"
    232             . "    setTimeout(injectFilikodIcon, 100);\n"
    233             . "})();";
    234         wp_add_inline_script( 'filikod-admin-menu-icon', $inline_js, 'after' );
    235     }
    236 
    237     /**
    238      * Obtenir le SVG de l'icône
     175    }
     176
     177    /**
     178     * Icône du menu en SVG encodé (data URI), le format prévu par add_menu_page().
    239179     *
    240      * @return string SVG avec currentColor
     180     * WordPress l'affiche dès le rendu de la page, sans JavaScript, et son script svg-painter
     181     * remplace chaque fill="…" par les couleurs du jeu de couleurs de l'utilisateur (normal,
     182     * survol, page active), comme pour les icônes du cœur. L'ancienne icône, injectée en
     183     * JavaScript après le chargement, apparaissait en retard ou pas du tout, dans un gris codé
     184     * en dur qui ne suivait ni le jeu de couleurs ni le survol.
     185     *
     186     * @return string
     187     */
     188    private function get_menu_icon_url() {
     189        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Menu icon data URI: svg-painter only recolors base64 SVGs.
     190        return 'data:image/svg+xml;base64,' . base64_encode( $this->get_svg_icon() );
     191    }
     192
     193    /**
     194     * SVG de l'icône du menu. Le fill #a7aaad (couleur des icônes du jeu par défaut) sert
     195     * seulement le temps que svg-painter applique les couleurs de l'utilisateur.
     196     *
     197     * @return string
    241198     */
    242199    private function get_svg_icon() {
    243         return '<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 20.0468L14 20.0468L4.50001 6.04681L1 11.5468V20.0468Z" fill="currentColor"/><path d="M7.00001 20.0468H19L13 11.0468L7.00001 20.0468Z" fill="currentColor"/><path d="M16 4.04688L14.5 2.04688L19.8595 2.14577e-05L17.5 2.04688L19 3.98544L12.9356 8.4258L16 4.04688Z" fill="currentColor"/><path d="M2.00001 6.04681V16.0468C2.00001 17.0179 2.00228 17.6458 2.06446 18.1083C2.12278 18.5419 2.21686 18.6777 2.29297 18.7538C2.36908 18.8299 2.50494 18.924 2.93848 18.9824C3.40099 19.0445 4.02893 19.0468 5.00001 19.0468H15C15.9711 19.0468 16.599 19.0445 17.0615 18.9824C17.4951 18.924 17.6309 18.8299 17.707 18.7538C17.7831 18.6777 17.8772 18.5419 17.9356 18.1083C17.9977 17.6458 18 17.0179 18 16.0468V10.0468H20V16.0468C20 16.9613 20.0022 17.7482 19.918 18.3749C19.8298 19.0303 19.6307 19.6583 19.1211 20.1679C18.6115 20.6775 17.9835 20.8766 17.3281 20.9648C16.7014 21.049 15.9145 21.0468 15 21.0468H5.00001C4.08547 21.0468 3.29863 21.049 2.67188 20.9648C2.01648 20.8766 1.38849 20.6775 0.878912 20.1679C0.369331 19.6583 0.170191 19.0303 0.0820374 18.3749C-0.00222694 17.7482 5.65403e-06 16.9613 5.65403e-06 16.0468V6.04681C5.65403e-06 5.13227 -0.00222695 4.34543 0.0820374 3.71868C0.170191 3.06328 0.369331 2.43529 0.878912 1.92571C1.38849 1.41613 2.01648 1.21699 2.67188 1.12884C3.29863 1.04457 4.08547 1.04681 5.00001 1.04681H10V3.04681H5.00001C4.02893 3.04681 3.40099 3.04908 2.93848 3.11126C2.50494 3.16958 2.36908 3.26366 2.29297 3.33977C2.21686 3.41589 2.12278 3.55174 2.06446 3.98528C2.00228 4.44779 2.00001 5.07573 2.00001 6.04681Z" fill="currentColor"/></svg>';
     200        return '<svg width="20" height="22" viewBox="0 0 20 22" xmlns="http://www.w3.org/2000/svg"><path d="M1 20.0468L14 20.0468L4.50001 6.04681L1 11.5468V20.0468Z" fill="#a7aaad"/><path d="M7.00001 20.0468H19L13 11.0468L7.00001 20.0468Z" fill="#a7aaad"/><path d="M16 4.04688L14.5 2.04688L19.8595 2.14577e-05L17.5 2.04688L19 3.98544L12.9356 8.4258L16 4.04688Z" fill="#a7aaad"/><path d="M2.00001 6.04681V16.0468C2.00001 17.0179 2.00228 17.6458 2.06446 18.1083C2.12278 18.5419 2.21686 18.6777 2.29297 18.7538C2.36908 18.8299 2.50494 18.924 2.93848 18.9824C3.40099 19.0445 4.02893 19.0468 5.00001 19.0468H15C15.9711 19.0468 16.599 19.0445 17.0615 18.9824C17.4951 18.924 17.6309 18.8299 17.707 18.7538C17.7831 18.6777 17.8772 18.5419 17.9356 18.1083C17.9977 17.6458 18 17.0179 18 16.0468V10.0468H20V16.0468C20 16.9613 20.0022 17.7482 19.918 18.3749C19.8298 19.0303 19.6307 19.6583 19.1211 20.1679C18.6115 20.6775 17.9835 20.8766 17.3281 20.9648C16.7014 21.049 15.9145 21.0468 15 21.0468H5.00001C4.08547 21.0468 3.29863 21.049 2.67188 20.9648C2.01648 20.8766 1.38849 20.6775 0.878912 20.1679C0.369331 19.6583 0.170191 19.0303 0.0820374 18.3749C-0.00222694 17.7482 5.65403e-06 16.9613 5.65403e-06 16.0468V6.04681C5.65403e-06 5.13227 -0.00222695 4.34543 0.0820374 3.71868C0.170191 3.06328 0.369331 2.43529 0.878912 1.92571C1.38849 1.41613 2.01648 1.21699 2.67188 1.12884C3.29863 1.04457 4.08547 1.04681 5.00001 1.04681H10V3.04681H5.00001C4.02893 3.04681 3.40099 3.04908 2.93848 3.11126C2.50494 3.16958 2.36908 3.26366 2.29297 3.33977C2.21686 3.41589 2.12278 3.55174 2.06446 3.98528C2.00228 4.44779 2.00001 5.07573 2.00001 6.04681Z" fill="#a7aaad"/></svg>';
    244201    }
    245202
  • filikod/trunk/includes/admin/class-filikod-media-alt-status.php

    r3737001 r3763820  
    148148                $image_alt = wp_unslash( $attachment['image_alt'] );
    149149                if ( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) !== $image_alt ) {
    150                         $image_alt = wp_strip_all_tags( $image_alt, true );
     150                        // Ancienne forme qTranslate « <!--:fr--> » convertie en « [:fr] » avant de retirer le HTML.
     151                        $image_alt = wp_strip_all_tags( class_exists( 'Filikod_Alt_Audit' ) ? Filikod_Alt_Audit::protect_multilingual_markup( $image_alt ) : $image_alt, true );
    151152                        update_post_meta( $attachment_id, '_wp_attachment_image_alt', wp_slash( $image_alt ) );
    152153                }
  • filikod/trunk/includes/class-filikod-alt-audit.php

    r3737001 r3763820  
    4242         * reparcourir tous les fichiers à chaque requête (ex. preview modale).
    4343         */
    44         const ALT_NORM_COUNTS_TRANSIENT = 'filikod_alt_norm_counts_v3';
     44        const ALT_NORM_COUNTS_TRANSIENT = 'filikod_alt_norm_counts_v5';
    4545
    4646        /**
    4747         * Clé du transient d’audit (incrémenter à chaque évolution de structure).
    4848         */
    49         const AUDIT_TRANSIENT = 'filikod_alt_audit_v7';
     49        const AUDIT_TRANSIENT = 'filikod_alt_audit_v9';
    5050
    5151        /**
    … …  
    183183
    184184        /**
     185         * Balises de langue que WPGlobus et qTranslate-XT écrivent dans un seul champ ALT :
     186         * « {:fr}Usine{:}{:en}Factory{:} », « [:fr]Usine[:en]Factory[:] » ou l'ancienne forme
     187         * « <!--:fr-->Usine<!--:--> ». Ouverture avec code de langue (fr, en, pt-br) ou fermeture vide.
     188         */
     189        const MULTILINGUAL_TAG_PATTERN = '/(\{:[a-z]{2}(?:[-_][a-z0-9]{2,4})?\}|\[:[a-z]{2}(?:[-_][a-z0-9]{2,4})?\]|<!--:[a-z]{2}(?:[-_][a-z0-9]{2,4})?-->|\{:\}|\[:\]|<!--:-->)/i';
     190
     191        /**
     192         * Découpe un ALT multilingue (WPGlobus, qTranslate-XT) en textes par langue.
     193         *
     194         * @param string $alt ALT brut.
     195         * @return array<string, string>|null Code de langue (2 lettres) => texte, null si l'ALT n'a pas de balise de langue.
     196         */
     197        public static function split_multilingual_alt( $alt ) {
     198                if ( ! is_string( $alt ) || ( false === strpos( $alt, '{:' ) && false === strpos( $alt, '[:' ) && false === strpos( $alt, '<!--:' ) ) ) {
     199                        return null;
     200                }
     201                $parts    = preg_split( self::MULTILINGUAL_TAG_PATTERN, $alt, -1, PREG_SPLIT_DELIM_CAPTURE );
     202                $segments = array();
     203                $lang     = null;
     204                foreach ( $parts as $i => $part ) {
     205                        if ( 1 === $i % 2 ) {
     206                                // Délimiteur : ouverture d'une langue, ou fermeture ({:}, [:], <!--:-->).
     207                                $lang = preg_match( '/[a-z]{2}/i', substr( $part, 2 ), $m ) ? strtolower( $m[0] ) : null;
     208                                if ( null !== $lang && ! isset( $segments[ $lang ] ) ) {
     209                                        $segments[ $lang ] = '';
     210                                }
     211                                continue;
     212                        }
     213                        if ( null !== $lang ) {
     214                                $segments[ $lang ] .= $part;
     215                        }
     216                }
     217                if ( empty( $segments ) ) {
     218                        return null;
     219                }
     220                return array_map( 'trim', $segments );
     221        }
     222
     223        /**
     224         * Applique une fonction au texte de chaque langue d'un ALT multilingue en gardant ses balises
     225         * (nettoyage des caractères spéciaux : les accolades et crochets des balises ne doivent pas partir).
     226         *
     227         * @param string   $alt      ALT brut.
     228         * @param callable $callback Reçoit et renvoie le texte d'une langue.
     229         * @return string|null ALT reconstruit, null si l'ALT n'a pas de balise de langue.
     230         */
     231        public static function map_multilingual_alt( $alt, $callback ) {
     232                if ( null === self::split_multilingual_alt( $alt ) ) {
     233                        return null;
     234                }
     235                $parts = preg_split( self::MULTILINGUAL_TAG_PATTERN, $alt, -1, PREG_SPLIT_DELIM_CAPTURE );
     236                foreach ( $parts as $i => $part ) {
     237                        if ( 0 === $i % 2 && trim( $part ) !== '' ) {
     238                                $parts[ $i ] = call_user_func( $callback, $part );
     239                        }
     240                }
     241                return implode( '', $parts );
     242        }
     243
     244        /**
     245         * Remplace l'ancienne forme qTranslate « <!--:fr--> » par « [:fr] » avant un nettoyage HTML
     246         * (sanitize_text_field, wp_strip_all_tags) qui supprimerait ces commentaires et mélangerait les langues.
     247         *
     248         * @param string $alt ALT brut.
     249         * @return string
     250         */
     251        public static function protect_multilingual_markup( $alt ) {
     252                if ( ! is_string( $alt ) || false === strpos( $alt, '<!--:' ) ) {
     253                        return (string) $alt;
     254                }
     255                return preg_replace( '/<!--:([a-z]{2}(?:[-_][a-z0-9]{2,4})?)?-->/i', '[:$1]', $alt );
     256        }
     257
     258        /**
     259         * Extension qui écrit plusieurs langues dans le même champ ALT.
     260         *
     261         * @return string 'wpglobus' | 'qtranslate' | ''.
     262         */
     263        public static function get_alt_language_engine() {
     264                if ( defined( 'WPGLOBUS_VERSION' ) || class_exists( 'WPGlobus', false ) ) {
     265                        return 'wpglobus';
     266                }
     267                if ( defined( 'QTX_VERSION' ) || ! empty( $GLOBALS['q_config']['enabled_languages'] ) ) {
     268                        return 'qtranslate';
     269                }
     270                return '';
     271        }
     272
     273        /**
     274         * Langues activées dans WPGlobus ou qTranslate-XT, langue par défaut en premier (codes à 2 lettres).
     275         * Chaque ALT multilingue doit avoir un texte dans chacune ; vide quand aucune des deux n'est active.
     276         *
     277         * @return string[]
     278         */
     279        public static function get_alt_languages() {
     280                $languages = array();
     281                $default   = '';
     282                if ( 'wpglobus' === self::get_alt_language_engine() && is_callable( array( 'WPGlobus', 'Config' ) ) ) {
     283                        $config = call_user_func( array( 'WPGlobus', 'Config' ) );
     284                        if ( is_object( $config ) ) {
     285                                $languages = isset( $config->enabled_languages ) ? (array) $config->enabled_languages : array();
     286                                $default   = isset( $config->default_language ) ? (string) $config->default_language : '';
     287                        }
     288                } elseif ( 'qtranslate' === self::get_alt_language_engine() ) {
     289                        $q_config  = isset( $GLOBALS['q_config'] ) && is_array( $GLOBALS['q_config'] ) ? $GLOBALS['q_config'] : array();
     290                        $languages = isset( $q_config['enabled_languages'] ) ? (array) $q_config['enabled_languages'] : array();
     291                        $default   = isset( $q_config['default_language'] ) ? (string) $q_config['default_language'] : '';
     292                }
     293                $out = array();
     294                foreach ( array_merge( array( $default ), $languages ) as $code ) {
     295                        $code = strtolower( substr( (string) $code, 0, 2 ) );
     296                        if ( preg_match( '/^[a-z]{2}$/', $code ) && ! in_array( $code, $out, true ) ) {
     297                                $out[] = $code;
     298                        }
     299                }
     300                return $out;
     301        }
     302
     303        /**
     304         * Clés de l'ALT pour la détection des doublons : l'ALT normalisé, ou un « langue:texte » par langue
     305         * pour un ALT multilingue (deux images qui partagent leur texte français sont en doublon en français).
     306         * Les deux-points ne survivent pas à normalize_alt() : ces clés ne croisent jamais un ALT simple.
     307         *
     308         * @param string $alt ALT brut.
     309         * @return string[]
     310         */
     311        public static function alt_keys( $alt ) {
     312                $segments = self::split_multilingual_alt( $alt );
     313                if ( null === $segments ) {
     314                        $norm = self::normalize_alt( $alt );
     315                        return $norm === '' ? array() : array( $norm );
     316                }
     317                $keys = array();
     318                foreach ( $segments as $lang => $text ) {
     319                        $norm = self::normalize_alt( $text );
     320                        if ( $norm !== '' ) {
     321                                $keys[] = $lang . ':' . $norm;
     322                        }
     323                }
     324                return array_values( array_unique( $keys ) );
     325        }
     326
     327        /**
    185328         * Récupère la langue et la locale WordPress.
    186329         *
    … …  
    451594                                continue;
    452595                        }
    453                         $norm = self::normalize_alt( $alt );
    454                         if ( $norm !== '' ) {
    455                                 self::add_identity( $identities, $norm, self::identity_for( (int) $id ) );
     596                        foreach ( self::alt_keys( $alt ) as $key ) {
     597                                self::add_identity( $identities, $key, self::identity_for( (int) $id ) );
    456598                        }
    457599                }
    … …  
    482624                                continue;
    483625                        }
    484                         $norm = self::normalize_alt( $alt );
    485                         if ( $norm !== '' ) {
    486                                 self::add_identity( $identities, $norm, self::identity_for( $id ) );
     626                        foreach ( self::alt_keys( $alt ) as $key ) {
     627                                self::add_identity( $identities, $key, self::identity_for( $id ) );
    487628                        }
    488629                }
    … …  
    521662                                continue;
    522663                        }
    523                         $norm = self::normalize_alt( $raw );
    524                         if ( $norm === '' ) {
     664                        $keys = self::alt_keys( $raw );
     665                        if ( empty( $keys ) ) {
    525666                                continue;
    526667                        }
    … …  
    528669                                ? Filikod_Duplicates::identity_from_hash( (int) $row->ID, isset( $row->hash ) ? $row->hash : null )
    529670                                : 'id:' . (int) $row->ID;
    530                         self::add_identity( $map, $norm, $identity );
     671                        foreach ( $keys as $key ) {
     672                                self::add_identity( $map, $key, $identity );
     673                        }
    531674                }
    532675                return $map;
    … …  
    572715                $old_raw       = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
    573716                $old_raw       = is_string( $old_raw ) ? $old_raw : '';
    574                 $old_norm      = self::normalize_alt( $old_raw );
    575                 $new_norm      = self::normalize_alt( $alt_raw );
    576                 if ( $old_norm !== '' ) {
    577                         self::remove_identity( $adj, $old_norm, $identity );
    578                 }
    579                 if ( $new_norm !== '' ) {
    580                         self::add_identity( $adj, $new_norm, $identity );
     717                foreach ( self::alt_keys( $old_raw ) as $key ) {
     718                        self::remove_identity( $adj, $key, $identity );
     719                }
     720                foreach ( self::alt_keys( $alt_raw ) as $key ) {
     721                        self::add_identity( $adj, $key, $identity );
    581722                }
    582723                return self::counts_from_identity_map( $adj );
    … …  
    599740                        return array( 'decorative' );
    600741                }
    601                 $dup_map = self::duplicate_map_for_alt_preview( $attachment_id, is_string( $alt_raw ) ? $alt_raw : '' );
    602                 $generic_terms = self::get_audit_generic_terms();
    603                 $alt_norm      = self::normalize_alt( is_string( $alt_raw ) ? $alt_raw : '' );
    604                 return self::collect_alt_issue_codes( $alt_norm, $generic_terms, $dup_map, false );
     742                $alt_raw = is_string( $alt_raw ) ? $alt_raw : '';
     743                $dup_map = self::duplicate_map_for_alt_preview( $attachment_id, $alt_raw );
     744                $result  = self::evaluate_alt( $alt_raw, $dup_map );
     745                return $result['issues'];
    605746        }
    606747
    … …  
    675816                $alt_raw = is_string( $alt_raw ) ? maybe_unserialize( $alt_raw ) : '';
    676817                $alt_raw = is_string( $alt_raw ) ? $alt_raw : '';
    677                 $alt_norm = self::normalize_alt( $alt_raw );
    678 
    679                 // 1. missing
     818
     819                $result            = self::evaluate_alt( $alt_raw, $dup_map );
     820                $result['alt_raw'] = $alt_raw;
     821                return $result;
     822        }
     823
     824        /**
     825         * Évalue un ALT brut : simple, ou multilingue (WPGlobus, qTranslate-XT) langue par langue.
     826         *
     827         * Un ALT multilingue est jugé sur sa langue la plus faible : chaque langue attendue doit avoir un
     828         * texte (sinon « missing »), puis chaque texte passe les règles habituelles, avec les termes
     829         * génériques de sa langue. La longueur se mesure par langue : un ALT bilingue n'est plus « too_long »
     830         * parce que ses deux langues sont comptées ensemble.
     831         *
     832         * @param string             $alt_raw ALT brut.
     833         * @param array<string, int> $dup_map Map clé d'ALT (alt_keys()) => images distinctes.
     834         * @return array{score: int, status: string, alt_normalized: string, issues: string[], languages: array|null}
     835         */
     836        public static function evaluate_alt( $alt_raw, $dup_map ) {
     837                $segments = self::split_multilingual_alt( $alt_raw );
     838                if ( null === $segments ) {
     839                        $alt_norm = self::normalize_alt( $alt_raw );
     840                        $result   = self::evaluate_plain_alt( $alt_norm, self::get_audit_generic_terms(), $dup_map, $alt_norm );
     841                        $result['languages'] = null;
     842                        return $result;
     843                }
     844
     845                $expected = self::get_alt_languages();
     846                if ( empty( $expected ) ) {
     847                        // Balises sans WPGlobus ni qTranslate-XT actif : on contrôle les langues présentes.
     848                        $expected = array_keys( $segments );
     849                }
     850                $present = array();
     851                $missing = array();
     852                $worst   = null;
     853                $issues  = array();
     854                $norms   = array();
     855                foreach ( $expected as $lang ) {
     856                        $norm = self::normalize_alt( isset( $segments[ $lang ] ) ? $segments[ $lang ] : '' );
     857                        if ( $norm === '' ) {
     858                                $missing[] = $lang;
     859                                continue;
     860                        }
     861                        $present[] = $lang;
     862                        $norms[]   = $norm;
     863                        $result    = self::evaluate_plain_alt( $norm, self::get_generic_terms_for_language( $lang ), $dup_map, $lang . ':' . $norm );
     864                        $issues    = array_merge( $issues, $result['issues'] );
     865                        if ( null === $worst || $result['score'] < $worst['score'] ) {
     866                                $worst = $result;
     867                        }
     868                }
     869
     870                if ( ! empty( $missing ) || null === $worst ) {
     871                        $worst = array(
     872                                'score'  => 0,
     873                                'status' => 'missing',
     874                        );
     875                        array_unshift( $issues, 'missing' );
     876                }
     877                return array(
     878                        'score'          => $worst['score'],
     879                        'status'         => $worst['status'],
     880                        'alt_normalized' => implode( ' ', $norms ),
     881                        'issues'         => array_values( array_unique( $issues ) ),
     882                        'languages'      => array(
     883                                'present' => $present,
     884                                'missing' => $missing,
     885                        ),
     886                );
     887        }
     888
     889        /**
     890         * Score et statut d'un texte ALT normalisé (une seule langue).
     891         * Ordre : missing → generic → too_short → duplicate → too_long → correct.
     892         *
     893         * @param string             $alt_norm      ALT normalisé.
     894         * @param string[]           $generic_terms Termes génériques.
     895         * @param array<string, int> $dup_map       Map clé d'ALT => images distinctes.
     896         * @param string             $dup_key       Clé de cet ALT dans $dup_map.
     897         * @return array{score: int, status: string, alt_normalized: string, issues: string[]}
     898         */
     899        private static function evaluate_plain_alt( $alt_norm, $generic_terms, $dup_map, $dup_key ) {
     900                $issues = self::collect_alt_issue_codes( $alt_norm, $generic_terms, $dup_map, false, $dup_key );
    680901                if ( $alt_norm === '' ) {
    681                         return array(
    682                                 'score'           => 0,
    683                                 'status'          => 'missing',
    684                                 'alt_raw'         => $alt_raw,
    685                                 'alt_normalized'  => $alt_norm,
    686                         );
    687                 }
    688 
    689                 $generic_terms = self::get_audit_generic_terms();
    690 
    691                 // 2. generic
    692                 if ( self::is_generic_alt( $alt_norm, $generic_terms ) ) {
    693                         return array(
    694                                 'score'           => 20,
    695                                 'status'          => 'generic',
    696                                 'alt_raw'         => $alt_raw,
    697                                 'alt_normalized'  => $alt_norm,
    698                         );
    699                 }
    700 
    701                 // 3. too short (moins de 8 caractères)
    702                 if ( self::alt_length( $alt_norm ) < self::TOO_SHORT_MAX_LEN ) {
    703                         return array(
    704                                 'score'           => 40,
    705                                 'status'          => 'too_short',
    706                                 'alt_raw'         => $alt_raw,
    707                                 'alt_normalized'  => $alt_norm,
    708                         );
    709                 }
    710 
    711                 // 4. duplicate
    712                 $count = isset( $dup_map[ $alt_norm ] ) ? (int) $dup_map[ $alt_norm ] : 0;
    713                 if ( $count >= 2 ) {
    714                         return array(
    715                                 'score'           => 60,
    716                                 'status'          => 'duplicate',
    717                                 'alt_raw'         => $alt_raw,
    718                                 'alt_normalized'  => $alt_norm,
    719                         );
    720                 }
    721 
    722                 // 5. too long (> 125 caractères) : moins grave que doublon mais signalé pour a11y/SEO.
    723                 if ( self::alt_length( $alt_norm ) > self::TOO_LONG_MIN_LEN ) {
    724                         return array(
    725                                 'score'           => 70,
    726                                 'status'          => 'too_long',
    727                                 'alt_raw'         => $alt_raw,
    728                                 'alt_normalized'  => $alt_norm,
    729                         );
    730                 }
    731 
    732                 // 6. correct → base 90 + bonus
    733                 $score = 90;
    734                 if ( self::alt_length( $alt_norm ) >= 8 ) {
    735                         $score += 5;
    736                 }
    737                 $word_count = count( preg_split( '/\s+/', $alt_norm, -1, PREG_SPLIT_NO_EMPTY ) );
    738                 if ( $word_count >= 2 ) {
    739                         $score += 5;
    740                 }
    741                 $score = min( 100, $score );
    742 
     902                        $score  = 0;
     903                        $status = 'missing';
     904                } elseif ( in_array( 'generic', $issues, true ) ) {
     905                        $score  = 20;
     906                        $status = 'generic';
     907                } elseif ( in_array( 'too_short', $issues, true ) ) {
     908                        $score  = 40;
     909                        $status = 'too_short';
     910                } elseif ( in_array( 'duplicate', $issues, true ) ) {
     911                        $score  = 60;
     912                        $status = 'duplicate';
     913                } elseif ( in_array( 'too_long', $issues, true ) ) {
     914                        // Moins grave que doublon mais signalé pour a11y/SEO.
     915                        $score  = 70;
     916                        $status = 'too_long';
     917                } else {
     918                        // Correct → base 90 + bonus (8 caractères et plus, deux mots et plus).
     919                        $score  = 95;
     920                        $status = 'correct';
     921                        if ( count( preg_split( '/\s+/', $alt_norm, -1, PREG_SPLIT_NO_EMPTY ) ) >= 2 ) {
     922                                $score += 5;
     923                        }
     924                }
    743925                return array(
    744                         'score'           => $score,
    745                         'status'          => 'correct',
    746                         'alt_raw'         => $alt_raw,
    747                         'alt_normalized'  => $alt_norm,
     926                        'score'          => $score,
     927                        'status'         => $status,
     928                        'alt_normalized' => $alt_norm,
     929                        'issues'         => $issues,
    748930                );
     931        }
     932
     933        /**
     934         * Termes génériques pour le texte d'une langue donnée : ceux de l'audit (anglais + langue du site)
     935         * plus ceux de cette langue (« Bild » dans l'ALT allemand d'un site français).
     936         *
     937         * @param string $lang Code à 2 lettres.
     938         * @return string[]
     939         */
     940        private static function get_generic_terms_for_language( $lang ) {
     941                static $cache = array();
     942                if ( ! isset( $cache[ $lang ] ) ) {
     943                        $terms = self::get_audit_generic_terms();
     944                        if ( isset( self::$generic_terms_by_lang[ $lang ] ) ) {
     945                                $terms = array_values( array_unique( array_merge( $terms, self::get_generic_terms( $lang, $lang ) ) ) );
     946                        }
     947                        $cache[ $lang ] = $terms;
     948                }
     949                return $cache[ $lang ];
    749950        }
    750951
    … …  
    757958         * @param array<string, int>  $dup_map        Map alt normalisé => occurrences.
    758959         * @param bool                $is_decorative  Image marquée décorative.
     960         * @param string|null         $dup_key        Clé dans $dup_map (« fr:usine » pour une langue d'un ALT multilingue) ; null = $alt_norm.
    759961         * @return string[] Codes : decorative, missing, generic, too_short, duplicate, too_long.
    760962         */
    761         public static function collect_alt_issue_codes( $alt_norm, $generic_terms, $dup_map, $is_decorative = false ) {
     963        public static function collect_alt_issue_codes( $alt_norm, $generic_terms, $dup_map, $is_decorative = false, $dup_key = null ) {
    762964                if ( $is_decorative ) {
    763965                        return array( 'decorative' );
    … …  
    774976                        $issues[] = 'too_short';
    775977                }
    776                 $cnt = isset( $dup_map[ $alt_norm ] ) ? (int) $dup_map[ $alt_norm ] : 0;
     978                $dup_key = null === $dup_key ? $alt_norm : (string) $dup_key;
     979                $cnt     = isset( $dup_map[ $dup_key ] ) ? (int) $dup_map[ $dup_key ] : 0;
    777980                if ( $cnt >= 2 ) {
    778981                        $issues[] = 'duplicate';
    … …  
    7991002                foreach ( array_chunk( $ids, 1000 ) as $chunk ) {
    8001003                        $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%d' ) );
    801                         // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- placeholders %d générés pour la clause IN ; résultat mis en cache dans le transient d'audit.
     1004                        // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- un %d généré par ID pour la clause IN, valeurs passées à prepare() ; résultat mis en cache dans le transient d'audit.
    8021005                        $rows = $wpdb->get_results(
    8031006                                $wpdb->prepare(
    … …  
    8061009                                )
    8071010                        );
     1011                        // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
    8081012                        if ( ! is_array( $rows ) ) {
    8091013                                continue;
    … …  
    8801084                                continue;
    8811085                        }
    882                         $result['issues'] = self::collect_alt_issue_codes(
    883                                 isset( $result['alt_normalized'] ) ? $result['alt_normalized'] : '',
    884                                 $generic_terms,
    885                                 $dup_map,
    886                                 false
    887                         );
     1086                        if ( ! isset( $result['issues'] ) ) {
     1087                                $result['issues'] = self::collect_alt_issue_codes(
     1088                                        isset( $result['alt_normalized'] ) ? $result['alt_normalized'] : '',
     1089                                        $generic_terms,
     1090                                        $dup_map,
     1091                                        false
     1092                                );
     1093                        }
    8881094                        $items[ $id ] = $result;
    8891095                        if ( isset( $counts[ $result['status'] ] ) ) {
    … …  
    9121118         */
    9131119        public static function get_image_attachment_ids( $limit = 0 ) {
    914                 $args = array(
    915                         'post_type'        => 'attachment',
    916                         'post_status'      => 'inherit',
    917                         'post_mime_type'   => 'image',
    918                         'fields'           => 'ids',
    919                         'posts_per_page'   => $limit ? $limit : -1,
    920                         'orderby'          => 'ID',
    921                         'order'            => 'ASC',
    922                         // Multilingue : WPML et Polylang filtrent les requêtes admin sur la langue courante.
    923                         // L'audit doit couvrir toute la médiathèque, comme les compteurs SQL du plugin.
    924                         'suppress_filters' => true,
    925                 );
    926                 $q = new WP_Query( $args );
    927                 return is_array( $q->posts ) ? $q->posts : array();
     1120                global $wpdb;
     1121                $limit = max( 0, (int) $limit );
     1122                // Requête directe plutôt que WP_Query : WPML et Polylang filtrent les requêtes admin sur la langue
     1123                // courante, et l'audit doit couvrir toute la médiathèque, comme les compteurs SQL du plugin
     1124                // (l'ancien 'suppress_filters' => true est refusé par Plugin Check).
     1125                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Résultat mis en cache dans le transient d'audit.
     1126                if ( $limit > 0 ) {
     1127                        $ids = $wpdb->get_col(
     1128                                $wpdb->prepare(
     1129                                        "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_mime_type LIKE %s ORDER BY ID ASC LIMIT %d",
     1130                                        'image/%',
     1131                                        $limit
     1132                                )
     1133                        );
     1134                } else {
     1135                        $ids = $wpdb->get_col(
     1136                                $wpdb->prepare(
     1137                                        "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND post_status = 'inherit' AND post_mime_type LIKE %s ORDER BY ID ASC",
     1138                                        'image/%'
     1139                                )
     1140                        );
     1141                }
     1142                // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
     1143                return is_array( $ids ) ? array_map( 'intval', $ids ) : array();
    9281144        }
    9291145
    … …  
    9971213                        return array( 'decorative' );
    9981214                }
    999                 $dup_map       = self::get_cached_global_alt_norm_counts();
    1000                 $generic_terms = self::get_audit_generic_terms();
    1001                 $result        = self::score_for_attachment( $attachment_id, $dup_map );
    1002                 $alt_norm      = isset( $result['alt_normalized'] ) ? $result['alt_normalized'] : '';
    1003                 return self::collect_alt_issue_codes( $alt_norm, $generic_terms, $dup_map, false );
     1215                $dup_map = self::get_cached_global_alt_norm_counts();
     1216                $result  = self::score_for_attachment( $attachment_id, $dup_map );
     1217                return isset( $result['issues'] ) ? $result['issues'] : array();
    10041218        }
    10051219
  • filikod/trunk/includes/dashboard/class-filikod-dashboard.php

    r3737001 r3763820  
    168168            wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'filikod' ) ) );
    169169        }
    170         $new_alt = isset( $_POST['filikod_alt_value'] ) ? sanitize_text_field( wp_unslash( $_POST['filikod_alt_value'] ) ) : '';
     170        $new_alt = isset( $_POST['filikod_alt_value'] ) ? wp_unslash( $_POST['filikod_alt_value'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on the next line, after the multilingual markup is protected.
     171        // qTranslate (ancienne forme « <!--:fr--> ») : sanitize_text_field supprimerait les balises et mélangerait les langues.
     172        $new_alt = sanitize_text_field( class_exists( 'Filikod_Alt_Audit' ) ? Filikod_Alt_Audit::protect_multilingual_markup( (string) $new_alt ) : (string) $new_alt );
    171173        $new_alt = trim( $new_alt );
    172174        // Plafond de sécurité : un ALT légitime dépasse rarement 250 caractères ; on accepte 1000 pour rester souple.
  • filikod/trunk/includes/duplicates/class-filikod-duplicates.php

    r3704678 r3763820  
    5050         */
    5151        const OPTION_UPLOAD_ALERT = 'filikod_duplicate_upload_alert';
     52
     53        /**
     54         * Attachment ID => identity for the ALT audit, built once per request (see get_identity_map()).
     55         *
     56         * @var array<int, string>|null
     57         */
     58        private static $identity_map = null;
    5259
    5360        /**
    … …  
    96103        /**
    97104         * Index attachment ID => group (from the cached groups), built once per request.
     105         * A group keeps one attachment per translation set (WPML, Polylang, Bogo): the other
     106         * language versions of that attachment are indexed too, so each of them carries the
     107         * badge in the Media Library, whatever the language it is browsed in.
    98108         *
    99109         * @return array<int, array{hash:string, ids:int[], sizes:array<int,int>}>
    … …  
    107117                        $index      = array();
    108118                        $built_from = $stamp;
     119                        $families   = empty( $data['groups'] ) ? array() : self::get_translation_family_map();
     120                        $members    = array();
     121                        foreach ( $families as $member_id => $family ) {
     122                                $members[ $family ][] = (int) $member_id;
     123                        }
    109124                        foreach ( $data['groups'] as $group ) {
    110125                                foreach ( $group['ids'] as $id ) {
    111126                                        $index[ (int) $id ] = $group;
     127                                        $family = isset( $families[ (int) $id ] ) ? $families[ (int) $id ] : '';
     128                                        if ( '' === $family ) {
     129                                                continue;
     130                                        }
     131                                        foreach ( $members[ $family ] as $member_id ) {
     132                                                if ( ! isset( $index[ $member_id ] ) ) {
     133                                                        $index[ $member_id ] = $group;
     134                                                }
     135                                        }
    112136                                }
    113137                        }
    … …  
    139163                }
    140164                $reference_id = (int) $ids[0];
     165                // A language version of the reference is the reference too, not a copy of it.
     166                $family       = self::get_translation_family_of( $attachment_id );
     167                $is_reference = $reference_id === $attachment_id
     168                        || ( '' !== $family && self::get_translation_family_of( $reference_id ) === $family );
    141169                $path         = get_attached_file( $reference_id );
    142170                $name         = ( is_string( $path ) && $path !== '' ) ? wp_basename( $path ) : get_the_title( $reference_id );
    … …  
    148176                        'edit_url'     => is_string( $edit ) ? $edit : '',
    149177                        'library_url'  => add_query_arg( array( 'item' => $reference_id ), admin_url( 'upload.php' ) ),
    150                         'is_reference' => ( $reference_id === $attachment_id ),
     178                        'is_reference' => $is_reference,
    151179                        'copies'       => count( $ids ) - 1,
    152180                        'ids'          => $ids,
    … …  
    390418
    391419        /**
    392          * Identity used by the ALT audit to group images: two copies of the same
    393          * file share one identity, an unscanned image is its own identity.
     420         * Identity used by the ALT audit to group images: copies of the same file and
     421         * translations of one media share one identity, any other image is its own identity.
    394422         *
    395423         * @param int $attachment_id Attachment ID.
    396          * @return string "h:<md5>" or "id:<ID>".
     424         * @return string "f:<set>", "h:<md5>" or "id:<ID>".
    397425         */
    398426        public static function identity_for_attachment( $attachment_id ) {
    … …  
    408436         */
    409437        public static function identity_from_hash( $attachment_id, $hash ) {
     438                $attachment_id = (int) $attachment_id;
     439                $map           = self::get_identity_map();
     440                if ( isset( $map[ $attachment_id ] ) ) {
     441                        return $map[ $attachment_id ];
     442                }
    410443                if ( is_string( $hash ) && strlen( $hash ) === 32 ) {
    411444                        return 'h:' . $hash;
    412445                }
    413                 // Not fingerprinted yet: the translations of one media (WPML, Polylang, Bogo) still
    414                 // count as a single image, so the same ALT on its FR/EN/ES versions is not "duplicated".
    415                 $family = self::get_translation_family_of( (int) $attachment_id );
    416                 return $family !== '' ? 'f:' . $family : 'id:' . (int) $attachment_id;
     446                return 'id:' . $attachment_id;
     447        }
     448
     449        /**
     450         * Groups the attachments linked by a common file (same hash) or by a translation link
     451         * (WPML, Polylang, Bogo), transitively, so the same ALT on them is never "duplicated".
     452         *
     453         * The identity used to be the hash when there was one, the translation set otherwise. A WPML
     454         * original is fingerprinted on upload but its language copies are not (they get their file
     455         * after add_attachment): the original counted as "h:<md5>", each copy as "f:<set>", and
     456         * giving the copies their ALT turned the whole set into "Duplicated ALT".
     457         *
     458         * @return array<int, string> Attachment ID => identity, only for attachments that have a hash or a translation.
     459         */
     460        private static function get_identity_map() {
     461                if ( null !== self::$identity_map ) {
     462                        return self::$identity_map;
     463                }
     464                global $wpdb;
     465                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One query for all hashes, cached per request; the ALT audit using it is cached as a transient.
     466                $rows = $wpdb->get_results(
     467                        $wpdb->prepare(
     468                                "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
     469                                self::META_HASH
     470                        )
     471                );
     472
     473                // Union-find on the nodes "id:N", "h:<md5>" and "f:<set>". The smallest node name becomes
     474                // the root, so a group keeps the same identity from one request to the next.
     475                $parent = array();
     476                $find   = static function ( $node ) use ( &$parent ) {
     477                        $root = $node;
     478                        while ( $parent[ $root ] !== $root ) {
     479                                $root = $parent[ $root ];
     480                        }
     481                        while ( $parent[ $node ] !== $root ) {
     482                                $next            = $parent[ $node ];
     483                                $parent[ $node ] = $root;
     484                                $node            = $next;
     485                        }
     486                        return $root;
     487                };
     488                $union  = static function ( $a, $b ) use ( &$parent, $find ) {
     489                        if ( ! isset( $parent[ $a ] ) ) {
     490                                $parent[ $a ] = $a;
     491                        }
     492                        if ( ! isset( $parent[ $b ] ) ) {
     493                                $parent[ $b ] = $b;
     494                        }
     495                        $root_a = $find( $a );
     496                        $root_b = $find( $b );
     497                        if ( $root_a === $root_b ) {
     498                                return;
     499                        }
     500                        if ( strcmp( $root_a, $root_b ) < 0 ) {
     501                                $parent[ $root_b ] = $root_a;
     502                        } else {
     503                                $parent[ $root_a ] = $root_b;
     504                        }
     505                };
     506
     507                if ( is_array( $rows ) ) {
     508                        foreach ( $rows as $row ) {
     509                                if ( is_string( $row->meta_value ) && strlen( $row->meta_value ) === 32 ) {
     510                                        $union( 'id:' . (int) $row->post_id, 'h:' . $row->meta_value );
     511                                }
     512                        }
     513                }
     514                foreach ( self::get_translation_family_map() as $id => $family ) {
     515                        $union( 'id:' . (int) $id, 'f:' . $family );
     516                }
     517
     518                // "f:" and "h:" sort before "id:": a linked attachment always gets a file or set identity.
     519                $map = array();
     520                foreach ( array_keys( $parent ) as $node ) {
     521                        if ( strpos( $node, 'id:' ) === 0 ) {
     522                                $map[ (int) substr( $node, 3 ) ] = $find( $node );
     523                        }
     524                }
     525                self::$identity_map = $map;
     526                return $map;
    417527        }
    418528
    … …  
    488598         */
    489599        public static function invalidate_cache() {
     600                self::$identity_map = null;
    490601                delete_transient( self::GROUPS_TRANSIENT );
     602                if ( class_exists( 'Filikod_Visual_Duplicates' ) ) {
     603                        // Similar images skip exact copies and group translations: both depend on hashes and links.
     604                        Filikod_Visual_Duplicates::invalidate_cache();
     605                }
    491606                if ( class_exists( 'Filikod_Alt_Audit' ) ) {
    492607                        Filikod_Alt_Audit::invalidate_cache();
    … …  
    513628                        self::invalidate_cache();
    514629                } elseif ( '_original_post' === $meta_key ) {
    515                         // Bogo : lien de traduction posé ou modifié.
    516                         delete_transient( self::GROUPS_TRANSIENT );
     630                        // Bogo : lien de traduction posé ou modifié (groupes et identités de l'audit ALT).
     631                        self::invalidate_cache();
    517632                }
    518633        }
    … …  
    528643                        self::invalidate_cache();
    529644                } elseif ( '_original_post' === $meta_key ) {
    530                         delete_transient( self::GROUPS_TRANSIENT );
     645                        self::invalidate_cache();
    531646                }
    532647        }
    … …  
    553668                        return 'polylang';
    554669                }
    555                 if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
     670                if ( defined( 'ICL_SITEPRESS_VERSION' ) || class_exists( 'SitePress', false ) ) {
    556671                        return 'wpml';
    557672                }
    … …  
    593708                                break;
    594709                        case 'wpml':
    595                                 $table = $wpdb->prefix . 'icl_translations';
     710                                $table = esc_sql( $wpdb->prefix . 'icl_translations' );
    596711                                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPML core table, name derived from $wpdb->prefix; one-shot map cached per request.
    597712                                $rows = $wpdb->get_results( "SELECT element_id, trid FROM {$table} WHERE element_type = 'post_attachment'" );
    … …  
    656771                                break;
    657772                        case 'wpml':
     773                                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML public API filter.
    658774                                $trid = apply_filters( 'wpml_element_trid', null, $attachment_id, 'post_attachment' );
    659775                                if ( $trid ) {
     776                                        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML public API filter.
    660777                                        $translations = apply_filters( 'wpml_get_element_translations', null, $trid, 'post_attachment' );
    661778                                        if ( is_array( $translations ) ) {
    … …  
    720837
    721838        /**
     839         * Map attachment ID => language code (lowercase, e.g. "fr"), one query per engine, cached for the
     840         * request. Used by the similar images to tell a duplicate (same language) from a language version.
     841         *
     842         * @return array<int, string> Empty when no multilingual engine assigns languages to media.
     843         */
     844        public static function get_language_map() {
     845                static $map = null;
     846                if ( null !== $map ) {
     847                        return $map;
     848                }
     849                global $wpdb;
     850                $map  = array();
     851                $rows = array();
     852                switch ( self::get_multilingual_engine() ) {
     853                        case 'polylang':
     854                                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-shot map, cached per request.
     855                                $rows = $wpdb->get_results(
     856                                        "SELECT tr.object_id AS id, t.slug AS lang
     857                                        FROM {$wpdb->term_relationships} tr
     858                                        INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
     859                                        INNER JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
     860                                        INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id
     861                                        WHERE tt.taxonomy = 'language' AND p.post_type = 'attachment'"
     862                                );
     863                                break;
     864                        case 'wpml':
     865                                $table = esc_sql( $wpdb->prefix . 'icl_translations' );
     866                                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- WPML core table, name derived from $wpdb->prefix; one-shot map cached per request.
     867                                $rows = $wpdb->get_results( "SELECT element_id AS id, language_code AS lang FROM {$table} WHERE element_type = 'post_attachment'" );
     868                                break;
     869                        case 'bogo':
     870                                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-shot map, cached per request.
     871                                $rows = $wpdb->get_results(
     872                                        $wpdb->prepare(
     873                                                "SELECT pm.post_id AS id, pm.meta_value AS lang FROM {$wpdb->postmeta} pm
     874                                                INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
     875                                                WHERE pm.meta_key = %s AND p.post_type = 'attachment'",
     876                                                '_locale'
     877                                        )
     878                                );
     879                                break;
     880                }
     881                if ( is_array( $rows ) ) {
     882                        foreach ( $rows as $row ) {
     883                                $lang = strtolower( substr( (string) $row->lang, 0, 2 ) );
     884                                if ( $lang !== '' ) {
     885                                        $map[ (int) $row->id ] = $lang;
     886                                }
     887                        }
     888                }
     889                return $map;
     890        }
     891
     892        /**
    722893         * Language label of an attachment, empty when the multilingual plugin does not assign
    723894         * a language to media (Weglot, GTranslate, TranslatePress, WPGlobus, MultilingualPress).
    … …  
    736907                                break;
    737908                        case 'wpml':
     909                                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WPML public API filter.
    738910                                $details = apply_filters( 'wpml_post_language_details', null, $attachment_id );
    739911                                if ( is_array( $details ) && ! empty( $details['language_code'] ) ) {
    … …  
    762934         */
    763935        public static function on_translations_linked( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) {
    764                 if ( 'post_translations' === $taxonomy ) {
     936                if ( 'post_translations' !== $taxonomy ) {
     937                        return;
     938                }
     939                // Les identités de l'audit ALT dépendent des liens entre médias seulement : la traduction
     940                // d'un article ne doit pas faire recalculer l'audit de toute la médiathèque.
     941                if ( 'attachment' === get_post_type( (int) $object_id ) ) {
     942                        self::invalidate_cache();
     943                } else {
    765944                        delete_transient( self::GROUPS_TRANSIENT );
    766945                }
    … …  
    769948        /**
    770949         * WPML: a translation link was created or updated.
    771          */
    772         public static function on_wpml_translation_update() {
    773                 delete_transient( self::GROUPS_TRANSIENT );
     950         *
     951         * @param array $args Translation details sent by WPML (element_type among others).
     952         */
     953        public static function on_wpml_translation_update( $args = array() ) {
     954                if ( is_array( $args ) && isset( $args['element_type'] ) && 'post_attachment' !== $args['element_type'] ) {
     955                        delete_transient( self::GROUPS_TRANSIENT );
     956                        return;
     957                }
     958                self::invalidate_cache();
    774959        }
    775960
    … …  
    8821067                foreach ( $ids as $id ) {
    8831068                        $result = self::hash_attachment( (int) $id, (bool) $force );
    884                         if ( 'hashed' === $result ) {
     1069                        // Same pass for the visual signature of the "Similar images" tab.
     1070                        $visual = class_exists( 'Filikod_Visual_Duplicates' )
     1071                                ? Filikod_Visual_Duplicates::sign_attachment( (int) $id, (bool) $force )
     1072                                : 'skipped';
     1073                        if ( 'hashed' === $result || 'signed' === $visual ) {
    8851074                                $processed++;
    8861075                        } else {
    … …  
    9861175                        foreach ( array_chunk( $hashes, 200 ) as $chunk ) {
    9871176                                $placeholders = implode( ',', array_fill( 0, count( $chunk ), '%s' ) );
    988                                 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Placeholders generated for the IN() list; values passed to prepare().
     1177                                // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- One %s generated per value of the IN() list; values passed to prepare().
    9891178                                $rows = $wpdb->get_results(
    9901179                                        $wpdb->prepare(
    … …  
    10011190                                        )
    10021191                                );
     1192                                // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
    10031193                                if ( is_array( $rows ) ) {
    10041194                                        foreach ( $rows as $row ) {
  • filikod/trunk/languages/filikod-fr_FR.po

    r3737001 r3763820  
    11msgid ""
    22msgstr ""
    3 "Project-Id-Version: Filikod 1.2.1\n"
     3"Project-Id-Version: Filikod 1.3.0\n"
    44"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/filikod\n"
    55"PO-Revision-Date: 2026-10-06 12:00+0000\n"
    … …  
    1313"X-Domain: filikod\n"
    1414
    15 #: admin/views/alt-audit.php:111
     15#: admin/views/alt-audit.php:109
     16msgid "With WPGlobus or qTranslate-XT, an image is also listed here when its ALT is empty in one of the site languages (see the Language column)."
     17msgstr "Avec WPGlobus ou qTranslate-XT, une image apparaît aussi ici quand son ALT est vide dans l’une des langues du site (voir la colonne Langue)."
     18
     19#: admin/views/alt-audit.php:122
     20#: includes/admin/class-filikod-admin.php:68
    1621#: includes/admin/class-filikod-admin.php:69
    17 #: includes/admin/class-filikod-admin.php:70
    1822msgid "ALT Audit"
    1923msgstr "Audit ALT"
    2024
    21 #: admin/views/alt-audit.php:117
     25#: admin/views/alt-audit.php:128
    2226msgid "Filter by issue type"
    2327msgstr "Trier par type de problème"
    2428
    25 #: admin/views/alt-audit.php:147
     29#: admin/views/alt-audit.php:158
    2630msgid "No images match this filter."
    2731msgstr "Aucune image ne correspond à ce filtre."
    2832
    29 #: admin/views/alt-audit.php:152
    30 #: admin/views/duplicates.php:167
    31 #: admin/views/duplicates.php:276
     33#: admin/views/alt-audit.php:163
     34#: admin/views/duplicates-similar.php:88
     35#: admin/views/duplicates.php:192
     36#: admin/views/duplicates.php:308
    3237msgid "Thumbnail"
    3338msgstr "Miniature"
    3439
    35 #: admin/views/alt-audit.php:153
    36 #: admin/views/duplicates.php:277
     40#: admin/views/alt-audit.php:164
     41#: admin/views/duplicates-similar.php:89
     42#: admin/views/duplicates.php:309
    3743msgid "File name"
    3844msgstr "Nom du fichier"
    3945
    40 #: admin/views/alt-audit.php:154
    41 #: admin/views/duplicates.php:169
    42 #: admin/views/duplicates.php:278
     46#: admin/views/alt-audit.php:166
     47msgid "Language"
     48msgstr "Langue"
     49
     50#: admin/views/alt-audit.php:168
     51#: admin/views/duplicates-similar.php:90
     52#: admin/views/duplicates.php:194
     53#: admin/views/duplicates.php:310
    4354msgid "Context"
    4455msgstr "Contexte"
    4556
    46 #: admin/views/alt-audit.php:155
    47 #: admin/views/duplicates.php:170
    48 #: admin/views/duplicates.php:279
     57#: admin/views/alt-audit.php:169
     58#: admin/views/duplicates-similar.php:91
     59#: admin/views/duplicates.php:195
     60#: admin/views/duplicates.php:311
    4961msgid "ALT"
    5062msgstr "ALT"
    5163
    52 #: admin/views/alt-audit.php:156
    53 #: admin/views/duplicates.php:280
     64#: admin/views/alt-audit.php:170
     65#: admin/views/duplicates-similar.php:92
     66#: admin/views/duplicates.php:312
    5467msgid "Issue"
    5568msgstr "Problème"
    5669
    57 #: admin/views/alt-audit.php:158
    58 #: admin/views/duplicates.php:171
    59 #: admin/views/duplicates.php:281
     70#: admin/views/alt-audit.php:172
     71#: admin/views/duplicates-similar.php:93
     72#: admin/views/duplicates.php:196
     73#: admin/views/duplicates.php:313
    6074msgid "Action"
    6175msgstr "Action"
    6276
    63 #: admin/views/alt-audit.php:211
     77#: admin/views/alt-audit.php:231
     78msgid "empty: %s"
     79msgstr "vide : %s"
     80
     81#: admin/views/alt-audit.php:240
     82msgid "Not translated"
     83msgstr "Non traduit"
     84
     85#: admin/views/alt-audit.php:252
    6486msgid "ALT text for this image"
    6587msgstr "Texte ALT pour cette image"
    6688
    67 #: admin/views/alt-audit.php:211
     89#: admin/views/alt-audit.php:252
    6890msgid "Decorative image (alt=\"\")"
    6991msgstr "Image décorative (alt=\"\")"
    7092
    71 #: admin/views/alt-audit.php:240
     93#: admin/views/alt-audit.php:281
    7294msgid "Mark this image as a regular content image (requires an ALT text)."
    7395msgstr "Marquer cette image comme image de contenu normale (un texte ALT est requis)."
    7496
    75 #: admin/views/alt-audit.php:240
     97#: admin/views/alt-audit.php:281
    7698msgid "Mark this image as decorative (alt=\"\", excluded from the ALT score)."
    7799msgstr "Marquer cette image comme décorative (alt=\"\", exclue du score ALT)."
    78100
    79 #: admin/views/alt-audit.php:252
     101#: admin/views/alt-audit.php:293
    80102msgid "Save"
    81103msgstr "Enregistrer"
    82104
    83 #: admin/views/alt-audit.php:268
    84 #: admin/views/duplicates.php:224
    85 #: admin/views/duplicates.php:375
     105#: admin/views/alt-audit.php:309
     106#: admin/views/duplicates-similar.php:185
     107#: admin/views/duplicates.php:249
     108#: admin/views/duplicates.php:407
    86109msgid "Pagination"
    87110msgstr "Pagination"
    88111
    89 #: admin/views/alt-audit.php:273
    90 #: admin/views/duplicates.php:229
     112#: admin/views/alt-audit.php:314
     113#: admin/views/duplicates.php:254
    91114msgid "Page %1$d of %2$d (%3$d items)"
    92115msgstr "Page %1$d sur %2$d (%3$d éléments)"
    93116
    94 #: admin/views/alt-audit.php:282
    95 #: admin/views/duplicates.php:238
    96 #: admin/views/duplicates.php:389
     117#: admin/views/alt-audit.php:323
     118#: admin/views/duplicates-similar.php:199
     119#: admin/views/duplicates.php:263
     120#: admin/views/duplicates.php:421
    97121msgid "&laquo; Previous"
    98122msgstr "&laquo; Précédent"
    99123
    100 #: admin/views/alt-audit.php:285
    101 #: admin/views/duplicates.php:241
    102 #: admin/views/duplicates.php:392
     124#: admin/views/alt-audit.php:326
     125#: admin/views/duplicates-similar.php:202
     126#: admin/views/duplicates.php:266
     127#: admin/views/duplicates.php:424
    103128msgid "Next &raquo;"
    104129msgstr "Suivant &raquo;"
    … …  
    106131#: admin/views/alt-audit.php:67
    107132#: admin/views/dashboard.php:103
    108 #: includes/admin/class-filikod-media-alt-status.php:302
     133#: includes/admin/class-filikod-media-alt-status.php:303
    109134msgid "Missing"
    110135msgstr "Manquant"
    … …  
    112137#: admin/views/alt-audit.php:68
    113138#: admin/views/dashboard.php:107
    114 #: includes/admin/class-filikod-media-alt-status.php:304
     139#: includes/admin/class-filikod-media-alt-status.php:305
    115140msgid "Generic"
    116141msgstr "Générique"
    … …  
    118143#: admin/views/alt-audit.php:69
    119144#: admin/views/dashboard.php:111
    120 #: includes/admin/class-filikod-media-alt-status.php:306
     145#: includes/admin/class-filikod-media-alt-status.php:307
    121146msgid "Too short"
    122147msgstr "Trop court"
    … …  
    124149#: admin/views/alt-audit.php:70
    125150#: admin/views/dashboard.php:115
    126 #: includes/admin/class-filikod-media-alt-status.php:308
     151#: includes/admin/class-filikod-media-alt-status.php:309
    127152msgid "Too long"
    128153msgstr "Trop long"
    … …  
    130155#: admin/views/alt-audit.php:71
    131156#: admin/views/dashboard.php:119
    132 #: includes/admin/class-filikod-media-alt-status.php:310
     157#: includes/admin/class-filikod-media-alt-status.php:311
    133158msgid "Duplicated"
    134159msgstr "Dupliqué"
    135160
    136161#: admin/views/alt-audit.php:72
    137 #: admin/views/alt-audit.php:157
    138 #: admin/views/alt-audit.php:242
     162#: admin/views/alt-audit.php:171
     163#: admin/views/alt-audit.php:283
    139164#: admin/views/dashboard.php:123
    140 #: includes/admin/class-filikod-media-alt-status.php:312
     165#: includes/admin/class-filikod-media-alt-status.php:313
    141166msgid "Decorative"
    142167msgstr "Décorative"
    … …  
    210235#: includes/dashboard/class-filikod-dashboard.php:161
    211236#: includes/dashboard/class-filikod-dashboard.php:168
    212 #: includes/dashboard/class-filikod-dashboard.php:200
    213 #: includes/dashboard/class-filikod-dashboard.php:207
    214 #: includes/duplicates/class-filikod-duplicates.php:905
    215 #: includes/duplicates/class-filikod-duplicates.php:918
     237#: includes/dashboard/class-filikod-dashboard.php:202
     238#: includes/dashboard/class-filikod-dashboard.php:209
     239#: includes/duplicates/class-filikod-duplicates.php:1091
     240#: includes/duplicates/class-filikod-duplicates.php:1104
     241#: includes/duplicates/class-filikod-visual-duplicates.php:742
    216242#: includes/settings/class-filikod-settings.php:253
    217243#: includes/settings/class-filikod-settings.php:275
    … …  
    282308msgstr "Score ALT"
    283309
    284 #: admin/views/duplicates.php:104
     310#: admin/views/duplicates-similar.php:108
     311msgid "Group %1$d: %2$d similar images"
     312msgstr "Groupe %1$d : %2$d images semblables"
     313
     314#: admin/views/duplicates-similar.php:114
     315msgid "Show again"
     316msgstr "Afficher de nouveau"
     317
     318#: admin/views/duplicates-similar.php:125
     319#: admin/views/duplicates.php:204
     320#: admin/views/duplicates.php:348
     321#: includes/duplicates/class-filikod-duplicates.php:248
     322msgid "ID %d"
     323msgstr "ID %d"
     324
     325#: admin/views/duplicates-similar.php:156
     326#: admin/views/duplicates.php:229
     327#: admin/views/duplicates.php:378
     328msgid "No ALT"
     329msgstr "Sans ALT"
     330
     331#: admin/views/duplicates-similar.php:164
     332#: admin/views/duplicates.php:233
     333#: admin/views/duplicates.php:386
     334msgid "Edit"
     335msgstr "Modifier"
     336
     337#: admin/views/duplicates-similar.php:166
     338#: admin/views/duplicates.php:235
     339#: admin/views/duplicates.php:388
     340msgid "Library"
     341msgstr "Médiathèque"
     342
     343#: admin/views/duplicates-similar.php:17
     344msgid "Same language"
     345msgstr "Même langue"
     346
     347#: admin/views/duplicates-similar.php:17
     348msgid "To check"
     349msgstr "À vérifier"
     350
     351#: admin/views/duplicates-similar.php:18
     352msgid "Other languages"
     353msgstr "Autres langues"
     354
     355#: admin/views/duplicates-similar.php:19
     356#: admin/views/duplicates-similar.php:114
     357msgid "Intentional"
     358msgstr "C’est voulu"
     359
     360#: admin/views/duplicates-similar.php:190
     361#: admin/views/duplicates.php:412
     362msgid "Page %1$d of %2$d (%3$d groups)"
     363msgstr "Page %1$d sur %2$d (%3$d groupes)"
     364
     365#: admin/views/duplicates-similar.php:27
     366msgid "Oldest upload"
     367msgstr "Premier envoi"
     368
     369#: admin/views/duplicates-similar.php:28
     370msgid "Similar"
     371msgstr "Semblable"
     372
     373#: admin/views/duplicates-similar.php:31
     374msgid "Uploaded first among these images."
     375msgstr "Envoyée en premier parmi ces images."
     376
     377#: admin/views/duplicates-similar.php:32
     378msgid "Shows the same picture as the first image but is a different file (re-export, compression, new name). Check the Context column before removing it from the Media Library."
     379msgstr "Montre la même photo que la première image, mais dans un autre fichier (réexport, compression, nouveau nom). Vérifiez la colonne Contexte avant de la retirer de la médiathèque."
     380
     381#: admin/views/duplicates-similar.php:39
     382msgid "Look-alike images uploaded in different languages, for example a map with French labels and the same map with English labels. They are usually intentional language versions, so they stay out of the main list. Link them as translations in your multilingual plugin, or mark them as intentional."
     383msgstr "Images semblables envoyées dans des langues différentes, par exemple une carte aux libellés en français et la même carte en anglais. Ce sont en général des versions de langue voulues : elles restent hors de la liste principale. Reliez-les comme traductions dans votre extension multilingue, ou marquez-les comme voulues."
     384
     385#: admin/views/duplicates-similar.php:41
     386msgid "Groups you marked as intentional. They stay out of the list until an image is added to or removed from the group."
     387msgstr "Groupes que vous avez marqués comme voulus. Ils restent hors de la liste tant qu’aucune image n’est ajoutée au groupe ou retirée du groupe."
     388
     389#: admin/views/duplicates-similar.php:43
     390msgid "The same photo saved as different files: re-exported, compressed or renamed before being uploaded again. Filikod compares what the images show, not the files, and only lists images of the same dimensions: a banner and its smaller version are never listed. Check where each one is used, keep that one, and remove the others from the Media Library once you are sure. Nothing is deleted automatically."
     391msgstr "La même photo enregistrée dans des fichiers différents : réexportée, compressée ou renommée avant d’être envoyée de nouveau. Filikod compare ce que montrent les images, pas les fichiers. Il ne liste que des images de mêmes dimensions : une bannière et sa version réduite ne sont jamais listées. Vérifiez où chacune est utilisée, gardez celle-là, puis retirez les autres de la médiathèque une fois sûr de vous. Rien n’est supprimé automatiquement."
     392
     393#: admin/views/duplicates-similar.php:49
     394msgid "Your server has no GD image library, so Filikod cannot compare images. Ask your host to enable the PHP GD extension."
     395msgstr "Votre serveur n’a pas la bibliothèque d’images GD : Filikod ne peut donc pas comparer les images. Demandez à votre hébergeur d’activer l’extension PHP GD."
     396
     397#: admin/views/duplicates-similar.php:55
     398msgid "%1$s of %2$s images have a visual signature; %3$s cannot be compared (SVG, icons or missing files)."
     399msgstr "%1$s images sur %2$s ont une signature visuelle ; %3$s ne peuvent pas être comparées (SVG, icônes ou fichiers manquants)."
     400
     401#: admin/views/duplicates-similar.php:81
     402msgid "No visual signature yet. Click \"Scan library\" above to compute them."
     403msgstr "Aucune signature visuelle pour l’instant. Cliquez sur « Analyser la médiathèque » ci-dessus pour les calculer."
     404
     405#: admin/views/duplicates-similar.php:83
     406msgid "No similar images in this list."
     407msgstr "Aucune image semblable dans cette liste."
     408
     409#: admin/views/duplicates.php:107
     410msgid "%1$s of %2$s images fingerprinted."
     411msgstr "%1$s images sur %2$s ont une empreinte."
     412
     413#: admin/views/duplicates.php:115
    285414msgid "%s images are still waiting. \"Scan library\" fingerprints them; \"Full rescan\" recomputes every fingerprint, useful after files were replaced outside WordPress."
    286415msgstr "%s images sont encore en attente. « Analyser la médiathèque » calcule leur empreinte ; « Analyse complète » recalcule toutes les empreintes, utile après un remplacement de fichiers hors de WordPress."
    287416
    288 #: admin/views/duplicates.php:108
     417#: admin/views/duplicates.php:119
    289418msgid "Every image is fingerprinted. \"Full rescan\" recomputes every fingerprint, useful after files were replaced outside WordPress."
    290419msgstr "Toutes les images ont une empreinte. « Analyse complète » recalcule toutes les empreintes, utile après un remplacement de fichiers hors de WordPress."
    291420
    292 #: admin/views/duplicates.php:114
     421#: admin/views/duplicates.php:125
    293422msgid "%s images have no file on disk (broken attachments) and cannot be fingerprinted."
    294423msgstr "%s images n’ont aucun fichier sur le disque (fichiers joints cassés) et ne peuvent pas recevoir d’empreinte."
    295424
    296 #: admin/views/duplicates.php:121
     425#: admin/views/duplicates.php:134
     426msgid "%s images have no visual signature yet (Similar images tab): \"Scan library\" computes them."
     427msgstr "%s images n’ont pas encore de signature visuelle (onglet Images semblables) : « Analyser la médiathèque » les calcule."
     428
     429#: admin/views/duplicates.php:141
    297430msgid "Scan library"
    298431msgstr "Analyser la médiathèque"
    299432
    300 #: admin/views/duplicates.php:124
     433#: admin/views/duplicates.php:144
    301434msgid "Full rescan"
    302435msgstr "Analyse complète"
    303436
    304 #: admin/views/duplicates.php:130
     437#: admin/views/duplicates.php:150
    305438#: admin/views/settings.php:143
    306439#: admin/views/settings.php:294
    … …  
    308441msgstr "Important :"
    309442
    310 #: admin/views/duplicates.php:131
     443#: admin/views/duplicates.php:151
    311444#: admin/views/settings.php:144
    312445#: admin/views/settings.php:295
    … …  
    314447msgstr "Veuillez conserver cette page ouverte jusqu’à ce que le processus soit terminé. Ne naviguez pas ailleurs ou fermez cette page."
    315448
    316 #: admin/views/duplicates.php:146
     449#: admin/views/duplicates.php:166
    317450msgid "Duplicated files"
    318451msgstr "Fichiers en double"
    319452
    320 #: admin/views/duplicates.php:149
     453#: admin/views/duplicates.php:169
     454msgid "Similar images"
     455msgstr "Images semblables"
     456
     457#: admin/views/duplicates.php:172
    321458msgid "Missing files"
    322459msgstr "Fichiers manquants"
    323460
    324 #: admin/views/duplicates.php:159
     461#: admin/views/duplicates.php:184
    325462msgid "Image attachments whose file could not be found on this server: the scan could not fingerprint them. They still appear in the Media Library and may be referenced in your content. If your media is offloaded to external storage (S3, CDN), these files are simply not fingerprinted and display normally. Otherwise they render as broken images: restore the file before deleting anything from the Media Library."
    326463msgstr "Images dont le fichier est introuvable sur ce serveur : l’analyse n’a pas pu calculer leur empreinte. Elles apparaissent toujours dans la médiathèque et peuvent être utilisées dans vos contenus. Si vos médias sont déportés sur un stockage externe (S3, CDN), ces fichiers n’ont simplement pas d’empreinte et s’affichent normalement. Sinon, ils s’affichent comme des images cassées : restaurez le fichier avant de supprimer quoi que ce soit dans la médiathèque."
    327464
    328 #: admin/views/duplicates.php:162
     465#: admin/views/duplicates.php:187
    329466msgid "No missing file among the scanned images."
    330467msgstr "Aucun fichier manquant parmi les images analysées."
    331468
    332 #: admin/views/duplicates.php:168
     469#: admin/views/duplicates.php:193
    333470msgid "Expected file"
    334471msgstr "Fichier attendu"
    335472
    336 #: admin/views/duplicates.php:179
    337 #: admin/views/duplicates.php:316
    338 #: includes/duplicates/class-filikod-duplicates.php:220
    339 msgid "ID %d"
    340 msgstr "ID %d"
    341 
    342 #: admin/views/duplicates.php:204
    343 #: admin/views/duplicates.php:346
    344 msgid "No ALT"
    345 msgstr "Sans ALT"
    346 
    347 #: admin/views/duplicates.php:208
    348 #: admin/views/duplicates.php:354
    349 msgid "Edit"
    350 msgstr "Modifier"
    351 
    352 #: admin/views/duplicates.php:210
    353 #: admin/views/duplicates.php:356
    354 msgid "Library"
    355 msgstr "Médiathèque"
    356 
    357 #: admin/views/duplicates.php:253
    358 msgid "Exact copies of the same file (same md5 fingerprint) uploaded more than once. Nothing is deleted or merged: check where each copy is used, keep that one, and remove the others from the Media Library once you are sure. Media linked as translations of each other (Polylang, WPML, Bogo) are intentional and never listed here."
    359 msgstr "Copies exactes d’un même fichier (même empreinte md5) envoyées plusieurs fois. Rien n’est supprimé ni fusionné : vérifiez où chaque copie est utilisée, gardez celle-là et retirez les autres de la médiathèque une fois sûr de vous. Les médias liés comme traductions les uns des autres (Polylang, WPML, Bogo) sont volontaires et ne sont jamais listés ici."
    360 
    361 #: admin/views/duplicates.php:257
     473#: admin/views/duplicates.php:279
     474msgid "Exact copies of the same file (same md5 fingerprint) uploaded more than once. Nothing is deleted or merged: check where each copy is used, keep that one, and remove the others from the Media Library once you are sure."
     475msgstr "Copies exactes d’un même fichier (même empreinte md5) envoyées plusieurs fois. Rien n’est supprimé ni fusionné : vérifiez où chaque copie est utilisée, gardez celle-là et retirez les autres de la médiathèque une fois sûr de vous."
     476
     477#: admin/views/duplicates.php:283
     478msgid "Media linked as translations of each other (Polylang, WPML, Bogo) are intentional and never listed here."
     479msgstr "Les médias liés comme traductions les uns des autres (Polylang, WPML, Bogo) sont volontaires et ne sont jamais listés ici."
     480
     481#: admin/views/duplicates.php:289
    362482msgid "No scan yet. Click \"Scan library\" above to fingerprint your images."
    363483msgstr "Aucune analyse pour l’instant. Cliquez sur « Analyser la médiathèque » ci-dessus pour calculer l’empreinte de vos images."
    364484
    365 #: admin/views/duplicates.php:259
     485#: admin/views/duplicates.php:291
    366486msgid "No duplicate images found among the fingerprinted files."
    367487msgstr "Aucune image en double parmi les fichiers analysés."
    368488
    369 #: admin/views/duplicates.php:265
     489#: admin/views/duplicates.php:297
    370490msgid "%1$s groups, %2$s redundant files, %3$s that could be freed by keeping a single copy per group."
    371491msgstr "%1$s groupes, %2$s fichiers redondants, %3$s libérables en ne gardant qu’une copie par groupe."
    372492
    373 #: admin/views/duplicates.php:301
     493#: admin/views/duplicates.php:333
    374494msgid "Group %1$d: %2$d identical copies, %3$s could be freed"
    375495msgstr "Groupe %1$d : %2$d copies identiques, %3$s libérables"
    376496
    377 #: admin/views/duplicates.php:37
     497#: admin/views/duplicates.php:48
    378498msgid "Reference"
    379499msgstr "Référence"
    380500
    381 #: admin/views/duplicates.php:38
    382 #: includes/duplicates/class-filikod-duplicates.php:240
     501#: admin/views/duplicates.php:49
     502#: includes/duplicates/class-filikod-duplicates.php:268
    383503msgid "Duplicate"
    384504msgstr "Doublon"
    385505
    386 #: admin/views/duplicates.php:380
    387 msgid "Page %1$d of %2$d (%3$d groups)"
    388 msgstr "Page %1$d sur %2$d (%3$d groupes)"
    389 
    390 #: admin/views/duplicates.php:41
     506#: admin/views/duplicates.php:52
    391507msgid "Oldest copy of this file: the one to keep, unless another copy is the one actually used in your content."
    392508msgstr "Copie la plus ancienne de ce fichier : celle à garder, sauf si une autre copie est celle réellement utilisée dans vos contenus."
    393509
    394 #: admin/views/duplicates.php:42
     510#: admin/views/duplicates.php:53
    395511msgid "Exact copy (same md5 fingerprint) of an image uploaded earlier. Check the Context column before removing it from the Media Library."
    396512msgstr "Copie exacte (même empreinte md5) d’une image envoyée plus tôt. Vérifiez la colonne Contexte avant de la retirer de la médiathèque."
    397513
    398 #: admin/views/duplicates.php:83
    399 #: admin/views/duplicates.php:144
    400 #: includes/admin/class-filikod-admin.php:81
     514#: admin/views/duplicates.php:94
     515#: admin/views/duplicates.php:164
     516#: includes/admin/class-filikod-admin.php:80
    401517msgid "Duplicates"
    402518msgstr "Doublons"
    403519
    404 #: admin/views/duplicates.php:88
     520#: admin/views/duplicates.php:99
    405521msgid "Filikod fingerprints the original file of every image (md5) to find exact copies. New uploads are fingerprinted automatically; run the scan once for the images already in your library."
    406522msgstr "Filikod calcule l’empreinte (md5) du fichier original de chaque image pour trouver les copies exactes. Les nouveaux envois reçoivent leur empreinte automatiquement ; lancez l’analyse une fois pour les images déjà présentes dans votre médiathèque."
    407 
    408 #: admin/views/duplicates.php:96
    409 msgid "%1$s of %2$s images fingerprinted."
    410 msgstr "%1$s images sur %2$s ont une empreinte."
    411523
    412524#: admin/views/settings.php:101
    … …  
    571683msgstr "Définissez la largeur maximale en pixels pour les images téléversées. Les images plus larges que cette largeur seront automatiquement redimensionnées tout en conservant leur proportion de taille. Recommandé : 2000px pour la plupart des sites."
    572684
    573 #: filikod.php:251
     685#: filikod.php:261
    574686msgid "Filikod requires WordPress 5.8 or higher."
    575687msgstr "Filikod nécessite une version de WordPress 5.8 ou supérieure."
    576688
    577 #: filikod.php:261
     689#: filikod.php:271
    578690msgid "Filikod requires PHP 7.4 or higher."
    579691msgstr "Filikod nécessite une version de PHP 7.4 ou supérieure."
    580692
    581 #: includes/admin/class-filikod-admin.php:142
     693#: includes/admin/class-filikod-admin.php:141
    582694msgid "Settings saved successfully!"
    583695msgstr "Les réglages ont bien été enregistrés !"
    584696
    585 #: includes/admin/class-filikod-admin.php:143
     697#: includes/admin/class-filikod-admin.php:142
    586698msgid "Error saving settings."
    587699msgstr "Erreur dans l’enregistrement des réglages."
    588700
    589 #: includes/admin/class-filikod-admin.php:144
     701#: includes/admin/class-filikod-admin.php:143
    590702msgid "Processing..."
    591703msgstr "Traitement en cours…"
    592704
    593 #: includes/admin/class-filikod-admin.php:145
     705#: includes/admin/class-filikod-admin.php:144
     706#: includes/duplicates/class-filikod-visual-duplicates.php:750
    594707msgid "An error occurred."
    595708msgstr "Une erreur s’est produite."
    596709
    597 #: includes/admin/class-filikod-admin.php:146
     710#: includes/admin/class-filikod-admin.php:145
    598711msgid "An error occurred during processing."
    599712msgstr "Une erreur s’est produite durant le traitement."
    600713
    601 #: includes/admin/class-filikod-admin.php:147
     714#: includes/admin/class-filikod-admin.php:146
    602715msgid "Starting processing..."
    603716msgstr "Traitement en cours…"
    604717
    605 #: includes/admin/class-filikod-admin.php:148
     718#: includes/admin/class-filikod-admin.php:147
    606719msgid "images resized"
    607720msgstr "images retaillées"
    608721
    609 #: includes/admin/class-filikod-admin.php:149
     722#: includes/admin/class-filikod-admin.php:148
    610723msgid "image resized"
    611724msgstr "image retaillée"
    612725
    613 #: includes/admin/class-filikod-admin.php:150
     726#: includes/admin/class-filikod-admin.php:149
    614727msgid "ignored"
    615728msgstr "ignoré"
    616729
    617 #: includes/admin/class-filikod-admin.php:151
     730#: includes/admin/class-filikod-admin.php:150
    618731msgid "saved"
    619732msgstr "enregistré"
    620733
    621 #: includes/admin/class-filikod-admin.php:153
     734#: includes/admin/class-filikod-admin.php:152
    622735msgid "Resize complete: %1$d images resized, %2$d ignored. Space saved: %3$s"
    623736msgstr "Traitement validé : %1$d images traitées, %2$d ignorées. Gain de place : %3$s"
    624737
    625 #: includes/admin/class-filikod-admin.php:154
     738#: includes/admin/class-filikod-admin.php:153
    626739msgid "Unable to retrieve image count"
    627740msgstr "Impossible de récupérer le total des images"
    628741
    629 #: includes/admin/class-filikod-admin.php:155
     742#: includes/admin/class-filikod-admin.php:154
    630743msgid "Error retrieving image count"
    631744msgstr "Erreur lors de la récupération du total des images"
    632745
    633 #: includes/admin/class-filikod-admin.php:156
     746#: includes/admin/class-filikod-admin.php:155
    634747msgid "image processed"
    635748msgstr "image traitée"
    636749
    637 #: includes/admin/class-filikod-admin.php:157
     750#: includes/admin/class-filikod-admin.php:156
    638751msgid "images processed"
    639752msgstr "images traitées"
    640753
     754#: includes/admin/class-filikod-admin.php:157
    641755#: includes/admin/class-filikod-admin.php:158
    642 #: includes/admin/class-filikod-admin.php:159
    643756msgid "skipped"
    644757msgstr "ignoré"
    645758
    646 #: includes/admin/class-filikod-admin.php:161
     759#: includes/admin/class-filikod-admin.php:160
    647760msgid "Processing complete: %1$d images processed, %2$d skipped."
    648761msgstr "Traitement validé : %1$d images traitées, %2$d ignorées."
    649762
    650 #: includes/admin/class-filikod-admin.php:162
     763#: includes/admin/class-filikod-admin.php:161
    651764msgid "Saved."
    652765msgstr "Enregistré."
    653766
    654 #: includes/admin/class-filikod-admin.php:163
     767#: includes/admin/class-filikod-admin.php:162
    655768msgid "Error saving ALT."
    656769msgstr "Erreur lors de l’enregistrement du ALT."
    657770
    658 #: includes/admin/class-filikod-admin.php:164
     771#: includes/admin/class-filikod-admin.php:163
    659772msgid "Marked as decorative (excluded from the ALT score)."
    660773msgstr "Marquée comme décorative (exclue du score ALT)."
    661774
    662 #: includes/admin/class-filikod-admin.php:165
     775#: includes/admin/class-filikod-admin.php:164
    663776msgid "Image restored as a regular image, please add an ALT text."
    664777msgstr "Image rétablie comme image normale, veuillez ajouter un texte ALT."
    665778
    666 #: includes/admin/class-filikod-admin.php:167
     779#: includes/admin/class-filikod-admin.php:166
    667780msgid "%1$d fingerprinted, %2$d skipped (%3$d%%)"
    668781msgstr "%1$d empreintes calculées, %2$d ignorées (%3$d %%)"
    669782
    670 #: includes/admin/class-filikod-admin.php:169
     783#: includes/admin/class-filikod-admin.php:168
    671784msgid "Scan complete: %1$d images fingerprinted, %2$d duplicate groups found (%3$d redundant files). Reloading..."
    672785msgstr "Analyse terminée : %1$d images avec empreinte, %2$d groupes de doublons trouvés (%3$d fichiers redondants). Rechargement…"
    673786
    674 #: includes/admin/class-filikod-admin.php:47
     787#: includes/admin/class-filikod-admin.php:46
    675788msgid "Filikod"
    676789msgstr "Filikod"
    677790
     791#: includes/admin/class-filikod-admin.php:58
    678792#: includes/admin/class-filikod-admin.php:59
    679 #: includes/admin/class-filikod-admin.php:60
    680793msgid "Dashboard"
    681794msgstr "Tableau de bord"
    682795
    683 #: includes/admin/class-filikod-admin.php:80
     796#: includes/admin/class-filikod-admin.php:79
    684797msgid "Duplicate Images"
    685798msgstr "Images en double"
    686799
     800#: includes/admin/class-filikod-admin.php:89
    687801#: includes/admin/class-filikod-admin.php:90
    688 #: includes/admin/class-filikod-admin.php:91
    689802msgid "Settings"
    690803msgstr "Réglages"
    691804
    692 #: includes/admin/class-filikod-media-alt-status.php:223
    693 #: includes/admin/class-filikod-media-alt-status.php:231
     805#: includes/admin/class-filikod-media-alt-status.php:224
     806#: includes/admin/class-filikod-media-alt-status.php:232
    694807msgid "Forbidden."
    695808msgstr "Interdit."
    696809
    697 #: includes/admin/class-filikod-media-alt-status.php:228
     810#: includes/admin/class-filikod-media-alt-status.php:229
    698811#: includes/dashboard/class-filikod-dashboard.php:165
    699 #: includes/dashboard/class-filikod-dashboard.php:204
     812#: includes/dashboard/class-filikod-dashboard.php:206
    700813msgid "Invalid attachment."
    701814msgstr "Fichier joint invalide."
    702815
    703 #: includes/admin/class-filikod-media-alt-status.php:269
    704 #: includes/admin/class-filikod-media-alt-status.php:396
     816#: includes/admin/class-filikod-media-alt-status.php:270
     817#: includes/admin/class-filikod-media-alt-status.php:397
    705818msgid "OK"
    706819msgstr "OK"
    707820
    708 #: includes/admin/class-filikod-media-alt-status.php:290
     821#: includes/admin/class-filikod-media-alt-status.php:291
    709822msgid "Alt: %s"
    710823msgstr "Alt : %s"
    711824
    712 #: includes/admin/class-filikod-media-alt-status.php:330
     825#: includes/admin/class-filikod-media-alt-status.php:331
    713826msgid "ALT text passes all checks: present, specific, well-sized and unique."
    714827msgstr "Le texte ALT passe tous les contrôles : présent, spécifique, de bonne longueur et unique."
    715828
    716 #: includes/admin/class-filikod-media-alt-status.php:332
     829#: includes/admin/class-filikod-media-alt-status.php:333
    717830msgid "No ALT text defined. Add a short description, or mark the image as Decorative if it is purely visual."
    718831msgstr "Aucun texte ALT défini. Ajoutez une courte description, ou marquez l’image comme Décorative si elle est purement visuelle."
    719832
    720 #: includes/admin/class-filikod-media-alt-status.php:334
     833#: includes/admin/class-filikod-media-alt-status.php:335
    721834msgid "ALT text is too generic (e.g. \"logo\", \"image1\"). Replace it with a description specific to this image."
    722835msgstr "Le texte ALT est trop générique (par ex. « logo », « image1 »). Remplacez-le par une description propre à cette image."
    723836
    724 #: includes/admin/class-filikod-media-alt-status.php:338
     837#: includes/admin/class-filikod-media-alt-status.php:339
    725838msgid "ALT text is under %d characters after normalization, too short to describe the image meaningfully."
    726839msgstr "Le texte ALT fait moins de %d caractères après normalisation, trop court pour décrire l’image utilement."
    727840
    728 #: includes/admin/class-filikod-media-alt-status.php:344
     841#: includes/admin/class-filikod-media-alt-status.php:345
    729842msgid "ALT text exceeds %d characters. Long ALTs are poorly read by screen readers and dilute SEO signals. Aim for a concise description."
    730843msgstr "Le texte ALT dépasse %d caractères. Les ALT longs sont mal lus par les lecteurs d’écran et diluent les signaux SEO. Visez une description concise."
    731844
    732 #: includes/admin/class-filikod-media-alt-status.php:348
     845#: includes/admin/class-filikod-media-alt-status.php:349
    733846msgid "The same ALT text is used on at least one other image. Differentiate each image with a unique description."
    734847msgstr "Le même texte ALT est utilisé sur au moins une autre image. Différenciez chaque image avec une description unique."
    735848
    736 #: includes/admin/class-filikod-media-alt-status.php:350
     849#: includes/admin/class-filikod-media-alt-status.php:351
    737850msgid "Image marked as decorative (alt=\"\"). Excluded from the ALT score and ignored by screen readers."
    738851msgstr "Image marquée comme décorative (alt=\"\"). Exclue du score ALT et ignorée par les lecteurs d’écran."
    739852
    740 #: includes/admin/class-filikod-media-alt-status.php:403
    741 #: includes/duplicates/class-filikod-duplicates.php:202
     853#: includes/admin/class-filikod-media-alt-status.php:404
     854#: includes/duplicates/class-filikod-duplicates.php:230
    742855msgid "Duplicate file"
    743856msgstr "Fichier en double"
    744857
    745 #: includes/admin/class-filikod-media-alt-status.php:406
     858#: includes/admin/class-filikod-media-alt-status.php:407
    746859msgid "Exact copy of an image already in the library (same file fingerprint). See Filikod > Duplicates."
    747860msgstr "Copie exacte d’une image déjà présente dans la médiathèque (même empreinte de fichier). Voir Filikod > Doublons."
    … …  
    762875
    763876#: includes/dashboard/class-filikod-dashboard.php:148
    764 #: includes/dashboard/class-filikod-dashboard.php:235
    765 #: includes/duplicates/class-filikod-duplicates.php:1229
     877#: includes/dashboard/class-filikod-dashboard.php:237
     878#: includes/duplicates/class-filikod-duplicates.php:1415
    766879#: includes/settings/class-filikod-settings.php:90
    767880msgid "You do not have sufficient permissions to access this page."
    768881msgstr "Vous n’avez pas les droits suffisants pour accéder à cette page."
    769882
    770 #: includes/dashboard/class-filikod-dashboard.php:211
     883#: includes/dashboard/class-filikod-dashboard.php:213
    771884msgid "Audit module unavailable."
    772885msgstr "Module d’audit indisponible."
    773886
    774 #: includes/dashboard/class-filikod-dashboard.php:215
     887#: includes/dashboard/class-filikod-dashboard.php:217
    775888msgid "Unable to update decorative state."
    776889msgstr "Impossible de mettre à jour l’état décoratif."
    777890
    778 #: includes/duplicates/class-filikod-duplicates.php:206
    779 msgid "This file exists %d time in your library:"
    780 msgid_plural "This file exists %d times in your library (identical content, different names):"
    781 msgstr[0] "Ce fichier existe %d fois dans votre médiathèque :"
    782 msgstr[1] "Ce fichier existe %d fois dans votre médiathèque :"
    783 
    784 #: includes/duplicates/class-filikod-duplicates.php:225
    785 msgid "oldest"
    786 msgstr "la plus ancienne"
    787 
    788 #: includes/duplicates/class-filikod-duplicates.php:228
    789 msgid "this file"
    790 msgstr "ce fichier"
    791 
    792 #: includes/duplicates/class-filikod-duplicates.php:236
    793 msgid "Review and clean up in Filikod > Duplicates"
    794 msgstr "Vérifier et nettoyer dans Filikod > Doublons"
    795 
    796 #: includes/duplicates/class-filikod-duplicates.php:279
    797 msgid "Duplicate file:"
    798 msgstr "Fichier en double :"
    799 
    800 #: includes/duplicates/class-filikod-duplicates.php:281
    801 msgid "%1$s is an exact copy of %2$s (ID %3$s) already in your library."
    802 msgstr "%1$s est une copie exacte de %2$s (ID %3$s) déjà présent dans votre médiathèque."
    803 
    804 #: includes/duplicates/class-filikod-duplicates.php:282
    805 msgid "Review in Filikod > Duplicates"
    806 msgstr "Vérifier dans Filikod > Doublons"
    807 
    808 #: includes/duplicates/class-filikod-duplicates.php:283
    809 msgid "Dismiss this notice."
    810 msgstr "Ignorer cet avertissement."
    811 
    812 #: includes/duplicates/class-filikod-duplicates.php:908
    813 #: includes/duplicates/class-filikod-duplicates.php:921
     891#: includes/duplicates/class-filikod-duplicates.php:1094
     892#: includes/duplicates/class-filikod-duplicates.php:1107
     893#: includes/duplicates/class-filikod-visual-duplicates.php:745
    814894#: includes/settings/class-filikod-settings.php:258
    815895#: includes/settings/class-filikod-settings.php:280
    … …  
    819899msgstr "Le contrôle de sécurité a échoué."
    820900
     901#: includes/duplicates/class-filikod-duplicates.php:234
     902msgid "This file exists %d time in your library:"
     903msgid_plural "This file exists %d times in your library (identical content, different names):"
     904msgstr[0] "Ce fichier existe %d fois dans votre médiathèque :"
     905msgstr[1] "Ce fichier existe %d fois dans votre médiathèque :"
     906
     907#: includes/duplicates/class-filikod-duplicates.php:253
     908msgid "oldest"
     909msgstr "la plus ancienne"
     910
     911#: includes/duplicates/class-filikod-duplicates.php:256
     912msgid "this file"
     913msgstr "ce fichier"
     914
     915#: includes/duplicates/class-filikod-duplicates.php:264
     916msgid "Review and clean up in Filikod > Duplicates"
     917msgstr "Vérifier et nettoyer dans Filikod > Doublons"
     918
     919#: includes/duplicates/class-filikod-duplicates.php:307
     920msgid "Duplicate file:"
     921msgstr "Fichier en double :"
     922
     923#: includes/duplicates/class-filikod-duplicates.php:309
     924msgid "%1$s is an exact copy of %2$s (ID %3$s) already in your library."
     925msgstr "%1$s est une copie exacte de %2$s (ID %3$s) déjà présent dans votre médiathèque."
     926
     927#: includes/duplicates/class-filikod-duplicates.php:310
     928msgid "Review in Filikod > Duplicates"
     929msgstr "Vérifier dans Filikod > Doublons"
     930
     931#: includes/duplicates/class-filikod-duplicates.php:311
     932msgid "Dismiss this notice."
     933msgstr "Ignorer cet avertissement."
     934
    821935#: includes/file-types/class-filikod-file-types.php:100
    822936msgid "RAR archives"
  • filikod/trunk/languages/filikod.pot

    r3737001 r3763820  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: Filikod 1.2.1\n"
     5"Project-Id-Version: Filikod 1.3.0\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/filikod\n"
    7 "POT-Creation-Date: 2026-10-06 12:03+0000\n"
     7"POT-Creation-Date: 2026-10-24 19:43+0000\n"
    88"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    99"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1616"X-Domain: filikod\n"
    1717
    18 #: admin/views/alt-audit.php:111
     18#: admin/views/alt-audit.php:109
     19msgid "With WPGlobus or qTranslate-XT, an image is also listed here when its ALT is empty in one of the site languages (see the Language column)."
     20msgstr ""
     21
     22#: admin/views/alt-audit.php:122
     23#: includes/admin/class-filikod-admin.php:68
    1924#: includes/admin/class-filikod-admin.php:69
    20 #: includes/admin/class-filikod-admin.php:70
    2125msgid "ALT Audit"
    2226msgstr ""
    2327
    24 #: admin/views/alt-audit.php:117
     28#: admin/views/alt-audit.php:128
    2529msgid "Filter by issue type"
    2630msgstr ""
    2731
    28 #: admin/views/alt-audit.php:147
     32#: admin/views/alt-audit.php:158
    2933msgid "No images match this filter."
    3034msgstr ""
    3135
    32 #: admin/views/alt-audit.php:152
    33 #: admin/views/duplicates.php:167
    34 #: admin/views/duplicates.php:276
     36#: admin/views/alt-audit.php:163
     37#: admin/views/duplicates-similar.php:88
     38#: admin/views/duplicates.php:192
     39#: admin/views/duplicates.php:308
    3540msgid "Thumbnail"
    3641msgstr ""
    3742
    38 #: admin/views/alt-audit.php:153
    39 #: admin/views/duplicates.php:277
     43#: admin/views/alt-audit.php:164
     44#: admin/views/duplicates-similar.php:89
     45#: admin/views/duplicates.php:309
    4046msgid "File name"
    4147msgstr ""
    4248
    43 #: admin/views/alt-audit.php:154
    44 #: admin/views/duplicates.php:169
    45 #: admin/views/duplicates.php:278
     49#: admin/views/alt-audit.php:166
     50msgid "Language"
     51msgstr ""
     52
     53#: admin/views/alt-audit.php:168
     54#: admin/views/duplicates-similar.php:90
     55#: admin/views/duplicates.php:194
     56#: admin/views/duplicates.php:310
    4657msgid "Context"
    4758msgstr ""
    4859
    49 #: admin/views/alt-audit.php:155
    50 #: admin/views/duplicates.php:170
    51 #: admin/views/duplicates.php:279
     60#: admin/views/alt-audit.php:169
     61#: admin/views/duplicates-similar.php:91
     62#: admin/views/duplicates.php:195
     63#: admin/views/duplicates.php:311
    5264msgid "ALT"
    5365msgstr ""
    5466
    55 #: admin/views/alt-audit.php:156
    56 #: admin/views/duplicates.php:280
     67#: admin/views/alt-audit.php:170
     68#: admin/views/duplicates-similar.php:92
     69#: admin/views/duplicates.php:312
    5770msgid "Issue"
    5871msgstr ""
    5972
    60 #: admin/views/alt-audit.php:158
    61 #: admin/views/duplicates.php:171
    62 #: admin/views/duplicates.php:281
     73#: admin/views/alt-audit.php:172
     74#: admin/views/duplicates-similar.php:93
     75#: admin/views/duplicates.php:196
     76#: admin/views/duplicates.php:313
    6377msgid "Action"
    6478msgstr ""
    6579
    66 #: admin/views/alt-audit.php:211
     80#: admin/views/alt-audit.php:231
     81msgid "empty: %s"
     82msgstr ""
     83
     84#: admin/views/alt-audit.php:240
     85msgid "Not translated"
     86msgstr ""
     87
     88#: admin/views/alt-audit.php:252
    6789msgid "ALT text for this image"
    6890msgstr ""
    6991
    70 #: admin/views/alt-audit.php:211
     92#: admin/views/alt-audit.php:252
    7193msgid "Decorative image (alt=\"\")"
    7294msgstr ""
    7395
    74 #: admin/views/alt-audit.php:240
     96#: admin/views/alt-audit.php:281
    7597msgid "Mark this image as a regular content image (requires an ALT text)."
    7698msgstr ""
    7799
    78 #: admin/views/alt-audit.php:240
     100#: admin/views/alt-audit.php:281
    79101msgid "Mark this image as decorative (alt=\"\", excluded from the ALT score)."
    80102msgstr ""
    81103
    82 #: admin/views/alt-audit.php:252
     104#: admin/views/alt-audit.php:293
    83105msgid "Save"
    84106msgstr ""
    85107
    86 #: admin/views/alt-audit.php:268
    87 #: admin/views/duplicates.php:224
    88 #: admin/views/duplicates.php:375
     108#: admin/views/alt-audit.php:309
     109#: admin/views/duplicates-similar.php:185
     110#: admin/views/duplicates.php:249
     111#: admin/views/duplicates.php:407
    89112msgid "Pagination"
    90113msgstr ""
    91114
    92 #: admin/views/alt-audit.php:273
    93 #: admin/views/duplicates.php:229
     115#: admin/views/alt-audit.php:314
     116#: admin/views/duplicates.php:254
    94117msgid "Page %1$d of %2$d (%3$d items)"
    95118msgstr ""
    96119
    97 #: admin/views/alt-audit.php:282
    98 #: admin/views/duplicates.php:238
    99 #: admin/views/duplicates.php:389
     120#: admin/views/alt-audit.php:323
     121#: admin/views/duplicates-similar.php:199
     122#: admin/views/duplicates.php:263
     123#: admin/views/duplicates.php:421
    100124msgid "&laquo; Previous"
    101125msgstr ""
    102126
    103 #: admin/views/alt-audit.php:285
    104 #: admin/views/duplicates.php:241
    105 #: admin/views/duplicates.php:392
     127#: admin/views/alt-audit.php:326
     128#: admin/views/duplicates-similar.php:202
     129#: admin/views/duplicates.php:266
     130#: admin/views/duplicates.php:424
    106131msgid "Next &raquo;"
    107132msgstr ""
    … …  
    109134#: admin/views/alt-audit.php:67
    110135#: admin/views/dashboard.php:103
    111 #: includes/admin/class-filikod-media-alt-status.php:302
     136#: includes/admin/class-filikod-media-alt-status.php:303
    112137msgid "Missing"
    113138msgstr ""
    … …  
    115140#: admin/views/alt-audit.php:68
    116141#: admin/views/dashboard.php:107
    117 #: includes/admin/class-filikod-media-alt-status.php:304
     142#: includes/admin/class-filikod-media-alt-status.php:305
    118143msgid "Generic"
    119144msgstr ""
    … …  
    121146#: admin/views/alt-audit.php:69
    122147#: admin/views/dashboard.php:111
    123 #: includes/admin/class-filikod-media-alt-status.php:306
     148#: includes/admin/class-filikod-media-alt-status.php:307
    124149msgid "Too short"
    125150msgstr ""
    … …  
    127152#: admin/views/alt-audit.php:70
    128153#: admin/views/dashboard.php:115
    129 #: includes/admin/class-filikod-media-alt-status.php:308
     154#: includes/admin/class-filikod-media-alt-status.php:309
    130155msgid "Too long"
    131156msgstr ""
    … …  
    133158#: admin/views/alt-audit.php:71
    134159#: admin/views/dashboard.php:119
    135 #: includes/admin/class-filikod-media-alt-status.php:310
     160#: includes/admin/class-filikod-media-alt-status.php:311
    136161msgid "Duplicated"
    137162msgstr ""
    138163
    139164#: admin/views/alt-audit.php:72
    140 #: admin/views/alt-audit.php:157
    141 #: admin/views/alt-audit.php:242
     165#: admin/views/alt-audit.php:171
     166#: admin/views/alt-audit.php:283
    142167#: admin/views/dashboard.php:123
    143 #: includes/admin/class-filikod-media-alt-status.php:312
     168#: includes/admin/class-filikod-media-alt-status.php:313
    144169msgid "Decorative"
    145170msgstr ""
    … …  
    213238#: includes/dashboard/class-filikod-dashboard.php:161
    214239#: includes/dashboard/class-filikod-dashboard.php:168
    215 #: includes/dashboard/class-filikod-dashboard.php:200
    216 #: includes/dashboard/class-filikod-dashboard.php:207
    217 #: includes/duplicates/class-filikod-duplicates.php:905
    218 #: includes/duplicates/class-filikod-duplicates.php:918
     240#: includes/dashboard/class-filikod-dashboard.php:202
     241#: includes/dashboard/class-filikod-dashboard.php:209
     242#: includes/duplicates/class-filikod-duplicates.php:1091
     243#: includes/duplicates/class-filikod-duplicates.php:1104
     244#: includes/duplicates/class-filikod-visual-duplicates.php:742
    219245#: includes/settings/class-filikod-settings.php:253
    220246#: includes/settings/class-filikod-settings.php:275
    … …  
    285311msgstr ""
    286312
    287 #: admin/views/duplicates.php:104
     313#: admin/views/duplicates-similar.php:108
     314msgid "Group %1$d: %2$d similar images"
     315msgstr ""
     316
     317#: admin/views/duplicates-similar.php:114
     318msgid "Show again"
     319msgstr ""
     320
     321#: admin/views/duplicates-similar.php:125
     322#: admin/views/duplicates.php:204
     323#: admin/views/duplicates.php:348
     324#: includes/duplicates/class-filikod-duplicates.php:248
     325msgid "ID %d"
     326msgstr ""
     327
     328#: admin/views/duplicates-similar.php:156
     329#: admin/views/duplicates.php:229
     330#: admin/views/duplicates.php:378
     331msgid "No ALT"
     332msgstr ""
     333
     334#: admin/views/duplicates-similar.php:164
     335#: admin/views/duplicates.php:233
     336#: admin/views/duplicates.php:386
     337msgid "Edit"
     338msgstr ""
     339
     340#: admin/views/duplicates-similar.php:166
     341#: admin/views/duplicates.php:235
     342#: admin/views/duplicates.php:388
     343msgid "Library"
     344msgstr ""
     345
     346#: admin/views/duplicates-similar.php:17
     347msgid "Same language"
     348msgstr ""
     349
     350#: admin/views/duplicates-similar.php:17
     351msgid "To check"
     352msgstr ""
     353
     354#: admin/views/duplicates-similar.php:18
     355msgid "Other languages"
     356msgstr ""
     357
     358#: admin/views/duplicates-similar.php:19
     359#: admin/views/duplicates-similar.php:114
     360msgid "Intentional"
     361msgstr ""
     362
     363#: admin/views/duplicates-similar.php:190
     364#: admin/views/duplicates.php:412
     365msgid "Page %1$d of %2$d (%3$d groups)"
     366msgstr ""
     367
     368#: admin/views/duplicates-similar.php:27
     369msgid "Oldest upload"
     370msgstr ""
     371
     372#: admin/views/duplicates-similar.php:28
     373msgid "Similar"
     374msgstr ""
     375
     376#: admin/views/duplicates-similar.php:31
     377msgid "Uploaded first among these images."
     378msgstr ""
     379
     380#: admin/views/duplicates-similar.php:32
     381msgid "Shows the same picture as the first image but is a different file (re-export, compression, new name). Check the Context column before removing it from the Media Library."
     382msgstr ""
     383
     384#: admin/views/duplicates-similar.php:39
     385msgid "Look-alike images uploaded in different languages, for example a map with French labels and the same map with English labels. They are usually intentional language versions, so they stay out of the main list. Link them as translations in your multilingual plugin, or mark them as intentional."
     386msgstr ""
     387
     388#: admin/views/duplicates-similar.php:41
     389msgid "Groups you marked as intentional. They stay out of the list until an image is added to or removed from the group."
     390msgstr ""
     391
     392#: admin/views/duplicates-similar.php:43
     393msgid "The same photo saved as different files: re-exported, compressed or renamed before being uploaded again. Filikod compares what the images show, not the files, and only lists images of the same dimensions: a banner and its smaller version are never listed. Check where each one is used, keep that one, and remove the others from the Media Library once you are sure. Nothing is deleted automatically."
     394msgstr ""
     395
     396#: admin/views/duplicates-similar.php:49
     397msgid "Your server has no GD image library, so Filikod cannot compare images. Ask your host to enable the PHP GD extension."
     398msgstr ""
     399
     400#: admin/views/duplicates-similar.php:55
     401msgid "%1$s of %2$s images have a visual signature; %3$s cannot be compared (SVG, icons or missing files)."
     402msgstr ""
     403
     404#: admin/views/duplicates-similar.php:81
     405msgid "No visual signature yet. Click \"Scan library\" above to compute them."
     406msgstr ""
     407
     408#: admin/views/duplicates-similar.php:83
     409msgid "No similar images in this list."
     410msgstr ""
     411
     412#: admin/views/duplicates.php:107
     413msgid "%1$s of %2$s images fingerprinted."
     414msgstr ""
     415
     416#: admin/views/duplicates.php:115
    288417msgid "%s images are still waiting. \"Scan library\" fingerprints them; \"Full rescan\" recomputes every fingerprint, useful after files were replaced outside WordPress."
    289418msgstr ""
    290419
    291 #: admin/views/duplicates.php:108
     420#: admin/views/duplicates.php:119
    292421msgid "Every image is fingerprinted. \"Full rescan\" recomputes every fingerprint, useful after files were replaced outside WordPress."
    293422msgstr ""
    294423
    295 #: admin/views/duplicates.php:114
     424#: admin/views/duplicates.php:125
    296425msgid "%s images have no file on disk (broken attachments) and cannot be fingerprinted."
    297426msgstr ""
    298427
    299 #: admin/views/duplicates.php:121
     428#: admin/views/duplicates.php:134
     429msgid "%s images have no visual signature yet (Similar images tab): \"Scan library\" computes them."
     430msgstr ""
     431
     432#: admin/views/duplicates.php:141
    300433msgid "Scan library"
    301434msgstr ""
    302435
    303 #: admin/views/duplicates.php:124
     436#: admin/views/duplicates.php:144
    304437msgid "Full rescan"
    305438msgstr ""
    306439
    307 #: admin/views/duplicates.php:130
     440#: admin/views/duplicates.php:150
    308441#: admin/views/settings.php:143
    309442#: admin/views/settings.php:294
    … …  
    311444msgstr ""
    312445
    313 #: admin/views/duplicates.php:131
     446#: admin/views/duplicates.php:151
    314447#: admin/views/settings.php:144
    315448#: admin/views/settings.php:295
    … …  
    317450msgstr ""
    318451
    319 #: admin/views/duplicates.php:146
     452#: admin/views/duplicates.php:166
    320453msgid "Duplicated files"
    321454msgstr ""
    322455
    323 #: admin/views/duplicates.php:149
     456#: admin/views/duplicates.php:169
     457msgid "Similar images"
     458msgstr ""
     459
     460#: admin/views/duplicates.php:172
    324461msgid "Missing files"
    325462msgstr ""
    326463
    327 #: admin/views/duplicates.php:159
     464#: admin/views/duplicates.php:184
    328465msgid "Image attachments whose file could not be found on this server: the scan could not fingerprint them. They still appear in the Media Library and may be referenced in your content. If your media is offloaded to external storage (S3, CDN), these files are simply not fingerprinted and display normally. Otherwise they render as broken images: restore the file before deleting anything from the Media Library."
    329466msgstr ""
    330467
    331 #: admin/views/duplicates.php:162
     468#: admin/views/duplicates.php:187
    332469msgid "No missing file among the scanned images."
    333470msgstr ""
    334471
    335 #: admin/views/duplicates.php:168
     472#: admin/views/duplicates.php:193
    336473msgid "Expected file"
    337474msgstr ""
    338475
    339 #: admin/views/duplicates.php:179
    340 #: admin/views/duplicates.php:316
    341 #: includes/duplicates/class-filikod-duplicates.php:220
    342 msgid "ID %d"
    343 msgstr ""
    344 
    345 #: admin/views/duplicates.php:204
    346 #: admin/views/duplicates.php:346
    347 msgid "No ALT"
    348 msgstr ""
    349 
    350 #: admin/views/duplicates.php:208
    351 #: admin/views/duplicates.php:354
    352 msgid "Edit"
    353 msgstr ""
    354 
    355 #: admin/views/duplicates.php:210
    356 #: admin/views/duplicates.php:356
    357 msgid "Library"
    358 msgstr ""
    359 
    360 #: admin/views/duplicates.php:253
    361 msgid "Exact copies of the same file (same md5 fingerprint) uploaded more than once. Nothing is deleted or merged: check where each copy is used, keep that one, and remove the others from the Media Library once you are sure. Media linked as translations of each other (Polylang, WPML, Bogo) are intentional and never listed here."
    362 msgstr ""
    363 
    364 #: admin/views/duplicates.php:257
     476#: admin/views/duplicates.php:279
     477msgid "Exact copies of the same file (same md5 fingerprint) uploaded more than once. Nothing is deleted or merged: check where each copy is used, keep that one, and remove the others from the Media Library once you are sure."
     478msgstr ""
     479
     480#: admin/views/duplicates.php:283
     481msgid "Media linked as translations of each other (Polylang, WPML, Bogo) are intentional and never listed here."
     482msgstr ""
     483
     484#: admin/views/duplicates.php:289
    365485msgid "No scan yet. Click \"Scan library\" above to fingerprint your images."
    366486msgstr ""
    367487
    368 #: admin/views/duplicates.php:259
     488#: admin/views/duplicates.php:291
    369489msgid "No duplicate images found among the fingerprinted files."
    370490msgstr ""
    371491
    372 #: admin/views/duplicates.php:265
     492#: admin/views/duplicates.php:297
    373493msgid "%1$s groups, %2$s redundant files, %3$s that could be freed by keeping a single copy per group."
    374494msgstr ""
    375495
    376 #: admin/views/duplicates.php:301
     496#: admin/views/duplicates.php:333
    377497msgid "Group %1$d: %2$d identical copies, %3$s could be freed"
    378498msgstr ""
    379499
    380 #: admin/views/duplicates.php:37
     500#: admin/views/duplicates.php:48
    381501msgid "Reference"
    382502msgstr ""
    383503
    384 #: admin/views/duplicates.php:38
    385 #: includes/duplicates/class-filikod-duplicates.php:240
     504#: admin/views/duplicates.php:49
     505#: includes/duplicates/class-filikod-duplicates.php:268
    386506msgid "Duplicate"
    387507msgstr ""
    388508
    389 #: admin/views/duplicates.php:380
    390 msgid "Page %1$d of %2$d (%3$d groups)"
    391 msgstr ""
    392 
    393 #: admin/views/duplicates.php:41
     509#: admin/views/duplicates.php:52
    394510msgid "Oldest copy of this file: the one to keep, unless another copy is the one actually used in your content."
    395511msgstr ""
    396512
    397 #: admin/views/duplicates.php:42
     513#: admin/views/duplicates.php:53
    398514msgid "Exact copy (same md5 fingerprint) of an image uploaded earlier. Check the Context column before removing it from the Media Library."
    399515msgstr ""
    400516
    401 #: admin/views/duplicates.php:83
    402 #: admin/views/duplicates.php:144
    403 #: includes/admin/class-filikod-admin.php:81
     517#: admin/views/duplicates.php:94
     518#: admin/views/duplicates.php:164
     519#: includes/admin/class-filikod-admin.php:80
    404520msgid "Duplicates"
    405521msgstr ""
    406522
    407 #: admin/views/duplicates.php:88
     523#: admin/views/duplicates.php:99
    408524msgid "Filikod fingerprints the original file of every image (md5) to find exact copies. New uploads are fingerprinted automatically; run the scan once for the images already in your library."
    409 msgstr ""
    410 
    411 #: admin/views/duplicates.php:96
    412 msgid "%1$s of %2$s images fingerprinted."
    413525msgstr ""
    414526
    … …  
    574686msgstr ""
    575687
    576 #: filikod.php:251
     688#: filikod.php:261
    577689msgid "Filikod requires WordPress 5.8 or higher."
    578690msgstr ""
    579691
    580 #: filikod.php:261
     692#: filikod.php:271
    581693msgid "Filikod requires PHP 7.4 or higher."
    582694msgstr ""
    583695
     696#: includes/admin/class-filikod-admin.php:141
     697msgid "Settings saved successfully!"
     698msgstr ""
     699
    584700#: includes/admin/class-filikod-admin.php:142
    585 msgid "Settings saved successfully!"
     701msgid "Error saving settings."
    586702msgstr ""
    587703
    588704#: includes/admin/class-filikod-admin.php:143
    589 msgid "Error saving settings."
     705msgid "Processing..."
    590706msgstr ""
    591707
    592708#: includes/admin/class-filikod-admin.php:144
    593 msgid "Processing..."
     709#: includes/duplicates/class-filikod-visual-duplicates.php:750
     710msgid "An error occurred."
    594711msgstr ""
    595712
    596713#: includes/admin/class-filikod-admin.php:145
    597 msgid "An error occurred."
     714msgid "An error occurred during processing."
    598715msgstr ""
    599716
    600717#: includes/admin/class-filikod-admin.php:146
    601 msgid "An error occurred during processing."
     718msgid "Starting processing..."
    602719msgstr ""
    603720
    604721#: includes/admin/class-filikod-admin.php:147
    605 msgid "Starting processing..."
     722msgid "images resized"
    606723msgstr ""
    607724
    608725#: includes/admin/class-filikod-admin.php:148
    609 msgid "images resized"
     726msgid "image resized"
    610727msgstr ""
    611728
    612729#: includes/admin/class-filikod-admin.php:149
    613 msgid "image resized"
     730msgid "ignored"
    614731msgstr ""
    615732
    616733#: includes/admin/class-filikod-admin.php:150
    617 msgid "ignored"
    618 msgstr ""
    619 
    620 #: includes/admin/class-filikod-admin.php:151
    621734msgid "saved"
    622735msgstr ""
    623736
     737#: includes/admin/class-filikod-admin.php:152
     738msgid "Resize complete: %1$d images resized, %2$d ignored. Space saved: %3$s"
     739msgstr ""
     740
    624741#: includes/admin/class-filikod-admin.php:153
    625 msgid "Resize complete: %1$d images resized, %2$d ignored. Space saved: %3$s"
     742msgid "Unable to retrieve image count"
    626743msgstr ""
    627744
    628745#: includes/admin/class-filikod-admin.php:154
    629 msgid "Unable to retrieve image count"
     746msgid "Error retrieving image count"
    630747msgstr ""
    631748
    632749#: includes/admin/class-filikod-admin.php:155
    633 msgid "Error retrieving image count"
     750msgid "image processed"
    634751msgstr ""
    635752
    636753#: includes/admin/class-filikod-admin.php:156
    637 msgid "image processed"
     754msgid "images processed"
    638755msgstr ""
    639756
    640757#: includes/admin/class-filikod-admin.php:157
    641 msgid "images processed"
    642 msgstr ""
    643 
    644758#: includes/admin/class-filikod-admin.php:158
    645 #: includes/admin/class-filikod-admin.php:159
    646759msgid "skipped"
    647760msgstr ""
    648761
     762#: includes/admin/class-filikod-admin.php:160
     763msgid "Processing complete: %1$d images processed, %2$d skipped."
     764msgstr ""
     765
    649766#: includes/admin/class-filikod-admin.php:161
    650 msgid "Processing complete: %1$d images processed, %2$d skipped."
     767msgid "Saved."
    651768msgstr ""
    652769
    653770#: includes/admin/class-filikod-admin.php:162
    654 msgid "Saved."
     771msgid "Error saving ALT."
    655772msgstr ""
    656773
    657774#: includes/admin/class-filikod-admin.php:163
    658 msgid "Error saving ALT."
     775msgid "Marked as decorative (excluded from the ALT score)."
    659776msgstr ""
    660777
    661778#: includes/admin/class-filikod-admin.php:164
    662 msgid "Marked as decorative (excluded from the ALT score)."
    663 msgstr ""
    664 
    665 #: includes/admin/class-filikod-admin.php:165
    666779msgid "Image restored as a regular image, please add an ALT text."
    667780msgstr ""
    668781
    669 #: includes/admin/class-filikod-admin.php:167
     782#: includes/admin/class-filikod-admin.php:166
    670783msgid "%1$d fingerprinted, %2$d skipped (%3$d%%)"
    671784msgstr ""
    672785
    673 #: includes/admin/class-filikod-admin.php:169
     786#: includes/admin/class-filikod-admin.php:168
    674787msgid "Scan complete: %1$d images fingerprinted, %2$d duplicate groups found (%3$d redundant files). Reloading..."
    675788msgstr ""
    676789
    677 #: includes/admin/class-filikod-admin.php:47
     790#: includes/admin/class-filikod-admin.php:46
    678791msgid "Filikod"
    679792msgstr ""
    680793
     794#: includes/admin/class-filikod-admin.php:58
    681795#: includes/admin/class-filikod-admin.php:59
    682 #: includes/admin/class-filikod-admin.php:60
    683796msgid "Dashboard"
    684797msgstr ""
    685798
    686 #: includes/admin/class-filikod-admin.php:80
     799#: includes/admin/class-filikod-admin.php:79
    687800msgid "Duplicate Images"
    688801msgstr ""
    689802
     803#: includes/admin/class-filikod-admin.php:89
    690804#: includes/admin/class-filikod-admin.php:90
    691 #: includes/admin/class-filikod-admin.php:91
    692805msgid "Settings"
    693806msgstr ""
    694807
    695 #: includes/admin/class-filikod-media-alt-status.php:223
    696 #: includes/admin/class-filikod-media-alt-status.php:231
     808#: includes/admin/class-filikod-media-alt-status.php:224
     809#: includes/admin/class-filikod-media-alt-status.php:232
    697810msgid "Forbidden."
    698811msgstr ""
    699812
    700 #: includes/admin/class-filikod-media-alt-status.php:228
     813#: includes/admin/class-filikod-media-alt-status.php:229
    701814#: includes/dashboard/class-filikod-dashboard.php:165
    702 #: includes/dashboard/class-filikod-dashboard.php:204
     815#: includes/dashboard/class-filikod-dashboard.php:206
    703816msgid "Invalid attachment."
    704817msgstr ""
    705818
    706 #: includes/admin/class-filikod-media-alt-status.php:269
    707 #: includes/admin/class-filikod-media-alt-status.php:396
     819#: includes/admin/class-filikod-media-alt-status.php:270
     820#: includes/admin/class-filikod-media-alt-status.php:397
    708821msgid "OK"
    709822msgstr ""
    710823
    711 #: includes/admin/class-filikod-media-alt-status.php:290
     824#: includes/admin/class-filikod-media-alt-status.php:291
    712825msgid "Alt: %s"
    713826msgstr ""
    714827
    715 #: includes/admin/class-filikod-media-alt-status.php:330
     828#: includes/admin/class-filikod-media-alt-status.php:331
    716829msgid "ALT text passes all checks: present, specific, well-sized and unique."
    717830msgstr ""
    718831
    719 #: includes/admin/class-filikod-media-alt-status.php:332
     832#: includes/admin/class-filikod-media-alt-status.php:333
    720833msgid "No ALT text defined. Add a short description, or mark the image as Decorative if it is purely visual."
    721834msgstr ""
    722835
    723 #: includes/admin/class-filikod-media-alt-status.php:334
     836#: includes/admin/class-filikod-media-alt-status.php:335
    724837msgid "ALT text is too generic (e.g. \"logo\", \"image1\"). Replace it with a description specific to this image."
    725838msgstr ""
    726839
    727 #: includes/admin/class-filikod-media-alt-status.php:338
     840#: includes/admin/class-filikod-media-alt-status.php:339
    728841msgid "ALT text is under %d characters after normalization, too short to describe the image meaningfully."
    729842msgstr ""
    730843
    731 #: includes/admin/class-filikod-media-alt-status.php:344
     844#: includes/admin/class-filikod-media-alt-status.php:345
    732845msgid "ALT text exceeds %d characters. Long ALTs are poorly read by screen readers and dilute SEO signals. Aim for a concise description."
    733846msgstr ""
    734847
    735 #: includes/admin/class-filikod-media-alt-status.php:348
     848#: includes/admin/class-filikod-media-alt-status.php:349
    736849msgid "The same ALT text is used on at least one other image. Differentiate each image with a unique description."
    737850msgstr ""
    738851
    739 #: includes/admin/class-filikod-media-alt-status.php:350
     852#: includes/admin/class-filikod-media-alt-status.php:351
    740853msgid "Image marked as decorative (alt=\"\"). Excluded from the ALT score and ignored by screen readers."
    741854msgstr ""
    742855
    743 #: includes/admin/class-filikod-media-alt-status.php:403
    744 #: includes/duplicates/class-filikod-duplicates.php:202
     856#: includes/admin/class-filikod-media-alt-status.php:404
     857#: includes/duplicates/class-filikod-duplicates.php:230
    745858msgid "Duplicate file"
    746859msgstr ""
    747860
    748 #: includes/admin/class-filikod-media-alt-status.php:406
     861#: includes/admin/class-filikod-media-alt-status.php:407
    749862msgid "Exact copy of an image already in the library (same file fingerprint). See Filikod > Duplicates."
    750863msgstr ""
    … …  
    765878
    766879#: includes/dashboard/class-filikod-dashboard.php:148
    767 #: includes/dashboard/class-filikod-dashboard.php:235
    768 #: includes/duplicates/class-filikod-duplicates.php:1229
     880#: includes/dashboard/class-filikod-dashboard.php:237
     881#: includes/duplicates/class-filikod-duplicates.php:1415
    769882#: includes/settings/class-filikod-settings.php:90
    770883msgid "You do not have sufficient permissions to access this page."
    771884msgstr ""
    772885
    773 #: includes/dashboard/class-filikod-dashboard.php:211
     886#: includes/dashboard/class-filikod-dashboard.php:213
    774887msgid "Audit module unavailable."
    775888msgstr ""
    776889
    777 #: includes/dashboard/class-filikod-dashboard.php:215
     890#: includes/dashboard/class-filikod-dashboard.php:217
    778891msgid "Unable to update decorative state."
    779892msgstr ""
    780893
    781 #: includes/duplicates/class-filikod-duplicates.php:206
    782 msgid "This file exists %d time in your library:"
    783 msgid_plural "This file exists %d times in your library (identical content, different names):"
    784 msgstr[0] ""
    785 msgstr[1] ""
    786 
    787 #: includes/duplicates/class-filikod-duplicates.php:225
    788 msgid "oldest"
    789 msgstr ""
    790 
    791 #: includes/duplicates/class-filikod-duplicates.php:228
    792 msgid "this file"
    793 msgstr ""
    794 
    795 #: includes/duplicates/class-filikod-duplicates.php:236
    796 msgid "Review and clean up in Filikod > Duplicates"
    797 msgstr ""
    798 
    799 #: includes/duplicates/class-filikod-duplicates.php:279
    800 msgid "Duplicate file:"
    801 msgstr ""
    802 
    803 #: includes/duplicates/class-filikod-duplicates.php:281
    804 msgid "%1$s is an exact copy of %2$s (ID %3$s) already in your library."
    805 msgstr ""
    806 
    807 #: includes/duplicates/class-filikod-duplicates.php:282
    808 msgid "Review in Filikod > Duplicates"
    809 msgstr ""
    810 
    811 #: includes/duplicates/class-filikod-duplicates.php:283
    812 msgid "Dismiss this notice."
    813 msgstr ""
    814 
    815 #: includes/duplicates/class-filikod-duplicates.php:908
    816 #: includes/duplicates/class-filikod-duplicates.php:921
     894#: includes/duplicates/class-filikod-duplicates.php:1094
     895#: includes/duplicates/class-filikod-duplicates.php:1107
     896#: includes/duplicates/class-filikod-visual-duplicates.php:745
    817897#: includes/settings/class-filikod-settings.php:258
    818898#: includes/settings/class-filikod-settings.php:280
    … …  
    822902msgstr ""
    823903
     904#: includes/duplicates/class-filikod-duplicates.php:234
     905msgid "This file exists %d time in your library:"
     906msgid_plural "This file exists %d times in your library (identical content, different names):"
     907msgstr[0] ""
     908msgstr[1] ""
     909
     910#: includes/duplicates/class-filikod-duplicates.php:253
     911msgid "oldest"
     912msgstr ""
     913
     914#: includes/duplicates/class-filikod-duplicates.php:256
     915msgid "this file"
     916msgstr ""
     917
     918#: includes/duplicates/class-filikod-duplicates.php:264
     919msgid "Review and clean up in Filikod > Duplicates"
     920msgstr ""
     921
     922#: includes/duplicates/class-filikod-duplicates.php:307
     923msgid "Duplicate file:"
     924msgstr ""
     925
     926#: includes/duplicates/class-filikod-duplicates.php:309
     927msgid "%1$s is an exact copy of %2$s (ID %3$s) already in your library."
     928msgstr ""
     929
     930#: includes/duplicates/class-filikod-duplicates.php:310
     931msgid "Review in Filikod > Duplicates"
     932msgstr ""
     933
     934#: includes/duplicates/class-filikod-duplicates.php:311
     935msgid "Dismiss this notice."
     936msgstr ""
     937
    824938#: includes/file-types/class-filikod-file-types.php:100
    825939msgid "RAR archives"
  • filikod/trunk/readme.txt

    r3737019 r3763820  
    66Tested up to: 7.1
    77Requires PHP: 7.4
    8 Stable tag: 1.2.1
     8Stable tag: 1.3.0
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    3838
    3939* **SEO professionals** who want ALT text treated as a measurable image SEO signal, not an afterthought
    40 * **Accessibility-focused teams** who need a structured audit to meet WCAG 2.1 success criterion 1.1.1 (non-text content)
     40* **Accessibility-focused teams** who need a structured audit to meet WCAG 2.2 success criterion 1.1.1 (non-text content)
    4141* **Content managers** with large media libraries who need to fix issues fast, in bulk
    4242* **Agencies** managing multiple WordPress sites who need a repeatable quality process
    … …  
    6060---
    6161
     62= Accessibility Standards: WCAG 2.2, ADA, Section 508, EAA, RGAA =
     63
     64Every major accessibility standard starts with the same rule for images: WCAG success criterion 1.1.1 (Non-text Content, Level A). An image that carries information needs a text alternative. A purely decorative image needs an empty one (`alt=""`) so screen readers skip it.
     65
     66That rule sits behind the laws and frameworks your clients ask about:
     67
     68* **WCAG 2.2** (and 2.1, 2.0): success criterion 1.1.1, unchanged since 2008
     69* **ADA** (United States): the Title II rule for state and local governments references WCAG 2.1 AA, and WCAG is the usual benchmark in ADA website cases
     70* **Section 508** (US federal websites): WCAG 2.0 AA
     71* **European Accessibility Act (EAA)**: applies since June 28, 2025, through EN 301 549, which references WCAG 2.1 AA
     72* **RGAA** (France): theme 1 "Images", criteria 1.1 to 1.9
     73
     74Filikod checks the image part of these standards across your whole media library: images with no text alternative, generic or filename-like ALT text that a screen reader reads out as noise ("IMG_4832", "photo"), and images you mark as **decorative** so they keep an intentional empty `alt=""`. WooCommerce product images live in the media library too, so they are audited like any other image.
     75
     76Filikod covers this one criterion, not a whole standard. It does not replace a full accessibility audit, and no plugin can make a site compliant on its own. What it gives you is a measured starting point for one of the most frequent failures on the web.
     77
     78---
     79
    6280= Bulk ALT Management =
    6381
    … …  
    96114
    97115Multilingual sites are handled. Media linked as translations of each other by **Polylang**, **WPML** (Media Translation) or **Bogo**, one attachment per language for the same file, are intentional and never reported as duplicates; unlinked copies are listed with their language so you can link them or keep one. **Weglot**, **GTranslate** and **TranslatePress** translate on the fly and never duplicate media, **WPGlobus** keeps every language in the same post, and **MultilingualPress** uses one media library per site, each scanned on its own: nothing to configure with them.
     116
     117**Similar images.** The same photo is often uploaded twice as two different files: re-exported, compressed or renamed. Filikod gives every image a small visual signature, computed once on your server from a thumbnail WordPress already made (no AI, no external service), and lists look-alike images of the same dimensions in **Filikod > Duplicates > Similar images**. A banner and its smaller version are never listed. On multilingual sites, look-alikes uploaded in two different languages, such as a map with French labels and the same map in English, are language versions: they are listed apart and hidden by default. Any group can be marked as intentional.
    98118
    99119Once the library is scanned, every copy carries a red **Duplicate file** badge in the Media Library grid and a **Duplicate** warning in the attachment details listing every copy of the group, with a link to the Duplicates page. The **Duplicate Upload Alert** (on by default, switch in Settings > Optimizations) adds a notice in the uploader the moment you upload a copy of an existing image. Nothing is blocked.
    … …  
    156176* Compatible with Elementor, Divi, Gutenberg, WPBakery and most major page builders
    157177* Single site and multisite supported
    158 * Multilingual: Polylang, WPML, Bogo (translation links respected by the duplicate detection), Weglot, GTranslate, TranslatePress, WPGlobus, MultilingualPress
     178* Multilingual: Polylang, WPML, Bogo (translation links respected by the duplicate detection, language column in the ALT Audit), Weglot, GTranslate, TranslatePress, MultilingualPress
     179* WPGlobus and qTranslate-XT: ALT text written in several languages is audited language by language, and an ALT left empty in one language is reported
    159180* No external services required, runs entirely inside WordPress
    160181
    … …  
    184205A checker first. Filikod audits the alt text that already exists in your media library and scores it. It never sends your images to an external AI service and never invents descriptions behind your back. An optional, local filename-based setting (off by default) can fill a starting ALT for images that have none, and you then review it in the ALT Audit like any other ALT text.
    185206
     207= Does Filikod make my site WCAG, ADA or EAA compliant? =
     208
     209No plugin can do that on its own. Filikod covers the image part of these standards (WCAG success criterion 1.1.1, RGAA theme 1): it finds images without a proper text alternative, lets you fix them in bulk and keeps decorative images with an intentional empty `alt=""`. Color contrast, keyboard navigation, forms and the rest of WCAG need their own checks.
     210
    186211= Does Filikod generate ALT text automatically? =
    187212
    … …  
    247272
    248273== Changelog ==
     274
     275= 1.3.0 =
     276* **New – Similar images (Filikod > Duplicates)**: the same photo saved as different files (re-exported, compressed or renamed before being uploaded again) is now listed in its own tab. No AI and no external service: a small visual signature is computed once on your server, from a thumbnail WordPress already made. Only images of the same dimensions with the same colours are compared, so a banner and its smaller version are never listed, and plain graphics such as logos on a solid background are only listed when they match exactly. On multilingual sites, look-alikes uploaded in different languages are listed apart and hidden by default. Any group can be marked as intentional. Click "Scan library" once to compute the signatures of the images already in your library.
     277* **New – Language column in the ALT Audit** on multilingual sites (Polylang, WPML, Bogo), to spot an ALT left in the wrong language. The column and every language label stay hidden on single-language sites.
     278* **New – WPGlobus and qTranslate-XT**: ALT text written in several languages in one field is audited language by language (generic terms of each language, length per language), and an ALT left empty in one of the site languages is reported as missing.
     279* **Fix – automatic ALT on WPML sites**: the language copies that WPML Media Translation creates on upload now get their ALT too. Before, only the uploaded image did and its copies stayed empty.
     280* **Fix – no more false "Duplicated ALT" on translated images**: the language versions of one image are recognised as one image even when only one of them was fingerprinted, so filling their ALT no longer flags the whole set.
     281* **Fix – "Remove Special Characters" keeps the language tags** of WPGlobus and qTranslate-XT and cleans the text of each language instead.
     282* **Fix – "Duplicate file" badge on every language version** of a duplicated image, whatever the language the Media Library is browsed in.
     283* **Fix – menu icon**: the Filikod icon is displayed as soon as the page loads and follows your admin colour scheme (normal, hover, current page), like the WordPress icons.
     284* **Readme**: new section on WCAG 2.2, ADA, Section 508, EAA and RGAA, and what Filikod covers.
    249285
    250286= 1.2.1 =
    … …  
    385421== Upgrade Notice ==
    386422
     423= 1.3.0 =
     424New Similar images tab (no AI), language column and WPGlobus / qTranslate-XT support in the ALT Audit, automatic ALT on WPML copies, fewer false duplicated ALTs. Run "Scan library" once in Filikod > Duplicates after updating.
     425
    387426= 1.2.1 =
    388427Security and data fixes: hardened SVG sanitizer, decorative images never overwritten, ALT clean-up keeps punctuation, EXIF-safe resizing, faster audit, keyboard-accessible settings, French translation. Update recommended.
  • filikod/trunk/uninstall.php

    r3737001 r3763820  
    2626    // Alerte anti-doublon à l'upload (module Doublons)
    2727    'filikod_duplicate_upload_alert',
     28    // Groupes d'images semblables marqués « C'est voulu » (module Doublons)
     29    'filikod_visual_ignored',
    2830
    2931    // Options de nettoyage des caractères spéciaux
    … …  
    4143// La désinstallation d'un grand réseau peut être longue : on lève la limite de temps quand c'est permis.
    4244if (function_exists('set_time_limit')) {
    43     @set_time_limit(0); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- set_time_limit may be disabled by the host.
     45    @set_time_limit(0); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Squiz.PHP.DiscouragedFunctions.Discouraged -- Uninstalling a large network can take long; the host may have disabled set_time_limit.
    4446}
    4547
    … …  
    6163            delete_option($filikod_option);
    6264        }
    63         // Empreintes de fichiers du module Doublons (données de cache propres au plugin).
     65        // Empreintes de fichiers et signatures visuelles du module Doublons (données de cache propres au plugin).
    6466        delete_post_meta_by_key('_filikod_file_hash');
     67        delete_post_meta_by_key('_filikod_visual_hash');
    6568
    6669        // Supprimer aussi les transients du sous-site ({$wpdb->options} pointe sur la table options du blog actif après switch_to_blog).
    … …  
    8285
    8386// Métadonnées d'images : le marqueur « décorative » (_filikod_decorative) est une décision éditoriale
    84 // et reste en base ; l'empreinte md5 du module Doublons est un cache propre au plugin, sans valeur éditoriale.
     87// et reste en base ; l'empreinte md5 et la signature visuelle du module Doublons sont des caches propres
     88// au plugin, sans valeur éditoriale.
    8589delete_post_meta_by_key('_filikod_file_hash');
     90delete_post_meta_by_key('_filikod_visual_hash');
    8691
    8792// Supprimer les tâches cron
Note: See TracChangeset for help on using the changeset viewer.