38ac8b06d1
Adds ~100 new purchasable gifts that are LOCKED until a level/rating gate is met, then buyable with coins — value scales with the gate: - 45 gift avatars (types.ts), 35 gift titles + 20 gift card backs (gamification.ts), all reusing existing renderers. Tier (1-5) encoded in the id (-t<n>-). - Gate model: GIFT_TIERS (shared) → reqLevel/reqRating on AvatarDef/TitleDef/ CardBackDef + ShopItem. Tiers: t1 free, t2 Lv10, t3 Lv20, t4 Lv35, t5 Rating1700. - Shop UI: locked cards dim + show the requirement (Lock + "Level 20"), buy disabled until met; mock buyItem enforces it offline. - Server enforces generically — ProfileService parses the tier from the id and checks the player's level/rating (no 100-entry mirror). Mirrors GIFT_TIERS. - i18n shop.reqLevel/reqRating (fa+en). Verified: tsc + sim + next build + dotnet build all pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
743 lines
25 KiB
TypeScript
743 lines
25 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
|
|
export type Locale = "fa" | "en";
|
|
|
|
type Dict = Record<string, string>;
|
|
|
|
const fa: Dict = {
|
|
"app.title": "برگ وسط",
|
|
"app.subtitle": "بازی حکم آنلاین",
|
|
"app.tagline": "تجربهای لوکس از بازی حکم، با حریفهای هوشمند",
|
|
|
|
"home.play": "شروع بازی",
|
|
"home.continue": "ادامه بازی",
|
|
"home.vsAI": "بازی با کامپیوتر",
|
|
"home.target": "امتیاز برد",
|
|
"home.targetHint": "تعداد دست برای برنده شدن",
|
|
"home.yourName": "نام شما",
|
|
"home.start": "بزن بریم",
|
|
"home.howTo": "آموزش بازی",
|
|
"home.lang": "English",
|
|
"home.onlineCount": "{n} نفر آنلاین",
|
|
|
|
"resume.title": "بازی در جریان",
|
|
"resume.cta": "بازگشت به بازی",
|
|
"resume.matchEnded": "بازی به پایان رسید",
|
|
"resume.matchEndedBody": "نتیجه و جایزه را ببینید",
|
|
"celebrate.purchased": "خرید با موفقیت انجام شد!",
|
|
|
|
"achv.title": "دستاوردها",
|
|
"achv.unlocked": "باز شده",
|
|
"achv.coinsEarned": "سکه کسبشده",
|
|
"achv.viewAll": "همه",
|
|
"achv.unlocksSticker": "استیکر",
|
|
"lobby.chooseLeague": "لیگ را انتخاب کنید",
|
|
"lobby.lvl": "سطح",
|
|
"profile.uploadLocked": "آپلود عکس از سطح ۲۵ فعال میشود",
|
|
|
|
"forfeit.title": "تسلیم",
|
|
"forfeit.ask": "از این بازی تسلیم میشوید؟",
|
|
"forfeit.teammateAsks": "{name} میخواهد تسلیم شود. موافقید؟",
|
|
"forfeit.rule": "با تسلیم، دو برابر سکهٔ ورودی را از دست میدهید و هیچ امتیاز تجربهای نمیگیرید.",
|
|
"forfeit.confirm": "تسلیم",
|
|
"forfeit.keepPlaying": "ادامه میدهم",
|
|
"match.players": "بازیکنان",
|
|
"match.you": "شما",
|
|
"match.bot": "ربات",
|
|
"match.addFriend": "افزودن",
|
|
"match.sent": "ارسال شد",
|
|
|
|
"seat.you": "شما",
|
|
"team.us": "ما",
|
|
"team.them": "حریف",
|
|
"team.0": "تیم ما",
|
|
"team.1": "تیم حریف",
|
|
|
|
"hakem.title": "تعیین حاکم",
|
|
"hakem.desc": "ورق میچینیم تا اولین آس بیاید",
|
|
"hakem.is": "حاکم: {name}",
|
|
|
|
"trump.title": "حکم را انتخاب کنید",
|
|
"trump.desc": "شما حاکم هستید — خال حکم را تعیین کنید",
|
|
"trump.waiting": "{name} در حال انتخاب حکم است…",
|
|
"trump.label": "حکم",
|
|
|
|
"turn.you": "نوبت شماست",
|
|
"turn.other": "نوبت {name}",
|
|
|
|
"keys.title": "میانبرهای صفحهکلید",
|
|
"keys.play": "بازی کردن کارت",
|
|
"keys.first": "اولین کارت مجاز",
|
|
"keys.trump": "انتخاب حکم",
|
|
"keys.mute": "قطع صدا",
|
|
"keys.forfeit": "تسلیم",
|
|
"keys.quit": "خروج",
|
|
|
|
"trick.wins": "{name} دست را برد",
|
|
|
|
"round.over": "پایان دست",
|
|
"round.kot": "کُت! ",
|
|
"round.won": "{team} برنده شد",
|
|
"round.score": "امتیاز: {us} - {them}",
|
|
"round.next": "دست بعد…",
|
|
|
|
"match.over": "پایان بازی",
|
|
"match.youWin": "شما بردید! 🏆",
|
|
"match.youLose": "این بار باختید",
|
|
"match.again": "بازی دوباره",
|
|
"match.menu": "منوی اصلی",
|
|
|
|
"score.title": "امتیاز",
|
|
"score.tricks": "دستها",
|
|
"hud.menu": "منو",
|
|
"hud.quit": "خروج",
|
|
|
|
"menu.vsComputer": "بازی با کامپیوتر",
|
|
"menu.vsComputerDesc": "تمرین با حریفهای هوشمند",
|
|
"speed.label": "سریع",
|
|
"speed.normal": "عادی",
|
|
"speed.desc": "حالت سریع: نوبتهای کوتاهتر و بازی برقآسا",
|
|
"menu.online": "بازی آنلاین",
|
|
"menu.onlineDesc": "با دوستان یا بازیکنهای واقعی",
|
|
"menu.profile": "پروفایل",
|
|
"menu.friends": "دوستان",
|
|
"menu.leaderboard": "جدول امتیازات",
|
|
"menu.shop": "فروشگاه",
|
|
"menu.signIn": "ورود / ثبتنام",
|
|
"menu.guest": "مهمان",
|
|
"menu.signOut": "خروج از حساب",
|
|
|
|
"common.back": "بازگشت",
|
|
"common.coins": "سکه",
|
|
"common.level": "سطح",
|
|
"common.rating": "امتیاز",
|
|
"common.save": "ذخیره",
|
|
"common.cancel": "انصراف",
|
|
"common.confirm": "تأیید",
|
|
"common.soon": "بهزودی",
|
|
"common.copy": "کپی",
|
|
"common.copied": "کپی شد",
|
|
"common.free": "رایگان",
|
|
"common.yes": "بله",
|
|
"common.no": "خیر",
|
|
|
|
"buy.title": "خرید سکه",
|
|
"buy.note": "پرداخت امن — درگاه پرداخت ایرانی بهزودی اضافه میشود",
|
|
"buy.toman": "تومان",
|
|
"buy.bonus": "هدیه",
|
|
"buy.redirecting": "صفحهٔ پرداخت در تب جدید باز شد. پس از پرداخت، سکهها بهصورت خودکار اضافه میشوند.",
|
|
"buy.failed": "پرداخت در دسترس نیست. بعداً دوباره تلاش کنید.",
|
|
"buy.popular": "محبوب",
|
|
"buy.best": "بهترین",
|
|
"buy.starter": "شروع",
|
|
|
|
"lobby.entry": "ورودی",
|
|
"lobby.free": "رایگان",
|
|
"lobby.needCoins": "سکه کافی نیست — شارژ کنید",
|
|
"friends.removeQ": "این دوست حذف شود؟",
|
|
|
|
"chat.title": "گفتگو",
|
|
"chat.placeholder": "پیام بنویسید…",
|
|
"chat.send": "ارسال",
|
|
"chat.empty": "گفتگو را شروع کنید",
|
|
"friends.message": "پیام",
|
|
|
|
"profile.title": "پروفایل",
|
|
"profile.stats": "آمار",
|
|
"profile.games": "بازیها",
|
|
"profile.wins": "بردها",
|
|
"profile.winrate": "درصد برد",
|
|
"profile.kots": "کُتها",
|
|
"profile.streak": "بهترین نوار",
|
|
"profile.achievements": "دستاوردها",
|
|
"profile.sendRequest": "افزودن دوست",
|
|
"profile.requestSent": "درخواست ارسال شد",
|
|
"profile.alreadyFriend": "دوست شماست",
|
|
"profile.memberSince": "عضو از",
|
|
"profile.editName": "ویرایش نام",
|
|
"profile.chooseAvatar": "انتخاب آواتار",
|
|
|
|
"friends.title": "دوستان",
|
|
"friends.add": "افزودن",
|
|
"friends.addPlaceholder": "نام کاربری یا شماره",
|
|
"friends.requests": "درخواستها",
|
|
"friends.online": "آنلاین",
|
|
"friends.offline": "آفلاین",
|
|
"friends.inGame": "در حال بازی",
|
|
"friends.invite": "دعوت",
|
|
"friends.accept": "قبول",
|
|
"friends.decline": "رد",
|
|
"friends.remove": "حذف",
|
|
"friends.empty": "هنوز دوستی ندارید",
|
|
|
|
"social.title": "اجتماعی",
|
|
"social.tabFriends": "دوستان",
|
|
"social.tabDiscover": "یافتن",
|
|
"social.tabMessages": "پیامها",
|
|
"discover.searchPlaceholder": "جستجوی بازیکن با نام…",
|
|
"discover.results": "نتایج جستجو",
|
|
"discover.suggested": "بازیکنان پیشنهادی",
|
|
"discover.noResults": "بازیکنی پیدا نشد",
|
|
"discover.friend": "دوست",
|
|
"messages.empty": "هنوز گفتگویی ندارید",
|
|
"messages.you": "شما",
|
|
"time.now": "همین حالا",
|
|
"time.min": "{n} دقیقه پیش",
|
|
"time.hour": "{n} ساعت پیش",
|
|
"time.day": "{n} روز پیش",
|
|
"common.retry": "تلاش دوباره",
|
|
|
|
"lobby.title": "بازی آنلاین",
|
|
"lobby.createRoom": "ساخت اتاق خصوصی",
|
|
"lobby.createDesc": "همتیمی و حریفها را خودتان انتخاب کنید",
|
|
"lobby.random": "بازی رتبهای",
|
|
"lobby.randomDesc": "حریف تصادفی و کسب امتیاز و سکه",
|
|
|
|
"room.title": "اتاق بازی",
|
|
"room.code": "کد اتاق",
|
|
"room.partner": "همتیمی",
|
|
"room.opponents": "حریفها",
|
|
"room.choosePartner": "انتخاب همتیمی",
|
|
"room.invite": "دعوت دوست",
|
|
"room.addBot": "ربات",
|
|
"room.empty": "خالی",
|
|
"room.waiting": "در انتظار…",
|
|
"room.start": "شروع بازی",
|
|
"room.stake": "سکه ورودی",
|
|
"room.leave": "ترک اتاق",
|
|
"room.pickFriend": "یک دوست را انتخاب کنید",
|
|
|
|
"mm.title": "جستجوی بازیکن",
|
|
"mm.searching": "در حال یافتن حریف…",
|
|
"mm.found": "بازیکنان پیدا شدند!",
|
|
"mm.ready": "آماده شروع",
|
|
"mm.fillHint": "اگر بازیکن آنلاینی پیدا نشود، رباتها جایگزین میشوند",
|
|
"intro.found": "بازیکنان آمادهاند!",
|
|
"intro.getReady": "بازی در حال شروع است…",
|
|
"intro.go": "شروع!",
|
|
"mm.cancel": "لغو",
|
|
"mm.start": "ورود به بازی",
|
|
|
|
"lead.title": "جدول امتیازات",
|
|
"lead.rank": "رتبه",
|
|
|
|
"shop.title": "فروشگاه",
|
|
"shop.buy": "خرید",
|
|
"shop.owned": "موجود",
|
|
"shop.luxury": "ویژه",
|
|
"shop.avatars": "آواتارها",
|
|
"shop.themes": "تمها",
|
|
"shop.notEnough": "سکه کافی نیست",
|
|
|
|
"auth.title": "ورود به حکم",
|
|
"auth.subtitle": "برای بازی آنلاین وارد شوید",
|
|
"auth.phone": "موبایل",
|
|
"auth.email": "ایمیل",
|
|
"auth.phoneLabel": "شماره موبایل",
|
|
"auth.phonePlaceholder": "۰۹۱۲۳۴۵۶۷۸۹",
|
|
"auth.sendCode": "ارسال کد",
|
|
"auth.codeLabel": "کد تأیید",
|
|
"auth.codePlaceholder": "کد ۴ رقمی",
|
|
"auth.verify": "تأیید و ورود",
|
|
"auth.devCode": "کد آزمایشی: {code}",
|
|
"auth.emailLabel": "ایمیل",
|
|
"auth.passLabel": "رمز عبور",
|
|
"auth.nameLabel": "نام نمایشی",
|
|
"auth.signIn": "ورود",
|
|
"auth.signUp": "ثبتنام",
|
|
"auth.google": "ورود با گوگل",
|
|
"auth.toggleSignup": "حساب ندارید؟ ثبتنام کنید",
|
|
"auth.toggleSignin": "حساب دارید؟ وارد شوید",
|
|
"auth.invalidCode": "کد نادرست است",
|
|
"auth.otherSoon": "سایر روشهای ورود بهزودی فعال میشوند",
|
|
|
|
"reward.title": "پاداش بازی",
|
|
"reward.rating": "امتیاز رتبهای",
|
|
"reward.coins": "سکه",
|
|
"reward.xp": "تجربه",
|
|
"reward.levelUp": "ارتقای سطح!",
|
|
"reward.promoted": "ارتقای لیگ!",
|
|
"reward.demoted": "سقوط لیگ",
|
|
"reward.newAchievement": "دستاورد جدید",
|
|
"reward.stickerUnlocked": "بستهٔ استیکر باز شد",
|
|
"reward.continue": "ادامه",
|
|
"reward.win": "بردید! 🏆",
|
|
"reward.lose": "باختید",
|
|
|
|
"daily.title": "پاداش روزانه",
|
|
"daily.day": "روز {n}",
|
|
"daily.claim": "دریافت",
|
|
"daily.claimed": "دریافت شد",
|
|
"daily.come": "فردا برگردید",
|
|
"daily.special": "پاداش ویژه",
|
|
|
|
"rank.label": "لیگ",
|
|
|
|
"dc.waiting": "{name} قطع شد — منتظر بازگشت ({s})",
|
|
|
|
"profile.titleLabel": "عنوان",
|
|
"profile.cardStyleLabel": "طرح کارت",
|
|
"profile.image": "تصویر پروفایل",
|
|
"profile.upload": "آپلود تصویر",
|
|
"profile.plan": "اشتراک",
|
|
|
|
"plan.pro": "ویژه",
|
|
"plan.free": "رایگان",
|
|
"plan.upgrade": "ارتقا به ویژه",
|
|
"plan.proDesc": "بازی بدون صف، در هر زمان",
|
|
"plan.active": "اشتراک ویژه فعال است",
|
|
|
|
"queue.title": "در صف بازی",
|
|
"queue.busy": "سرور شلوغ است",
|
|
"queue.position": "نفر {n} در صف",
|
|
"queue.skip": "با اشتراک ویژه بدون صف وارد شوید",
|
|
"queue.upgrade": "ورود سریع (ویژه)",
|
|
|
|
"shop.cardstyles": "طرح کارتها",
|
|
"shop.reactions": "بسته شکلکها",
|
|
"shop.stickers": "بسته استیکرها",
|
|
"shop.titles": "عناوین",
|
|
"shop.titlesHint": "عنوان شما زیر نامتان در بازی و لیستها نمایش داده میشود",
|
|
"shop.xp": "امتیاز تجربه (XP)",
|
|
"shop.xpHint": "افزایش سریع سطح — XP گران است",
|
|
"shop.includes": "شامل",
|
|
"shop.reqLevel": "سطح",
|
|
"shop.reqRating": "امتیاز",
|
|
"reward.newTitle": "عنوان جدید",
|
|
|
|
"reactions.title": "شکلک",
|
|
"stickers.title": "استیکر",
|
|
|
|
"notif.title": "اعلانها",
|
|
"notif.empty": "اعلانی ندارید",
|
|
|
|
"settings.audio": "تنظیمات صدا",
|
|
"settings.sound": "افکت صدا",
|
|
"settings.music": "موسیقی پسزمینه",
|
|
"settings.musicStyle": "سبک موسیقی",
|
|
"settings.trackSantoor": "سنتی (سنتور)",
|
|
"settings.trackPlayful": "شاد",
|
|
|
|
"profile.cardFront": "روی کارت",
|
|
"profile.cardBack": "پشت کارت",
|
|
"profile.social": "اجتماعی و ارتباط",
|
|
"profile.gender": "جنسیت",
|
|
"profile.genderNone": "نامشخص",
|
|
"profile.socialLinks": "شبکههای اجتماعی",
|
|
"profile.socialsVisibility": "نمایش شبکهها به",
|
|
"profile.visPublic": "همه",
|
|
"profile.visFriends": "دوستان",
|
|
"profile.visHidden": "هیچکس",
|
|
"profile.socialsHint": "میتوانید نمایش پیجهایتان را عمومی، فقط برای دوستان یا غیرفعال کنید.",
|
|
"profile.saveLinks": "ذخیره شبکهها",
|
|
"profile.saved": "ذخیره شد",
|
|
"shop.cardfronts": "روی کارتها",
|
|
"shop.cardbacks": "پشت کارتها",
|
|
};
|
|
|
|
const en: Dict = {
|
|
"app.title": "Barg-e Vasat",
|
|
"app.subtitle": "Online Hokm",
|
|
"app.tagline": "A luxury Hokm experience with smart opponents",
|
|
|
|
"home.play": "Play",
|
|
"home.continue": "Continue",
|
|
"home.vsAI": "Play vs Computer",
|
|
"home.target": "Target score",
|
|
"home.targetHint": "Rounds needed to win",
|
|
"home.yourName": "Your name",
|
|
"home.start": "Let's go",
|
|
"home.howTo": "How to play",
|
|
"home.lang": "فارسی",
|
|
"home.onlineCount": "{n} players online",
|
|
|
|
"resume.title": "Game in progress",
|
|
"resume.cta": "Return to game",
|
|
"resume.matchEnded": "Match ended",
|
|
"resume.matchEndedBody": "See the result and reward",
|
|
"celebrate.purchased": "Purchase complete!",
|
|
|
|
"achv.title": "Achievements",
|
|
"achv.unlocked": "Unlocked",
|
|
"achv.coinsEarned": "Coins earned",
|
|
"achv.viewAll": "All",
|
|
"achv.unlocksSticker": "Sticker",
|
|
"lobby.chooseLeague": "Choose a league",
|
|
"lobby.lvl": "Lvl",
|
|
"profile.uploadLocked": "Photo upload unlocks at level 25",
|
|
|
|
"forfeit.title": "Forfeit",
|
|
"forfeit.ask": "Surrender this match?",
|
|
"forfeit.teammateAsks": "{name} wants to forfeit. Agree?",
|
|
"forfeit.rule": "Forfeiting costs double your entry coins and earns no XP.",
|
|
"forfeit.confirm": "Forfeit",
|
|
"forfeit.keepPlaying": "Keep playing",
|
|
"match.players": "Players",
|
|
"match.you": "You",
|
|
"match.bot": "Bot",
|
|
"match.addFriend": "Add",
|
|
"match.sent": "Sent",
|
|
|
|
"intro.found": "Players ready!",
|
|
"intro.getReady": "The game is starting…",
|
|
"intro.go": "GO!",
|
|
|
|
"seat.you": "You",
|
|
"team.us": "Us",
|
|
"team.them": "Them",
|
|
"team.0": "Our team",
|
|
"team.1": "Their team",
|
|
|
|
"hakem.title": "Choosing the Hakem",
|
|
"hakem.desc": "Dealing face-up until the first Ace",
|
|
"hakem.is": "Hakem: {name}",
|
|
|
|
"trump.title": "Choose the trump",
|
|
"trump.desc": "You are the Hakem — pick the trump suit",
|
|
"trump.waiting": "{name} is choosing trump…",
|
|
"trump.label": "Trump",
|
|
|
|
"turn.you": "Your turn",
|
|
"turn.other": "{name}'s turn",
|
|
|
|
"keys.title": "Keyboard shortcuts",
|
|
"keys.play": "Play a card",
|
|
"keys.first": "First legal card",
|
|
"keys.trump": "Choose trump",
|
|
"keys.mute": "Mute",
|
|
"keys.forfeit": "Forfeit",
|
|
"keys.quit": "Quit",
|
|
|
|
"trick.wins": "{name} wins the trick",
|
|
|
|
"round.over": "Round over",
|
|
"round.kot": "Kot! ",
|
|
"round.won": "{team} wins",
|
|
"round.score": "Score: {us} - {them}",
|
|
"round.next": "Next round…",
|
|
|
|
"match.over": "Game over",
|
|
"match.youWin": "You win! 🏆",
|
|
"match.youLose": "You lost this time",
|
|
"match.again": "Play again",
|
|
"match.menu": "Main menu",
|
|
|
|
"score.title": "Score",
|
|
"score.tricks": "Tricks",
|
|
"hud.menu": "Menu",
|
|
"hud.quit": "Quit",
|
|
|
|
"menu.vsComputer": "Play vs Computer",
|
|
"menu.vsComputerDesc": "Practice against smart bots",
|
|
"speed.label": "Speed",
|
|
"speed.normal": "Normal",
|
|
"speed.desc": "Blitz mode: short turns, lightning-fast match",
|
|
"menu.online": "Play Online",
|
|
"menu.onlineDesc": "With friends or real players",
|
|
"menu.profile": "Profile",
|
|
"menu.friends": "Friends",
|
|
"menu.leaderboard": "Leaderboard",
|
|
"menu.shop": "Shop",
|
|
"menu.signIn": "Sign in / Sign up",
|
|
"menu.guest": "Guest",
|
|
"menu.signOut": "Sign out",
|
|
|
|
"common.back": "Back",
|
|
"common.coins": "Coins",
|
|
"common.level": "Level",
|
|
"common.rating": "Rating",
|
|
"common.save": "Save",
|
|
"common.cancel": "Cancel",
|
|
"common.confirm": "Confirm",
|
|
"common.soon": "Coming soon",
|
|
"common.copy": "Copy",
|
|
"common.copied": "Copied",
|
|
"common.free": "Free",
|
|
"common.yes": "Yes",
|
|
"common.no": "No",
|
|
|
|
"buy.title": "Buy Coins",
|
|
"buy.note": "Secure payment — Iranian gateway coming soon",
|
|
"buy.toman": "Toman",
|
|
"buy.bonus": "bonus",
|
|
"buy.redirecting": "Payment opened in a new tab. Your coins will be added automatically once you pay.",
|
|
"buy.failed": "Payment unavailable. Please try again later.",
|
|
"buy.popular": "Popular",
|
|
"buy.best": "Best value",
|
|
"buy.starter": "Starter",
|
|
|
|
"lobby.entry": "Entry",
|
|
"lobby.free": "Free",
|
|
"lobby.needCoins": "Not enough coins — top up",
|
|
"friends.removeQ": "Remove this friend?",
|
|
|
|
"chat.title": "Chat",
|
|
"chat.placeholder": "Type a message…",
|
|
"chat.send": "Send",
|
|
"chat.empty": "Start the conversation",
|
|
"friends.message": "Message",
|
|
|
|
"profile.title": "Profile",
|
|
"profile.stats": "Stats",
|
|
"profile.games": "Games",
|
|
"profile.wins": "Wins",
|
|
"profile.winrate": "Win rate",
|
|
"profile.kots": "Kots",
|
|
"profile.streak": "Best streak",
|
|
"profile.achievements": "Achievements",
|
|
"profile.sendRequest": "Add friend",
|
|
"profile.requestSent": "Request sent",
|
|
"profile.alreadyFriend": "Your friend",
|
|
"profile.memberSince": "Member since",
|
|
"profile.editName": "Edit name",
|
|
"profile.chooseAvatar": "Choose avatar",
|
|
|
|
"friends.title": "Friends",
|
|
"friends.add": "Add",
|
|
"friends.addPlaceholder": "Username or phone",
|
|
"friends.requests": "Requests",
|
|
"friends.online": "Online",
|
|
"friends.offline": "Offline",
|
|
"friends.inGame": "In game",
|
|
"friends.invite": "Invite",
|
|
"friends.accept": "Accept",
|
|
"friends.decline": "Decline",
|
|
"friends.remove": "Remove",
|
|
"friends.empty": "No friends yet",
|
|
|
|
"social.title": "Social",
|
|
"social.tabFriends": "Friends",
|
|
"social.tabDiscover": "Discover",
|
|
"social.tabMessages": "Messages",
|
|
"discover.searchPlaceholder": "Search players by name…",
|
|
"discover.results": "Search results",
|
|
"discover.suggested": "Suggested players",
|
|
"discover.noResults": "No players found",
|
|
"discover.friend": "Friend",
|
|
"messages.empty": "No conversations yet",
|
|
"messages.you": "You",
|
|
"time.now": "now",
|
|
"time.min": "{n}m ago",
|
|
"time.hour": "{n}h ago",
|
|
"time.day": "{n}d ago",
|
|
"common.retry": "Retry",
|
|
|
|
"lobby.title": "Play Online",
|
|
"lobby.createRoom": "Create private room",
|
|
"lobby.createDesc": "Choose your partner and opponents",
|
|
"lobby.random": "Ranked match",
|
|
"lobby.randomDesc": "Random opponents, earn rating & coins",
|
|
|
|
"room.title": "Game Room",
|
|
"room.code": "Room code",
|
|
"room.partner": "Partner",
|
|
"room.opponents": "Opponents",
|
|
"room.choosePartner": "Choose partner",
|
|
"room.invite": "Invite friend",
|
|
"room.addBot": "Bot",
|
|
"room.empty": "Empty",
|
|
"room.waiting": "Waiting…",
|
|
"room.start": "Start game",
|
|
"room.stake": "Entry coins",
|
|
"room.leave": "Leave room",
|
|
"room.pickFriend": "Pick a friend",
|
|
|
|
"mm.title": "Finding players",
|
|
"mm.searching": "Searching for opponents…",
|
|
"mm.found": "Players found!",
|
|
"mm.ready": "Ready to start",
|
|
"mm.fillHint": "If no online players are found, bots will fill in",
|
|
"mm.cancel": "Cancel",
|
|
"mm.start": "Enter game",
|
|
|
|
"lead.title": "Leaderboard",
|
|
"lead.rank": "Rank",
|
|
|
|
"shop.title": "Shop",
|
|
"shop.buy": "Buy",
|
|
"shop.owned": "Owned",
|
|
"shop.luxury": "Luxury",
|
|
"shop.avatars": "Avatars",
|
|
"shop.themes": "Themes",
|
|
"shop.notEnough": "Not enough coins",
|
|
|
|
"auth.title": "Sign in to Hokm",
|
|
"auth.subtitle": "Sign in to play online",
|
|
"auth.phone": "Phone",
|
|
"auth.email": "Email",
|
|
"auth.phoneLabel": "Mobile number",
|
|
"auth.phonePlaceholder": "0912 345 6789",
|
|
"auth.sendCode": "Send code",
|
|
"auth.codeLabel": "Verification code",
|
|
"auth.codePlaceholder": "4-digit code",
|
|
"auth.verify": "Verify & sign in",
|
|
"auth.devCode": "Dev code: {code}",
|
|
"auth.emailLabel": "Email",
|
|
"auth.passLabel": "Password",
|
|
"auth.nameLabel": "Display name",
|
|
"auth.signIn": "Sign in",
|
|
"auth.signUp": "Sign up",
|
|
"auth.google": "Continue with Google",
|
|
"auth.toggleSignup": "No account? Sign up",
|
|
"auth.toggleSignin": "Have an account? Sign in",
|
|
"auth.invalidCode": "Invalid code",
|
|
"auth.otherSoon": "Other sign-in methods coming soon",
|
|
|
|
"reward.title": "Match rewards",
|
|
"reward.rating": "Rating",
|
|
"reward.coins": "Coins",
|
|
"reward.xp": "XP",
|
|
"reward.levelUp": "Level up!",
|
|
"reward.promoted": "Promoted!",
|
|
"reward.demoted": "Demoted",
|
|
"reward.newAchievement": "New achievement",
|
|
"reward.stickerUnlocked": "Sticker pack unlocked",
|
|
"reward.continue": "Continue",
|
|
"reward.win": "You won! 🏆",
|
|
"reward.lose": "You lost",
|
|
|
|
"daily.title": "Daily reward",
|
|
"daily.day": "Day {n}",
|
|
"daily.claim": "Claim",
|
|
"daily.claimed": "Claimed",
|
|
"daily.come": "Come back tomorrow",
|
|
"daily.special": "Special Reward",
|
|
|
|
"rank.label": "League",
|
|
|
|
"dc.waiting": "{name} disconnected — waiting ({s})",
|
|
|
|
"profile.titleLabel": "Title",
|
|
"profile.cardStyleLabel": "Card style",
|
|
"profile.image": "Profile image",
|
|
"profile.upload": "Upload image",
|
|
"profile.plan": "Plan",
|
|
|
|
"plan.pro": "Pro",
|
|
"plan.free": "Free",
|
|
"plan.upgrade": "Upgrade to Pro",
|
|
"plan.proDesc": "Skip the queue, play anytime",
|
|
"plan.active": "Pro plan active",
|
|
|
|
"queue.title": "In queue",
|
|
"queue.busy": "Server is busy",
|
|
"queue.position": "{n} in line",
|
|
"queue.skip": "Go Pro to skip the queue",
|
|
"queue.upgrade": "Skip queue (Pro)",
|
|
|
|
"shop.cardstyles": "Card styles",
|
|
"shop.reactions": "Reaction packs",
|
|
"shop.stickers": "Sticker packs",
|
|
"shop.titles": "Titles",
|
|
"shop.titlesHint": "Your title shows under your name in games & lists",
|
|
"shop.xp": "XP packs",
|
|
"shop.xpHint": "Level up faster — XP is expensive",
|
|
"shop.includes": "Includes",
|
|
"shop.reqLevel": "Level",
|
|
"shop.reqRating": "Rating",
|
|
"reward.newTitle": "New title",
|
|
|
|
"reactions.title": "Emoji",
|
|
"stickers.title": "Stickers",
|
|
|
|
"notif.title": "Notifications",
|
|
"notif.empty": "No notifications yet",
|
|
|
|
"settings.audio": "Audio",
|
|
"settings.sound": "Sound effects",
|
|
"settings.music": "Background music",
|
|
"settings.musicStyle": "Music style",
|
|
"settings.trackSantoor": "Traditional (Santoor)",
|
|
"settings.trackPlayful": "Playful",
|
|
|
|
"profile.cardFront": "Card front",
|
|
"profile.cardBack": "Card back",
|
|
"profile.social": "Social & contact",
|
|
"profile.gender": "Gender",
|
|
"profile.genderNone": "Unspecified",
|
|
"profile.socialLinks": "Social media",
|
|
"profile.socialsVisibility": "Show socials to",
|
|
"profile.visPublic": "Everyone",
|
|
"profile.visFriends": "Friends",
|
|
"profile.visHidden": "Nobody",
|
|
"profile.socialsHint": "Choose who can see your social pages: everyone, only friends, or nobody.",
|
|
"profile.saveLinks": "Save links",
|
|
"profile.saved": "Saved",
|
|
"shop.cardfronts": "Card fronts",
|
|
"shop.cardbacks": "Card backs",
|
|
};
|
|
|
|
const DICTS: Record<Locale, Dict> = { fa, en };
|
|
|
|
interface I18nValue {
|
|
locale: Locale;
|
|
dir: "rtl" | "ltr";
|
|
t: (key: string, vars?: Record<string, string | number>) => string;
|
|
setLocale: (l: Locale) => void;
|
|
toggle: () => void;
|
|
}
|
|
|
|
const I18nContext = createContext<I18nValue | null>(null);
|
|
|
|
export function I18nProvider({ children }: { children: React.ReactNode }) {
|
|
const [locale, setLocaleState] = useState<Locale>("fa");
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem("hokm.locale") as Locale | null;
|
|
if (saved === "fa" || saved === "en") setLocaleState(saved);
|
|
}, []);
|
|
|
|
const setLocale = useCallback((l: Locale) => {
|
|
setLocaleState(l);
|
|
localStorage.setItem("hokm.locale", l);
|
|
}, []);
|
|
|
|
const dir: "rtl" | "ltr" = locale === "fa" ? "rtl" : "ltr";
|
|
|
|
useEffect(() => {
|
|
document.documentElement.lang = locale;
|
|
document.documentElement.dir = dir;
|
|
}, [locale, dir]);
|
|
|
|
const t = useCallback(
|
|
(key: string, vars?: Record<string, string | number>) => {
|
|
let str = DICTS[locale][key] ?? DICTS.en[key] ?? key;
|
|
if (vars) {
|
|
for (const [k, v] of Object.entries(vars)) {
|
|
str = str.replace(new RegExp(`\\{${k}\\}`, "g"), String(v));
|
|
}
|
|
}
|
|
return str;
|
|
},
|
|
[locale]
|
|
);
|
|
|
|
const value = useMemo<I18nValue>(
|
|
() => ({
|
|
locale,
|
|
dir,
|
|
t,
|
|
setLocale,
|
|
toggle: () => setLocale(locale === "fa" ? "en" : "fa"),
|
|
}),
|
|
[locale, dir, t, setLocale]
|
|
);
|
|
|
|
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
|
|
}
|
|
|
|
export function useI18n(): I18nValue {
|
|
const ctx = useContext(I18nContext);
|
|
if (!ctx) throw new Error("useI18n must be used within I18nProvider");
|
|
return ctx;
|
|
}
|