import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; export type Language = "en" | "zh" | "ru"; export type TranslationParams = Record; export type TFunction = (key: string, params?: TranslationParams) => string; const storageKey = "telesrv.admin.lang"; const translations: Record> = { en: { "app.adminConsole": "Admin Console", "app.localAccess": "Local access", "app.title": "telesrv admin", "common.actions": "Actions", "common.admins": "Admins", "common.backToList": "Back to list", "common.channel": "Channel", "common.channelOrGroup": "Channel / Group", "common.clear": "Clear", "common.close": "Close", "common.count": "Count", "common.deleted": "Deleted", "common.detail": "Details", "common.device": "Device", "common.disabled": "Disabled", "common.enabled": "Enabled", "common.fromPeer": "From Peer", "common.group": "Group", "common.id": "ID", "common.limit": "Limit", "common.loading": "Loading", "common.member": "Member", "common.members": "Members", "common.messageId": "Message ID", "common.name": "Name", "common.no": "No", "common.noResults": "No results", "common.none": "None", "common.normal": "Normal", "common.operations": "Operations", "common.owner": "Owner", "common.platform": "Platform", "common.refresh": "Refresh", "common.search": "Search", "common.sender": "Sender", "common.status": "Status", "common.survived": "Live", "common.time": "Time", "common.type": "Type", "common.updatedAt": "Updated", "common.username": "Username", "common.valid": "Valid", "common.verified": "Verified", "common.views": "Views", "common.yes": "Yes", "route.accounts": "Accounts", "route.accountsSubtitle": "Console / Accounts", "route.channels": "Supergroups and Channels", "route.channelsSubtitle": "Console / Channels", "route.dashboard": "Operations Console", "route.dashboardSubtitle": "Console / Overview", "route.messages": "Message Audit", "route.messagesSubtitle": "Console / Messages", "route.gifts": "Star Gifts", "route.giftsSubtitle": "Console / Star Gifts", "layout.navigation": "Navigation", "layout.primaryNav": "Primary navigation", "layout.dashboard": "Overview", "layout.accounts": "Accounts", "layout.channels": "Supergroups / Channels", "layout.messages": "Messages", "layout.gifts": "Star Gifts", "layout.privateMessages": "Private", "layout.groupMessages": "Groups", "layout.runtime": "Runtime", "layout.adminBackend": "Admin backend", "layout.ready": "Ready", "layout.pgRead": "PG read", "layout.readOnly": "Read-only", "layout.writeOps": "Write operations", "layout.dryRun": "Dry-run", "layout.actor": "Actor: {actor}", "layout.logout": "Log out", "language.en": "EN", "language.zh": "中文", "language.ru": "RU", "login.heading": "Operations Admin", "login.body": "Enter credentials to open the console.", "login.secret": "Admin password or token", "login.submit": "Log in", "login.submitting": "Logging in", "dashboard.eyebrow": "Runtime Overview", "dashboard.title": "Console Overview", "dashboard.readPath": "Read path", "dashboard.readPathValue": "PG read-only", "dashboard.writePath": "Write path", "dashboard.executionPolicy": "Execution policy", "dashboard.dryRunFirst": "Dry-run first", "dashboard.accountsText": "Account status, premium, verification, sessions.", "dashboard.channelsText": "Public entities, member counts, verification state.", "dashboard.messagesText": "Message boxes, updates, outbox state.", "dashboard.strip.dryRun": "All dangerous actions start with dry-run", "dashboard.strip.token": "Browser never stores internal tokens", "dashboard.strip.pagination": "Lists use cursor pagination", "dashboard.strip.snapshot": "Detail pages retain raw state snapshots", "account.pageTitle": "Accounts", "account.queryResults": "Search results", "account.recentActive": "Recently active accounts", "account.currentPage": "Accounts on page", "account.onlineDevices": "Online device records", "account.premium": "Premium", "account.frozen": "Frozen", "account.searchPlaceholder": "User ID / phone / username", "account.userID": "User ID", "account.phone": "Phone", "account.lastActive": "Last active", "account.notVerified": "Not verified", "account.notPremium": "Not premium", "account.premiumUntil": "Premium expires", "account.starsBalance": "Stars balance", "account.startingGrantApplied": "initial grant applied", "account.startingGrantPending": "initial grant pending", "account.activeSessions": "Authorized devices", "account.accountFlags": "Account flags", "account.restriction": "Restriction", "account.restricted": "Restricted", "account.createdAt": "Created", "account.detailTitle": "Account #{id}", "account.profile": "Account Profile", "account.loadingDetail": "Loading account detail", "account.waitingData": "Waiting for data", "account.noUsername": "No username", "account.noPhone": "No phone", "account.accountFrozen": "Account frozen", "account.accountActive": "Account active", "account.authorizationsTitle": "Authorized Devices", "account.authorizationsCount": "{count} authorizations", "account.recentAdminOps": "Recent Admin Actions", "account.recent30Audit": "Last 30 audit rows", "account.actionDock": "Account Actions", "account.freezeAccount": "Freeze account", "account.updateFreeze": "Update freeze", "account.unfreezeAccount": "Unfreeze account", "account.freezeSince": "Frozen since", "account.freezeUntil": "Appeal deadline", "account.freezeUntilAria": "Freeze appeal deadline", "account.freezeAppealURL": "Appeal URL", "account.freezeAppealURLAria": "Freeze appeal URL", "account.premiumMonths": "Premium duration (months)", "account.premiumMonthsAria": "Set premium duration in months", "account.setPremium": "Set premium", "account.clearPremium": "Clear premium", "account.starsAmount": "Stars to grant", "account.starsAmountAria": "Set Stars amount to grant", "account.grantStars": "Grant Stars", "account.setVerified": "Set verified", "account.clearVerified": "Clear verified", "channel.pageTitle": "Supergroups and Channels", "channel.recentUpdated": "Recently updated", "channel.currentPage": "Entities on page", "channel.megagroups": "Supergroups", "channel.broadcasts": "Channels", "channel.verifiedCount": "Verified", "channel.searchPlaceholder": "Channel ID / username / title", "channel.channelID": "Channel ID", "channel.kind": "Kind", "channel.title": "Title", "channel.pts": "PTS", "channel.detailProfile": "Channel Profile", "channel.loadingDetail": "Loading channel detail", "channel.creator": "Creator {id}", "channel.governance": "Moderation", "channel.governanceValue": "Banned {banned} / Kicked {kicked}", "channel.flags": "Channel flags", "channel.rawRow": "Channel Raw Row", "channel.rawRowText": "Database read-only snapshot", "channel.actionDock": "Channel Actions", "channel.setVerified": "Set verified", "channel.clearVerified": "Clear verified", "channel.kind.broadcast": "Channel", "channel.kind.forum": "Supergroup / Forum", "channel.kind.megagroup": "Supergroup", "channel.kind.generic": "Channel / Group", "messages.privateTitle": "Private Messages", "messages.privateEyebrow": "Private message boxes", "messages.groupTitle": "Group Messages", "messages.groupEyebrow": "Supergroup / channel messages", "messages.selectPrivatePeers": "Search and select the owner user and peer user first", "messages.selectChannel": "Search and select a supergroup or channel first", "messages.ownerUser": "Owner user", "messages.peerUser": "Peer user", "messages.beforeDatePlaceholder": "before_date cursor", "messages.beforeIDPlaceholder": "before_msg_id cursor", "messages.limitPlaceholder": "limit <= 100", "messages.searchMessages": "Search messages", "messages.nextPage": "Next page", "messages.currentPage": "Messages on page", "messages.deleted": "Deleted", "messages.outgoing": "Outgoing", "messages.incoming": "Incoming", "messages.ownerPeer": "Owner / Peer", "messages.deleteSelected": "Delete selected messages", "messages.idsPlaceholder": "Message IDs, comma separated", "messages.revoke": "Revoke for both sides", "messages.previewDelete": "Dry-run delete", "messages.clearHistory": "Clear private history", "messages.maxIDPlaceholder": "max_id cutoff", "messages.maxBatchesPlaceholder": "max_batches", "messages.justClear": "Clear only this side", "messages.previewClearHistory": "Dry-run clear history", "messages.direction": "Direction", "messages.body": "Body", "messages.privateDetailTitle": "Message #{id}", "messages.detailEyebrow": "Message Detail", "messages.backPrivate": "Back to private messages", "messages.backGroup": "Back to group messages", "messages.ownerPeerTitle": "Owner {owner} · Peer {peer}", "messages.senderSubtitle": "Sender {sender} · {date}", "messages.boxID": "Message box ID", "messages.privateMessageID": "Private message ID", "messages.messageSender": "Message sender", "messages.messageBox": "Message Box", "messages.dialogRow": "Dialog Row", "messages.privateRow": "Private Message Row", "messages.channelMessageRow": "Channel Message Row", "messages.channelRow": "Channel Row", "messages.userUpdateEvents": "Update Events", "messages.channelUpdateEvents": "Channel Update Events", "messages.eventJson": "Event JSON", "messages.dispatchOutbox": "Dispatch Queue", "messages.messageBoxesSnapshot": "message_boxes read-only snapshot", "messages.dialogSnapshot": "dialogs read-only snapshot", "messages.privateSnapshot": "private_messages read-only snapshot", "messages.channelMessagesSnapshot": "channel_messages read-only snapshot", "messages.channelSnapshot": "channels read-only snapshot", "messages.userEventsSource": "durable user_update_events", "messages.channelEventsSource": "durable channel_update_events", "messages.outboxSource": "online/offline dispatch_outbox", "messages.attempts": "Attempts", "messages.deleteThis": "Delete this message", "messages.groupDetailTitle": "Group Message #{id}", "messages.channelGroupTitle": "Channel / Group {id}", "messages.mediaCount": "With media", "messages.channelPosts": "Channel posts", "messages.channelGroup": "Channel / Group", "messages.pinned": "Pinned", "messages.channelPost": "Channel post", "gifts.pageTitle": "Star Gift Catalog", "gifts.eyebrow": "Catalog, immutable revisions and animation assets", "gifts.total": "Catalog entries", "gifts.enabled": "Enabled", "gifts.received": "Received gifts", "gifts.formats": "Accepted formats", "gifts.add": "Add gift", "gifts.searchPlaceholder": "Search gift ID, title or format", "gifts.listSummary": "Showing {shown} of {total}", "gifts.idRevision": "ID / Revision", "gifts.price": "Price / Conversion", "gifts.importTitle": "Import a Star Gift", "gifts.importEyebrow": "Gift catalog operation", "gifts.newRevision": "Create revision for gift #{id}", "gifts.importHint": "Upload TGS or plain Lottie JSON. Lottie is normalized and compressed to TGS.", "gifts.officialSource": "Official snapshot", "gifts.fileSource": "Upload file", "gifts.officialHint": "Choose a verified gift from data/official-gifts. Complete collectible pools are imported atomically.", "gifts.officialSearch": "Search official gift ID or title", "gifts.officialSelect": "Choose an official gift", "gifts.officialRequired": "Choose an official gift first", "gifts.officialResults": "Showing {shown} of {total}", "gifts.officialCategoryLabel": "Official gift capability category", "gifts.officialCategory.all": "All", "gifts.officialCategory.upgrade": "Upgradable", "gifts.officialCategory.craft": "Craftable", "gifts.officialCategory.basic": "Not upgradable", "gifts.officialUnnamed": "Unnamed official gift #{id}", "gifts.officialAttributes": "{count} attributes", "gifts.canUpgrade": "Can upgrade", "gifts.cannotUpgrade": "Cannot upgrade", "gifts.canCraft": "Can Craft", "gifts.cannotCraft": "Cannot Craft", "gifts.officialEmpty": "No official gifts match this category and search.", "gifts.includeCollectible": "Import the complete collectible pool, including crafted models", "gifts.animation": "Animation file", "gifts.filePrompt": "Drop or choose a TGS / Lottie file", "gifts.fileHint": "TGS, JSON or Lottie · validated before import", "gifts.chooseFile": "Choose file", "gifts.changeFile": "Change file", "gifts.title": "Display title", "gifts.titlePlaceholder": "e.g. Celebration Star", "gifts.stars": "Price in Stars", "gifts.convertStars": "Conversion Stars", "gifts.sortOrder": "Sort order", "gifts.reason": "Audit reason", "gifts.reasonPlaceholder": "Briefly describe why this gift is being imported", "gifts.enableAfterImport": "Enable after import", "gifts.validate": "Dry-run validation", "gifts.confirmImport": "Confirm import", "gifts.stepDetails": "File and details", "gifts.stepValidate": "Dry-run validation", "gifts.stepImport": "Confirm import", "gifts.fileRequired": "Choose a TGS or Lottie file first", "gifts.source": "Source", "gifts.replace": "New revision", "gifts.disable": "Disable", "gifts.enable": "Enable", "gifts.empty": "No Star Gifts have been imported.", "gifts.emptyHint": "Import the first animation above to build the gift catalog.", "gifts.validationReady": "Validation passed", "gifts.validationHint": "Review the normalized metadata, then confirm the import.", "gifts.confirmState": "Apply the validated state change to gift #{id}?", "collectibles.manage": "Attribute pool", "collectibles.title": "Collectible pool · Gift #{id}", "collectibles.eyebrow": "Unique gift attributes", "collectibles.activeRevision": "Published revision {revision}", "collectibles.published": "Published", "collectibles.noPool": "No collectible pool published", "collectibles.noPoolHint": "Publish models, patterns and backdrops to enable upgrades.", "collectibles.publishNew": "Publish a new immutable revision", "collectibles.immutableHint": "Dry-run checks every file and rarity total before the revision becomes active.", "collectibles.upgradeStars": "Upgrade price in Stars", "collectibles.supply": "Unique supply", "collectibles.slug": "Public slug prefix", "collectibles.models": "Models", "collectibles.patterns": "Patterns", "collectibles.backdrops": "Backdrops", "collectibles.model": "Model", "collectibles.pattern": "Pattern", "collectibles.backdrop": "Backdrop", "collectibles.rarity": "Rarity ‰", "collectibles.rarityHint": "Permille values are relative regular-upgrade weights; their total does not need to equal 1000.", "collectibles.colorHint": "Colors are stored as 24-bit RGB values.", "collectibles.addAttribute": "Add", "collectibles.remove": "Remove attribute", "collectibles.fileRequired": "Every model and pattern needs a TGS or Lottie file.", "collectibles.backdropID": "Backdrop ID", "collectibles.color.center": "Center", "collectibles.color.edge": "Edge", "collectibles.color.pattern": "Pattern", "collectibles.color.text": "Text", "collectibles.validationReady": "Attribute pool is valid", "collectibles.validationHint": "Review the normalized assets, then publish this immutable revision.", "collectibles.publish": "Publish revision", "messages.msgIDsInvalid": "Message IDs are invalid", "auth.device": "Device", "auth.platform": "Platform", "auth.ip": "IP", "auth.lastActive": "Last active", "auth.revokeCurrent": "Revoke current", "auth.keepCurrent": "Keep current", "auth.revokeAll": "Revoke all devices", "picker.userPlaceholder": "Search user_id / phone / username", "picker.channelPlaceholder": "Search channel_id / username / title", "picker.verified": "Verified", "picker.regular": "Regular", "action.reasonRequired": "Please enter an operation reason", "action.flow": "Action Flow", "action.close": "Close", "action.stepReason": "Enter reason", "action.stepDryRun": "Dry-run check", "action.stepConfirm": "Confirm execution", "action.reason": "Operation reason", "action.reasonPlaceholder": "Describe why this operation is being performed", "action.requestPreview": "Request preview", "action.result": "Action result", "action.commandID": "Command ID", "action.status": "Status", "action.dryRun": "Dry-run", "action.runAgain": "Run dry-run again", "action.runDry": "Run dry-run first", "action.confirm": "Confirm execution", "audit.id": "ID", "audit.commandID": "Command ID", "audit.action": "Action", "audit.actor": "Actor", "audit.status": "Status", "audit.dryRun": "Dry-run", "audit.reason": "Reason", "audit.time": "Time" }, zh: { "app.adminConsole": "管理控制台", "app.localAccess": "本地访问", "app.title": "telesrv 管理后台", "common.actions": "操作", "common.admins": "管理员", "common.backToList": "返回列表", "common.channel": "频道", "common.channelOrGroup": "频道/群", "common.clear": "清除", "common.close": "关闭", "common.count": "数量", "common.deleted": "已删除", "common.detail": "详情", "common.device": "设备", "common.disabled": "已禁用", "common.enabled": "已启用", "common.fromPeer": "From Peer", "common.group": "群组", "common.id": "ID", "common.limit": "条数", "common.loading": "加载中", "common.member": "成员", "common.members": "成员", "common.messageId": "消息 ID", "common.name": "姓名", "common.no": "否", "common.noResults": "无结果", "common.none": "无", "common.normal": "正常", "common.operations": "操作", "common.owner": "所属", "common.platform": "平台", "common.refresh": "刷新", "common.search": "查询", "common.sender": "发送方", "common.status": "状态", "common.survived": "存活", "common.time": "时间", "common.type": "类型", "common.updatedAt": "更新时间", "common.username": "用户名", "common.valid": "有效", "common.verified": "已认证", "common.views": "浏览", "common.yes": "是", "route.accounts": "账号管理", "route.accountsSubtitle": "控制台 / 账号", "route.channels": "超级群与频道", "route.channelsSubtitle": "控制台 / 频道", "route.dashboard": "运维控制台", "route.dashboardSubtitle": "控制台 / 总览", "route.messages": "消息审计", "route.messagesSubtitle": "控制台 / 消息", "route.gifts": "星星礼物", "route.giftsSubtitle": "控制台 / 星星礼物", "layout.navigation": "导航", "layout.primaryNav": "主导航", "layout.dashboard": "总览", "layout.accounts": "账号", "layout.channels": "超级群/频道", "layout.messages": "消息", "layout.gifts": "礼物目录", "layout.privateMessages": "私聊", "layout.groupMessages": "群聊", "layout.runtime": "运行状态", "layout.adminBackend": "管理后台", "layout.ready": "就绪", "layout.pgRead": "PG 读取", "layout.readOnly": "只读", "layout.writeOps": "写操作", "layout.dryRun": "预演", "layout.actor": "操作者:{actor}", "layout.logout": "退出", "language.en": "EN", "language.zh": "中文", "language.ru": "RU", "login.heading": "运维后台", "login.body": "输入凭据后进入控制台。", "login.secret": "管理员密码或 token", "login.submit": "登录", "login.submitting": "登录中", "dashboard.eyebrow": "运行总览", "dashboard.title": "控制台总览", "dashboard.readPath": "读路径", "dashboard.readPathValue": "PG 只读", "dashboard.writePath": "写路径", "dashboard.executionPolicy": "执行策略", "dashboard.dryRunFirst": "先预演", "dashboard.accountsText": "账号状态、会员、认证、会话。", "dashboard.channelsText": "公开实体、成员计数、认证状态。", "dashboard.messagesText": "消息盒、update、outbox 状态。", "dashboard.strip.dryRun": "所有危险操作先预演", "dashboard.strip.token": "浏览器不持有内部 token", "dashboard.strip.pagination": "列表使用游标分页", "dashboard.strip.snapshot": "详情页保留原始状态快照", "account.pageTitle": "账号", "account.queryResults": "查询结果", "account.recentActive": "最近活跃账号", "account.currentPage": "当前页账号", "account.onlineDevices": "在线设备记录", "account.premium": "会员", "account.frozen": "冻结", "account.searchPlaceholder": "用户 ID / 手机号 / 用户名", "account.userID": "用户 ID", "account.phone": "手机号", "account.lastActive": "最近活跃", "account.notVerified": "未认证", "account.notPremium": "非会员", "account.premiumUntil": "会员到期", "account.starsBalance": "Stars 余额", "account.startingGrantApplied": "初始赠送已发放", "account.startingGrantPending": "初始赠送未触发", "account.activeSessions": "授权设备", "account.accountFlags": "账号标记", "account.restriction": "限制状态", "account.restricted": "已限制", "account.createdAt": "创建时间", "account.detailTitle": "账号 #{id}", "account.profile": "账号档案", "account.loadingDetail": "加载账号详情", "account.waitingData": "等待数据", "account.noUsername": "无用户名", "account.noPhone": "无手机号", "account.accountFrozen": "账号已冻结", "account.accountActive": "账号正常", "account.authorizationsTitle": "授权设备", "account.authorizationsCount": "共 {count} 个授权", "account.recentAdminOps": "最近后台操作", "account.recent30Audit": "最近 30 条审计", "account.actionDock": "账号操作", "account.freezeAccount": "冻结账号", "account.updateFreeze": "更新冻结信息", "account.unfreezeAccount": "解冻账号", "account.freezeSince": "冻结开始时间", "account.freezeUntil": "申诉截止时间", "account.freezeUntilAria": "账号冻结申诉截止时间", "account.freezeAppealURL": "申诉链接", "account.freezeAppealURLAria": "账号冻结申诉链接", "account.premiumMonths": "会员时长(月)", "account.premiumMonthsAria": "设置会员时长,单位月", "account.setPremium": "设置会员", "account.clearPremium": "取消会员", "account.starsAmount": "赠送 Stars 数量", "account.starsAmountAria": "设置要赠送的 Stars 数量", "account.grantStars": "赠送 Stars", "account.setVerified": "设置认证", "account.clearVerified": "取消认证", "channel.pageTitle": "超级群与频道", "channel.recentUpdated": "最近更新", "channel.currentPage": "当前页实体", "channel.megagroups": "超级群", "channel.broadcasts": "频道", "channel.verifiedCount": "已认证", "channel.searchPlaceholder": "频道 ID / 用户名 / 标题", "channel.channelID": "频道 ID", "channel.kind": "类型", "channel.title": "标题", "channel.pts": "PTS", "channel.detailProfile": "频道档案", "channel.loadingDetail": "加载频道详情", "channel.creator": "创建者 {id}", "channel.governance": "治理状态", "channel.governanceValue": "封禁 {banned} / 踢出 {kicked}", "channel.flags": "频道标记", "channel.rawRow": "频道原始行", "channel.rawRowText": "数据库只读快照", "channel.actionDock": "频道操作", "channel.setVerified": "设置认证", "channel.clearVerified": "取消认证", "channel.kind.broadcast": "频道", "channel.kind.forum": "超级群/论坛", "channel.kind.megagroup": "超级群", "channel.kind.generic": "频道/群", "messages.privateTitle": "私聊消息", "messages.privateEyebrow": "私聊消息盒", "messages.groupTitle": "群聊消息", "messages.groupEyebrow": "超级群 / 频道消息", "messages.selectPrivatePeers": "请先搜索并选择所属用户和对端用户", "messages.selectChannel": "请先搜索并选择超级群或频道", "messages.ownerUser": "所属用户", "messages.peerUser": "对端用户", "messages.beforeDatePlaceholder": "before_date 游标", "messages.beforeIDPlaceholder": "before_msg_id 游标", "messages.limitPlaceholder": "条数 <= 100", "messages.searchMessages": "查询消息", "messages.nextPage": "下一页", "messages.currentPage": "当前页消息", "messages.deleted": "已删除", "messages.outgoing": "发出消息", "messages.incoming": "收到", "messages.ownerPeer": "所属 / 对端", "messages.deleteSelected": "删除指定消息", "messages.idsPlaceholder": "消息 ID,逗号分隔", "messages.revoke": "同步撤回", "messages.previewDelete": "预演删除", "messages.clearHistory": "清空私聊历史", "messages.maxIDPlaceholder": "max_id 截止消息", "messages.maxBatchesPlaceholder": "max_batches 批次数", "messages.justClear": "仅清本侧", "messages.previewClearHistory": "预演清历史", "messages.direction": "方向", "messages.body": "正文", "messages.privateDetailTitle": "消息 #{id}", "messages.detailEyebrow": "消息详情", "messages.backPrivate": "返回私聊消息", "messages.backGroup": "返回群聊消息", "messages.ownerPeerTitle": "所属 {owner} · 对端 {peer}", "messages.senderSubtitle": "发送方 {sender} · {date}", "messages.boxID": "消息盒 ID", "messages.privateMessageID": "私聊消息 ID", "messages.messageSender": "发送方", "messages.messageBox": "消息盒", "messages.dialogRow": "会话行", "messages.privateRow": "私聊消息行", "messages.channelMessageRow": "消息行", "messages.channelRow": "频道行", "messages.userUpdateEvents": "更新事件", "messages.channelUpdateEvents": "频道更新事件", "messages.eventJson": "事件 JSON", "messages.dispatchOutbox": "分发队列", "messages.messageBoxesSnapshot": "message_boxes 只读快照", "messages.dialogSnapshot": "dialogs 只读快照", "messages.privateSnapshot": "private_messages 只读快照", "messages.channelMessagesSnapshot": "channel_messages 只读快照", "messages.channelSnapshot": "channels 只读快照", "messages.userEventsSource": "durable user_update_events", "messages.channelEventsSource": "durable channel_update_events", "messages.outboxSource": "在线/离线 dispatch_outbox", "messages.attempts": "尝试", "messages.deleteThis": "删除此消息", "messages.groupDetailTitle": "群聊消息 #{id}", "messages.channelGroupTitle": "频道/群 {id}", "messages.mediaCount": "有媒体", "messages.channelPosts": "频道帖子", "messages.channelGroup": "频道 / 群", "messages.pinned": "置顶", "messages.channelPost": "频道帖子", "gifts.pageTitle": "星星礼物目录", "gifts.eyebrow": "目录、不可变版本与动画资源", "gifts.total": "目录条目", "gifts.enabled": "已启用", "gifts.received": "已领取礼物", "gifts.formats": "支持格式", "gifts.add": "添加礼物", "gifts.searchPlaceholder": "搜索礼物 ID、标题或格式", "gifts.listSummary": "显示 {shown} / {total} 项", "gifts.idRevision": "ID / 版本", "gifts.price": "售价 / 兑换", "gifts.importTitle": "导入星星礼物", "gifts.importEyebrow": "礼物目录操作", "gifts.newRevision": "为礼物 #{id} 创建新版本", "gifts.importHint": "支持 TGS 或纯 Lottie JSON;Lottie 会规范化并压缩成 TGS。", "gifts.officialSource": "官方资源库", "gifts.fileSource": "上传文件", "gifts.officialHint": "从 data/official-gifts 的已校验快照中选择;完整 collectible 属性池会与礼物原子导入。", "gifts.officialSearch": "搜索官方礼物 ID 或标题", "gifts.officialSelect": "请选择官方礼物", "gifts.officialRequired": "请先选择一个官方礼物", "gifts.officialResults": "显示 {shown} / {total} 项", "gifts.officialCategoryLabel": "官方礼物能力分类", "gifts.officialCategory.all": "全部", "gifts.officialCategory.upgrade": "可升级", "gifts.officialCategory.craft": "可 Craft", "gifts.officialCategory.basic": "不可升级", "gifts.officialUnnamed": "未命名官方礼物 #{id}", "gifts.officialAttributes": "{count} 个属性", "gifts.canUpgrade": "可升级", "gifts.cannotUpgrade": "不可升级", "gifts.canCraft": "可 Craft", "gifts.cannotCraft": "不可 Craft", "gifts.officialEmpty": "当前分类和搜索条件下没有官方礼物。", "gifts.includeCollectible": "完整导入 collectible 属性池(包含 crafted 模型)", "gifts.animation": "动画文件", "gifts.filePrompt": "拖放或选择 TGS / Lottie 文件", "gifts.fileHint": "支持 TGS、JSON、Lottie,导入前会先进行校验", "gifts.chooseFile": "选择文件", "gifts.changeFile": "更换文件", "gifts.title": "显示标题", "gifts.titlePlaceholder": "例如:庆典星星", "gifts.stars": "售价 Stars", "gifts.convertStars": "可兑换 Stars", "gifts.sortOrder": "排序值", "gifts.reason": "审计原因", "gifts.reasonPlaceholder": "简要说明本次导入礼物的原因", "gifts.enableAfterImport": "导入后启用", "gifts.validate": "Dry-run 校验", "gifts.confirmImport": "确认导入", "gifts.stepDetails": "文件与信息", "gifts.stepValidate": "Dry-run 校验", "gifts.stepImport": "确认导入", "gifts.fileRequired": "请先选择 TGS 或 Lottie 文件", "gifts.source": "来源", "gifts.replace": "创建新版本", "gifts.disable": "停用", "gifts.enable": "启用", "gifts.empty": "尚未导入星星礼物。", "gifts.emptyHint": "从上方导入第一个动画,开始搭建礼物目录。", "gifts.validationReady": "校验已通过", "gifts.validationHint": "确认规范化后的元数据无误,再执行正式导入。", "gifts.confirmState": "确认执行礼物 #{id} 的状态变更吗?", "collectibles.manage": "属性池", "collectibles.title": "Collectibles 属性池 · 礼物 #{id}", "collectibles.eyebrow": "唯一礼物属性管理", "collectibles.activeRevision": "已发布版本 {revision}", "collectibles.published": "已发布", "collectibles.noPool": "尚未发布 Collectibles 属性池", "collectibles.noPoolHint": "发布模型、图案与背景后,客户端即可升级为唯一礼物。", "collectibles.publishNew": "发布新的不可变版本", "collectibles.immutableHint": "Dry-run 会校验全部文件和稀有度总和,通过后才切换为当前版本。", "collectibles.upgradeStars": "升级价格 Stars", "collectibles.supply": "唯一礼物总量", "collectibles.slug": "公开 Slug 前缀", "collectibles.models": "模型", "collectibles.patterns": "图案", "collectibles.backdrops": "背景", "collectibles.model": "模型", "collectibles.pattern": "图案", "collectibles.backdrop": "背景", "collectibles.rarity": "稀有度 ‰", "collectibles.rarityHint": "Permille 是普通升级的相对权重,不要求每类合计正好为 1000。", "collectibles.colorHint": "颜色会按 24 位 RGB 数值保存。", "collectibles.addAttribute": "添加", "collectibles.remove": "删除属性", "collectibles.fileRequired": "每个模型和图案都必须选择 TGS 或 Lottie 文件。", "collectibles.backdropID": "背景 ID", "collectibles.color.center": "中心色", "collectibles.color.edge": "边缘色", "collectibles.color.pattern": "图案色", "collectibles.color.text": "文字色", "collectibles.validationReady": "属性池校验通过", "collectibles.validationHint": "确认规范化资源无误后,即可发布这个不可变版本。", "collectibles.publish": "发布版本", "messages.msgIDsInvalid": "消息 ID 无效", "auth.device": "设备", "auth.platform": "平台", "auth.ip": "IP", "auth.lastActive": "最近活跃", "auth.revokeCurrent": "撤销当前", "auth.keepCurrent": "保留当前", "auth.revokeAll": "撤销全部设备", "picker.userPlaceholder": "搜索 user_id / phone / username", "picker.channelPlaceholder": "搜索 channel_id / username / title", "picker.verified": "认证", "picker.regular": "普通", "action.reasonRequired": "请填写操作原因", "action.flow": "操作流程", "action.close": "关闭", "action.stepReason": "填写原因", "action.stepDryRun": "预演检查", "action.stepConfirm": "确认执行", "action.reason": "操作原因", "action.reasonPlaceholder": "说明本次操作原因", "action.requestPreview": "请求预览", "action.result": "操作结果", "action.commandID": "命令 ID", "action.status": "状态", "action.dryRun": "预演", "action.runAgain": "重新预演", "action.runDry": "先预演", "action.confirm": "确认执行", "audit.id": "ID", "audit.commandID": "命令 ID", "audit.action": "动作", "audit.actor": "操作者", "audit.status": "状态", "audit.dryRun": "预演", "audit.reason": "原因", "audit.time": "时间" }, ru: { "app.adminConsole": "Панель администратора", "app.localAccess": "Локальный доступ", "app.title": "telesrv admin", "common.actions": "Действия", "common.admins": "Администраторы", "common.backToList": "Назад к списку", "common.channel": "Канал", "common.channelOrGroup": "Канал / Группа", "common.clear": "Очистить", "common.close": "Закрыть", "common.count": "Количество", "common.deleted": "Удалено", "common.detail": "Детали", "common.device": "Устройство", "common.disabled": "Отключено", "common.enabled": "Включено", "common.fromPeer": "От пира", "common.group": "Группа", "common.id": "ID", "common.limit": "Лимит", "common.loading": "Загрузка...", "common.member": "Участник", "common.members": "Участники", "common.messageId": "ID сообщения", "common.name": "Имя", "common.no": "Нет", "common.noResults": "Нет результатов", "common.none": "Нет", "common.normal": "Обычный", "common.operations": "Операции", "common.owner": "Владелец", "common.platform": "Платформа", "common.refresh": "Обновить", "common.search": "Поиск", "common.sender": "Отправитель", "common.status": "Статус", "common.survived": "Уцелело", "common.time": "Время", "common.type": "Тип", "common.updatedAt": "Обновлено", "common.username": "Имя пользователя", "common.valid": "Действителен", "common.verified": "Подтвержден", "common.views": "Просмотры", "common.yes": "Да", "route.accounts": "Аккаунты", "route.accountsSubtitle": "Консоль / Аккаунты", "route.channels": "Супергруппы и каналы", "route.channelsSubtitle": "Консоль / Каналы", "route.dashboard": "Панель управления", "route.dashboardSubtitle": "Консоль / Обзор", "route.messages": "Аудит сообщений", "route.messagesSubtitle": "Консоль / Сообщения", "route.gifts": "Звездные подарки", "route.giftsSubtitle": "Консоль / Звездные подарки", "layout.navigation": "Навигация", "layout.primaryNav": "Основное меню", "layout.dashboard": "Обзор", "layout.accounts": "Аккаунты", "layout.channels": "Супергруппы / Каналы", "layout.messages": "Сообщения", "layout.gifts": "Звездные подарки", "layout.privateMessages": "Личные", "layout.groupMessages": "Группы", "layout.runtime": "Среда выполнения", "layout.adminBackend": "Админ-бэкенд", "layout.ready": "Готов", "layout.pgRead": "Чтение из PG", "layout.readOnly": "Только чтение", "layout.writeOps": "Операции записи", "layout.dryRun": "Тестовый запуск", "layout.actor": "Вход выполнен как: {actor}", "layout.logout": "Выйти", "language.en": "EN", "language.zh": "中文", "language.ru": "RU", "login.heading": "Панель администратора", "login.body": "Введите учетные данные для входа в консоль.", "login.secret": "Пароль или токен администратора", "login.submit": "Войти", "login.submitting": "Вход...", "dashboard.eyebrow": "Состояние системы", "dashboard.title": "Обзор консоли", "dashboard.readPath": "Путь чтения", "dashboard.readPathValue": "PG только для чтения", "dashboard.writePath": "Путь записи", "dashboard.executionPolicy": "Политика выполнения", "dashboard.dryRunFirst": "Сначала тестовый запуск", "dashboard.accountsText": "Статус аккаунтов, премиум, верификация, сессии.", "dashboard.channelsText": "Публичные каналы и группы, количество участников, статус верификации.", "dashboard.messagesText": "Ящики сообщений, обновления, состояние исходящих.", "dashboard.strip.dryRun": "Все опасные действия начинаются с тестового запуска", "dashboard.strip.token": "Браузер никогда не сохраняет внутренние токены", "dashboard.strip.pagination": "Списки используют курсорную пагинацию", "dashboard.strip.snapshot": "Детальные страницы сохраняют моментальные снимки исходного состояния", "account.pageTitle": "Аккаунты", "account.queryResults": "Результаты поиска", "account.recentActive": "Недавно активные аккаунты", "account.currentPage": "Аккаунты на странице", "account.onlineDevices": "Активные сессии устройств", "account.premium": "Premium", "account.frozen": "Заморожен", "account.searchPlaceholder": "ID пользователя / телефон / имя пользователя", "account.userID": "ID пользователя", "account.phone": "Телефон", "account.lastActive": "Последняя активность", "account.notVerified": "Не подтвержден", "account.notPremium": "Без Premium", "account.premiumUntil": "Premium истекает", "account.starsBalance": "Баланс Звезд", "account.startingGrantApplied": "стартовый бонус начислен", "account.startingGrantPending": "ожидает стартового бонуса", "account.activeSessions": "Авторизованные устройства", "account.accountFlags": "Флаги аккаунта", "account.restriction": "Ограничение", "account.restricted": "Ограничен", "account.createdAt": "Создан", "account.detailTitle": "Аккаунт #{id}", "account.profile": "Профиль аккаунта", "account.loadingDetail": "Загрузка данных аккаунта", "account.waitingData": "Ожидание данных", "account.noUsername": "Нет имени пользователя", "account.noPhone": "Нет телефона", "account.accountFrozen": "Аккаунт заморожен", "account.accountActive": "Аккаунт активен", "account.authorizationsTitle": "Авторизованные устройства", "account.authorizationsCount": "Авторизаций: {count}", "account.recentAdminOps": "Последние действия администратора", "account.recent30Audit": "Последние 30 записей аудита", "account.actionDock": "Действия с аккаунтом", "account.freezeAccount": "Заморозить аккаунт", "account.updateFreeze": "Обновить параметры заморозки", "account.unfreezeAccount": "Разморозить аккаунт", "account.freezeSince": "Заморожен с", "account.freezeUntil": "Срок подачи апелляции", "account.freezeUntilAria": "Срок подачи апелляции на заморозку", "account.freezeAppealURL": "URL для апелляции", "account.freezeAppealURLAria": "URL для апелляции на заморозку", "account.premiumMonths": "Срок действия Premium (в месяцах)", "account.premiumMonthsAria": "Указать срок действия Premium в месяцах", "account.setPremium": "Выдать Premium", "account.clearPremium": "Снять Premium", "account.starsAmount": "Количество звёзд", "account.starsAmountAria": "Указать количество начисляемых звёзд", "account.grantStars": "Начислить звёзды", "account.setVerified": "Подтвердить аккаунт", "account.clearVerified": "Снять подтверждение", "channel.pageTitle": "Супергруппы и каналы", "channel.recentUpdated": "Недавно обновленные", "channel.currentPage": "Объекты на странице", "channel.megagroups": "Супергруппы", "channel.broadcasts": "Каналы", "channel.verifiedCount": "Подтверждено", "channel.searchPlaceholder": "ID канала / имя пользователя / название", "channel.channelID": "ID канала", "channel.kind": "Тип", "channel.title": "Название", "channel.pts": "PTS", "channel.detailProfile": "Профиль канала", "channel.loadingDetail": "Загрузка данных канала", "channel.creator": "Создатель: {id}", "channel.governance": "Модерация", "channel.governanceValue": "Заблокировано {banned} / Исключено {kicked}", "channel.flags": "Флаги канала", "channel.rawRow": "Исходная строка БД", "channel.rawRowText": "Снимок базы данных только для чтения", "channel.actionDock": "Действия с каналом", "channel.setVerified": "Подтвердить канал", "channel.clearVerified": "Снять подтверждение", "channel.kind.broadcast": "Канал", "channel.kind.forum": "Супергруппа / Форум", "channel.kind.megagroup": "Супергруппа", "channel.kind.generic": "Канал / Группа", "messages.privateTitle": "Личные сообщения", "messages.privateEyebrow": "Личные ящики сообщений", "messages.groupTitle": "Групповые сообщения", "messages.groupEyebrow": "Сообщения супергрупп и каналов", "messages.selectPrivatePeers": "Сначала найдите и выберите владельца и собеседника", "messages.selectChannel": "Сначала найдите и выберите супергруппу или канал", "messages.ownerUser": "Пользователь-владелец", "messages.peerUser": "Собеседник", "messages.beforeDatePlaceholder": "курсор before_date", "messages.beforeIDPlaceholder": "курсор before_msg_id", "messages.limitPlaceholder": "лимит <= 100", "messages.searchMessages": "Поиск сообщений", "messages.nextPage": "Следующая страница", "messages.currentPage": "Сообщения на странице", "messages.deleted": "Удалено", "messages.outgoing": "Исходящее", "messages.incoming": "Входящее", "messages.ownerPeer": "Владелец / Собеседник", "messages.deleteSelected": "Указать и удалить выбранные сообщения", "messages.idsPlaceholder": "ID сообщений через запятую", "messages.revoke": "Удалить для обеих сторон", "messages.previewDelete": "Тестовое удаление", "messages.clearHistory": "Очистить историю личной переписки", "messages.maxIDPlaceholder": "граница max_id", "messages.maxBatchesPlaceholder": "max_batches", "messages.justClear": "Очистить только у себя", "messages.previewClearHistory": "Тестовая очистка истории", "messages.direction": "Направление", "messages.body": "Текст сообщения", "messages.privateDetailTitle": "Сообщение #{id}", "messages.detailEyebrow": "Детали сообщения", "messages.backPrivate": "Назад к личным сообщениям", "messages.backGroup": "Назад к групповым сообщениям", "messages.ownerPeerTitle": "Владелец {owner} · Собеседник {peer}", "messages.senderSubtitle": "Отправитель {sender} · {date}", "messages.boxID": "ID ящика сообщений", "messages.privateMessageID": "ID личного сообщения", "messages.messageSender": "Отправитель сообщения", "messages.messageBox": "Ящик сообщений", "messages.dialogRow": "Строка диалога", "messages.privateRow": "Строка личного сообщения", "messages.channelMessageRow": "Строка сообщения канала", "messages.channelRow": "Строка канала", "messages.userUpdateEvents": "События обновления пользователей", "messages.channelUpdateEvents": "События обновления каналов", "messages.eventJson": "JSON события", "messages.dispatchOutbox": "Очередь отправки (Outbox)", "messages.messageBoxesSnapshot": "Снимок message_boxes только для чтения", "messages.dialogSnapshot": "Снимок dialogs только для чтения", "messages.privateSnapshot": "Снимок private_messages только для чтения", "messages.channelMessagesSnapshot": "Снимок channel_messages только для чтения", "messages.channelSnapshot": "Снимок channels только для чтения", "messages.userEventsSource": "постоянные user_update_events", "messages.channelEventsSource": "постоянные channel_update_events", "messages.outboxSource": "онлайн/офлайн dispatch_outbox", "messages.attempts": "Попытки", "messages.deleteThis": "Удалить это сообщение", "messages.groupDetailTitle": "Групповое сообщение #{id}", "messages.channelGroupTitle": "Канал / Группа {id}", "messages.mediaCount": "С медиафайлами", "messages.channelPosts": "Посты канала", "messages.channelGroup": "Канал / Группа", "messages.pinned": "Закреплено", "messages.channelPost": "Пост в канале", "gifts.pageTitle": "Каталог звездных подарков", "gifts.eyebrow": "Каталог, неизменяемые версии и файлы анимаций", "gifts.total": "Подарков в каталоге", "gifts.enabled": "Включено", "gifts.received": "Полученные подарки", "gifts.formats": "Поддерживаемые форматы", "gifts.add": "Добавить подарок", "gifts.searchPlaceholder": "Поиск по ID подарка, названию или формату", "gifts.listSummary": "Показано {shown} из {total}", "gifts.idRevision": "ID / Версия", "gifts.price": "Цена / Конвертация", "gifts.importTitle": "Импорт звездного подарка", "gifts.importEyebrow": "Управление каталогом подарков", "gifts.newRevision": "Создать версию для подарка #{id}", "gifts.importHint": "Загрузите файл TGS или обычный Lottie JSON. Lottie нормализуется и сжимается в формат TGS.", "gifts.officialSource": "Официальный снимок", "gifts.fileSource": "Загрузить файл", "gifts.officialHint": "Выберите проверенный подарок из data/official-gifts. Полные пулы коллекционных предметов импортируются атомарно.", "gifts.officialSearch": "Поиск по ID или названию официального подарка", "gifts.officialSelect": "Выберите официальный подарок", "gifts.officialRequired": "Сначала выберите официальный подарок", "gifts.officialResults": "Показано {shown} из {total}", "gifts.officialCategoryLabel": "Категория возможностей официального подарка", "gifts.officialCategory.all": "Все", "gifts.officialCategory.upgrade": "Можно улучшить", "gifts.officialCategory.craft": "Можно создать", "gifts.officialCategory.basic": "Нельзя улучшить", "gifts.officialUnnamed": "Официальный подарок без названия #{id}", "gifts.officialAttributes": "Атрибутов: {count}", "gifts.canUpgrade": "Можно улучшить", "gifts.cannotUpgrade": "Нельзя улучшить", "gifts.canCraft": "Можно создать", "gifts.cannotCraft": "Нельзя создать", "gifts.officialEmpty": "Нет подарков, соответствующих категории и поиску.", "gifts.includeCollectible": "Импортировать полный пул коллекционных предметов, включая созданные модели", "gifts.animation": "Файл анимации", "gifts.filePrompt": "Перетащите или выберите файл TGS / Lottie", "gifts.fileHint": "TGS, JSON или Lottie · файл проверяется перед импортом", "gifts.chooseFile": "Выбрать файл", "gifts.changeFile": "Изменить файл", "gifts.title": "Отображаемое название", "gifts.titlePlaceholder": "например, Праздничная звезда", "gifts.stars": "Цена в Звездах", "gifts.convertStars": "Звезд при конвертации", "gifts.sortOrder": "Порядок сортировки", "gifts.reason": "Причина для аудита", "gifts.reasonPlaceholder": "Кратко опишите причину импорта этого подарка", "gifts.enableAfterImport": "Включить после импорта", "gifts.validate": "Тестовая проверка", "gifts.confirmImport": "Подтвердить импорт", "gifts.stepDetails": "Файл и описание", "gifts.stepValidate": "Тестовая проверка", "gifts.stepImport": "Подтверждение импорта", "gifts.fileRequired": "Сначала выберите файл TGS или Lottie", "gifts.source": "Источник", "gifts.replace": "Новая версия", "gifts.disable": "Отключить", "gifts.enable": "Включить", "gifts.empty": "Звездные подарки еще не импортированы.", "gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.", "gifts.validationReady": "Проверка пройдена", "gifts.validationHint": "Проверьте нормализованные метаданные и подтвердите импорт.", "gifts.confirmState": "Применить проверенные изменения состояния к подарку #{id}?", "collectibles.manage": "Пул атрибутов", "collectibles.title": "Пул коллекционных предметов · Подарок #{id}", "collectibles.eyebrow": "Уникальные атрибуты подарка", "collectibles.activeRevision": "Опубликованная версия {revision}", "collectibles.published": "Опубликовано", "collectibles.noPool": "Нет опубликованного пула коллекционных предметов", "collectibles.noPoolHint": "Опубликуйте модели, узоры и фоны для активации улучшений.", "collectibles.publishNew": "Опубликовать новую неизменяемую версию", "collectibles.immutableHint": "Тестовый запуск проверяет каждый файл и итоговые показатели редкости перед тем, как версия станет активной.", "collectibles.upgradeStars": "Цена улучшения в Звездах", "collectibles.supply": "Уникальный тираж", "collectibles.slug": "Публичный префикс ссылки (slug)", "collectibles.models": "Модели", "collectibles.patterns": "Узоры", "collectibles.backdrops": "Фоны", "collectibles.model": "Модель", "collectibles.pattern": "Узор", "collectibles.backdrop": "Фон", "collectibles.rarity": "Редкость ‰", "collectibles.rarityHint": "Значения permille — это относительные веса обычного улучшения; их сумма не обязана равняться 1000.", "collectibles.colorHint": "Цвета сохраняются как 24-битные RGB-значения.", "collectibles.addAttribute": "Добавить", "collectibles.remove": "Удалить атрибут", "collectibles.fileRequired": "Для каждой модели и узора требуется файл TGS или Lottie.", "collectibles.backdropID": "ID фона", "collectibles.color.center": "Центр", "collectibles.color.edge": "Край", "collectibles.color.pattern": "Узор", "collectibles.color.text": "Текст", "collectibles.validationReady": "Пул атрибутов корректен", "collectibles.validationHint": "Проверьте нормализованные ресурсы и опубликуйте эту неизменяемую версию.", "collectibles.publish": "Опубликовать версию", "messages.msgIDsInvalid": "Некорректные ID сообщений", "auth.device": "Устройство", "auth.platform": "Платформа", "auth.ip": "IP-адрес", "auth.lastActive": "Последняя активность", "auth.revokeCurrent": "Отозвать текущую", "auth.keepCurrent": "Оставить текущую", "auth.revokeAll": "Разлогинить все устройства", "picker.userPlaceholder": "Поиск по user_id / телефону / имени пользователя", "picker.channelPlaceholder": "Поиск по channel_id / имени пользователя / названию", "picker.verified": "Подтвержденные", "picker.regular": "Обычные", "action.reasonRequired": "Пожалуйста, укажите причину операции", "action.flow": "Процесс выполнения", "action.close": "Закрыть", "action.stepReason": "Укажите причину", "action.stepDryRun": "Тестовый запуск", "action.stepConfirm": "Подтверждение выполнения", "action.reason": "Причина операции", "action.reasonPlaceholder": "Опишите, почему выполняется эта операция", "action.requestPreview": "Запросить предпросмотр", "action.result": "Результат действия", "action.commandID": "ID команды", "action.status": "Статус", "action.dryRun": "Тестовый запуск", "action.runAgain": "Запустить тестовый запуск снова", "action.runDry": "Сначала выполните тестовый запуск", "action.confirm": "Подтвердить выполнение", "audit.id": "ID", "audit.commandID": "ID команды", "audit.action": "Действие", "audit.actor": "Исполнитель", "audit.status": "Статус", "audit.dryRun": "Тестовый запуск", "audit.reason": "Причина", "audit.time": "Время" } }; type I18nContextValue = { lang: Language; setLang: (lang: Language) => void; t: TFunction; }; const I18nContext = createContext(null); export function I18nProvider({ children }: { children: ReactNode }) { const [lang, setLang] = useState(() => initialLanguage()); useEffect(() => { try { localStorage.setItem(storageKey, lang); } catch { // Language persistence is best-effort. } const langAttr = lang === "zh" ? "zh-CN" : lang === "ru" ? "ru" : "en"; document.documentElement.lang = langAttr; document.documentElement.dir = "ltr"; document.documentElement.setAttribute("translate", "no"); document.body.classList.add("notranslate"); document.title = translate(lang, "app.title"); }, [lang]); const value = useMemo(() => ({ lang, setLang, t: (key, params) => translate(lang, key, params) }), [lang]); return {children}; } export function useI18n(): I18nContextValue { const value = useContext(I18nContext); if (!value) { throw new Error("useI18n must be used inside I18nProvider"); } return value; } export function LanguageSwitch() { const { lang, setLang, t } = useI18n(); return (
{(["en", "zh", "ru"] as const).map((item) => ( ))}
); } export function translate(lang: Language, key: string, params?: TranslationParams): string { const value = translations[lang][key] ?? translations.en[key] ?? key; if (!params) { return value; } return value.replace(/\{(\w+)\}/g, (_, name: string) => String(params[name] ?? "")); } function initialLanguage(): Language { try { const requested = normalizeLanguage(new URLSearchParams(window.location.search).get("lang")); if (requested) { return requested; } } catch { // URLSearchParams can fail in unusual embedded contexts. } try { const saved = normalizeLanguage(localStorage.getItem(storageKey)); if (saved) { return saved; } } catch { // Storage is optional. } const browserLanguages = navigator.languages?.length ? navigator.languages : [navigator.language]; for (const item of browserLanguages) { const normalized = normalizeLanguage(item); if (normalized) { return normalized; } } return "en"; } function normalizeLanguage(value: string | null | undefined): Language | null { if (!value) { return null; } const tag = value.trim().toLowerCase().replace("_", "-"); if (tag === "zh" || tag.startsWith("zh-")) { return "zh"; } if (tag === "en" || tag.startsWith("en-")) { return "en"; } if (tag === "ru" || tag.startsWith("ru-")) { return "ru"; } return null; }