Plugin Directory

Changeset 3389106


Ignore:
Timestamp:
11/03/2025 07:11:13 PM (2 months ago)
Author:
ivijanstefan
Message:

2.3.8

  • Fixed critical issue with Polylang integration where non-Cyrillic languages (e.g., DE, EN) were incorrectly transliterated to Cyrillic on first visit.
  • Added robust detection logic with safe fallback to Latin for unresolved or unknown locales.
  • Improved is_cyrillic_locale() to include lazy caching and reliable Polylang language detection through pll_language_defined and wp hooks.
  • Added safety-net inside transliteration process to prevent accidental lat_to_cyr mode when language is not confirmed as Cyrillic.
  • Restricted Polylang “first-visit” language list strictly to Cyrillic-script languages.
  • Ensured consistent behavior in phantom mode and early-page rendering phases.
Location:
serbian-transliteration
Files:
147 added
6 edited

Legend:

Unmodified
Added
Removed
  • serbian-transliteration/trunk/CHANGELOG.txt

    r3333992 r3389106  
     1= 2.3.8 =
     2* Fixed critical issue with Polylang integration where non-Cyrillic languages (e.g., DE, EN) were incorrectly transliterated to Cyrillic on first visit.
     3* Added robust detection logic with safe fallback to Latin for unresolved or unknown locales.
     4* Improved `is_cyrillic_locale()` to include lazy caching and reliable Polylang language detection through `pll_language_defined` and `wp` hooks.
     5* Added safety-net inside transliteration process to prevent accidental `lat_to_cyr` mode when language is not confirmed as Cyrillic.
     6* Restricted Polylang “first-visit” language list strictly to Cyrillic-script languages.
     7* Ensured consistent behavior in phantom mode and early-page rendering phases.
     8
    19= 2.3.7 =
    210* Added Croatian Cyrillic alphabet support
  • serbian-transliteration/trunk/classes/controller.php

    r3333304 r3389106  
    8080                return 'cyr_to_lat';
    8181            }
    82 
     82           
     83            // Force Latin for non-Cyrillic locales to avoid accidental Cyrillic on e.g. DE/EN.
     84            if (!Transliteration_Utilities::is_cyrillic_locale()) {
     85                Transliteration_Utilities::setcookie('lat');
     86                return 'cyr_to_lat';
     87            }
     88
     89            // CONTINUE:
    8390            // Settings mode
    8491            $mode = get_rstr_option('transliteration-mode', 'cyr_to_lat');
     
    197204            $mode = $this->mode();
    198205        }
     206       
     207        if ($mode === 'lat_to_cyr' && !Transliteration_Utilities::is_cyrillic_locale()) {
     208            return $content;
     209        }
    199210
    200211        if (!$mode) {
  • serbian-transliteration/trunk/classes/plugins/polylang.php

    r3269990 r3389106  
    3636        if (!$languages) {
    3737            $languages = apply_filters('rstr_plugin_polylang_languages', [
    38                 'uk', 'kk', 'el', 'ar', 'hy', 'sr', 'mk',
    39                 'bg', 'ru', 'bel', 'sah', 'bs', 'kir',
    40                 'mn', 'ba', 'uz', 'ka', 'tg', 'cnr',
     38                'sr','mk','bg','ru','uk','kk','tg','ky','mn','ba','be','sah','tt','uz_Cyrl','cnr'
    4139            ], $url, $slug);
    4240        }
  • serbian-transliteration/trunk/classes/utilities.php

    r3333992 r3389106  
    88{
    99    use Transliteration__Cache;
     10   
     11    /** @var array|null Request-scope language cache: ['slug'=>string,'locale'=>string,'ready'=>bool] */
     12    protected static $lang_cache = null;
    1013
    1114    /*
     
    4548            'site-script'               => 'cyr',
    4649            'transliteration-mode'      => 'cyr_to_lat',
    47             'mode'                      => 'advanced',
     50            'mode'                      => class_exists('DOMDocument', false) ? 'phantom' : 'advanced',
    4851            'avoid-admin'               => 'no',
    4952            'allow-admin-tools'         => 'yes',
     
    7780        ]);
    7881    }
     82   
     83    /**
     84     * Determine if current language is Cyrillic-script.
     85     * Safe default: if language is not ready/resolved => return false (Latin).
     86     *
     87     * @param string|null $slug
     88     * @param string|null $locale
     89     * @return bool
     90     */
     91    public static function is_cyrillic_locale(?string $slug = null, ?string $locale = null): bool
     92    {
     93        static $cache = [];
     94
     95        // Prefer pre-resolved info
     96        $info = self::current_language_info();
     97        if ($slug === null)   { $slug = $info['slug'] ?: null; }
     98        if ($locale === null) { $locale = $info['locale'] ?: null; }
     99
     100        $k = ($slug ?: '_') . '|' . ($locale ?: '_');
     101        if (array_key_exists($k, $cache)) {
     102            return $cache[$k];
     103        }
     104
     105        // Tight Cyrillic lists
     106        $cyr_slugs_default = [
     107            'sr','sr_RS','sr_ME','sr_BA','sr-cyrl','sr_Cyrl',
     108            'ru','uk','bg','mk','kk','tg','ky','mn','ba','be','sah','tt','uz_Cyrl',
     109            // 'cnr' only if your site really uses Cyrillic for it
     110        ];
     111        $cyr_locales_default = [
     112            'sr_RS','sr_ME','sr_BA',
     113            'ru_RU','uk_UA','bg_BG','mk_MK',
     114            'kk_KZ','tg_TJ','ky_KG','mn_MN','ba_RU','be_BY','sah_RU','tt_RU','uz_UZ_Cyrl'
     115        ];
     116
     117        $cyr_slugs   = apply_filters('rstr_cyrillic_slugs', $cyr_slugs_default, $slug, $locale);
     118        $cyr_locales = apply_filters('rstr_cyrillic_locales', $cyr_locales_default, $slug, $locale);
     119
     120        // Optional narrowing to registered set
     121        $restrict_to_registered = apply_filters('rstr_cyrillic_restrict_to_registered', true, $slug, $locale);
     122        if ($restrict_to_registered && method_exists(__CLASS__, 'registered_languages')) {
     123            // Expect keys to include locales; if you also store slugs, adjust accordingly.
     124            $registered = array_map('strval', array_keys((array) self::registered_languages()));
     125            if (!empty($registered)) {
     126                $cyr_slugs   = array_values(array_intersect($cyr_slugs, $registered));
     127                $cyr_locales = array_values(array_intersect($cyr_locales, $registered));
     128            }
     129        }
     130
     131        // SAFE DEFAULT: if not ready to decide, choose Latin (false).
     132        if (!$info['ready']) {
     133            $cache[$k] = false;
     134            return false;
     135        }
     136
     137        $is_cyr = in_array((string)$slug, $cyr_slugs, true) || in_array((string)$locale, $cyr_locales, true);
     138        $cache[$k] = $is_cyr;
     139        return $is_cyr;
     140    }
     141
    79142
    80143    /*
     
    148211        // Map available modes to their descriptions
    149212        $modes = [
    150             'phantom'     => __('Phantom Mode (ultra fast DOM-based transliteration, experimental)', 'serbian-transliteration'),
     213            'phantom'     => __('Phantom Mode (ultra fast DOM-based transliteration)', 'serbian-transliteration'),
    151214            'light'       => __('Light mode (basic parts of the site)', 'serbian-transliteration'),
    152215            'standard'    => __('Standard mode (content, themes, plugins, translations, menu)', 'serbian-transliteration'),
     
    189252
    190253            if (empty($get_locale)) {
    191                 $get_locale = function_exists('pll_current_language') ? pll_current_language('locale') : get_locale();
     254                $get_locale = self::is_pll() ? pll_current_language('locale') : get_locale();
    192255
    193256                if (is_user_logged_in() && empty($get_locale)) {
     
    697760        return self::cached_static('already_cyrillic', fn (): bool => in_array(self::get_locale(), apply_filters('rstr_already_cyrillic', ['sr_RS', 'mk_MK', 'bg_BG', 'ru_RU', 'uk', 'kk', 'el', 'ar', 'hy', 'sr', 'mk', 'bg', 'ru', 'bel', 'sah', 'bs', 'kir', 'mn', 'ba', 'uz', 'ka', 'tg', 'cnr', 'bs_BA'])) !== (1 === 2));
    698761    }
     762   
     763    /*
     764     * Check is Polylang
     765     * @return        boolean
     766     * @author        Ivijan-Stefan Stipic
     767     */
     768    public static function is_pll()
     769    {
     770        static $is_pll = NULL;
     771       
     772        if($is_pll === NULL) {
     773            $is_pll = function_exists('pll_current_language');
     774        }
     775       
     776        return $is_pll;
     777    }
    699778
    700779    /*
     
    702781     * @return        boolean
    703782     * @author        Ivijan-Stefan Stipic
    704     */
     783     */
    705784    public static function is_cyr($string)
    706785    {
     
    14811560        echo '</table>';
    14821561    }
    1483 
     1562   
     1563    /**
     1564     * Hook to capture language when Polylang announces it. Runs very early.
     1565     * @param string $slug
     1566     */
     1567    public static function on_pll_language_defined($slug) : void
     1568    {
     1569        // Try to read precise locale from Polylang if available
     1570        $locale = self::is_pll() ? pll_current_language('locale') : null;
     1571        if (!$locale) {
     1572            $locale = self::get_locale();
     1573        }
     1574        self::$lang_cache = [
     1575            'slug'   => (string) $slug,
     1576            'locale' => (string) $locale,
     1577            'ready'  => true,
     1578        ];
     1579    }
     1580
     1581    /**
     1582     * Fallback detector executed on 'wp' if language was not captured earlier.
     1583     * Guarantees we attempt a late resolve once the query is parsed.
     1584     */
     1585    public static function late_language_resolve() : void
     1586    {
     1587        if (self::$lang_cache && !empty(self::$lang_cache['ready'])) {
     1588            return; // already resolved
     1589        }
     1590        $slug = self::is_pll() ? pll_current_language('slug') : null;
     1591        $locale = self::is_pll() ? pll_current_language('locale') : null;
     1592        if (!$locale) {
     1593            $locale = self::get_locale();
     1594        }
     1595        self::$lang_cache = [
     1596            'slug'   => (string) ($slug ?: ''),
     1597            'locale' => (string) ($locale ?: ''),
     1598            'ready'  => (bool) $slug || (bool) $locale,
     1599        ];
     1600    }
     1601
     1602    /**
     1603     * Returns the best-known current language tuple.
     1604     * If not ready, returns ['slug'=>'','locale'=>'','ready'=>false].
     1605     *
     1606     * @return array{slug:string,locale:string,ready:bool}
     1607     */
     1608    public static function current_language_info() : array
     1609    {
     1610        if (!self::$lang_cache) {
     1611            // Very early call (before hooks). Populate with whatever we can guess.
     1612            $slug = self::is_pll() ? pll_current_language('slug') : null;
     1613            $locale = self::is_pll() ? pll_current_language('locale') : null;
     1614            if (!$locale) {
     1615                $locale = self::get_locale();
     1616            }
     1617            self::$lang_cache = [
     1618                'slug'   => (string) ($slug ?: ''),
     1619                'locale' => (string) ($locale ?: ''),
     1620                'ready'  => (bool) $slug || (bool) $locale,
     1621            ];
     1622        }
     1623        return self::$lang_cache;
     1624    }
    14841625}
  • serbian-transliteration/trunk/readme.txt

    r3333992 r3389106  
    55Tested up to: 6.8
    66Requires PHP: 7.4
    7 Stable tag: 2.3.7
     7Stable tag: 2.3.8
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    8989
    9090== Changelog ==
     91
     92= 2.3.8 =
     93* Fixed critical issue with Polylang integration where non-Cyrillic languages (e.g., DE, EN) were incorrectly transliterated to Cyrillic on first visit.
     94* Added robust detection logic with safe fallback to Latin for unresolved or unknown localeaas.
     95* Improved `is_cyrillic_locale()` to include lazy caching and reliable Polylang language detection through `pll_language_defined` and `wp` hooks.
     96* Added safety-net inside transliteration process to prevent accidental `lat_to_cyr` mode when language is not confirmed as Cyrillic.
     97* Restricted Polylang “first-visit” language list strictly to Cyrillic-script languages.
     98* Ensured consistent behavior in phantom mode and early-page rendering phases.
    9199
    92100= 2.3.7 =
     
    257265
    258266== Upgrade Notice ==
     267
     268= 2.3.8 =
     269* Fixed critical issue with Polylang integration where non-Cyrillic languages (e.g., DE, EN) were incorrectly transliterated to Cyrillic on first visit.
     270* Added robust detection logic with safe fallback to Latin for unresolved or unknown locales.
     271* Improved `is_cyrillic_locale()` to include lazy caching and reliable Polylang language detection through `pll_language_defined` and `wp` hooks.
     272* Added safety-net inside transliteration process to prevent accidental `lat_to_cyr` mode when language is not confirmed as Cyrillic.
     273* Restricted Polylang “first-visit” language list strictly to Cyrillic-script languages.
     274* Ensured consistent behavior in phantom mode and early-page rendering phases.
     275
    259276
    260277= 2.3.7 =
  • serbian-transliteration/trunk/serbian-transliteration.php

    r3333992 r3389106  
    55 * Plugin URI:        https://wordpress.org/plugins/serbian-transliteration/
    66 * Description:       All-in-one Cyrillic to Latin transliteration plugin for WordPress. Supports Slavic, Arabic, Greek, and Central Asian scripts.
    7  * Version:           2.3.7
     7 * Version:           2.3.8
    88 * Requires at least: 5.4
    99 * Tested up to:      6.8
     
    8787}
    8888
     89if (is_admin()) {
     90    // Needed for is_plugin_active_for_network
     91    include_once ABSPATH . 'wp-admin/includes/plugin.php';
     92}
     93
     94/**
     95 * Check if current blog_id is allow-listed when plugin is network-activated.
     96 */
     97if(!function_exists('rstr_is_allowed_site')) : function rstr_is_allowed_site($blog_id) {
     98    $allowed = (array) get_site_option('rstr_allowed_sites', array());
     99    $allowed = array_map('intval', $allowed);
     100    return in_array((int) $blog_id, $allowed, true);
     101} endif;
     102
     103/**
     104 * Decide whether to boot heavy plugin logic on this site.
     105 * Rules:
     106 * - Single-site WP: always boot.
     107 * - Multisite, plugin not network active: boot (site-level activation).
     108 * - Multisite, plugin network active: boot only if current site is in allow-list.
     109 */
     110if(!function_exists('rstr_should_boot')) : function rstr_should_boot() {
     111    if (!is_multisite()) {
     112        return true;
     113    }
     114
     115    $basename = plugin_basename(__FILE__);
     116    $is_network_active = function_exists('is_plugin_active_for_network') && is_plugin_active_for_network($basename);
     117
     118    if (!$is_network_active) {
     119        // Site-only activation path
     120        return true;
     121    }
     122
     123    // Network active path -> allow-list check
     124    return rstr_is_allowed_site(get_current_blog_id());
     125} endif;
     126
     127/**
     128 * Ensure network allow-list option exists on network activation.
     129 */
     130register_activation_hook(__FILE__, function ($network_wide) {
     131    if (is_multisite() && $network_wide) {
     132        if (false === get_site_option('rstr_allowed_sites', false)) {
     133            add_site_option('rstr_allowed_sites', array());
     134        }
     135    }
     136});
     137
     138/**
     139 * Network settings menu and screen for selecting allowed sites.
     140 * Text Domain: serbian-transliteration
     141 */
     142
     143// Register Network Admin submenu
     144if (!function_exists('rstr_register_network_settings_menu')) :
     145/**
     146 * Register network submenu under Settings.
     147 */
     148function rstr_register_network_settings_menu() {
     149    if (!is_multisite()) {
     150        return;
     151    }
     152
     153    add_submenu_page(
     154        'settings.php',
     155        __('Transliterator', 'serbian-transliteration'),
     156        __('Transliterator', 'serbian-transliteration'),
     157        'manage_network_options',
     158        'rstr-network',
     159        'rstr_render_network_settings'
     160    );
     161}
     162add_action('network_admin_menu', 'rstr_register_network_settings_menu');
     163endif;
     164
     165if (!function_exists('rstr_render_network_settings')) :
     166/**
     167 * Render the network settings screen with allow-list of sites.
     168 */
     169function rstr_render_network_settings() {
     170    if (!current_user_can('manage_network_options')) {
     171        wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'serbian-transliteration'));
     172    }
     173
     174    // Handle POST
     175    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
     176        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- We verify nonce explicitly below.
     177        if (isset($_POST['rstr_network_nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rstr_network_nonce'])), 'rstr_network_save')) {
     178            // phpcs:ignore WordPress.Security.NonceVerification.Missing
     179            $raw_sites = isset($_POST['rstr_sites']) ? (array) wp_unslash($_POST['rstr_sites']) : array();
     180            $site_ids  = array_map('absint', $raw_sites);
     181
     182            update_site_option('rstr_allowed_sites', $site_ids);
     183
     184            add_settings_error(
     185                'rstr_messages',
     186                'rstr_saved',
     187                esc_html__('Settings saved.', 'serbian-transliteration'),
     188                'updated'
     189            );
     190        } else {
     191            add_settings_error(
     192                'rstr_messages',
     193                'rstr_nonce',
     194                esc_html__('Security check failed. Please try again.', 'serbian-transliteration'),
     195                'error'
     196            );
     197        }
     198    }
     199
     200    // Read current value
     201    $enabled_sites = array_map('intval', (array) get_site_option('rstr_allowed_sites', array()));
     202
     203    // Fetch sites with pagination for large networks
     204    $sites  = array();
     205    $paged  = 1;
     206    $number = 100;
     207
     208    do {
     209        $batch = get_sites(array(
     210            'number' => $number,
     211            'paged'  => $paged,
     212            'fields' => 'all',
     213        ));
     214        if (empty($batch)) {
     215            break;
     216        }
     217        $sites = array_merge($sites, $batch);
     218        $paged++;
     219    } while (count($batch) === $number);
     220
     221    ?>
     222    <div class="wrap">
     223        <h1><?php echo esc_html__('Transliterator - Network Settings', 'serbian-transliteration'); ?></h1>
     224
     225        <?php settings_errors('rstr_messages'); ?>
     226
     227        <form method="post" action="<?php echo esc_url(network_admin_url('settings.php?page=rstr-network')); ?>">
     228            <?php wp_nonce_field('rstr_network_save', 'rstr_network_nonce'); ?>
     229
     230            <p><?php echo esc_html__('Select sites where the plugin should run when it is network-activated.', 'serbian-transliteration'); ?></p>
     231
     232            <table class="widefat fixed striped">
     233                <thead>
     234                    <tr>
     235                        <th style="width:60px;"><?php echo esc_html__('Enable', 'serbian-transliteration'); ?></th>
     236                        <th><?php echo esc_html__('Site', 'serbian-transliteration'); ?></th>
     237                        <th><?php echo esc_html__('Domain', 'serbian-transliteration'); ?></th>
     238                        <th><?php echo esc_html__('Path', 'serbian-transliteration'); ?></th>
     239                    </tr>
     240                </thead>
     241                <tbody>
     242                <?php
     243                if (!empty($sites)) :
     244                    foreach ($sites as $site) :
     245                        $blog_id = isset($site->blog_id) ? (int) $site->blog_id : 0;
     246                        if ($blog_id <= 0) {
     247                            continue;
     248                        }
     249                        $checked = in_array($blog_id, $enabled_sites, true);
     250
     251                        // get_blog_details can return false on rare cases; guard it.
     252                        $details = get_blog_details($blog_id);
     253                        $blogname = $details && !empty($details->blogname) ? $details->blogname : sprintf(__('Site #%d', 'serbian-transliteration'), $blog_id);
     254                        $domain   = $details && !empty($details->domain) ? $details->domain : '';
     255                        $path     = $details && !empty($details->path) ? $details->path : '';
     256                        ?>
     257                        <tr>
     258                            <td>
     259                                <label class="screen-reader-text" for="rstr_site_<?php echo esc_attr($blog_id); ?>">
     260                                    <?php echo esc_html(sprintf(__('Enable Transliterator on site ID %d', 'serbian-transliteration'), $blog_id)); ?>
     261                                </label>
     262                                <input
     263                                    id="rstr_site_<?php echo esc_attr($blog_id); ?>"
     264                                    type="checkbox"
     265                                    name="rstr_sites[]"
     266                                    value="<?php echo esc_attr($blog_id); ?>"
     267                                    <?php checked($checked); ?>
     268                                />
     269                            </td>
     270                            <td><?php echo esc_html($blogname) . ' ' . esc_html(sprintf('(ID: %d)', $blog_id)); ?></td>
     271                            <td><?php echo esc_html($domain); ?></td>
     272                            <td><?php echo esc_html($path); ?></td>
     273                        </tr>
     274                        <?php
     275                    endforeach;
     276                else :
     277                    ?>
     278                    <tr>
     279                        <td colspan="4"><?php echo esc_html__('No sites found.', 'serbian-transliteration'); ?></td>
     280                    </tr>
     281                    <?php
     282                endif;
     283                ?>
     284                </tbody>
     285            </table>
     286
     287            <p>
     288                <input type="submit" class="button button-primary" value="<?php echo esc_attr__('Save changes', 'serbian-transliteration'); ?>" />
     289            </p>
     290        </form>
     291    </div>
     292    <?php
     293}
     294endif;
     295
    89296// Required constants
    90297if (!defined('COOKIEHASH') || !defined('COOKIEPATH') || !defined('COOKIE_DOMAIN')) {
     
    190397    // Redirect after activation
    191398    add_action('admin_init', ['Transliteration_Init', 'register_redirection'], 10, 2);
     399   
     400    // Support for the Polylang
     401    add_action('pll_language_defined', ['Transliteration_Utilities','on_pll_language_defined'], 1);
     402    add_action('wp', ['Transliteration_Utilities','late_language_resolve'], 1);
    192403
    193404    // Run the plugin
    194     Transliteration::run_the_plugin();
     405    if (rstr_should_boot()) {
     406        Transliteration::run_the_plugin();
     407    }
    195408
    196409    // Plugin Functions
Note: See TracChangeset for help on using the changeset viewer.