import_from_file( $file ); } } $translated = self::$ar_mo->translate( $text ); if ( $translated !== $text ) { return $translated; } } // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- every caller passes a literal string wrapped for extraction via self::t(). return __( $text, 'adworksbid-seo' ); } // ── Option lists ───────────────────────────────────────────────────────── public static function currencies() { return [ 'USD' => self::t( 'US Dollar (USD)' ), 'EUR' => self::t( 'Euro (EUR)' ), 'GBP' => self::t( 'British Pound (GBP)' ), 'SAR' => self::t( 'Saudi Riyal (SAR)' ), 'AED' => self::t( 'UAE Dirham (AED)' ), 'KWD' => self::t( 'Kuwaiti Dinar (KWD)' ), 'QAR' => self::t( 'Qatari Riyal (QAR)' ), 'BHD' => self::t( 'Bahraini Dinar (BHD)' ), 'OMR' => self::t( 'Omani Rial (OMR)' ), 'LBP' => self::t( 'Lebanese Pound (LBP)' ), 'EGP' => self::t( 'Egyptian Pound (EGP)' ), 'JOD' => self::t( 'Jordanian Dinar (JOD)' ), 'IQD' => self::t( 'Iraqi Dinar (IQD)' ), 'MAD' => self::t( 'Moroccan Dirham (MAD)' ), ]; } /** schema.org @type => group of fields it uses. Types not listed keep the generic builder. */ public static function type_groups() { $map = [ 'Event' => 'event', 'BusinessEvent' => 'event', 'MusicEvent' => 'event', 'SportsEvent' => 'event', 'FoodEvent' => 'event', 'EducationEvent' => 'event', 'OnlineEvent' => 'event', 'Course' => 'course', 'Review' => 'review', 'VideoObject' => 'video', 'Organization' => 'organization', 'NGO' => 'organization', 'Corporation' => 'organization', 'EducationalOrganization' => 'organization', 'GovernmentOrganization' => 'organization', 'Person' => 'person', 'SoftwareApplication' => 'software', 'MobileApplication' => 'software', 'WebApplication' => 'software', 'Book' => 'book', 'Service' => 'service', 'Dataset' => 'dataset', 'Product' => 'product', 'JobPosting' => 'job', 'FAQPage' => 'faq', 'HowTo' => 'howto', 'Recipe' => 'recipe', ]; foreach ( array_keys( AWBSEO_Local_SEO::business_types() ) as $lb ) { $map[ $lb ] = 'localbusiness'; } return $map; } /** Types with a real builder - shown with a "Rich-result ready" badge on the Schema page. */ public static function supported_types() { return array_merge( array_keys( self::type_groups() ), [ 'Article', 'NewsArticle', 'BlogPosting', 'TechArticle', 'WebPage' ] ); } // ── Field definitions ──────────────────────────────────────────────────── /** * Field shape: key, label, type (text|textarea|url|number|date|datetime| * select|image|list|checkbox|location|repeater), optional required (bool), * required_when ([field, [values]]), required_unless ('featured' - satisfied * by the post's featured image), options, placeholder, help, fields (repeater). */ public static function definitions() { $currency = [ 'key' => 'currency', 'label' => self::t( 'Currency' ), 'type' => 'select', 'options' => self::currencies() ]; $address = [ [ 'key' => 'street', 'label' => self::t( 'Street address' ), 'type' => 'text' ], [ 'key' => 'city', 'label' => self::t( 'City' ), 'type' => 'text' ], [ 'key' => 'region', 'label' => self::t( 'State / Region' ), 'type' => 'text' ], [ 'key' => 'zip', 'label' => self::t( 'Postal code' ), 'type' => 'text' ], [ 'key' => 'country', 'label' => self::t( 'Country code' ), 'type' => 'text', 'placeholder' => 'LB, AE, SA...' ], ]; $rating = [ [ 'key' => 'rating_value', 'label' => self::t( 'Average rating' ), 'type' => 'number', 'placeholder' => '4.7' ], [ 'key' => 'rating_count', 'label' => self::t( 'Number of ratings' ), 'type' => 'number', 'placeholder' => '128' ], ]; return [ 'event' => [ 'intro' => self::t( 'Google shows events with a date, place, and ticket info right in search results.' ), 'fields' => array_merge( [ [ 'key' => 'start_date', 'label' => self::t( 'Starts' ), 'type' => 'datetime', 'required' => true ], [ 'key' => 'end_date', 'label' => self::t( 'Ends' ), 'type' => 'datetime' ], [ 'key' => 'attendance_mode', 'label' => self::t( 'Where does it happen?' ), 'type' => 'select', 'options' => [ 'offline' => self::t( 'In person' ), 'online' => self::t( 'Online' ), 'mixed' => self::t( 'Both in person and online' ), ] ], [ 'key' => 'status', 'label' => self::t( 'Status' ), 'type' => 'select', 'options' => [ 'EventScheduled' => self::t( 'Scheduled' ), 'EventRescheduled' => self::t( 'Rescheduled' ), 'EventPostponed' => self::t( 'Postponed' ), 'EventMovedOnline' => self::t( 'Moved online' ), 'EventCancelled' => self::t( 'Cancelled' ), ] ], [ 'key' => 'venue_location', 'label' => self::t( 'Use one of your saved locations as the venue' ), 'type' => 'location', 'help' => self::t( 'Or fill in the venue below.' ) ], [ 'key' => 'venue_name', 'label' => self::t( 'Venue name' ), 'type' => 'text', 'required_when' => [ 'attendance_mode', [ '', 'offline', 'mixed' ] ], 'satisfied_by' => 'venue_location' ], ], $address, [ [ 'key' => 'online_url', 'label' => self::t( 'Online event link' ), 'type' => 'url', 'required_when' => [ 'attendance_mode', [ 'online', 'mixed' ] ] ], [ 'key' => 'organizer_name', 'label' => self::t( 'Organizer' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'organizer_url', 'label' => self::t( 'Organizer website' ), 'type' => 'url' ], [ 'key' => 'performer', 'label' => self::t( 'Performer or speaker' ), 'type' => 'text' ], [ 'key' => 'price', 'label' => self::t( 'Ticket price' ), 'type' => 'number', 'help' => self::t( 'Enter 0 for a free event.' ) ], $currency, [ 'key' => 'ticket_url', 'label' => self::t( 'Ticket / registration link' ), 'type' => 'url' ], [ 'key' => 'availability', 'label' => self::t( 'Ticket availability' ), 'type' => 'select', 'options' => [ 'InStock' => self::t( 'Available' ), 'SoldOut' => self::t( 'Sold out' ), 'PreOrder' => self::t( 'Not on sale yet' ), ] ], ] ), ], 'course' => [ 'intro' => self::t( 'Course schema can show your course in Google\'s course lists. The course description comes from your meta description.' ), 'needs_description' => true, 'fields' => [ [ 'key' => 'provider_name', 'label' => self::t( 'Provider (school or company)' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'provider_url', 'label' => self::t( 'Provider website' ), 'type' => 'url' ], [ 'key' => 'course_code', 'label' => self::t( 'Course code' ), 'type' => 'text' ], [ 'key' => 'course_mode', 'label' => self::t( 'Format' ), 'type' => 'select', 'options' => [ 'online' => self::t( 'Online' ), 'onsite' => self::t( 'In person' ), 'blended' => self::t( 'Blended' ), ] ], [ 'key' => 'workload_hours', 'label' => self::t( 'Total hours' ), 'type' => 'number' ], [ 'key' => 'start_date', 'label' => self::t( 'Next start date' ), 'type' => 'date' ], [ 'key' => 'end_date', 'label' => self::t( 'End date' ), 'type' => 'date' ], [ 'key' => 'price', 'label' => self::t( 'Price' ), 'type' => 'number', 'help' => self::t( 'Enter 0 for a free course.' ) ], $currency, ], ], 'review' => [ 'intro' => self::t( 'Review stars can appear in search results. Google ignores reviews a business writes about itself, so use this for reviews of other products, places, or services.' ), 'fields' => [ [ 'key' => 'item_type', 'label' => self::t( 'What is being reviewed?' ), 'type' => 'select', 'options' => [ 'Product' => self::t( 'Product' ), 'LocalBusiness' => self::t( 'Local business' ), 'Organization' => self::t( 'Organization' ), 'Book' => self::t( 'Book' ), 'Course' => self::t( 'Course' ), 'Event' => self::t( 'Event' ), 'Movie' => self::t( 'Movie' ), 'Recipe' => self::t( 'Recipe' ), 'SoftwareApplication' => self::t( 'App or software' ), ] ], [ 'key' => 'item_name', 'label' => self::t( 'Name of the item reviewed' ), 'type' => 'text', 'required' => true ], [ 'key' => 'rating_value', 'label' => self::t( 'Your rating' ), 'type' => 'number', 'required' => true, 'placeholder' => '4.5' ], [ 'key' => 'best_rating', 'label' => self::t( 'Out of' ), 'type' => 'number', 'placeholder' => '5' ], [ 'key' => 'author_name', 'label' => self::t( 'Reviewer name' ), 'type' => 'text', 'help' => self::t( 'Defaults to the post author.' ) ], ], ], 'video' => [ 'intro' => self::t( 'Video schema helps your video show up in Google Videos and video results. If the post embeds a YouTube or Vimeo video, CitedSEO finds it automatically.' ), 'fields' => [ [ 'key' => 'embed_url', 'label' => self::t( 'Video URL (YouTube, Vimeo...)' ), 'type' => 'url', 'help' => self::t( 'Leave blank to use the first YouTube or Vimeo video in the post.' ) ], [ 'key' => 'content_url', 'label' => self::t( 'Direct video file URL' ), 'type' => 'url', 'placeholder' => 'https://.../video.mp4' ], [ 'key' => 'thumbnail_url', 'label' => self::t( 'Thumbnail image' ), 'type' => 'image', 'required_unless' => 'featured', 'help' => self::t( 'Defaults to the video\'s YouTube thumbnail or the featured image.' ) ], [ 'key' => 'upload_date', 'label' => self::t( 'Upload date' ), 'type' => 'date', 'help' => self::t( 'Defaults to the post date.' ) ], [ 'key' => 'duration_min', 'label' => self::t( 'Length (minutes)' ), 'type' => 'number' ], [ 'key' => 'duration_sec', 'label' => self::t( 'Length (extra seconds)' ), 'type' => 'number' ], ], ], 'organization' => [ 'intro' => self::t( 'Tells Google and AI engines who is behind the site: name, logo, contact details, and official profiles.' ), 'fields' => array_merge( [ [ 'key' => 'name', 'label' => self::t( 'Name' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'name_ar', 'label' => self::t( 'Arabic name' ), 'type' => 'text', 'dir' => 'rtl', 'help' => self::t( 'Used as the main name on Arabic pages and as the alternate name on English pages.' ) ], [ 'key' => 'legal_name', 'label' => self::t( 'Legal name' ), 'type' => 'text' ], [ 'key' => 'logo', 'label' => self::t( 'Logo' ), 'type' => 'image', 'help' => self::t( 'Defaults to your site icon.' ) ], [ 'key' => 'phone', 'label' => self::t( 'Phone' ), 'type' => 'text' ], [ 'key' => 'email', 'label' => self::t( 'Email' ), 'type' => 'text' ], ], $address, [ [ 'key' => 'languages', 'label' => self::t( 'Languages you serve customers in' ), 'type' => 'text', 'placeholder' => 'Arabic, English, French' ], [ 'key' => 'founding_date', 'label' => self::t( 'Founded' ), 'type' => 'date' ], [ 'key' => 'same_as', 'label' => self::t( 'Official profiles (one URL per line)' ), 'type' => 'list', 'placeholder' => "https://www.linkedin.com/company/...\nhttps://www.instagram.com/..." ], ] ), ], 'person' => [ 'intro' => self::t( 'Describes a person, for an author, doctor, lawyer, or team member page.' ), 'fields' => [ [ 'key' => 'name', 'label' => self::t( 'Full name' ), 'type' => 'text', 'help' => self::t( 'Defaults to the page title.' ) ], [ 'key' => 'name_ar', 'label' => self::t( 'Arabic name' ), 'type' => 'text', 'dir' => 'rtl' ], [ 'key' => 'job_title', 'label' => self::t( 'Job title' ), 'type' => 'text' ], [ 'key' => 'works_for', 'label' => self::t( 'Works for' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'image', 'label' => self::t( 'Photo' ), 'type' => 'image', 'help' => self::t( 'Defaults to the featured image.' ) ], [ 'key' => 'email', 'label' => self::t( 'Email' ), 'type' => 'text' ], [ 'key' => 'knows_about', 'label' => self::t( 'Areas of expertise (one per line)' ), 'type' => 'list' ], [ 'key' => 'same_as', 'label' => self::t( 'Profiles (one URL per line)' ), 'type' => 'list', 'placeholder' => 'https://www.linkedin.com/in/...' ], ], ], 'localbusiness' => [ 'intro' => self::t( 'Pulls the address, hours, phone, and map from one of your saved locations, in Arabic on Arabic pages.' ), 'fields' => [ [ 'key' => 'location_id', 'label' => self::t( 'Location' ), 'type' => 'location', 'required' => true, 'help' => self::t( 'Add or edit locations under CitedSEO → Local SEO.' ) ], [ 'key' => 'serves_cuisine', 'label' => self::t( 'Cuisine (restaurants and cafés)' ), 'type' => 'text', 'placeholder' => 'Lebanese, Mediterranean' ], [ 'key' => 'menu_url', 'label' => self::t( 'Menu link' ), 'type' => 'url' ], [ 'key' => 'accepts_reservations', 'label' => self::t( 'Accepts reservations' ), 'type' => 'checkbox' ], [ 'key' => 'star_rating', 'label' => self::t( 'Hotel star rating' ), 'type' => 'number', 'placeholder' => '4' ], ], ], 'software' => [ 'intro' => self::t( 'Google only shows app rich results when the app has a price (0 if free) and a rating.' ), 'fields' => array_merge( [ [ 'key' => 'operating_system', 'label' => self::t( 'Operating system' ), 'type' => 'text', 'placeholder' => 'Android, iOS, Windows' ], [ 'key' => 'category', 'label' => self::t( 'Category' ), 'type' => 'select', 'options' => [ 'BusinessApplication' => self::t( 'Business' ), 'EducationalApplication' => self::t( 'Education' ), 'FinanceApplication' => self::t( 'Finance' ), 'GameApplication' => self::t( 'Game' ), 'HealthApplication' => self::t( 'Health and fitness' ), 'LifestyleApplication' => self::t( 'Lifestyle' ), 'MultimediaApplication' => self::t( 'Multimedia' ), 'ShoppingApplication' => self::t( 'Shopping' ), 'SocialNetworkingApplication' => self::t( 'Social' ), 'TravelApplication' => self::t( 'Travel' ), 'UtilitiesApplication' => self::t( 'Utilities' ), 'DeveloperApplication' => self::t( 'Developer tools' ), ] ], [ 'key' => 'price', 'label' => self::t( 'Price' ), 'type' => 'number', 'required' => true, 'help' => self::t( 'Enter 0 for a free app.' ) ], $currency, ], array_map( function ( $f ) { $f['required'] = true; return $f; }, $rating ), [ [ 'key' => 'download_url', 'label' => self::t( 'Download link' ), 'type' => 'url' ], [ 'key' => 'version', 'label' => self::t( 'Version' ), 'type' => 'text' ], ] ), ], 'book' => [ 'intro' => self::t( 'Describes a book with its author, publisher, and ISBN.' ), 'fields' => [ [ 'key' => 'author_name', 'label' => self::t( 'Author' ), 'type' => 'text', 'required' => true ], [ 'key' => 'isbn', 'label' => 'ISBN', 'type' => 'text' ], [ 'key' => 'book_format', 'label' => self::t( 'Format' ), 'type' => 'select', 'options' => [ 'Hardcover' => self::t( 'Hardcover' ), 'Paperback' => self::t( 'Paperback' ), 'EBook' => self::t( 'E-book' ), 'AudiobookFormat' => self::t( 'Audiobook' ), ] ], [ 'key' => 'publisher', 'label' => self::t( 'Publisher' ), 'type' => 'text' ], [ 'key' => 'date_published', 'label' => self::t( 'Publication date' ), 'type' => 'date' ], [ 'key' => 'pages', 'label' => self::t( 'Number of pages' ), 'type' => 'number' ], ], ], 'service' => [ 'intro' => self::t( 'Describes a service you offer and where you offer it, which helps AI engines recommend you for that service in your area.' ), 'fields' => [ [ 'key' => 'service_type', 'label' => self::t( 'Type of service' ), 'type' => 'text', 'placeholder' => 'SEO consulting' ], [ 'key' => 'location_id', 'label' => self::t( 'Provided by (location)' ), 'type' => 'location', 'help' => self::t( 'Leave empty to use your site name as the provider.' ) ], [ 'key' => 'area_served', 'label' => self::t( 'Areas served (one per line)' ), 'type' => 'list', 'placeholder' => "Beirut\nDubai\nRiyadh" ], [ 'key' => 'price', 'label' => self::t( 'Starting price' ), 'type' => 'number' ], $currency, ], ], 'dataset' => [ 'intro' => self::t( 'Makes a dataset findable in Google Dataset Search. Google needs a description of at least 50 characters, taken from your meta description.' ), 'needs_description' => true, 'fields' => [ [ 'key' => 'creator', 'label' => self::t( 'Creator' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'license', 'label' => self::t( 'License URL' ), 'type' => 'url', 'placeholder' => 'https://creativecommons.org/licenses/by/4.0/' ], [ 'key' => 'download_url', 'label' => self::t( 'Download link' ), 'type' => 'url' ], [ 'key' => 'file_format', 'label' => self::t( 'File format' ), 'type' => 'text', 'placeholder' => 'CSV' ], [ 'key' => 'temporal', 'label' => self::t( 'Time period covered' ), 'type' => 'text', 'placeholder' => '2020-01-01/2025-12-31' ], [ 'key' => 'spatial', 'label' => self::t( 'Place covered' ), 'type' => 'text', 'placeholder' => 'Lebanon' ], [ 'key' => 'keywords', 'label' => self::t( 'Keywords (one per line)' ), 'type' => 'list' ], ], ], 'product' => [ 'intro' => self::t( 'Google needs a price, a rating, or a review before it shows product rich results.' ), 'fields' => array_merge( [ [ 'key' => 'price', 'label' => self::t( 'Price' ), 'type' => 'number', 'required' => true ], $currency, [ 'key' => 'availability', 'label' => self::t( 'Availability' ), 'type' => 'select', 'options' => [ 'InStock' => self::t( 'In stock' ), 'OutOfStock' => self::t( 'Out of stock' ), 'PreOrder' => self::t( 'Pre-order' ), 'BackOrder' => self::t( 'Back order' ), ] ], [ 'key' => 'condition', 'label' => self::t( 'Condition' ), 'type' => 'select', 'options' => [ 'NewCondition' => self::t( 'New' ), 'UsedCondition' => self::t( 'Used' ), 'RefurbishedCondition' => self::t( 'Refurbished' ), ] ], [ 'key' => 'brand', 'label' => self::t( 'Brand' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'sku', 'label' => 'SKU', 'type' => 'text' ], [ 'key' => 'gtin', 'label' => self::t( 'GTIN / barcode' ), 'type' => 'text' ], [ 'key' => 'mpn', 'label' => self::t( 'Manufacturer part number' ), 'type' => 'text' ], [ 'key' => 'price_valid_until', 'label' => self::t( 'Price valid until' ), 'type' => 'date' ], ], $rating ), ], 'job' => [ 'intro' => self::t( 'Job postings can appear in Google for Jobs. The full job description is taken from the post content.' ), 'fields' => array_merge( [ [ 'key' => 'company', 'label' => self::t( 'Hiring company' ), 'type' => 'text', 'help' => self::t( 'Defaults to your site name.' ) ], [ 'key' => 'employmentType', 'label' => self::t( 'Employment type' ), 'type' => 'select', 'options' => [ 'FULL_TIME' => self::t( 'Full time' ), 'PART_TIME' => self::t( 'Part time' ), 'CONTRACTOR' => self::t( 'Contractor' ), 'TEMPORARY' => self::t( 'Temporary' ), 'INTERN' => self::t( 'Internship' ), ] ], [ 'key' => 'remote', 'label' => self::t( 'Fully remote job' ), 'type' => 'checkbox' ], [ 'key' => 'validThrough', 'label' => self::t( 'Applications close' ), 'type' => 'date' ], ], array_map( function ( $f ) { if ( in_array( $f['key'], [ 'city', 'country' ], true ) ) { $f['required_when'] = [ 'remote', [ '' ] ]; } return $f; }, $address ), [ [ 'key' => 'salary', 'label' => self::t( 'Salary' ), 'type' => 'number' ], $currency, [ 'key' => 'salary_unit', 'label' => self::t( 'Salary is per' ), 'type' => 'select', 'options' => [ 'MONTH' => self::t( 'Month' ), 'YEAR' => self::t( 'Year' ), 'WEEK' => self::t( 'Week' ), 'DAY' => self::t( 'Day' ), 'HOUR' => self::t( 'Hour' ), ] ], ] ), ], 'faq' => [ 'intro' => self::t( 'Questions and answers from this page. Google now shows FAQ rich results mainly for government and health sites, but AI engines read this markup on every site.' ), 'fields' => [ [ 'key' => 'faqs', 'label' => self::t( 'Questions' ), 'type' => 'repeater', 'required' => true, 'add_label' => self::t( 'Add question' ), 'fields' => [ [ 'key' => 'question', 'label' => self::t( 'Question' ), 'type' => 'text' ], [ 'key' => 'answer', 'label' => self::t( 'Answer' ), 'type' => 'textarea' ], ] ], ], ], 'howto' => [ 'intro' => self::t( 'Step-by-step instructions. AI engines use this to quote your steps accurately.' ), 'fields' => [ [ 'key' => 'steps', 'label' => self::t( 'Steps' ), 'type' => 'repeater', 'required' => true, 'add_label' => self::t( 'Add step' ), 'fields' => [ [ 'key' => 'name', 'label' => self::t( 'Step title' ), 'type' => 'text' ], [ 'key' => 'text', 'label' => self::t( 'Instructions' ), 'type' => 'textarea' ], ] ], [ 'key' => 'total_minutes', 'label' => self::t( 'Total time (minutes)' ), 'type' => 'number' ], [ 'key' => 'supplies', 'label' => self::t( 'Supplies needed (one per line)' ), 'type' => 'list' ], [ 'key' => 'tools', 'label' => self::t( 'Tools needed (one per line)' ), 'type' => 'list' ], [ 'key' => 'cost', 'label' => self::t( 'Estimated cost' ), 'type' => 'number' ], $currency, ], ], 'recipe' => [ 'intro' => self::t( 'Google needs an image for recipe rich results. Ingredients, steps, and times make the result much richer.' ), 'fields' => array_merge( [ [ 'key' => 'image', 'label' => self::t( 'Recipe photo' ), 'type' => 'image', 'required_unless' => 'featured', 'help' => self::t( 'Defaults to the featured image.' ) ], [ 'key' => 'cuisine', 'label' => self::t( 'Cuisine' ), 'type' => 'text', 'placeholder' => 'Lebanese' ], [ 'key' => 'category', 'label' => self::t( 'Course' ), 'type' => 'text', 'placeholder' => 'Mezze, Dessert' ], [ 'key' => 'prep_minutes', 'label' => self::t( 'Prep time (minutes)' ), 'type' => 'number' ], [ 'key' => 'cook_minutes', 'label' => self::t( 'Cook time (minutes)' ), 'type' => 'number' ], [ 'key' => 'yield', 'label' => self::t( 'Servings' ), 'type' => 'text', 'placeholder' => '4' ], [ 'key' => 'calories', 'label' => self::t( 'Calories per serving' ), 'type' => 'number' ], [ 'key' => 'ingredients', 'label' => self::t( 'Ingredients (one per line)' ), 'type' => 'list' ], [ 'key' => 'instructions', 'label' => self::t( 'Steps (one per line)' ), 'type' => 'list' ], ], $rating ), ], ]; } // ── Reading values ─────────────────────────────────────────────────────── /** Decoded field values for a post ([] when none saved). */ public static function get( $post_id ) { $raw = AWBSEO_Meta_Manager::get( $post_id, self::META_KEY ); $val = $raw ? json_decode( $raw, true ) : []; return is_array( $val ) ? $val : []; } // ── Editor assets ──────────────────────────────────────────────────────── public static function enqueue( $hook ) { if ( ! in_array( $hook, [ 'post.php', 'post-new.php' ], true ) || ! wp_script_is( 'awbseo-meta-box', 'enqueued' ) ) { return; } global $post; if ( ! $post ) { return; } $post_is_ar = class_exists( 'AWBSEO_White_Label' ) && AWBSEO_White_Label::is_post_arabic( $post->ID ); self::$force_ar = $post_is_ar; $locations = []; $primary = 0; foreach ( (array) AWBSEO_Local_SEO::get_locations() as $loc ) { $locations[] = [ 'id' => (int) $loc->id, 'name' => $loc->name . ( $loc->city ? ' - ' . $loc->city : '' ) ]; if ( $loc->is_primary ) { $primary = (int) $loc->id; } } wp_enqueue_script( 'awbseo-schema-fields', AWBSEO_PLUGIN_URL . 'assets/js/schema-fields.js', [ 'awbseo-meta-box' ], AWBSEO_VERSION, true ); wp_localize_script( 'awbseo-schema-fields', 'AWBSEOSchemaFields', [ 'defs' => self::definitions(), 'typeGroups' => self::type_groups(), 'values' => (object) self::get( $post->ID ), 'isRtl' => $post_is_ar || is_rtl(), 'context' => [ 'hasFeatured' => has_post_thumbnail( $post->ID ), 'hasDescription' => '' !== trim( (string) AWBSEO_Meta_Manager::get( $post->ID, 'description' ) ), 'locations' => $locations, 'primaryId' => $primary, 'localSeoUrl' => admin_url( 'admin.php?page=awbseo-local' ), ], 'i18n' => [ 'title' => self::t( 'Schema details' ), 'pickType' => self::t( 'Choose a schema type in the Schema tab above to add the details Google needs for rich results.' ), 'genericType' => self::t( 'This schema type only needs the basics (title, description, URL), which CitedSEO fills in automatically.' ), 'customOverride' => self::t( 'You pasted a complete schema block in Schema Data, so that block is used instead of these fields.' ), 'ready' => self::t( 'Ready for Google rich results' ), 'missing' => self::t( 'Still needed for Google rich results:' ), 'needDesc' => self::t( 'A meta description (General tab)' ), 'needFeatured' => self::t( 'or set a featured image' ), 'noLocations' => self::t( 'You have no saved locations yet.' ), 'addLocation' => self::t( 'Add one in Local SEO' ), 'choose' => self::t( 'Choose' ), 'none' => self::t( '- None -' ), 'primary' => self::t( 'Main location' ), 'remove' => self::t( 'Remove' ), 'select' => self::t( '- Select -' ), ], ] ); self::$force_ar = false; } }