Changeset 3754115
- Timestamp:
- 10/19/2026 06:40:44 AM (4 weeks ago)
- Location:
- welcoma-accessibility-widget/trunk
- Files:
-
- 5 added
- 4 edited
-
changelog.txt (added)
-
includes/class-welcoma-frontend.php (modified) (7 diffs)
-
public/css/welcoma-font.min.css (added)
-
public/css/welcoma-widget.min.css (added)
-
public/js/welcoma-prepaint.min.js (added)
-
public/js/welcoma-widget.js (modified) (4 diffs)
-
public/js/welcoma-widget.min.js (added)
-
readme.txt (modified) (2 diffs)
-
welcoma-accessibility-widget.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
welcoma-accessibility-widget/trunk/includes/class-welcoma-frontend.php
r3749719 r3754115 26 26 add_action( 'wp_body_open', array( $this, 'render_skip_link' ) ); 27 27 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' ) ); 28 30 } 29 31 … … 143 145 } 144 146 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 ) { 147 192 // Add-ons register their own modules through welcoma_widget_modules. Core 148 193 // does not perform payment/license checks for locally implemented features. … … 171 216 ); 172 217 } 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() { 175 226 wp_enqueue_script( 176 227 'welcoma-prepaint', 177 WELCOMA_WIDGET_URL . 'public/js/ welcoma-prepaint.js',228 WELCOMA_WIDGET_URL . 'public/js/' . welcoma_widget_asset( 'welcoma-prepaint.js' ), 178 229 array(), 179 230 WELCOMA_WIDGET_VERSION, 180 false 231 false // must run before the first paint to restore the visitor's settings 181 232 ); 182 233 wp_localize_script( … … 188 239 wp_enqueue_style( 189 240 'welcoma-widget', 190 WELCOMA_WIDGET_URL . 'public/css/ welcoma-widget.css',241 WELCOMA_WIDGET_URL . 'public/css/' . welcoma_widget_asset( 'welcoma-widget.css' ), 191 242 array(), 192 243 WELCOMA_WIDGET_VERSION … … 195 246 wp_enqueue_style( 196 247 'welcoma-fonts', 197 WELCOMA_WIDGET_URL . 'public/css/ welcoma-font.css',248 WELCOMA_WIDGET_URL . 'public/css/' . welcoma_widget_asset( 'welcoma-font.css' ), 198 249 array(), 199 250 WELCOMA_WIDGET_VERSION … … 202 253 wp_enqueue_script( 203 254 'welcoma-widget', 204 WELCOMA_WIDGET_URL . 'public/js/ welcoma-widget.js',255 WELCOMA_WIDGET_URL . 'public/js/' . welcoma_widget_asset( 'welcoma-widget.js' ), 205 256 array(), 206 257 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 285 267 wp_localize_script( 286 268 'welcoma-widget', … … 340 322 : array(), 341 323 'flags_url' => WELCOMA_WIDGET_URL . 'public/assets/flags/', 324 'i18n_loaded' => $inline_langs, 325 'i18n_url' => $i18n_url, 342 326 ) 343 327 ); 344 328 } 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 } 345 479 } -
welcoma-accessibility-widget/trunk/public/js/welcoma-widget.js
r3749719 r3754115 29 29 DATA.default_language || 'en'; 30 30 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 } 31 83 32 84 /* ---------------------------------------------------------------- … … 1909 1961 closeLangDropdown(true); 1910 1962 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 () { 1911 1970 // Update panel title 1912 1971 var titleEl = panel.querySelector('.welcoma-panel-title'); … … 1953 2012 1954 2013 closeLangDropdown(); 1955 }); 1956 }); 2014 }; 1957 2015 } 1958 2016 … … 2508 2566 loadState(); 2509 2567 buildUI(); 2568 // A language picked earlier in this session may not be in the page yet. 2569 if (!isLangLoaded(activeLang)) { ensureLang(activeLang, function () { refreshLanguageTexts(); }); } 2510 2570 renderProfiles(); 2511 2571 renderTiles(); -
welcoma-accessibility-widget/trunk/readme.txt
r3749719 r3754115 6 6 Tested up to: 7.1 7 7 Requires PHP: 7.2 8 Stable tag: 1.0. 18 Stable tag: 1.0.4 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 324 324 == Changelog == 325 325 326 = 1.0.4 = 327 Fix: the language files loaded by the widget's language switcher are now cached by the browser as intended. 328 329 = 1.0.3 = 330 Performance: 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 = 333 Security: translated accessibility statements are filtered before they are shown to visitors. 334 326 335 = 1.0.1 = 327 336 Improved 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 4 4 * Plugin URI: https://welcoma.app/ 5 5 * Description: Accessibility widget with A11y profiles, keyboard navigation, text and contrast controls, multilingual UI, and accessibility statements. 6 * Version: 1.0. 16 * Version: 1.0.4 7 7 * Requires at least: 6.2 8 8 * Requires PHP: 7.2 … … 24 24 * Plugin constants. 25 25 */ 26 define( 'WELCOMA_WIDGET_VERSION', '1.0. 1' );26 define( 'WELCOMA_WIDGET_VERSION', '1.0.4' ); 27 27 define( 'WELCOMA_WIDGET_FILE', __FILE__ ); 28 28 define( 'WELCOMA_WIDGET_PATH', plugin_dir_path( __FILE__ ) ); … … 90 90 * @return array<string,string> Language code => brand text. 91 91 */ 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 */ 100 function 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 92 110 function welcoma_widget_footer_brand_defaults() { 93 111 return array(
Note:
See TracChangeset
for help on using the changeset viewer.