owpengram-server/cmd/telesrv-admin/web/src/i18n.tsx
A 2848ff0987 feat: sync visible product branding
Sync telesrv c779c48 (feat(branding): unify visible product naming).

Skipped private www files and preserved public README files; mapped orange appearance seed names to public default names.
2026-07-20 16:49:17 +08:00

1233 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
export type Language = "en" | "zh" | "ru";
export type TranslationParams = Record<string, string | number | boolean>;
export type TFunction = (key: string, params?: TranslationParams) => string;
const storageKey = "telesrv.admin.lang";
const translations: Record<Language, Record<string, string>> = {
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 JSONLottie 会规范化并压缩成 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<I18nContextValue | null>(null);
export function I18nProvider({ children }: { children: ReactNode }) {
const [lang, setLang] = useState<Language>(() => 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<I18nContextValue>(() => ({
lang,
setLang,
t: (key, params) => translate(lang, key, params)
}), [lang]);
return <I18nContext.Provider value={value}>{children}</I18nContext.Provider>;
}
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 (
<div className="language-switch" role="group" aria-label="Language">
{(["en", "zh", "ru"] as const).map((item) => (
<button
key={item}
className={lang === item ? "active" : ""}
type="button"
aria-pressed={lang === item}
onClick={() => setLang(item)}
>
{t(`language.${item}`)}
</button>
))}
</div>
);
}
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;
}