Plugin Directory

Changeset 3754115


Ignore:
Timestamp:
10/19/2026 06:40:44 AM (4 weeks ago)
Author:
autoaltai
Message:

[200~releasing

Location:
welcoma-accessibility-widget/trunk
Files:
5 added
4 edited

Legend:

Unmodified
Added
Removed
  • welcoma-accessibility-widget/trunk/includes/class-welcoma-frontend.php

    r3749719 r3754115  
    2626                add_action( 'wp_body_open', array( $this, 'render_skip_link' ) );
    2727                add_action( 'wp_footer', array( $this, 'render_widget_landmark' ), 5 );
     28                add_action( 'wp_ajax_welcoma_i18n', array( $this, 'ajax_translations' ) );
     29                add_action( 'wp_ajax_nopriv_welcoma_i18n', array( $this, 'ajax_translations' ) );
    2830        }
    2931
    … …  
    143145                }
    144146
    145                 $settings = Welcoma_Settings::get();
    146 
     147                $settings       = Welcoma_Settings::get();
     148                $active_modules = $this->active_modules( $settings );
     149
     150                // Pre-paint state restoration is shipped as a normal enqueued script.
     151                $this->enqueue_core_assets();
     152
     153                // ── Build title / branding translations (flat cache shape) ──
     154                $title_translations = array();
     155                foreach ( Welcoma_Translator::all_lang_codes() as $lang ) {
     156                        $val = Welcoma_Translator::get_string( 'accessibility_title', $lang );
     157                        if ( '' !== $val ) {
     158                                $title_translations[ $lang ] = $val;
     159                        }
     160                }
     161                // Ensure the current source-locale/settings value is always present
     162                // even before any background job has run.
     163                if ( empty( $title_translations['en'] ) ) {
     164                        $title_translations['en'] = 'Accessibility';
     165                }
     166
     167                // Only the start language goes into the page. Every language would add ~150 KB to
     168                // each HTML page; other languages are fetched when a visitor switches (ajax_translations).
     169                $inline_langs   = $this->initial_langs( $settings );
     170                $translations   = $this->build_translations( $active_modules, $inline_langs );
     171                $statement_data = get_option( 'welcoma_statement', array( 'content' => '' ) );
     172                $statement_data['translations'] = empty( $translations['statement'] ) ? null : $translations['statement'];
     173                $module_translations            = $translations['module_translations'];
     174                $static_ui                      = $translations['static_ui'];
     175                $i18n_url                       = add_query_arg(
     176                        array(
     177                                'action' => 'welcoma_i18n',
     178                                'v'      => $this->translations_version( $active_modules ),
     179                        ),
     180                        admin_url( 'admin-ajax.php' )
     181                );
     182                $this->localize( $settings, $active_modules, $title_translations, $module_translations, $statement_data, $static_ui, $inline_langs, $i18n_url );
     183        }
     184
     185        /**
     186         * Modules shown in the widget: enabled ones plus every module a built-in profile uses.
     187         *
     188         * @param array $settings Widget settings.
     189         * @return array[]
     190         */
     191        private function active_modules( $settings ) {
    147192                // Add-ons register their own modules through welcoma_widget_modules. Core
    148193                // does not perform payment/license checks for locally implemented features.
    … …  
    171216                        );
    172217                }
    173 
    174                 // Pre-paint state restoration is shipped as a normal enqueued script.
     218                return $active_modules;
     219        }
     220
     221        /**
     222         * CSS/JS of the widget. Minified files (built by the release workflow) unless
     223         * SCRIPT_DEBUG is on or they are missing (development checkout).
     224         */
     225        private function enqueue_core_assets() {
    175226                wp_enqueue_script(
    176227                        'welcoma-prepaint',
    177                         WELCOMA_WIDGET_URL . 'public/js/welcoma-prepaint.js',
     228                        WELCOMA_WIDGET_URL . 'public/js/' . welcoma_widget_asset( 'welcoma-prepaint.js' ),
    178229                        array(),
    179230                        WELCOMA_WIDGET_VERSION,
    180                         false
     231                        false // must run before the first paint to restore the visitor's settings
    181232                );
    182233                wp_localize_script(
    … …  
    188239                wp_enqueue_style(
    189240                        'welcoma-widget',
    190                         WELCOMA_WIDGET_URL . 'public/css/welcoma-widget.css',
     241                        WELCOMA_WIDGET_URL . 'public/css/' . welcoma_widget_asset( 'welcoma-widget.css' ),
    191242                        array(),
    192243                        WELCOMA_WIDGET_VERSION
    … …  
    195246                wp_enqueue_style(
    196247                        'welcoma-fonts',
    197                         WELCOMA_WIDGET_URL . 'public/css/welcoma-font.css',
     248                        WELCOMA_WIDGET_URL . 'public/css/' . welcoma_widget_asset( 'welcoma-font.css' ),
    198249                        array(),
    199250                        WELCOMA_WIDGET_VERSION
    … …  
    202253                wp_enqueue_script(
    203254                        'welcoma-widget',
    204                         WELCOMA_WIDGET_URL . 'public/js/welcoma-widget.js',
     255                        WELCOMA_WIDGET_URL . 'public/js/' . welcoma_widget_asset( 'welcoma-widget.js' ),
    205256                        array(),
    206257                        WELCOMA_WIDGET_VERSION,
    207                         true
    208                 );
    209 
    210                 // ── Build title / branding translations (flat cache shape) ──
    211                 $cache              = Welcoma_Translator::get_cache();
    212                 $snapshot           = Welcoma_Translator::get_snapshot();
    213                 $title_translations = array();
    214 
    215                 foreach ( Welcoma_Translator::all_lang_codes() as $lang ) {
    216                         $val = Welcoma_Translator::get_string( 'accessibility_title', $lang );
    217                         if ( '' !== $val ) {
    218                                 $title_translations[ $lang ] = $val;
    219                         }
    220                 }
    221                 // Ensure the current source-locale/settings value is always present
    222                 // even before any background job has run.
    223                 if ( empty( $title_translations['en'] ) ) {
    224                         $title_translations['en'] = 'Accessibility';
    225                 }
    226 
    227                 // ── Build module translations ──
    228                 // Cache keys are stored as '{module_id}_name' and '{module_id}_desc'
    229                 // e.g. 'font-size_name', 'font-size_desc'
    230                 $module_translations = array();
    231 
    232                 foreach ( $active_modules as $m ) {
    233                         $id = $m['id'];
    234 
    235                         $module_translations[ $id ] = array(
    236                                 'name' => array(),
    237                                 'desc' => array(),
    238                         );
    239 
    240                         $safe_id        = str_replace( '-', '_', $id );
    241                         $is_custom      = ! empty( $m['is_custom'] );
    242                         $name_cache_key = ( $is_custom ? 'custom_profile__' : 'mod__' ) . $safe_id . '__name';
    243                         $desc_cache_key = ( $is_custom ? 'custom_profile__' : 'mod__' ) . $safe_id . '__desc';
    244 
    245                         foreach ( Welcoma_Translator::all_lang_codes() as $lang ) {
    246                                 $name_val = Welcoma_Translator::get_string( $name_cache_key, $lang );
    247                                 if ( '' !== $name_val ) {
    248                                         $module_translations[ $id ]['name'][ $lang ] = $name_val;
    249                                 }
    250                                 $desc_val = Welcoma_Translator::get_string( $desc_cache_key, $lang );
    251                                 if ( '' !== $desc_val ) {
    252                                         $module_translations[ $id ]['desc'][ $lang ] = $desc_val;
    253                                 }
    254                         }
    255                 }
    256 
    257                 $statement_data    = get_option( 'welcoma_statement', array( 'content' => '' ) );
    258                 $stmt_translations = array();
    259 
    260                 foreach ( Welcoma_Translator::all_lang_codes() as $lang ) {
    261                         $val = Welcoma_Translator::get_string( 'statement', $lang );
    262                         if ( '' !== $val ) {
    263                                 $stmt_translations[ $lang ] = $val;
    264                         }
    265                 }
    266 
    267                 $statement_data['translations'] = empty( $stmt_translations ) ? null : $stmt_translations;
    268 
    269                 // Static UI strings are now translated via the same Translation
    270                 // API cache as everything else (per-language, follows the
    271                 // widget's own language switcher rather than the site locale).
    272                 $static_ui = array();
    273                 foreach ( Welcoma_Translator::static_ui_strings() as $ui_key => $ui_en_default ) {
    274                         $static_ui[ $ui_key ] = array();
    275                         foreach ( Welcoma_Translator::all_lang_codes() as $lang ) {
    276                                 $val = Welcoma_Translator::get_string( $ui_key, $lang );
    277                                 if ( '' !== $val ) {
    278                                         $static_ui[ $ui_key ][ $lang ] = $val;
    279                                 }
    280                         }
    281                         if ( empty( $static_ui[ $ui_key ] ) ) {
    282                                 unset( $static_ui[ $ui_key ] );
    283                         }
    284                 }
     258                        true // footer, not deferred: Pro extends window.WelcomaWidget before the widget's init on DOMContentLoaded
     259                );
     260        }
     261
     262        /**
     263         * Hand the widget its configuration.
     264         */
     265        private function localize( $settings, $active_modules, $title_translations, $module_translations, $statement_data, $static_ui, $inline_langs, $i18n_url ) {
     266
    285267                wp_localize_script(
    286268                        'welcoma-widget',
    … …  
    340322                                        : array(),
    341323                                'flags_url'                => WELCOMA_WIDGET_URL . 'public/assets/flags/',
     324                                'i18n_loaded'              => $inline_langs,
     325                                'i18n_url'                 => $i18n_url,
    342326                        )
    343327                );
    344328        }
     329
     330        /**
     331         * Languages the visitor starts with: the admin default, or the first enabled language when
     332         * the default isn't enabled (same rule as the widget JS), plus the base language of a
     333         * regional code (pt-br → pt) because the widget falls back to it.
     334         *
     335         * @param array $settings Widget settings.
     336         * @return string[]
     337         */
     338        private function initial_langs( $settings ) {
     339                $default = ! empty( $settings['default_language'] ) ? (string) $settings['default_language'] : 'en';
     340                $enabled = ! empty( $settings['enabled_languages'] ) ? (array) $settings['enabled_languages'] : array();
     341                if ( $enabled && ! in_array( $default, $enabled, true ) ) {
     342                        foreach ( Welcoma_Translator::all_lang_codes() as $code ) {
     343                                if ( in_array( $code, $enabled, true ) ) {
     344                                        $default = $code;
     345                                        break;
     346                                }
     347                        }
     348                }
     349                return $this->with_base_lang( array( $default ) );
     350        }
     351
     352        /**
     353         * @param string[] $langs Language codes.
     354         * @return string[] Codes plus their base language (pt-br → pt), only known codes.
     355         */
     356        private function with_base_lang( array $langs ) {
     357                $known = Welcoma_Translator::all_lang_codes();
     358                $out   = array();
     359                foreach ( $langs as $lang ) {
     360                        foreach ( array( $lang, strtok( $lang, '-' ) ) as $code ) {
     361                                if ( in_array( $code, $known, true ) && ! in_array( $code, $out, true ) ) {
     362                                        $out[] = $code;
     363                                }
     364                        }
     365                }
     366                return $out;
     367        }
     368
     369        /**
     370         * Module, UI and statement translations for the given languages, in the shape the widget
     371         * reads (module_translations[id].name[lang], static_ui[key][lang], statement[lang]).
     372         *
     373         * @param array    $active_modules Modules shown in the widget.
     374         * @param string[] $langs          Language codes.
     375         * @return array{module_translations: array, static_ui: array, statement: array}
     376         */
     377        private function build_translations( $active_modules, array $langs ) {
     378                $module_translations = array();
     379                foreach ( $active_modules as $m ) {
     380                        $id                         = $m['id'];
     381                        $module_translations[ $id ] = array(
     382                                'name' => array(),
     383                                'desc' => array(),
     384                        );
     385                        // Cache keys: 'mod__{id}__name' / '__desc' (custom profiles: 'custom_profile__…').
     386                        $prefix = ( empty( $m['is_custom'] ) ? 'mod__' : 'custom_profile__' ) . str_replace( '-', '_', $id );
     387                        foreach ( $langs as $lang ) {
     388                                foreach ( array( 'name', 'desc' ) as $field ) {
     389                                        $val = Welcoma_Translator::get_string( $prefix . '__' . $field, $lang );
     390                                        if ( '' !== $val ) {
     391                                                $module_translations[ $id ][ $field ][ $lang ] = $val;
     392                                        }
     393                                }
     394                        }
     395                }
     396
     397                $statement = array();
     398                foreach ( $langs as $lang ) {
     399                        $val = Welcoma_Translator::get_string( 'statement', $lang );
     400                        if ( '' !== $val ) {
     401                                // Inserted as HTML by the widget – filter on output, so translations cached
     402                                // before the Pro translator sanitised them can't carry script either.
     403                                $statement[ $lang ] = wp_kses_post( $val );
     404                        }
     405                }
     406
     407                // Static UI strings follow the widget's own language switcher, not the site locale.
     408                $static_ui = array();
     409                foreach ( array_keys( Welcoma_Translator::static_ui_strings() ) as $ui_key ) {
     410                        foreach ( $langs as $lang ) {
     411                                $val = Welcoma_Translator::get_string( $ui_key, $lang );
     412                                if ( '' !== $val ) {
     413                                        $static_ui[ $ui_key ][ $lang ] = $val;
     414                                }
     415                        }
     416                }
     417
     418                return array(
     419                        'module_translations' => $module_translations,
     420                        'static_ui'           => $static_ui,
     421                        'statement'           => $statement,
     422                );
     423        }
     424
     425        /**
     426         * Changes whenever a translation, the module list or the statement changes, so browsers
     427         * can cache ajax_translations() responses for long and still never get stale text.
     428         *
     429         * @param array $active_modules Modules shown in the widget.
     430         * @return string
     431         */
     432        private function translations_version( $active_modules ) {
     433                $statement = get_option( 'welcoma_statement', array() );
     434                return substr(
     435                        md5( WELCOMA_WIDGET_VERSION . '|' . serialize( Welcoma_Translator::get_cache() ) . '|' . wp_json_encode( wp_list_pluck( $active_modules, 'id' ) ) . '|' . wp_json_encode( $statement ) ),
     436                        0,
     437                        12
     438                );
     439        }
     440
     441        /**
     442         * admin-ajax?action=welcoma_i18n&lang=xx – translations of one language for the widget's
     443         * language switcher. Public data, no nonce; cached by the browser (the URL carries a
     444         * content version). admin-ajax instead of REST: security plugins often block the REST API
     445         * for visitors.
     446         */
     447        public function ajax_translations() {
     448                $lang = isset( $_GET['lang'] ) ? sanitize_text_field( wp_unslash( $_GET['lang'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
     449                $settings = Welcoma_Settings::get();
     450                if ( empty( $settings['widget_enabled'] ) || ! in_array( $lang, Welcoma_Translator::all_lang_codes(), true ) ) {
     451                        wp_send_json_error( null, 404 );
     452                }
     453
     454                $langs = $this->with_base_lang( array( $lang ) );
     455                $data  = $this->build_translations( $this->active_modules( $settings ), $langs );
     456
     457                // admin-ajax sends no-cache headers, and wp_send_json() → wp_die() sends them again while
     458                // the output is still buffered – so a plain header() call gets overwritten. The filter
     459                // covers both calls. The URL carries a content version, so caching is safe.
     460                $cache_headers = static function () {
     461                        return array(
     462                                'Cache-Control' => 'public, max-age=86400',
     463                                'Expires'       => gmdate( 'D, d M Y H:i:s', time() + DAY_IN_SECONDS ) . ' GMT',
     464                        );
     465                };
     466                add_filter( 'nocache_headers', $cache_headers, PHP_INT_MAX );
     467                foreach ( $cache_headers() as $name => $value ) {
     468                        header( $name . ': ' . $value );
     469                }
     470                wp_send_json(
     471                        array(
     472                                'langs'               => $langs,
     473                                'module_translations' => $data['module_translations'],
     474                                'static_ui'           => $data['static_ui'],
     475                                'statement'           => $data['statement'],
     476                        )
     477                );
     478        }
    345479}
  • welcoma-accessibility-widget/trunk/public/js/welcoma-widget.js

    r3749719 r3754115  
    2929        DATA.default_language || 'en';
    3030    var WELCOMA_FLAG_BASE_URL = DATA.flags_url || '';
     31
     32    /* ----------------------------------------------------------------
     33     * Translations are delivered for the start language only (they used to add
     34     * ~150 KB of every language to each page). Other languages are fetched once
     35     * from DATA.i18n_url when needed and merged into the same DATA structures,
     36     * so getModuleName(), getStatic() etc. work unchanged.
     37     * ---------------------------------------------------------------- */
     38    var loadedLangs = Array.isArray(DATA.i18n_loaded) ? DATA.i18n_loaded.slice() : null;
     39    var refreshLanguageTexts = function () { };
     40
     41    function isLangLoaded(lang) {
     42        if (!loadedLangs || !DATA.i18n_url) { return true; } // old data shape: everything inline
     43        return loadedLangs.indexOf(lang) !== -1;
     44    }
     45
     46    function mergeTranslations(data) {
     47        if (!data || typeof data !== 'object') { return; }
     48        var mt = data.module_translations || {};
     49        DATA.module_translations = DATA.module_translations || {};
     50        Object.keys(mt).forEach(function (id) {
     51            var target = DATA.module_translations[id] = DATA.module_translations[id] || { name: {}, desc: {} };
     52            ['name', 'desc'].forEach(function (field) {
     53                target[field] = target[field] || {};
     54                Object.keys(mt[id][field] || {}).forEach(function (lang) { target[field][lang] = mt[id][field][lang]; });
     55            });
     56        });
     57        var ui = data.static_ui || {};
     58        DATA.static_ui = DATA.static_ui || {};
     59        Object.keys(ui).forEach(function (key) {
     60            DATA.static_ui[key] = DATA.static_ui[key] || {};
     61            Object.keys(ui[key]).forEach(function (lang) { DATA.static_ui[key][lang] = ui[key][lang]; });
     62        });
     63        var st = data.statement || {};
     64        if (Object.keys(st).length) {
     65            DATA.statement = DATA.statement || {};
     66            DATA.statement.translations = DATA.statement.translations || {};
     67            Object.keys(st).forEach(function (lang) { DATA.statement.translations[lang] = st[lang]; });
     68        }
     69        (data.langs || []).forEach(function (lang) {
     70            if (loadedLangs.indexOf(lang) === -1) { loadedLangs.push(lang); }
     71        });
     72    }
     73
     74    // Calls done() once the language is available – or failed to load, in which case the
     75    // widget simply shows the source-language texts as before.
     76    function ensureLang(lang, done) {
     77        if (isLangLoaded(lang) || !window.fetch) { done(); return; }
     78        var url = DATA.i18n_url + (DATA.i18n_url.indexOf('?') === -1 ? '?' : '&') + 'lang=' + encodeURIComponent(lang);
     79        window.fetch(url, { credentials: 'same-origin' })
     80            .then(function (r) { return r.ok ? r.json() : null; })
     81            .then(function (json) { mergeTranslations(json); done(); }, function () { done(); });
     82    }
    3183
    3284    /* ----------------------------------------------------------------
    … …  
    19091961                    closeLangDropdown(true);
    19101962
     1963                    ensureLang(activeLang, refreshLanguageTexts);
     1964                });
     1965            });
     1966
     1967            // Re-label everything in the active language (after a switch, or once a
     1968            // language remembered from earlier in the session has been loaded).
     1969            refreshLanguageTexts = function () {
    19111970                    // Update panel title
    19121971                    var titleEl = panel.querySelector('.welcoma-panel-title');
    … …  
    19532012
    19542013                    closeLangDropdown();
    1955                 });
    1956             });
     2014            };
    19572015        }
    19582016
    … …  
    25082566        loadState();
    25092567        buildUI();
     2568        // A language picked earlier in this session may not be in the page yet.
     2569        if (!isLangLoaded(activeLang)) { ensureLang(activeLang, function () { refreshLanguageTexts(); }); }
    25102570        renderProfiles();
    25112571        renderTiles();
  • welcoma-accessibility-widget/trunk/readme.txt

    r3749719 r3754115  
    66Tested up to: 7.1
    77Requires PHP: 7.2
    8 Stable tag: 1.0.1
     8Stable tag: 1.0.4
    99License: GPLv2 or later
    1010License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    324324== Changelog ==
    325325
     326= 1.0.4 =
     327Fix: the language files loaded by the widget's language switcher are now cached by the browser as intended.
     328
     329= 1.0.3 =
     330Performance: pages only carry the widget texts of the start language (other languages load when a visitor switches), and the front-end JS/CSS ship minified. On a typical page the HTML is about 150 KB smaller.
     331
     332= 1.0.2 =
     333Security: translated accessibility statements are filtered before they are shown to visitors.
     334
    326335= 1.0.1 =
    327336Improved the "Reduced Motion & Flash" profile: it now also mutes sounds and lowers color saturation, in addition to stopping animations.
  • welcoma-accessibility-widget/trunk/welcoma-accessibility-widget.php

    r3749719 r3754115  
    44 * Plugin URI:        https://welcoma.app/
    55 * Description:       Accessibility widget with A11y profiles, keyboard navigation, text and contrast controls, multilingual UI, and accessibility statements.
    6  * Version:           1.0.1
     6 * Version:           1.0.4
    77 * Requires at least: 6.2
    88 * Requires PHP:      7.2
    … …  
    2424 * Plugin constants.
    2525 */
    26 define( 'WELCOMA_WIDGET_VERSION', '1.0.1' );
     26define( 'WELCOMA_WIDGET_VERSION', '1.0.4' );
    2727define( 'WELCOMA_WIDGET_FILE', __FILE__ );
    2828define( 'WELCOMA_WIDGET_PATH', plugin_dir_path( __FILE__ ) );
    … …  
    9090 * @return array<string,string> Language code => brand text.
    9191 */
     92/**
     93 * File name of a front-end asset: the minified build (name.min.js / name.min.css, created by
     94 * the release workflow) unless SCRIPT_DEBUG is on or no build exists (development checkout).
     95 *
     96 * @param string $file File name in public/js or public/css, e.g. 'welcoma-widget.js'.
     97 * @param string $base Plugin directory that contains public/ (defaults to this plugin).
     98 * @return string
     99 */
     100function welcoma_widget_asset( $file, $base = '' ) {
     101        if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) {
     102                return $file;
     103        }
     104        $base = $base ? $base : plugin_dir_path( __FILE__ );
     105        $dir  = '.css' === substr( $file, -4 ) ? 'public/css/' : 'public/js/';
     106        $min  = preg_replace( '/\.(js|css)$/', '.min.$1', $file );
     107        return is_readable( trailingslashit( $base ) . $dir . $min ) ? $min : $file;
     108}
     109
    92110function welcoma_widget_footer_brand_defaults() {
    93111        return array(
Note: See TracChangeset for help on using the changeset viewer.