| 1 |
<?php |
| 2 |
|
| 3 |
namespace WPDeveloper\BetterDocs\Core; |
| 4 |
|
| 5 |
/** |
| 6 |
* StoreFaqContent — deterministic, store-aware Product FAQ generator. |
| 7 |
* |
| 8 |
* Single source of truth for the WooCommerce "sample Product FAQs" starter set: |
| 9 |
* four groups (Payments & Billing, Shipping & Delivery, Returns & Refunds, |
| 10 |
* Orders & Account) of three questions each, curated to cover the highest |
| 11 |
* friction pre-/post-purchase buyer questions. |
| 12 |
* |
| 13 |
* It serves two callers: |
| 14 |
* - SampleDocs::suggested_categories() uses definition() for the chips/titles |
| 15 |
* shown on the "detected profile" screen. |
| 16 |
* - SampleDocs::generate() uses generate() as the hybrid fallback when the AI |
| 17 |
* proxy is unavailable (offline / quota / no model), producing real answers |
| 18 |
* grounded in the site's actual store config (payment gateways, currency, |
| 19 |
* tax, shipping regions, return window, policy pages) — no AI required. |
| 20 |
* |
| 21 |
* Answers degrade gracefully: when a datum is missing, the copy falls back to a |
| 22 |
* generic-but-accurate sentence rather than inventing specifics. |
| 23 |
* |
| 24 |
* @since 4.5.3 |
| 25 |
*/ |
| 26 |
class StoreFaqContent { |
| 27 |
/** |
| 28 |
* The curated group + question structure. Titles are static; answers are |
| 29 |
* filled per-site in generate(). |
| 30 |
* |
| 31 |
* @return array |
| 32 |
*/ |
| 33 |
public function definition() { |
| 34 |
return [ |
| 35 |
[ |
| 36 |
'name' => __( 'Payments & Billing', 'betterdocs' ), |
| 37 |
'slug' => 'payments-billing', |
| 38 |
'icon' => 'help', |
| 39 |
'description' => __( 'How customers can pay and what to expect when checking out.', 'betterdocs' ), |
| 40 |
'questions' => [ |
| 41 |
'payment_methods' => __( 'What payment methods do you accept?', 'betterdocs' ), |
| 42 |
'payment_security' => __( 'Is it safe to enter my card details at checkout?', 'betterdocs' ), |
| 43 |
'currency_tax' => __( 'What currency are prices in, and will I be charged tax?', 'betterdocs' ), |
| 44 |
], |
| 45 |
], |
| 46 |
[ |
| 47 |
'name' => __( 'Shipping & Delivery', 'betterdocs' ), |
| 48 |
'slug' => 'shipping-delivery', |
| 49 |
'icon' => 'truck', |
| 50 |
'description' => __( 'Where you ship, how long it takes, and how to track an order.', 'betterdocs' ), |
| 51 |
'questions' => [ |
| 52 |
'shipping_coverage' => __( 'Where do you ship and how long does delivery take?', 'betterdocs' ), |
| 53 |
'shipping_cost' => __( 'How much does shipping cost?', 'betterdocs' ), |
| 54 |
'order_tracking' => __( 'How can I track my order?', 'betterdocs' ), |
| 55 |
], |
| 56 |
], |
| 57 |
[ |
| 58 |
'name' => __( 'Returns & Refunds', 'betterdocs' ), |
| 59 |
'slug' => 'returns-refunds', |
| 60 |
'icon' => 'refund', |
| 61 |
'description' => __( 'Your return window, how to request a refund, and what to expect.', 'betterdocs' ), |
| 62 |
'questions' => [ |
| 63 |
'return_policy' => __( 'What is your return & refund policy?', 'betterdocs' ), |
| 64 |
'return_how' => __( 'How do I request a return or refund?', 'betterdocs' ), |
| 65 |
'refund_timing' => __( 'How long do refunds take, and who pays return shipping?', 'betterdocs' ), |
| 66 |
], |
| 67 |
], |
| 68 |
[ |
| 69 |
'name' => __( 'Orders & Account', 'betterdocs' ), |
| 70 |
'slug' => 'orders-account', |
| 71 |
'icon' => 'book', |
| 72 |
'description' => __( 'Placing, changing, and tracking orders, and whether an account is needed.', 'betterdocs' ), |
| 73 |
'questions' => [ |
| 74 |
'place_order' => __( 'How do I place an order?', 'betterdocs' ), |
| 75 |
'modify_cancel' => __( 'Can I change or cancel my order after checkout?', 'betterdocs' ), |
| 76 |
'account_status' => __( 'Do I need an account to buy, and how do I check order status?', 'betterdocs' ), |
| 77 |
], |
| 78 |
], |
| 79 |
]; |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Build the full categories array (proxy-shaped) with real store answers. |
| 84 |
* |
| 85 |
* @param array $profile The SiteProfiler profile (uses ['woocommerce'] + ['site']). |
| 86 |
* @return array [{ name, slug, description, articles:[{ title, content_html, excerpt }] }] |
| 87 |
*/ |
| 88 |
public function generate( array $profile ) { |
| 89 |
$woo = isset( $profile['woocommerce'] ) && is_array( $profile['woocommerce'] ) ? $profile['woocommerce'] : []; |
| 90 |
$site = isset( $profile['site'] ) && is_array( $profile['site'] ) ? $profile['site'] : []; |
| 91 |
|
| 92 |
$categories = []; |
| 93 |
foreach ( $this->definition() as $group ) { |
| 94 |
$articles = []; |
| 95 |
foreach ( $group['questions'] as $id => $title ) { |
| 96 |
$html = $this->answer_html( $id, $woo, $site ); |
| 97 |
$articles[] = [ |
| 98 |
'title' => $title, |
| 99 |
'content_html' => $html, |
| 100 |
'excerpt' => $this->excerpt( $html ), |
| 101 |
]; |
| 102 |
} |
| 103 |
|
| 104 |
$categories[] = [ |
| 105 |
'name' => $group['name'], |
| 106 |
'slug' => $group['slug'], |
| 107 |
'description' => $group['description'], |
| 108 |
'articles' => $articles, |
| 109 |
]; |
| 110 |
} |
| 111 |
|
| 112 |
return $categories; |
| 113 |
} |
| 114 |
|
| 115 |
/* --------------------------------------------------------------------- */ |
| 116 |
/* Answer builders */ |
| 117 |
/* --------------------------------------------------------------------- */ |
| 118 |
|
| 119 |
/** |
| 120 |
* @return string Safe HTML (paragraphs/lists/links) for one answer. |
| 121 |
*/ |
| 122 |
protected function answer_html( $id, array $woo, array $site ) { |
| 123 |
switch ( $id ) { |
| 124 |
case 'payment_methods': |
| 125 |
return $this->answer_payment_methods( $woo ); |
| 126 |
case 'payment_security': |
| 127 |
return $this->answer_payment_security( $site, $woo ); |
| 128 |
case 'currency_tax': |
| 129 |
return $this->answer_currency_tax( $woo ); |
| 130 |
case 'shipping_coverage': |
| 131 |
return $this->answer_shipping_coverage( $woo ); |
| 132 |
case 'shipping_cost': |
| 133 |
return $this->answer_shipping_cost( $woo ); |
| 134 |
case 'order_tracking': |
| 135 |
return $this->answer_order_tracking( $woo ); |
| 136 |
case 'return_policy': |
| 137 |
return $this->answer_return_policy( $woo ); |
| 138 |
case 'return_how': |
| 139 |
return $this->answer_return_how( $woo ); |
| 140 |
case 'refund_timing': |
| 141 |
return $this->answer_refund_timing( $woo ); |
| 142 |
case 'place_order': |
| 143 |
return $this->answer_place_order( $woo ); |
| 144 |
case 'modify_cancel': |
| 145 |
return $this->answer_modify_cancel(); |
| 146 |
case 'account_status': |
| 147 |
return $this->answer_account_status( $woo ); |
| 148 |
} |
| 149 |
|
| 150 |
return ''; |
| 151 |
} |
| 152 |
|
| 153 |
protected function answer_payment_methods( array $woo ) { |
| 154 |
$methods = isset( $woo['payment_methods'] ) ? (array) $woo['payment_methods'] : []; |
| 155 |
|
| 156 |
if ( ! empty( $methods ) ) { |
| 157 |
$intro = sprintf( |
| 158 |
/* translators: %s: list of payment method names. */ |
| 159 |
__( 'We accept %s.', 'betterdocs' ), |
| 160 |
$this->human_list( $methods ) |
| 161 |
); |
| 162 |
} else { |
| 163 |
$intro = __( 'We accept all major payment methods, including popular credit and debit cards and digital wallets.', 'betterdocs' ); |
| 164 |
} |
| 165 |
|
| 166 |
$second = __( 'You can review every available option on the checkout page before you place your order, and all payments are processed securely.', 'betterdocs' ); |
| 167 |
|
| 168 |
return $this->p( $intro ) . $this->p( $second ); |
| 169 |
} |
| 170 |
|
| 171 |
protected function answer_payment_security( array $site, array $woo = [] ) { |
| 172 |
$name = isset( $site['title'] ) && '' !== $site['title'] ? $site['title'] : __( 'our store', 'betterdocs' ); |
| 173 |
|
| 174 |
$first = __( 'Yes. Your payment is processed over a secure, encrypted (SSL) connection.', 'betterdocs' ); |
| 175 |
$second = sprintf( |
| 176 |
/* translators: %s: site/store name. */ |
| 177 |
__( 'Card details are handled by our trusted payment provider — %s never stores your full card number on its own servers.', 'betterdocs' ), |
| 178 |
$name |
| 179 |
); |
| 180 |
|
| 181 |
$privacy = isset( $woo['privacy_page_url'] ) ? (string) $woo['privacy_page_url'] : ''; |
| 182 |
$third = ''; |
| 183 |
if ( '' !== $privacy ) { |
| 184 |
$third = $this->p( |
| 185 |
sprintf( |
| 186 |
/* translators: %s: link to the store's privacy policy page. */ |
| 187 |
__( 'See our %s for how your information is collected and used.', 'betterdocs' ), |
| 188 |
'<a href="' . esc_url( $privacy ) . '">' . esc_html__( 'privacy policy', 'betterdocs' ) . '</a>' |
| 189 |
) |
| 190 |
); |
| 191 |
} |
| 192 |
|
| 193 |
return $this->p( $first ) . $this->p( $second ) . $third; |
| 194 |
} |
| 195 |
|
| 196 |
protected function answer_currency_tax( array $woo ) { |
| 197 |
$currency = isset( $woo['currency'] ) ? (string) $woo['currency'] : ''; |
| 198 |
$symbol = isset( $woo['currency_symbol'] ) ? (string) $woo['currency_symbol'] : ''; |
| 199 |
|
| 200 |
if ( '' !== $currency ) { |
| 201 |
$label = '' !== $symbol ? sprintf( '%s (%s)', $currency, $symbol ) : $currency; |
| 202 |
$first = sprintf( |
| 203 |
/* translators: %s: currency code (and symbol). */ |
| 204 |
__( 'All prices on our store are shown in %s.', 'betterdocs' ), |
| 205 |
$label |
| 206 |
); |
| 207 |
} else { |
| 208 |
$first = __( 'All prices are shown in our store currency, displayed throughout checkout.', 'betterdocs' ); |
| 209 |
} |
| 210 |
|
| 211 |
if ( ! empty( $woo['tax_enabled'] ) ) { |
| 212 |
$second = ! empty( $woo['prices_include_tax'] ) |
| 213 |
? __( 'Applicable taxes are already included in the price you see.', 'betterdocs' ) |
| 214 |
: __( 'Any applicable taxes are calculated based on your location and shown at checkout before you pay.', 'betterdocs' ); |
| 215 |
} else { |
| 216 |
$second = __( 'The total you see at checkout is the final amount you pay.', 'betterdocs' ); |
| 217 |
} |
| 218 |
|
| 219 |
return $this->p( $first ) . $this->p( $second ); |
| 220 |
} |
| 221 |
|
| 222 |
protected function answer_shipping_coverage( array $woo ) { |
| 223 |
$regions = isset( $woo['shipping_regions'] ) ? (array) $woo['shipping_regions'] : []; |
| 224 |
$location = isset( $woo['store_location'] ) ? (string) $woo['store_location'] : ''; |
| 225 |
|
| 226 |
if ( ! empty( $regions ) ) { |
| 227 |
$first = sprintf( |
| 228 |
/* translators: %s: list of shipping region names. */ |
| 229 |
__( 'We currently ship to %s.', 'betterdocs' ), |
| 230 |
$this->human_list( $regions ) |
| 231 |
); |
| 232 |
} else { |
| 233 |
$first = __( 'We ship to all the destinations available at checkout — enter your address to see the options for your area.', 'betterdocs' ); |
| 234 |
} |
| 235 |
|
| 236 |
$second = __( 'Most orders are processed within 1–2 business days. Delivery time then depends on your destination and the shipping method you choose at checkout.', 'betterdocs' ); |
| 237 |
|
| 238 |
if ( '' !== $location ) { |
| 239 |
$second .= ' ' . sprintf( |
| 240 |
/* translators: %s: store base location. */ |
| 241 |
__( 'Orders ship from %s.', 'betterdocs' ), |
| 242 |
$location |
| 243 |
); |
| 244 |
} |
| 245 |
|
| 246 |
return $this->p( $first ) . $this->p( $second ); |
| 247 |
} |
| 248 |
|
| 249 |
protected function answer_shipping_cost( array $woo = [] ) { |
| 250 |
$first = __( 'Shipping cost is calculated automatically at checkout based on your delivery address and the method you select, so you always see the exact amount before you pay.', 'betterdocs' ); |
| 251 |
|
| 252 |
$free_min = isset( $woo['free_shipping_min'] ) ? (float) $woo['free_shipping_min'] : 0; |
| 253 |
if ( $free_min > 0 ) { |
| 254 |
$first .= ' ' . sprintf( |
| 255 |
/* translators: %s: formatted minimum order amount for free shipping. */ |
| 256 |
__( 'Orders over %s qualify for free shipping.', 'betterdocs' ), |
| 257 |
$this->format_amount( $free_min, $woo ) |
| 258 |
); |
| 259 |
} |
| 260 |
|
| 261 |
return $this->p( $first ) |
| 262 |
. $this->p( __( 'Add your items to the cart and enter your address to view the available shipping rates for your order.', 'betterdocs' ) ); |
| 263 |
} |
| 264 |
|
| 265 |
protected function answer_order_tracking( array $woo = [] ) { |
| 266 |
$first = __( 'As soon as your order ships, we email you a confirmation with the details you need to follow its progress.', 'betterdocs' ); |
| 267 |
$account = isset( $woo['account_page_url'] ) ? (string) $woo['account_page_url'] : ''; |
| 268 |
|
| 269 |
if ( '' !== $account ) { |
| 270 |
$second = sprintf( |
| 271 |
/* translators: %s: link to the customer's account orders page. */ |
| 272 |
__( 'If you have an account, you can also track every order any time from your %s.', 'betterdocs' ), |
| 273 |
'<a href="' . esc_url( $account ) . '">' . esc_html__( 'account\'s Orders page', 'betterdocs' ) . '</a>' |
| 274 |
); |
| 275 |
} else { |
| 276 |
$second = __( 'If you have an account, you can also see the current status of every order any time from your account\'s Orders page.', 'betterdocs' ); |
| 277 |
} |
| 278 |
|
| 279 |
return $this->p( $first ) . $this->p( $second ); |
| 280 |
} |
| 281 |
|
| 282 |
protected function answer_return_policy( array $woo ) { |
| 283 |
$window = isset( $woo['return_window'] ) ? (int) $woo['return_window'] : 0; |
| 284 |
|
| 285 |
if ( $window > 0 ) { |
| 286 |
$first = sprintf( |
| 287 |
/* translators: %d: number of days in the return window. */ |
| 288 |
_n( |
| 289 |
'You can request a return or refund within %d day of receiving your order, as long as the item is unused and in its original condition.', |
| 290 |
'You can request a return or refund within %d days of receiving your order, as long as the item is unused and in its original condition.', |
| 291 |
$window, |
| 292 |
'betterdocs' |
| 293 |
), |
| 294 |
$window |
| 295 |
); |
| 296 |
} else { |
| 297 |
$first = __( 'If you are not completely happy with your purchase, you can request a return or refund — just get in touch and we will make it right.', 'betterdocs' ); |
| 298 |
} |
| 299 |
|
| 300 |
return $this->p( $first ) . $this->policy_line( $woo ); |
| 301 |
} |
| 302 |
|
| 303 |
protected function answer_return_how( array $woo = [] ) { |
| 304 |
$steps = '<ol>' |
| 305 |
. '<li>' . esc_html__( 'Contact us with your order number and the item you would like to return.', 'betterdocs' ) . '</li>' |
| 306 |
. '<li>' . esc_html__( 'We confirm eligibility and send you return instructions.', 'betterdocs' ) . '</li>' |
| 307 |
. '<li>' . esc_html__( 'Pack the item securely and ship it back to the address we provide.', 'betterdocs' ) . '</li>' |
| 308 |
. '<li>' . esc_html__( 'Once it arrives, we process your refund or exchange.', 'betterdocs' ) . '</li>' |
| 309 |
. '</ol>'; |
| 310 |
|
| 311 |
return $this->p( __( 'Requesting a return is simple:', 'betterdocs' ) ) . $steps . $this->policy_line( $woo ); |
| 312 |
} |
| 313 |
|
| 314 |
protected function answer_refund_timing( array $woo ) { |
| 315 |
$first = __( 'Approved refunds are issued to your original payment method, typically within 5–10 business days after we receive the returned item.', 'betterdocs' ); |
| 316 |
$second = __( 'Return shipping is the customer\'s responsibility unless the item arrived damaged, defective, or incorrect — in which case we cover it.', 'betterdocs' ); |
| 317 |
|
| 318 |
return $this->p( $first ) . $this->p( $second ) . $this->policy_line( $woo ); |
| 319 |
} |
| 320 |
|
| 321 |
protected function answer_place_order( array $woo = [] ) { |
| 322 |
$shop = isset( $woo['shop_page_url'] ) ? (string) $woo['shop_page_url'] : ''; |
| 323 |
$first = '' !== $shop |
| 324 |
? sprintf( |
| 325 |
/* translators: %s: link to the shop page. */ |
| 326 |
__( 'Browse %s, choose any options you need such as size or color, then click Add to Cart.', 'betterdocs' ), |
| 327 |
'<a href="' . esc_url( $shop ) . '">' . esc_html__( 'our products', 'betterdocs' ) . '</a>' |
| 328 |
) |
| 329 |
: __( 'Browse our products and choose any options you need, such as size or color, then click Add to Cart.', 'betterdocs' ); |
| 330 |
|
| 331 |
$checkout = isset( $woo['checkout_page_url'] ) ? (string) $woo['checkout_page_url'] : ''; |
| 332 |
$second = '' !== $checkout |
| 333 |
? sprintf( |
| 334 |
/* translators: %s: link to the checkout page. */ |
| 335 |
__( 'When you are ready, open your cart and proceed to %s, enter your shipping and payment details, and confirm your order. You will receive a confirmation email right away.', 'betterdocs' ), |
| 336 |
'<a href="' . esc_url( $checkout ) . '">' . esc_html__( 'checkout', 'betterdocs' ) . '</a>' |
| 337 |
) |
| 338 |
: __( 'When you are ready, open your cart and select Checkout, enter your shipping and payment details, and confirm your order. You will receive a confirmation email right away.', 'betterdocs' ); |
| 339 |
|
| 340 |
return $this->p( $first ) . $this->p( $second ); |
| 341 |
} |
| 342 |
|
| 343 |
protected function answer_modify_cancel() { |
| 344 |
return $this->p( __( 'If you need to change or cancel an order, please contact us as soon as possible and we will do our best to update it before it ships.', 'betterdocs' ) ) |
| 345 |
. $this->p( __( 'Once an order has shipped it can no longer be changed, but you can still return it under our return policy.', 'betterdocs' ) ); |
| 346 |
} |
| 347 |
|
| 348 |
protected function answer_account_status( array $woo = [] ) { |
| 349 |
$account = isset( $woo['account_page_url'] ) ? (string) $woo['account_page_url'] : ''; |
| 350 |
$second = '' !== $account |
| 351 |
? sprintf( |
| 352 |
/* translators: %s: link to the customer's account page. */ |
| 353 |
__( 'Creating an %s lets you track orders, save your addresses, and reorder faster. Either way, you will receive an email confirmation for every order and can reply to it any time with questions.', 'betterdocs' ), |
| 354 |
'<a href="' . esc_url( $account ) . '">' . esc_html__( 'account', 'betterdocs' ) . '</a>' |
| 355 |
) |
| 356 |
: __( 'Creating an account lets you track orders, save your addresses, and reorder faster. Either way, you will receive an email confirmation for every order and can reply to it any time with questions.', 'betterdocs' ); |
| 357 |
|
| 358 |
return $this->p( __( 'An account is not required — you are welcome to check out as a guest.', 'betterdocs' ) ) . $this->p( $second ); |
| 359 |
} |
| 360 |
|
| 361 |
/* --------------------------------------------------------------------- */ |
| 362 |
/* Helpers */ |
| 363 |
/* --------------------------------------------------------------------- */ |
| 364 |
|
| 365 |
/** |
| 366 |
* A trailing "see our policies" line, linked to the Terms page when known. |
| 367 |
* |
| 368 |
* @return string |
| 369 |
*/ |
| 370 |
protected function policy_line( array $woo ) { |
| 371 |
// Prefer the real refund/returns policy page; fall back to terms & conditions. |
| 372 |
$url = isset( $woo['refund_policy_url'] ) ? (string) $woo['refund_policy_url'] : ''; |
| 373 |
$label = __( 'refund & returns policy', 'betterdocs' ); |
| 374 |
|
| 375 |
if ( '' === $url ) { |
| 376 |
$url = isset( $woo['terms_page_url'] ) ? (string) $woo['terms_page_url'] : ''; |
| 377 |
$label = __( 'terms & policies', 'betterdocs' ); |
| 378 |
} |
| 379 |
|
| 380 |
if ( '' === $url ) { |
| 381 |
return ''; |
| 382 |
} |
| 383 |
|
| 384 |
return $this->p( |
| 385 |
sprintf( |
| 386 |
/* translators: %s: link to the store's refund/returns or terms policy page. */ |
| 387 |
__( 'For full details, please see our %s.', 'betterdocs' ), |
| 388 |
'<a href="' . esc_url( $url ) . '">' . esc_html( $label ) . '</a>' |
| 389 |
) |
| 390 |
); |
| 391 |
} |
| 392 |
|
| 393 |
/** |
| 394 |
* Format a money amount using the store's currency symbol when known. |
| 395 |
* |
| 396 |
* @return string |
| 397 |
*/ |
| 398 |
protected function format_amount( $amount, array $woo ) { |
| 399 |
$amount = (float) $amount; |
| 400 |
// Trim a trailing ".00" for whole amounts, keep cents otherwise. |
| 401 |
$number = ( floor( $amount ) === $amount ) ? number_format( $amount ) : number_format( $amount, 2 ); |
| 402 |
$symbol = isset( $woo['currency_symbol'] ) ? (string) $woo['currency_symbol'] : ''; |
| 403 |
|
| 404 |
if ( '' !== $symbol ) { |
| 405 |
return $symbol . $number; |
| 406 |
} |
| 407 |
|
| 408 |
$code = isset( $woo['currency'] ) ? (string) $woo['currency'] : ''; |
| 409 |
return '' !== $code ? $number . ' ' . $code : $number; |
| 410 |
} |
| 411 |
|
| 412 |
/** |
| 413 |
* Wrap text in a paragraph. Text is escaped; callers that embed a link build |
| 414 |
* the HTML themselves and are wp_kses_post-sanitized downstream. |
| 415 |
* |
| 416 |
* @return string |
| 417 |
*/ |
| 418 |
protected function p( $text ) { |
| 419 |
// Allow the few inline tags our answers use (links); escape the rest. |
| 420 |
return '<p>' . wp_kses( $text, [ 'a' => [ 'href' => [], 'title' => [] ] ] ) . '</p>'; |
| 421 |
} |
| 422 |
|
| 423 |
/** |
| 424 |
* Natural-language list join: "A", "A and B", "A, B, and C". |
| 425 |
* |
| 426 |
* @return string |
| 427 |
*/ |
| 428 |
protected function human_list( array $items ) { |
| 429 |
$items = array_values( array_filter( array_map( 'trim', array_map( 'strval', $items ) ) ) ); |
| 430 |
$count = count( $items ); |
| 431 |
|
| 432 |
if ( 0 === $count ) { |
| 433 |
return ''; |
| 434 |
} |
| 435 |
if ( 1 === $count ) { |
| 436 |
return $items[0]; |
| 437 |
} |
| 438 |
if ( 2 === $count ) { |
| 439 |
/* translators: 1: first item, 2: second item. */ |
| 440 |
return sprintf( __( '%1$s and %2$s', 'betterdocs' ), $items[0], $items[1] ); |
| 441 |
} |
| 442 |
|
| 443 |
$last = array_pop( $items ); |
| 444 |
/* translators: 1: comma-separated list of items, 2: final item. */ |
| 445 |
return sprintf( __( '%1$s, and %2$s', 'betterdocs' ), implode( ', ', $items ), $last ); |
| 446 |
} |
| 447 |
|
| 448 |
/** |
| 449 |
* Short plain-text excerpt from an answer's HTML. |
| 450 |
* |
| 451 |
* @return string |
| 452 |
*/ |
| 453 |
protected function excerpt( $html ) { |
| 454 |
return wp_trim_words( wp_strip_all_tags( $html ), 22, '…' ); |
| 455 |
} |
| 456 |
} |
| 457 |
|