= 3 && $tail <= 10) { return sprintf($ar_few, $n); } if ($tail >= 11) { return sprintf($ar_many, $n); } // 0, 100, 101, 102, 200 ...: genitive singular (drop the accusative alif tanween). return sprintf(str_replace(["\u{064B}\u{0627}", "\u{0627}\u{064B}"], '', $ar_many), $n); } public static function is_arabic(): bool { return self::lang() === 'ar'; } public static function contains_arabic(string $text): bool { return preg_match('/[\x{0600}-\x{06FF}]/u', $text) === 1; } public static function english_safe(string $text, string $fallback = ''): string { $text = trim($text); if ($text === '') { return $fallback; } if (!self::is_arabic() && self::contains_arabic($text)) { return $fallback; } return $text; } /** * 4.0.18: run $fn with I18n::tr() resolving to $lang, then restore the * previous language. The audit uses this to render every finding in both * shipped languages during one scan, so switching the admin language later * shows the finding in the new language instead of the scan's language. * * @template T * @param callable():T $fn * @return T */ public static function with_lang(string $lang, callable $fn) { $lang = in_array($lang, ['ar', 'en'], true) ? $lang : 'en'; $previous = self::$cached_lang; self::$cached_lang = $lang; try { return $fn(); } finally { self::$cached_lang = $previous; } } /** * The shipped language other than $lang. */ public static function other_lang(string $lang): string { return $lang === 'ar' ? 'en' : 'ar'; } public static function lang(): string { if (self::$cached_lang !== null) { return self::$cached_lang; } $lang = self::resolve_lang(); if (function_exists('apply_filters')) { $lang = (string) apply_filters('aetos_seo_admin_locale', $lang); } if (!in_array($lang, ['ar', 'en'], true)) { $lang = 'en'; } self::$cached_lang = $lang; return $lang; } private static function resolve_lang(): string { // 1. Plugin Settings override. if (class_exists(Settings::class)) { try { $s = Settings::get(); $ui = isset($s['ui']) && is_array($s['ui']) ? $s['ui'] : []; $admin_locale = isset($ui['admin_locale']) ? strtolower((string) $ui['admin_locale']) : 'auto'; if ($admin_locale === 'ar' || $admin_locale === 'en') { return $admin_locale; } } catch (\Throwable $e) { // Fall through. } } // 2. Auto: derive from WP locale. if (function_exists('is_rtl') && is_rtl()) { return 'ar'; } if (function_exists('get_locale')) { $loc = (string) get_locale(); if (strpos($loc, 'ar') === 0) { return 'ar'; } } return 'en'; } /** * For wp_localize_script: build a single payload of common JS strings * so admin.js can read them via AetosSeoData.i18n. without * hardcoding Arabic in JS. * * @return array */ public static function js_payload(): array { return [ 'lang' => self::lang(), 'is_rtl' => self::is_arabic() ? '1' : '0', 'confirm_reset_data' => self::tr( 'This will erase all scan data. Continue?', 'سيتم حذف كل بيانات الفحص. متأكد؟' ), 'confirm_factory_reset' => self::tr( 'Factory reset will erase scans + integrations + settings. Continue?', 'سيحذف Factory Reset الفحوصات والتكاملات والإعدادات. هل أنت متأكد؟' ), 'loading' => self::tr('Loading…', 'جاري التحميل…'), 'saving' => self::tr('Saving…', 'جاري الحفظ…'), 'saved' => self::tr('Saved', 'تم الحفظ'), 'failed' => self::tr('Failed', 'فشل'), 'retrying' => self::tr('Retrying…', 'إعادة المحاولة…'), 'connection_error' => self::tr('Connection error', 'خطأ اتصال'), 'copy' => self::tr('Copy', 'نسخ'), 'copied' => self::tr('Copied', 'تم النسخ'), 'prompt_copied' => self::tr('Copied - paste into any AI assistant', 'اتنسخ، الصقه في أي مساعد AI'), 'send' => self::tr('Send', 'إرسال'), 'cancel' => self::tr('Cancel', 'إلغاء'), 'view_details' => self::tr('View details', 'عرض التفاصيل'), 'mark_done' => self::tr('Mark done', 'تم الحل'), 'retest' => self::tr('Retest', 'إعادة فحص'), 'no_data' => self::tr('No data', 'لا توجد بيانات'), 'thinking' => self::tr('Thinking…', 'يفكر…'), ]; } /** * Reset the static cache (used by tests). */ public static function reset_cache(): void { self::$cached_lang = null; } }