Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Widget types: carry the declarative widget.json metadata (category, title, description, help, keywords) from the build manifest through the registry into the widget-modules REST record, translated and sanitized at registration.
51 changes: 51 additions & 0 deletions projects/packages/premium-analytics/src/class-widget-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,57 @@ class Widget_Type {
*/
public $presentation = null;

/**
* Widget types are grouped into categories to help users browse and
* discover them. Static and declarative; not a user-editable attribute.
*
* Null when the widget did not declare the field.
*
* @var string|null
*/
public $category = null;

/**
* Human-readable title that names the widget type. Translated
* at registration time using the widget's text domain.
*
* Null when the widget did not declare the field.
*
* @var string|null
*/
public $title = null;

/**
* Human-readable description of what the widget type does.
* Translated at registration time using the widget's text domain.
*
* Null when the widget did not declare the field.
*
* @var string|null
*/
public $description = null;

/**
* Contextual help note: `content` plus optional `links`.
* Translated at registration time using the widget's text domain.
*
* Null when the widget did not declare the field.
*
* @var array|null
*/
public $help = null;

/**
* Alternative terms used to match the widget type when searching,
* e.g. "calendar" for an events widget. Translated at registration
* time using the widget's text domain.
*
* Null when the widget did not declare the field.
*
* @var string[]|null
*/
public $keywords = null;

/**
* Constructor.
*
Expand Down
9 changes: 9 additions & 0 deletions projects/packages/premium-analytics/src/widget-i18n.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"title": "widget title",
"description": "widget description",
"help": {
"content": "widget help content",
"links": [ { "label": "widget help link label" } ]
},
"keywords": [ "widget keyword" ]
}
5 changes: 5 additions & 0 deletions projects/packages/premium-analytics/src/widget-modules.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ function get_widget_modules_response() {
'render_module' => $widget_type->render_module,
'widget_module' => $widget_type->widget_module,
'presentation' => $widget_type->presentation,
'category' => $widget_type->category,
'title' => $widget_type->title,
'description' => $widget_type->description,
'help' => $widget_type->help,
'keywords' => $widget_type->keywords,
);
}

Expand Down
104 changes: 102 additions & 2 deletions projects/packages/premium-analytics/src/widget-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*
* Copies the wp-build manifest (`jpa_get_registered_widget_modules()`) into the
* in-memory Widget_Type_Registry, so the plugin queries the registry instead
* of re-parsing the manifest.
* of re-parsing the manifest. On the way in, user-facing metadata strings are
* translated (per the widget-i18n.json schema) and the `help` note sanitized.
*
* This is the problem-agnostic "core" layer (a PA-namespaced copy of the
* experimental Gutenberg API): it exposes the hooks a consumer uses to scope
Expand Down Expand Up @@ -33,6 +34,97 @@
*/
const WIDGET_TYPES_FILTER = 'jetpack_premium_analytics_widget_types';

/**
* Returns the i18n schema describing which widget metadata fields are
* translatable and the gettext context to use for each.
*
* Read once from widget-i18n.json and memoized for the rest of the request.
* Decoded as objects, not associative arrays: that is how
* `translate_settings_using_i18n_schema()` tells keyed maps apart from lists.
*
* @return object Map of translatable field name to gettext context.
*/
function get_widget_metadata_i18n_schema() {
static $i18n_schema = null;

if ( null === $i18n_schema ) {
$schema = wp_json_file_decode( __DIR__ . '/widget-i18n.json' );
$i18n_schema = is_object( $schema ) ? $schema : new \stdClass();
}

return $i18n_schema;
}

/**
* Translates a widget's user-facing metadata strings.
*
* Runs `title`, `description`, `help`, and `keywords` through the widget
* i18n schema, leaving every other key untouched. Unlike the upstream copy,
* a widget with no `textdomain` falls back to the package text domain
* instead of skipping translation: every bundled widget shares it.
*
* @param array $widget Widget data from the build manifest.
* @return array Widget data with its translatable strings localized.
*/
function translate_widget_metadata( $widget ) {
$textdomain = ! empty( $widget['textdomain'] ) ? $widget['textdomain'] : 'jetpack-premium-analytics';
$i18n_schema = get_widget_metadata_i18n_schema();

foreach ( array( 'title', 'description', 'help', 'keywords' ) as $field ) {
if ( isset( $widget[ $field ] ) && isset( $i18n_schema->$field ) ) {
$widget[ $field ] = translate_settings_using_i18n_schema( $i18n_schema->$field, $widget[ $field ], $textdomain );
}
}

return $widget;
}

/**
* Constrains a widget help note to its allowed shape: `content` keeps
* only `em`/`strong` markup, and links are dropped unless they carry a
* `label` and an `href` that survives `esc_url_raw()`.
*
* @param array|null $help Help note from the build manifest.
* @return array|null Sanitized help note, or null when there is no content.
*/
function sanitize_widget_help( $help ) {
if ( ! is_array( $help ) || empty( $help['content'] ) || ! is_string( $help['content'] ) ) {
return null;
}

$sanitized = array(
'content' => wp_kses(
$help['content'],
array(
'em' => array(),
'strong' => array(),
)
),
);

if ( ! empty( $help['links'] ) && is_array( $help['links'] ) ) {
$links = array();
foreach ( $help['links'] as $link ) {
if ( is_array( $link ) && ! empty( $link['label'] ) && ! empty( $link['href'] ) ) {
$href = esc_url_raw( $link['href'] );

if ( $href ) {
$links[] = array(
'label' => $link['label'],
'href' => $href,
);
}
}
}

if ( $links ) {
$sanitized['links'] = $links;
}
}

return $sanitized;
}

/**
* Hydrates the widget type registry from the build manifest.
*
Expand All @@ -48,7 +140,8 @@ function register_widget_types() {

$registry = Widget_Type_Registry::get_instance();

// @phan-suppress-next-line PhanUndeclaredFunction -- Generated by wp-build into build/widgets.php, outside Phan's analysis scope. The function_exists() guard above protects the call at runtime.
// Generated by wp-build into build/widgets.php, outside Phan's analysis scope.
// The function_exists() guard above protects the call at runtime.
$jetpack_widget_modules = jpa_get_registered_widget_modules();

/**
Expand All @@ -68,12 +161,19 @@ function register_widget_types() {
continue;
}

$widget = translate_widget_metadata( $widget );

$registry->register(
$widget['name'],
array(
'render_module' => $widget['render_module'] ?? null,
'widget_module' => $widget['widget_module'] ?? null,
'presentation' => $widget['presentation'] ?? null,
'category' => $widget['category'] ?? null,
'title' => $widget['title'] ?? null,
'description' => $widget['description'] ?? null,
'help' => sanitize_widget_help( $widget['help'] ?? null ),
'keywords' => $widget['keywords'] ?? null,
)
);
}
Expand Down
Loading