.
* Shares the exact jump-and-focus logic the widget panel's own Skip to
* Content control uses, via window.WelcomaWidget.skipToMain() — but is
* reachable by the very first Tab press, without opening the widget.
* Fixes Issue N5b.
*/
public function render_skip_link() {
if ( ! $this->should_load() ) {
return;
}
?>
should_load() ) {
return;
}
echo '' . "\n";
}
/**
* Print the widget landmark server-side so it exists in the raw HTML
* before JavaScript runs. buildUI() reuses this node.
*/
public function render_widget_landmark() {
if ( ! $this->should_load() ) {
return;
}
$settings = Welcoma_Settings::get();
$position = ( isset( $settings['position'] ) && 'left' === $settings['position'] ) ? 'left' : 'right';
$title = Welcoma_Translator::get_string( 'accessibility_title', Welcoma_Translator::current_source_lang() );
if ( '' === $title ) {
$title = 'Accessibility';
}
/*
* The landmark name must always contain the literal English phrase
* "Accessibility Options". Detection heuristics look for the two words
* adjacent, so "Accessibility Hilfe Options" would NOT qualify, and a
* localised title such as "Barrierefreiheit" would not either. Only
* append when the exact phrase is missing, so an English install does
* not get it twice.
*/
$label = $title;
if ( false === stripos( $label, 'accessibility options' ) ) {
// "Accessibility" / "Web Accessibility" -> just append " Options".
// "Barrierefreiheit" / "Accessibility Hilfe" -> append the full phrase.
$label .= preg_match( '/accessibility\s*$/i', $label ) ? ' Options' : ' – Accessibility Options';
}
printf(
'' . "\n",
esc_attr( $position ),
esc_attr( $label ),
esc_attr( WELCOMA_WIDGET_VERSION )
);
}
/**
* Should the widget load on the current request?
*
* @return bool
*/
private function should_load() {
$settings = Welcoma_Settings::get();
if ( empty( $settings['widget_enabled'] ) ) {
return false;
}
if ( is_admin() || is_feed() ) {
return false;
}
return (bool) apply_filters( 'welcoma_widget_load_widget', true );
}
/**
* Footer brand strings for the widget, or null when hidden.
*
* Always shown unless an add-on (Welcoma Pro, with a connected paid
* licence) returns false from 'welcoma_widget_footer_brand_visible'.
*
* @return array|null
*/
private function footer_brand_data() {
if ( ! apply_filters( 'welcoma_widget_footer_brand_visible', true ) ) {
return null;
}
$texts = get_option( WELCOMA_WIDGET_FOOTER_BRAND_OPTION );
if ( ! is_array( $texts ) || empty( $texts ) ) {
$texts = welcoma_widget_footer_brand_defaults();
}
return array(
'texts' => $texts,
'url' => 'https://welcoma.app/',
);
}
/**
* Enqueue the widget CSS/JS and hand it the active configuration.
*/
public function enqueue_assets() {
if ( ! $this->should_load() ) {
return;
}
$settings = Welcoma_Settings::get();
$active_modules = $this->active_modules( $settings );
// Pre-paint state restoration is shipped as a normal enqueued script.
$this->enqueue_core_assets();
// ── Build title / branding translations (flat cache shape) ──
$title_translations = array();
foreach ( Welcoma_Translator::all_lang_codes() as $lang ) {
$val = Welcoma_Translator::get_string( 'accessibility_title', $lang );
if ( '' !== $val ) {
$title_translations[ $lang ] = $val;
}
}
// Ensure the current source-locale/settings value is always present
// even before any background job has run.
if ( empty( $title_translations['en'] ) ) {
$title_translations['en'] = 'Accessibility';
}
// Only the start language goes into the page. Every language would add ~150 KB to
// each HTML page; other languages are fetched when a visitor switches (ajax_translations).
$inline_langs = $this->initial_langs( $settings );
$translations = $this->build_translations( $active_modules, $inline_langs );
$statement_data = get_option( 'welcoma_statement', array( 'content' => '' ) );
$statement_data['translations'] = empty( $translations['statement'] ) ? null : $translations['statement'];
$module_translations = $translations['module_translations'];
$static_ui = $translations['static_ui'];
$i18n_url = add_query_arg(
array(
'action' => 'welcoma_i18n',
'v' => $this->translations_version( $active_modules ),
),
admin_url( 'admin-ajax.php' )
);
$this->localize( $settings, $active_modules, $title_translations, $module_translations, $statement_data, $static_ui, $inline_langs, $i18n_url );
}
/**
* Modules shown in the widget: enabled ones plus every module a built-in profile uses.
*
* @param array $settings Widget settings.
* @return array[]
*/
private function active_modules( $settings ) {
// Add-ons register their own modules through welcoma_widget_modules. Core
// does not perform payment/license checks for locally implemented features.
$profile_preset_ids = array();
foreach ( Welcoma_Settings::builtin_profile_module_map() as $preset ) {
if ( is_array( $preset ) ) {
$profile_preset_ids = array_merge( $profile_preset_ids, array_keys( $preset ) ); }
}
$profile_preset_ids = array_unique( $profile_preset_ids );
$active_modules = array();
foreach ( welcoma_widget_get_modules() as $module ) {
$id = $module['id'];
if ( empty( $settings['enabled'][ $id ] ) && ! in_array( $id, $profile_preset_ids, true ) ) {
continue; }
$active_modules[] = array(
'id' => $id,
'name' => $module['name'],
'desc' => $module['desc'] ?? '',
'icon' => $module['icon'] ?? 'access',
'category' => $module['category'] ?? '',
'type' => $module['type'],
'stepped' => ! empty( $module['stepped'] ),
'is_custom' => ! empty( $module['is_custom'] ),
'modules' => ! empty( $module['is_custom'] ) && isset( $module['modules'] ) ? $module['modules'] : array(),
);
}
return $active_modules;
}
/**
* CSS/JS of the widget. Minified files (built by the release workflow) unless
* SCRIPT_DEBUG is on or they are missing (development checkout).
*/
private function enqueue_core_assets() {
wp_enqueue_script(
'welcoma-prepaint',
WELCOMA_WIDGET_URL . 'public/js/' . welcoma_widget_asset( 'welcoma-prepaint.js' ),
array(),
WELCOMA_WIDGET_VERSION,
false // must run before the first paint to restore the visitor's settings
);
wp_localize_script(
'welcoma-prepaint',
'welcomaPrepaintData',
array( 'classMap' => Welcoma_Settings::prepaint_class_map() )
);
wp_enqueue_style(
'welcoma-widget',
WELCOMA_WIDGET_URL . 'public/css/' . welcoma_widget_asset( 'welcoma-widget.css' ),
array(),
WELCOMA_WIDGET_VERSION
);
wp_enqueue_style(
'welcoma-fonts',
WELCOMA_WIDGET_URL . 'public/css/' . welcoma_widget_asset( 'welcoma-font.css' ),
array(),
WELCOMA_WIDGET_VERSION
);
wp_enqueue_script(
'welcoma-widget',
WELCOMA_WIDGET_URL . 'public/js/' . welcoma_widget_asset( 'welcoma-widget.js' ),
array(),
WELCOMA_WIDGET_VERSION,
true // footer, not deferred: Pro extends window.WelcomaWidget before the widget's init on DOMContentLoaded
);
}
/**
* Hand the widget its configuration.
*/
private function localize( $settings, $active_modules, $title_translations, $module_translations, $statement_data, $static_ui, $inline_langs, $i18n_url ) {
wp_localize_script(
'welcoma-widget',
'welcomaWidgetData',
array(
'modules' => $active_modules,
'footer_brand' => $this->footer_brand_data(),
'profile_module_map' => Welcoma_Settings::builtin_profile_module_map(),
'position' => $settings['position'],
'main_icon_bg' => $settings['main_icon_bg'],
'icon_color' => $settings['icon_color'],
'widget_icon' => $settings['widget_icon'],
'icon_size' => $settings['icon_size'],
'horizontal_offset' => $settings['horizontal_offset'],
'vertical_offset' => $settings['vertical_offset'],
'version' => $settings['version'],
'accessibility_title' => ! empty( $title_translations[ Welcoma_Translator::current_source_lang() ] )
? $title_translations[ Welcoma_Translator::current_source_lang() ]
: ( ! empty( $title_translations['en'] ) ? $title_translations['en'] : 'Accessibility' ),
'module_translations' => $module_translations,
'title_translations' => $title_translations,
'header_bg' => $settings['header_bg'],
'header_text_color' => $settings['header_text_color'],
'header_icon_bg' => $settings['header_icon_bg'],
'header_icon_color' => $settings['header_icon_color'],
'box_bg_color' => $settings['box_bg_color'],
'box_text_color' => $settings['box_text_color'],
'module_bg' => $settings['module_bg'],
'module_text_color' => $settings['module_text_color'],
'module_icon_color' => $settings['module_icon_color'],
'module_icon_bg_color' => $settings['module_icon_bg_color'],
'module_border_color' => $settings['module_border_color'],
'act_module_bg' => $settings['act_module_bg'],
'act_module_text_color' => $settings['act_module_text_color'],
'act_module_icon_color' => $settings['act_module_icon_color'],
'act_module_icon_bg_color' => $settings['act_module_icon_bg_color'],
'act_module_border_color' => $settings['act_module_border_color'],
'footer_bg' => $settings['footer_bg'],
'footer_text_color' => $settings['footer_text_color'],
'reset_btn_bg' => $settings['reset_btn_bg'],
'reset_btn_text_color' => $settings['reset_btn_text_color'],
'useWebsiteFont' => ! empty( $settings['use_website_font'] ),
'statement' => $statement_data,
'static_ui' => $static_ui,
'i18n' => array(
'title' => __( 'Accessibility', 'welcoma-accessibility-widget' ),
'open' => __( 'Open accessibility menu', 'welcoma-accessibility-widget' ),
'close' => __( 'Close', 'welcoma-accessibility-widget' ),
'reset' => __( 'Reset all', 'welcoma-accessibility-widget' ),
'statement' => __( 'Accessibility statement', 'welcoma-accessibility-widget' ),
'noModules' => __( 'No accessibility modules are enabled yet.', 'welcoma-accessibility-widget' ),
),
'default_language' => $settings['default_language'] ?? 'en',
'translation_source_lang' => Welcoma_Translator::current_source_lang(),
'enabled_languages' => ! empty( $settings['enabled_languages'] )
? $settings['enabled_languages']
: array(),
'flags_url' => WELCOMA_WIDGET_URL . 'public/assets/flags/',
'i18n_loaded' => $inline_langs,
'i18n_url' => $i18n_url,
)
);
}
/**
* Languages the visitor starts with: the admin default, or the first enabled language when
* the default isn't enabled (same rule as the widget JS), plus the base language of a
* regional code (pt-br → pt) because the widget falls back to it.
*
* @param array $settings Widget settings.
* @return string[]
*/
private function initial_langs( $settings ) {
$default = ! empty( $settings['default_language'] ) ? (string) $settings['default_language'] : 'en';
$enabled = ! empty( $settings['enabled_languages'] ) ? (array) $settings['enabled_languages'] : array();
if ( $enabled && ! in_array( $default, $enabled, true ) ) {
foreach ( Welcoma_Translator::all_lang_codes() as $code ) {
if ( in_array( $code, $enabled, true ) ) {
$default = $code;
break;
}
}
}
return $this->with_base_lang( array( $default ) );
}
/**
* @param string[] $langs Language codes.
* @return string[] Codes plus their base language (pt-br → pt), only known codes.
*/
private function with_base_lang( array $langs ) {
$known = Welcoma_Translator::all_lang_codes();
$out = array();
foreach ( $langs as $lang ) {
foreach ( array( $lang, strtok( $lang, '-' ) ) as $code ) {
if ( in_array( $code, $known, true ) && ! in_array( $code, $out, true ) ) {
$out[] = $code;
}
}
}
return $out;
}
/**
* Module, UI and statement translations for the given languages, in the shape the widget
* reads (module_translations[id].name[lang], static_ui[key][lang], statement[lang]).
*
* @param array $active_modules Modules shown in the widget.
* @param string[] $langs Language codes.
* @return array{module_translations: array, static_ui: array, statement: array}
*/
private function build_translations( $active_modules, array $langs ) {
$module_translations = array();
foreach ( $active_modules as $m ) {
$id = $m['id'];
$module_translations[ $id ] = array(
'name' => array(),
'desc' => array(),
);
// Cache keys: 'mod__{id}__name' / '__desc' (custom profiles: 'custom_profile__…').
$prefix = ( empty( $m['is_custom'] ) ? 'mod__' : 'custom_profile__' ) . str_replace( '-', '_', $id );
foreach ( $langs as $lang ) {
foreach ( array( 'name', 'desc' ) as $field ) {
$val = Welcoma_Translator::get_string( $prefix . '__' . $field, $lang );
if ( '' !== $val ) {
$module_translations[ $id ][ $field ][ $lang ] = $val;
}
}
}
}
$statement = array();
foreach ( $langs as $lang ) {
$val = Welcoma_Translator::get_string( 'statement', $lang );
if ( '' !== $val ) {
// Inserted as HTML by the widget – filter on output, so translations cached
// before the Pro translator sanitised them can't carry script either.
$statement[ $lang ] = wp_kses_post( $val );
}
}
// Static UI strings follow the widget's own language switcher, not the site locale.
$static_ui = array();
foreach ( array_keys( Welcoma_Translator::static_ui_strings() ) as $ui_key ) {
foreach ( $langs as $lang ) {
$val = Welcoma_Translator::get_string( $ui_key, $lang );
if ( '' !== $val ) {
$static_ui[ $ui_key ][ $lang ] = $val;
}
}
}
return array(
'module_translations' => $module_translations,
'static_ui' => $static_ui,
'statement' => $statement,
);
}
/**
* Changes whenever a translation, the module list or the statement changes, so browsers
* can cache ajax_translations() responses for long and still never get stale text.
*
* @param array $active_modules Modules shown in the widget.
* @return string
*/
private function translations_version( $active_modules ) {
$statement = get_option( 'welcoma_statement', array() );
return substr(
md5( WELCOMA_WIDGET_VERSION . '|' . serialize( Welcoma_Translator::get_cache() ) . '|' . wp_json_encode( wp_list_pluck( $active_modules, 'id' ) ) . '|' . wp_json_encode( $statement ) ),
0,
12
);
}
/**
* admin-ajax?action=welcoma_i18n&lang=xx – translations of one language for the widget's
* language switcher. Public data, no nonce; cached by the browser (the URL carries a
* content version). admin-ajax instead of REST: security plugins often block the REST API
* for visitors.
*/
public function ajax_translations() {
$lang = isset( $_GET['lang'] ) ? sanitize_text_field( wp_unslash( $_GET['lang'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
$settings = Welcoma_Settings::get();
if ( empty( $settings['widget_enabled'] ) || ! in_array( $lang, Welcoma_Translator::all_lang_codes(), true ) ) {
wp_send_json_error( null, 404 );
}
$langs = $this->with_base_lang( array( $lang ) );
$data = $this->build_translations( $this->active_modules( $settings ), $langs );
// admin-ajax sends no-cache headers, and wp_send_json() → wp_die() sends them again while
// the output is still buffered – so a plain header() call gets overwritten. The filter
// covers both calls. The URL carries a content version, so caching is safe.
$cache_headers = static function () {
return array(
'Cache-Control' => 'public, max-age=86400',
'Expires' => gmdate( 'D, d M Y H:i:s', time() + DAY_IN_SECONDS ) . ' GMT',
);
};
add_filter( 'nocache_headers', $cache_headers, PHP_INT_MAX );
foreach ( $cache_headers() as $name => $value ) {
header( $name . ': ' . $value );
}
wp_send_json(
array(
'langs' => $langs,
'module_translations' => $data['module_translations'],
'static_ui' => $data['static_ui'],
'statement' => $data['statement'],
)
);
}
}