" — so it carries no leading * capital and no trailing period. */ public static function describe(array $tpl): string { if (($tpl['kind'] ?? '') === 'shell') { return __('every page on the site (header and footer)', 'aumframe'); } $rules = json_decode((string) ($tpl['rules'] ?? ''), true); if (!is_array($rules) || !$rules) { /* Empty rules match nothing — see Router::rules_match(). Saying "nothing" is the honest answer. */ return __('nothing yet — it has no routing rules', 'aumframe'); } switch ($rules['kind'] ?? '') { case 'front_page': return __('the homepage', 'aumframe'); case 'any': return __('every page on the site', 'aumframe'); case 'search': return __('the search results page', 'aumframe'); case 'notfound': return __('the 404 page', 'aumframe'); case 'singular': $ids = self::stated_ids($rules, 'post_ids'); if ($ids !== null) { return $ids ? self::name_list($ids) : self::matches_nothing(); } return sprintf( /* translators: %s: a post type's singular name, e.g. "Post". */ __('every single %s', 'aumframe'), self::type_label($rules['post_type'] ?? 'post', true) ); case 'archive': $terms = self::stated_ids($rules, 'term_ids'); if ($terms !== null) { return $terms ? self::term_list($terms) : self::matches_nothing(); } if (!empty($rules['taxonomy'])) { $tax = get_taxonomy($rules['taxonomy']); return sprintf( /* translators: %s: a taxonomy's name, e.g. "Category". */ __('every %s archive', 'aumframe'), $tax ? $tax->labels->singular_name : $rules['taxonomy'] ); } return sprintf( /* translators: %s: a post type's plural name, e.g. "Products". */ __('the %s archive', 'aumframe'), self::type_label($rules['post_type'] ?? 'post') ); } /* * Never return an empty string. * * These templates are generated by AI from the spec, so a rule shape nobody anticipated is a * question of when, not if — and the failure mode of returning '' is that the scope line simply * vanishes from the row, which reads as "this one is safe to edit". A vague sentence that is * still there beats a confident silence. */ return __('whatever its routing rules match — the rule uses a shape this screen cannot describe yet', 'aumframe'); } /** * True when opening this template edits more than the one thing the buyer clicked from. * * A shell is deliberately included: it is the widest scope there is. */ public static function is_shared(array $tpl): bool { if (($tpl['kind'] ?? '') === 'shell') { return true; } $rules = json_decode((string) ($tpl['rules'] ?? ''), true); if (!is_array($rules) || !$rules) { return false; } if (($rules['kind'] ?? '') === 'singular') { $ids = self::stated_ids($rules, 'post_ids'); if ($ids !== null) { /* Empty means it renders nothing at all, so there is nothing to warn about. */ return count($ids) > 1; } /* No post_ids key = every post of the type. */ return true; } if (($rules['kind'] ?? '') === 'archive') { $terms = self::stated_ids($rules, 'term_ids'); if ($terms !== null) { return count($terms) > 1; } return true; } /* front_page is one page today, but it follows the setting rather than a post — still not private. */ return ($rules['kind'] ?? '') !== 'front_page'; } /** "Cart, Checkout and My account" — or "Cart, Checkout and 4 others" once the list gets long. */ private static function name_list(array $ids): string { $titles = []; foreach ($ids as $id) { /* * `get_the_title()` returns the title with entities still encoded, so an ampersand arrives as * `&` and would be shown literally. Decode here; every call site escapes on output. */ $t = wp_specialchars_decode(get_the_title($id), ENT_QUOTES); if ($t !== '') { $titles[] = $t; } } if (!$titles) { return __('a page that no longer exists', 'aumframe'); } return self::join($titles); } /** * "Cart, Checkout and My account" — or "Cart, Checkout, Shop and 4 others" once the list runs long. * * Shared by pages and terms: one place that knows how a list of names is written, so the two cannot * drift into two different house styles. */ private static function join(array $items): string { if (count($items) === 1) { return $items[0]; } $extra = count($items) - self::NAME_LIMIT; if ($extra > 0) { $items = array_slice($items, 0, self::NAME_LIMIT); $items[] = sprintf( /* translators: %d: how many further items are not named. */ _n('%d other', '%d others', $extra, 'aumframe'), $extra ); } $last = array_pop($items); return sprintf( /* translators: 1: a comma-separated list of names, 2: the last name. */ __('%1$s and %2$s', 'aumframe'), implode(', ', $items), $last ); } /** * `$singular` matters grammatically: "every single Product" reads, "every single Products" does not, * while "the Products archive" needs the plural. One flag rather than two call sites getting it wrong. */ private static function matches_nothing(): string { return __('nothing — its rule is pinned to an empty list, so no page uses it', 'aumframe'); } /** "the E-bikes and Accessories archives" */ private static function term_list(array $ids): string { $names = []; foreach ($ids as $id) { $term = get_term($id); if ($term && !is_wp_error($term)) { $names[] = wp_specialchars_decode($term->name, ENT_QUOTES); } } if (!$names) { return __('a term archive that no longer exists', 'aumframe'); } return sprintf( /* translators: %s: one or more term names, e.g. "E-bikes and Accessories". */ __('the %s archive', 'aumframe'), self::join($names) ); } private static function type_label(string $post_type, bool $singular = false): string { $obj = get_post_type_object($post_type); if (!$obj) { return $post_type; } return $singular ? $obj->labels->singular_name : $obj->labels->name; } }