Commit 4140b7da authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: complete i18n pass — replace 900+ hardcoded Arabic strings with t() calls

All UI text across 57 files now goes through the i18n system (core/i18n.js).
Added ~120 new translation keys covering: auth, daily rewards, challenges,
ranks, shop, groups, tournaments, puzzles, orgs, emotes, and backgammon
doubling. Both ar and en dictionaries are complete and in sync.

Data strings (country names, FIDE titles, variant names, puzzle themes)
are intentionally left as-is — they use name/nameEn data pattern.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 43b1e2a0
......@@ -126,7 +126,547 @@ const strings = {
'achievements.gameplay': 'اللعب',
'achievements.social': 'اجتماعي',
'achievements.progression': 'التقدم',
'achievements.collection': 'جمع'
'achievements.collection': 'جمع',
'common.ok': 'حسناً',
'common.you': 'أنت',
'common.opponent': 'خصم',
'common.player': 'لاعب',
'common.friend': 'صديق',
'common.back': 'رجوع',
'common.send': 'إرسال',
'common.sending': 'جاري الإرسال...',
'common.sent': '✓ تم الإرسال',
'common.failed': 'فشل',
'common.already_sent': 'مرسل سابقاً',
'common.already_friends': 'صديق بالفعل',
'common.accepted': '✓ تم القبول',
'common.done': '✓ تم',
'common.error_load': 'فشل التحميل',
'common.now': 'الآن',
'common.minutes_ago': 'منذ {n} دقيقة',
'common.hours_ago': 'منذ {n} ساعة',
'common.days_ago': 'منذ {n} يوم',
'common.online': 'متصل الآن',
'common.offline': 'غير متصل',
'common.level': 'مستوى {n}',
'common.pieces': '{n} قطع',
'game.round_won': 'فزت بالجولة!',
'game.round_lost': 'خسرت الجولة',
'game.round_end': 'انتهت الجولة',
'game.round_next': 'الجولة التالية تبدأ...',
'game.points': '+{n} نقطة',
'game.choose_placement': 'اختر مكان الوضع',
'game.opponent_plays': 'الخصم يلعب...',
'game.one_piece_warning': '⚠️ قطعة واحدة!',
'game.two_piece_warning': '⚠️ قطعتين!',
'game.resign_confirm': 'هل تريد الاستسلام؟',
'game.resign_yes': 'نعم، استسلم',
'game.resign_no': 'تراجع',
'game.place_first': 'ضع أول قطعة',
'game.vs_bot': 'ضد البوت',
'game.online': 'أونلاين',
'game.vs_friend': 'تحدي صديق',
'game.roll_dice': 'ارمِ النرد',
'game.choose_piece': 'اختر قطعة',
'game.speed_mode': '⚡ وضع السرعة!',
'game.turn_of': 'دور {name}',
'game.bot_thinking': '{name} يفكر...',
'game.rating': 'التصنيف',
'game.coins': 'عملات',
'game.xp': 'خبرة',
'game.moves_count': '{n} نقلة',
'game.rounds_count': '{n} جولة',
'game.rounds_count_plural': '{n} جولات',
'game.checkmate': 'كش ملك',
'game.stalemate': 'بات (جمود)',
'game.timeout': 'انتهاء الوقت',
'game.threefold': 'تكرار ثلاثي',
'game.insufficient': 'قطع غير كافية',
'game.win': 'فوز!',
'game.loss': 'خسارة',
'game.draw_game': 'تعادل',
'game.resigned': 'استسلمت',
'game.opponent_resigned': 'الخصم استسلم!',
'game.opponent_abandoned': 'الخصم انقطع',
'game.congrats': 'مبروك! أنت البطل',
'game.second_place': 'المركز الثاني — أحسنت!',
'game.third_place': 'المركز الثالث',
'game.fourth_place': 'المركز الرابع',
'game.withdrew': 'انسحبت من المباراة',
'game.player_ranking': 'ترتيب اللاعبين',
'game.play_again': 'العب مرة ثانية',
'game.back_to_menu': 'رجوع للقائمة',
'game.not_found': 'مباراة غير موجودة',
'match.disconnected_title': 'الخصم انقطع اتصاله',
'match.disconnected_waiting': 'في انتظار عودته...',
'match.disconnected_auto_win': 'ستفوز تلقائياً إذا لم يعد خلال الوقت المحدد',
'match.claim_early_win': 'فوز مبكر',
'match.opponent_returned': '✅ الخصم عاد — استمروا!',
'match.you_won': 'فزت!',
'match.opponent_left': 'الخصم غادر المباراة',
'match.return': 'العودة',
'match.connection_lost': 'انقطع الاتصال',
'match.reconnecting': 'جاري إعادة الاتصال...',
'match.reconnected': '✅ تم استعادة الاتصال',
'match.restoring': 'جاري استعادة المباراة...',
'match.your_turn_warning': '⏱️ دورك! العب قبل انتهاء الوقت',
'match.opponent_thinking': 'الخصم يفكر...',
'match.bot_replacement': '🤖 الخصم غادر — بوت يكمل بدلاً منه',
'match.your_time_expired': '⏱️ انتهى وقتك! تم التمرير تلقائياً',
'match.opponent_time_expired': '⏱️ انتهى وقت الخصم — دورك!',
'tournament.round_ready': 'جولة {n} جاهزة!',
'tournament.tap_to_play': 'اضغط للعب مباراتك',
'tournament.play': 'العب',
'tournament.back_to': 'العودة للبطولة',
'mp.profile': 'الملف الشخصي',
'mp.add_friend': 'إضافة صديق',
'mp.report': 'إبلاغ',
'mp.reported': '✓ تم الإبلاغ',
'mp.block': 'حظر',
'social.friends_tab': 'الأصدقاء',
'social.pending_tab': 'الطلبات',
'social.online_tab': 'متصلين',
'social.groups_tab': 'المجموعات',
'social.activity_tab': 'أخبار',
'social.search_btn': 'بحث',
'social.search_placeholder': 'اسم المستخدم أو الاسم...',
'social.search_hint': 'ابحث بأي جزء من الاسم (حرفين على الأقل)',
'social.search_min_chars': 'أدخل حرفين على الأقل',
'social.searching': 'جاري البحث...',
'social.no_results': 'لم يتم العثور على نتائج — جرب اسم آخر',
'social.search_failed': 'فشل البحث',
'social.add_btn': '+ أضف',
'social.no_friends': 'لا يوجد أصدقاء بعد',
'social.no_friends_hint': 'ابحث عن لاعبين وأرسل لهم طلب صداقة',
'social.search_players': 'ابحث عن لاعبين',
'social.friends_count': '{n} أصدقاء',
'social.online_count': '{n} متصل',
'social.no_pending': 'لا توجد طلبات معلقة',
'social.pending_count': '{n} طلب معلق',
'social.sent_friend_request': 'أرسل لك طلب صداقة',
'social.no_online_friends': 'لا يوجد أصدقاء متصلين الآن',
'social.will_appear_online': 'سيظهرون هنا عند دخولهم',
'social.online_now_count': '{n} متصل الآن',
'social.remove_friend': 'إزالة صديق',
'social.remove_confirm': 'إزالة {name} من الأصدقاء؟',
'social.challenges_you': '{name} يتحداك!',
'social.accept': 'قبول',
'social.error': 'خطأ',
'social.retry_load': 'حاول مرة أخرى',
'social.no_activity': 'لا توجد أخبار — العب لتظهر أخبارك هنا',
'social.no_activity_short': 'لا توجد أخبار',
'social.game_win': 'فاز بمباراة',
'social.game_loss': 'خسر مباراة',
'social.game_draw': 'تعادل',
'social.achievement_unlock': 'حصل على إنجاز',
'social.level_up': 'ارتقى لمستوى جديد',
'social.tournament_join': 'انضم لبطولة',
'social.tournament_win': 'فاز ببطولة',
'social.friend_add': 'أضاف صديقاً جديداً',
'challenge.title': 'تحدّي صديق',
'challenge.loading_friends': 'جاري تحميل الأصدقاء...',
'challenge.no_friends': 'لا يوجد أصدقاء بعد',
'challenge.no_friends_hint': 'أضف أصدقاء أولاً لتتمكن من تحديهم',
'challenge.online_now': 'متصلين الآن ({n})',
'challenge.offline': 'غير متصلين',
'challenge.select_game': 'اختر اللعبة ونوع الوقت',
'challenge.send': 'أرسل التحدي',
'challenge.sending': '⏳ جاري الإرسال...',
'challenge.sent': '✓ تم إرسال التحدي!',
'challenge.failed_create': 'فشل إنشاء المباراة',
'challenge.failed_retry': 'فشل — حاول مرة أخرى',
'challenge.sent_msg': 'أرسل تحدي {game}',
'chat.placeholder': 'اكتب رسالة...',
'chat.loading_messages': 'جاري التحميل...',
'chat.failed_load': 'فشل تحميل الرسائل',
'chat.start_conversation': 'ابدأ المحادثة مع صديقك!',
'chat.challenge_label': 'تحدي {game}',
'chat.challenge_title': 'تحدّي {name}',
'chat.choose_game': 'اختر اللعبة',
'lobby.title': 'غرفة التحدي',
'lobby.ready': 'جاهز',
'lobby.waiting': 'في الانتظار...',
'lobby.waiting_accept': 'في انتظار القبول...',
'lobby.ready_start': 'جاهز للبدء!',
'lobby.waiting_opponent': 'في انتظار الخصم...',
'lobby.auto_start': 'سيتم بدء المباراة تلقائياً عند قبول التحدي',
'lobby.start': 'ابدأ المباراة',
'lobby.accepted_preparing': 'تم القبول! جاري تجهيز المباراة...',
'lobby.opponent_accepted': 'الخصم قبل! جاري البدء...',
'time.bullet': 'رصاصة',
'time.blitz': 'خاطفة',
'time.rapid': 'سريعة',
'time.classical': 'كلاسيكية',
'time.1min': '1 دقيقة',
'time.3min': '3 دقائق',
'time.5min': '5 دقائق',
'time.10min': '10 دقائق',
'time.15min': '15 دقيقة',
'time.20min': '20 دقيقة',
'time.30min': '30 دقيقة',
'time.60min': '60 دقيقة',
'play.single_player': 'لاعب واحد',
'play.single_player_desc': 'العب ضد الكمبيوتر',
'play.online_desc': 'نافس لاعبين حقيقيين',
'play.achievements': 'إنجازات',
'play.gift': 'هدية',
'play.coming_soon': 'قريباً',
'play.login_required_online': 'سجّل دخولك للعب أونلاين',
'play.login_required_friend': 'سجّل دخولك لتحدي صديق',
'domino.title': 'دومينو',
'domino.subtitle': 'أول من يوصل الهدف يفوز!',
'domino.draw_boneyard': 'سحب من المخزن',
'domino.pass': 'تمرير',
'domino.select_bot': 'اختر مستوى البوت',
'domino.bot_strategy': 'كل مستوى له استراتيجية مختلفة',
'domino.beginner': 'مبتدئ',
'domino.beginner_desc': 'يلعب عشوائي',
'domino.intermediate': 'متوسط',
'domino.intermediate_desc': 'يفضل النقاط العالية',
'domino.expert': 'خبير',
'domino.expert_desc': 'استراتيجي ومخادع',
'domino.select_target': 'اختر الهدف',
'domino.target_hint': 'أول من يوصل النقاط يفوز بالمباراة',
'domino.50_points': '50 نقطة',
'domino.50_desc': 'مباراة سريعة (~5 دقائق)',
'domino.100_points': '100 نقطة',
'domino.100_desc': 'كلاسيك (~10 دقائق)',
'domino.150_points': '150 نقطة',
'domino.150_desc': 'مباراة طويلة (~15 دقيقة)',
'ludo.title': 'لودو',
'ludo.subtitle': 'أول من يوصّل كل قطعه يفوز!',
'ludo.local_play': 'لعب محلي',
'ludo.local_desc': 'اختر عدد اللاعبين والبوتات',
'ludo.online_desc': 'العب ضد لاعبين حقيقيين',
'ludo.friend_desc': 'ادعُ أصدقاءك للعب',
'ludo.settings_online': 'إعدادات الأونلاين',
'ludo.settings_play': 'إعدادات اللعب',
'ludo.settings_online_hint': 'اختر عدد اللاعبين الحقيقيين',
'ludo.settings_play_hint': 'اختر تشكيلة اللاعبين',
'ludo.player_count': 'عدد اللاعبين',
'ludo.classic': 'كلاسيك',
'ludo.triple': 'ثلاثي',
'ludo.duel': 'مبارزة',
'ludo.player_layout': 'توزيعة اللاعبين',
'ludo.bot_level': 'مستوى البوت',
'ludo.easy': 'سهل',
'ludo.medium': 'متوسط',
'ludo.hard': 'صعب',
'ludo.seat_order': 'ترتيب المقاعد',
'ludo.search_match': 'ابحث عن مباراة',
'ludo.start_play': 'ابدأ اللعب',
'ludo.players_no_bot': '{n} لاعبين (بدون بوت)',
'ludo.one_player_bots': 'لاعب واحد + {n} بوت',
'ludo.players_bots': '{h} لاعبين + {n} بوت',
'ludo.red': 'أحمر',
'ludo.green': 'أخضر',
'ludo.yellow': 'أصفر',
'ludo.blue': 'أزرق',
'ludo.searching': 'جاري البحث...',
'ludo.searching_hint': 'بنوصّلك بلاعبين قريب',
'ludo.place_first': 'الأول',
'ludo.place_second': 'الثاني',
'ludo.place_third': 'الثالث',
'ludo.place_fourth': 'الرابع',
'backgammon.title': 'طاولة',
'backgammon.subtitle': 'اول من يطلّع كل قطعه يفوز!',
'backgammon.bot_desc': 'اختر المستوى والنوع',
'backgammon.online_desc': 'العب ضد لاعبين حقيقيين',
'backgammon.friend_desc': 'ادعُ صديقك للعب',
'backgammon.settings_online': 'إعدادات الأونلاين',
'backgammon.settings_game': 'إعدادات اللعبة',
'backgammon.game_type': 'نوع اللعبة',
'backgammon.sheshbesh': 'شيش بيش',
'backgammon.mahbousa': 'محبوسة',
'backgammon.thirtyone': '٣١',
'backgammon.match_length': 'طول الماتش',
'backgammon.short_match': 'سريع',
'backgammon.medium_match': 'قصير',
'backgammon.normal_match': 'عادي',
'backgammon.long_match': 'طويل',
'backgammon.doubling_cube': 'مكعب المضاعفة',
'backgammon.enabled': 'مفعّل',
'backgammon.disabled': 'بدون',
'backgammon.searching': 'جاري البحث...',
'backgammon.searching_hint': 'بنوصّلك بلاعب قريب',
'backgammon.win_title': 'مبروك! فزت بالماتش!',
'backgammon.lose_title': 'خسرت هالمرة...',
'backgammon.win_subtitle': 'أداء ممتاز!',
'backgammon.lose_subtitle': 'حاول مرة ثانية!',
'backgammon.abandon_subtitle': 'الخصم انسحب من اللعبة',
'backgammon.from_match': 'من {n}',
'backgammon.opponent_doubles': 'الخصم يضاعف!',
'backgammon.decline': 'رفض',
'backgammon.accept': 'قبول',
'backgammon.no_moves': 'لا حركات متاحة!',
'backgammon.bot_accepted': 'البوت قبل! ×{n}',
'backgammon.bot_declined': 'البوت رفض! فزت بالجولة',
'emote.good_move': 'نقلة ممتازة',
'emote.think': 'يفكر...',
'emote.hurry': 'أسرع',
'emote.wow': 'واو!',
'emote.laugh': 'هههه',
'emote.angry': 'غاضب',
'emote.hello': 'مرحبا',
'emote.nice_game': '!لعبة حلوة',
'emote.great_move': '!حركة ممتازة',
'emote.lucky': '!حظ',
'emote.think_faster': '...فكّر أسرع',
'emote.rematch': '?ريماتش',
'analysis.title': 'تحليل المباراة',
'analysis.analyzing': 'جاري التحليل...',
'analysis.start': 'بداية',
'analysis.engine_analyzing': 'جاري تحليل المحرك...',
'analysis.failed': 'فشل التحليل',
'analysis.openings_book': 'كتاب الافتتاحات',
'review.title': 'مراجعة المباراة',
'review.analyzing_moves': 'جاري تحليل كل نقلة...',
'review.move_classification': 'تصنيف النقلات',
'review.brilliant': '!! رائعة',
'review.great': '! ممتازة',
'review.best': 'أفضل نقلة',
'review.good': '✓ جيدة',
'review.book': 'نظرية',
'review.inaccuracy': '?! عدم دقة',
'review.mistake': '? خطأ',
'review.blunder': '?? خطأ فادح',
'review.full_analysis': 'التحليل التفصيلي (نقلة بنقلة)',
'review.unknown_opening': 'غير معروف',
'spectate.live': 'مشاهدة مباشرة',
'spectate.black': 'أسود',
'spectate.white': 'أبيض',
'spectate.white_wins': 'فاز الأبيض',
'spectate.black_wins': 'فاز الأسود',
'spectate.game_over': 'انتهت المباراة',
'history.no_matches': 'لا توجد مباريات بعد — العب لتظهر هنا',
'history.win': 'فوز',
'history.loss': 'خسارة',
'history.draw': 'تعادل',
'history.insufficient_data': 'بيانات غير كافية',
'history.matches_count': '{n} مباراة',
'result.copied': '✓ تم النسخ',
'result.copy_pgn': 'نسخ',
'result.share_pgn': 'مشاركة PGN',
'result.analyze': 'تحليل المباراة',
'result.rematch': 'إعادة',
'classifier.excellent': 'ممتاز',
'classifier.great': 'جيد جداً',
'classifier.good': 'جيد',
'classifier.average': 'متوسط',
'classifier.poor': 'ضعيف',
'profile.edit_title': 'تعديل الملف الشخصي',
'profile.basic_info': 'المعلومات الأساسية',
'profile.display_name_en': 'الاسم المعروض (إنجليزي)',
'profile.display_name_ar': 'الاسم المعروض (عربي)',
'profile.bio_en': 'النبذة (إنجليزي)',
'profile.bio_ar': 'النبذة (عربي)',
'profile.location': 'الموقع',
'profile.country': 'الدولة',
'profile.select_country': 'اختر الدولة',
'profile.city': 'المدينة',
'profile.preferred_language': 'اللغة المفضلة',
'profile.fide_info': 'معلومات FIDE',
'profile.fide_classical': 'تصنيف FIDE الكلاسيكي',
'profile.fide_rapid': 'تصنيف FIDE السريع',
'profile.fide_blitz': 'تصنيف FIDE الخاطف',
'profile.fide_title': 'لقب FIDE',
'profile.save': 'حفظ التغييرات',
'profile.saving': 'جاري الحفظ...',
'profile.save_success': 'تم حفظ التغييرات بنجاح',
'profile.save_failed': 'فشل حفظ التغييرات',
'profile.uploading': 'جاري الرفع...',
'profile.upload_failed': 'فشل رفع الصورة',
'profile.matches': 'مباريات',
'profile.wins': 'فوز',
'profile.streak': 'سلسلة',
'profile.rating_section': 'التصنيف',
'profile.chess_rapid': 'شطرنج (سريع)',
'profile.chess_blitz': 'شطرنج (خاطف)',
'profile.chess_bullet': 'شطرنج (رصاصة)',
'profile.org_section': 'المنظمة',
'profile.edit_btn': 'تعديل الملف الشخصي',
'profile.org_apply': 'الانضمام لمنظمة',
'profile.logout': 'خروج',
'profile.view_title': 'الملف الشخصي',
'profile.add_friend': '➕ إضافة صديق',
'profile.friend_sent': '✓ تم إرسال الطلب',
'profile.pending_request': 'طلب صداقة معلق',
'profile.challenge': 'تحدي',
'profile.message': 'مراسلة',
'profile.playing_now': 'يلعب الآن — {game}',
'profile.watch': 'شاهد',
'profile.member': 'عضو',
'profile.pending_review': 'قيد المراجعة',
'profile.rejected': 'مرفوض',
'profile.rejection_reason': 'السبب: {reason}',
'profile.membership_request': 'طلب انضمام',
'profile.no_org': 'لست عضواً في أي منظمة بعد',
'org.join_title': 'الانضمام لمنظمة',
'org.no_orgs': 'لا توجد منظمات متاحة حالياً',
'org.apply': 'تقديم طلب',
'org.reapply': 'إعادة التقديم',
'org.apply_title': 'تقديم طلب انضمام — {name}',
'org.doc_type': 'نوع المستند',
'org.notes': 'ملاحظات (اختياري)',
'org.notes_placeholder': 'رسالة لإدارة المنظمة...',
'org.proof_image': 'صورة إثبات',
'org.tap_to_select': 'اضغط لاختيار صورة',
'org.file_limit': 'PNG, JPG, WebP — حد أقصى 5MB',
'org.file_too_large': 'حجم الملف أكبر من 5MB',
'org.submit': 'إرسال الطلب',
'org.submitting': 'جاري الإرسال...',
'org.submit_success': 'تم إرسال الطلب بنجاح',
'org.submit_failed': 'فشل إرسال الطلب',
'room.waiting_friend': 'بانتظار الصديق...',
'room.challenge_from': 'تحدي من {name}',
'room.send_invite': 'أرسل الدعوة لصديقك',
'room.tap_accept': 'اضغط قبول للبدء',
'room.match_created': 'تم إنشاء المباراة، بانتظار القبول...',
'daily.title': 'المكافأة اليومية',
'daily.streak_days': '{n} يوم متتالي',
'daily.day': 'يوم {n}',
'daily.claimed_today': 'تم استلام مكافأة اليوم',
'daily.come_back': 'عد غداً لمكافأة أكبر!',
'daily.today_reward': 'مكافأة اليوم',
'daily.claim_btn': 'استلم المكافأة',
'daily.hint': 'كل يوم تجمع فيه المكافأة يزيد المبلغ • اليوم السابع = 300 عملة!',
'daily.claiming': 'جاري الاستلام...',
'daily.failed': 'فشل — حاول مرة أخرى',
'challenges.title': 'التحديات اليومية',
'challenges.all_done': 'أنجزت كل التحديات اليوم!',
'challenges.streak': '{n} يوم',
'challenges.claim': 'اجمع',
'rank.bronze': 'برونزي',
'rank.silver': 'فضي',
'rank.gold': 'ذهبي',
'rank.platinum': 'بلاتيني',
'rank.diamond': 'ماسي',
'rank.master': 'أسطوري',
'rank.grandmaster': 'جراند ماستر',
'shop.insufficient_balance': 'رصيد غير كافي',
'shop.purchased': 'تم الشراء!',
'group.create': 'إنشاء',
'group.no_groups': 'لا توجد مجموعات بعد',
'group.no_groups_hint': 'أنشئ مجموعة وادعُ أصدقائك!',
'group.members': 'أعضاء',
'group.owner': 'مالك',
'group.admin': 'مشرف',
'group.play': 'لعب',
'group.load_error': 'خطأ في تحميل المجموعة',
'group.play_invite': 'دعوة لعب',
'group.players': 'لاعبين',
'group.join': 'انضم',
'group.accepted': '✓ قبلت',
'group.invite_sent': 'تم إرسال دعوة اللعب!',
'group.create_title': 'إنشاء مجموعة',
'group.name_label': 'اسم المجموعة',
'group.select_friends': 'اختر أصدقاء للإضافة',
'group.create_btn': 'إنشاء المجموعة',
'group.name_min': 'أدخل اسم المجموعة (حرفين على الأقل)',
'group.creating': 'جاري الإنشاء...',
'group.created': 'تم إنشاء المجموعة!',
'group.add': 'إضافة',
'group.leave': 'مغادرة المجموعة',
'group.remove': 'إزالة',
'group.remove_member': 'إزالة عضو',
'group.leave_confirm': 'هل تريد مغادرة المجموعة؟',
'group.leave_btn': 'غادر',
'group.stay': 'ابقَ',
'auth.login_title': 'تسجيل الدخول',
'auth.login_btn': 'دخول',
'auth.register_link': 'سجّل الآن',
'auth.forgot_password': 'نسيت كلمة المرور؟',
'auth.login_failed': 'فشل تسجيل الدخول',
'auth.invalid_email': 'بريد إلكتروني غير صحيح',
'auth.guest_login': 'دخول كضيف',
'auth.register_title': 'إنشاء حساب',
'auth.confirm_password': 'تأكيد كلمة المرور',
'auth.has_account': 'لديك حساب بالفعل؟',
'auth.login_link': 'سجّل دخول',
'auth.register_failed': 'فشل التسجيل',
'auth.password_mismatch': 'كلمتا المرور غير متطابقتين',
'auth.username_short': 'اسم المستخدم قصير جداً',
'auth.password_short': 'كلمة المرور قصيرة (6 أحرف على الأقل)',
'app.tagline': 'العب مع أصدقائك',
'common.or': 'أو',
'common.play_again': 'العب مرة أخرى',
'tournament.title': 'البطولات',
'tournament.upcoming': 'القادمة',
'tournament.active': 'جارية',
'tournament.completed': 'منتهية',
'tournament.no_tournaments': 'لا توجد بطولات حالياً',
'tournament.players': '{n} لاعب',
'tournament.join': 'انضم',
'tournament.joined': 'مسجّل',
'tournament.starts_in': 'تبدأ خلال {n}',
'tournament.entry_fee': 'رسوم الدخول',
'tournament.prize': 'الجائزة',
'tournament.free': 'مجاني',
'tournament.waiting': 'في انتظار اللاعبين...',
'tournament.players_ready': '{n}/{total} لاعب جاهز',
'tournament.starting_soon': 'تبدأ قريباً!',
'tournament.leave': 'مغادرة',
'tournament.ready': 'جاهز',
'tournament.bracket': 'شجرة البطولة',
'tournament.round': 'الجولة {n}',
'tournament.final': 'النهائي',
'tournament.semifinal': 'نصف النهائي',
'tournament.quarterfinal': 'ربع النهائي',
'tournament.bye': 'ترقية مباشرة',
'tournament.match_starting': 'المباراة تبدأ...',
'tournament.opponent': 'الخصم',
'tournament.round_of': 'جولة {n}',
'tournament.champion': 'البطل!',
'tournament.eliminated': 'خسرت',
'tournament.final_rank': 'المركز {n}',
'tournament.prize_won': 'الجائزة: {n} عملة',
'tournament.back_to_lobby': 'العودة',
'tournament.hub_title': 'بطولات مباشرة',
'tournament.create': 'إنشاء بطولة',
'tournament.browse': 'تصفح البطولات',
'tournament.arena': 'أرينا',
'tournament.leave_arena': 'مغادرة الأرينا',
'tournament.no_results': 'لا توجد نتائج بعد',
'tournament.leaderboard': 'المتصدرون',
'tournament.tab_info': 'معلومات',
'tournament.tab_standings': 'الترتيب',
'tournament.tab_rounds': 'الجولات',
'tournament.tab_my_games': 'مبارياتي',
'tournament.tab_bracket': 'الشجرة',
'tournament.tab_arena': 'أرينا',
'tournament.registration_open': 'تسجيل مفتوح',
'tournament.coming_soon': 'قريباً',
'tournament.round_n': 'الجولة {n}/{total}',
'tournament.live_matches': 'مباريات جارية',
'tournament.no_match': 'لا توجد مباراة',
'spectate.watch': 'شاهد',
'chess.rating': 'التصنيف',
'chess.rating_history': 'سجل التصنيف',
'chess.highest': 'الأعلى',
'chess.lowest': 'الأدنى',
'chess.current': 'الحالي',
'puzzle.title': 'أحجية شطرنج',
'puzzle.correct': 'أحسنت! حل صحيح',
'puzzle.wrong': 'حل خاطئ',
'org.members': 'أعضاء',
'org.game': 'اللعبة',
'org.members_section': 'الأعضاء',
'org.join': 'انضم للمنظمة',
'org.joined': 'تم الانضمام',
'org.title': 'المنظمات',
'org.join_btn': 'انضم',
'org.doc_membership': 'بطاقة عضوية',
'org.doc_id': 'بطاقة هوية',
'org.doc_receipt': 'إيصال دفع',
'org.doc_other': 'أخرى',
'ludo.emote_sad': 'حزين',
'ludo.emote_strong': 'قوي',
'ludo.emote_cool': 'كول',
'ludo.phrase_gl': 'حظ سعيد!',
'ludo.phrase_hurry': 'يلا بسرعة!',
'ludo.phrase_nice': 'نايس!',
'ludo.phrase_oops': 'يا ساتر!',
'ludo.phrase_wow': 'ما شاء الله!'
},
en: {
'app.name': 'EL3AB',
......@@ -253,7 +793,547 @@ const strings = {
'achievements.gameplay': 'Gameplay',
'achievements.social': 'Social',
'achievements.progression': 'Progression',
'achievements.collection': 'Collection'
'achievements.collection': 'Collection',
'common.ok': 'OK',
'common.you': 'You',
'common.opponent': 'Opponent',
'common.player': 'Player',
'common.friend': 'Friend',
'common.back': 'Back',
'common.send': 'Send',
'common.sending': 'Sending...',
'common.sent': '✓ Sent',
'common.failed': 'Failed',
'common.already_sent': 'Already sent',
'common.already_friends': 'Already friends',
'common.accepted': '✓ Accepted',
'common.done': '✓ Done',
'common.error_load': 'Failed to load',
'common.now': 'Now',
'common.minutes_ago': '{n}m ago',
'common.hours_ago': '{n}h ago',
'common.days_ago': '{n}d ago',
'common.online': 'Online',
'common.offline': 'Offline',
'common.level': 'Level {n}',
'common.pieces': '{n} pieces',
'game.round_won': 'You won the round!',
'game.round_lost': 'You lost the round',
'game.round_end': 'Round over',
'game.round_next': 'Next round starting...',
'game.points': '+{n} points',
'game.choose_placement': 'Choose placement',
'game.opponent_plays': 'Opponent playing...',
'game.one_piece_warning': '⚠️ One piece left!',
'game.two_piece_warning': '⚠️ Two pieces left!',
'game.resign_confirm': 'Do you want to resign?',
'game.resign_yes': 'Yes, resign',
'game.resign_no': 'Cancel',
'game.place_first': 'Place first tile',
'game.vs_bot': 'vs Bot',
'game.online': 'Online',
'game.vs_friend': 'Challenge Friend',
'game.roll_dice': 'Roll Dice',
'game.choose_piece': 'Choose piece',
'game.speed_mode': '⚡ Speed Mode!',
'game.turn_of': "{name}'s turn",
'game.bot_thinking': '{name} thinking...',
'game.rating': 'Rating',
'game.coins': 'Coins',
'game.xp': 'XP',
'game.moves_count': '{n} moves',
'game.rounds_count': '{n} round',
'game.rounds_count_plural': '{n} rounds',
'game.checkmate': 'Checkmate',
'game.stalemate': 'Stalemate',
'game.timeout': 'Timeout',
'game.threefold': 'Threefold Repetition',
'game.insufficient': 'Insufficient Material',
'game.win': 'Victory!',
'game.loss': 'Defeat',
'game.draw_game': 'Draw',
'game.resigned': 'You resigned',
'game.opponent_resigned': 'Opponent resigned!',
'game.opponent_abandoned': 'Opponent disconnected',
'game.congrats': 'Congratulations! You won!',
'game.second_place': '2nd Place — Well played!',
'game.third_place': '3rd Place',
'game.fourth_place': '4th Place',
'game.withdrew': 'You withdrew',
'game.player_ranking': 'Player Rankings',
'game.play_again': 'Play Again',
'game.back_to_menu': 'Back to Menu',
'game.not_found': 'Match not found',
'match.disconnected_title': 'Opponent disconnected',
'match.disconnected_waiting': 'Waiting for them to return...',
'match.disconnected_auto_win': 'You will win automatically if they don\'t return in time',
'match.claim_early_win': 'Claim Win',
'match.opponent_returned': '✅ Opponent returned — continue!',
'match.you_won': 'You Won!',
'match.opponent_left': 'Opponent left the match',
'match.return': 'Return',
'match.connection_lost': 'Connection Lost',
'match.reconnecting': 'Reconnecting...',
'match.reconnected': '✅ Connection restored',
'match.restoring': 'Restoring match...',
'match.your_turn_warning': '⏱️ Your turn! Play before time runs out',
'match.opponent_thinking': 'Opponent thinking...',
'match.bot_replacement': '🤖 Opponent left — bot taking over',
'match.your_time_expired': '⏱️ Time expired! Auto-passed',
'match.opponent_time_expired': '⏱️ Opponent\'s time expired — your turn!',
'tournament.round_ready': 'Round {n} ready!',
'tournament.tap_to_play': 'Tap to play your match',
'tournament.play': 'Play',
'tournament.back_to': 'Back to Tournament',
'mp.profile': 'Profile',
'mp.add_friend': 'Add Friend',
'mp.report': 'Report',
'mp.reported': '✓ Reported',
'mp.block': 'Block',
'social.friends_tab': 'Friends',
'social.pending_tab': 'Requests',
'social.online_tab': 'Online',
'social.groups_tab': 'Groups',
'social.activity_tab': 'News',
'social.search_btn': 'Search',
'social.search_placeholder': 'Username or name...',
'social.search_hint': 'Search by any part of the name (min 2 chars)',
'social.search_min_chars': 'Enter at least 2 characters',
'social.searching': 'Searching...',
'social.no_results': 'No results found — try another name',
'social.search_failed': 'Search failed',
'social.add_btn': '+ Add',
'social.no_friends': 'No friends yet',
'social.no_friends_hint': 'Search for players and send friend requests',
'social.search_players': 'Search for players',
'social.friends_count': '{n} friends',
'social.online_count': '{n} online',
'social.no_pending': 'No pending requests',
'social.pending_count': '{n} pending',
'social.sent_friend_request': 'Sent you a friend request',
'social.no_online_friends': 'No friends online right now',
'social.will_appear_online': 'They\'ll appear here when they log in',
'social.online_now_count': '{n} online now',
'social.remove_friend': 'Remove Friend',
'social.remove_confirm': 'Remove {name} from friends?',
'social.challenges_you': '{name} challenges you!',
'social.accept': 'Accept',
'social.error': 'Error',
'social.retry_load': 'Try again',
'social.no_activity': 'No news — play to see your news here',
'social.no_activity_short': 'No news',
'social.game_win': 'Won a match',
'social.game_loss': 'Lost a match',
'social.game_draw': 'Drew a match',
'social.achievement_unlock': 'Unlocked achievement',
'social.level_up': 'Leveled up',
'social.tournament_join': 'Joined tournament',
'social.tournament_win': 'Won tournament',
'social.friend_add': 'Added a new friend',
'challenge.title': 'Challenge Friend',
'challenge.loading_friends': 'Loading friends...',
'challenge.no_friends': 'No friends yet',
'challenge.no_friends_hint': 'Add friends first to challenge them',
'challenge.online_now': 'Online now ({n})',
'challenge.offline': 'Offline',
'challenge.select_game': 'Choose game and time control',
'challenge.send': 'Send Challenge',
'challenge.sending': '⏳ Sending...',
'challenge.sent': '✓ Challenge sent!',
'challenge.failed_create': 'Failed to create match',
'challenge.failed_retry': 'Failed — try again',
'challenge.sent_msg': 'Sent {game} challenge',
'chat.placeholder': 'Type a message...',
'chat.loading_messages': 'Loading...',
'chat.failed_load': 'Failed to load messages',
'chat.start_conversation': 'Start chatting with your friend!',
'chat.challenge_label': '{game} challenge',
'chat.challenge_title': 'Challenge {name}',
'chat.choose_game': 'Choose game',
'lobby.title': 'Challenge Room',
'lobby.ready': 'Ready',
'lobby.waiting': 'Waiting...',
'lobby.waiting_accept': 'Waiting for acceptance...',
'lobby.ready_start': 'Ready to start!',
'lobby.waiting_opponent': 'Waiting for opponent...',
'lobby.auto_start': 'Match will start automatically when challenge is accepted',
'lobby.start': 'Start Match',
'lobby.accepted_preparing': 'Accepted! Preparing match...',
'lobby.opponent_accepted': 'Opponent accepted! Starting...',
'time.bullet': 'Bullet',
'time.blitz': 'Blitz',
'time.rapid': 'Rapid',
'time.classical': 'Classical',
'time.1min': '1 min',
'time.3min': '3 min',
'time.5min': '5 min',
'time.10min': '10 min',
'time.15min': '15 min',
'time.20min': '20 min',
'time.30min': '30 min',
'time.60min': '60 min',
'play.single_player': 'Single Player',
'play.single_player_desc': 'Play against computer',
'play.online_desc': 'Compete against real players',
'play.achievements': 'Achievements',
'play.gift': 'Gift',
'play.coming_soon': 'Coming Soon',
'play.login_required_online': 'Log in to play online',
'play.login_required_friend': 'Log in to challenge a friend',
'domino.title': 'Domino',
'domino.subtitle': 'First to reach the target wins!',
'domino.draw_boneyard': 'Draw from boneyard',
'domino.pass': 'Pass',
'domino.select_bot': 'Choose bot level',
'domino.bot_strategy': 'Each level has a different strategy',
'domino.beginner': 'Beginner',
'domino.beginner_desc': 'Plays randomly',
'domino.intermediate': 'Intermediate',
'domino.intermediate_desc': 'Prefers high points',
'domino.expert': 'Expert',
'domino.expert_desc': 'Strategic and tricky',
'domino.select_target': 'Choose target',
'domino.target_hint': 'First to reach the points wins the match',
'domino.50_points': '50 points',
'domino.50_desc': 'Quick match (~5 min)',
'domino.100_points': '100 points',
'domino.100_desc': 'Classic (~10 min)',
'domino.150_points': '150 points',
'domino.150_desc': 'Long match (~15 min)',
'ludo.title': 'Ludo',
'ludo.subtitle': 'First to get all pieces home wins!',
'ludo.local_play': 'Local Play',
'ludo.local_desc': 'Choose players and bots',
'ludo.online_desc': 'Play against real players',
'ludo.friend_desc': 'Invite your friends to play',
'ludo.settings_online': 'Online Settings',
'ludo.settings_play': 'Game Settings',
'ludo.settings_online_hint': 'Choose number of real players',
'ludo.settings_play_hint': 'Choose player layout',
'ludo.player_count': 'Player Count',
'ludo.classic': 'Classic',
'ludo.triple': 'Triple',
'ludo.duel': 'Duel',
'ludo.player_layout': 'Player Layout',
'ludo.bot_level': 'Bot Level',
'ludo.easy': 'Easy',
'ludo.medium': 'Medium',
'ludo.hard': 'Hard',
'ludo.seat_order': 'Seat Order',
'ludo.search_match': 'Find Match',
'ludo.start_play': 'Start Game',
'ludo.players_no_bot': '{n} players (no bot)',
'ludo.one_player_bots': '1 player + {n} bots',
'ludo.players_bots': '{h} players + {n} bots',
'ludo.red': 'Red',
'ludo.green': 'Green',
'ludo.yellow': 'Yellow',
'ludo.blue': 'Blue',
'ludo.searching': 'Searching...',
'ludo.searching_hint': 'Finding nearby players',
'ludo.place_first': '1st',
'ludo.place_second': '2nd',
'ludo.place_third': '3rd',
'ludo.place_fourth': '4th',
'backgammon.title': 'Backgammon',
'backgammon.subtitle': 'First to bear off all pieces wins!',
'backgammon.bot_desc': 'Choose level and variant',
'backgammon.online_desc': 'Play against real players',
'backgammon.friend_desc': 'Invite your friend to play',
'backgammon.settings_online': 'Online Settings',
'backgammon.settings_game': 'Game Settings',
'backgammon.game_type': 'Game Type',
'backgammon.sheshbesh': 'Shesh Besh',
'backgammon.mahbousa': 'Mahbousa',
'backgammon.thirtyone': '31',
'backgammon.match_length': 'Match Length',
'backgammon.short_match': 'Quick',
'backgammon.medium_match': 'Short',
'backgammon.normal_match': 'Normal',
'backgammon.long_match': 'Long',
'backgammon.doubling_cube': 'Doubling Cube',
'backgammon.enabled': 'Enabled',
'backgammon.disabled': 'Disabled',
'backgammon.searching': 'Searching...',
'backgammon.searching_hint': 'Finding a nearby player',
'backgammon.win_title': 'Congratulations! You won!',
'backgammon.lose_title': 'You lost this time...',
'backgammon.win_subtitle': 'Great performance!',
'backgammon.lose_subtitle': 'Try again!',
'backgammon.abandon_subtitle': 'Opponent left the game',
'backgammon.from_match': 'of {n}',
'backgammon.opponent_doubles': 'Opponent doubles!',
'backgammon.decline': 'Decline',
'backgammon.accept': 'Accept',
'backgammon.no_moves': 'No moves available!',
'backgammon.bot_accepted': 'Bot accepted! ×{n}',
'backgammon.bot_declined': 'Bot declined! You win the round',
'emote.good_move': 'Great move',
'emote.think': 'Thinking...',
'emote.hurry': 'Hurry up',
'emote.wow': 'Wow!',
'emote.laugh': 'Haha',
'emote.angry': 'Angry',
'emote.hello': 'Hello',
'emote.nice_game': 'Nice game!',
'emote.great_move': 'Great move!',
'emote.lucky': 'Lucky!',
'emote.think_faster': 'Think faster...',
'emote.rematch': 'Rematch?',
'analysis.title': 'Game Analysis',
'analysis.analyzing': 'Analyzing...',
'analysis.start': 'Start',
'analysis.engine_analyzing': 'Engine analyzing...',
'analysis.failed': 'Analysis failed',
'analysis.openings_book': 'Openings Book',
'review.title': 'Game Review',
'review.analyzing_moves': 'Analyzing each move...',
'review.move_classification': 'Move Classification',
'review.brilliant': '!! Brilliant',
'review.great': '! Great',
'review.best': 'Best move',
'review.good': '✓ Good',
'review.book': 'Book',
'review.inaccuracy': '?! Inaccuracy',
'review.mistake': '? Mistake',
'review.blunder': '?? Blunder',
'review.full_analysis': 'Full Analysis (move by move)',
'review.unknown_opening': 'Unknown',
'spectate.live': 'Live',
'spectate.black': 'Black',
'spectate.white': 'White',
'spectate.white_wins': 'White wins',
'spectate.black_wins': 'Black wins',
'spectate.game_over': 'Game over',
'history.no_matches': 'No matches yet — play to see them here',
'history.win': 'Win',
'history.loss': 'Loss',
'history.draw': 'Draw',
'history.insufficient_data': 'Insufficient data',
'history.matches_count': '{n} matches',
'result.copied': '✓ Copied',
'result.copy_pgn': 'Copy',
'result.share_pgn': 'Share PGN',
'result.analyze': 'Analyze Game',
'result.rematch': 'Rematch',
'classifier.excellent': 'Excellent',
'classifier.great': 'Great',
'classifier.good': 'Good',
'classifier.average': 'Average',
'classifier.poor': 'Poor',
'profile.edit_title': 'Edit Profile',
'profile.basic_info': 'Basic Info',
'profile.display_name_en': 'Display Name (English)',
'profile.display_name_ar': 'Display Name (Arabic)',
'profile.bio_en': 'Bio (English)',
'profile.bio_ar': 'Bio (Arabic)',
'profile.location': 'Location',
'profile.country': 'Country',
'profile.select_country': 'Select Country',
'profile.city': 'City',
'profile.preferred_language': 'Preferred Language',
'profile.fide_info': 'FIDE Info',
'profile.fide_classical': 'FIDE Classical Rating',
'profile.fide_rapid': 'FIDE Rapid Rating',
'profile.fide_blitz': 'FIDE Blitz Rating',
'profile.fide_title': 'FIDE Title',
'profile.save': 'Save Changes',
'profile.saving': 'Saving...',
'profile.save_success': 'Changes saved successfully',
'profile.save_failed': 'Failed to save changes',
'profile.uploading': 'Uploading...',
'profile.upload_failed': 'Failed to upload image',
'profile.matches': 'Matches',
'profile.wins': 'Wins',
'profile.streak': 'Streak',
'profile.rating_section': 'Rating',
'profile.chess_rapid': 'Chess (Rapid)',
'profile.chess_blitz': 'Chess (Blitz)',
'profile.chess_bullet': 'Chess (Bullet)',
'profile.org_section': 'Organization',
'profile.edit_btn': 'Edit Profile',
'profile.org_apply': 'Join Organization',
'profile.logout': 'Logout',
'profile.view_title': 'Profile',
'profile.add_friend': '➕ Add Friend',
'profile.friend_sent': '✓ Request sent',
'profile.pending_request': 'Friend request pending',
'profile.challenge': 'Challenge',
'profile.message': 'Message',
'profile.playing_now': 'Playing now — {game}',
'profile.watch': 'Watch',
'profile.member': 'Member',
'profile.pending_review': 'Under Review',
'profile.rejected': 'Rejected',
'profile.rejection_reason': 'Reason: {reason}',
'profile.membership_request': 'Membership Request',
'profile.no_org': 'Not a member of any organization yet',
'org.join_title': 'Join Organization',
'org.no_orgs': 'No organizations available',
'org.apply': 'Apply',
'org.reapply': 'Reapply',
'org.apply_title': 'Apply to join — {name}',
'org.doc_type': 'Document Type',
'org.notes': 'Notes (optional)',
'org.notes_placeholder': 'Message to organization admin...',
'org.proof_image': 'Proof Image',
'org.tap_to_select': 'Tap to select image',
'org.file_limit': 'PNG, JPG, WebP — max 5MB',
'org.file_too_large': 'File is larger than 5MB',
'org.submit': 'Submit Application',
'org.submitting': 'Submitting...',
'org.submit_success': 'Application submitted successfully',
'org.submit_failed': 'Failed to submit application',
'room.waiting_friend': 'Waiting for friend...',
'room.challenge_from': 'Challenge from {name}',
'room.send_invite': 'Send the invite to your friend',
'room.tap_accept': 'Tap accept to start',
'room.match_created': 'Match created, waiting for acceptance...',
'daily.title': 'Daily Reward',
'daily.streak_days': '{n} day streak',
'daily.day': 'Day {n}',
'daily.claimed_today': 'Today\'s reward claimed',
'daily.come_back': 'Come back tomorrow for a bigger reward!',
'daily.today_reward': 'Today\'s Reward',
'daily.claim_btn': 'Claim Reward',
'daily.hint': 'Each day you claim increases the amount • Day 7 = 300 coins!',
'daily.claiming': 'Claiming...',
'daily.failed': 'Failed — try again',
'challenges.title': 'Daily Challenges',
'challenges.all_done': 'All challenges completed today!',
'challenges.streak': '{n} days',
'challenges.claim': 'Claim',
'rank.bronze': 'Bronze',
'rank.silver': 'Silver',
'rank.gold': 'Gold',
'rank.platinum': 'Platinum',
'rank.diamond': 'Diamond',
'rank.master': 'Master',
'rank.grandmaster': 'Grandmaster',
'shop.insufficient_balance': 'Insufficient balance',
'shop.purchased': 'Purchased!',
'group.create': 'Create',
'group.no_groups': 'No groups yet',
'group.no_groups_hint': 'Create a group and invite your friends!',
'group.members': 'Members',
'group.owner': 'Owner',
'group.admin': 'Admin',
'group.play': 'Play',
'group.load_error': 'Error loading group',
'group.play_invite': 'Play invite',
'group.players': 'Players',
'group.join': 'Join',
'group.accepted': '✓ Accepted',
'group.invite_sent': 'Play invite sent!',
'group.create_title': 'Create Group',
'group.name_label': 'Group Name',
'group.select_friends': 'Choose friends to add',
'group.create_btn': 'Create Group',
'group.name_min': 'Enter group name (at least 2 characters)',
'group.creating': 'Creating...',
'group.created': 'Group created!',
'group.add': 'Add',
'group.leave': 'Leave Group',
'group.remove': 'Remove',
'group.remove_member': 'Remove Member',
'group.leave_confirm': 'Do you want to leave this group?',
'group.leave_btn': 'Leave',
'group.stay': 'Stay',
'auth.login_title': 'Login',
'auth.login_btn': 'Login',
'auth.register_link': 'Register now',
'auth.forgot_password': 'Forgot password?',
'auth.login_failed': 'Login failed',
'auth.invalid_email': 'Invalid email',
'auth.guest_login': 'Login as guest',
'auth.register_title': 'Create Account',
'auth.confirm_password': 'Confirm Password',
'auth.has_account': 'Already have an account?',
'auth.login_link': 'Login',
'auth.register_failed': 'Registration failed',
'auth.password_mismatch': 'Passwords do not match',
'auth.username_short': 'Username too short',
'auth.password_short': 'Password too short (min 6 characters)',
'app.tagline': 'Play with your friends',
'common.or': 'or',
'common.play_again': 'Play Again',
'tournament.title': 'Tournaments',
'tournament.upcoming': 'Upcoming',
'tournament.active': 'Active',
'tournament.completed': 'Completed',
'tournament.no_tournaments': 'No tournaments available',
'tournament.players': '{n} players',
'tournament.join': 'Join',
'tournament.joined': 'Joined',
'tournament.starts_in': 'Starts in {n}',
'tournament.entry_fee': 'Entry Fee',
'tournament.prize': 'Prize',
'tournament.free': 'Free',
'tournament.waiting': 'Waiting for players...',
'tournament.players_ready': '{n}/{total} players ready',
'tournament.starting_soon': 'Starting soon!',
'tournament.leave': 'Leave',
'tournament.ready': 'Ready',
'tournament.bracket': 'Tournament Bracket',
'tournament.round': 'Round {n}',
'tournament.final': 'Final',
'tournament.semifinal': 'Semifinal',
'tournament.quarterfinal': 'Quarterfinal',
'tournament.bye': 'Bye',
'tournament.match_starting': 'Match starting...',
'tournament.opponent': 'Opponent',
'tournament.round_of': 'Round {n}',
'tournament.champion': 'Champion!',
'tournament.eliminated': 'Eliminated',
'tournament.final_rank': 'Rank #{n}',
'tournament.prize_won': 'Prize: {n} coins',
'tournament.back_to_lobby': 'Back',
'tournament.hub_title': 'Live Tournaments',
'tournament.create': 'Create Tournament',
'tournament.browse': 'Browse Tournaments',
'tournament.arena': 'Arena',
'tournament.leave_arena': 'Leave Arena',
'tournament.no_results': 'No results yet',
'tournament.leaderboard': 'Leaderboard',
'tournament.tab_info': 'Info',
'tournament.tab_standings': 'Standings',
'tournament.tab_rounds': 'Rounds',
'tournament.tab_my_games': 'My Games',
'tournament.tab_bracket': 'Bracket',
'tournament.tab_arena': 'Arena',
'tournament.registration_open': 'Registration Open',
'tournament.coming_soon': 'Coming Soon',
'tournament.round_n': 'Round {n}/{total}',
'tournament.live_matches': 'Live Matches',
'tournament.no_match': 'No match found',
'spectate.watch': 'Watch',
'chess.rating': 'Rating',
'chess.rating_history': 'Rating History',
'chess.highest': 'Highest',
'chess.lowest': 'Lowest',
'chess.current': 'Current',
'puzzle.title': 'Chess Puzzle',
'puzzle.correct': 'Correct!',
'puzzle.wrong': 'Wrong answer',
'org.members': 'Members',
'org.game': 'Game',
'org.members_section': 'Members',
'org.join': 'Join Organization',
'org.joined': 'Joined',
'org.title': 'Organizations',
'org.join_btn': 'Join',
'org.doc_membership': 'Membership Card',
'org.doc_id': 'ID Card',
'org.doc_receipt': 'Payment Receipt',
'org.doc_other': 'Other',
'ludo.emote_sad': 'Sad',
'ludo.emote_strong': 'Strong',
'ludo.emote_cool': 'Cool',
'ludo.phrase_gl': 'Good luck!',
'ludo.phrase_hurry': 'Hurry up!',
'ludo.phrase_nice': 'Nice!',
'ludo.phrase_oops': 'Oops!',
'ludo.phrase_wow': 'Wow!'
}
};
......
......@@ -5,6 +5,7 @@ import * as audio from './audio.js';
import * as juice from './juice.js';
import * as bus from './bus.js';
import * as scene from './scene.js';
import { t } from './i18n.js';
let overlayEl = null;
......@@ -45,11 +46,11 @@ export function showOpponentDisconnect() {
<div style="width:64px;height:64px;margin:0 auto 16px;border-radius:50%;background:#1e1e3a;display:flex;align-items:center;justify-content:center;">
<div style="width:16px;height:16px;border:3px solid #FBBF24;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;"></div>
</div>
<div style="font-size:18px;font-weight:700;color:#f8fafc;margin-bottom:8px;">الخصم انقطع اتصاله</div>
<div style="font-size:13px;color:#94a3b8;margin-bottom:16px;">في انتظار عودته...</div>
<div style="font-size:18px;font-weight:700;color:#f8fafc;margin-bottom:8px;">${t('match.disconnected_title')}</div>
<div style="font-size:13px;color:#94a3b8;margin-bottom:16px;">${t('match.disconnected_waiting')}</div>
<div id="abandon-countdown" style="font-size:32px;font-weight:800;color:#FBBF24;font-family:Inter,monospace;margin-bottom:16px;">60</div>
<div style="font-size:11px;color:#64748b;">ستفوز تلقائياً إذا لم يعد خلال الوقت المحدد</div>
<button id="claim-win-btn" style="margin-top:16px;padding:10px 24px;background:#E4AC38;border:none;border-radius:8px;color:#1a1a1a;font-weight:700;font-size:13px;cursor:pointer;opacity:0.5;pointer-events:none;">فوز مبكر</button>
<div style="font-size:11px;color:#64748b;">${t('match.disconnected_auto_win')}</div>
<button id="claim-win-btn" style="margin-top:16px;padding:10px 24px;background:#E4AC38;border:none;border-radius:8px;color:#1a1a1a;font-weight:700;font-size:13px;cursor:pointer;opacity:0.5;pointer-events:none;">${t('match.claim_early_win')}</button>
</div>
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
`);
......@@ -87,7 +88,7 @@ export function showOpponentReconnect() {
hide();
// Brief toast
showToast('✅ الخصم عاد — استمروا!', '#34D399');
showToast(t('match.opponent_returned'), '#34D399');
audio.play('notification');
}
......@@ -101,9 +102,9 @@ function showAutoWin() {
show(`
<div style="text-align:center;padding:32px;max-width:300px;">
<div style="font-size:56px;margin-bottom:12px;animation:float 2s ease-in-out infinite;">🏆</div>
<div style="font-size:22px;font-weight:800;color:#34D399;margin-bottom:8px;">فزت!</div>
<div style="font-size:14px;color:#94a3b8;margin-bottom:20px;">الخصم غادر المباراة</div>
<button id="abandon-back-btn" style="padding:12px 32px;background:linear-gradient(135deg,#E4AC38,#FFCC66);border:none;border-radius:10px;color:#1a1a1a;font-weight:700;font-size:15px;cursor:pointer;">العودة</button>
<div style="font-size:22px;font-weight:800;color:#34D399;margin-bottom:8px;">${t('match.you_won')}</div>
<div style="font-size:14px;color:#94a3b8;margin-bottom:20px;">${t('match.opponent_left')}</div>
<button id="abandon-back-btn" style="padding:12px 32px;background:linear-gradient(135deg,#E4AC38,#FFCC66);border:none;border-radius:10px;color:#1a1a1a;font-weight:700;font-size:15px;cursor:pointer;">${t('match.return')}</button>
</div>
`);
......@@ -125,8 +126,8 @@ export function showConnectionLost() {
<div style="width:48px;height:48px;margin:0 auto 16px;border-radius:50%;background:#EF4444;display:flex;align-items:center;justify-content:center;">
<span style="font-size:24px;">⚠️</span>
</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:6px;">انقطع الاتصال</div>
<div style="font-size:13px;color:#94a3b8;">جاري إعادة الاتصال...</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:6px;">${t('match.connection_lost')}</div>
<div style="font-size:13px;color:#94a3b8;">${t('match.reconnecting')}</div>
<div style="margin-top:16px;">
<div style="width:120px;height:4px;background:#1e1e3a;border-radius:2px;margin:0 auto;overflow:hidden;">
<div style="width:30%;height:100%;background:#FBBF24;border-radius:2px;animation:loading 1.5s ease-in-out infinite;"></div>
......@@ -140,7 +141,7 @@ export function showConnectionLost() {
// ========== CONNECTION RESTORED ==========
export function showConnectionRestored() {
hide();
showToast('✅ تم استعادة الاتصال', '#34D399');
showToast(t('match.reconnected'), '#34D399');
}
// ========== RECONNECTING (tab refresh recovery) ==========
......@@ -148,7 +149,7 @@ export function showReconnecting() {
show(`
<div style="text-align:center;padding:32px;">
<div style="width:48px;height:48px;margin:0 auto 16px;border:3px solid #E4AC38;border-top-color:transparent;border-radius:50%;animation:spin 1s linear infinite;"></div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;">جاري استعادة المباراة...</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;">${t('match.restoring')}</div>
</div>
<style>@keyframes spin { to { transform: rotate(360deg); } }</style>
`);
......@@ -156,7 +157,7 @@ export function showReconnecting() {
// ========== YOUR TURN REMINDER (if idle too long) ==========
export function showTurnReminder() {
showToast('⏱️ دورك! العب قبل انتهاء الوقت', '#FBBF24');
showToast(t('match.your_turn_warning'), '#FBBF24');
juice.hapticMedium();
}
......@@ -168,7 +169,7 @@ export function showOpponentThinking() {
const el = document.createElement('div');
el.id = 'thinking-indicator';
el.style.cssText = 'position:fixed;top:60px;left:50%;transform:translateX(-50%);background:rgba(30,30,58,0.95);padding:6px 14px;border-radius:20px;font-size:12px;color:#94a3b8;z-index:400;display:flex;align-items:center;gap:6px;border:1px solid rgba(255,255,255,0.06);';
el.innerHTML = `<div style="width:8px;height:8px;border:2px solid #94a3b8;border-top-color:transparent;border-radius:50%;animation:spin 0.8s linear infinite;"></div> الخصم يفكر...`;
el.innerHTML = `<div style="width:8px;height:8px;border:2px solid #94a3b8;border-top-color:transparent;border-radius:50%;animation:spin 0.8s linear infinite;"></div> ${t('match.opponent_thinking')}`;
document.body.appendChild(el);
}
......@@ -190,17 +191,17 @@ function showToast(message, color = '#f8fafc') {
// ========== BOT REPLACEMENT ==========
export function showBotReplacement() {
showToast('🤖 الخصم غادر — بوت يكمل بدلاً منه', '#FBBF24');
showToast(t('match.bot_replacement'), '#FBBF24');
juice.hapticLight();
}
// ========== TURN TIMED OUT ==========
export function showTurnTimedOut(isMyTimeout) {
if (isMyTimeout) {
showToast('⏱️ انتهى وقتك! تم التمرير تلقائياً', '#EF4444');
showToast(t('match.your_time_expired'), '#EF4444');
juice.hapticMedium();
} else {
showToast('⏱️ انتهى وقت الخصم — دورك!', '#34D399');
showToast(t('match.opponent_time_expired'), '#34D399');
juice.hapticLight();
}
}
......
......@@ -3,6 +3,7 @@
import * as audio from './audio.js';
import * as juice from './juice.js';
import { t } from './i18n.js';
let modalEl = null;
let backdropEl = null;
......@@ -86,8 +87,8 @@ export function confirm(message, options = {}) {
const {
title = '',
confirmText = 'تأكيد',
cancelText = 'إلغاء',
confirmText = t('common.confirm'),
cancelText = t('common.cancel'),
confirmColor = '#E4AC38',
danger = false,
icon = ''
......@@ -163,7 +164,7 @@ export function confirm(message, options = {}) {
* Returns a Promise<void>
*/
export function alert(message, options = {}) {
const { title = '', buttonText = 'حسناً', icon = '' } = options;
const { title = '', buttonText = t('common.ok'), icon = '' } = options;
ensureContainer();
audio.play('notification');
......
......@@ -33,7 +33,7 @@ export function renderOpponentBar(container, opponent, options = {}) {
<div id="mp-conn-dot" style="position:absolute;bottom:-1px;right:-1px;width:10px;height:10px;border-radius:50%;background:#34D399;border:2px solid #0f0f1e;"></div>
</div>
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${opponent.display_name || opponent.username || 'خصم'}</div>
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${opponent.display_name || opponent.username || t('common.opponent')}</div>
${showRating ? `<div style="font-size:11px;color:#64748b;">${emoji('star', '⭐', 11)} ${opponent.rating || opponent.elo_rapid || '1200'}</div>` : ''}
</div>
<div id="mp-opponent-status" style="font-size:10px;color:#64748b;"></div>
......@@ -59,11 +59,11 @@ function showOpponentActions(container, opponent) {
menu.id = 'mp-opponent-menu';
menu.style.cssText = 'position:absolute;top:50px;right:12px;background:#1a1a2e;border:1px solid rgba(255,255,255,0.08);border-radius:12px;padding:8px;z-index:100;box-shadow:0 8px 24px rgba(0,0,0,0.5);display:flex;flex-direction:column;gap:4px;min-width:140px;';
menu.innerHTML = `
<button class="mp-action" data-action="profile">${emoji('person', '👤', 12)} الملف الشخصي</button>
<button class="mp-action" data-action="friend">➕ إضافة صديق</button>
<button class="mp-action" data-action="profile">${emoji('person', '👤', 12)} ${t('mp.profile')}</button>
<button class="mp-action" data-action="friend">➕ ${t('mp.add_friend')}</button>
<button class="mp-action" data-action="mute">${emoji('mute', '🔇', 12)} ${t('block.mute')}</button>
<button class="mp-action" data-action="block" style="color:#EF4444;">${emoji('block', '🚫', 12)} ${t('block.block')}</button>
<button class="mp-action" data-action="report" style="color:#EF4444;">⚠️ إبلاغ</button>
<button class="mp-action" data-action="report" style="color:#EF4444;">⚠️ ${t('mp.report')}</button>
`;
const style = document.createElement('style');
......@@ -76,10 +76,10 @@ function showOpponentActions(container, opponent) {
btn.style.pointerEvents = 'none';
const result = await addFriendFromGame(opponent.id);
if (result === true) {
btn.textContent = '✓ تم الإرسال';
btn.textContent = t('common.sent');
btn.style.color = '#34D399';
} else {
btn.textContent = result || '✓ مرسل سابقاً';
btn.textContent = result || t('common.already_sent');
btn.style.color = '#64748b';
}
juice.hapticLight();
......@@ -102,10 +102,10 @@ function showOpponentActions(container, opponent) {
menu.querySelector('[data-action="block"]').addEventListener('click', async () => {
menu.remove();
const confirmed = await modal.confirm(t('block.confirm_block'), {
title: 'حظر',
title: t('mp.block'),
icon: '🚫',
confirmText: 'حظر',
cancelText: 'إلغاء',
confirmText: t('common.confirm'),
cancelText: t('common.cancel'),
danger: true
});
if (!confirmed) return;
......@@ -117,7 +117,7 @@ function showOpponentActions(container, opponent) {
menu.querySelector('[data-action="report"]').addEventListener('click', () => {
reportOpponent(opponent.id);
menu.querySelector('[data-action="report"]').textContent = '✓ تم الإبلاغ';
menu.querySelector('[data-action="report"]').textContent = t('mp.reported');
setTimeout(() => menu.remove(), 1000);
});
......@@ -189,12 +189,12 @@ export function startDisconnectWatch(matchId, matchType, timeoutMs = 60000) {
if (elapsed > timeoutMs) {
// Opponent disconnected too long — claim win
if (dot) dot.style.background = '#EF4444';
if (status) status.textContent = 'انقطع الاتصال';
if (status) status.textContent = t('game.opponent_disconnected');
clearInterval(disconnectTimer);
// Could auto-claim win here
} else if (elapsed > 15000) {
if (dot) dot.style.background = '#FBBF24';
if (status) status.textContent = 'اتصال ضعيف';
if (status) status.textContent = t('game.weak_connection');
} else {
if (dot) dot.style.background = '#34D399';
if (status) status.textContent = '';
......@@ -230,15 +230,15 @@ export async function addFriendFromGame(opponentId) {
try {
const res = await net.post('friends.php', { action: 'request', target_id: opponentId });
if (res.error) {
if (res.error.includes('Already friends')) return 'صديق بالفعل';
if (res.error.includes('pending')) return 'مرسل سابقاً';
if (res.error.includes('Already friends')) return t('common.already_friends');
if (res.error.includes('pending')) return t('common.already_sent');
return res.error;
}
juice.hapticLight();
return true;
} catch (e) {
const msg = e.message || '';
if (msg.includes('Already') || msg.includes('pending')) return 'مرسل سابقاً';
if (msg.includes('Already') || msg.includes('pending')) return t('common.already_sent');
return false;
}
}
......
......@@ -5,6 +5,7 @@ import * as audio from './audio.js';
import * as mp from './multiplayer.js';
import { getTier } from '../modules/rewards/scenes/ranks.js';
import { emoji } from './theme.js';
import { t } from './i18n.js';
export function render(container, player, options = {}) {
const { position = 'top', isActive = false, showRating = true, showLevel = true, isSelf = false } = options;
......@@ -23,7 +24,7 @@ export function render(container, player, options = {}) {
<div class="pp-status-dot ${player.isOnline !== false ? 'online' : ''}"></div>
</div>
<div class="pp-info">
<div class="pp-name">${player.display_name || player.username || (isSelf ? 'أنت' : 'خصم')}</div>
<div class="pp-name">${player.display_name || player.username || (isSelf ? t('common.you') : t('common.opponent'))}</div>
<div class="pp-meta">
${showLevel ? `<span class="pp-level">Lv.${player.level || 1}</span>` : ''}
${tier ? `<span class="pp-tier" style="color:${tier.color};">${tier.icon}</span>` : ''}
......@@ -84,8 +85,8 @@ function showPlayerPopup(container, profile) {
<div style="font-size:16px;font-weight:700;color:#f8fafc;">${profile.display_name || profile.username}</div>
<div style="font-size:12px;color:#64748b;margin:4px 0 16px;">Level ${profile.level || 1} · ${profile.elo_rapid || 1200} ELO</div>
<div style="display:flex;gap:8px;justify-content:center;">
<button id="pp-add" style="padding:8px 18px;background:#2563EB;border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;">➕ أضف صديق</button>
<button id="pp-close" style="padding:8px 18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#94a3b8;font-size:12px;cursor:pointer;">إغلاق</button>
<button id="pp-add" style="padding:8px 18px;background:#2563EB;border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;">${t('social.add_btn')}</button>
<button id="pp-close" style="padding:8px 18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#94a3b8;font-size:12px;cursor:pointer;">${t('common.close')}</button>
</div>
`;
document.body.appendChild(popup);
......@@ -94,7 +95,7 @@ function showPlayerPopup(container, profile) {
popup.querySelector('#pp-add').addEventListener('click', async () => {
const success = await mp.addFriendFromGame(profile.id);
if (success) {
popup.querySelector('#pp-add').textContent = '✓ تم';
popup.querySelector('#pp-add').textContent = t('common.done');
popup.querySelector('#pp-add').style.background = '#34D399';
}
setTimeout(() => popup.remove(), 1000);
......
......@@ -2,6 +2,7 @@ import * as net from './net.js';
import * as bus from './bus.js';
import * as store from './store.js';
import * as realtime from './realtime.js';
import { t } from './i18n.js';
let pendingMatches = [];
let activeTournaments = [];
......@@ -25,10 +26,10 @@ function showPairingToast(data) {
toast.innerHTML = `
<span style="font-size:20px;">🏆</span>
<div style="flex:1;">
<div style="font-size:13px;font-weight:700;color:#f8fafc;">جولة ${data.roundNumber} جاهزة!</div>
<div style="font-size:11px;color:#94a3b8;">اضغط للعب مباراتك</div>
<div style="font-size:13px;font-weight:700;color:#f8fafc;">${t('tournament.round_ready', { n: data.roundNumber })}</div>
<div style="font-size:11px;color:#94a3b8;">${t('tournament.tap_to_play')}</div>
</div>
<button id="toast-play-btn" style="padding:6px 14px;background:#E4AC38;border:none;border-radius:8px;color:#000;font-weight:700;font-size:12px;cursor:pointer;">العب</button>
<button id="toast-play-btn" style="padding:6px 14px;background:#E4AC38;border:none;border-radius:8px;color:#000;font-weight:700;font-size:12px;cursor:pointer;">${t('tournament.play')}</button>
`;
document.body.appendChild(toast);
......
......@@ -23,7 +23,7 @@ export function mountLogin(el) {
<div style="width:100%;max-width:320px;margin-top:var(--s-4);display:flex;flex-direction:column;gap:var(--s-3);">
<div style="display:flex;align-items:center;gap:var(--s-3);margin:var(--s-2) 0;">
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
<span style="color:var(--text-secondary);font-size:12px;">أو</span>
<span style="color:var(--text-secondary);font-size:12px;">${t('common.or')}</span>
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
</div>
<button class="btn w-full" id="google-btn" style="background:#fff;color:#333;font-weight:600;min-height:44px;display:flex;align-items:center;justify-content:center;gap:var(--s-2);">
......
......@@ -29,7 +29,7 @@ export function mountRegister(el) {
<div style="width:100%;max-width:320px;margin-top:var(--s-4);">
<div style="display:flex;align-items:center;gap:var(--s-3);margin:var(--s-2) 0;">
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
<span style="color:var(--text-secondary);font-size:12px;">أو</span>
<span style="color:var(--text-secondary);font-size:12px;">${t('common.or')}</span>
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
</div>
<button class="btn w-full" id="google-reg-btn" style="background:#fff;color:#333;font-weight:600;min-height:44px;display:flex;align-items:center;justify-content:center;gap:var(--s-2);">
......
......@@ -9,7 +9,7 @@ export function mountSplash(el) {
el.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:var(--s-6);padding:var(--s-4);">
<div class="splash-logo" style="animation:glow 2s ease-in-out infinite;">${assetImg('logo', 'EL3AB', 180, 60)}</div>
<div class="splash-sub" style="color:var(--text-secondary);font-size:14px;animation:pulse 2s ease-in-out infinite;">العب · نافس · اكسب</div>
<div class="splash-sub" style="color:var(--text-secondary);font-size:14px;animation:pulse 2s ease-in-out infinite;">${t('app.tagline')}</div>
<div class="splash-loader" style="margin-top:var(--s-8);">
<div style="width:120px;height:4px;background:var(--bg-elevated);border-radius:2px;overflow:hidden;">
<div style="width:0%;height:100%;background:var(--gold);border-radius:2px;animation:loadBar 2s ease-out forwards;"></div>
......
......@@ -41,13 +41,13 @@ export function mountGame(el, p) {
if (!useCube) match.cube = null;
const player = store.get('player') || {};
const myName = player.display_name || player.username || 'أنت';
const myName = player.display_name || player.username || t('common.you');
const myAvatar = player.avatar_url
? `<img src="${player.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`
: emoji('person', '👤', 18);
const myLevel = player.level || 1;
const rawOppName = mode === 'bot' ? 'بوت' : (params.opponentName || 'خصم');
const rawOppName = mode === 'bot' ? t('game.bot') : (params.opponentName || t('common.opponent'));
const oppName = rawOppName.replace(/[<>"&]/g, c => ({'<':'&lt;','>':'&gt;','"':'&quot;','&':'&amp;'}[c]));
const oppAvatarUrl = params.opponentAvatar ? String(params.opponentAvatar).replace(/[<>"]/g, '') : '';
const oppAvatar = oppAvatarUrl
......@@ -70,28 +70,28 @@ export function mountGame(el, p) {
</div>
<div class="bgg-match-info">
<span class="bgg-cube-display" id="cube-display">×${cubeVal}</span>
<span class="bgg-match-length">${matchLength} نقاط</span>
<span class="bgg-match-length">${matchLength} ${t('game.points', { n: '' }).trim()}</span>
</div>
</div>
<div class="bgg-board-area">
<canvas id="bg-canvas"></canvas>
<div class="bgg-turn-badge" id="turn-indicator">دورك</div>
<div class="bgg-turn-badge" id="turn-indicator">${t('game.your_turn')}</div>
</div>
<div class="bgg-controls">
<button class="bgg-action-btn bgg-quit" id="btn-quit">✕</button>
<button class="bgg-action-btn bgg-emote-btn" id="btn-emote">${emoji('sparkles', '✨', 18)}</button>
<button class="bgg-roll-btn" id="btn-roll">${emoji('game_die', '🎲', 20)} ارمِ النرد</button>
<button class="bgg-roll-btn" id="btn-roll">${emoji('game_die', '🎲', 20)} ${t('game.roll_dice')}</button>
<button class="bgg-cube-btn" id="btn-double" style="display:none;">×2</button>
</div>
<div class="bgg-double-offer" id="double-offer" style="display:none;">
<div class="bgg-double-icon">×<span id="double-val">2</span></div>
<p>الخصم يضاعف!</p>
<p>${t('backgammon.opponent_doubles')}</p>
<div class="bgg-double-actions">
<button class="bgg-decline-btn" id="btn-decline-double">رفض</button>
<button class="bgg-accept-btn" id="btn-accept-double">قبول</button>
<button class="bgg-decline-btn" id="btn-decline-double">${t('backgammon.decline')}</button>
<button class="bgg-accept-btn" id="btn-accept-double">${t('backgammon.accept')}</button>
</div>
</div>
......@@ -131,7 +131,7 @@ export function mountGame(el, p) {
if (mode === 'live') {
matchSession.create(params.matchId, 'backgammon', {
onOpponentMove: handleServerState,
onOpponentDisconnect: () => bus.emit('toast', { text: 'الخصم انقطع...' }),
onOpponentDisconnect: () => bus.emit('toast', { text: t('game.opponent_disconnected') }),
onOpponentAbandon: () => endMatchWithWin()
});
}
......@@ -305,7 +305,7 @@ function updateUI() {
// Turn indicator
const ti = container.querySelector('#turn-indicator');
if (ti) {
ti.textContent = isMyTurn ? 'دورك' : 'دور الخصم';
ti.textContent = isMyTurn ? t('game.your_turn') : t('game.opponent_turn');
ti.className = `bgg-turn-badge ${isMyTurn ? 'bgg-turn-mine' : 'bgg-turn-opp'}`;
}
}
......@@ -443,7 +443,7 @@ function onRollClick() {
rollDice(game);
if (game.movesLeft.length === 0) {
bus.emit('toast', { text: 'لا حركات متاحة!' });
bus.emit('toast', { text: t('backgammon.no_moves') });
setTimeout(endTurn, 1000);
return;
}
......@@ -472,10 +472,10 @@ function onDoubleClick() {
const oppPips = getPipCount(game.state, myColor === WHITE ? BLACK : WHITE, game.variant);
if (shouldBotAccept(match.cube, oppPips, myPips, params.difficulty)) {
acceptDouble(match.cube);
bus.emit('toast', { text: `البوت قبل! ×${match.cube.value}` });
bus.emit('toast', { text: t('backgammon.bot_accepted', { n: match.cube.value }) });
} else {
declineDouble(match.cube);
bus.emit('toast', { text: 'البوت رفض! فزت بالجولة' });
bus.emit('toast', { text: t('backgammon.bot_declined') });
showGameResult(endGame(match, myColor, 1));
return;
}
......@@ -583,8 +583,8 @@ function showGameResult(result) {
});
}, 1800);
} else {
const who = result.winner === myColor ? 'فزت' : 'البوت فاز';
bus.emit('toast', { text: `${who} بالجولة (+${result.stake})` });
const who = result.winner === myColor ? t('game.round_won') : t('game.round_lost');
bus.emit('toast', { text: `${who} (+${result.stake})` });
setTimeout(startNewGameRound, 2000);
}
}
......@@ -648,7 +648,7 @@ function handleServerState(data) {
function setupEmotePanel(el) {
const panel = el.querySelector('#emote-panel');
const emotes = ['🎲', '😤', '🤔', '👏', '😂', '🔥', '💀', '🫡'];
const phrases = ['!لعبة حلوة', '!حركة ممتازة', '!حظ', '...فكّر أسرع', '?ريماتش', 'gg wp'];
const phrases = [t('emote.nice_game'), t('emote.great_move'), t('emote.lucky'), t('emote.think_faster'), t('emote.rematch'), 'gg wp'];
panel.innerHTML = `
<div class="bgg-emotes">${emotes.map(e => `<button class="bgg-emote-btn">${e}</button>`).join('')}</div>
<div class="bgg-phrases">${phrases.map(p => `<button class="bgg-phrase-btn">${p}</button>`).join('')}</div>
......
......@@ -2,6 +2,7 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
let confettiRaf = null;
......@@ -19,23 +20,23 @@ export function mountResult(el, params) {
<div class="bgr-trophy-ring ${didWin ? 'bgr-win-ring' : 'bgr-lose-ring'}">
<div class="bgr-trophy">${didWin ? emoji('trophy', '🏆', 56) : emoji('pensive', '😔', 56)}</div>
</div>
<h1 class="bgr-title">${didWin ? 'مبروك! فزت بالماتش!' : 'خسرت هالمرة...'}</h1>
<p class="bgr-subtitle">${reason === 'abandon' ? 'الخصم انسحب من اللعبة' : didWin ? 'أداء ممتاز!' : 'حاول مرة ثانية!'}</p>
<h1 class="bgr-title">${didWin ? t('backgammon.win_title') : t('backgammon.lose_title')}</h1>
<p class="bgr-subtitle">${reason === 'abandon' ? t('backgammon.abandon_subtitle') : didWin ? t('backgammon.win_subtitle') : t('backgammon.lose_subtitle')}</p>
</div>
<div class="bgr-scoreboard">
<div class="bgr-score-col">
<div class="bgr-score-val ${didWin ? 'bgr-val-win' : ''}">${scores[0]}</div>
<div class="bgr-score-label">أنت</div>
<div class="bgr-score-label">${t('common.you')}</div>
</div>
<div class="bgr-score-vs">
<div class="bgr-vs-line"></div>
<span class="bgr-vs-text">من ${matchLength}</span>
<span class="bgr-vs-text">${t('backgammon.from_match', { n: matchLength })}</span>
<div class="bgr-vs-line"></div>
</div>
<div class="bgr-score-col">
<div class="bgr-score-val ${!didWin ? 'bgr-val-win' : ''}">${scores[1]}</div>
<div class="bgr-score-label">الخصم</div>
<div class="bgr-score-label">${t('common.opponent')}</div>
</div>
</div>
......@@ -52,10 +53,10 @@ export function mountResult(el, params) {
<div class="bgr-buttons">
<button class="bgr-btn bgr-btn-primary" id="btn-rematch">
${emoji('fire', '🔥', 18)} العب مرة ثانية
${emoji('fire', '🔥', 18)} ${t('game.play_again')}
</button>
<button class="bgr-btn bgr-btn-secondary" id="btn-exit">
رجوع للقائمة
${t('game.back_to_menu')}
</button>
</div>
</div>
......
......@@ -3,6 +3,7 @@ import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import * as store from '../../../core/store.js';
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
export function mountRoom(el, params) {
const { mode = 'menu' } = params || {};
......@@ -17,29 +18,29 @@ function renderMenu(el) {
<div class="bg-wrap">
<div class="bg-hero">
<div class="bg-icon">${emoji('game_die', '🎲', 56)}</div>
<h1 class="bg-title">طاولة</h1>
<p class="bg-subtitle">اول من يطلّع كل قطعه يفوز!</p>
<h1 class="bg-title">${t('backgammon.title')}</h1>
<p class="bg-subtitle">${t('backgammon.subtitle')}</p>
</div>
<div class="bg-buttons">
<button class="bg-btn bg-btn-primary" id="btn-local">
<span class="bg-btn-icon">${emoji('gamepad', '🎮', 22)}</span>
<span class="bg-btn-label">ضد البوت</span>
<span class="bg-btn-desc">اختر المستوى والنوع</span>
<span class="bg-btn-label">${t('game.vs_bot')}</span>
<span class="bg-btn-desc">${t('backgammon.bot_desc')}</span>
</button>
<button class="bg-btn bg-btn-online" id="btn-online">
<span class="bg-btn-icon">${emoji('globe', '🌍', 22)}</span>
<span class="bg-btn-label">أونلاين</span>
<span class="bg-btn-desc">العب ضد لاعبين حقيقيين</span>
<span class="bg-btn-label">${t('game.online')}</span>
<span class="bg-btn-desc">${t('backgammon.online_desc')}</span>
</button>
<button class="bg-btn bg-btn-friend" id="btn-friend">
<span class="bg-btn-icon">${emoji('handshake', '🤝', 20)}</span>
<span class="bg-btn-label">تحدي صديق</span>
<span class="bg-btn-desc">ادعُ صديقك للعب</span>
<span class="bg-btn-label">${t('game.vs_friend')}</span>
<span class="bg-btn-desc">${t('backgammon.friend_desc')}</span>
</button>
</div>
<button class="bg-back" id="btn-back">رجوع</button>
<button class="bg-back" id="btn-back">${t('common.back')}</button>
</div>
${getStyles()}
`;
......@@ -51,7 +52,7 @@ function renderMenu(el) {
el.querySelector('#btn-online').onclick = () => {
audio.play('click');
if (store.get('auth.isGuest')) {
bus.emit('toast', { text: 'سجّل دخولك للعب أونلاين' });
bus.emit('toast', { text: t('play.login_required_online') });
return;
}
scene.replace('backgammon-room', { mode: 'setup', type: 'online' });
......@@ -59,7 +60,7 @@ function renderMenu(el) {
el.querySelector('#btn-friend').onclick = () => {
audio.play('click');
if (store.get('auth.isGuest')) {
bus.emit('toast', { text: 'سجّل دخولك لتحدي صديق' });
bus.emit('toast', { text: t('play.login_required_friend') });
return;
}
scene.push('challenge-friend', { game: 'backgammon' });
......@@ -77,44 +78,44 @@ function renderSetup(el, params) {
el.innerHTML = `
<div class="bg-wrap">
<div class="bg-hero" style="margin-bottom:16px;">
<h2 class="bg-title" style="font-size:20px;">${isOnline ? 'إعدادات الأونلاين' : 'إعدادات اللعبة'}</h2>
<h2 class="bg-title" style="font-size:20px;">${isOnline ? t('backgammon.settings_online') : t('backgammon.settings_game')}</h2>
</div>
<!-- Variant -->
<div class="bg-section">
<div class="bg-section-title">نوع اللعبة</div>
<div class="bg-section-title">${t('backgammon.game_type')}</div>
<div class="bg-grid" id="variant-grid">
<button class="bg-chip bg-chip-active" data-variant="sheshbesh">
<span class="bg-chip-label">شيش بيش</span>
<span class="bg-chip-label">${t('backgammon.sheshbesh')}</span>
</button>
<button class="bg-chip" data-variant="mahbousa">
<span class="bg-chip-label">محبوسة</span>
<span class="bg-chip-label">${t('backgammon.mahbousa')}</span>
</button>
<button class="bg-chip" data-variant="thirtyone">
<span class="bg-chip-label">٣١</span>
<span class="bg-chip-label">${t('backgammon.thirtyone')}</span>
</button>
</div>
</div>
<!-- Match Length -->
<div class="bg-section">
<div class="bg-section-title">طول الماتش</div>
<div class="bg-section-title">${t('backgammon.match_length')}</div>
<div class="bg-grid" id="length-grid">
<button class="bg-chip" data-len="1">
<span class="bg-chip-num">1</span>
<span class="bg-chip-label">سريع</span>
<span class="bg-chip-label">${t('backgammon.short_match')}</span>
</button>
<button class="bg-chip bg-chip-active" data-len="3">
<span class="bg-chip-num">3</span>
<span class="bg-chip-label">قصير</span>
<span class="bg-chip-label">${t('backgammon.medium_match')}</span>
</button>
<button class="bg-chip" data-len="5">
<span class="bg-chip-num">5</span>
<span class="bg-chip-label">عادي</span>
<span class="bg-chip-label">${t('backgammon.normal_match')}</span>
</button>
<button class="bg-chip" data-len="7">
<span class="bg-chip-num">7</span>
<span class="bg-chip-label">طويل</span>
<span class="bg-chip-label">${t('backgammon.long_match')}</span>
</button>
</div>
</div>
......@@ -122,19 +123,19 @@ function renderSetup(el, params) {
${!isOnline ? `
<!-- Bot Difficulty -->
<div class="bg-section">
<div class="bg-section-title">مستوى البوت</div>
<div class="bg-section-title">${t('ludo.bot_level')}</div>
<div class="bg-grid" id="difficulty-grid">
<button class="bg-chip" data-diff="easy">
<span>😊</span>
<span class="bg-chip-label">سهل</span>
<span class="bg-chip-label">${t('ludo.easy')}</span>
</button>
<button class="bg-chip bg-chip-active" data-diff="medium">
<span>🧐</span>
<span class="bg-chip-label">متوسط</span>
<span class="bg-chip-label">${t('ludo.medium')}</span>
</button>
<button class="bg-chip" data-diff="hard">
<span>🧠</span>
<span class="bg-chip-label">صعب</span>
<span class="bg-chip-label">${t('ludo.hard')}</span>
</button>
</div>
</div>
......@@ -142,21 +143,21 @@ function renderSetup(el, params) {
<!-- Doubling Cube Toggle -->
<div class="bg-section">
<div class="bg-section-title">مكعب المضاعفة</div>
<div class="bg-section-title">${t('backgammon.doubling_cube')}</div>
<div class="bg-grid" id="cube-grid">
<button class="bg-chip bg-chip-active" data-cube="on">
<span class="bg-chip-label">مفعّل</span>
<span class="bg-chip-label">${t('backgammon.enabled')}</span>
</button>
<button class="bg-chip" data-cube="off">
<span class="bg-chip-label">بدون</span>
<span class="bg-chip-label">${t('backgammon.disabled')}</span>
</button>
</div>
</div>
<button class="bg-btn bg-btn-start" id="btn-start">
${isOnline ? 'ابحث عن مباراة' : 'ابدأ اللعب'}
${isOnline ? t('ludo.search_match') : t('ludo.start_play')}
</button>
<button class="bg-back" id="btn-back-setup">رجوع</button>
<button class="bg-back" id="btn-back-setup">${t('common.back')}</button>
</div>
${getStyles()}
`;
......@@ -200,10 +201,10 @@ function renderSearching(el) {
<div class="bg-pulse-ring">
<div class="bg-pulse-inner">${emoji('game_die', '🎲', 32)}</div>
</div>
<h2 class="bg-title" style="font-size:18px;margin-top:20px;">جاري البحث...</h2>
<p class="bg-subtitle">بنوصّلك بلاعب قريب</p>
<h2 class="bg-title" style="font-size:18px;margin-top:20px;">${t('backgammon.searching')}</h2>
<p class="bg-subtitle">${t('backgammon.searching_hint')}</p>
</div>
<button class="bg-btn bg-btn-friend" id="btn-cancel" style="max-width:200px;">إلغاء</button>
<button class="bg-btn bg-btn-friend" id="btn-cancel" style="max-width:200px;">${t('common.cancel')}</button>
</div>
${getStyles()}
`;
......
// Emote panel for backgammon — with cooldown + net sync
import * as audio from '../../../core/audio.js';
import * as net from '../../../core/net.js';
import { t } from '../../../core/i18n.js';
const EMOTES = ['🎲', '😤', '🤔', '👏', '😂', '🔥', '💀', '🫡'];
const PHRASES = ['!لعبة حلوة', '!حركة ممتازة', '!حظ', '...فكّر أسرع', '?ريماتش', 'gg wp'];
const PHRASES = [
() => t('emote.nice_game'),
() => t('emote.great_move'),
() => t('emote.lucky'),
() => t('emote.think_faster'),
() => t('emote.rematch'),
() => 'gg wp'
];
const COOLDOWN = 3000;
let lastEmoteTime = 0;
......@@ -17,7 +25,7 @@ export function createEmotePanel(container, options = {}) {
panel.innerHTML = `
<div class="bgg-emotes">${EMOTES.map(e => `<button class="bgg-emote-btn">${e}</button>`).join('')}</div>
<div class="bgg-phrases">${PHRASES.map(p => `<button class="bgg-phrase-btn">${p}</button>`).join('')}</div>
<div class="bgg-phrases">${PHRASES.map(p => `<button class="bgg-phrase-btn">${p()}</button>`).join('')}</div>
`;
panel.querySelectorAll('.bgg-emote-btn').forEach(btn => {
......
// In-game emote system for chess
// Preset emotes that both players can send during a match
import { t } from '../../../core/i18n.js';
const EMOTES = [
{ key: 'gg', emoji: '🤝', label: 'GG' },
{ key: 'good_move', emoji: '👏', label: 'نقلة ممتازة' },
{ key: 'think', emoji: '🤔', label: 'يفكر...' },
{ key: 'hurry', emoji: '⏱️', label: 'أسرع' },
{ key: 'wow', emoji: '😮', label: 'واو!' },
{ key: 'laugh', emoji: '😂', label: 'هههه' },
{ key: 'angry', emoji: '😤', label: 'غاضب' },
{ key: 'hello', emoji: '👋', label: 'مرحبا' },
{ key: 'good_move', emoji: '👏', get label() { return t('emote.good_move'); } },
{ key: 'think', emoji: '🤔', get label() { return t('emote.think'); } },
{ key: 'hurry', emoji: '⏱️', get label() { return t('emote.hurry'); } },
{ key: 'wow', emoji: '😮', get label() { return t('emote.wow'); } },
{ key: 'laugh', emoji: '😂', get label() { return t('emote.laugh'); } },
{ key: 'angry', emoji: '😤', get label() { return t('emote.angry'); } },
{ key: 'hello', emoji: '👋', get label() { return t('emote.hello'); } },
];
let emoteBar = null;
......
// Rating history graph — canvas sparkline with interactive hover
import { createCanvas, clear } from '../../../core/canvas.js';
import { t } from '../../../core/i18n.js';
export function renderRatingGraph(container, history, options = {}) {
const { width = 340, height = 140, color = '#3B82F6', bgColor = '#0f0f1e', gridColor = 'rgba(255,255,255,0.05)' } = options;
container.innerHTML = '';
if (!history || history.length < 2) {
container.innerHTML = `<div style="height:${height}px;display:flex;align-items:center;justify-content:center;color:#64748b;font-size:12px;">بيانات غير كافية</div>`;
container.innerHTML = `<div style="height:${height}px;display:flex;align-items:center;justify-content:center;color:#64748b;font-size:12px;">${t('chess.insufficient_data')}</div>`;
return;
}
......@@ -112,5 +113,5 @@ export function renderRatingGraph(container, history, options = {}) {
ctx.fillStyle = '#64748b';
ctx.font = '9px Inter, sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${ratings.length} مباراة`, width / 2, height - 5);
ctx.fillText(`${ratings.length} ${t('chess.games_count')}`, width / 2, height - 5);
}
// Move Classification Engine
// Classifies moves as: brilliant, great, best, good, book, inaccuracy, mistake, blunder
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
const THRESHOLDS = {
blunder: 2.0, // Lost 2+ pawns worth of eval
......@@ -66,11 +67,11 @@ export function calculateAccuracy(classifications) {
}
export function getAccuracyLabel(accuracy) {
if (accuracy >= 95) return { label: 'ممتاز', labelEn: 'Excellent', color: '#10B981' };
if (accuracy >= 85) return { label: 'جيد جداً', labelEn: 'Great', color: '#3B82F6' };
if (accuracy >= 70) return { label: 'جيد', labelEn: 'Good', color: '#FBBF24' };
if (accuracy >= 50) return { label: 'متوسط', labelEn: 'Average', color: '#F97316' };
return { label: 'ضعيف', labelEn: 'Poor', color: '#EF4444' };
if (accuracy >= 95) return { label: t('classifier.excellent'), labelEn: 'Excellent', color: '#10B981' };
if (accuracy >= 85) return { label: t('classifier.great'), labelEn: 'Great', color: '#3B82F6' };
if (accuracy >= 70) return { label: t('classifier.good'), labelEn: 'Good', color: '#FBBF24' };
if (accuracy >= 50) return { label: t('classifier.average'), labelEn: 'Average', color: '#F97316' };
return { label: t('classifier.poor'), labelEn: 'Poor', color: '#EF4444' };
}
export function getMoveClassSummary(classifications) {
......
......@@ -23,7 +23,7 @@ export function mountAnalysis(el, params) {
<div style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">← ${t('game.back')}</button>
<span style="font-size:14px;font-weight:700;color:#f8fafc;">${emoji('chart', '📊', 14)} تحليل المباراة</span>
<span style="font-size:14px;font-weight:700;color:#f8fafc;">${emoji('chart', '📊', 14)} ${t('analysis.title')}</span>
<div style="width:60px;"></div>
</div>
......@@ -39,12 +39,12 @@ export function mountAnalysis(el, params) {
<!-- Analysis lines + Opening explorer -->
<div id="analysis-lines" style="padding:6px 12px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.04);min-height:50px;max-height:90px;overflow-y:auto;font-size:12px;">
<div style="color:#64748b;font-size:12px;text-align:center;">جاري التحليل...</div>
<div style="color:#64748b;font-size:12px;text-align:center;">${t('analysis.analyzing')}</div>
</div>
<!-- Timeline scrubber -->
<div style="padding:8px 12px;background:#0a0a18;border-top:1px solid rgba(255,255,255,0.04);">
<div id="move-display" style="text-align:center;font-family:Inter,monospace;font-size:13px;font-weight:600;color:#f8fafc;margin-bottom:6px;">بداية</div>
<div id="move-display" style="text-align:center;font-family:Inter,monospace;font-size:13px;font-weight:600;color:#f8fafc;margin-bottom:6px;">${t('analysis.start')}</div>
<div id="scrubber-track" style="position:relative;height:32px;background:#1a1a2e;border-radius:8px;cursor:pointer;touch-action:none;user-select:none;">
<div id="scrubber-fill" style="position:absolute;top:0;right:0;height:100%;background:linear-gradient(90deg,#2563EB,#3B82F6);border-radius:8px;width:0%;transition:width 0.15s;"></div>
<div id="scrubber-thumb" style="position:absolute;top:50%;transform:translate(50%,-50%);right:0%;width:18px;height:18px;background:#f8fafc;border-radius:50%;box-shadow:0 2px 6px rgba(0,0,0,0.4);transition:right 0.15s;"></div>
......@@ -107,7 +107,7 @@ export function mountAnalysis(el, params) {
function renderMoveChips(el, moves) {
const container = el.querySelector('#analysis-moves');
let html = '<span class="move-chip active" data-idx="0">بداية</span>';
let html = `<span class="move-chip active" data-idx="0">${t('analysis.start')}</span>`;
for (let i = 0; i < moves.length; i += 2) {
const num = Math.floor(i / 2) + 1;
html += `<span style="color:#475569;font-size:10px;">${num}.</span>`;
......@@ -143,7 +143,7 @@ function goToMove(el, moves, idx) {
// Update display
const display = el.querySelector('#move-display');
if (idx === 0) display.textContent = 'بداية';
if (idx === 0) display.textContent = t('analysis.start');
else display.textContent = `${Math.ceil(idx / 2)}. ${moves[idx - 1]?.san || ''}`;
// Update scrubber position
......@@ -168,7 +168,7 @@ function goToMove(el, moves, idx) {
async function analyzePosition(el, fen) {
const linesContainer = el.querySelector('#analysis-lines');
linesContainer.innerHTML = '<div style="color:#64748b;font-size:12px;text-align:center;">جاري التحليل...</div>';
linesContainer.innerHTML = `<div style="color:#64748b;font-size:12px;text-align:center;">${t('analysis.analyzing')}</div>`;
// Show opening explorer if data exists
const explorerData = lookup(fen);
......@@ -181,8 +181,8 @@ async function analyzePosition(el, fen) {
</div>`
).join('');
linesContainer.innerHTML = `<div style="border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:4px;margin-bottom:4px;">
<div style="font-size:9px;color:#64748b;margin-bottom:2px;">${emoji('book', '📖', 9)} كتاب الافتتاحات</div>${explorerHtml}</div>
<div style="color:#64748b;font-size:11px;text-align:center;">جاري تحليل المحرك...</div>`;
<div style="font-size:9px;color:#64748b;margin-bottom:2px;">${emoji('book', '📖', 9)} ${t('analysis.openings_book')}</div>${explorerHtml}</div>
<div style="color:#64748b;font-size:11px;text-align:center;">${t('analysis.engine_analyzing')}</div>`;
}
try {
......@@ -193,7 +193,7 @@ async function analyzePosition(el, fen) {
}
} catch (e) {
if (!explorerData) {
linesContainer.innerHTML = '<div style="color:#ef4444;font-size:12px;text-align:center;">فشل التحليل</div>';
linesContainer.innerHTML = `<div style="color:#ef4444;font-size:12px;text-align:center;">${t('analysis.failed')}</div>`;
}
}
}
......@@ -204,7 +204,7 @@ function renderAnalysisLines(el, lines, fen, explorerData) {
let explorerHtml = '';
if (explorerData) {
explorerHtml = `<div style="border-bottom:1px solid rgba(255,255,255,0.05);padding-bottom:4px;margin-bottom:4px;">
<div style="font-size:9px;color:#64748b;margin-bottom:2px;">${emoji('book', '📖', 9)} كتاب الافتتاحات</div>
<div style="font-size:9px;color:#64748b;margin-bottom:2px;">${emoji('book', '📖', 9)} ${t('analysis.openings_book')}</div>
${explorerData.slice(0, 3).map(e =>
`<div style="display:flex;align-items:center;gap:6px;padding:1px 0;">
<span style="font-size:11px;font-weight:600;color:#f8fafc;min-width:32px;font-family:Inter,monospace;">${e.move}</span>
......
......@@ -897,7 +897,7 @@ function fetchAndRenderOpponent(el, oppId) {
const nameEl = el.querySelector('#opponent-name');
const levelEl = el.querySelector('#opponent-level');
const avatarEl = el.querySelector('#opponent-avatar');
if (nameEl) nameEl.textContent = opp.display_name || opp.username || 'خصم';
if (nameEl) nameEl.textContent = opp.display_name || opp.username || t('common.opponent');
if (levelEl) levelEl.textContent = `Lv.${opp.level || 1}`;
if (avatarEl && opp.avatar_url) {
avatarEl.innerHTML = `<img src="${opp.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">`;
......
......@@ -40,7 +40,7 @@ function renderHistory(el, matches) {
list.innerHTML = `
<div style="text-align:center;padding:40px;">
<div style="font-size:40px;margin-bottom:8px;opacity:0.4;">${emoji('clipboard', '📋', 40)}</div>
<div style="font-size:14px;color:#64748b;">لا توجد مباريات بعد — العب لتظهر هنا</div>
<div style="font-size:14px;color:#64748b;">${t('history.no_matches')}</div>
</div>`;
return;
}
......@@ -49,16 +49,16 @@ function renderHistory(el, matches) {
const isWhite = m.white_player_id === userId;
const myResult = getMyResult(m.result, isWhite);
const resultConfig = {
win: { label: 'فوز', color: '#34D399', icon: emoji('trophy', '🏆', 20) },
loss: { label: 'خسارة', color: '#F87171', icon: emoji('skull', '💀', 20) },
draw: { label: 'تعادل', color: '#E4AC38', icon: emoji('handshake', '🤝', 20) },
win: { label: t('history.win'), color: '#34D399', icon: emoji('trophy', '🏆', 20) },
loss: { label: t('history.loss'), color: '#F87171', icon: emoji('skull', '💀', 20) },
draw: { label: t('history.draw'), color: '#E4AC38', icon: emoji('handshake', '🤝', 20) },
unknown: { label: '—', color: '#64748b', icon: '•' }
};
const cfg = resultConfig[myResult] || resultConfig.unknown;
const ratingChange = isWhite ? m.rating_change_white : m.rating_change_black;
const ratingStr = ratingChange ? (ratingChange > 0 ? `+${ratingChange}` : `${ratingChange}`) : '';
const timeAgo = formatTimeAgo(m.created_at);
const opponent = isWhite ? (m.black_name || m.bot_id || 'خصم') : (m.white_name || m.bot_id || 'خصم');
const opponent = isWhite ? (m.black_name || m.bot_id || t('common.opponent')) : (m.white_name || m.bot_id || t('common.opponent'));
const tc = formatTimeControl(m.time_control);
return `
......@@ -102,10 +102,10 @@ function formatTimeAgo(date) {
if (!date) return '';
const diff = Date.now() - new Date(date).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `${mins}د`;
if (mins < 1) return t('common.now');
if (mins < 60) return t('common.minutes_ago', { n: mins });
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}س`;
if (hours < 24) return t('common.hours_ago', { n: hours });
const days = Math.floor(hours / 24);
return `${days}ي`;
return t('common.days_ago', { n: days });
}
......@@ -23,12 +23,12 @@ export function mountResult(el, params) {
const cfg = resultConfig[result] || resultConfig.loss;
const reasonText = {
checkmate: 'كش ملك',
stalemate: 'بات (جمود)',
resign: 'استسلام',
timeout: 'انتهاء الوقت',
threefold: 'تكرار ثلاثي',
insufficient: 'قطع غير كافية'
checkmate: t('game.checkmate'),
stalemate: t('game.stalemate'),
resign: t('game.resign'),
timeout: t('game.timeout'),
threefold: t('game.threefold'),
insufficient: t('game.insufficient')
}[reason] || reason;
el.innerHTML = `
......@@ -40,24 +40,24 @@ export function mountResult(el, params) {
<div style="text-align:center;">
<div style="font-size:32px;font-weight:800;color:${cfg.color};">${cfg.title}</div>
<div style="font-size:14px;color:#94a3b8;margin-top:4px;">${reasonText}</div>
${moves ? `<div style="font-size:12px;color:#475569;margin-top:2px;">${moves} نقلة</div>` : ''}
${moves ? `<div style="font-size:12px;color:#475569;margin-top:2px;">${t('game.moves_count', { n: moves })}</div>` : ''}
</div>
<!-- Stats Row -->
<div style="display:flex;gap:24px;margin-top:8px;">
<div style="text-align:center;">
<div style="font-size:28px;font-weight:700;color:${cfg.color};font-family:Inter,monospace;">${ratingStr}</div>
<div style="font-size:11px;color:#64748b;">تصنيف</div>
<div style="font-size:11px;color:#64748b;">${t('game.rating')}</div>
</div>
<div style="width:1px;background:rgba(255,255,255,0.06);"></div>
<div style="text-align:center;">
<div style="font-size:28px;font-weight:700;color:#E4AC38;font-family:Inter,monospace;">+${coins}</div>
<div style="font-size:11px;color:#64748b;">عملات</div>
<div style="font-size:11px;color:#64748b;">${t('game.coins')}</div>
</div>
<div style="width:1px;background:rgba(255,255,255,0.06);"></div>
<div style="text-align:center;">
<div style="font-size:28px;font-weight:700;color:#00FFFF;font-family:Inter,monospace;">+${xp}</div>
<div style="font-size:11px;color:#64748b;">خبرة</div>
<div style="font-size:11px;color:#64748b;">${t('game.xp')}</div>
</div>
</div>
......@@ -72,13 +72,13 @@ export function mountResult(el, params) {
<!-- Actions -->
<div style="display:flex;flex-direction:column;gap:8px;width:100%;max-width:280px;margin-top:12px;">
${params.tournamentId
? `<button class="btn btn-primary w-full" id="btn-back-tournament" style="font-size:15px;">${emoji('trophy', '🏆', 15)} العودة للبطولة</button>`
? `<button class="btn btn-primary w-full" id="btn-back-tournament" style="font-size:15px;">${emoji('trophy', '🏆', 15)} ${t('tournament.back_to')}</button>`
: `<button class="btn btn-primary w-full" id="btn-rematch" style="font-size:15px;">${t('game.rematch')}</button>`
}
<button class="btn btn-secondary w-full" id="btn-analyze" style="font-size:13px;">${emoji('chart', '📊', 13)} تحليل المباراة</button>
<button class="btn btn-secondary w-full" id="btn-analyze" style="font-size:13px;">${emoji('chart', '📊', 13)} ${t('result.analyze')}</button>
<div style="display:flex;gap:8px;">
<button class="btn btn-secondary" id="btn-share" style="flex:1;font-size:12px;">${emoji('share', '📤', 12)} مشاركة PGN</button>
<button class="btn btn-secondary" id="btn-copy-pgn" style="flex:1;font-size:12px;">${emoji('clipboard', '📋', 12)} نسخ</button>
<button class="btn btn-secondary" id="btn-share" style="flex:1;font-size:12px;">${emoji('share', '📤', 12)} ${t('result.share_pgn')}</button>
<button class="btn btn-secondary" id="btn-copy-pgn" style="flex:1;font-size:12px;">${emoji('clipboard', '📋', 12)} ${t('result.copy_pgn')}</button>
</div>
<button class="btn btn-secondary w-full" id="btn-back" style="font-size:13px;">${t('game.back')}</button>
</div>
......@@ -162,9 +162,9 @@ export function mountResult(el, params) {
const pgnText = pgn || formatMoveHistory(moveHistory);
navigator.clipboard?.writeText(pgnText).then(() => {
const btn = el.querySelector('#btn-copy-pgn');
btn.textContent = '✓ تم النسخ';
btn.textContent = t('result.copied');
juice.pulseElement(btn, '#34D399');
setTimeout(() => { btn.innerHTML = `${emoji('clipboard', '📋', 12)} نسخ`; }, 2000);
setTimeout(() => { btn.innerHTML = `${emoji('clipboard', '📋', 12)} ${t('result.copy_pgn')}`; }, 2000);
});
});
......
......@@ -18,13 +18,13 @@ export async function mountReview(el, params) {
<div style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;overflow-y:auto;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:30px;padding:4px 12px;font-size:12px;">← ${t('game.back')}</button>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">${emoji('star', '⭐', 15)} مراجعة المباراة</span>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">${emoji('star', '⭐', 15)} ${t('review.title')}</span>
<div style="width:50px;"></div>
</div>
<!-- Progress -->
<div id="review-progress" style="padding:16px;text-align:center;">
<div style="font-size:14px;color:#94a3b8;margin-bottom:8px;">جاري تحليل كل نقلة...</div>
<div style="font-size:14px;color:#94a3b8;margin-bottom:8px;">${t('review.analyzing_moves')}</div>
<div style="background:#1e1e3a;border-radius:6px;height:8px;overflow:hidden;">
<div id="progress-bar" style="height:100%;background:#E4AC38;width:0%;transition:width 0.3s;border-radius:6px;"></div>
</div>
......@@ -126,7 +126,7 @@ function renderReview(el, results, moves, playerColor) {
const playerLabel = getAccuracyLabel(playerAccuracy);
const opponentLabel = getAccuracyLabel(opponentAccuracy);
const opening = getOpeningName(moves) || 'غير معروف';
const opening = getOpeningName(moves) || t('review.unknown_opening');
content.innerHTML = `
<!-- Eval Graph -->
......@@ -134,13 +134,13 @@ function renderReview(el, results, moves, playerColor) {
<!-- Accuracy Comparison -->
<div style="background:#0f0f1e;border-radius:10px;padding:16px;margin-bottom:12px;">
<div style="text-align:center;font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:12px;">${emoji('star', '⭐', 15)} مراجعة المباراة</div>
<div style="text-align:center;font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:12px;">${emoji('star', '⭐', 15)} ${t('review.title')}</div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
<!-- Player accuracy -->
<div style="flex:1;text-align:center;">
<div style="font-size:28px;font-weight:800;color:${playerLabel.color};font-family:Inter,monospace;">${playerAccuracy}%</div>
<div style="font-size:11px;color:#94a3b8;">أنت</div>
<div style="font-size:11px;color:#94a3b8;">${t('common.you')}</div>
<div style="height:6px;background:#1e1e3a;border-radius:3px;margin-top:6px;overflow:hidden;">
<div style="height:100%;width:${playerAccuracy}%;background:${playerLabel.color};border-radius:3px;"></div>
</div>
......@@ -151,7 +151,7 @@ function renderReview(el, results, moves, playerColor) {
<!-- Opponent accuracy -->
<div style="flex:1;text-align:center;">
<div style="font-size:28px;font-weight:800;color:${opponentLabel.color};font-family:Inter,monospace;">${opponentAccuracy}%</div>
<div style="font-size:11px;color:#94a3b8;">الخصم</div>
<div style="font-size:11px;color:#94a3b8;">${t('common.opponent')}</div>
<div style="height:6px;background:#1e1e3a;border-radius:3px;margin-top:6px;overflow:hidden;">
<div style="height:100%;width:${opponentAccuracy}%;background:${opponentLabel.color};border-radius:3px;"></div>
</div>
......@@ -164,30 +164,30 @@ function renderReview(el, results, moves, playerColor) {
<!-- Move Classification Breakdown -->
<div style="background:#0f0f1e;border-radius:10px;padding:14px;margin-bottom:12px;">
<div style="font-size:13px;font-weight:700;color:#f8fafc;margin-bottom:10px;text-align:center;">تصنيف النقلات</div>
<div style="font-size:13px;font-weight:700;color:#f8fafc;margin-bottom:10px;text-align:center;">${t('review.move_classification')}</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="color:#64748b;">
<th style="text-align:right;padding:3px 0;font-weight:600;">أنت</th>
<th style="text-align:right;padding:3px 0;font-weight:600;">${t('common.you')}</th>
<th style="text-align:center;padding:3px 8px;"></th>
<th style="text-align:left;padding:3px 0;font-weight:600;">الخصم</th>
<th style="text-align:left;padding:3px 0;font-weight:600;">${t('common.opponent')}</th>
</tr>
</thead>
<tbody>
${renderClassRow('brilliant', '!! رائعة', '#06B6D4', whiteSummary, blackSummary, playerColor)}
${renderClassRow('great', '! ممتازة', '#3B82F6', whiteSummary, blackSummary, playerColor)}
${renderClassRow('best', emoji('best_move_star', '★', 12) + ' أفضل نقلة', '#10B981', whiteSummary, blackSummary, playerColor)}
${renderClassRow('good', '✓ جيدة', '#94a3b8', whiteSummary, blackSummary, playerColor)}
${renderClassRow('book', emoji('book', '📖', 12) + ' نظرية', '#94a3b8', whiteSummary, blackSummary, playerColor)}
${renderClassRow('inaccuracy', '?! عدم دقة', '#FBBF24', whiteSummary, blackSummary, playerColor)}
${renderClassRow('mistake', '? خطأ', '#F97316', whiteSummary, blackSummary, playerColor)}
${renderClassRow('blunder', '?? خطأ فادح', '#EF4444', whiteSummary, blackSummary, playerColor)}
${renderClassRow('brilliant', t('review.brilliant'), '#06B6D4', whiteSummary, blackSummary, playerColor)}
${renderClassRow('great', t('review.great'), '#3B82F6', whiteSummary, blackSummary, playerColor)}
${renderClassRow('best', emoji('best_move_star', '★', 12) + ' ' + t('review.best'), '#10B981', whiteSummary, blackSummary, playerColor)}
${renderClassRow('good', t('review.good'), '#94a3b8', whiteSummary, blackSummary, playerColor)}
${renderClassRow('book', emoji('book', '📖', 12) + ' ' + t('review.book'), '#94a3b8', whiteSummary, blackSummary, playerColor)}
${renderClassRow('inaccuracy', t('review.inaccuracy'), '#FBBF24', whiteSummary, blackSummary, playerColor)}
${renderClassRow('mistake', t('review.mistake'), '#F97316', whiteSummary, blackSummary, playerColor)}
${renderClassRow('blunder', t('review.blunder'), '#EF4444', whiteSummary, blackSummary, playerColor)}
</tbody>
</table>
</div>
<!-- Deep analysis button -->
<button class="btn btn-secondary w-full" id="btn-full-analysis" style="font-size:13px;margin-bottom:12px;">${emoji('chart', '📊', 13)} التحليل التفصيلي (نقلة بنقلة)</button>
<button class="btn btn-secondary w-full" id="btn-full-analysis" style="font-size:13px;margin-bottom:12px;">${emoji('chart', '📊', 13)} ${t('review.full_analysis')}</button>
`;
// Render eval graph
......
......@@ -22,13 +22,13 @@ export async function mountSpectate(el, params = {}) {
<button class="btn btn-secondary" id="back-btn" style="width:34px;height:34px;padding:0;">←</button>
<div style="flex:1;display:flex;align-items:center;gap:8px;">
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#EF4444;animation:spectatePulse 1.5s infinite;"></span>
<span style="font-size:13px;font-weight:600;color:#EF4444;">مشاهدة مباشرة</span>
<span style="font-size:13px;font-weight:600;color:#EF4444;">${t('spectate.live')}</span>
</div>
</div>
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 14px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:32px;height:32px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;">${emoji('person', '👤', 14)}</div>
<span id="black-name" style="font-size:13px;font-weight:600;color:#f8fafc;">أسود</span>
<span id="black-name" style="font-size:13px;font-weight:600;color:#f8fafc;">${t('spectate.black')}</span>
</div>
<div id="clock-black" style="font-size:16px;font-weight:700;font-family:'SF Mono',monospace;background:#1a1a2e;padding:4px 10px;border-radius:6px;color:#94a3b8;">--:--</div>
</div>
......@@ -36,7 +36,7 @@ export async function mountSpectate(el, params = {}) {
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:8px 14px;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:32px;height:32px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;">${emoji('person', '👤', 14)}</div>
<span id="white-name" style="font-size:13px;font-weight:600;color:#f8fafc;">أبيض</span>
<span id="white-name" style="font-size:13px;font-weight:600;color:#f8fafc;">${t('spectate.white')}</span>
</div>
<div id="clock-white" style="font-size:16px;font-weight:700;font-family:'SF Mono',monospace;background:#1a1a2e;padding:4px 10px;border-radius:6px;color:#94a3b8;">--:--</div>
</div>
......@@ -63,7 +63,7 @@ export async function mountSpectate(el, params = {}) {
try {
matchData = await net.post('game.php', { action: 'get', match_id: matchId });
if (!matchData || matchData.error) {
el.querySelector('#board-container').innerHTML = `<div style="color:var(--error);padding:24px;">مباراة غير موجودة</div>`;
el.querySelector('#board-container').innerHTML = `<div style="color:var(--error);padding:24px;">${t('game.not_found')}</div>`;
return;
}
} catch (e) {
......@@ -133,8 +133,8 @@ async function loadPlayerNames(el, whiteId, blackId) {
net.get('profile.php', { id: whiteId }),
net.get('profile.php', { id: blackId })
]);
if (wp && !wp.error) el.querySelector('#white-name').textContent = wp.display_name || 'أبيض';
if (bp && !bp.error) el.querySelector('#black-name').textContent = bp.display_name || 'أسود';
if (wp && !wp.error) el.querySelector('#white-name').textContent = wp.display_name || t('spectate.white');
if (bp && !bp.error) el.querySelector('#black-name').textContent = bp.display_name || t('spectate.black');
} catch (e) {}
}
......@@ -160,9 +160,9 @@ function renderMoveList(el, moves) {
}
function showResult(el, result) {
const text = result === 'white_wins' ? 'فاز الأبيض' :
result === 'black_wins' ? 'فاز الأسود' :
result === 'draw' ? 'تعادل' : 'انتهت المباراة';
const text = result === 'white_wins' ? t('spectate.white_wins') :
result === 'black_wins' ? t('spectate.black_wins') :
result === 'draw' ? t('game.draw_game') : t('spectate.game_over');
const overlay = document.createElement('div');
overlay.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);color:#f8fafc;padding:16px 32px;border-radius:12px;font-size:16px;font-weight:700;z-index:30;text-align:center;';
overlay.textContent = text;
......
......@@ -2,6 +2,7 @@ import { createCanvas, clear } from '../../../core/canvas.js';
import { computeLayout, hitTestEndpoint, getSnapRadius, getAutoZoom } from '../logic/layout.js';
import { drawTile, drawEndpointGlow } from './tile-renderer.js';
import { TILE_W, TILE_H } from '../logic/layout.js';
import { t } from '../../../core/i18n.js';
export class DominoBoard {
constructor(container, options = {}) {
......@@ -257,7 +258,7 @@ export class DominoBoard {
ctx.font = '600 13px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('ضع أول قطعة', this.width / 2, this.height / 2);
ctx.fillText(t('game.place_first'), this.width / 2, this.height / 2);
this._drawCenterGlow(ctx);
ctx.restore();
return;
......
......@@ -111,10 +111,10 @@ function buildLayout(mode) {
<div class="dg-opp-left">
<div id="opp-avatar" class="dg-avatar">${isLive ? '👤' : '🤖'}</div>
<div class="dg-opp-info">
<div id="opp-name" class="dg-opp-name">${isLive ? 'خصم' : 'بوت'}</div>
<div id="opp-name" class="dg-opp-name">${isLive ? t('common.opponent') : t('game.bot')}</div>
<div class="dg-opp-meta">
<span id="opp-count" class="dg-opp-count">7 قطع</span>
<span id="bot-thinking" class="dg-thinking">يفكر<span class="dg-dots"></span></span>
<span id="opp-count" class="dg-opp-count">${t('common.pieces', { n: 7 })}</span>
<span id="bot-thinking" class="dg-thinking">${t('game.thinking')}<span class="dg-dots"></span></span>
</div>
</div>
</div>
......@@ -140,7 +140,7 @@ function buildLayout(mode) {
<div class="dg-status-ribbon">
<div class="dg-score-section">
<div class="dg-score-me">
<span class="dg-score-label">أنت</span>
<span class="dg-score-label">${t('common.you')}</span>
<span class="dg-score-value" id="my-score">0</span>
</div>
<div class="dg-score-divider"></div>
......@@ -149,11 +149,11 @@ function buildLayout(mode) {
</div>
<div class="dg-score-divider"></div>
<div class="dg-score-opp">
<span class="dg-score-label">خصم</span>
<span class="dg-score-label">${t('common.opponent')}</span>
<span class="dg-score-value" id="opp-score">0</span>
</div>
</div>
<div id="turn-status" class="dg-turn-status">دورك!</div>
<div id="turn-status" class="dg-turn-status">${t('game.your_turn')}</div>
</div>
<!-- Player hand -->
......@@ -161,10 +161,10 @@ function buildLayout(mode) {
<!-- Controls -->
<div id="domino-controls" class="dg-controls">
<button class="dg-ctrl-btn dg-btn-resign" id="btn-resign">استسلام</button>
<button class="dg-ctrl-btn dg-btn-resign" id="btn-resign">${t('game.resign')}</button>
<button class="dg-ctrl-btn dg-btn-emote" id="btn-emote">😄</button>
<button class="dg-ctrl-btn dg-btn-draw" id="btn-draw">سحب من المخزن</button>
<button class="dg-ctrl-btn dg-btn-pass" id="btn-pass" style="display:none;">تمرير</button>
<button class="dg-ctrl-btn dg-btn-draw" id="btn-draw">${t('domino.draw_boneyard')}</button>
<button class="dg-ctrl-btn dg-btn-pass" id="btn-pass" style="display:none;">${t('domino.pass')}</button>
</div>
</div>
......@@ -517,7 +517,7 @@ function handleLivePollData(el, data) {
if (data.opponent_count !== undefined) {
const oppIdx = 1 - state.myPlayerIndex;
const oppEl = el.querySelector('#opp-count');
if (oppEl) oppEl.textContent = `${data.opponent_count} قطع`;
if (oppEl) oppEl.textContent = t('common.pieces', { n: data.opponent_count });
}
if (gs.scores) {
......@@ -930,21 +930,21 @@ function showRoundOverlay(el, winnerIdx, points) {
overlay.innerHTML = `
<div style="font-size:48px;">${isMyWin ? emoji('trophy', '🏆', 48) : emoji('skull', '💀', 48)}</div>
<div style="font-size:20px;font-weight:800;color:${isMyWin ? '#4ade80' : '#fca5a5'};">
${isMyWin ? 'فزت بالجولة!' : 'خسرت الجولة'}
${isMyWin ? t('game.round_won') : t('game.round_lost')}
</div>
<div style="font-size:14px;color:#94a3b8;">+${points} نقطة</div>
<div style="font-size:14px;color:#94a3b8;">${t('game.points', { n: points })}</div>
<div style="display:flex;gap:16px;margin-top:8px;">
<div style="text-align:center;">
<div style="font-size:24px;font-weight:800;color:#4ade80;">${state.matchScores[state.myPlayerIndex]}</div>
<div style="font-size:11px;color:#6ee7b7;">أنت</div>
<div style="font-size:11px;color:#6ee7b7;">${t('common.you')}</div>
</div>
<div style="font-size:20px;color:#475569;align-self:center;">—</div>
<div style="text-align:center;">
<div style="font-size:24px;font-weight:800;color:#fca5a5;">${state.matchScores[1 - state.myPlayerIndex]}</div>
<div style="font-size:11px;color:#fca5a5;">خصم</div>
<div style="font-size:11px;color:#fca5a5;">${t('common.opponent')}</div>
</div>
</div>
<div style="font-size:12px;color:#64748b;margin-top:8px;">الجولة التالية تبدأ...</div>
<div style="font-size:12px;color:#64748b;margin-top:8px;">${t('game.round_next')}</div>
`;
const wrap = el.querySelector('#domino-wrap');
......@@ -1124,16 +1124,16 @@ function updateUI(el) {
const turnEl = el.querySelector('#turn-status');
if (turnEl) {
if (state.gameOver) {
turnEl.textContent = 'انتهت الجولة';
turnEl.textContent = t('game.round_end');
turnEl.className = 'dg-turn-status dg-waiting';
} else if (isMyTurn && state.selectedTile) {
turnEl.textContent = 'اختر مكان الوضع';
turnEl.textContent = t('game.choose_placement');
turnEl.className = 'dg-turn-status';
} else if (isMyTurn) {
turnEl.textContent = 'دورك!';
turnEl.textContent = t('game.your_turn');
turnEl.className = 'dg-turn-status';
} else {
turnEl.textContent = 'الخصم يلعب...';
turnEl.textContent = t('game.opponent_plays');
turnEl.className = 'dg-turn-status dg-waiting';
}
}
......@@ -1142,7 +1142,7 @@ function updateUI(el) {
const oppIdx = 1 - state.myPlayerIndex;
const oppHandLen = state.hands[oppIdx]?.length || 0;
if (oppCountEl) {
oppCountEl.textContent = `${oppHandLen} قطع`;
oppCountEl.textContent = t('common.pieces', { n: oppHandLen });
}
// Reset tension on new round
......@@ -1162,7 +1162,7 @@ function updateUI(el) {
juice.hapticLight();
const alert = document.createElement('div');
alert.style.cssText = 'position:absolute;top:56px;left:50%;transform:translateX(-50%);background:rgba(239,68,68,0.9);color:#fff;padding:6px 16px;border-radius:8px;font-size:12px;font-weight:700;z-index:50;';
alert.textContent = oppHandLen === 1 ? '⚠️ قطعة واحدة!' : '⚠️ قطعتين!';
alert.textContent = oppHandLen === 1 ? t('game.one_piece_warning') : t('game.two_piece_warning');
el.querySelector('#domino-wrap')?.appendChild(alert);
alert.animate([{opacity:0,transform:'translateX(-50%) translateY(-10px)'},{opacity:1,transform:'translateX(-50%) translateY(0)'},{opacity:0,transform:'translateX(-50%) translateY(-10px)'}], {duration:2500}).onfinish = () => alert.remove();
}
......@@ -1255,11 +1255,11 @@ function refreshHand() {
async function confirmResign(el) {
if (state.gameOver || state.matchOver) return;
const confirmed = await modal.confirm('هل تريد الاستسلام؟', {
title: 'استسلام',
const confirmed = await modal.confirm(t('game.resign_confirm'), {
title: t('game.resign'),
icon: '🏳️',
confirmText: 'نعم، استسلم',
cancelText: 'تراجع',
confirmText: t('game.resign_yes'),
cancelText: t('game.resign_no'),
danger: true
});
if (!confirmed) return;
......
......@@ -13,7 +13,7 @@ export function mountResult(el, params) {
const isDraw = result === 'draw';
const icon = isWin ? `${emoji('crown', '👑', 32)}<br>${emoji('trophy', '🏆', 56)}` : isDraw ? emoji('handshake', '🤝', 56) : emoji('skull', '💀', 56);
const title = resigned ? 'استسلمت' : reason === 'resign' ? 'الخصم استسلم!' : reason === 'abandon' ? 'الخصم انقطع' : isWin ? 'فوز!' : isDraw ? 'تعادل' : 'خسارة';
const title = resigned ? t('game.resigned') : reason === 'resign' ? t('game.opponent_resigned') : reason === 'abandon' ? t('game.opponent_abandoned') : isWin ? t('game.win') : isDraw ? t('game.draw_game') : t('game.loss');
const titleColor = isWin ? '#4ade80' : isDraw ? '#fbbf24' : '#fca5a5';
el.innerHTML = `
......@@ -25,16 +25,16 @@ export function mountResult(el, params) {
<div style="display:flex;gap:20px;align-items:center;animation:fadeSlideUp 0.4s ease 0.3s both;">
<div style="text-align:center;">
<div style="font-size:32px;font-weight:800;color:#4ade80;" id="score-me">0</div>
<div style="font-size:11px;color:#86efac;">أنت</div>
<div style="font-size:11px;color:#86efac;">${t('common.you')}</div>
</div>
<div style="font-size:18px;color:#475569;">—</div>
<div style="text-align:center;">
<div style="font-size:32px;font-weight:800;color:#fca5a5;" id="score-opp">0</div>
<div style="font-size:11px;color:#fca5a5;">خصم</div>
<div style="font-size:11px;color:#fca5a5;">${t('common.opponent')}</div>
</div>
</div>
<div style="font-size:12px;color:#64748b;animation:fadeSlideUp 0.4s ease 0.35s both;">${rounds} ${rounds === 1 ? 'جولة' : 'جولات'}</div>
<div style="font-size:12px;color:#64748b;animation:fadeSlideUp 0.4s ease 0.35s both;">${rounds === 1 ? t('game.rounds_count', { n: rounds }) : t('game.rounds_count_plural', { n: rounds })}</div>
<!-- Rewards (populated after server response) -->
<div id="rewards-section" style="display:flex;gap:12px;margin-top:8px;animation:fadeSlideUp 0.4s ease 0.5s both;">
......@@ -49,12 +49,12 @@ export function mountResult(el, params) {
</div>
<!-- Rating change -->
<div id="rating-section" style="font-size:14px;font-weight:600;color:#94a3b8;animation:fadeSlideUp 0.4s ease 0.6s both;">التصنيف: ...</div>
<div id="rating-section" style="font-size:14px;font-weight:600;color:#94a3b8;animation:fadeSlideUp 0.4s ease 0.6s both;">${t('game.rating')}: ...</div>
<!-- Actions -->
<div style="display:flex;gap:10px;margin-top:16px;width:100%;max-width:300px;animation:fadeSlideUp 0.4s ease 0.7s both;">
<button class="btn btn-primary" id="btn-rematch" style="flex:1;min-height:48px;border-radius:14px;font-size:15px;font-weight:700;background:linear-gradient(135deg,#10b981,#06b6d4);">إعادة</button>
<button class="btn btn-secondary" id="btn-back" style="flex:1;min-height:48px;border-radius:14px;font-size:15px;font-weight:700;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#e2e8f0;">رجوع</button>
<button class="btn btn-primary" id="btn-rematch" style="flex:1;min-height:48px;border-radius:14px;font-size:15px;font-weight:700;background:linear-gradient(135deg,#10b981,#06b6d4);">${t('result.rematch')}</button>
<button class="btn btn-secondary" id="btn-back" style="flex:1;min-height:48px;border-radius:14px;font-size:15px;font-weight:700;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);color:#e2e8f0;">${t('common.back')}</button>
</div>
</div>
<style>
......@@ -150,7 +150,7 @@ async function completeOnServer(el, matchId, result, mode) {
if (ratingEl) {
const sign = ratingChange >= 0 ? '+' : '';
const color = ratingChange > 0 ? '#4ade80' : ratingChange === 0 ? '#fbbf24' : '#fca5a5';
ratingEl.innerHTML = `التصنيف: <span style="color:${color};font-weight:700;">${sign}${ratingChange}</span>`;
ratingEl.innerHTML = `${t('game.rating')}: <span style="color:${color};font-weight:700;">${sign}${ratingChange}</span>`;
}
bus.emit('coins:earned', { amount: coins });
......
......@@ -25,26 +25,26 @@ function renderMenu(el) {
<div class="domino-menu">
<div class="dm-hero">
<div class="dm-icon">${emoji('domino_tile', '🁣', 56)}</div>
<h1 class="dm-title">دومينو</h1>
<p class="dm-subtitle">أول من يوصل الهدف يفوز!</p>
<h1 class="dm-title">${t('domino.title')}</h1>
<p class="dm-subtitle">${t('domino.subtitle')}</p>
</div>
<div class="dm-buttons">
<button class="dm-btn dm-btn-primary" id="btn-bot">
<span class="dm-btn-icon">${emoji('robot', '🤖', 22)}</span>
<span class="dm-btn-label">ضد البوت</span>
<span class="dm-btn-label">${t('game.vs_bot')}</span>
</button>
<button class="dm-btn dm-btn-online" id="btn-online">
<span class="dm-btn-icon">${emoji('globe', '🌍', 22)}</span>
<span class="dm-btn-label">أونلاين</span>
<span class="dm-btn-label">${t('game.online')}</span>
</button>
<button class="dm-btn dm-btn-friend" id="btn-friend">
<span class="dm-btn-icon">${emoji('handshake', '🤝', 20)}</span>
<span class="dm-btn-label">تحدي صديق</span>
<span class="dm-btn-label">${t('game.vs_friend')}</span>
</button>
</div>
<button class="dm-back" id="btn-back">رجوع</button>
<button class="dm-back" id="btn-back">${t('common.back')}</button>
</div>
<style>
.domino-menu {
......@@ -98,16 +98,16 @@ function renderMenu(el) {
function renderBotPicker(el) {
const levels = [
{ key: 'beginner', label: 'مبتدئ', desc: 'يلعب عشوائي', icon: '😊', color: '#4ade80', bg: 'rgba(74,222,128,0.08)', border: 'rgba(74,222,128,0.2)' },
{ key: 'intermediate', label: 'متوسط', desc: 'يفضل النقاط العالية', icon: '🧐', color: '#fbbf24', bg: 'rgba(251,191,36,0.08)', border: 'rgba(251,191,36,0.2)' },
{ key: 'expert', label: 'خبير', desc: 'استراتيجي ومخادع', icon: '🧠', color: '#f87171', bg: 'rgba(248,113,113,0.08)', border: 'rgba(248,113,113,0.2)' }
{ key: 'beginner', label: t('domino.beginner'), desc: t('domino.beginner_desc'), icon: '😊', color: '#4ade80', bg: 'rgba(74,222,128,0.08)', border: 'rgba(74,222,128,0.2)' },
{ key: 'intermediate', label: t('domino.intermediate'), desc: t('domino.intermediate_desc'), icon: '🧐', color: '#fbbf24', bg: 'rgba(251,191,36,0.08)', border: 'rgba(251,191,36,0.2)' },
{ key: 'expert', label: t('domino.expert'), desc: t('domino.expert_desc'), icon: '🧠', color: '#f87171', bg: 'rgba(248,113,113,0.08)', border: 'rgba(248,113,113,0.2)' }
];
el.innerHTML = `
<div class="domino-menu">
<div class="dm-hero">
<h2 style="font-size:20px;font-weight:700;color:#f8fafc;margin:0;">اختر مستوى البوت</h2>
<p style="font-size:12px;color:#94a3b8;margin:8px 0 0;">كل مستوى له استراتيجية مختلفة</p>
<h2 style="font-size:20px;font-weight:700;color:#f8fafc;margin:0;">${t('domino.select_bot')}</h2>
<p style="font-size:12px;color:#94a3b8;margin:8px 0 0;">${t('domino.bot_strategy')}</p>
</div>
<div class="dm-buttons">
${levels.map(l => `
......@@ -121,7 +121,7 @@ function renderBotPicker(el) {
</button>
`).join('')}
</div>
<button class="dm-back" id="btn-back-bot">رجوع</button>
<button class="dm-back" id="btn-back-bot">${t('common.back')}</button>
</div>
<style>
.dm-level-btn {
......@@ -150,16 +150,16 @@ function renderBotPicker(el) {
function renderTargetPicker(el, botLevel) {
const targets = [
{ value: 50, label: '50 نقطة', desc: 'مباراة سريعة (~5 دقائق)', icon: '⚡', color: '#4ade80', bg: 'rgba(74,222,128,0.08)', border: 'rgba(74,222,128,0.2)' },
{ value: 100, label: '100 نقطة', desc: 'كلاسيك (~10 دقائق)', icon: '🎯', color: '#fbbf24', bg: 'rgba(251,191,36,0.08)', border: 'rgba(251,191,36,0.2)' },
{ value: 150, label: '150 نقطة', desc: 'مباراة طويلة (~15 دقيقة)', icon: '🔥', color: '#f87171', bg: 'rgba(248,113,113,0.08)', border: 'rgba(248,113,113,0.2)' }
{ value: 50, label: t('domino.50_points'), desc: t('domino.50_desc'), icon: '⚡', color: '#4ade80', bg: 'rgba(74,222,128,0.08)', border: 'rgba(74,222,128,0.2)' },
{ value: 100, label: t('domino.100_points'), desc: t('domino.100_desc'), icon: '🎯', color: '#fbbf24', bg: 'rgba(251,191,36,0.08)', border: 'rgba(251,191,36,0.2)' },
{ value: 150, label: t('domino.150_points'), desc: t('domino.150_desc'), icon: '🔥', color: '#f87171', bg: 'rgba(248,113,113,0.08)', border: 'rgba(248,113,113,0.2)' }
];
el.innerHTML = `
<div class="domino-menu">
<div class="dm-hero">
<h2 style="font-size:20px;font-weight:700;color:#f8fafc;margin:0;">اختر الهدف</h2>
<p style="font-size:12px;color:#94a3b8;margin:8px 0 0;">أول من يوصل النقاط يفوز بالمباراة</p>
<h2 style="font-size:20px;font-weight:700;color:#f8fafc;margin:0;">${t('domino.select_target')}</h2>
<p style="font-size:12px;color:#94a3b8;margin:8px 0 0;">${t('domino.target_hint')}</p>
</div>
<div class="dm-buttons">
${targets.map(t => `
......@@ -173,7 +173,7 @@ function renderTargetPicker(el, botLevel) {
</button>
`).join('')}
</div>
<button class="dm-back" id="btn-back-target">رجوع</button>
<button class="dm-back" id="btn-back-target">${t('common.back')}</button>
</div>
`;
......@@ -199,7 +199,7 @@ function renderLobby(el, { challengeId, friendId, friendName }) {
<div class="dm-hero">
<div style="font-size:44px;margin-bottom:12px;">${emoji('domino_tile', '🁣', 44)}</div>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;margin:0;">
${isHost ? 'بانتظار الصديق...' : `تحدي من ${friendName || 'صديق'}`}
${isHost ? t('room.waiting_friend') : t('room.challenge_from', { name: friendName || t('common.friend') })}
</h2>
</div>
......@@ -207,12 +207,12 @@ function renderLobby(el, { challengeId, friendId, friendName }) {
<div style="width:64px;height:64px;border-radius:50%;border:3px solid rgba(228,172,56,0.4);display:flex;align-items:center;justify-content:center;animation:dmPulseRing 2s ease-in-out infinite;">
${emoji('clock', '⏳', 26)}
</div>
<div style="font-size:13px;color:#E4AC38;" id="lobby-msg">${isHost ? 'أرسل الدعوة لصديقك' : 'اضغط قبول للبدء'}</div>
<div style="font-size:13px;color:#E4AC38;" id="lobby-msg">${isHost ? t('room.send_invite') : t('room.tap_accept')}</div>
</div>
<div style="display:flex;gap:10px;margin-top:20px;">
${!isHost ? `<button class="dm-btn dm-btn-online" id="btn-accept" style="min-height:48px;padding:0 28px;font-size:15px;">قبول</button>` : ''}
<button class="dm-btn dm-btn-friend" id="btn-cancel-lobby" style="min-height:48px;padding:0 20px;font-size:14px;border-color:rgba(239,68,68,0.3);color:#fca5a5;">إلغاء</button>
${!isHost ? `<button class="dm-btn dm-btn-online" id="btn-accept" style="min-height:48px;padding:0 28px;font-size:15px;">${t('social.accept')}</button>` : ''}
<button class="dm-btn dm-btn-friend" id="btn-cancel-lobby" style="min-height:48px;padding:0 20px;font-size:14px;border-color:rgba(239,68,68,0.3);color:#fca5a5;">${t('common.cancel')}</button>
</div>
</div>
<style>
......@@ -246,7 +246,7 @@ async function pollForAcceptance(el, friendId) {
if (data?.id) {
const matchId = data.id;
const msgEl = el.querySelector('#lobby-msg');
if (msgEl) msgEl.textContent = 'تم إنشاء المباراة، بانتظار القبول...';
if (msgEl) msgEl.textContent = t('room.match_created');
pollTimer = setInterval(async () => {
try {
......
......@@ -29,7 +29,7 @@ let COLORS = DEFAULT_COLORS;
let COLORS_LIGHT = DEFAULT_COLORS_LIGHT;
const PAWN_SLOTS = ['ludo_pawn_red', 'ludo_pawn_green', 'ludo_pawn_yellow', 'ludo_pawn_blue'];
const pawnImages = [null, null, null, null];
let PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3'];
let PLAYER_NAMES = [t('common.you'), 'Bot 1', 'Bot 2', 'Bot 3'];
function lightenColor(hex, amount) {
const num = parseInt(hex.replace('#', ''), 16);
......@@ -80,18 +80,18 @@ export function mountGame(el, params) {
if (mode === 'live' && params.players) {
PLAYER_NAMES = params.players.map((p, i) => {
if (i === myPlayerIndex) return 'أنت';
if (i === myPlayerIndex) return t('common.you');
if (p.startsWith('bot')) return 'Bot ' + p.split('_')[1];
return 'لاعب ' + (i + 1);
return t('common.player') + ' ' + (i + 1);
});
} else if (mode === 'local-multi') {
PLAYER_NAMES = activeSeats.map((seatIdx, i) => {
if (i === 0) return 'أنت';
if (i < humanCount) return 'لاعب ' + (i + 1);
if (i === 0) return t('common.you');
if (i < humanCount) return t('common.player') + ' ' + (i + 1);
return 'Bot ' + (i - humanCount + 1);
});
} else {
PLAYER_NAMES = ['أنت', 'Bot 1', 'Bot 2', 'Bot 3'].slice(0, numPlayers);
PLAYER_NAMES = [t('common.you'), 'Bot 1', 'Bot 2', 'Bot 3'].slice(0, numPlayers);
}
game = rules.createGame(numPlayers, activeSeats);
......@@ -113,7 +113,7 @@ export function mountGame(el, params) {
const isMe = i === myPlayerIndex;
const isBot = PLAYER_NAMES[i]?.startsWith('Bot');
const avatar = isMe && player.avatar_url ? `<img src="${player.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : isBot ? '🤖' : '👤';
const name = isMe ? (player.display_name || player.username || 'أنت') : (PLAYER_NAMES[i] || 'Bot');
const name = isMe ? (player.display_name || player.username || t('common.you')) : (PLAYER_NAMES[i] || 'Bot');
const boardSlot = activeSeats[i] ?? i;
const level = isMe ? `Lv.${player.level || 1}` : (isBot ? '' : '');
return { i, avatar, name, level, color: COLORS[boardSlot] };
......@@ -137,7 +137,7 @@ export function mountGame(el, params) {
<button class="btn btn-secondary" id="emote-btn" style="min-height:44px;min-width:44px;padding:0;font-size:18px;border-radius:50%;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);">😄</button>
<div id="dice-box" style="width:56px;height:56px;background:linear-gradient(145deg,#ffffff,#f0ede8);border-radius:12px;display:grid;grid-template:repeat(3,1fr)/repeat(3,1fr);padding:7px;box-shadow:0 4px 12px rgba(0,0,0,0.4),inset 0 2px 0 rgba(255,255,255,0.9),0 0 0 2px rgba(228,172,56,0.15);transition:transform 0.15s cubic-bezier(0.34,1.56,0.64,1),opacity 0.3s ease,filter 0.3s ease;">
</div>
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 32px;min-height:52px;border-radius:14px;background:linear-gradient(135deg,#E4AC38,#F59E0B);box-shadow:0 4px 14px rgba(228,172,56,0.3);font-weight:800;transition:background 0.3s ease,color 0.3s ease,opacity 0.3s ease;" disabled>ارمِ النرد</button>
<button class="btn btn-primary" id="roll-btn" style="font-size:15px;padding:14px 32px;min-height:52px;border-radius:14px;background:linear-gradient(135deg,#E4AC38,#F59E0B);box-shadow:0 4px 14px rgba(228,172,56,0.3);font-weight:800;transition:background 0.3s ease,color 0.3s ease,opacity 0.3s ease;" disabled>${t('game.roll_dice')}</button>
<div id="turn-status" style="position:absolute;top:0;left:50%;transform:translateX(-50%);font-size:11px;font-weight:700;padding:2px 12px;border-radius:0 0 8px 8px;display:none;"></div>
</div>
</div>
......@@ -240,7 +240,7 @@ export function mountGame(el, params) {
const panel = el.querySelector(`#pp-${i}`);
if (panel) {
const nameEl = panel.querySelector('.pp-name');
const displayName = profile.display_name || profile.username || 'لاعب';
const displayName = profile.display_name || profile.username || t('common.player');
if (nameEl) nameEl.textContent = displayName;
PLAYER_NAMES[i] = displayName;
const avatarEl = panel.querySelector('.pp-avatar');
......@@ -385,7 +385,7 @@ async function botLoop(el) {
const botPanel = el.querySelector(`#pp-${game.currentPlayer}`);
const botStatusEl = botPanel ? botPanel.querySelector('.pp-status') : null;
const originalStatusText = botStatusEl ? botStatusEl.textContent : '';
if (botStatusEl) botStatusEl.textContent = 'يفكر...';
if (botStatusEl) botStatusEl.textContent = t('game.thinking');
await new Promise(r => setTimeout(r, thinkDelay));
if (game.gameOver || isMyTurn()) { if (botStatusEl) botStatusEl.textContent = originalStatusText; return; }
......@@ -398,7 +398,7 @@ async function botLoop(el) {
const decideDelay = (personality.thinkMin * 0.5 + Math.random() * (personality.thinkMax - personality.thinkMin) * 0.5) * turboMul;
// Show thinking indicator again while deciding
if (botStatusEl) botStatusEl.textContent = 'يفكر...';
if (botStatusEl) botStatusEl.textContent = t('game.thinking');
await new Promise(r => setTimeout(r, decideDelay));
if (botStatusEl) botStatusEl.textContent = originalStatusText;
if (game.gameOver || isMyTurn()) return;
......@@ -665,8 +665,8 @@ function showOpponentPopup(el, profile) {
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:4px;">${profile.display_name || profile.username}</div>
<div style="font-size:11px;color:#64748b;margin-bottom:14px;">Level ${profile.level || 1}</div>
<div style="display:flex;gap:8px;justify-content:center;">
<button id="opp-add-friend" style="padding:8px 16px;background:#2563EB;border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;">➕ صديق</button>
<button id="opp-close" style="padding:8px 16px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#94a3b8;font-size:12px;cursor:pointer;">إغلاق</button>
<button id="opp-add-friend" style="padding:8px 16px;background:#2563EB;border:none;border-radius:8px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;">➕ ${t('mp.add_friend')}</button>
<button id="opp-close" style="padding:8px 16px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:8px;color:#94a3b8;font-size:12px;cursor:pointer;">${t('common.close')}</button>
</div>
`;
document.body.appendChild(popup);
......@@ -674,7 +674,7 @@ function showOpponentPopup(el, profile) {
popup.querySelector('#opp-close').addEventListener('click', () => popup.remove());
popup.querySelector('#opp-add-friend').addEventListener('click', async () => {
await mp.addFriendFromGame(profile.id);
popup.querySelector('#opp-add-friend').textContent = '✓ تم';
popup.querySelector('#opp-add-friend').textContent = t('common.done');
popup.querySelector('#opp-add-friend').style.background = '#34D399';
juice.hapticLight();
setTimeout(() => popup.remove(), 1000);
......@@ -1137,7 +1137,7 @@ function checkTurboMode(el) {
game.turboMode = true;
const banner = document.createElement('div');
banner.style.cssText = 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(228,172,56,0.95);color:#000;padding:10px 24px;border-radius:12px;font-size:14px;font-weight:700;z-index:100;';
banner.textContent = '⚡ وضع السرعة!';
banner.textContent = t('game.speed_mode');
el.querySelector('#ludo-wrap')?.appendChild(banner);
banner.animate([{opacity:0,transform:'translate(-50%,-50%) scale(0.5)'},{opacity:1,transform:'translate(-50%,-50%) scale(1)'},{opacity:0,transform:'translate(-50%,-50%) translateY(-20px)'}], {duration:2000}).onfinish = () => banner.remove();
juice.hapticHeavy();
......@@ -1190,13 +1190,13 @@ function updatePanels(el) {
? COLORS[slot]
: 'linear-gradient(135deg,#E4AC38,#F59E0B)';
turnStatus.style.color = game.mode === 'local-multi' && slot === 2 ? '#000' : '#000';
turnStatus.textContent = game.mode === 'local-multi' ? `دور ${currentName}` : 'دورك!';
turnStatus.textContent = game.mode === 'local-multi' ? t('game.turn_of', { name: currentName }) : t('game.your_turn');
turnStatus.style.animation = 'fadeIn 0.3s ease-out';
} else {
turnStatus.style.display = 'block';
turnStatus.style.background = 'rgba(255,255,255,0.08)';
turnStatus.style.color = '#94a3b8';
turnStatus.textContent = isBot ? `${currentName} يفكر...` : `دور ${currentName}`;
turnStatus.textContent = isBot ? t('game.bot_thinking', { name: currentName }) : t('game.turn_of', { name: currentName });
turnStatus.style.animation = 'fadeIn 0.3s ease-out';
}
}
......@@ -1219,7 +1219,7 @@ function updatePanels(el) {
if (myTurn) {
btn.style.visibility = 'visible';
btn.disabled = !canRoll;
btn.textContent = game.rolled ? 'اختر قطعة' : 'ارمِ النرد';
btn.textContent = game.rolled ? t('game.choose_piece') : t('game.roll_dice');
btn.style.opacity = canRoll ? '1' : '0.5';
btn.style.background = 'linear-gradient(135deg,#E4AC38,#F59E0B)';
btn.style.color = '#000';
......@@ -1229,7 +1229,7 @@ function updatePanels(el) {
} else {
btn.style.visibility = 'visible';
btn.disabled = true;
btn.textContent = isBot ? 'Bot يلعب...' : 'انتظر دورك';
btn.textContent = isBot ? t('game.bot_thinking', { name: 'Bot' }) : t('game.opponent_turn');
btn.style.opacity = '0.5';
btn.style.background = 'rgba(255,255,255,0.06)';
btn.style.color = '#64748b';
......@@ -1368,11 +1368,11 @@ function renderMiniDice(miniDice, value) {
async function handleExit(el) {
if (game.gameOver) return;
const confirmed = await modal.confirm('هل تريد الخروج من المباراة؟', {
title: 'مغادرة',
const confirmed = await modal.confirm(t('game.leave_confirm'), {
title: t('game.leave_title'),
icon: '🚪',
confirmText: 'نعم، اخرج',
cancelText: 'ابقَ',
confirmText: t('game.leave'),
cancelText: t('game.stay'),
danger: true
});
if (!confirmed) return;
......@@ -1467,23 +1467,23 @@ function endGame(el) {
// ===== SOCIAL: EMOTES + PHRASES =====
const EMOTES = [
{ key: '😂', label: 'ههه' },
{ key: '😮', label: 'واو' },
{ key: '😡', label: 'غضب' },
{ key: '👏', label: 'برافو' },
{ key: '🔥', label: 'حماس' },
{ key: '😢', label: 'حزين' },
{ key: '💪', label: 'قوي' },
{ key: '😎', label: 'كول' },
{ key: '😂', get label() { return t('emote.laugh'); } },
{ key: '😮', get label() { return t('emote.wow'); } },
{ key: '😡', get label() { return t('emote.angry'); } },
{ key: '👏', get label() { return t('emote.good_move'); } },
{ key: '🔥', get label() { return t('emote.hurry'); } },
{ key: '😢', get label() { return t('ludo.emote_sad'); } },
{ key: '💪', get label() { return t('ludo.emote_strong'); } },
{ key: '😎', get label() { return t('ludo.emote_cool'); } },
];
const PHRASES = [
{ key: 'gl', text: 'حظ سعيد!' },
{ key: 'gl', get text() { return t('ludo.phrase_gl'); } },
{ key: 'gg', text: 'GG!' },
{ key: 'hurry', text: 'يلا بسرعة!' },
{ key: 'nice', text: 'نايس!' },
{ key: 'oops', text: 'يا ساتر!' },
{ key: 'wow', text: 'ما شاء الله!' },
{ key: 'hurry', get text() { return t('ludo.phrase_hurry'); } },
{ key: 'nice', get text() { return t('ludo.phrase_nice'); } },
{ key: 'oops', get text() { return t('ludo.phrase_oops'); } },
{ key: 'wow', get text() { return t('ludo.phrase_wow'); } },
];
let emoteCooldown = false;
......
......@@ -6,7 +6,7 @@ import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
const PLACE_EMOJI = ['🥇', '🥈', '🥉', '4️⃣'];
const PLACE_LABEL = ['الأول', 'الثاني', 'الثالث', 'الرابع'];
const PLACE_LABEL = [t('ludo.place_first'), t('ludo.place_second'), t('ludo.place_third'), t('ludo.place_fourth')];
const PLACE_COLOR = ['#FFD700', '#C0C0C0', '#CD7F32', '#64748b'];
export function mountResult(el, params) {
......@@ -25,7 +25,7 @@ export function mountResult(el, params) {
const pIdx = winners[rank];
leaderboard.push({
rank: rank + 1,
name: playerNames[pIdx] || `لاعب ${pIdx + 1}`,
name: playerNames[pIdx] || `${t('common.player')} ${pIdx + 1}`,
color: playerColors[pIdx] || '#94a3b8',
isMe: pIdx === 0,
});
......@@ -35,7 +35,7 @@ export function mountResult(el, params) {
if (!winners.includes(i)) {
leaderboard.push({
rank: leaderboard.length + 1,
name: playerNames[i] || `لاعب ${i + 1}`,
name: playerNames[i] || `${t('common.player')} ${i + 1}`,
color: playerColors[i] || '#94a3b8',
isMe: i === 0,
});
......@@ -50,11 +50,11 @@ export function mountResult(el, params) {
: place === 3 ? emoji('medal_3', '🥉', 64)
: emoji('skull', '💀', 64);
const heroTitle = resigned ? 'انسحبت من المباراة'
: place === 1 ? 'مبروك! أنت البطل'
: place === 2 ? 'المركز الثاني — أحسنت!'
: place === 3 ? 'المركز الثالث'
: 'المركز الرابع';
const heroTitle = resigned ? t('game.withdrew')
: place === 1 ? t('game.congrats')
: place === 2 ? t('game.second_place')
: place === 3 ? t('game.third_place')
: t('game.fourth_place');
const heroColor = resigned ? '#EF4444'
: place === 1 ? '#FFD700'
......@@ -73,14 +73,14 @@ export function mountResult(el, params) {
<!-- Leaderboard -->
${leaderboard.length > 0 ? `
<div class="lr-leaderboard">
<div class="lr-lb-title">ترتيب اللاعبين</div>
<div class="lr-lb-title">${t('game.player_ranking')}</div>
${leaderboard.map(p => `
<div class="lr-lb-row ${p.isMe ? 'lr-lb-me' : ''}" style="--pc:${p.color};">
<div class="lr-lb-rank" style="color:${PLACE_COLOR[p.rank - 1] || '#64748b'};">
${p.rank <= 3 ? PLACE_EMOJI[p.rank - 1] : p.rank}
</div>
<div class="lr-lb-color" style="background:${p.color};"></div>
<div class="lr-lb-name">${p.name}${p.isMe ? ' (أنت)' : ''}</div>
<div class="lr-lb-name">${p.name}${p.isMe ? ` (${t('common.you')})` : ''}</div>
<div class="lr-lb-place">${PLACE_LABEL[p.rank - 1] || ''}</div>
</div>
`).join('')}
......
......@@ -22,29 +22,29 @@ function renderMenu(el) {
<div class="lr-wrap">
<div class="lr-hero">
<div class="lr-icon">${emoji('dice', '🎲', 56)}</div>
<h1 class="lr-title">لودو</h1>
<p class="lr-subtitle">أول من يوصّل كل قطعه يفوز!</p>
<h1 class="lr-title">${t('ludo.title')}</h1>
<p class="lr-subtitle">${t('ludo.subtitle')}</p>
</div>
<div class="lr-buttons">
<button class="lr-btn lr-btn-primary" id="btn-local">
<span class="lr-btn-icon">${emoji('gamepad', '🎮', 22)}</span>
<span class="lr-btn-label">لعب محلي</span>
<span class="lr-btn-desc">اختر عدد اللاعبين والبوتات</span>
<span class="lr-btn-label">${t('ludo.local_play')}</span>
<span class="lr-btn-desc">${t('ludo.local_desc')}</span>
</button>
<button class="lr-btn lr-btn-online" id="btn-online">
<span class="lr-btn-icon">${emoji('globe', '🌍', 22)}</span>
<span class="lr-btn-label">أونلاين</span>
<span class="lr-btn-desc">العب ضد لاعبين حقيقيين</span>
<span class="lr-btn-label">${t('game.online')}</span>
<span class="lr-btn-desc">${t('ludo.online_desc')}</span>
</button>
<button class="lr-btn lr-btn-friend" id="btn-friend">
<span class="lr-btn-icon">${emoji('handshake', '🤝', 20)}</span>
<span class="lr-btn-label">تحدي صديق</span>
<span class="lr-btn-desc">ادعُ أصدقاءك للعب</span>
<span class="lr-btn-label">${t('game.vs_friend')}</span>
<span class="lr-btn-desc">${t('ludo.friend_desc')}</span>
</button>
</div>
<button class="lr-back" id="btn-back">رجوع</button>
<button class="lr-back" id="btn-back">${t('common.back')}</button>
</div>
${getStyles()}
`;
......@@ -57,7 +57,7 @@ function renderMenu(el) {
el.querySelector('#btn-online').addEventListener('click', () => {
audio.play('click');
if (store.get('auth.isGuest')) {
bus.emit('toast', { text: 'سجّل دخولك للعب أونلاين' });
bus.emit('toast', { text: t('play.login_required_online') });
return;
}
scene.replace('ludo-room', { mode: 'setup', type: 'online' });
......@@ -66,7 +66,7 @@ function renderMenu(el) {
el.querySelector('#btn-friend').addEventListener('click', () => {
audio.play('click');
if (store.get('auth.isGuest')) {
bus.emit('toast', { text: 'سجّل دخولك لتحدي صديق' });
bus.emit('toast', { text: t('play.login_required_friend') });
return;
}
scene.push('challenge-friend', { game: 'ludo' });
......@@ -85,25 +85,25 @@ function renderSetup(el, params) {
el.innerHTML = `
<div class="lr-wrap">
<div class="lr-hero" style="margin-bottom:16px;">
<h2 class="lr-title" style="font-size:20px;">${isOnline ? 'إعدادات الأونلاين' : 'إعدادات اللعب'}</h2>
<p class="lr-subtitle">${isOnline ? 'اختر عدد اللاعبين الحقيقيين' : 'اختر تشكيلة اللاعبين'}</p>
<h2 class="lr-title" style="font-size:20px;">${isOnline ? t('ludo.settings_online') : t('ludo.settings_play')}</h2>
<p class="lr-subtitle">${isOnline ? t('ludo.settings_online_hint') : t('ludo.settings_play_hint')}</p>
</div>
<!-- Player Count -->
<div class="lr-section">
<div class="lr-section-title">عدد اللاعبين</div>
<div class="lr-section-title">${t('ludo.player_count')}</div>
<div class="lr-grid" id="player-count-grid">
<button class="lr-chip lr-chip-active" data-count="4">
<span class="lr-chip-num">4</span>
<span class="lr-chip-label">كلاسيك</span>
<span class="lr-chip-label">${t('ludo.classic')}</span>
</button>
<button class="lr-chip" data-count="3">
<span class="lr-chip-num">3</span>
<span class="lr-chip-label">ثلاثي</span>
<span class="lr-chip-label">${t('ludo.triple')}</span>
</button>
<button class="lr-chip" data-count="2">
<span class="lr-chip-num">2</span>
<span class="lr-chip-label">مبارزة</span>
<span class="lr-chip-label">${t('ludo.duel')}</span>
</button>
</div>
</div>
......@@ -111,7 +111,7 @@ function renderSetup(el, params) {
<!-- Bot/Human Distribution (local only) -->
${!isOnline ? `
<div class="lr-section" id="distribution-section">
<div class="lr-section-title">توزيعة اللاعبين</div>
<div class="lr-section-title">${t('ludo.player_layout')}</div>
<div id="dist-options"></div>
</div>
` : ''}
......@@ -119,19 +119,19 @@ function renderSetup(el, params) {
${!isOnline ? `
<!-- Bot Difficulty -->
<div class="lr-section" id="difficulty-section">
<div class="lr-section-title">مستوى البوت</div>
<div class="lr-section-title">${t('ludo.bot_level')}</div>
<div class="lr-grid" id="difficulty-grid">
<button class="lr-chip" data-diff="easy">
<span>😊</span>
<span class="lr-chip-label">سهل</span>
<span class="lr-chip-label">${t('ludo.easy')}</span>
</button>
<button class="lr-chip lr-chip-active" data-diff="medium">
<span>🧐</span>
<span class="lr-chip-label">متوسط</span>
<span class="lr-chip-label">${t('ludo.medium')}</span>
</button>
<button class="lr-chip" data-diff="hard">
<span>🧠</span>
<span class="lr-chip-label">صعب</span>
<span class="lr-chip-label">${t('ludo.hard')}</span>
</button>
</div>
</div>
......@@ -139,16 +139,16 @@ function renderSetup(el, params) {
<!-- Seating Preview -->
<div class="lr-section">
<div class="lr-section-title">ترتيب المقاعد</div>
<div class="lr-section-title">${t('ludo.seat_order')}</div>
<div id="seat-preview" class="lr-seat-preview"></div>
</div>
<!-- Start Button -->
<button class="lr-btn lr-btn-start" id="btn-start">
${isOnline ? 'ابحث عن مباراة' : 'ابدأ اللعب'}
${isOnline ? t('ludo.search_match') : t('ludo.start_play')}
</button>
<button class="lr-back" id="btn-back-setup">رجوع</button>
<button class="lr-back" id="btn-back-setup">${t('common.back')}</button>
</div>
${getStyles()}
`;
......@@ -189,10 +189,10 @@ function renderSetup(el, params) {
for (let h = 1; h <= playerCount; h++) {
const bots = playerCount - h;
const label = h === playerCount
? `${h} لاعبين (بدون بوت)`
? t('ludo.players_no_bot', { n: h })
: h === 1
? `لاعب واحد + ${bots} بوت`
: `${h} لاعبين + ${bots} بوت`;
? t('ludo.one_player_bots', { n: bots })
: t('ludo.players_bots', { h, n: bots });
options.push({ humans: h, bots, label });
}
......@@ -225,7 +225,7 @@ function renderSetup(el, params) {
const seats = getSeatPositions(playerCount);
const colors = ['#E53935', '#43A047', '#FDD835', '#1E88E5'];
const labels = ['أحمر', 'أخضر', 'أصفر', 'أزرق'];
const labels = [t('ludo.red'), t('ludo.green'), t('ludo.yellow'), t('ludo.blue')];
const positions = ['bottom-left', 'top-left', 'top-right', 'bottom-right'];
preview.innerHTML = `
......@@ -290,10 +290,10 @@ function renderSearching(el, params) {
<div class="lr-pulse-ring">
<div class="lr-pulse-inner">${emoji('dice', '🎲', 32)}</div>
</div>
<h2 class="lr-title" style="font-size:18px;margin-top:20px;">جاري البحث...</h2>
<p class="lr-subtitle">بنوصّلك بلاعبين قريب</p>
<h2 class="lr-title" style="font-size:18px;margin-top:20px;">${t('ludo.searching')}</h2>
<p class="lr-subtitle">${t('ludo.searching_hint')}</p>
</div>
<button class="lr-btn lr-btn-friend" id="btn-cancel" style="max-width:200px;">إلغاء</button>
<button class="lr-btn lr-btn-friend" id="btn-cancel" style="max-width:200px;">${t('common.cancel')}</button>
</div>
${getStyles()}
`;
......
......@@ -7,7 +7,7 @@ import { emoji } from '../../../core/theme.js';
export async function mountBrowser(el) {
el.innerHTML = `
<div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);">
<h2 style="font-size:20px;font-weight:700;">المنظمات</h2>
<h2 style="font-size:20px;font-weight:700;">${t('org.title')}</h2>
<div id="org-list">
<div class="skeleton" style="height:80px;margin-bottom:var(--s-3);"></div>
<div class="skeleton" style="height:80px;"></div>
......@@ -37,9 +37,9 @@ function renderOrgs(el, orgs) {
</div>
<div style="flex:1;">
<div style="font-size:14px;font-weight:600;">${org.name_ar || org.name}</div>
<div style="font-size:11px;color:var(--text-secondary);">${emoji('people', '👥', 11)} ${org.member_count || 0} عضو</div>
<div style="font-size:11px;color:var(--text-secondary);">${emoji('people', '👥', 11)} ${org.member_count || 0} ${t('profile.member')}</div>
</div>
<button class="btn btn-secondary" style="font-size:11px;min-height:32px;padding:var(--s-1) var(--s-3);">انضم</button>
<button class="btn btn-secondary" style="font-size:11px;min-height:32px;padding:var(--s-1) var(--s-3);">${t('org.join_btn')}</button>
</div>
`).join('');
......
......@@ -43,18 +43,18 @@ function renderOrg(el, org) {
<div style="display:flex;gap:var(--s-4);justify-content:center;margin-top:var(--s-4);">
<div style="text-align:center;">
<div style="font-size:18px;font-weight:700;color:var(--gold);">${org.member_count || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">أعضاء</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('org.members')}</div>
</div>
<div style="text-align:center;">
<div style="font-size:18px;font-weight:700;color:var(--cyan);">${org.game_focus || 'chess'}</div>
<div style="font-size:11px;color:var(--text-secondary);">اللعبة</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('org.game')}</div>
</div>
</div>
</div>
${org.members && org.members.length > 0 ? `
<div class="card" style="padding:var(--s-4);">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">الأعضاء (${org.members.length})</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${t('org.members_section')} (${org.members.length})</div>
${org.members.slice(0, 10).map(m => `
<div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-1) 0;">
<div style="width:24px;height:24px;border-radius:50%;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;font-size:12px;">${emoji('person', '👤', 12)}</div>
......@@ -65,14 +65,14 @@ function renderOrg(el, org) {
</div>
` : ''}
<button class="btn btn-primary w-full" id="join-btn">انضم للمنظمة</button>
<button class="btn btn-primary w-full" id="join-btn">${t('org.join')}</button>
`;
el.querySelector('#join-btn')?.addEventListener('click', async () => {
try {
await net.post('orgs.php', { action: 'join', org_id: org.id });
audio.play('coin', 'reward');
el.querySelector('#join-btn').textContent = '✅ تم الانضمام';
el.querySelector('#join-btn').textContent = `✅ ${t('org.joined')}`;
el.querySelector('#join-btn').disabled = true;
} catch (e) {
audio.play('click');
......
......@@ -3,6 +3,7 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
export function mountChallenge(el) {
......@@ -10,10 +11,10 @@ export function mountChallenge(el) {
<div class="cf-layout">
<div class="cf-header">
<button id="cf-back" class="cf-back-btn">→</button>
<div class="cf-title">${emoji('challenge_swords', '⚔️', 18)} تحدّي صديق</div>
<div class="cf-title">${emoji('challenge_swords', '⚔️', 18)} ${t('challenge.title')}</div>
</div>
<div class="cf-body" id="cf-body">
<div style="text-align:center;padding:32px;color:#64748b;font-size:13px;">${emoji('loading', '⏳', 14)} جاري تحميل الأصدقاء...</div>
<div style="text-align:center;padding:32px;color:#64748b;font-size:13px;">${emoji('loading', '⏳', 14)} ${t('challenge.loading_friends')}</div>
</div>
</div>
<style>
......@@ -55,9 +56,9 @@ async function loadFriendsForChallenge(el) {
body.innerHTML = `
<div style="text-align:center;padding:48px 24px;">
<div style="font-size:48px;margin-bottom:12px;opacity:0.5;">${emoji('people', '👥', 48)}</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:6px;">لا يوجد أصدقاء بعد</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">أضف أصدقاء أولاً لتتمكن من تحديهم</div>
<button class="btn btn-primary" id="cf-go-social" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ابحث عن لاعبين</button>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:6px;">${t('challenge.no_friends')}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">${t('challenge.no_friends_hint')}</div>
<button class="btn btn-primary" id="cf-go-social" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ${t('social.search_players')}</button>
</div>`;
body.querySelector('#cf-go-social')?.addEventListener('click', () => scene.push('friends'));
return;
......@@ -69,12 +70,12 @@ async function loadFriendsForChallenge(el) {
let html = '';
if (online.length > 0) {
html += `<div class="cf-section-title">${emoji('green_circle', '🟢', 11)} متصلين الآن (${online.length})</div>`;
html += `<div class="cf-section-title">${emoji('green_circle', '🟢', 11)} ${t('challenge.online_now', { n: online.length })}</div>`;
html += online.map(f => renderChallengeCard(f, true)).join('');
}
if (offline.length > 0) {
html += `<div class="cf-section-title" style="margin-top:${online.length > 0 ? '16px' : '0'};">${emoji('gray_circle', '⚪', 11)} غير متصلين</div>`;
html += `<div class="cf-section-title" style="margin-top:${online.length > 0 ? '16px' : '0'};">${emoji('gray_circle', '⚪', 11)} ${t('challenge.offline')}</div>`;
html += offline.map(f => renderChallengeCard(f, false)).join('');
}
......@@ -88,7 +89,7 @@ async function loadFriendsForChallenge(el) {
juice.hapticLight();
const uid = btn.dataset.challenge;
const card = btn.closest('.cf-card');
const name = card?.querySelector('.cf-card-name')?.textContent || 'صديق';
const name = card?.querySelector('.cf-card-name')?.textContent || t('common.friend');
const friend = friends.find(f => f.id === uid);
showChallengeOptions(el, uid, name, friend);
});
......@@ -107,7 +108,7 @@ async function loadFriendsForChallenge(el) {
});
} catch (e) {
body.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">فشل التحميل — <button id="cf-retry" style="color:#3b82f6;background:none;border:none;text-decoration:underline;cursor:pointer;font-family:inherit;">حاول مرة أخرى</button></div>`;
body.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">${t('common.error_load')} — <button id="cf-retry" style="color:#3b82f6;background:none;border:none;text-decoration:underline;cursor:pointer;font-family:inherit;">${t('social.retry_load')}</button></div>`;
body.querySelector('#cf-retry')?.addEventListener('click', () => loadFriendsForChallenge(el));
}
}
......@@ -120,10 +121,10 @@ function renderChallengeCard(f, isOnline) {
${isOnline ? '<div class="cf-card-online"></div>' : ''}
</div>
<div class="cf-card-info">
<div class="cf-card-name">${f.display_name || f.username || 'لاعب'}</div>
<div class="cf-card-sub">${isOnline ? 'متصل الآن' : 'غير متصل'}${f.level ? ` • مستوى ${f.level}` : ''}</div>
<div class="cf-card-name">${f.display_name || f.username || t('common.player')}</div>
<div class="cf-card-sub">${isOnline ? t('common.online') : t('common.offline')}${f.level ? ` • ${t('common.level', { n: f.level })}` : ''}</div>
</div>
<button class="cf-card-action" data-challenge="${f.id}">${emoji('challenge_swords', '⚔️', 12)} تحدّي</button>
<button class="cf-card-action" data-challenge="${f.id}">${emoji('challenge_swords', '⚔️', 12)} ${t('challenge.title')}</button>
</div>
`;
}
......@@ -139,27 +140,27 @@ function showChallengeOptions(el, targetId, targetName, friendProfile) {
<div style="background:#1a1a2e;border-radius:20px 20px 0 0;padding:24px 20px;width:100%;max-width:400px;animation:slideUp 0.25s ease;">
<div style="width:40px;height:4px;background:rgba(255,255,255,0.15);border-radius:2px;margin:0 auto 16px;"></div>
<div style="text-align:center;margin-bottom:16px;">
<div style="font-size:15px;font-weight:700;color:#f8fafc;">تحدّي ${targetName}</div>
<div style="font-size:12px;color:#64748b;margin-top:4px;">اختر اللعبة ونوع الوقت</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;">${t('challenge.title')} ${targetName}</div>
<div style="font-size:12px;color:#64748b;margin-top:4px;">${t('challenge.select_game')}</div>
</div>
<!-- Game selection -->
<div style="display:flex;gap:8px;margin-bottom:14px;">
<button class="cfo-game active" data-game="chess" style="flex:1;padding:12px;border-radius:12px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">♟ شطرنج</button>
<button class="cfo-game" data-game="ludo" style="flex:1;padding:12px;border-radius:12px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">🎲 لودو</button>
<button class="cfo-game" data-game="domino" style="flex:1;padding:12px;border-radius:12px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">🁣 دومينو</button>
<button class="cfo-game active" data-game="chess" style="flex:1;padding:12px;border-radius:12px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">♟ ${t('game.chess')}</button>
<button class="cfo-game" data-game="ludo" style="flex:1;padding:12px;border-radius:12px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">🎲 ${t('game.ludo')}</button>
<button class="cfo-game" data-game="domino" style="flex:1;padding:12px;border-radius:12px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:all 0.15s;">🁣 ${t('game.domino')}</button>
</div>
<!-- Time control -->
<div id="cfo-time" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:20px;">
<button class="cfo-tc" data-tc="bullet_1_0">⚡ 1 د</button>
<button class="cfo-tc active" data-tc="blitz_3_0">🔥 3 د</button>
<button class="cfo-tc" data-tc="blitz_5_0">💨 5 د</button>
<button class="cfo-tc" data-tc="rapid_10_0">🕐 10 د</button>
<button class="cfo-tc" data-tc="bullet_1_0">⚡ ${t('time.1min')}</button>
<button class="cfo-tc active" data-tc="blitz_3_0">🔥 ${t('time.3min')}</button>
<button class="cfo-tc" data-tc="blitz_5_0">💨 ${t('time.5min')}</button>
<button class="cfo-tc" data-tc="rapid_10_0">🕐 ${t('time.10min')}</button>
</div>
<button class="btn btn-primary" id="cfo-send" style="width:100%;min-height:50px;font-size:15px;font-weight:700;border-radius:14px;">⚔️ أرسل التحدي</button>
<button id="cfo-cancel" style="width:100%;margin-top:10px;background:none;border:none;color:#64748b;font-size:13px;cursor:pointer;font-family:inherit;padding:10px;">إلغاء</button>
<button class="btn btn-primary" id="cfo-send" style="width:100%;min-height:50px;font-size:15px;font-weight:700;border-radius:14px;">⚔️ ${t('challenge.send')}</button>
<button id="cfo-cancel" style="width:100%;margin-top:10px;background:none;border:none;color:#64748b;font-size:13px;cursor:pointer;font-family:inherit;padding:10px;">${t('common.cancel')}</button>
</div>
<style>
@keyframes slideUp { from{transform:translateY(100%)}to{transform:none} }
......@@ -194,7 +195,7 @@ function showChallengeOptions(el, targetId, targetName, friendProfile) {
dialog.querySelector('#cfo-send').addEventListener('click', async () => {
const sendBtn = dialog.querySelector('#cfo-send');
sendBtn.disabled = true;
sendBtn.textContent = '⏳ جاري الإرسال...';
sendBtn.textContent = t('challenge.sending');
try {
const res = await net.post('friends.php', {
......@@ -205,13 +206,13 @@ function showChallengeOptions(el, targetId, targetName, friendProfile) {
});
if (res.error) { sendBtn.textContent = res.error; sendBtn.disabled = false; return; }
if (!res.match_id) { sendBtn.textContent = 'فشل إنشاء المباراة'; sendBtn.disabled = false; return; }
if (!res.match_id) { sendBtn.textContent = t('challenge.failed_create'); sendBtn.disabled = false; return; }
// Also send a chat message
net.post('chat.php', {
action: 'send',
friend_id: targetId,
content: `أرسل تحدي ${selectedGame === 'ludo' ? 'لودو' : selectedGame === 'domino' ? 'دومينو' : 'شطرنج'}`,
content: t('challenge.sent_msg', { game: selectedGame === 'ludo' ? t('game.ludo') : selectedGame === 'domino' ? t('game.domino') : t('game.chess') }),
message_type: 'invite',
metadata: { game_key: selectedGame, time_control: selectedTc, match_id: res.match_id }
}).catch(() => {});
......@@ -231,7 +232,7 @@ function showChallengeOptions(el, targetId, targetName, friendProfile) {
isHost: true
});
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.textContent = t('challenge.failed_retry');
sendBtn.disabled = false;
}
});
......
......@@ -3,6 +3,7 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
let pollTimer = null;
......@@ -22,13 +23,13 @@ export function mountLobby(el, params = {}) {
if (pollTimer) clearInterval(pollTimer);
const myName = store.get('player.display_name') || store.get('player.username') || 'أنت';
const myName = store.get('player.display_name') || store.get('player.username') || t('common.you');
const myAvatar = store.get('player.avatar_url');
const friendName = friendProfile?.display_name || friendProfile?.username || 'الخصم';
const friendName = friendProfile?.display_name || friendProfile?.username || t('common.opponent');
const friendAvatar = friendProfile?.avatar_url;
const tcLabel = formatTimeControl(timeControl);
const gameLabel = gameKey === 'ludo' ? 'لودو' : gameKey === 'domino' ? 'دومينو' : 'شطرنج';
const gameLabel = gameKey === 'ludo' ? t('game.ludo') : gameKey === 'domino' ? t('game.domino') : t('game.chess');
const gameIcon = gameKey === 'ludo' ? '🎲' : gameKey === 'domino' ? '🁣' : '♟';
el.innerHTML = `
......@@ -36,7 +37,7 @@ export function mountLobby(el, params = {}) {
<!-- Header -->
<div class="lobby-header">
<button id="lobby-back" class="lobby-back-btn">←</button>
<div class="lobby-title">${emoji('challenge_swords', '⚔️', 18)} غرفة التحدي</div>
<div class="lobby-title">${emoji('challenge_swords', '⚔️', 18)} ${t('lobby.title')}</div>
</div>
<!-- Match Info -->
......@@ -56,7 +57,7 @@ export function mountLobby(el, params = {}) {
${myAvatar ? `<img src="${myAvatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 28)}
</div>
<div class="lobby-player-name">${myName}</div>
<div class="lobby-player-status ready">${emoji('check', '✓', 11)} جاهز</div>
<div class="lobby-player-status ready">${emoji('check', '✓', 11)} ${t('lobby.ready')}</div>
${color ? `<div class="lobby-color" style="background:${color === 'w' ? '#fff' : '#1a1a1a'};border:2px solid ${color === 'w' ? '#e2e8f0' : '#475569'};width:20px;height:20px;border-radius:50%;margin-top:6px;"></div>` : ''}
</div>
......@@ -70,21 +71,21 @@ export function mountLobby(el, params = {}) {
<div class="lobby-avatar opponent" id="lobby-opponent-avatar">
${friendAvatar ? `<img src="${friendAvatar}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : `<div class="lobby-waiting-pulse">${emoji('hourglass', '⏳', 28)}</div>`}
</div>
<div class="lobby-player-name" id="lobby-opponent-name">${isHost ? (friendName || 'في الانتظار...') : friendName}</div>
<div class="lobby-player-status" id="lobby-opponent-status">${isHost ? 'في انتظار القبول...' : `${emoji('check', '✓', 11)} جاهز`}</div>
<div class="lobby-player-name" id="lobby-opponent-name">${isHost ? (friendName || t('lobby.waiting')) : friendName}</div>
<div class="lobby-player-status" id="lobby-opponent-status">${isHost ? t('lobby.waiting_accept') : `${emoji('check', '✓', 11)} ${t('lobby.ready')}`}</div>
${color ? `<div class="lobby-color" style="background:${color === 'w' ? '#1a1a1a' : '#fff'};border:2px solid ${color === 'w' ? '#475569' : '#e2e8f0'};width:20px;height:20px;border-radius:50%;margin-top:6px;"></div>` : ''}
</div>
</div>
<!-- Status -->
<div class="lobby-status" id="lobby-status">
${isHost ? `<div class="lobby-status-text">${emoji('hourglass', '⏳', 14)} في انتظار الخصم...</div><div class="lobby-status-sub">سيتم بدء المباراة تلقائياً عند قبول التحدي</div>` : `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} جاهز للبدء!</div>`}
${isHost ? `<div class="lobby-status-text">${emoji('hourglass', '⏳', 14)} ${t('lobby.waiting_opponent')}</div><div class="lobby-status-sub">${t('lobby.auto_start')}</div>` : `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} ${t('lobby.ready_start')}</div>`}
</div>
<!-- Actions -->
<div class="lobby-actions">
${!isHost ? `<button class="btn btn-primary lobby-btn" id="lobby-start" style="background:#34D399;">${emoji('play', '▶', 14)} ابدأ المباراة</button>` : ''}
<button class="btn btn-secondary lobby-btn" id="lobby-cancel">${emoji('exit', '✕', 12)} إلغاء</button>
${!isHost ? `<button class="btn btn-primary lobby-btn" id="lobby-start" style="background:#34D399;">${emoji('play', '▶', 14)} ${t('lobby.start')}</button>` : ''}
<button class="btn btn-secondary lobby-btn" id="lobby-cancel">${emoji('exit', '✕', 12)} ${t('common.cancel')}</button>
</div>
</div>
<style>
......@@ -131,7 +132,7 @@ export function mountLobby(el, params = {}) {
// Guest: wait briefly so host can detect acceptance, then start
const statusEl = el.querySelector('#lobby-status');
if (statusEl) {
statusEl.innerHTML = `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} تم القبول! جاري تجهيز المباراة...</div>`;
statusEl.innerHTML = `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} ${t('lobby.accepted_preparing')}</div>`;
}
setTimeout(() => startGame(el, params), 2500);
}
......@@ -155,10 +156,10 @@ async function pollMatchStatus(el, params) {
const statusEl = el.querySelector('#lobby-status');
const oppStatus = el.querySelector('#lobby-opponent-status');
if (statusEl) {
statusEl.innerHTML = `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} الخصم قبل! جاري البدء...</div>`;
statusEl.innerHTML = `<div class="lobby-status-text" style="color:#34D399;">${emoji('check', '✓', 14)} ${t('lobby.opponent_accepted')}</div>`;
}
if (oppStatus) {
oppStatus.innerHTML = `<span style="color:#34D399;">${emoji('check', '✓', 11)} جاهز</span>`;
oppStatus.innerHTML = `<span style="color:#34D399;">${emoji('check', '✓', 11)} ${t('lobby.ready')}</span>`;
oppStatus.classList.add('ready');
}
......@@ -213,12 +214,12 @@ async function cancelAndLeave(el) {
}
function formatTimeControl(tc) {
if (tc.includes('bullet_1')) return '1 دقيقة';
if (tc.includes('blitz_3')) return '3 دقائق';
if (tc.includes('blitz_5')) return '5 دقائق';
if (tc.includes('rapid_10')) return '10 دقائق';
if (tc.includes('rapid_15')) return '15 دقيقة';
if (tc.includes('classical')) return '30 دقيقة';
if (tc.includes('bullet_1')) return t('time.1min');
if (tc.includes('blitz_3')) return t('time.3min');
if (tc.includes('blitz_5')) return t('time.5min');
if (tc.includes('rapid_10')) return t('time.10min');
if (tc.includes('rapid_15')) return t('time.15min');
if (tc.includes('classical')) return t('time.30min');
return tc;
}
......
......@@ -17,10 +17,10 @@ function getGames() {
const bgSec = getColor('backgammon_secondary', '#EF4444');
return [
{ key: 'chess', name: 'شطرنج', nameEn: 'Chess', color: chessPri, secondary: chessSec, icon: '♟', gradient: `linear-gradient(135deg, ${chessPri}, ${chessSec})` },
{ key: 'domino', name: 'دومينو', nameEn: 'Domino', color: dominoPri, secondary: dominoSec, icon: '🁣', gradient: `linear-gradient(135deg, ${dominoPri}, ${dominoSec})` },
{ key: 'ludo', name: 'لودو', nameEn: 'Ludo', color: ludoPri, secondary: ludoSec, icon: '🎲', gradient: `linear-gradient(135deg, ${ludoPri}, ${ludoSec})` },
{ key: 'backgammon', name: 'طاولة', nameEn: 'Backgammon', color: bgPri, secondary: bgSec, icon: '◎', gradient: `linear-gradient(135deg, ${bgPri}, ${bgSec})` }
{ key: 'chess', name: t('game.chess'), nameEn: 'Chess', color: chessPri, secondary: chessSec, icon: '♟', gradient: `linear-gradient(135deg, ${chessPri}, ${chessSec})` },
{ key: 'domino', name: t('game.domino'), nameEn: 'Domino', color: dominoPri, secondary: dominoSec, icon: '🁣', gradient: `linear-gradient(135deg, ${dominoPri}, ${dominoSec})` },
{ key: 'ludo', name: t('game.ludo'), nameEn: 'Ludo', color: ludoPri, secondary: ludoSec, icon: '🎲', gradient: `linear-gradient(135deg, ${ludoPri}, ${ludoSec})` },
{ key: 'backgammon', name: t('game.backgammon'), nameEn: 'Backgammon', color: bgPri, secondary: bgSec, icon: '◎', gradient: `linear-gradient(135deg, ${bgPri}, ${bgSec})` }
];
}
......@@ -32,24 +32,24 @@ export function mountTable(el) {
el.innerHTML = `
<div class="play-home">
<!-- Player greeting -->
<div style="width:100%;max-width:340px;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:16px;font-weight:700;color:#f8fafc;">أهلاً ${username} 👋</div>
<div style="font-size:11px;color:#64748b;">المستوى ${player?.level || 1}</div>
<div style="width:100%;max-width:var(--home-max-width);margin-bottom:var(--home-greeting-margin);display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:var(--home-greeting-font);font-weight:700;color:var(--text-primary);">${username} 👋</div>
<div style="font-size:var(--gm-btn-sub-font);color:var(--text-muted);">Lv. ${player?.level || 1}</div>
</div>
<!-- Quick actions row -->
<div id="daily-widget" style="display:flex;gap:8px;width:100%;max-width:340px;margin-bottom:16px;">
<div id="daily-widget" style="display:flex;gap:var(--quick-row-gap);width:100%;max-width:var(--home-max-width);margin-bottom:var(--quick-row-margin);">
<button class="quick-btn" id="btn-challenge-friend">
<span class="qb-icon" style="background:linear-gradient(135deg,#7c3aed,#a855f7);">${emoji('challenge_swords', '⚔️', 20)}</span>
<span class="qb-label">تحدّي صديق</span>
<span class="qb-label">${t('challenge.title')}</span>
</button>
<button class="quick-btn" id="btn-achievements">
<span class="qb-icon" style="background:linear-gradient(135deg,#854d0e,#ca8a04);">${emoji('trophy', '🏆', 20)}</span>
<span class="qb-label">إنجازات</span>
<span class="qb-label">${t('play.achievements')}</span>
</button>
<button class="quick-btn breathe-glow" id="btn-daily-reward">
<span class="qb-icon" style="background:linear-gradient(135deg,#92400e,#e4ac38);">${emoji('gift', '🎁', 20)}</span>
<span class="qb-label">هدية</span>
<span class="qb-label">${t('play.gift')}</span>
</button>
</div>
......@@ -63,7 +63,7 @@ export function mountTable(el) {
<div class="game-tile-content">
<div class="game-tile-icon">${assetImg(g.key + '_icon', g.icon, 48, 48)}</div>
<div class="game-tile-name">${g.name}</div>
${disabled ? '<div class="game-tile-soon">قريباً</div>' : ''}
${disabled ? `<div class="game-tile-soon">${t('play.coming_soon')}</div>` : ''}
</div>
</div>`;
}).join('')}
......@@ -77,32 +77,32 @@ export function mountTable(el) {
align-items: center;
justify-content: flex-start;
height: 100%;
padding: 16px 16px 0;
padding: var(--home-padding) var(--home-padding) 0;
overflow-y: auto;
}
.quick-btn { display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;background:none;border:none;cursor:pointer;padding:6px;transition:transform 0.1s; }
.quick-btn { display:flex;flex-direction:column;align-items:center;gap:var(--quick-btn-gap);flex:1;background:none;border:none;cursor:pointer;padding:var(--quick-btn-padding);transition:transform 0.1s; }
.quick-btn:active { transform:scale(0.9); }
.qb-icon { width:42px;height:42px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:20px;box-shadow:0 3px 10px rgba(0,0,0,0.3); }
.qb-label { font-size:10px;font-weight:600;color:#94a3b8; }
.qb-icon { width:var(--quick-btn-icon-size);height:var(--quick-btn-icon-size);border-radius:var(--quick-btn-icon-radius);display:flex;align-items:center;justify-content:center;font-size:var(--quick-btn-icon-font);box-shadow:0 3px 10px rgba(0,0,0,0.3); }
.qb-label { font-size:var(--quick-btn-label-font);font-weight:600;color:var(--text-secondary); }
.games-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
gap: var(--game-tile-gap);
width: 100%;
max-width: 340px;
max-width: var(--home-max-width);
}
.game-tile {
position: relative;
aspect-ratio: 1.1;
border-radius: 18px;
aspect-ratio: var(--game-tile-aspect);
border-radius: var(--game-tile-radius);
overflow: hidden;
cursor: pointer;
transition: transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.15s;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
box-shadow: var(--shadow-md);
}
.game-tile:active {
transform: scale(0.93);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
box-shadow: var(--shadow-sm);
}
.game-tile-disabled {
opacity: 0.4;
......@@ -111,12 +111,12 @@ export function mountTable(el) {
}
.game-tile-disabled:active { transform: none; }
.game-tile-soon {
font-size: 11px;
font-size: var(--gm-btn-sub-font);
font-weight: 700;
color: rgba(255,255,255,0.7);
background: rgba(0,0,0,0.3);
padding: 2px 10px;
border-radius: 8px;
border-radius: var(--r-sm);
margin-top: 2px;
}
.game-tile-bg {
......@@ -132,14 +132,15 @@ export function mountTable(el) {
align-items: center;
justify-content: center;
height: 100%;
gap: 8px;
gap: var(--game-tile-content-gap);
}
.game-tile-icon {
font-size: 42px;
font-size: var(--game-tile-icon-font);
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
}
.game-tile-icon img { width: var(--game-tile-icon-size); height: var(--game-tile-icon-size); object-fit: contain; }
.game-tile-name {
font-size: 16px;
font-size: var(--game-tile-name-font);
font-weight: 800;
color: white;
text-shadow: 0 1px 3px rgba(0,0,0,0.4);
......@@ -149,16 +150,16 @@ export function mountTable(el) {
bottom: 0;
left: 0;
right: 0;
background: #0f0f1e;
border-top-left-radius: 28px;
border-top-right-radius: 28px;
padding: 28px 20px;
padding-bottom: calc(28px + var(--tab-height, 60px) + var(--safe-bottom, 0px));
background: var(--sheet-bg);
border-top-left-radius: var(--sheet-radius);
border-top-right-radius: var(--sheet-radius);
padding: var(--sheet-padding-top) var(--sheet-padding-x);
padding-bottom: calc(var(--sheet-padding-top) + var(--tab-height, 60px) + var(--safe-bottom, 0px));
z-index: 50;
transform: translateY(0);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 -8px 40px rgba(0,0,0,0.6);
max-height: 75vh;
max-height: var(--sheet-max-height);
}
.game-menu.hidden {
transform: translateY(100%);
......@@ -168,78 +169,57 @@ export function mountTable(el) {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
margin-bottom: var(--menu-header-margin);
}
/* Game menu buttons */
.gm-btn {
display: flex;
align-items: center;
gap: 14px;
gap: var(--gm-btn-gap);
width: 100%;
padding: 18px 16px;
background: #1a1a2e;
border: 1.5px solid rgba(255,255,255,0.06);
border-radius: 16px;
padding: var(--gm-btn-padding-y) var(--gm-btn-padding-x);
background: var(--bg-card);
border: 1.5px solid var(--border);
border-radius: var(--gm-btn-radius);
cursor: pointer;
margin-bottom: 10px;
margin-bottom: var(--gm-btn-margin-bottom);
text-align: right;
transition: transform 0.1s, border-color 0.2s;
font-family: inherit;
}
.gm-btn:active { transform: scale(0.96); border-color: rgba(255,255,255,0.2); }
.gm-btn-icon {
width: 52px;
height: 52px;
border-radius: 14px;
width: var(--gm-btn-icon-size);
height: var(--gm-btn-icon-size);
border-radius: var(--gm-btn-icon-radius);
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
font-size: var(--gm-btn-icon-font);
flex-shrink: 0;
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
}
.gm-btn-body { flex: 1; }
.gm-btn-title { font-size: 17px; font-weight: 800; color: #f8fafc; }
.gm-btn-sub { font-size: 11px; color: #64748b; margin-top: 2px; }
.gm-btn-arrow { font-size: 22px; color: #475569; font-weight: 300; }
.gm-btn-title { font-size: var(--gm-btn-title-font); font-weight: 800; color: var(--text-primary); }
.gm-btn-sub { font-size: var(--gm-btn-sub-font); color: var(--text-muted); margin-top: 2px; }
.gm-btn-arrow { font-size: var(--gm-btn-arrow-font); color: var(--text-muted); font-weight: 300; }
.gm-chip {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 14px 8px;
background: #1a1a2e;
border: 1px solid rgba(255,255,255,0.06);
border-radius: 12px;
gap: var(--gm-chip-gap);
padding: var(--gm-chip-padding-y) var(--gm-chip-padding-x);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--gm-chip-radius);
cursor: pointer;
font-family: inherit;
color: #e2e8f0;
font-size: 11px;
color: var(--text-primary);
font-size: var(--gm-chip-font);
font-weight: 600;
transition: transform 0.1s, background 0.15s;
}
.gm-chip:active { transform: scale(0.93); background: rgba(255,255,255,0.08); }
.menu-features {
display: flex;
gap: 10px;
margin-top: 12px;
flex-wrap: wrap;
}
.feature-chip {
padding: 10px 16px;
border-radius: 10px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.08);
color: #e2e8f0;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, transform 0.1s;
min-height: 40px;
display: flex;
align-items: center;
}
.feature-chip:active { background: rgba(255,255,255,0.1); }
</style>
`;
......@@ -290,20 +270,20 @@ function showGameMenu(menu, game) {
menu.classList.remove('hidden');
menu.style.animation = 'slideUpBounce 0.4s cubic-bezier(0.16, 1, 0.3, 1)';
menu.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:42px;height:42px;border-radius:12px;background:${game.gradient};display:flex;align-items:center;justify-content:center;font-size:22px;">${game.icon}</div>
<div style="font-size:22px;font-weight:800;color:#f8fafc;">${game.name}</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--menu-header-margin);">
<div style="display:flex;align-items:center;gap:var(--menu-header-gap);">
<div style="width:var(--menu-header-icon-size);height:var(--menu-header-icon-size);border-radius:var(--menu-header-icon-radius);background:${game.gradient};display:flex;align-items:center;justify-content:center;font-size:var(--menu-header-icon-font);">${game.icon}</div>
<div style="font-size:var(--menu-header-title-font);font-weight:800;color:var(--text-primary);">${game.name}</div>
</div>
<button id="menu-close" style="width:36px;height:36px;border-radius:50%;background:rgba(255,255,255,0.06);border:none;color:#94a3b8;font-size:20px;cursor:pointer;display:flex;align-items:center;justify-content:center;">✕</button>
<button id="menu-close" style="width:var(--menu-close-size);height:var(--menu-close-size);border-radius:50%;background:var(--border);border:none;color:var(--text-secondary);font-size:var(--menu-close-font);cursor:pointer;display:flex;align-items:center;justify-content:center;">✕</button>
</div>
<!-- Main play buttons -->
<button class="gm-btn" id="btn-single">
<div class="gm-btn-icon" style="background:${game.gradient};">${emoji('gamepad', '🎮', 26)}</div>
<div class="gm-btn-body">
<div class="gm-btn-title">لاعب واحد</div>
<div class="gm-btn-sub">العب ضد الكمبيوتر</div>
<div class="gm-btn-title">${t('play.single_player')}</div>
<div class="gm-btn-sub">${t('play.single_player_desc')}</div>
</div>
<div class="gm-btn-arrow">›</div>
</button>
......@@ -311,25 +291,25 @@ function showGameMenu(menu, game) {
<button class="gm-btn" id="btn-multi">
<div class="gm-btn-icon" style="background:linear-gradient(135deg,#dc2626,#f97316);">${emoji('swords', '⚔️', 26)}</div>
<div class="gm-btn-body">
<div class="gm-btn-title">أونلاين</div>
<div class="gm-btn-sub">نافس لاعبين حقيقيين</div>
<div class="gm-btn-title">${t('game.online')}</div>
<div class="gm-btn-sub">${t('play.online_desc')}</div>
</div>
<div class="gm-btn-arrow">›</div>
</button>
<!-- Secondary actions -->
<div style="display:grid;grid-template-columns:${game.key === 'chess' ? '1fr 1fr 1fr' : '1fr 1fr'};gap:8px;margin-top:14px;">
<div style="display:grid;grid-template-columns:${game.key === 'chess' ? '1fr 1fr 1fr' : '1fr 1fr'};gap:var(--gm-chips-grid-gap);margin-top:var(--gm-chips-margin-top);">
<button class="gm-chip" id="btn-leaderboard">
<span style="font-size:18px;">${emoji('tournament_cup', '🏆', 18)}</span>
<span>الترتيب</span>
<span style="font-size:var(--gm-chip-icon-font);">${emoji('tournament_cup', '🏆', 18)}</span>
<span>Leaderboard</span>
</button>
<button class="gm-chip" id="btn-history">
<span style="font-size:18px;">${emoji('clipboard', '📋', 18)}</span>
<span>مبارياتي</span>
<span style="font-size:var(--gm-chip-icon-font);">${emoji('clipboard', '📋', 18)}</span>
<span>My Games</span>
</button>
${game.key === 'chess' ? `<button class="gm-chip" id="btn-puzzles">
<span style="font-size:18px;">${emoji('puzzle', '🧩', 18)}</span>
<span>أحجيات</span>
<span style="font-size:var(--gm-chip-icon-font);">${emoji('puzzle', '🧩', 18)}</span>
<span>Puzzles</span>
</button>` : ''}
</div>
`;
......@@ -402,7 +382,7 @@ function showGameMenu(menu, game) {
menu.querySelector('#btn-history')?.addEventListener('click', () => {
audio.play('click');
menu.classList.add('hidden');
scene.push('chess-history');
scene.push('chess-history', { game: game.key });
});
menu.querySelector('#btn-puzzles')?.addEventListener('click', () => {
......
......@@ -5,38 +5,38 @@ import { t } from '../../../core/i18n.js';
const categories = [
{
name: 'Bullet', nameAr: 'رصاصة', icon: '⚡', color: '#FBBF24',
name: 'Bullet', nameKey: 'time.bullet', icon: '⚡', color: '#FBBF24',
controls: [
{ key: 'bullet_1_0', label: '1 دقيقة', sub: '1+0' },
{ key: 'bullet_1_0', labelKey: 'time.1min', sub: '1+0' },
{ key: 'bullet_1_1', label: '1 | 1', sub: '1+1' },
{ key: 'bullet_2_1', label: '2 | 1', sub: '2+1' },
]
},
{
name: 'Blitz', nameAr: 'خاطفة', icon: '🔥', color: '#F97316',
name: 'Blitz', nameKey: 'time.blitz', icon: '🔥', color: '#F97316',
controls: [
{ key: 'blitz_3_0', label: '3 دقائق', sub: '3+0' },
{ key: 'blitz_3_0', labelKey: 'time.3min', sub: '3+0' },
{ key: 'blitz_3_2', label: '3 | 2', sub: '3+2' },
{ key: 'blitz_5_0', label: '5 دقائق', sub: '5+0' },
{ key: 'blitz_5_0', labelKey: 'time.5min', sub: '5+0' },
{ key: 'blitz_5_3', label: '5 | 3', sub: '5+3' },
{ key: 'blitz_5_5', label: '5 | 5', sub: '5+5' },
]
},
{
name: 'Rapid', nameAr: 'سريعة', icon: '🏃', color: '#22C55E',
name: 'Rapid', nameKey: 'time.rapid', icon: '🏃', color: '#22C55E',
controls: [
{ key: 'rapid_10_0', label: '10 دقائق', sub: '10+0' },
{ key: 'rapid_10_0', labelKey: 'time.10min', sub: '10+0' },
{ key: 'rapid_10_5', label: '10 | 5', sub: '10+5' },
{ key: 'rapid_15_10', label: '15 | 10', sub: '15+10' },
{ key: 'rapid_20_0', label: '20 دقيقة', sub: '20+0' },
{ key: 'rapid_30_0', label: '30 دقيقة', sub: '30+0' },
{ key: 'rapid_20_0', labelKey: 'time.20min', sub: '20+0' },
{ key: 'rapid_30_0', labelKey: 'time.30min', sub: '30+0' },
]
},
{
name: 'Classical', nameAr: 'كلاسيكية', icon: '♔', color: '#8B5CF6',
name: 'Classical', nameKey: 'time.classical', icon: '♔', color: '#8B5CF6',
controls: [
{ key: 'classical_45_45', label: '45 | 45', sub: '45+45' },
{ key: 'classical_60_0', label: '60 دقيقة', sub: '60+0' },
{ key: 'classical_60_0', labelKey: 'time.60min', sub: '60+0' },
{ key: 'classical_90_30', label: '90 | 30', sub: '90+30' },
]
}
......@@ -53,14 +53,14 @@ export function mountTimeSelect(el, params) {
<div class="tc-category">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<span style="font-size:18px;">${cat.icon}</span>
<span style="font-size:14px;font-weight:700;color:${cat.color};">${cat.nameAr}</span>
<span style="font-size:14px;font-weight:700;color:${cat.color};">${t(cat.nameKey)}</span>
<span style="font-size:11px;color:#64748b;font-family:var(--font-lat);">${cat.name}</span>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;">
${cat.controls.map(tc => `
<button class="time-btn" data-key="${tc.key}" style="display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;height:58px;background:#1e1e3a;border:1px solid rgba(255,255,255,0.08);border-radius:10px;cursor:pointer;transition:transform 0.1s,background 0.15s,border-color 0.15s;">
<span style="font-size:14px;font-weight:700;color:#f8fafc;font-family:var(--font-lat);">${tc.sub}</span>
<span style="font-size:10px;color:#94a3b8;">${tc.label}</span>
<span style="font-size:10px;color:#94a3b8;">${tc.labelKey ? t(tc.labelKey) : tc.label}</span>
</button>
`).join('')}
</div>
......
......@@ -3,6 +3,7 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import * as net from '../../../core/net.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
const COUNTRIES = [
......@@ -50,31 +51,31 @@ export async function mountEdit(el) {
<button id="btn-back" class="btn btn-secondary" style="min-width:40px;min-height:40px;padding:0;display:flex;align-items:center;justify-content:center;">
${emoji('arrow_back', '←', 18)}
</button>
<div style="font-size:18px;font-weight:700;">تعديل الملف الشخصي</div>
<div style="font-size:18px;font-weight:700;">${t('profile.edit_title')}</div>
</div>
<!-- Section: Basic Info -->
<div class="card">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('person', '👤', 18)} المعلومات الأساسية</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('person', '👤', 18)} ${t('profile.basic_info')}</div>
<div style="display:flex;flex-direction:column;gap:var(--s-3);">
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">الاسم المعروض (إنجليزي)</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.display_name_en')}</label>
<input id="field-display_name" type="text" maxlength="30" dir="auto" value="${escAttr(player.display_name || '')}"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">الاسم المعروض (عربي)</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.display_name_ar')}</label>
<input id="field-display_name_ar" type="text" maxlength="30" dir="rtl" value="${escAttr(player.display_name_ar || '')}"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">النبذة (إنجليزي)</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.bio_en')}</label>
<textarea id="field-bio" maxlength="200" dir="auto" rows="3"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;resize:vertical;">${escHtml(player.bio || '')}</textarea>
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">النبذة (عربي)</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.bio_ar')}</label>
<textarea id="field-bio_ar" maxlength="200" dir="rtl" rows="3"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;resize:vertical;">${escHtml(player.bio_ar || '')}</textarea>
</div>
......@@ -83,24 +84,24 @@ export async function mountEdit(el) {
<!-- Section: Location -->
<div class="card">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('globe', '🌍', 18)} الموقع</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('globe', '🌍', 18)} ${t('profile.location')}</div>
<div style="display:flex;flex-direction:column;gap:var(--s-3);">
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">الدولة</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.country')}</label>
<select id="field-country_code"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
<option value="">اختر الدولة</option>
<option value="">${t('profile.select_country')}</option>
${COUNTRIES.map(c => `<option value="${c.code}" ${player.country_code === c.code ? 'selected' : ''}>${c.name}</option>`).join('')}
</select>
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">المدينة</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.city')}</label>
<input id="field-city" type="text" dir="auto" value="${escAttr(player.city || '')}"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">اللغة المفضلة</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.preferred_language')}</label>
<div id="lang-toggle" style="display:flex;gap:var(--s-2);">
<button class="btn lang-opt ${(player.preferred_language || 'ar') === 'ar' ? 'btn-primary' : 'btn-secondary'}" data-lang="ar" style="flex:1;min-height:40px;">العربية</button>
<button class="btn lang-opt ${player.preferred_language === 'en' ? 'btn-primary' : 'btn-secondary'}" data-lang="en" style="flex:1;min-height:40px;">English</button>
......@@ -111,7 +112,7 @@ export async function mountEdit(el) {
<!-- Section: FIDE Info -->
<div class="card">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('trophy', '🏆', 18)} معلومات FIDE</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('trophy', '🏆', 18)} ${t('profile.fide_info')}</div>
<div style="display:flex;flex-direction:column;gap:var(--s-3);">
<div>
......@@ -120,25 +121,25 @@ export async function mountEdit(el) {
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">تصنيف FIDE الكلاسيكي</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.fide_classical')}</label>
<input id="field-fide_rating_standard" type="number" inputmode="numeric" dir="ltr" min="0" max="4000" value="${player.fide_rating_standard || ''}"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">تصنيف FIDE السريع</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.fide_rapid')}</label>
<input id="field-fide_rating_rapid" type="number" inputmode="numeric" dir="ltr" min="0" max="4000" value="${player.fide_rating_rapid || ''}"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">تصنيف FIDE الخاطف</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.fide_blitz')}</label>
<input id="field-fide_rating_blitz" type="number" inputmode="numeric" dir="ltr" min="0" max="4000" value="${player.fide_rating_blitz || ''}"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">لقب FIDE</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('profile.fide_title')}</label>
<select id="field-fide_title"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
${FIDE_TITLES.map(t => `<option value="${t.value}" ${player.fide_title === t.value ? 'selected' : ''}>${t.label}</option>`).join('')}
${FIDE_TITLES.map(ft => `<option value="${ft.value}" ${player.fide_title === ft.value ? 'selected' : ''}>${ft.label}</option>`).join('')}
</select>
</div>
</div>
......@@ -146,7 +147,7 @@ export async function mountEdit(el) {
<!-- Save Button -->
<button id="btn-save" class="btn btn-primary" style="width:100%;min-height:48px;font-size:16px;font-weight:700;">
${emoji('save', '💾', 18)} حفظ التغييرات
${emoji('save', '💾', 18)} ${t('profile.save')}
</button>
</div>
`;
......@@ -178,7 +179,7 @@ export async function mountEdit(el) {
const btn = el.querySelector('#btn-save');
btn.disabled = true;
btn.style.opacity = '0.6';
btn.textContent = 'جاري الحفظ...';
btn.textContent = t('profile.saving');
const data = {
display_name: el.querySelector('#field-display_name').value.trim(),
......@@ -206,15 +207,15 @@ export async function mountEdit(el) {
const updated = { ...store.get('player'), ...data };
store.set('player', updated);
bus.emit('store:player');
bus.emit('toast', { text: 'تم حفظ التغييرات بنجاح', type: 'success' });
bus.emit('toast', { text: t('profile.save_success'), type: 'success' });
scene.pop();
} catch (err) {
bus.emit('toast', { text: err.message || 'فشل حفظ التغييرات', type: 'error' });
bus.emit('toast', { text: err.message || t('profile.save_failed'), type: 'error' });
}
btn.disabled = false;
btn.style.opacity = '1';
btn.innerHTML = `${emoji('save', '💾', 18)} حفظ التغييرات`;
btn.innerHTML = `${emoji('save', '💾', 18)} ${t('profile.save')}`;
});
}
......
......@@ -3,13 +3,14 @@ import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import * as net from '../../../core/net.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
const DOC_TYPES = [
{ value: 'membership_card', label: 'بطاقة عضوية' },
{ value: 'id_card', label: 'بطاقة هوية' },
{ value: 'receipt', label: 'إيصال دفع' },
{ value: 'other', label: 'أخرى' }
{ value: 'membership_card', get label() { return t('org.doc_membership'); } },
{ value: 'id_card', get label() { return t('org.doc_id'); } },
{ value: 'receipt', get label() { return t('org.doc_receipt'); } },
{ value: 'other', get label() { return t('org.doc_other'); } }
];
export async function mountOrgApply(el) {
......@@ -19,10 +20,10 @@ export async function mountOrgApply(el) {
<button id="btn-back" class="btn btn-secondary" style="min-width:40px;min-height:40px;padding:0;display:flex;align-items:center;justify-content:center;">
${emoji('arrow_back', '←', 18)}
</button>
<div style="font-size:18px;font-weight:700;">الانضمام لمنظمة</div>
<div style="font-size:18px;font-weight:700;">${t('org.join_title')}</div>
</div>
<div id="org-content" style="display:flex;flex-direction:column;gap:var(--s-3);">
<div style="text-align:center;padding:var(--s-6);color:var(--text-secondary);">جاري التحميل...</div>
<div style="text-align:center;padding:var(--s-6);color:var(--text-secondary);">${t('common.loading')}</div>
</div>
</div>
`;
......@@ -46,7 +47,7 @@ async function loadOrgs(el) {
]);
if (!orgs || !Array.isArray(orgs) || orgs.length === 0) {
content.innerHTML = `<div class="card" style="text-align:center;padding:var(--s-6);color:var(--text-secondary);">لا توجد منظمات متاحة حالياً</div>`;
content.innerHTML = `<div class="card" style="text-align:center;padding:var(--s-6);color:var(--text-secondary);">${t('org.no_orgs')}</div>`;
return;
}
......@@ -70,17 +71,17 @@ async function loadOrgs(el) {
let actionBtn = '';
if (isMember) {
badge = `<span style="background:var(--success);color:#fff;font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600;">${emoji('check', '✓', 12)} عضو</span>`;
badge = `<span style="background:var(--success);color:#fff;font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600;">${emoji('check', '✓', 12)} ${t('profile.member')}</span>`;
} else if (app && app.status === 'pending') {
badge = `<span style="background:#f59e0b;color:#000;font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600;">قيد المراجعة</span>`;
badge = `<span style="background:#f59e0b;color:#000;font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600;">${t('profile.pending_review')}</span>`;
} else if (app && app.status === 'rejected') {
badge = `<span style="background:var(--error);color:#fff;font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600;">مرفوض</span>`;
badge = `<span style="background:var(--error);color:#fff;font-size:11px;padding:2px 8px;border-radius:12px;font-weight:600;">${t('profile.rejected')}</span>`;
if (app.rejection_reason) {
badge += `<div style="font-size:11px;color:var(--error);margin-top:4px;">السبب: ${escHtml(app.rejection_reason)}</div>`;
badge += `<div style="font-size:11px;color:var(--error);margin-top:4px;">${t('profile.rejection_reason', { reason: escHtml(app.rejection_reason) })}</div>`;
}
actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:36px;font-size:13px;margin-top:var(--s-2);">إعادة التقديم</button>`;
actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:36px;font-size:13px;margin-top:var(--s-2);">${t('org.reapply')}</button>`;
} else {
actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:36px;font-size:13px;margin-top:var(--s-2);">تقديم طلب</button>`;
actionBtn = `<button class="btn btn-primary btn-apply" data-org-id="${org.id}" style="min-height:36px;font-size:13px;margin-top:var(--s-2);">${t('org.apply')}</button>`;
}
const logoHtml = org.logo_url
......@@ -113,7 +114,7 @@ async function loadOrgs(el) {
});
} catch (err) {
content.innerHTML = `<div class="card" style="text-align:center;padding:var(--s-4);color:var(--error);">فشل تحميل المنظمات: ${escHtml(err.message)}</div>`;
content.innerHTML = `<div class="card" style="text-align:center;padding:var(--s-4);color:var(--error);">${t('common.error_load')}</div>`;
}
}
......@@ -122,11 +123,11 @@ function showApplyForm(el, org, contentParent) {
content.innerHTML = `
<div class="card" style="padding:var(--s-4);">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">تقديم طلب انضمام — ${escHtml(org.name)}</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${t('org.apply_title', { name: escHtml(org.name) })}</div>
<div style="display:flex;flex-direction:column;gap:var(--s-3);">
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">نوع المستند</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('org.doc_type')}</label>
<select id="apply-doc-type"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;">
${DOC_TYPES.map(d => `<option value="${d.value}">${d.label}</option>`).join('')}
......@@ -134,19 +135,19 @@ function showApplyForm(el, org, contentParent) {
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">ملاحظات (اختياري)</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('org.notes')}</label>
<textarea id="apply-notes" maxlength="500" dir="auto" rows="3"
style="background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;border-radius:8px;padding:10px 12px;font-size:14px;font-family:inherit;width:100%;box-sizing:border-box;resize:vertical;"
placeholder="رسالة لإدارة المنظمة..."></textarea>
placeholder="${t('org.notes_placeholder')}"></textarea>
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">صورة إثبات</label>
<label style="font-size:13px;color:var(--text-secondary);margin-bottom:4px;display:block;">${t('org.proof_image')}</label>
<div id="upload-area" style="background:#1e1e3a;border:2px dashed rgba(255,255,255,0.15);border-radius:8px;padding:var(--s-4);text-align:center;cursor:pointer;">
<div id="upload-placeholder">
<div>${emoji('camera', '📷', 28)}</div>
<div style="font-size:13px;color:var(--text-secondary);margin-top:var(--s-2);">اضغط لاختيار صورة</div>
<div style="font-size:11px;color:var(--text-secondary);">PNG, JPG, WebP — حد أقصى 5MB</div>
<div style="font-size:13px;color:var(--text-secondary);margin-top:var(--s-2);">${t('org.tap_to_select')}</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('org.file_limit')}</div>
</div>
<div id="upload-preview" style="display:none;">
<img id="preview-img" style="max-width:100%;max-height:200px;border-radius:6px;object-fit:contain;">
......@@ -156,8 +157,8 @@ function showApplyForm(el, org, contentParent) {
</div>
<div style="display:flex;gap:var(--s-3);margin-top:var(--s-2);">
<button id="btn-cancel-apply" class="btn btn-secondary" style="flex:1;min-height:44px;">إلغاء</button>
<button id="btn-submit-apply" class="btn btn-primary" style="flex:1;min-height:44px;font-weight:700;">إرسال الطلب</button>
<button id="btn-cancel-apply" class="btn btn-secondary" style="flex:1;min-height:44px;">${t('common.cancel')}</button>
<button id="btn-submit-apply" class="btn btn-primary" style="flex:1;min-height:44px;font-weight:700;">${t('org.submit')}</button>
</div>
</div>
</div>
......@@ -177,7 +178,7 @@ function showApplyForm(el, org, contentParent) {
const file = e.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) {
bus.emit('toast', { text: 'حجم الملف أكبر من 5MB', type: 'error' });
bus.emit('toast', { text: t('org.file_too_large'), type: 'error' });
return;
}
selectedFile = file;
......@@ -206,7 +207,7 @@ function showApplyForm(el, org, contentParent) {
btn.disabled = true;
btn.style.opacity = '0.6';
btn.textContent = 'جاري الإرسال...';
btn.textContent = t('org.submitting');
try {
const formData = new FormData();
......@@ -228,16 +229,16 @@ function showApplyForm(el, org, contentParent) {
const data = await res.json();
if (!res.ok || data.error) {
throw new Error(data.error || 'فشل إرسال الطلب');
throw new Error(data.error || t('org.submit_failed'));
}
bus.emit('toast', { text: 'تم إرسال الطلب بنجاح', type: 'success' });
bus.emit('toast', { text: t('org.submit_success'), type: 'success' });
await loadOrgs(el);
} catch (err) {
bus.emit('toast', { text: err.message || 'فشل إرسال الطلب', type: 'error' });
bus.emit('toast', { text: err.message || t('org.submit_failed'), type: 'error' });
btn.disabled = false;
btn.style.opacity = '1';
btn.textContent = 'إرسال الطلب';
btn.textContent = t('org.submit');
}
});
}
......
......@@ -38,7 +38,7 @@ async function mountOwnProfile(el) {
<div class="profile-avatar-edit">${emoji('camera', '📷', 14)}</div>
</div>
<input type="file" id="avatar-input" accept="image/*" style="display:none;">
<div id="avatar-uploading" style="display:none;margin:var(--s-2) auto 0;font-size:12px;color:var(--text-secondary);">جاري الرفع...</div>
<div id="avatar-uploading" style="display:none;margin:var(--s-2) auto 0;font-size:12px;color:var(--text-secondary);">${t('profile.uploading')}</div>
<div style="font-size:18px;font-weight:700;margin-top:var(--s-3);">${player.display_name || player.username || 'Player'}</div>
<div style="font-size:13px;color:var(--text-secondary);margin-top:2px;">Level ${player.level || 1}</div>
</div>
......@@ -49,45 +49,45 @@ async function mountOwnProfile(el) {
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s-3);text-align:center;">
<div>
<div style="font-size:20px;font-weight:700;color:var(--gold);font-family:var(--font-lat);">${player.games_played || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">مباريات</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.matches')}</div>
</div>
<div>
<div style="font-size:20px;font-weight:700;color:var(--win);font-family:var(--font-lat);">${player.games_won || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">فوز</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.wins')}</div>
</div>
<div>
<div style="font-size:20px;font-weight:700;color:var(--cyan);font-family:var(--font-lat);">${player.current_streak || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">سلسلة</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.streak')}</div>
</div>
</div>
</div>
<!-- Ratings -->
<div class="card">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">التصنيف</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${t('profile.rating_section')}</div>
<div style="display:flex;flex-direction:column;gap:var(--s-2);">
${renderRating('شطرنج (سريع)', player.elo_rapid)}
${renderRating('شطرنج (خاطف)', player.elo_blitz)}
${renderRating('شطرنج (رصاصة)', player.elo_bullet)}
${renderRating(t('profile.chess_rapid'), player.elo_rapid)}
${renderRating(t('profile.chess_blitz'), player.elo_blitz)}
${renderRating(t('profile.chess_bullet'), player.elo_bullet)}
</div>
</div>
<!-- Organization Membership -->
<div class="card" id="org-section">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('building', '🏢', 18)} المنظمة</div>
<div id="org-membership-content" style="font-size:13px;color:var(--text-secondary);">جاري التحميل...</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${emoji('building', '🏢', 18)} ${t('profile.org_section')}</div>
<div id="org-membership-content" style="font-size:13px;color:var(--text-secondary);">${t('common.loading')}</div>
</div>
<!-- Profile Actions -->
<div style="display:flex;flex-direction:column;gap:var(--s-3);">
<button class="btn btn-primary w-full" id="btn-edit-profile" style="min-height:44px;">${emoji('edit', '✏️', 16)} تعديل الملف الشخصي</button>
<button class="btn btn-secondary w-full" id="btn-org-apply" style="min-height:44px;">${emoji('building', '🏢', 16)} الانضمام لمنظمة</button>
<button class="btn btn-primary w-full" id="btn-edit-profile" style="min-height:44px;">${emoji('edit', '✏️', 16)} ${t('profile.edit_btn')}</button>
<button class="btn btn-secondary w-full" id="btn-org-apply" style="min-height:44px;">${emoji('building', '🏢', 16)} ${t('profile.org_apply')}</button>
</div>
<!-- Actions -->
<div style="display:flex;gap:var(--s-3);">
<button class="btn btn-secondary w-full" id="btn-settings">${t('profile.settings')}</button>
<button class="btn btn-secondary w-full" id="btn-logout" style="color:var(--error);">خروج</button>
<button class="btn btn-secondary w-full" id="btn-logout" style="color:var(--error);">${t('profile.logout')}</button>
</div>
</div>
`;
......@@ -126,10 +126,10 @@ async function mountOwnProfile(el) {
mountView(el);
return;
} else {
bus.emit('toast', { text: data.error || 'فشل رفع الصورة', type: 'error' });
bus.emit('toast', { text: data.error || t('profile.upload_failed'), type: 'error' });
}
} catch (err) {
bus.emit('toast', { text: 'فشل رفع الصورة', type: 'error' });
bus.emit('toast', { text: t('profile.upload_failed'), type: 'error' });
}
isUploading = false;
avatarWrap.style.opacity = '1';
......@@ -192,7 +192,7 @@ async function mountOtherProfile(el, playerId) {
<div style="padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-4);">
<div style="display:flex;align-items:center;gap:var(--s-3);">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;flex:1;">الملف الشخصي</h2>
<h2 style="font-size:18px;font-weight:700;flex:1;">${t('profile.view_title')}</h2>
</div>
<!-- Player Card -->
......@@ -212,26 +212,26 @@ async function mountOtherProfile(el, playerId) {
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s-3);text-align:center;">
<div>
<div style="font-size:20px;font-weight:700;color:var(--gold);font-family:var(--font-lat);">${player.games_played || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">مباريات</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.matches')}</div>
</div>
<div>
<div style="font-size:20px;font-weight:700;color:var(--win);font-family:var(--font-lat);">${player.games_won || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">فوز</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.wins')}</div>
</div>
<div>
<div style="font-size:20px;font-weight:700;color:var(--cyan);font-family:var(--font-lat);">${player.current_streak || 0}</div>
<div style="font-size:11px;color:var(--text-secondary);">سلسلة</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.streak')}</div>
</div>
</div>
</div>
<!-- Ratings -->
<div class="card">
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">التصنيف</div>
<div style="font-size:15px;font-weight:600;margin-bottom:var(--s-3);">${t('profile.rating_section')}</div>
<div style="display:flex;flex-direction:column;gap:var(--s-2);">
${renderRating('شطرنج (سريع)', player.elo_rapid)}
${renderRating('شطرنج (خاطف)', player.elo_blitz)}
${renderRating('شطرنج (رصاصة)', player.elo_bullet)}
${renderRating(t('profile.chess_rapid'), player.elo_rapid)}
${renderRating(t('profile.chess_blitz'), player.elo_blitz)}
${renderRating(t('profile.chess_bullet'), player.elo_bullet)}
</div>
</div>
......@@ -255,7 +255,7 @@ async function mountOtherProfile(el, playerId) {
addFriendBtn.style.opacity = '0.5';
try {
await net.post('friends.php', { action: 'request', target_id: playerId });
addFriendBtn.textContent = '✓ تم إرسال الطلب';
addFriendBtn.textContent = t('profile.friend_sent');
addFriendBtn.style.color = 'var(--success)';
} catch (e) {
addFriendBtn.textContent = e.message || t('common.error');
......@@ -285,10 +285,10 @@ async function mountOtherProfile(el, playerId) {
if (blockBtn) {
blockBtn.addEventListener('click', async () => {
const confirmed = await modal.confirm(t('block.confirm_block'), {
title: 'حظر',
title: t('mp.block'),
icon: '🚫',
confirmText: 'حظر',
cancelText: 'إلغاء',
confirmText: t('mp.block'),
cancelText: t('common.cancel'),
danger: true
});
if (!confirmed) return;
......@@ -337,14 +337,14 @@ async function checkActiveMatch(el, playerId) {
const data = await net.post('game.php', { action: 'find-active-match', player_id: playerId });
if (data.match_id) {
section.style.display = 'block';
const gameLabel = data.game_key === 'ludo' ? 'لودو' : data.game_key === 'domino' ? 'دومينو' : 'شطرنج';
const gameLabel = data.game_key === 'ludo' ? t('game.ludo') : data.game_key === 'domino' ? t('game.domino') : t('game.chess');
section.innerHTML = `
<div class="card" style="background:linear-gradient(135deg,#1a2a1a,#0f1f0f);border:1px solid rgba(52,211,153,0.3);padding:var(--s-3);display:flex;align-items:center;gap:var(--s-3);">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#EF4444;animation:specPulse 1.5s infinite;"></span>
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:#34D399;">يلعب الآن — ${gameLabel}</div>
<div style="font-size:13px;font-weight:600;color:#34D399;">${t('profile.playing_now', { game: gameLabel })}</div>
</div>
<button class="btn btn-primary" id="btn-spectate" style="min-height:32px;padding:4px 14px;font-size:12px;">${emoji('eye', '👁', 13)} شاهد</button>
<button class="btn btn-primary" id="btn-spectate" style="min-height:32px;padding:4px 14px;font-size:12px;">${emoji('eye', '👁', 13)} ${t('profile.watch')}</button>
</div>
<style>@keyframes specPulse{0%,100%{opacity:1}50%{opacity:0.3}}</style>
`;
......@@ -372,12 +372,12 @@ function renderActionButtons(friendStatus, blockStatus) {
let buttons = '';
if (friendStatus === 'accepted') {
buttons += `<button class="btn btn-primary w-full" id="btn-challenge" style="min-height:44px;">${emoji('swords', '⚔️', 16)} تحدي</button>`;
buttons += `<button class="btn btn-secondary w-full" id="btn-message" style="min-height:44px;">${emoji('chat', '💬', 16)} مراسلة</button>`;
buttons += `<button class="btn btn-primary w-full" id="btn-challenge" style="min-height:44px;">${emoji('swords', '⚔️', 16)} ${t('profile.challenge')}</button>`;
buttons += `<button class="btn btn-secondary w-full" id="btn-message" style="min-height:44px;">${emoji('chat', '💬', 16)} ${t('profile.message')}</button>`;
} else if (friendStatus === 'pending') {
buttons += `<button class="btn btn-secondary w-full" disabled style="min-height:44px;opacity:0.6;">طلب صداقة معلق</button>`;
buttons += `<button class="btn btn-secondary w-full" disabled style="min-height:44px;opacity:0.6;">${t('profile.pending_request')}</button>`;
} else {
buttons += `<button class="btn btn-primary w-full" id="btn-add-friend" style="min-height:44px;">➕ إضافة صديق</button>`;
buttons += `<button class="btn btn-primary w-full" id="btn-add-friend" style="min-height:44px;">${t('profile.add_friend')}</button>`;
}
buttons += `<button class="btn btn-secondary w-full" id="btn-block" style="min-height:44px;color:var(--error);">${emoji('block', '🚫', 16)} ${t('block.block')}</button>`;
......@@ -409,10 +409,10 @@ async function loadOrgMembership(el) {
<div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-2) 0;">
${logoHtml}
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${org ? org.name : 'منظمة'}</div>
<div style="font-size:11px;color:var(--text-secondary);">${m.role || 'عضو'}</div>
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${org ? org.name : t('org.title')}</div>
<div style="font-size:11px;color:var(--text-secondary);">${m.role || t('profile.member')}</div>
</div>
<span style="background:var(--success);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;">عضو</span>
<span style="background:var(--success);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;">${t('profile.member')}</span>
</div>
`;
}).join('');
......@@ -423,19 +423,19 @@ async function loadOrgMembership(el) {
if (pending.length > 0) {
html += pending.map(a => {
const org = a.organizations || a.el3ab_organizations;
const orgName = org ? org.name : 'منظمة';
const orgName = org ? org.name : t('org.title');
const logoHtml = org && org.logo_url
? `<img src="${org.logo_url}" style="width:28px;height:28px;border-radius:6px;object-fit:contain;" alt="">`
: `<div style="width:28px;height:28px;border-radius:6px;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;">${emoji('building', '🏢', 14)}</div>`;
const statusBadge = a.status === 'pending'
? `<span style="background:#f59e0b;color:#000;font-size:10px;padding:2px 6px;border-radius:10px;">قيد المراجعة</span>`
: `<span style="background:var(--error);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;">مرفوض</span>`;
? `<span style="background:#f59e0b;color:#000;font-size:10px;padding:2px 6px;border-radius:10px;">${t('profile.pending_review')}</span>`
: `<span style="background:var(--error);color:#fff;font-size:10px;padding:2px 6px;border-radius:10px;">${t('profile.rejected')}</span>`;
return `
<div style="display:flex;align-items:center;gap:var(--s-2);padding:var(--s-2) 0;">
${logoHtml}
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:var(--text-primary);">${orgName}</div>
<div style="font-size:11px;color:var(--text-secondary);">طلب انضمام</div>
<div style="font-size:11px;color:var(--text-secondary);">${t('profile.membership_request')}</div>
</div>
${statusBadge}
</div>
......@@ -443,9 +443,9 @@ async function loadOrgMembership(el) {
}).join('');
}
content.innerHTML = html || `<div style="font-size:13px;color:var(--text-secondary);">لست عضواً في أي منظمة بعد</div>`;
content.innerHTML = html || `<div style="font-size:13px;color:var(--text-secondary);">${t('profile.no_org')}</div>`;
} catch (e) {
content.innerHTML = `<div style="font-size:13px;color:var(--text-secondary);">لست عضواً في أي منظمة بعد</div>`;
content.innerHTML = `<div style="font-size:13px;color:var(--text-secondary);">${t('profile.no_org')}</div>`;
}
}
......
......@@ -16,7 +16,7 @@ export async function mountPuzzle(el) {
<div style="display:flex;flex-direction:column;height:100%;background:var(--bg-deep);">
<div style="display:flex;align-items:center;justify-content:space-between;padding:var(--s-2) var(--s-3);background:var(--bg-base);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:var(--s-1) var(--s-3);font-size:12px;">${t('game.back')}</button>
<span style="font-size:14px;font-weight:600;">أحجية شطرنج</span>
<span style="font-size:14px;font-weight:600;">${t('puzzle.title')}</span>
<span id="puzzle-rating" style="font-size:13px;color:var(--gold);font-family:var(--font-lat);"></span>
</div>
<div id="puzzle-board" style="flex:1;display:flex;align-items:center;justify-content:center;padding:var(--s-2);"></div>
......@@ -124,7 +124,7 @@ function onSolved(el) {
board.interactive = false;
audio.play('win', 'reward');
const status = el.querySelector('#puzzle-status');
status.innerHTML = `${emoji('checkmark', '✅', 15)} أحسنت! حل صحيح`;
status.innerHTML = `${emoji('checkmark', '✅', 15)} ${t('puzzle.correct')}`;
status.style.color = 'var(--win)';
setTimeout(() => {
......@@ -137,7 +137,7 @@ function onFailed(el) {
board.interactive = false;
audio.play('lose', 'game');
const status = el.querySelector('#puzzle-status');
status.innerHTML = `${emoji('cross', '❌', 15)} حل خاطئ`;
status.innerHTML = `${emoji('cross', '❌', 15)} ${t('puzzle.wrong')}`;
status.style.color = 'var(--loss)';
setTimeout(() => {
......
......@@ -4,6 +4,7 @@ import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import * as store from '../../../core/store.js';
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
let pollInterval = null;
let countdownInterval = null;
......@@ -18,7 +19,7 @@ export function mountTournamentArena(el, params) {
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">${emoji('lightning', '⚡', 15)} أرينا</span>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">${emoji('lightning', '⚡', 15)} ${t('tournament.arena')}</span>
</div>
<div id="arena-content" style="flex:1;overflow-y:auto;padding:14px;display:flex;flex-direction:column;align-items:center;gap:16px;">
</div>
......@@ -39,11 +40,11 @@ async function startArena(el, tournamentId, tournamentName) {
content.innerHTML = `
<div style="text-align:center;margin-top:24px;">
<div class="radar-pulse" style="width:80px;height:80px;margin:0 auto 16px;border-radius:50%;background:radial-gradient(circle,#E4AC38 0%,transparent 70%);animation:pulse 1.5s infinite;"></div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;">جاري البحث عن خصم...</div>
<div style="font-size:12px;color:#64748b;margin-top:4px;">${tournamentName || 'أرينا'}</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;">${t('play.searching')}</div>
<div style="font-size:12px;color:#64748b;margin-top:4px;">${tournamentName || t('tournament.arena')}</div>
</div>
<div id="arena-standings" style="width:100%;max-width:320px;margin-top:16px;"></div>
<button class="btn btn-secondary" id="leave-arena" style="margin-top:auto;margin-bottom:16px;color:#ef4444;border-color:#ef4444;">مغادرة الأرينا</button>
<button class="btn btn-secondary" id="leave-arena" style="margin-top:auto;margin-bottom:16px;color:#ef4444;border-color:#ef4444;">${t('tournament.leave_arena')}</button>
<style>
@keyframes pulse { 0%,100% { transform:scale(1);opacity:0.6; } 50% { transform:scale(1.3);opacity:0.2; } }
</style>
......@@ -129,12 +130,12 @@ async function loadArenaStandings(el, tournamentId) {
const userId = store.get('auth.userId');
if (standings.length === 0) {
container.innerHTML = '<div style="text-align:center;color:#64748b;font-size:12px;">لا توجد نتائج بعد</div>';
container.innerHTML = `<div style="text-align:center;color:#64748b;font-size:12px;">${t('tournament.no_results')}</div>`;
return;
}
container.innerHTML = `
<div style="font-size:13px;font-weight:700;color:#94a3b8;margin-bottom:8px;text-align:center;">المتصدرون</div>
<div style="font-size:13px;font-weight:700;color:#94a3b8;margin-bottom:8px;text-align:center;">${t('tournament.leaderboard')}</div>
${standings.slice(0, 10).map((p, i) => {
const isMe = p.player_id === userId;
const medals = ['🥇', '🥈', '🥉'];
......
......@@ -3,6 +3,7 @@ import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
export async function mountTournamentBracket(el, params) {
const { tournamentId } = params;
......@@ -12,10 +13,10 @@ export async function mountTournamentBracket(el, params) {
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">الشجرة</span>
<span style="font-size:15px;font-weight:700;color:#f8fafc;">${t('tournament.bracket')}</span>
</div>
<div id="bracket-container" style="flex:1;overflow:auto;padding:12px;">
<div style="text-align:center;color:#64748b;padding:32px;">جاري التحميل...</div>
<div style="text-align:center;color:#64748b;padding:32px;">${t('common.loading')}</div>
</div>
</div>
`;
......@@ -27,7 +28,7 @@ export async function mountTournamentBracket(el, params) {
const container = el.querySelector('#bracket-container');
if (!data.bracket || !data.matches || data.matches.length === 0) {
container.innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لا توجد شجرة بعد</div>';
container.innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">${t('tournament.no_bracket_yet')}</div>`;
return;
}
......@@ -47,7 +48,7 @@ export async function mountTournamentBracket(el, params) {
const gap = Math.pow(2, totalRounds - r) * 8;
html += `<div style="display:flex;flex-direction:column;justify-content:space-around;min-width:150px;gap:${gap}px;padding:8px 4px;">`;
html += `<div style="text-align:center;font-size:11px;font-weight:700;color:#64748b;margin-bottom:8px;">${roundLabels[r - 1] || 'دور ' + r}</div>`;
html += `<div style="text-align:center;font-size:11px;font-weight:700;color:#64748b;margin-bottom:8px;">${roundLabels[r - 1] || t('tournament.round', { n: r })}</div>`;
matches.forEach(m => {
const isMyMatch = (m.player_a_id === userId || m.player_b_id === userId);
......@@ -56,7 +57,7 @@ export async function mountTournamentBracket(el, params) {
html += `
<div class="bracket-match" data-match-id="${m.match_id || ''}" style="background:${statusBg};border:1px solid ${borderColor};border-radius:8px;padding:6px 8px;position:relative;">
${isMyMatch ? '<div style="position:absolute;top:-6px;right:4px;font-size:9px;background:#E4AC38;color:#000;padding:1px 4px;border-radius:4px;font-weight:700;">أنت</div>' : ''}
${isMyMatch ? `<div style="position:absolute;top:-6px;right:4px;font-size:9px;background:#E4AC38;color:#000;padding:1px 4px;border-radius:4px;font-weight:700;">${t('common.you')}</div>` : ''}
<div style="display:flex;justify-content:space-between;align-items:center;padding:3px 0;${m.winner_id === m.player_a_id ? 'font-weight:700;' : ''}">
<span style="font-size:11px;color:${m.winner_id === m.player_a_id ? '#34D399' : '#f8fafc'};max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.player_a_name || (m.player_a_id ? '...' : 'BYE')}</span>
<span style="font-size:10px;color:#64748b;">${m.result ? m.result.split('-')[0] : ''}</span>
......@@ -66,7 +67,7 @@ export async function mountTournamentBracket(el, params) {
<span style="font-size:11px;color:${m.winner_id === m.player_b_id ? '#34D399' : '#f8fafc'};max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${m.player_b_name || (m.player_b_id ? '...' : 'TBD')}</span>
<span style="font-size:10px;color:#64748b;">${m.result ? m.result.split('-')[1] : ''}</span>
</div>
${m.status === 'pending' && isMyMatch && m.player_a_id && m.player_b_id ? `<button class="bracket-play-btn" data-mid="${m.match_id}" style="width:100%;margin-top:4px;padding:4px;background:#E4AC38;border:none;border-radius:4px;color:#000;font-weight:700;font-size:10px;cursor:pointer;">العب</button>` : ''}
${m.status === 'pending' && isMyMatch && m.player_a_id && m.player_b_id ? `<button class="bracket-play-btn" data-mid="${m.match_id}" style="width:100%;margin-top:4px;padding:4px;background:#E4AC38;border:none;border-radius:4px;color:#000;font-weight:700;font-size:10px;cursor:pointer;">${t('common.play')}</button>` : ''}
</div>
`;
});
......@@ -94,7 +95,7 @@ export async function mountTournamentBracket(el, params) {
});
} catch (e) {
el.querySelector('#bracket-container').innerHTML = '<div style="text-align:center;color:#ef4444;padding:32px;">فشل تحميل الشجرة</div>';
el.querySelector('#bracket-container').innerHTML = `<div style="text-align:center;color:#ef4444;padding:32px;">${t('tournament.bracket_load_failed')}</div>`;
}
}
......@@ -102,10 +103,10 @@ function getRoundLabels(totalRounds) {
const labels = [];
for (let r = 1; r <= totalRounds; r++) {
const remaining = totalRounds - r;
if (remaining === 0) labels.push('النهائي');
else if (remaining === 1) labels.push('نصف النهائي');
else if (remaining === 2) labels.push('ربع النهائي');
else labels.push('دور ' + Math.pow(2, remaining + 1));
if (remaining === 0) labels.push(t('tournament.final'));
else if (remaining === 1) labels.push(t('tournament.semifinal'));
else if (remaining === 2) labels.push(t('tournament.quarterfinal'));
else labels.push(t('tournament.round', { n: Math.pow(2, remaining + 1) }));
}
return labels;
}
......@@ -3,6 +3,7 @@ import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js';
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
import * as realtime from '../../../core/realtime.js';
let countdownInterval = null;
......@@ -17,29 +18,29 @@ export function mountTournamentLobby(el, params) {
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;align-items:center;justify-content:center;padding:24px;gap:20px;">
<div style="text-align:center;">
<div style="font-size:20px;font-weight:800;color:#f8fafc;">${tournamentName || 'بطولة'}</div>
<div style="font-size:20px;font-weight:800;color:#f8fafc;">${tournamentName || t('tournament.title')}</div>
<div id="countdown" style="font-size:42px;font-weight:800;color:#E4AC38;margin-top:12px;font-family:Inter,monospace;">--:--</div>
<div style="font-size:13px;color:#64748b;margin-top:4px;">تبدأ خلال</div>
<div style="font-size:13px;color:#64748b;margin-top:4px;">${t('tournament.starts_in', { n: '' })}</div>
</div>
<div id="players-grid" style="display:flex;flex-wrap:wrap;gap:8px;justify-content:center;max-width:280px;"></div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;width:100%;max-width:300px;">
<div style="background:#1a1a2e;border-radius:8px;padding:10px;text-align:center;">
<div style="font-size:11px;color:#64748b;">النظام</div>
<div style="font-size:11px;color:#64748b;">${t('tournament.format_label')}</div>
<div style="font-size:13px;font-weight:700;color:#f8fafc;">${format || 'swiss'}</div>
</div>
<div style="background:#1a1a2e;border-radius:8px;padding:10px;text-align:center;">
<div style="font-size:11px;color:#64748b;">الوقت</div>
<div style="font-size:11px;color:#64748b;">${t('tournament.time_label')}</div>
<div style="font-size:13px;font-weight:700;color:#f8fafc;">${timeControl || '?'}</div>
</div>
<div style="background:#1a1a2e;border-radius:8px;padding:10px;text-align:center;">
<div style="font-size:11px;color:#64748b;">الجوائز</div>
<div style="font-size:11px;color:#64748b;">${t('tournament.prizes_label')}</div>
<div style="font-size:13px;font-weight:700;color:#E4AC38;">${prizePool || '0'} ${emoji('coin', '🪙', 13)}</div>
</div>
</div>
<button class="btn btn-secondary" id="leave-lobby" style="margin-top:auto;color:#ef4444;border-color:#ef4444;font-size:13px;">مغادرة البطولة</button>
<button class="btn btn-secondary" id="leave-lobby" style="margin-top:auto;color:#ef4444;border-color:#ef4444;font-size:13px;">${t('tournament.leave')}</button>
</div>
`;
......@@ -96,7 +97,7 @@ async function loadPlayers(el, tournamentId) {
const registrations = data.registrations || [];
if (registrations.length === 0) {
grid.innerHTML = '<div style="font-size:12px;color:#64748b;">في انتظار اللاعبين...</div>';
grid.innerHTML = `<div style="font-size:12px;color:#64748b;">${t('tournament.waiting')}</div>`;
return;
}
......
......@@ -33,7 +33,7 @@ export async function mountTournaments(el) {
function renderTournaments(el, tournaments) {
const list = el.querySelector('#tournament-list');
if (tournaments.length === 0) {
list.innerHTML = `<p style="color:#94a3b8;text-align:center;padding:32px;">لا توجد بطولات حالياً</p>`;
list.innerHTML = `<p style="color:#94a3b8;text-align:center;padding:32px;">${t('tournament.no_tournaments')}</p>`;
return;
}
......@@ -41,7 +41,7 @@ function renderTournaments(el, tournaments) {
<div class="card tournament-card" data-id="${tour.id}" style="padding:16px;margin-bottom:12px;cursor:pointer;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="font-size:15px;font-weight:700;">${tour.name || 'بطولة'}</div>
<div style="font-size:15px;font-weight:700;">${tour.name || t('tournament.title')}</div>
<div style="font-size:12px;color:#94a3b8;margin-top:2px;">${tour.game_key || 'chess'} · ${formatName(tour.format)}</div>
<div style="font-size:11px;color:#64748b;margin-top:4px;">${tour.starts_at ? new Date(tour.starts_at).toLocaleDateString('ar') : ''}</div>
</div>
......@@ -49,10 +49,10 @@ function renderTournaments(el, tournaments) {
</div>
<div style="display:flex;gap:16px;margin-top:12px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05);">
<span style="font-size:11px;color:#94a3b8;">${emoji('people', '👥', 11)} ${tour.player_count || 0}/${tour.max_players || 32}</span>
<span style="font-size:11px;color:#E4AC38;">${emoji('tournament_cup', '🏆', 11)} ${tour.prize_pool_coins ? tour.prize_pool_coins + ' عملة' : 'N/A'}</span>
<span style="font-size:11px;color:#E4AC38;">${emoji('tournament_cup', '🏆', 11)} ${tour.prize_pool_coins ? tour.prize_pool_coins + ' ' + t('common.coins') : 'N/A'}</span>
${tour.entry_fee_coins ? `<span style="font-size:11px;color:#F87171;">${emoji('money_bag', '💰', 11)} ${tour.entry_fee_coins}</span>` : ''}
</div>
${tour.status === 'registration' ? `<button class="btn btn-primary w-full register-btn" data-tid="${tour.id}" style="margin-top:12px;font-size:13px;">سجّل الآن</button>` : ''}
${tour.status === 'registration' ? `<button class="btn btn-primary w-full register-btn" data-tid="${tour.id}" style="margin-top:12px;font-size:13px;">${t('tournament.register_now')}</button>` : ''}
</div>
`).join('');
......@@ -63,13 +63,13 @@ function renderTournaments(el, tournaments) {
audio.play('click');
const tid = btn.dataset.tid;
btn.disabled = true;
btn.textContent = 'جاري التسجيل...';
btn.textContent = t('tournament.registering');
try {
await net.post('tournaments.php', { action: 'register', tournament_id: tid });
btn.textContent = '✅ تم التسجيل';
btn.textContent = '✅ ' + t('tournament.registered');
btn.style.background = '#34D399';
} catch (err) {
btn.textContent = err.message || 'فشل التسجيل';
btn.textContent = err.message || t('tournament.register_failed');
btn.disabled = false;
}
});
......@@ -88,19 +88,19 @@ async function showTournamentDetail(el, tournamentId, tour) {
const list = el.querySelector('#tournament-list');
list.innerHTML = `
<div style="margin-bottom:16px;">
<button class="btn btn-secondary" id="detail-back" style="font-size:12px;min-height:32px;padding:4px 12px;">← رجوع للقائمة</button>
<button class="btn btn-secondary" id="detail-back" style="font-size:12px;min-height:32px;padding:4px 12px;">← ${t('tournament.back_to_list')}</button>
</div>
<div class="card" style="padding:16px;">
<div style="font-size:18px;font-weight:700;margin-bottom:4px;">${tour?.name || 'بطولة'}</div>
<div style="font-size:18px;font-weight:700;margin-bottom:4px;">${tour?.name || t('tournament.title')}</div>
<div style="font-size:12px;color:#94a3b8;margin-bottom:12px;">${formatName(tour?.format)} · ${tour?.game_key || 'chess'}</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;">
<div style="background:#1a2440;padding:8px;border-radius:8px;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#E4AC38;">${tour?.player_count || 0}</div>
<div style="font-size:10px;color:#64748b;">لاعبين</div>
<div style="font-size:10px;color:#64748b;">${t('tournament.players_label')}</div>
</div>
<div style="background:#1a2440;padding:8px;border-radius:8px;text-align:center;">
<div style="font-size:16px;font-weight:700;color:#00FFFF;">${tour?.rounds_total || tour?.swiss_rounds || '?'}</div>
<div style="font-size:10px;color:#64748b;">جولات</div>
<div style="font-size:10px;color:#64748b;">${t('tournament.rounds_label')}</div>
</div>
</div>
</div>
......@@ -119,20 +119,20 @@ async function showTournamentDetail(el, tournamentId, tour) {
const data = await net.get('tournaments.php', { action: 'detail', id: tournamentId });
renderBracketOrStandings(el, data, tour?.format);
} catch (e) {
el.querySelector('#bracket-area').innerHTML = `<p style="color:#64748b;text-align:center;">لا توجد نتائج بعد</p>`;
el.querySelector('#bracket-area').innerHTML = `<p style="color:#64748b;text-align:center;">${t('tournament.no_results_yet')}</p>`;
}
}
function renderBracketOrStandings(el, data, format) {
const area = el.querySelector('#bracket-area');
if (!data || (!data.rounds && !data.brackets)) {
area.innerHTML = `<p style="color:#64748b;text-align:center;padding:16px;">لا توجد نتائج بعد</p>`;
area.innerHTML = `<p style="color:#64748b;text-align:center;padding:16px;">${t('tournament.no_results_yet')}</p>`;
return;
}
area.innerHTML = `
<div class="card" style="padding:12px;">
<div style="font-size:14px;font-weight:600;margin-bottom:8px;">النتائج</div>
<div style="font-size:12px;color:#94a3b8;">بيانات البطولة متاحة عند بدء الجولات</div>
<div style="font-size:14px;font-weight:600;margin-bottom:8px;">${t('tournament.results')}</div>
<div style="font-size:12px;color:#94a3b8;">${t('tournament.data_available_later')}</div>
</div>
`;
}
......@@ -148,15 +148,15 @@ function getStatusColor(status) {
function getStatusLabel(status) {
switch (status) {
case 'registration': return 'تسجيل مفتوح';
case 'in_progress': return 'جارية';
case 'completed': return 'منتهية';
case 'draft': return 'قريباً';
default: return status || 'قادمة';
case 'registration': return t('tournament.registration_open');
case 'in_progress': return t('tournament.active');
case 'completed': return t('tournament.completed');
case 'draft': return t('tournament.coming_soon');
default: return status || t('tournament.upcoming');
}
}
function formatName(format) {
const names = { swiss: 'سويسري', round_robin: 'دوري', single_elimination: 'خروج المغلوب', double_elimination: 'خروج مزدوج', arena: 'أرينا' };
const names = { swiss: t('tournament.format_swiss'), round_robin: t('tournament.format_round_robin'), single_elimination: t('tournament.format_single_elim'), double_elimination: t('tournament.format_double_elim'), arena: t('tournament.format_arena') };
return names[format] || format || '';
}
......@@ -2,13 +2,14 @@ import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import * as scene from '../../../core/scene.js';
import * as juice from '../../../core/juice.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
const CATEGORY_LABELS = {
gameplay: 'اللعب',
social: 'اجتماعي',
progression: 'التقدم',
collection: 'جمع',
gameplay: () => t('achievements.gameplay'),
social: () => t('achievements.social'),
progression: () => t('achievements.progression'),
collection: () => t('achievements.collection'),
};
const TIER_COLORS = {
......@@ -19,7 +20,7 @@ const TIER_COLORS = {
};
export async function mountAchievements(el) {
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;background:#0a0a1a;color:#64748b;">جاري التحميل...</div>`;
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;background:#0a0a1a;color:#64748b;">${t('common.loading')}</div>`;
let achievements = [];
let stats = { total: 0, completed: 0 };
......@@ -57,7 +58,7 @@ function render(el, achievements, stats) {
<!-- Header -->
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:16px;font-weight:700;color:#f8fafc;">${emoji('trophy', '🏆', 16)} الإنجازات</span>
<span style="font-size:16px;font-weight:700;color:#f8fafc;">${emoji('trophy', '🏆', 16)} ${t('achievements.title')}</span>
<span style="margin-inline-start:auto;font-size:12px;color:#64748b;">${stats.completed}/${stats.total}</span>
</div>
......@@ -67,15 +68,15 @@ function render(el, achievements, stats) {
<div style="background:linear-gradient(90deg,#E4AC38,#FFCC66);height:100%;width:${progressPct}%;border-radius:99px;transition:width 0.5s;"></div>
</div>
<div style="display:flex;justify-content:space-between;margin-top:4px;">
<span style="font-size:10px;color:#64748b;">${progressPct}% مكتمل</span>
<span style="font-size:10px;color:#E4AC38;">${stats.completed} إنجاز</span>
<span style="font-size:10px;color:#64748b;">${progressPct}% ${t('achievements.complete')}</span>
<span style="font-size:10px;color:#E4AC38;">${stats.completed} ${t('achievements.title')}</span>
</div>
</div>
<!-- Category filter -->
<div style="display:flex;gap:6px;padding:12px 16px;overflow-x:auto;">
<button class="ach-filter active" data-cat="all">الكل</button>
${categories.map(c => `<button class="ach-filter" data-cat="${c}">${CATEGORY_LABELS[c] || c}</button>`).join('')}
<button class="ach-filter active" data-cat="all">${t('achievements.all')}</button>
${categories.map(c => `<button class="ach-filter" data-cat="${c}">${CATEGORY_LABELS[c] ? CATEGORY_LABELS[c]() : c}</button>`).join('')}
</div>
<!-- Achievement list -->
......@@ -106,7 +107,7 @@ function render(el, achievements, stats) {
function renderList(achievements, category) {
const filtered = category === 'all' ? achievements : achievements.filter(a => a.category === category);
if (filtered.length === 0) {
return `<div style="text-align:center;padding:32px;color:#475569;font-size:13px;">لا توجد إنجازات في هذه الفئة</div>`;
return `<div style="text-align:center;padding:32px;color:#475569;font-size:13px;">${t('achievements.none')}</div>`;
}
return filtered.map(a => {
......@@ -131,12 +132,12 @@ function renderList(achievements, category) {
<span style="font-size:10px;color:#64748b;white-space:nowrap;">${a.progress}/${a.target}</span>
</div>
` : `
<div style="font-size:10px;color:#22c55e;">✓ مكتمل</div>
<div style="font-size:10px;color:#22c55e;">✓ ${t('achievements.complete')}</div>
`}
</div>
<div style="text-align:center;min-width:40px;">
<div style="font-size:11px;font-weight:700;color:#E4AC38;">${a.coins_reward}</div>
<div style="font-size:9px;color:#64748b;">عملة</div>
<div style="font-size:9px;color:#64748b;">${t('game.coins')}</div>
</div>
</div>
`;
......
......@@ -4,6 +4,7 @@ import * as juice from '../../../core/juice.js';
import * as bus from '../../../core/bus.js';
import * as store from '../../../core/store.js';
import * as scene from '../../../core/scene.js';
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
export async function mountChallenges(el) {
......@@ -11,13 +12,13 @@ export async function mountChallenges(el) {
<div style="padding:16px;display:flex;flex-direction:column;gap:14px;">
<div style="display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<h2 style="font-size:18px;font-weight:800;color:#f8fafc;flex:1;">${emoji('lightning', '⚡', 18)} التحديات اليومية</h2>
<h2 style="font-size:18px;font-weight:800;color:#f8fafc;flex:1;">${emoji('lightning', '⚡', 18)} ${t('challenges.title')}</h2>
<div id="streak-badge" style="background:linear-gradient(135deg,#E4AC38,#FFCC66);color:#1a1a1a;font-size:12px;font-weight:800;padding:5px 12px;border-radius:99px;"></div>
</div>
<div id="challenges-list"></div>
<div id="all-done" style="display:none;text-align:center;padding:20px;">
<div style="font-size:40px;margin-bottom:8px;">${emoji('checkmark', '✅', 40)}</div>
<div style="font-size:15px;font-weight:700;color:#34D399;">أنجزت كل التحديات اليوم!</div>
<div style="font-size:15px;font-weight:700;color:#34D399;">${t('challenges.all_done')}</div>
</div>
</div>
`;
......@@ -28,14 +29,14 @@ export async function mountChallenges(el) {
const data = await net.get('challenges.php');
renderChallenges(el, data);
} catch (e) {
el.querySelector('#challenges-list').innerHTML = '<div style="text-align:center;color:#ef4444;">فشل التحميل</div>';
el.querySelector('#challenges-list').innerHTML = `<div style="text-align:center;color:#ef4444;">${t('common.error_load')}</div>`;
}
}
function renderChallenges(el, data) {
const { challenges, streak, streak_bonus } = data;
el.querySelector('#streak-badge').textContent = `🔥 ${streak || 0} يوم`;
el.querySelector('#streak-badge').textContent = `🔥 ${t('challenges.streak', { n: streak || 0 })}`;
const list = el.querySelector('#challenges-list');
list.innerHTML = challenges.map((c, i) => `
......@@ -52,7 +53,7 @@ function renderChallenges(el, data) {
</div>
<div style="text-align:center;min-width:50px;">
${c.claimed ? '<span style="color:#34D399;font-size:16px;">✓</span>' :
c.completed ? `<button class="claim-btn" data-id="${c.id}" data-coins="${c.reward_coins}" data-xp="${c.reward_xp}" style="background:#E4AC38;color:#1a1a1a;border:none;border-radius:8px;padding:6px 10px;font-size:11px;font-weight:700;cursor:pointer;">اجمع</button>` :
c.completed ? `<button class="claim-btn" data-id="${c.id}" data-coins="${c.reward_coins}" data-xp="${c.reward_xp}" style="background:#E4AC38;color:#1a1a1a;border:none;border-radius:8px;padding:6px 10px;font-size:11px;font-weight:700;cursor:pointer;">${t('challenges.claim')}</button>` :
`<span style="font-size:11px;color:#E4AC38;font-weight:600;">${c.reward_coins}${emoji('coin', '🪙', 14)}</span>`}
</div>
</div>
......
......@@ -10,7 +10,7 @@ import { emoji } from '../../../core/theme.js';
const DAY_REWARDS = [50, 75, 100, 125, 150, 200, 300];
export async function mountDaily(el) {
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;background:#0a0a1a;color:#64748b;">جاري التحميل...</div>`;
el.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;background:#0a0a1a;color:#64748b;">${t('common.loading')}</div>`;
let streak = 0;
let alreadyClaimed = false;
......@@ -45,14 +45,14 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
<!-- Header -->
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<span style="font-size:16px;font-weight:700;color:#f8fafc;">${emoji('gift', '🎁', 16)} المكافأة اليومية</span>
<span style="font-size:16px;font-weight:700;color:#f8fafc;">${emoji('gift', '🎁', 16)} ${t('daily.title')}</span>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;align-items:center;gap:16px;">
<!-- Streak badge -->
<div style="background:linear-gradient(135deg,#92400e,#E4AC38);padding:8px 20px;border-radius:99px;display:flex;align-items:center;gap:6px;">
<span style="font-size:18px;">🔥</span>
<span style="font-size:14px;font-weight:800;color:#1a1a1a;">${streak} يوم متتالي</span>
<span style="font-size:14px;font-weight:800;color:#1a1a1a;">${t('daily.streak_days', { n: streak })}</span>
</div>
<!-- 7-day timeline -->
......@@ -69,7 +69,7 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
color:${claimed ? '#34D399' : isToday ? '#1a1a1a' : '#64748b'};">
${claimed ? '✓' : coins}
</div>
<span style="font-size:9px;color:${isToday ? '#E4AC38' : '#475569'};font-weight:${isToday ? '700' : '400'};">يوم ${i + 1}</span>
<span style="font-size:9px;color:${isToday ? '#E4AC38' : '#475569'};font-weight:${isToday ? '700' : '400'};">${t('daily.day', { n: i + 1 })}</span>
</div>
`;
}).join('')}
......@@ -79,19 +79,19 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
<div style="text-align:center;margin-top:8px;">
${alreadyClaimed ? `
<div style="font-size:48px;margin-bottom:8px;">${emoji('checkmark', '', 48)}</div>
<div style="font-size:16px;font-weight:700;color:#34D399;">تم استلام مكافأة اليوم</div>
<div style="font-size:13px;color:#64748b;margin-top:4px;">عد غداً لمكافأة أكبر!</div>
<div style="font-size:16px;font-weight:700;color:#34D399;">${t('daily.claimed_today')}</div>
<div style="font-size:13px;color:#64748b;margin-top:4px;">${t('daily.come_back')}</div>
` : `
<div style="font-size:56px;margin-bottom:8px;animation:float 3s ease-in-out infinite;">${emoji('gift', '🎁', 56)}</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">مكافأة اليوم</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">${t('daily.today_reward')}</div>
<div style="font-size:28px;font-weight:800;color:#E4AC38;margin-bottom:16px;">${todayReward} ${emoji('coin', '🪙', 24)}</div>
<button class="btn btn-primary" id="claim-btn" style="font-size:16px;padding:14px 48px;">استلم المكافأة</button>
<button class="btn btn-primary" id="claim-btn" style="font-size:16px;padding:14px 48px;">${t('daily.claim_btn')}</button>
`}
</div>
<!-- Info -->
<div style="text-align:center;color:#475569;font-size:11px;margin-top:auto;padding:12px;">
كل يوم تجمع فيه المكافأة يزيد المبلغ • اليوم السابع = 300 عملة!
${t('daily.hint')}
</div>
</div>
</div>
......@@ -103,13 +103,13 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
if (claimBtn) {
claimBtn.addEventListener('click', async () => {
claimBtn.disabled = true;
claimBtn.textContent = 'جاري الاستلام...';
claimBtn.textContent = t('daily.claiming');
try {
const data = await net.post('daily-reward.php', { action: 'claim' });
if (data.error) {
claimBtn.textContent = data.error;
setTimeout(() => { claimBtn.textContent = 'استلم المكافأة'; claimBtn.disabled = false; }, 2000);
setTimeout(() => { claimBtn.textContent = t('daily.claim_btn'); claimBtn.disabled = false; }, 2000);
return;
}
......@@ -132,7 +132,7 @@ function render(el, streak, alreadyClaimed, dayIndex, todayReward) {
// Re-render with claimed state
render(el, data.streak, true, data.day_index, data.coins);
} catch (e) {
claimBtn.textContent = 'فشل حاول مرة أخرى';
claimBtn.textContent = t('daily.failed');
claimBtn.disabled = false;
}
});
......
import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
// Rank tier system
export const TIERS = [
{ name: 'برونزي', nameEn: 'Bronze', min: 0, max: 999, color: '#CD7F32', get icon() { return emoji('medal_bronze', '🥉', 16); } },
{ name: 'فضي', nameEn: 'Silver', min: 1000, max: 1199, color: '#C0C0C0', get icon() { return emoji('medal_silver', '🥈', 16); } },
{ name: 'ذهبي', nameEn: 'Gold', min: 1200, max: 1399, color: '#FFD700', get icon() { return emoji('medal_gold', '🥇', 16); } },
{ name: 'بلاتيني', nameEn: 'Platinum', min: 1400, max: 1599, color: '#00CED1', get icon() { return emoji('gem', '💎', 16); } },
{ name: 'ماسي', nameEn: 'Diamond', min: 1600, max: 1799, color: '#B9F2FF', icon: '💠' },
{ name: 'أسطوري', nameEn: 'Master', min: 1800, max: 2000, color: '#FF4500', icon: '👑' },
{ name: 'جراند ماستر', nameEn: 'Grandmaster', min: 2000, max: 9999, color: '#8B008B', icon: '🏅' },
{ get name() { return t('rank.bronze'); }, nameEn: 'Bronze', min: 0, max: 999, color: '#CD7F32', get icon() { return emoji('medal_bronze', '🥉', 16); } },
{ get name() { return t('rank.silver'); }, nameEn: 'Silver', min: 1000, max: 1199, color: '#C0C0C0', get icon() { return emoji('medal_silver', '🥈', 16); } },
{ get name() { return t('rank.gold'); }, nameEn: 'Gold', min: 1200, max: 1399, color: '#FFD700', get icon() { return emoji('medal_gold', '🥇', 16); } },
{ get name() { return t('rank.platinum'); }, nameEn: 'Platinum', min: 1400, max: 1599, color: '#00CED1', get icon() { return emoji('gem', '💎', 16); } },
{ get name() { return t('rank.diamond'); }, nameEn: 'Diamond', min: 1600, max: 1799, color: '#B9F2FF', icon: '💠' },
{ get name() { return t('rank.master'); }, nameEn: 'Master', min: 1800, max: 2000, color: '#FF4500', icon: '👑' },
{ get name() { return t('rank.grandmaster'); }, nameEn: 'Grandmaster', min: 2000, max: 9999, color: '#8B008B', icon: '🏅' },
];
export function getTier(rating) {
......
......@@ -73,7 +73,7 @@ async function purchasePrompt(el, item) {
<div style="font-size:14px;color:var(--gold);margin-bottom:var(--s-4);">${item.price_coins || 0} ${emoji('coin', '🪙', 16)}</div>
${canAfford
? `<button class="btn btn-primary w-full" id="buy-btn">${t('common.confirm')}</button>`
: `<p style="color:var(--error);font-size:13px;">رصيد غير كافي</p>`
: `<p style="color:var(--error);font-size:13px;">${t('shop.insufficient_balance')}</p>`
}
<button class="btn btn-secondary w-full" id="cancel-buy" style="margin-top:var(--s-2);">${t('common.cancel')}</button>
</div>
......@@ -90,7 +90,7 @@ async function purchasePrompt(el, item) {
await net.post('shop.php', { action: 'purchase', cosmetic_id: item.id });
audio.play('coin', 'reward');
store.set('player.coins', (player.coins || 0) - (item.price_coins || 0));
overlay.innerHTML = `<div class="card" style="padding:var(--s-6);text-align:center;"><div style="font-size:36px;margin-bottom:var(--s-2);">${emoji('checkmark', '✅', 36)}</div><div>تم الشراء!</div></div>`;
overlay.innerHTML = `<div class="card" style="padding:var(--s-6);text-align:center;"><div style="font-size:36px;margin-bottom:var(--s-2);">${emoji('checkmark', '✅', 36)}</div><div>${t('shop.purchased')}</div></div>`;
setTimeout(() => { overlay.classList.remove('active'); overlay.innerHTML = ''; }, 1500);
} catch (e) {
overlay.innerHTML = `<div class="card" style="padding:var(--s-6);text-align:center;color:var(--error);">${t('common.error')}</div>`;
......
......@@ -5,7 +5,7 @@ import { emoji } from '../../../core/theme.js';
export async function mountActivity(el) {
el.innerHTML = `
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">📰 آخر الأخبار</h2>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">📰 ${t('social.activity_tab')}</h2>
<div id="activity-list"></div>
</div>
`;
......@@ -14,25 +14,25 @@ export async function mountActivity(el) {
const data = await net.get('activity.php');
renderActivity(el, data.activity || []);
} catch (e) {
el.querySelector('#activity-list').innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار</div>';
el.querySelector('#activity-list').innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">${t('social.no_activity_short')}</div>`;
}
}
function renderActivity(el, activities) {
const list = el.querySelector('#activity-list');
if (activities.length === 0) {
list.innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار — العب لتظهر أخبارك هنا</div>';
list.innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">${t('social.no_activity_short')}</div>`;
return;
}
const actionLabels = {
'game_win': 'فاز بمباراة',
'game_loss': 'خسر مباراة',
'game_draw': 'تعادل في مباراة',
'achievement_unlock': 'حصل على إنجاز',
'level_up': 'ارتقى لمستوى جديد',
'tournament_join': 'انضم لبطولة',
'friend_add': 'أضاف صديق جديد'
'game_win': t('social.game_win'),
'game_loss': t('social.game_loss'),
'game_draw': t('social.game_draw'),
'achievement_unlock': t('social.achievement_unlock'),
'level_up': t('social.level_up'),
'tournament_join': t('social.tournament_join'),
'friend_add': t('social.friend_add')
};
list.innerHTML = activities.map(a => {
......@@ -58,9 +58,9 @@ function timeAgo(date) {
if (!date) return '';
const diff = Date.now() - new Date(date).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `منذ ${mins} دقيقة`;
if (mins < 1) return t('common.now');
if (mins < 60) return t('common.minutes_ago', { n: mins });
const hours = Math.floor(mins / 60);
if (hours < 24) return `منذ ${hours} ساعة`;
return `منذ ${Math.floor(hours / 24)} يوم`;
if (hours < 24) return t('common.hours_ago', { n: hours });
return t('common.days_ago', { n: Math.floor(hours / 24) });
}
......@@ -4,6 +4,7 @@ import * as audio from '../../../core/audio.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js';
import { emoji } from '../../../core/theme.js';
import { t } from '../../../core/i18n.js';
let pollTimer = null;
let friendId = null;
......@@ -18,7 +19,7 @@ export function mountChat(el, params = {}) {
isLoading = false;
if (pollTimer) clearInterval(pollTimer);
const name = friendProfile?.display_name || friendProfile?.username || 'صديق';
const name = friendProfile?.display_name || friendProfile?.username || t('common.friend');
const avatar = friendProfile?.avatar_url;
el.innerHTML = `
......@@ -31,7 +32,7 @@ export function mountChat(el, params = {}) {
</div>
<div class="chat-header-info">
<div class="chat-header-name">${name}</div>
<div class="chat-header-status" id="chat-status">${friendProfile?.is_online ? 'متصل الآن' : 'غير متصل'}</div>
<div class="chat-header-status" id="chat-status">${friendProfile?.is_online ? t('common.online') : t('common.offline')}</div>
</div>
<button id="chat-invite" class="chat-action-btn">${emoji('challenge_swords', '⚔️', 16)}</button>
<button id="chat-profile" class="chat-action-btn">${emoji('person', '👤', 14)}</button>
......@@ -39,12 +40,12 @@ export function mountChat(el, params = {}) {
<!-- Messages -->
<div class="chat-messages" id="chat-messages">
<div class="chat-loading" id="chat-loading">${emoji('loading', '⏳', 16)} جاري التحميل...</div>
<div class="chat-loading" id="chat-loading">${emoji('loading', '⏳', 16)} ${t('common.loading')}</div>
</div>
<!-- Input -->
<div class="chat-input-bar">
<input type="text" id="chat-input" class="chat-input" placeholder="اكتب رسالة..." maxlength="500" autocomplete="off">
<input type="text" id="chat-input" class="chat-input" placeholder="${t('chat.placeholder')}" maxlength="500" autocomplete="off">
<button id="chat-send" class="chat-send-btn">${emoji('send', '📤', 18)}</button>
</div>
</div>
......@@ -122,7 +123,7 @@ async function loadMessages(el) {
renderMessages(el);
} catch (e) {
const container = el.querySelector('#chat-messages');
container.innerHTML = '<div class="chat-loading" style="color:#ef4444;">فشل تحميل الرسائل</div>';
container.innerHTML = `<div class="chat-loading" style="color:#ef4444;">${t('chat.failed_load')}</div>`;
}
}
......@@ -200,7 +201,7 @@ function renderMessages(el, scrollToBottom = false) {
container.innerHTML = `
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;opacity:0.6;">
<div style="font-size:32px;">${emoji('wave', '👋', 32)}</div>
<div style="font-size:13px;color:#64748b;">ابدأ المحادثة مع صديقك!</div>
<div style="font-size:13px;color:#64748b;">${t('chat.start_conversation')}</div>
</div>
`;
return;
......@@ -223,10 +224,10 @@ function renderMessages(el, scrollToBottom = false) {
html += `<div class="chat-bubble system">${escapeHtml(msg.content)}</div>`;
} else if (msg.message_type === 'invite') {
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata || '{}') : (msg.metadata || {});
const gameLabel = meta.game_key === 'ludo' ? '🎲 لودو' : '♟ شطرنج';
const gameLabel = meta.game_key === 'ludo' ? '🎲 ' + t('game.ludo') : '♟ ' + t('game.chess');
html += `
<div class="chat-bubble ${isMine ? 'mine' : 'theirs'}" style="background:${isMine ? 'rgba(37,99,235,0.3)' : 'rgba(228,172,56,0.15)'};border:1px solid ${isMine ? 'rgba(37,99,235,0.3)' : 'rgba(228,172,56,0.3)'};">
<div style="font-size:12px;font-weight:600;margin-bottom:4px;">${emoji('challenge_swords', '⚔️', 13)} تحدي ${gameLabel}</div>
<div style="font-size:12px;font-weight:600;margin-bottom:4px;">${emoji('challenge_swords', '⚔️', 13)} ${t('chat.challenge_title', { name: gameLabel })}</div>
<div style="font-size:12px;opacity:0.8;">${escapeHtml(msg.content)}</div>
<div class="chat-time">${time}</div>
</div>
......@@ -252,7 +253,7 @@ function showInviteFromChat(el) {
const existing = document.getElementById('chat-invite-dialog');
if (existing) { existing.remove(); return; }
const name = friendProfile?.display_name || friendProfile?.username || 'صديق';
const name = friendProfile?.display_name || friendProfile?.username || t('common.friend');
const dialog = document.createElement('div');
dialog.id = 'chat-invite-dialog';
......@@ -260,24 +261,24 @@ function showInviteFromChat(el) {
dialog.innerHTML = `
<div style="background:#1a1a2e;border-radius:16px;padding:24px;width:100%;max-width:300px;text-align:center;">
<div style="font-size:24px;margin-bottom:8px;">${emoji('challenge_swords', '⚔️', 24)}</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:4px;">تحدّي ${name}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">اختر اللعبة</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:4px;">${t('chat.challenge_title', { name })}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">${t('chat.choose_game')}</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button class="cig active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ شطرنج</button>
<button class="cig" data-game="domino" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🁣 دومينو</button>
<button class="cig" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 لودو</button>
<button class="cig active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ ${t('game.chess')}</button>
<button class="cig" data-game="domino" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🁣 ${t('game.domino')}</button>
<button class="cig" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 ${t('game.ludo')}</button>
</div>
<div id="cig-time" style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;flex-wrap:wrap;">
<button class="cit" data-tc="bullet_1_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">1 د</button>
<button class="cit active" data-tc="blitz_3_0" style="padding:6px 10px;border-radius:8px;background:#E4AC38;border:none;color:#1a1a1a;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">3 د</button>
<button class="cit" data-tc="blitz_5_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">5 د</button>
<button class="cit" data-tc="rapid_10_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">10 د</button>
<button class="cit" data-tc="bullet_1_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.1min')}</button>
<button class="cit active" data-tc="blitz_3_0" style="padding:6px 10px;border-radius:8px;background:#E4AC38;border:none;color:#1a1a1a;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.3min')}</button>
<button class="cit" data-tc="blitz_5_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.5min')}</button>
<button class="cit" data-tc="rapid_10_0" style="padding:6px 10px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.10min')}</button>
</div>
<button class="btn btn-primary" id="cig-send" style="width:100%;font-size:14px;padding:12px;">أرسل التحدي</button>
<button id="cig-cancel" style="margin-top:8px;background:none;border:none;color:#64748b;font-size:12px;cursor:pointer;font-family:inherit;">إلغاء</button>
<button class="btn btn-primary" id="cig-send" style="width:100%;font-size:14px;padding:12px;">${t('challenge.send')}</button>
<button id="cig-cancel" style="margin-top:8px;background:none;border:none;color:#64748b;font-size:12px;cursor:pointer;font-family:inherit;">${t('common.cancel')}</button>
</div>
`;
......@@ -306,7 +307,7 @@ function showInviteFromChat(el) {
dialog.querySelector('#cig-send').addEventListener('click', async () => {
const sendBtn = dialog.querySelector('#cig-send');
sendBtn.disabled = true;
sendBtn.textContent = 'جاري الإرسال...';
sendBtn.textContent = t('common.sending');
try {
const res = await net.post('friends.php', {
......@@ -322,7 +323,7 @@ function showInviteFromChat(el) {
await net.post('chat.php', {
action: 'send',
friend_id: friendId,
content: `أرسل تحدي ${selectedGame === 'ludo' ? 'لودو' : selectedGame === 'domino' ? 'دومينو' : 'شطرنج'}`,
content: t('challenge.sent_msg', { game: selectedGame === 'ludo' ? t('game.ludo') : selectedGame === 'domino' ? t('game.domino') : t('game.chess') }),
message_type: 'invite',
metadata: { game_key: selectedGame, time_control: selectedTc, match_id: res.match_id }
});
......@@ -342,7 +343,7 @@ function showInviteFromChat(el) {
isHost: true
});
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.textContent = t('challenge.failed_retry');
sendBtn.disabled = false;
}
});
......
......@@ -22,14 +22,14 @@ export function mountFriends(el) {
<div style="padding:12px 16px;background:#0f0f1e;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">${t('social.friends')}</h2>
<button class="btn btn-secondary" id="btn-search" style="min-height:34px;padding:6px 14px;font-size:12px;">${emoji('search_icon', '🔍', 13)} بحث</button>
<button class="btn btn-secondary" id="btn-search" style="min-height:34px;padding:6px 14px;font-size:12px;">${emoji('search_icon', '🔍', 13)} ${t('social.search_btn')}</button>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="social-tab active" data-tab="friends">الأصدقاء</button>
<button class="social-tab" data-tab="pending">الطلبات <span id="pending-count" style="display:none;font-size:10px;background:#EF4444;color:#fff;border-radius:50%;padding:1px 5px;margin-right:2px;"></span></button>
<button class="social-tab" data-tab="online">متصلين</button>
<button class="social-tab" data-tab="groups">${emoji('group', '👥', 12)} المجموعات</button>
<button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} أخبار</button>
<button class="social-tab active" data-tab="friends">${t('social.friends_tab')}</button>
<button class="social-tab" data-tab="pending">${t('social.pending_tab')} <span id="pending-count" style="display:none;font-size:10px;background:#EF4444;color:#fff;border-radius:50%;padding:1px 5px;margin-right:2px;"></span></button>
<button class="social-tab" data-tab="online">${t('social.online_tab')}</button>
<button class="social-tab" data-tab="groups">${emoji('group', '👥', 12)} ${t('social.groups_tab')}</button>
<button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} ${t('social.activity_tab')}</button>
</div>
</div>
<!-- Invite banner -->
......@@ -102,16 +102,16 @@ async function checkInvites(el) {
banner.style.display = 'block';
banner.innerHTML = invites.map(inv => {
const from = profiles[inv.from_id] || {};
const name = from.display_name || from.username || 'صديق';
const gameLabel = inv.game_key === 'ludo' ? 'لودو' : inv.game_key === 'domino' ? 'دومينو' : 'شطرنج';
const name = from.display_name || from.username || t('common.friend');
const gameLabel = inv.game_key === 'ludo' ? t('game.ludo') : inv.game_key === 'domino' ? t('game.domino') : t('game.chess');
return `
<div style="display:flex;align-items:center;gap:10px;padding:10px 16px;background:linear-gradient(135deg,#1a2a1a,#0f1f0f);border-bottom:1px solid rgba(52,211,153,0.2);animation:slideDown 0.3s;">
<span style="font-size:20px;">⚔️</span>
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:#34D399;">${name} يتحداك!</div>
<div style="font-size:13px;font-weight:600;color:#34D399;">${t('social.challenges_you', { name })}</div>
<div style="font-size:11px;color:#64748b;">${gameLabel}</div>
</div>
<button class="btn btn-primary" data-accept-invite="${inv.match_id}" style="min-height:30px;padding:5px 14px;font-size:11px;">قبول</button>
<button class="btn btn-primary" data-accept-invite="${inv.match_id}" style="min-height:30px;padding:5px 14px;font-size:11px;">${t('social.accept')}</button>
<button class="btn btn-secondary" data-decline-invite="${inv.match_id}" style="min-height:30px;padding:5px 10px;font-size:11px;">✕</button>
</div>
`;
......@@ -124,7 +124,7 @@ async function checkInvites(el) {
try {
const inv = invites.find(i => i.match_id === btn.dataset.acceptInvite);
const res = await net.post('friends.php', { action: 'accept-invite', match_id: btn.dataset.acceptInvite, game_key: inv?.game_key || 'chess' });
if (res.error) { btn.textContent = 'خطأ'; return; }
if (res.error) { btn.textContent = t('social.error'); return; }
audio.play('reward');
juice.hapticSuccess();
scene.push('game-lobby', {
......@@ -137,7 +137,7 @@ async function checkInvites(el) {
isHost: false
});
} catch (e) {
btn.textContent = 'فشل';
btn.textContent = t('common.failed');
}
});
});
......@@ -173,7 +173,7 @@ async function loadPendingCount(el) {
async function loadTab(el, tab) {
const content = el.querySelector('#social-content');
content.innerHTML = '<div style="text-align:center;padding:20px;color:#64748b;">جاري التحميل...</div>';
content.innerHTML = `<div style="text-align:center;padding:20px;color:#64748b;">${t('common.loading')}</div>`;
switch (tab) {
case 'friends': await loadFriends(content); break;
......@@ -193,9 +193,9 @@ async function loadFriends(content) {
content.innerHTML = `
<div style="text-align:center;padding:40px 20px;">
<div style="font-size:48px;margin-bottom:12px;opacity:0.5;">${emoji('people', '👥', 48)}</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:6px;">لا يوجد أصدقاء بعد</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">ابحث عن لاعبين وأرسل لهم طلب صداقة</div>
<button class="btn btn-primary" id="empty-search" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ابحث عن لاعبين</button>
<div style="font-size:15px;font-weight:700;color:#f8fafc;margin-bottom:6px;">${t('social.no_friends')}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">${t('social.no_friends_hint')}</div>
<button class="btn btn-primary" id="empty-search" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ${t('social.search_players')}</button>
</div>`;
content.querySelector('#empty-search')?.addEventListener('click', () => {
showSearch(content.closest('[style*="height:100%"]') || content.parentElement);
......@@ -208,12 +208,12 @@ async function loadFriends(content) {
const sorted = [...online, ...offline];
content.innerHTML = `
<div style="font-size:11px;color:#64748b;margin-bottom:8px;">${friends.length} أصدقاء${online.length > 0 ? ` — ${online.length} متصل` : ''}</div>
<div style="font-size:11px;color:#64748b;margin-bottom:8px;">${t('social.friends_count', { n: friends.length })}${online.length > 0 ? ` — ${t('social.online_count', { n: online.length })}` : ''}</div>
${sorted.map(f => renderFriendCard(f)).join('')}
`;
bindFriendActions(content);
} catch (e) {
content.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">فشل التحميل — <span style="text-decoration:underline;cursor:pointer;" id="retry-friends">حاول مرة أخرى</span></div>`;
content.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">${t('common.error_load')} — <span style="text-decoration:underline;cursor:pointer;" id="retry-friends">${t('social.retry_load')}</span></div>`;
content.querySelector('#retry-friends')?.addEventListener('click', () => loadFriends(content));
}
}
......@@ -227,7 +227,7 @@ async function loadPending(content, rootEl) {
content.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">${emoji('inbox', '📥', 32)}</div>
<div>لا توجد طلبات معلقة</div>
<div>${t('social.no_pending')}</div>
</div>`;
return;
}
......@@ -242,10 +242,10 @@ async function loadPending(content, rootEl) {
}
content.innerHTML = `
<div style="font-size:11px;color:#64748b;margin-bottom:10px;">${pending.length} طلب معلق</div>
<div style="font-size:11px;color:#64748b;margin-bottom:10px;">${t('social.pending_count', { n: pending.length })}</div>
${pending.map(p => {
const profile = profiles[p.requester_id] || {};
const name = profile.display_name || profile.username || p.requester_id?.substring(0, 8) || 'لاعب';
const name = profile.display_name || profile.username || p.requester_id?.substring(0, 8) || t('common.player');
const avatar = profile.avatar_url;
const level = profile.level || 1;
return `
......@@ -255,11 +255,11 @@ async function loadPending(content, rootEl) {
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${name}</div>
<div style="font-size:11px;color:#64748b;">مستوى ${level} — أرسل لك طلب صداقة</div>
<div style="font-size:11px;color:#64748b;">${t('common.level', { n: level })}${t('social.sent_friend_request')}</div>
</div>
<div class="friend-actions">
<div class="friend-action" data-accept="${p.id}" title="قبول" style="background:rgba(52,211,153,0.15);border-color:rgba(52,211,153,0.3);color:#34D399;font-weight:700;">✓</div>
<div class="friend-action" data-reject="${p.id}" title="رفض" style="background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.2);color:#EF4444;">✕</div>
<div class="friend-action" data-accept="${p.id}" title="${t('social.accept')}" style="background:rgba(52,211,153,0.15);border-color:rgba(52,211,153,0.3);color:#34D399;font-weight:700;">✓</div>
<div class="friend-action" data-reject="${p.id}" title="${t('common.cancel')}" style="background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.2);color:#EF4444;">✕</div>
</div>
</div>
`;
......@@ -281,7 +281,7 @@ async function loadPending(content, rootEl) {
}
audio.play('reward');
juice.hapticLight();
card.querySelector('.friend-actions').innerHTML = '<span style="font-size:11px;color:#34D399;font-weight:600;">✓ تم القبول</span>';
card.querySelector('.friend-actions').innerHTML = `<span style="font-size:11px;color:#34D399;font-weight:600;">${t('common.accepted')}</span>`;
card.style.borderRight = '3px solid #34D399';
loadPendingCount(rootEl);
} catch (e) {
......@@ -307,7 +307,7 @@ async function loadPending(content, rootEl) {
});
});
} catch (e) {
content.innerHTML = `<div style="text-align:center;color:#ef4444;">فشل التحميل</div>`;
content.innerHTML = `<div style="text-align:center;color:#ef4444;">${t('common.error_load')}</div>`;
}
}
......@@ -320,19 +320,19 @@ async function loadOnline(content) {
content.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">${emoji('sleeping', '😴', 32)}</div>
<div style="font-size:14px;margin-bottom:4px;">لا يوجد أصدقاء متصلين الآن</div>
<div style="font-size:11px;color:#475569;">سيظهرون هنا عند دخولهم</div>
<div style="font-size:14px;margin-bottom:4px;">${t('social.no_online_friends')}</div>
<div style="font-size:11px;color:#475569;">${t('social.will_appear_online')}</div>
</div>`;
return;
}
content.innerHTML = `
<div style="font-size:11px;color:#34D399;margin-bottom:10px;">${friends.length} متصل الآن</div>
<div style="font-size:11px;color:#34D399;margin-bottom:10px;">${t('social.online_now_count', { n: friends.length })}</div>
${friends.map(f => renderFriendCard(f)).join('')}
`;
bindFriendActions(content);
} catch (e) {
content.innerHTML = `<div style="text-align:center;color:#ef4444;">فشل التحميل</div>`;
content.innerHTML = `<div style="text-align:center;color:#ef4444;">${t('common.error_load')}</div>`;
}
}
......@@ -344,13 +344,13 @@ function renderFriendCard(f) {
${f.is_online ? '<div class="online-dot"></div>' : ''}
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${f.display_name || f.username || 'لاعب'}</div>
<div style="font-size:11px;color:${f.is_online ? '#34D399' : '#64748b'};">${f.is_online ? 'متصل الآن' : 'غير متصل'}${f.level ? ` — مستوى ${f.level}` : ''}</div>
<div style="font-size:13px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${f.display_name || f.username || t('common.player')}</div>
<div style="font-size:11px;color:${f.is_online ? '#34D399' : '#64748b'};">${f.is_online ? t('common.online') : t('common.offline')}${f.level ? ` — ${t('common.level', { n: f.level })}` : ''}</div>
</div>
<div class="friend-actions">
<div class="friend-action" data-chat="${f.id}" title="محادثة" style="background:rgba(37,99,235,0.15);border-color:rgba(37,99,235,0.3);color:#3B82F6;">💬</div>
${f.is_online ? `<div class="friend-action" data-invite="${f.id}" title="تحدّي" style="background:rgba(228,172,56,0.15);border-color:rgba(228,172,56,0.3);color:#E4AC38;">${emoji('challenge_swords', '⚔️', 14)}</div>` : ''}
<div class="friend-action" data-remove="${f.id}" title="إزالة" style="font-size:11px;color:#64748b;">✕</div>
<div class="friend-action" data-chat="${f.id}" title="${t('social.chat')}" style="background:rgba(37,99,235,0.15);border-color:rgba(37,99,235,0.3);color:#3B82F6;">💬</div>
${f.is_online ? `<div class="friend-action" data-invite="${f.id}" title="${t('challenge.send')}" style="background:rgba(228,172,56,0.15);border-color:rgba(228,172,56,0.3);color:#E4AC38;">${emoji('challenge_swords', '⚔️', 14)}</div>` : ''}
<div class="friend-action" data-remove="${f.id}" title="${t('social.remove_friend')}" style="font-size:11px;color:#64748b;">✕</div>
</div>
</div>
`;
......@@ -375,7 +375,7 @@ function bindFriendActions(content) {
juice.hapticLight();
const uid = btn.dataset.invite;
const card = btn.closest('.friend-card');
const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'صديق';
const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || t('common.friend');
showInviteDialog(content, uid, name);
});
});
......@@ -384,12 +384,12 @@ function bindFriendActions(content) {
btn.addEventListener('click', async () => {
audio.play('click');
const card = btn.closest('.friend-card');
const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'هذا الصديق';
const confirmed = await modal.confirm(`إزالة ${name} من الأصدقاء؟`, {
title: 'إزالة صديق',
const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || t('common.friend');
const confirmed = await modal.confirm(t('social.remove_confirm', { name }), {
title: t('social.remove_friend'),
icon: '👋',
confirmText: 'إزالة',
cancelText: 'إلغاء',
confirmText: t('social.remove_friend'),
cancelText: t('common.cancel'),
danger: true
});
if (!confirmed) return;
......@@ -414,23 +414,23 @@ function showInviteDialog(content, targetId, targetName) {
dialog.innerHTML = `
<div style="background:#1a1a2e;border-radius:16px;padding:24px;width:100%;max-width:320px;text-align:center;">
<div style="font-size:24px;margin-bottom:8px;">⚔️</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">تحدّي ${targetName}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">اختر اللعبة ونوع الوقت</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">${t('challenge.send')} ${targetName}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">${t('challenge.select_game')}</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button class="inv-game active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ شطرنج</button>
<button class="inv-game" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 لودو</button>
<button class="inv-game active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ ${t('game.chess')}</button>
<button class="inv-game" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 ${t('game.ludo')}</button>
</div>
<div id="time-options" style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;flex-wrap:wrap;">
<button class="inv-time active" data-tc="bullet_1_0" style="padding:6px 12px;border-radius:8px;background:#E4AC38;border:none;color:#1a1a1a;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">1 دقيقة</button>
<button class="inv-time" data-tc="blitz_3_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">3 دقائق</button>
<button class="inv-time" data-tc="blitz_5_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">5 دقائق</button>
<button class="inv-time" data-tc="rapid_10_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">10 دقائق</button>
<button class="inv-time active" data-tc="bullet_1_0" style="padding:6px 12px;border-radius:8px;background:#E4AC38;border:none;color:#1a1a1a;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.1min')}</button>
<button class="inv-time" data-tc="blitz_3_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.3min')}</button>
<button class="inv-time" data-tc="blitz_5_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.5min')}</button>
<button class="inv-time" data-tc="rapid_10_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">${t('time.10min')}</button>
</div>
<button class="btn btn-primary" id="send-invite" style="width:100%;font-size:14px;padding:12px;">أرسل التحدي</button>
<button id="cancel-invite" style="margin-top:8px;background:none;border:none;color:#64748b;font-size:12px;cursor:pointer;font-family:inherit;">إلغاء</button>
<button class="btn btn-primary" id="send-invite" style="width:100%;font-size:14px;padding:12px;">${t('challenge.send')}</button>
<button id="cancel-invite" style="margin-top:8px;background:none;border:none;color:#64748b;font-size:12px;cursor:pointer;font-family:inherit;">${t('common.cancel')}</button>
</div>
`;
......@@ -477,7 +477,7 @@ function showInviteDialog(content, targetId, targetName) {
dialog.querySelector('#send-invite').addEventListener('click', async () => {
const sendBtn = dialog.querySelector('#send-invite');
sendBtn.disabled = true;
sendBtn.textContent = 'جاري الإرسال...';
sendBtn.textContent = t('common.sending');
try {
const res = await net.post('friends.php', {
......@@ -495,7 +495,7 @@ function showInviteDialog(content, targetId, targetName) {
audio.play('reward');
juice.hapticSuccess();
sendBtn.textContent = '✓ تم إرسال التحدي!';
sendBtn.textContent = t('challenge.sent');
sendBtn.style.background = '#34D399';
// Navigate to lobby
......@@ -511,7 +511,7 @@ function showInviteDialog(content, targetId, targetName) {
});
}, 800);
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.textContent = t('challenge.failed_retry');
sendBtn.disabled = false;
}
});
......@@ -525,10 +525,10 @@ function showSearch(el) {
content.innerHTML = `
<div style="margin-bottom:16px;">
<div style="display:flex;gap:8px;">
<input class="input" id="search-input" type="text" placeholder="اسم المستخدم أو الاسم..." style="flex:1;min-height:40px;font-size:14px;" autocomplete="off">
<button class="btn btn-primary" id="search-go" style="min-height:40px;padding:8px 16px;font-size:13px;">بحث</button>
<input class="input" id="search-input" type="text" placeholder="${t('social.search_placeholder')}" style="flex:1;min-height:40px;font-size:14px;" autocomplete="off">
<button class="btn btn-primary" id="search-go" style="min-height:40px;padding:8px 16px;font-size:13px;">${t('social.search_btn')}</button>
</div>
<div style="font-size:11px;color:#475569;margin-top:4px;">ابحث بأي جزء من الاسم (حرفين على الأقل)</div>
<div style="font-size:11px;color:#475569;margin-top:4px;">${t('social.search_hint')}</div>
</div>
<div id="search-results"></div>
`;
......@@ -541,17 +541,17 @@ function showSearch(el) {
const doSearch = async () => {
const query = input.value.trim();
if (query.length < 2) {
content.querySelector('#search-results').innerHTML = '<div style="text-align:center;color:#64748b;padding:12px;font-size:12px;">أدخل حرفين على الأقل</div>';
content.querySelector('#search-results').innerHTML = `<div style="text-align:center;color:#64748b;padding:12px;font-size:12px;">${t('social.search_min_chars')}</div>`;
return;
}
const results = content.querySelector('#search-results');
results.innerHTML = '<div style="text-align:center;color:#64748b;padding:12px;">جاري البحث...</div>';
results.innerHTML = `<div style="text-align:center;color:#64748b;padding:12px;">${t('social.searching')}</div>`;
try {
const data = await net.get('friends.php', { action: 'search', query });
const players = data.players || [];
if (players.length === 0) {
results.innerHTML = '<div style="text-align:center;color:#64748b;padding:24px;font-size:13px;">لم يتم العثور على نتائج — جرب اسم آخر</div>';
results.innerHTML = `<div style="text-align:center;color:#64748b;padding:24px;font-size:13px;">${t('social.no_results')}</div>`;
return;
}
results.innerHTML = players.map(p => `
......@@ -561,10 +561,10 @@ function showSearch(el) {
${p.is_online ? '<div class="online-dot"></div>' : ''}
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${p.display_name || p.username || 'لاعب'}</div>
<div style="font-size:11px;color:#64748b;">${p.is_online ? 'متصل' : 'غير متصل'} — مستوى ${p.level || 1}</div>
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${p.display_name || p.username || t('common.player')}</div>
<div style="font-size:11px;color:#64748b;">${p.is_online ? t('common.online') : t('common.offline')}${t('common.level', { n: p.level || 1 })}</div>
</div>
<button class="btn btn-primary" data-add="${p.id}" style="min-height:32px;padding:6px 12px;font-size:11px;flex-shrink:0;">+ أضف</button>
<button class="btn btn-primary" data-add="${p.id}" style="min-height:32px;padding:6px 12px;font-size:11px;flex-shrink:0;">${t('social.add_btn')}</button>
</div>
`).join('');
......@@ -575,30 +575,30 @@ function showSearch(el) {
try {
const res = await net.post('friends.php', { action: 'request', target_id: addBtn.dataset.add });
if (res.error) {
addBtn.textContent = res.error === 'Already friends' ? 'صديق بالفعل' : res.error === 'Request already pending' ? 'مرسل سابقاً' : res.error;
addBtn.textContent = res.error === 'Already friends' ? t('common.already_friends') : res.error === 'Request already pending' ? t('common.already_sent') : res.error;
addBtn.style.background = '#64748b';
return;
}
audio.play('reward');
juice.hapticLight();
addBtn.textContent = '✓ تم الإرسال';
addBtn.textContent = t('common.sent');
addBtn.style.background = '#34D399';
addBtn.style.borderColor = '#34D399';
} catch (e) {
const msg = e.message || '';
if (msg.includes('duplicate') || msg.includes('already') || msg.includes('Already')) {
addBtn.textContent = 'مرسل سابقاً';
addBtn.textContent = t('common.already_sent');
addBtn.style.background = '#64748b';
} else {
addBtn.textContent = 'فشل';
addBtn.textContent = t('common.failed');
addBtn.disabled = false;
setTimeout(() => { addBtn.textContent = '+ أضف'; }, 2000);
setTimeout(() => { addBtn.textContent = t('social.add_btn'); }, 2000);
}
}
});
});
} catch (e) {
results.innerHTML = '<div style="text-align:center;color:#ef4444;">فشل البحث</div>';
results.innerHTML = `<div style="text-align:center;color:#ef4444;">${t('social.search_failed')}</div>`;
}
};
......@@ -618,19 +618,19 @@ async function loadActivity(content) {
content.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">${emoji('news', '📰', 32)}</div>
<div>لا توجد أخبار — العب لتظهر أخبارك هنا</div>
<div>${t('social.no_activity')}</div>
</div>`;
return;
}
const labels = {
game_win: 'فاز بمباراة',
game_loss: 'خسر مباراة',
game_draw: 'تعادل',
achievement_unlock: 'حصل على إنجاز',
level_up: 'ارتقى لمستوى جديد',
tournament_join: 'انضم لبطولة',
tournament_win: 'فاز ببطولة',
friend_add: 'أضاف صديقاً جديداً'
game_win: t('social.game_win'),
game_loss: t('social.game_loss'),
game_draw: t('social.game_draw'),
achievement_unlock: t('social.achievement_unlock'),
level_up: t('social.level_up'),
tournament_join: t('social.tournament_join'),
tournament_win: t('social.tournament_win'),
friend_add: t('social.friend_add')
};
const icons = {
game_win: '🏆', game_loss: '💔', game_draw: '🤝',
......@@ -652,19 +652,19 @@ async function loadActivity(content) {
</div>`;
}).join('');
} catch (e) {
content.innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار</div>`;
content.innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">${t('social.no_activity_short')}</div>`;
}
}
function timeAgo(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `منذ ${mins} دقيقة`;
if (mins < 1) return t('common.now');
if (mins < 60) return t('common.minutes_ago', { n: mins });
const hours = Math.floor(mins / 60);
if (hours < 24) return `منذ ${hours} ساعة`;
if (hours < 24) return t('common.hours_ago', { n: hours });
const days = Math.floor(hours / 24);
if (days < 7) return `منذ ${days} يوم`;
if (days < 7) return t('common.days_ago', { n: days });
return new Date(dateStr).toLocaleDateString('ar');
}
......
......@@ -21,15 +21,15 @@ export async function mountGroupChat(el, params = {}) {
<div style="padding:10px 16px;background:#0f0f1e;display:flex;align-items:center;gap:10px;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="width:34px;height:34px;padding:0;font-size:14px;">←</button>
<div id="group-header" style="flex:1;cursor:pointer;">
<div style="font-size:15px;font-weight:600;color:#f8fafc;">جاري التحميل...</div>
<div style="font-size:15px;font-weight:600;color:#f8fafc;">${t('common.loading')}</div>
</div>
<button class="btn btn-secondary" id="invite-game-btn" style="min-height:32px;padding:5px 10px;font-size:12px;">${emoji('swords', '⚔️', 13)} لعب</button>
<button class="btn btn-secondary" id="invite-game-btn" style="min-height:32px;padding:5px 10px;font-size:12px;">${emoji('swords', '⚔️', 13)} ${t('group.play')}</button>
</div>
<div id="invite-banner" style="display:none;"></div>
<div id="chat-messages" style="flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:6px;"></div>
<div style="padding:10px 16px;background:#0f0f1e;border-top:1px solid rgba(255,255,255,0.06);display:flex;gap:8px;align-items:center;">
<input class="input" id="msg-input" type="text" placeholder="اكتب رسالة..." style="flex:1;min-height:38px;" maxlength="500">
<button class="btn btn-primary" id="send-btn" style="min-height:38px;padding:0 14px;">إرسال</button>
<input class="input" id="msg-input" type="text" placeholder="${t('chat.placeholder')}" style="flex:1;min-height:38px;" maxlength="500">
<button class="btn btn-primary" id="send-btn" style="min-height:38px;padding:0 14px;">${t('common.send')}</button>
</div>
</div>
`;
......@@ -55,14 +55,14 @@ export async function mountGroupChat(el, params = {}) {
const header = el.querySelector('#group-header');
header.innerHTML = `
<div style="font-size:15px;font-weight:600;color:#f8fafc;">${escapeHtml(groupData.name)}</div>
<div style="font-size:11px;color:#64748b;">${members.length} أعضاء</div>
<div style="font-size:11px;color:#64748b;">${members.length} ${t('group.members')}</div>
`;
header.addEventListener('click', () => {
audio.play('click');
scene.push('group-members', { groupId, myRole: detail.my_role });
});
} catch (e) {
el.querySelector('#group-header').innerHTML = `<div style="color:var(--error);">خطأ في تحميل المجموعة</div>`;
el.querySelector('#group-header').innerHTML = `<div style="color:var(--error);">${t('group.load_error')}</div>`;
return;
}
......@@ -138,8 +138,8 @@ function messageHtml(msg, myId, memberMap) {
const meta = typeof msg.metadata === 'string' ? JSON.parse(msg.metadata) : (msg.metadata || {});
let systemText = msg.content;
if (msg.content === '__system:game_invite') {
const gameLabel = meta.game_key === 'ludo' ? 'لودو' : meta.game_key === 'domino' ? 'دومينو' : 'شطرنج';
systemText = `${emoji('swords', '⚔️', 12)} دعوة لعب ${gameLabel} (${meta.required_players} لاعبين)`;
const gameLabel = meta.game_key === 'ludo' ? t('game.ludo') : meta.game_key === 'domino' ? t('game.domino') : t('game.chess');
systemText = `${emoji('swords', '⚔️', 12)} ${t('group.play_invite')} ${gameLabel} (${meta.required_players} ${t('group.players')})`;
}
return `<div style="text-align:center;padding:6px;font-size:11px;color:#64748b;">${systemText}</div>`;
}
......@@ -187,7 +187,7 @@ async function checkGroupInvites(el, groupId, myId) {
banner.style.display = 'block';
banner.innerHTML = invites.map(inv => {
const gameLabel = inv.game_key === 'ludo' ? 'لودو' : inv.game_key === 'domino' ? 'دومينو' : 'شطرنج';
const gameLabel = inv.game_key === 'ludo' ? t('game.ludo') : inv.game_key === 'domino' ? t('game.domino') : t('game.chess');
const accepted = inv.accepted || [];
const isAccepted = accepted.includes(myId);
const isInviter = accepted[0] === myId;
......@@ -197,7 +197,7 @@ async function checkGroupInvites(el, groupId, myId) {
<div style="flex:1;">
<div style="font-size:12px;font-weight:600;color:#34D399;">${gameLabel}${accepted.length}/${inv.required_players}</div>
</div>
${isAccepted || isInviter ? `<span style="font-size:11px;color:#64748b;">✓ قبلت</span>` : `<button class="btn btn-primary accept-invite-btn" data-match="${inv.match_id}" data-game="${inv.game_key}" style="min-height:28px;padding:4px 12px;font-size:11px;">انضم</button>`}
${isAccepted || isInviter ? `<span style="font-size:11px;color:#64748b;">${t('group.accepted')}</span>` : `<button class="btn btn-primary accept-invite-btn" data-match="${inv.match_id}" data-game="${inv.game_key}" style="min-height:28px;padding:4px 12px;font-size:11px;">${t('group.join')}</button>`}
</div>`;
}).join('');
......@@ -219,12 +219,12 @@ async function checkGroupInvites(el, groupId, myId) {
scene.push('chess-game', { matchId: res.match_id, color: res.color });
}
} else {
btn.textContent = '✓ قبلت';
btn.textContent = t('group.accepted');
btn.style.background = 'transparent';
btn.style.color = '#64748b';
}
} catch (e) {
btn.textContent = 'خطأ';
btn.textContent = t('social.error');
btn.disabled = false;
}
});
......@@ -241,26 +241,26 @@ function showGamePicker(el, groupId) {
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:flex-end;justify-content:center;';
overlay.innerHTML = `
<div style="background:#1a1a2e;border-radius:20px 20px 0 0;padding:24px;width:100%;max-width:400px;display:flex;flex-direction:column;gap:12px;">
<div style="font-size:16px;font-weight:700;color:#f8fafc;text-align:center;">دعوة لعب في المجموعة</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;text-align:center;">${t('group.play_invite')}</div>
<button class="game-pick" data-game="chess" data-players="2" style="display:flex;align-items:center;gap:12px;padding:14px;background:#2a2a4a;border-radius:12px;border:none;cursor:pointer;width:100%;">
<span style="font-size:24px;">♟️</span>
<div style="text-align:right;flex:1;">
<div style="font-size:14px;font-weight:600;color:#f8fafc;">شطرنج</div>
<div style="font-size:11px;color:#64748b;">لاعبين</div>
<div style="font-size:14px;font-weight:600;color:#f8fafc;">${t('game.chess')}</div>
<div style="font-size:11px;color:#64748b;">${t('group.players')}</div>
</div>
</button>
<button class="game-pick" data-game="ludo" data-players="4" style="display:flex;align-items:center;gap:12px;padding:14px;background:#2a2a4a;border-radius:12px;border:none;cursor:pointer;width:100%;">
<span style="font-size:24px;">🎲</span>
<div style="text-align:right;flex:1;">
<div style="font-size:14px;font-weight:600;color:#f8fafc;">لودو</div>
<div style="font-size:11px;color:#64748b;">2-4 لاعبين</div>
<div style="font-size:14px;font-weight:600;color:#f8fafc;">${t('game.ludo')}</div>
<div style="font-size:11px;color:#64748b;">2-4 ${t('group.players')}</div>
</div>
</button>
<button class="game-pick" data-game="domino" data-players="2" style="display:flex;align-items:center;gap:12px;padding:14px;background:#2a2a4a;border-radius:12px;border:none;cursor:pointer;width:100%;">
<span style="font-size:24px;">🁣</span>
<div style="text-align:right;flex:1;">
<div style="font-size:14px;font-weight:600;color:#f8fafc;">دومينو</div>
<div style="font-size:11px;color:#64748b;">لاعبين</div>
<div style="font-size:14px;font-weight:600;color:#f8fafc;">${t('game.domino')}</div>
<div style="font-size:11px;color:#64748b;">${t('group.players')}</div>
</div>
</button>
<button id="cancel-pick" class="btn btn-secondary w-full" style="min-height:40px;">${t('common.cancel')}</button>
......@@ -282,7 +282,7 @@ function showGamePicker(el, groupId) {
game_key: gameKey,
required_players: requiredPlayers
});
bus.emit('toast', { text: 'تم إرسال دعوة اللعب!', type: 'success' });
bus.emit('toast', { text: t('group.invite_sent'), type: 'success' });
} catch (e) {
bus.emit('toast', { text: e.message || t('common.error'), type: 'error' });
}
......
......@@ -10,22 +10,22 @@ export async function mountGroupCreate(el) {
<div style="display:flex;flex-direction:column;height:100%;">
<div style="padding:12px 16px;background:#0f0f1e;display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">إنشاء مجموعة</h2>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">${t('group.create_title')}</h2>
</div>
<div style="flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:16px;">
<div>
<label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:6px;">اسم المجموعة</label>
<input class="input" id="group-name" type="text" placeholder="اسم المجموعة..." maxlength="50" style="width:100%;">
<label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:6px;">${t('group.name_label')}</label>
<input class="input" id="group-name" type="text" placeholder="${t('group.name_label')}..." maxlength="50" style="width:100%;">
</div>
<div>
<label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:6px;">اختر أصدقاء للإضافة</label>
<label style="font-size:13px;color:var(--text-secondary);display:block;margin-bottom:6px;">${t('group.select_friends')}</label>
<div id="friends-picker" style="display:flex;flex-direction:column;gap:6px;">
<div style="text-align:center;padding:12px;color:var(--text-secondary);font-size:13px;">${t('common.loading')}</div>
</div>
</div>
<button class="btn btn-primary w-full" id="create-btn" style="min-height:48px;font-size:15px;font-weight:700;">إنشاء المجموعة</button>
<button class="btn btn-primary w-full" id="create-btn" style="min-height:48px;font-size:15px;font-weight:700;">${t('group.create_btn')}</button>
</div>
</div>
`;
......@@ -41,7 +41,7 @@ export async function mountGroupCreate(el) {
const picker = el.querySelector('#friends-picker');
if (!friends.length) {
picker.innerHTML = `<div style="text-align:center;padding:12px;color:var(--text-secondary);font-size:13px;">لا يوجد أصدقاء</div>`;
picker.innerHTML = `<div style="text-align:center;padding:12px;color:var(--text-secondary);font-size:13px;">${t('social.no_friends')}</div>`;
} else {
picker.innerHTML = friends.map(f => `
<label data-id="${f.id}" style="display:flex;align-items:center;gap:10px;padding:10px;background:#1a1a2e;border-radius:10px;cursor:pointer;transition:background 0.1s;">
......@@ -67,13 +67,13 @@ export async function mountGroupCreate(el) {
el.querySelector('#create-btn').addEventListener('click', async () => {
const name = el.querySelector('#group-name').value.trim();
if (!name || name.length < 2) {
bus.emit('toast', { text: 'أدخل اسم المجموعة (حرفين على الأقل)', type: 'error' });
bus.emit('toast', { text: t('group.name_min'), type: 'error' });
return;
}
const btn = el.querySelector('#create-btn');
btn.disabled = true;
btn.textContent = 'جاري الإنشاء...';
btn.textContent = t('group.creating');
try {
const res = await net.post('groups.php', {
......@@ -83,13 +83,13 @@ export async function mountGroupCreate(el) {
});
if (res.error) throw new Error(res.error);
audio.play('click');
bus.emit('toast', { text: 'تم إنشاء المجموعة!', type: 'success' });
bus.emit('toast', { text: t('group.created'), type: 'success' });
scene.pop();
scene.push('group-chat', { groupId: res.group?.id });
} catch (e) {
bus.emit('toast', { text: e.message || t('common.error'), type: 'error' });
btn.disabled = false;
btn.textContent = 'إنشاء المجموعة';
btn.textContent = t('group.create_btn');
}
});
}
......
......@@ -18,14 +18,14 @@ export async function mountGroupMembers(el, params = {}) {
<div style="display:flex;flex-direction:column;height:100%;">
<div style="padding:12px 16px;background:#0f0f1e;display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;flex:1;">الأعضاء</h2>
${canManage ? `<button class="btn btn-primary" id="add-member-btn" style="min-height:34px;padding:6px 12px;font-size:12px;">+ إضافة</button>` : ''}
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;flex:1;">${t('group.members')}</h2>
${canManage ? `<button class="btn btn-primary" id="add-member-btn" style="min-height:34px;padding:6px 12px;font-size:12px;">+ ${t('group.add')}</button>` : ''}
</div>
<div id="members-list" style="flex:1;overflow-y:auto;padding:12px 16px;">
<div style="text-align:center;padding:24px;color:var(--text-secondary);">${t('common.loading')}</div>
</div>
<div style="padding:12px 16px;border-top:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary w-full" id="leave-btn" style="min-height:40px;color:var(--error);">مغادرة المجموعة</button>
<button class="btn btn-secondary w-full" id="leave-btn" style="min-height:40px;color:var(--error);">${t('group.leave')}</button>
</div>
</div>
`;
......@@ -40,7 +40,7 @@ export async function mountGroupMembers(el, params = {}) {
container.innerHTML = members.map(m => {
const p = m.profiles || {};
const roleLabel = m.role === 'owner' ? '👑 مالك' : m.role === 'admin' ? '⭐ مشرف' : '';
const roleLabel = m.role === 'owner' ? `👑 ${t('group.owner')}` : m.role === 'admin' ? `⭐ ${t('group.admin')}` : '';
const isMe = m.user_id === myId;
const showRemove = canManage && !isMe && m.role !== 'owner';
......@@ -50,11 +50,11 @@ export async function mountGroupMembers(el, params = {}) {
${p.avatar_url ? `<img src="${p.avatar_url}" style="width:40px;height:40px;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 18)}
</div>
<div style="flex:1;cursor:pointer;" data-profile="${m.user_id}">
<div style="font-size:14px;font-weight:500;color:#f8fafc;">${escapeHtml(p.display_name || 'Player')}${isMe ? ' (أنت)' : ''}</div>
<div style="font-size:14px;font-weight:500;color:#f8fafc;">${escapeHtml(p.display_name || 'Player')}${isMe ? ` ${t('common.you')}` : ''}</div>
${roleLabel ? `<div style="font-size:11px;color:#64748b;">${roleLabel}</div>` : ''}
</div>
${p.is_online ? '<div style="width:8px;height:8px;border-radius:50%;background:#34D399;"></div>' : ''}
${showRemove ? `<button class="btn btn-secondary remove-btn" data-id="${m.user_id}" style="min-height:28px;padding:4px 10px;font-size:11px;color:var(--error);">إزالة</button>` : ''}
${showRemove ? `<button class="btn btn-secondary remove-btn" data-id="${m.user_id}" style="min-height:28px;padding:4px 10px;font-size:11px;color:var(--error);">${t('group.remove')}</button>` : ''}
</div>`;
}).join('');
......@@ -72,11 +72,11 @@ export async function mountGroupMembers(el, params = {}) {
// Remove buttons
container.querySelectorAll('.remove-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const confirmed = await modal.confirm('إزالة هذا العضو من المجموعة؟', {
title: 'إزالة عضو',
const confirmed = await modal.confirm(t('group.remove_member'), {
title: t('group.remove'),
icon: '👤',
confirmText: 'إزالة',
cancelText: 'إلغاء',
confirmText: t('group.remove'),
cancelText: t('common.cancel'),
danger: true
});
if (!confirmed) return;
......@@ -106,11 +106,11 @@ export async function mountGroupMembers(el, params = {}) {
// Leave group
el.querySelector('#leave-btn').addEventListener('click', async () => {
const confirmed = await modal.confirm('هل تريد مغادرة المجموعة؟', {
title: 'مغادرة',
const confirmed = await modal.confirm(t('group.leave_confirm'), {
title: t('group.leave'),
icon: '🚪',
confirmText: 'غادر',
cancelText: 'ابقَ',
confirmText: t('group.leave_btn'),
cancelText: t('group.stay'),
danger: true
});
if (!confirmed) return;
......@@ -134,9 +134,9 @@ async function showAddMemberPicker(el, groupId) {
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:flex-end;justify-content:center;';
overlay.innerHTML = `
<div style="background:#1a1a2e;border-radius:20px 20px 0 0;padding:20px;width:100%;max-width:400px;max-height:60vh;display:flex;flex-direction:column;gap:12px;">
<div style="font-size:15px;font-weight:700;color:#f8fafc;text-align:center;">إضافة صديق</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;text-align:center;">${t('mp.add_friend')}</div>
<div id="add-friends-list" style="overflow-y:auto;flex:1;display:flex;flex-direction:column;gap:6px;">
<div style="text-align:center;color:var(--text-secondary);font-size:13px;">جاري التحميل...</div>
<div style="text-align:center;color:var(--text-secondary);font-size:13px;">${t('common.loading')}</div>
</div>
<button class="btn btn-secondary w-full" id="close-add">${t('common.close')}</button>
</div>
......@@ -152,7 +152,7 @@ async function showAddMemberPicker(el, groupId) {
const list = overlay.querySelector('#add-friends-list');
if (!friends.length) {
list.innerHTML = `<div style="text-align:center;color:var(--text-secondary);font-size:13px;">لا يوجد أصدقاء</div>`;
list.innerHTML = `<div style="text-align:center;color:var(--text-secondary);font-size:13px;">${t('social.no_friends')}</div>`;
return;
}
......@@ -162,7 +162,7 @@ async function showAddMemberPicker(el, groupId) {
${f.avatar_url ? `<img src="${f.avatar_url}" style="width:32px;height:32px;object-fit:cover;border-radius:50%;">` : emoji('person', '👤', 14)}
</div>
<span style="flex:1;font-size:13px;color:#f8fafc;">${escapeHtml(f.display_name || 'Player')}</span>
<button class="btn btn-primary add-one-btn" data-id="${f.id}" style="min-height:28px;padding:4px 12px;font-size:11px;">إضافة</button>
<button class="btn btn-primary add-one-btn" data-id="${f.id}" style="min-height:28px;padding:4px 12px;font-size:11px;">${t('group.add')}</button>
</div>
`).join('');
......@@ -172,11 +172,11 @@ async function showAddMemberPicker(el, groupId) {
btn.textContent = '...';
try {
await net.post('groups.php', { action: 'add-member', group_id: groupId, user_id: btn.dataset.id });
btn.textContent = '✓';
btn.textContent = t('common.done');
btn.style.background = 'transparent';
btn.style.color = '#34D399';
} catch (e) {
btn.textContent = e.message || 'خطأ';
btn.textContent = e.message || t('social.error');
btn.style.color = 'var(--error)';
}
});
......
......@@ -9,8 +9,8 @@ export async function mountGroups(el) {
<div style="display:flex;flex-direction:column;height:100%;">
<div style="padding:12px 16px;background:#0f0f1e;display:flex;align-items:center;gap:12px;">
<button class="btn btn-secondary" id="back-btn" style="width:36px;height:36px;padding:0;">←</button>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;flex:1;">المجموعات</h2>
<button class="btn btn-primary" id="create-btn" style="min-height:34px;padding:6px 14px;font-size:12px;">+ إنشاء</button>
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;flex:1;">${t('social.groups_tab')}</h2>
<button class="btn btn-primary" id="create-btn" style="min-height:34px;padding:6px 14px;font-size:12px;">+ ${t('group.create')}</button>
</div>
<div id="groups-list" style="flex:1;overflow-y:auto;padding:12px 16px;">
<div style="text-align:center;padding:24px;color:var(--text-secondary);">${t('common.loading')}</div>
......@@ -30,8 +30,8 @@ export async function mountGroups(el) {
container.innerHTML = `
<div style="text-align:center;padding:48px 24px;color:var(--text-secondary);">
<div style="font-size:40px;margin-bottom:12px;">${emoji('group', '👥', 40)}</div>
<div style="font-size:14px;">لا توجد مجموعات بعد</div>
<div style="font-size:12px;margin-top:4px;">أنشئ مجموعة وادعُ أصدقائك!</div>
<div style="font-size:14px;">${t('group.no_groups')}</div>
<div style="font-size:12px;margin-top:4px;">${t('group.no_groups_hint')}</div>
</div>`;
return;
}
......@@ -43,7 +43,7 @@ export async function mountGroups(el) {
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:14px;font-weight:600;color:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(g.name)}</div>
<div style="font-size:12px;color:#64748b;">${g.member_count || 1} أعضاء • ${g.my_role === 'owner' ? 'مالك' : g.my_role === 'admin' ? 'مشرف' : 'عضو'}</div>
<div style="font-size:12px;color:#64748b;">${g.member_count || 1} ${t('group.members')}${g.my_role === 'owner' ? t('group.owner') : g.my_role === 'admin' ? t('group.admin') : t('profile.member')}</div>
</div>
<span style="color:#64748b;font-size:16px;">←</span>
</div>
......
......@@ -32,7 +32,7 @@ function renderNotifications(el, notifications) {
}
const hasUnread = notifications.some(n => !n.is_read);
list.innerHTML = (hasUnread ? `<button id="mark-all-read" style="margin-bottom:8px;background:none;border:1px solid rgba(255,255,255,0.1);color:#94a3b8;font-size:12px;padding:6px 14px;border-radius:8px;cursor:pointer;font-family:inherit;">تعليم الكل كمقروء</button>` : '') +
list.innerHTML = (hasUnread ? `<button id="mark-all-read" style="margin-bottom:8px;background:none;border:1px solid rgba(255,255,255,0.1);color:#94a3b8;font-size:12px;padding:6px 14px;border-radius:8px;cursor:pointer;font-family:inherit;">${t('chat.mark_all_read')}</button>` : '') +
notifications.map(n => `
<div class="card notif-item" data-id="${n.id}" style="padding:var(--s-3);margin-bottom:var(--s-2);opacity:${n.is_read ? '0.7' : '1'};cursor:pointer;">
<div style="font-size:14px;font-weight:${n.is_read ? '400' : '600'};">${n.title_ar || n.title || ''}</div>
......@@ -71,10 +71,10 @@ function timeAgo(date) {
if (!date) return '';
const diff = Date.now() - new Date(date).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `منذ ${mins} دقيقة`;
if (mins < 1) return t('common.now');
if (mins < 60) return t('common.minutes_ago', { n: mins });
const hours = Math.floor(mins / 60);
if (hours < 24) return `منذ ${hours} ساعة`;
if (hours < 24) return t('common.hours_ago', { n: hours });
const days = Math.floor(hours / 24);
return `منذ ${days} يوم`;
return t('common.days_ago', { n: days });
}
......@@ -14,14 +14,14 @@ export async function mountTournamentsHub(el) {
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;">
<div style="padding:14px 16px 0;background:#0f0f1e;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<h2 style="font-size:20px;font-weight:800;color:#f8fafc;">${emoji('tournament_cup', '🏆', 20)} البطولات</h2>
<h2 style="font-size:20px;font-weight:800;color:#f8fafc;">${emoji('tournament_cup', '🏆', 20)} ${t('tournament.hub_title')}</h2>
</div>
<div id="tour-filters" style="display:flex;gap:6px;overflow-x:auto;padding-bottom:12px;border-bottom:1px solid rgba(255,255,255,0.04);">
<button class="tour-filter active" data-filter="all">الكل</button>
<button class="tour-filter" data-filter="my">بطولاتي</button>
<button class="tour-filter" data-filter="registration">تسجيل مفتوح</button>
<button class="tour-filter" data-filter="in_progress">جارية</button>
<button class="tour-filter" data-filter="completed">منتهية</button>
<button class="tour-filter active" data-filter="all">${t('common.all')}</button>
<button class="tour-filter" data-filter="my">${t('tournament.my_tournaments')}</button>
<button class="tour-filter" data-filter="registration">${t('tournament.registration_open')}</button>
<button class="tour-filter" data-filter="in_progress">${t('tournament.active')}</button>
<button class="tour-filter" data-filter="completed">${t('tournament.completed')}</button>
</div>
</div>
<div id="tour-hub-content" style="flex:1;overflow-y:auto;padding:14px 16px;"></div>
......@@ -92,17 +92,17 @@ async function loadTournaments(el) {
if (tournaments.length === 0) {
const emptyMessages = {
all: 'لا توجد بطولات حالياً',
my: 'لم تسجّل في أي بطولة بعد',
registration: 'لا توجد بطولات مفتوحة للتسجيل',
in_progress: 'لا توجد بطولات جارية',
completed: 'لا توجد بطولات منتهية'
all: t('tournament.no_tournaments'),
my: t('tournament.no_my_tournaments'),
registration: t('tournament.no_open_tournaments'),
in_progress: t('tournament.no_active_tournaments'),
completed: t('tournament.no_completed_tournaments')
};
content.innerHTML = `
<div style="text-align:center;padding:48px 20px;">
<div style="font-size:48px;margin-bottom:12px;opacity:0.4;">${emoji('tournament_cup', '🏆', 48)}</div>
<div style="font-size:15px;font-weight:600;color:#f8fafc;margin-bottom:6px;">${emptyMessages[activeFilter] || emptyMessages.all}</div>
<div style="font-size:12px;color:#64748b;">ستظهر البطولات الجديدة هنا</div>
<div style="font-size:12px;color:#64748b;">${t('tournament.new_tournaments_here')}</div>
</div>
`;
return;
......@@ -120,7 +120,7 @@ async function loadTournaments(el) {
<div class="tour-hub-card ${isRegistered ? 'registered' : ''} ${hasPending ? 'has-pending' : ''}" data-id="${tour.id}">
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px;">
<div style="flex:1;min-width:0;">
<div style="font-size:15px;font-weight:700;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${tour.name || 'بطولة'}</div>
<div style="font-size:15px;font-weight:700;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${tour.name || t('tournament.title')}</div>
<div style="font-size:11px;color:#64748b;margin-top:2px;">${formatGame(tour.game_key)} · ${formatName(tour.format)}</div>
</div>
<span class="tour-status-pill" style="background:${getStatusColor(tour.status)};color:${tour.status === 'in_progress' ? '#000' : '#fff'};">${getStatusLabel(tour.status)}</span>
......@@ -129,19 +129,19 @@ async function loadTournaments(el) {
<div style="display:flex;gap:12px;margin-bottom:10px;">
<div class="tour-stat">
<span class="tour-stat-val" style="color:#3B82F6;">${tour.player_count || 0}<span style="font-size:11px;color:#64748b;">/${tour.max_players || '∞'}</span></span>
<span class="tour-stat-label">لاعبين</span>
<span class="tour-stat-label">${t('tournament.players_label')}</span>
</div>
<div class="tour-stat">
<span class="tour-stat-val" style="color:#E4AC38;">${tour.swiss_rounds || tour.rounds_total || '?'}</span>
<span class="tour-stat-label">جولات</span>
<span class="tour-stat-label">${t('tournament.rounds_label')}</span>
</div>
<div class="tour-stat">
<span class="tour-stat-val" style="color:#10B981;">${formatTimeControl(tour.time_control)}</span>
<span class="tour-stat-label">الوقت</span>
<span class="tour-stat-label">${t('tournament.time_label')}</span>
</div>
${tour.prize_pool_coins ? `<div class="tour-stat">
<span class="tour-stat-val" style="color:#F59E0B;">${tour.prize_pool_coins}</span>
<span class="tour-stat-label">${emoji('coin', '🪙', 10)} جائزة</span>
<span class="tour-stat-label">${emoji('coin', '🪙', 10)} ${t('tournament.prize')}</span>
</div>` : ''}
</div>
......@@ -149,13 +149,13 @@ async function loadTournaments(el) {
${hasPending ? `
<div style="background:rgba(228,172,56,0.1);border:1px solid rgba(228,172,56,0.3);border-radius:10px;padding:10px;display:flex;align-items:center;justify-content:space-between;">
<div style="font-size:12px;color:#E4AC38;font-weight:600;">${emoji('swords', '⚔️', 12)} مباراتك جاهزة — ج${pendingMatch?.round_number || ''} vs ${pendingMatch?.opponent_name || 'خصم'}</div>
<button class="play-now-btn" data-tid="${tour.id}" data-rid="${pendingMatch?.round_id}" data-idx="${pendingMatch?.pairing_index}" style="background:#E4AC38;border:none;border-radius:8px;padding:6px 14px;color:#000;font-weight:700;font-size:11px;cursor:pointer;">العب</button>
<div style="font-size:12px;color:#E4AC38;font-weight:600;">${emoji('swords', '⚔️', 12)} ${t('tournament.match_ready')}${t('tournament.round_short')}${pendingMatch?.round_number || ''} vs ${pendingMatch?.opponent_name || t('tournament.opponent')}</div>
<button class="play-now-btn" data-tid="${tour.id}" data-rid="${pendingMatch?.round_id}" data-idx="${pendingMatch?.pairing_index}" style="background:#E4AC38;border:none;border-radius:8px;padding:6px 14px;color:#000;font-weight:700;font-size:11px;cursor:pointer;">${t('common.play')}</button>
</div>
` : isRegistered ? `
<div style="font-size:11px;color:#34D399;font-weight:600;">${emoji('checkmark', '✓', 11)} مسجّل</div>
<div style="font-size:11px;color:#34D399;font-weight:600;">${emoji('checkmark', '✓', 11)} ${t('tournament.joined')}</div>
` : tour.status === 'registration' ? `
<button class="register-btn" data-tid="${tour.id}" style="width:100%;background:#E4AC38;border:none;border-radius:10px;padding:12px;color:#000;font-weight:700;font-size:13px;cursor:pointer;margin-top:4px;">${emoji('swords', '⚔️', 13)} سجّل الآن${tour.entry_fee_coins ? ' — ' + tour.entry_fee_coins + ' عملة' : ''}</button>
<button class="register-btn" data-tid="${tour.id}" style="width:100%;background:#E4AC38;border:none;border-radius:10px;padding:12px;color:#000;font-weight:700;font-size:13px;cursor:pointer;margin-top:4px;">${emoji('swords', '⚔️', 13)} ${t('tournament.register_now')}${tour.entry_fee_coins ? ' — ' + tour.entry_fee_coins + ' ' + t('common.coins') : ''}</button>
` : ''}
</div>
`;
......@@ -174,16 +174,16 @@ async function loadTournaments(el) {
e.stopPropagation();
audio.play('click');
btn.disabled = true;
btn.textContent = 'جاري التسجيل...';
btn.textContent = t('tournament.registering');
try {
await net.post('tournaments.php', { action: 'register', tournament_id: btn.dataset.tid });
btn.textContent = '✓ تم التسجيل';
btn.textContent = '✓ ' + t('tournament.registered');
btn.style.background = '#34D399';
btn.closest('.tour-hub-card')?.classList.add('registered');
} catch (err) {
btn.textContent = err.message || 'فشل';
btn.textContent = err.message || t('common.failed');
btn.disabled = false;
setTimeout(() => { btn.textContent = 'سجّل الآن'; }, 2000);
setTimeout(() => { btn.textContent = t('tournament.register_now'); }, 2000);
}
});
});
......@@ -214,7 +214,7 @@ async function loadTournaments(el) {
recovered: data.already_exists && data.status === 'in_progress'
});
} catch (err) {
btn.textContent = 'فشل';
btn.textContent = t('common.failed');
btn.disabled = false;
}
});
......@@ -223,8 +223,8 @@ async function loadTournaments(el) {
} catch (e) {
content.innerHTML = `
<div style="text-align:center;padding:32px;color:#ef4444;">
<div style="margin-bottom:8px;">فشل تحميل البطولات</div>
<button class="btn btn-secondary" id="retry-tour" style="font-size:12px;padding:8px 16px;">حاول مرة أخرى</button>
<div style="margin-bottom:8px;">${t('tournament.load_failed')}</div>
<button class="btn btn-secondary" id="retry-tour" style="font-size:12px;padding:8px 16px;">${t('common.retry')}</button>
</div>
`;
content.querySelector('#retry-tour')?.addEventListener('click', () => loadTournaments(el));
......@@ -242,28 +242,28 @@ function getStatusColor(status) {
function getStatusLabel(status) {
switch (status) {
case 'registration': return 'تسجيل مفتوح';
case 'in_progress': return 'جارية';
case 'completed': return 'منتهية';
case 'draft': return 'قريباً';
default: return status || 'قادمة';
case 'registration': return t('tournament.registration_open');
case 'in_progress': return t('tournament.active');
case 'completed': return t('tournament.completed');
case 'draft': return t('tournament.coming_soon');
default: return status || t('tournament.upcoming');
}
}
function formatName(format) {
const names = { swiss: 'سويسري', round_robin: 'دوري', single_elimination: 'خروج المغلوب', double_elimination: 'خروج مزدوج', arena: 'أرينا', group_stage: 'مجموعات' };
const names = { swiss: t('tournament.format_swiss'), round_robin: t('tournament.format_round_robin'), single_elimination: t('tournament.format_single_elim'), double_elimination: t('tournament.format_double_elim'), arena: t('tournament.format_arena'), group_stage: t('tournament.format_group_stage') };
return names[format] || format || '';
}
function formatGame(key) {
const games = { chess: 'شطرنج', ludo: 'لودو', domino: 'دومينو' };
return games[key] || key || 'شطرنج';
const games = { chess: t('game.chess'), ludo: t('game.ludo'), domino: t('game.domino') };
return games[key] || key || t('game.chess');
}
function formatTimeControl(tc) {
if (!tc) return '?';
const parts = tc.split('_');
if (parts.length >= 2) return parts[1] + (parts[2] && parts[2] !== '0' ? '+' + parts[2] : '') + 'د';
if (parts.length >= 2) return parts[1] + (parts[2] && parts[2] !== '0' ? '+' + parts[2] : '') + t('common.minutes_abbr');
return tc;
}
......@@ -275,12 +275,12 @@ function formatDate(dateStr) {
if (diff > 0 && diff < 86400000) {
const hours = Math.floor(diff / 3600000);
const mins = Math.floor((diff % 3600000) / 60000);
if (hours > 0) return `تبدأ بعد ${hours} ساعة`;
return `تبدأ بعد ${mins} دقيقة`;
if (hours > 0) return t('tournament.starts_in_hours', { n: hours });
return t('tournament.starts_in_minutes', { n: mins });
}
if (diff > 0 && diff < 7 * 86400000) {
const days = Math.ceil(diff / 86400000);
return `بعد ${days} يوم`;
return t('tournament.in_days', { n: days });
}
return d.toLocaleDateString('ar', { day: 'numeric', month: 'short', year: d.getFullYear() !== now.getFullYear() ? 'numeric' : undefined });
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment