- Introduced a new DELETE endpoint `/api/admin/maintenance/factory-reset` for a complete reset of the admin panel, removing all users except the current admin and clearing various data. - Implemented the `FactoryReset` method in `AdminMaintenanceEndpoints` to handle the reset logic. - Added a new method `ListAllUserIdsExceptAsync` in `IIdentityService` to retrieve user IDs excluding a specified user, aiding in the factory reset process. - Updated the frontend to include a confirmation dialog for the factory reset action, enhancing user experience and safety. - Enhanced localization support for the new factory reset feature in both Russian and English, ensuring clarity for all users.
833 lines
36 KiB
TypeScript
833 lines
36 KiB
TypeScript
import i18n from 'i18next'
|
||
import { initReactI18next } from 'react-i18next'
|
||
|
||
const resources = {
|
||
ru: {
|
||
translation: {
|
||
appName: 'PnvPanel',
|
||
tagline: 'Self-service портал для VPN-конфигураций',
|
||
theme: 'Тема',
|
||
language: 'Язык',
|
||
light: 'Светлая',
|
||
dark: 'Тёмная',
|
||
system: 'Системная',
|
||
unlimited: 'Без лимита',
|
||
|
||
auth: {
|
||
loginTitle: 'Вход',
|
||
registerTitle: 'Регистрация',
|
||
userName: 'Имя пользователя',
|
||
password: 'Пароль',
|
||
submitLogin: 'Войти',
|
||
submitRegister: 'Зарегистрироваться',
|
||
noAccount: 'Нет аккаунта?',
|
||
haveAccount: 'Уже есть аккаунт?',
|
||
goRegister: 'Зарегистрироваться',
|
||
goLogin: 'Войти',
|
||
loginViaTelegram: 'Войти через Telegram',
|
||
invalidCredentials: 'Неверное имя пользователя или пароль.',
|
||
duplicateUserName: 'Пользователь с таким именем уже существует.',
|
||
genericError: 'Что-то пошло не так. Попробуйте ещё раз.',
|
||
userNameHint: 'Латиница, цифры, «_», «.», «-», от 3 до 32 символов.',
|
||
passwordHint: 'Не менее 8 символов, минимум одна заглавная буква и одна цифра.',
|
||
or: 'или',
|
||
telegramBotNotConfigured: 'Telegram-бот не настроен администратором.',
|
||
telegramQrHint:
|
||
'Перейдите по ссылке или отсканируйте QR-код — откроется бот в Telegram. Если открываете его впервые, нажмите «Старт».',
|
||
waitingForConfirmation: 'Ожидание подтверждения в Telegram…',
|
||
telegramLoginRejected: 'Вход отклонён в Telegram.',
|
||
telegramLoginExpired: 'Время ожидания истекло, попробуйте снова.',
|
||
},
|
||
|
||
telegramLinkWarning: {
|
||
message: 'Вы не привязали Telegram — без этого вы не будете получать уведомления и не сможете самостоятельно восстановить пароль.',
|
||
action: 'Привязать Telegram',
|
||
dismiss: 'Скрыть',
|
||
},
|
||
|
||
nav: {
|
||
dashboard: 'Мои конфиги',
|
||
instructions: 'Инструкции',
|
||
news: 'Новости',
|
||
support: 'Поддержка',
|
||
settings: 'Настройки',
|
||
admin: 'Админка',
|
||
logout: 'Выйти',
|
||
toggleMenu: 'Меню',
|
||
},
|
||
|
||
activation: {
|
||
title: 'Аккаунт не активирован',
|
||
description:
|
||
'Чтобы создавать конфиги, дождитесь активации администратором. Можно оставить комментарий к заявке.',
|
||
commentLabel: 'Комментарий (необязательно)',
|
||
submit: 'Запросить активацию',
|
||
pending: 'Заявка на активацию отправлена, ожидайте решения администратора.',
|
||
alreadyPending: 'У вас уже есть необработанная заявка на активацию.',
|
||
retry: 'Повторить',
|
||
},
|
||
|
||
configs: {
|
||
title: 'Мои конфиги',
|
||
quota: 'Использовано {{used}} из {{max}}',
|
||
quotaUnlimited: 'Использовано {{used}}, без лимита',
|
||
empty: 'У вас пока нет конфигов. Создайте первый.',
|
||
create: 'Создать конфиг',
|
||
selectLocation: 'Выберите локацию',
|
||
location: 'Локация',
|
||
label: 'Метка (необязательно)',
|
||
noInboundsNotice: 'Пока нет доступных локаций для создания конфига. Обратитесь к администратору — необходимо, чтобы он добавил сервер.',
|
||
created: 'Конфиг создан.',
|
||
quotaExceeded: 'Достигнут лимит конфигов для вашей роли.',
|
||
showLink: 'Ссылка / QR',
|
||
rotate: 'Перевыпустить',
|
||
rotated: 'Конфиг перевыпущен.',
|
||
revoked: 'Конфиг отозван.',
|
||
confirmRevoke: 'Отозвать этот конфиг? Действие необратимо.',
|
||
copied: 'Скопировано.',
|
||
copyLink: 'Скопировать ссылку',
|
||
loadingLink: 'Загрузка ссылки…',
|
||
subscriptionLink: 'Ссылка-подписка (для клиента):',
|
||
panelEmail: 'Email в панели: {{email}}',
|
||
aggregatedSubscription: 'Общая подписка',
|
||
aggregatedSubscriptionHint: 'Одна ссылка/QR со всеми активными конфигами — удобно добавить один раз в клиент.',
|
||
status: {
|
||
Active: 'Активен',
|
||
Disabled: 'Отключён',
|
||
Expired: 'Истёк',
|
||
LimitReached: 'Лимит исчерпан',
|
||
Revoked: 'Отозван',
|
||
},
|
||
},
|
||
|
||
instructions: {
|
||
title: 'Инструкции по подключению',
|
||
intro: 'Как подключиться за три шага — на любом устройстве.',
|
||
step1: 'Установите приложение для вашей ОС из списка ниже.',
|
||
step2: 'На странице «Мои конфиги» скопируйте ссылку или откройте QR-код нужного конфига.',
|
||
step3: 'Импортируйте ссылку или отсканируйте QR в приложении — готово.',
|
||
appsTitle: 'Приложения',
|
||
noApps: 'Каталог приложений пока пуст.',
|
||
os: {
|
||
IOS: 'iOS',
|
||
Android: 'Android',
|
||
Windows: 'Windows',
|
||
MacOS: 'macOS',
|
||
Linux: 'Linux',
|
||
},
|
||
},
|
||
|
||
news: {
|
||
title: 'Новости',
|
||
empty: 'Пока нет новостей.',
|
||
},
|
||
|
||
support: {
|
||
title: 'Поддержка',
|
||
empty: 'У вас пока нет обращений.',
|
||
reportBug: 'Сообщить об ошибке',
|
||
requestRole: 'Запросить роль',
|
||
submit: 'Отправить',
|
||
messageLabel: 'Опишите проблему или предложение',
|
||
attachmentsLabel: 'Скриншоты (необязательно, до 5)',
|
||
filesSelected: '{{count}} файл(ов) выбрано',
|
||
existingRole: 'Существующая роль',
|
||
newRole: 'Новая роль',
|
||
selectRole: 'Выберите роль',
|
||
newRoleName: 'Название роли',
|
||
newRoleMaxConfigs: 'Количество конфигов (-1 — без лимита)',
|
||
newRoleMaxIpLimit: 'Количество IP (-1 — без лимита)',
|
||
justification: 'Обоснование',
|
||
ticketCreated: 'Обращение отправлено.',
|
||
roleRequestPending: 'У вас уже есть необработанная заявка на роль.',
|
||
reply: 'Ответить',
|
||
replyPlaceholder: 'Написать комментарий…',
|
||
reopen: 'Переоткрыть',
|
||
reopened: 'Тикет переоткрыт.',
|
||
lastActivity: 'Последняя активность: {{date}}',
|
||
requestedExistingRole: 'Запрошена роль: {{role}}',
|
||
requestedNewRole: 'Запрошена новая роль «{{name}}» (конфигов: {{configs}}, IP: {{ip}})',
|
||
type: {
|
||
BugReport: 'Ошибка/предложение',
|
||
RoleRequest: 'Заявка на роль',
|
||
},
|
||
status: {
|
||
Open: 'Открыт',
|
||
Resolved: 'Решён',
|
||
Closed: 'Закрыт',
|
||
},
|
||
},
|
||
|
||
settings: {
|
||
changePassword: 'Сменить пароль',
|
||
currentPassword: 'Текущий пароль',
|
||
currentPasswordRequired: 'Введите текущий пароль.',
|
||
currentPasswordInvalid: 'Неверный текущий пароль.',
|
||
newPassword: 'Новый пароль',
|
||
passwordChanged: 'Пароль изменён.',
|
||
changeUserName: 'Сменить логин',
|
||
changeUserNameHint: 'Текущий логин: {{current}}.',
|
||
newUserName: 'Новый логин',
|
||
userNameChanged: 'Логин изменён.',
|
||
telegramHint: 'Привязка Telegram нужна для входа без пароля и восстановления доступа.',
|
||
telegramLinkedStatus: 'Привязан',
|
||
link: 'Привязать Telegram',
|
||
unlink: 'Отвязать',
|
||
confirmUnlink: 'Отвязать Telegram от аккаунта?',
|
||
telegramLinked: 'Telegram привязан.',
|
||
telegramUnlinked: 'Telegram отвязан.',
|
||
waitingForLink: 'Ожидание подтверждения в Telegram…',
|
||
deleteAccount: 'Удалить аккаунт',
|
||
deleteAccountHint: 'Отзовёт все конфиги и безвозвратно удалит аккаунт.',
|
||
confirmDelete: 'Вы уверены? Это действие необратимо.',
|
||
confirmDeleteYes: 'Да, удалить',
|
||
cancel: 'Отмена',
|
||
},
|
||
|
||
admin: {
|
||
prev: 'Назад',
|
||
next: 'Вперёд',
|
||
tabs: {
|
||
overview: 'Обзор',
|
||
activation: 'Запросы на активацию',
|
||
users: 'Пользователи',
|
||
configs: 'Все конфиги',
|
||
roles: 'Роли',
|
||
nodes: 'Ноды',
|
||
apps: 'Приложения',
|
||
news: 'Новости',
|
||
support: 'Тикеты',
|
||
audit: 'Аудит',
|
||
maintenance: 'Обслуживание',
|
||
},
|
||
users: {
|
||
searchPlaceholder: 'Поиск по имени пользователя',
|
||
userName: 'Имя пользователя',
|
||
role: 'Роль',
|
||
statusLabel: 'Статус',
|
||
manage: 'Управление',
|
||
empty: 'Пользователи не найдены.',
|
||
total: 'Всего: {{count}}',
|
||
status: {
|
||
blocked: 'Заблокирован',
|
||
active: 'Активен',
|
||
pending: 'Не активирован',
|
||
},
|
||
block: 'Заблокировать',
|
||
unblock: 'Разблокировать',
|
||
confirmBlock: 'Заблокировать пользователя? Все его конфиги будут отключены.',
|
||
blocked: 'Пользователь заблокирован.',
|
||
unblocked: 'Пользователь разблокирован.',
|
||
roleChanged: 'Роль изменена.',
|
||
resetPassword: 'Сбросить пароль',
|
||
reset: 'Сбросить',
|
||
passwordReset: 'Пароль сброшен.',
|
||
configs: 'Конфиги',
|
||
delete: 'Удалить пользователя',
|
||
confirmDelete: 'Удалить пользователя? Все его конфиги будут отозваны в 3x-ui, действие необратимо.',
|
||
deleted: 'Пользователь удалён.',
|
||
},
|
||
configs: {
|
||
searchPlaceholder: 'Поиск по email в панели или метке',
|
||
owner: 'Владелец',
|
||
node: 'Нода',
|
||
location: 'Локация',
|
||
protocol: 'Протокол',
|
||
traffic: 'Трафик',
|
||
statusLabel: 'Статус',
|
||
allStatuses: 'Все статусы',
|
||
created: 'Создан',
|
||
empty: 'Конфиги не найдены.',
|
||
total: 'Всего: {{count}}',
|
||
},
|
||
activation: {
|
||
empty: 'Нет ожидающих запросов на активацию.',
|
||
approved: 'Пользователь активирован.',
|
||
rejected: 'Запрос отклонён.',
|
||
approve: 'Активировать',
|
||
reject: 'Отклонить',
|
||
},
|
||
roles: {
|
||
create: 'Создать роль',
|
||
name: 'Название',
|
||
maxConfigs: 'Квота конфигов',
|
||
maxConfigsHint: '−1 = без лимита.',
|
||
maxIpLimit: 'Лимит IP на конфиг',
|
||
maxIpLimitHint: '−1 = без лимита. Применяется только к новым конфигам.',
|
||
system: 'системная',
|
||
edit: 'Изменить',
|
||
save: 'Сохранить',
|
||
delete: 'Удалить',
|
||
confirmDelete: 'Удалить роль? Это действие необратимо.',
|
||
created: 'Роль создана.',
|
||
updated: 'Квота обновлена.',
|
||
deleted: 'Роль удалена.',
|
||
},
|
||
nodes: {
|
||
create: 'Добавить ноду',
|
||
name: 'Название',
|
||
baseAddress: 'Адрес панели',
|
||
username: 'Логин',
|
||
password: 'Пароль',
|
||
location: 'Локация',
|
||
empty: 'Ноды не добавлены.',
|
||
created: 'Нода добавлена.',
|
||
updated: 'Нода обновлена.',
|
||
deleted: 'Нода удалена.',
|
||
confirmDelete: 'Удалить ноду? Существующие конфиги на ней перестанут синхронизироваться.',
|
||
edit: 'Изменить',
|
||
delete: 'Удалить',
|
||
probe: 'Проверить',
|
||
sync: 'Синхронизировать',
|
||
probeSuccess: 'Нода доступна.',
|
||
probeFailure: 'Нода недоступна: {{message}}',
|
||
syncSuccess: 'Синхронизировано инбаундов: {{count}}',
|
||
status: {
|
||
Unknown: 'Неизвестно',
|
||
Online: 'Онлайн',
|
||
Offline: 'Офлайн',
|
||
},
|
||
enabled: 'Включена',
|
||
disabled: 'Отключена',
|
||
inbounds: 'Инбаунды',
|
||
noInbounds: 'Инбаунды не найдены — нажмите «Синхронизировать».',
|
||
publish: 'Публикация',
|
||
published: 'Опубликован',
|
||
unpublished: 'Не опубликован',
|
||
publishSaved: 'Настройки публикации сохранены.',
|
||
displayName: 'Отображаемое имя',
|
||
maxClients: 'Лимит клиентов (необязательно)',
|
||
allowedRoles: 'Доступно ролям',
|
||
isPublishedLabel: 'Опубликовать инбаунд',
|
||
optional: 'необязательно',
|
||
},
|
||
apps: {
|
||
create: 'Добавить приложение',
|
||
name: 'Название',
|
||
downloadUrl: 'Ссылка на скачивание',
|
||
os: 'ОС',
|
||
description: 'Описание',
|
||
iconUrl: 'Ссылка на иконку',
|
||
sortOrder: 'Порядок',
|
||
enabled: 'Включено',
|
||
disabled: 'отключено',
|
||
empty: 'Каталог приложений пуст.',
|
||
created: 'Приложение добавлено.',
|
||
updated: 'Приложение обновлено.',
|
||
deleted: 'Приложение удалено.',
|
||
confirmDelete: 'Удалить приложение из каталога?',
|
||
},
|
||
news: {
|
||
create: 'Добавить новость',
|
||
title: 'Заголовок',
|
||
body: 'Текст (Markdown)',
|
||
preview: 'Предпросмотр',
|
||
empty: 'Новостей пока нет.',
|
||
created: 'Новость опубликована.',
|
||
updated: 'Новость обновлена.',
|
||
deleted: 'Новость удалена.',
|
||
confirmDelete: 'Удалить новость?',
|
||
},
|
||
support: {
|
||
empty: 'Обращений пока нет.',
|
||
resolve: 'Решено',
|
||
resolved: 'Тикет отмечен как решённый.',
|
||
close: 'Закрыть',
|
||
closed: 'Тикет закрыт.',
|
||
approve: 'Одобрить',
|
||
approved: 'Заявка одобрена, роль выдана.',
|
||
reject: 'Отклонить',
|
||
rejected: 'Заявка отклонена.',
|
||
},
|
||
audit: {
|
||
time: 'Время',
|
||
action: 'Действие',
|
||
target: 'Объект',
|
||
source: 'Источник',
|
||
empty: 'Журнал аудита пуст.',
|
||
},
|
||
maintenance: {
|
||
closedTickets: {
|
||
title: 'Закрытые обращения',
|
||
description: 'Удалить все закрытые обращения вместе с перепиской и вложениями (файлами). Действие необратимо.',
|
||
action: 'Удалить закрытые обращения',
|
||
confirm: 'Удалить все закрытые обращения и файлы в них? Действие необратимо.',
|
||
deleted: 'Удалено обращений: {{count}}.',
|
||
},
|
||
audit: {
|
||
title: 'Журнал аудита',
|
||
description: 'Удалить записи журнала аудита старше указанного числа дней. Действие необратимо.',
|
||
olderThan: 'Старше',
|
||
days: 'дней',
|
||
action: 'Удалить старые записи',
|
||
confirm: 'Удалить записи аудита старше {{days}} дней? Действие необратимо.',
|
||
deleted: 'Удалено записей аудита: {{count}}.',
|
||
},
|
||
apps: {
|
||
title: 'Отключённые приложения',
|
||
description: 'Удалить все отключённые приложения из каталога. Действие необратимо.',
|
||
action: 'Удалить отключённые приложения',
|
||
confirm: 'Удалить все отключённые приложения? Действие необратимо.',
|
||
deleted: 'Удалено приложений: {{count}}.',
|
||
},
|
||
factoryReset: {
|
||
dangerZone: 'Опасная зона',
|
||
title: 'Полный сброс панели',
|
||
description: 'Вернуть панель в состояние только что развёрнутой — удалить всех пользователей, конфиги, ноды и все данные. Ваш текущий аккаунт сохранится.',
|
||
action: 'Сбросить панель',
|
||
dialogTitle: 'Полный сброс панели',
|
||
warningIntro: 'Будет удалено безвозвратно:',
|
||
warningItems: [
|
||
'Все пользователи, кроме вашего текущего аккаунта',
|
||
'Все VPN-конфиги (сначала будут отозваны на нодах 3x-ui)',
|
||
'Все ноды и инбаунды — регистрация серверов 3x-ui и сохранённые креды',
|
||
'Все обращения в поддержку вместе с перепиской и вложениями',
|
||
'Все новости',
|
||
'Весь журнал аудита',
|
||
'Все роли, кроме системных admin/user',
|
||
'Каталог приложений (будет пересеян из стартового набора)',
|
||
],
|
||
irreversible: 'Действие необратимо и не может быть отменено.',
|
||
confirmPhrase: 'СБРОС',
|
||
confirmInputLabel: 'Введите «{{phrase}}» для подтверждения',
|
||
confirmButton: 'Сбросить панель безвозвратно',
|
||
done: 'Панель сброшена до начального состояния.',
|
||
},
|
||
},
|
||
stats: {
|
||
totalUsers: 'Всего пользователей',
|
||
activatedUsers: 'Активировано',
|
||
pendingActivationRequests: 'Ожидают активации',
|
||
totalNodes: 'Всего нод',
|
||
onlineNodes: 'Нод онлайн',
|
||
totalConfigs: 'Всего конфигов',
|
||
activeConfigs: 'Активных конфигов',
|
||
totalTraffic: 'Суммарный трафик',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
en: {
|
||
translation: {
|
||
appName: 'PnvPanel',
|
||
tagline: 'Self-service portal for VPN configurations',
|
||
theme: 'Theme',
|
||
language: 'Language',
|
||
light: 'Light',
|
||
dark: 'Dark',
|
||
system: 'System',
|
||
unlimited: 'Unlimited',
|
||
|
||
auth: {
|
||
loginTitle: 'Log in',
|
||
registerTitle: 'Register',
|
||
userName: 'Username',
|
||
password: 'Password',
|
||
submitLogin: 'Log in',
|
||
submitRegister: 'Register',
|
||
noAccount: "Don't have an account?",
|
||
haveAccount: 'Already have an account?',
|
||
goRegister: 'Register',
|
||
goLogin: 'Log in',
|
||
loginViaTelegram: 'Log in via Telegram',
|
||
invalidCredentials: 'Invalid username or password.',
|
||
duplicateUserName: 'A user with this name already exists.',
|
||
genericError: 'Something went wrong. Please try again.',
|
||
userNameHint: 'Latin letters, digits, "_", ".", "-", 3 to 32 characters.',
|
||
passwordHint: 'At least 8 characters, with one uppercase letter and one digit.',
|
||
or: 'or',
|
||
telegramBotNotConfigured: 'The Telegram bot has not been configured by the administrator.',
|
||
telegramQrHint:
|
||
'Follow the link or scan the QR code — it opens the bot in Telegram. If this is your first time, tap "Start".',
|
||
waitingForConfirmation: 'Waiting for confirmation in Telegram…',
|
||
telegramLoginRejected: 'Login was rejected in Telegram.',
|
||
telegramLoginExpired: 'The request expired, please try again.',
|
||
},
|
||
|
||
telegramLinkWarning: {
|
||
message: "You haven't linked Telegram — without it you won't receive notifications and won't be able to recover your password yourself.",
|
||
action: 'Link Telegram',
|
||
dismiss: 'Dismiss',
|
||
},
|
||
|
||
nav: {
|
||
dashboard: 'My configs',
|
||
instructions: 'Instructions',
|
||
news: 'News',
|
||
support: 'Support',
|
||
settings: 'Settings',
|
||
admin: 'Admin',
|
||
logout: 'Log out',
|
||
toggleMenu: 'Menu',
|
||
},
|
||
|
||
activation: {
|
||
title: 'Account not activated',
|
||
description:
|
||
'Wait for an administrator to activate your account before creating configs. You can leave a comment with your request.',
|
||
commentLabel: 'Comment (optional)',
|
||
submit: 'Request activation',
|
||
pending: 'Activation request sent, waiting for administrator review.',
|
||
alreadyPending: 'You already have a pending activation request.',
|
||
retry: 'Retry',
|
||
},
|
||
|
||
configs: {
|
||
title: 'My configs',
|
||
quota: 'Used {{used}} of {{max}}',
|
||
quotaUnlimited: 'Used {{used}}, unlimited',
|
||
empty: "You don't have any configs yet. Create your first one.",
|
||
create: 'Create config',
|
||
selectLocation: 'Select location',
|
||
location: 'Location',
|
||
label: 'Label (optional)',
|
||
noInboundsNotice: 'No locations are available for creating a config yet. Please contact the administrator — a server needs to be added.',
|
||
created: 'Config created.',
|
||
quotaExceeded: 'Config quota reached for your role.',
|
||
showLink: 'Link / QR',
|
||
rotate: 'Rotate',
|
||
rotated: 'Config rotated.',
|
||
revoked: 'Config revoked.',
|
||
confirmRevoke: 'Revoke this config? This cannot be undone.',
|
||
copied: 'Copied.',
|
||
copyLink: 'Copy link',
|
||
loadingLink: 'Loading link…',
|
||
subscriptionLink: 'Subscription link (for the client app):',
|
||
panelEmail: 'Panel email: {{email}}',
|
||
aggregatedSubscription: 'Aggregated subscription',
|
||
aggregatedSubscriptionHint: 'One link/QR with all active configs — add it once to your client.',
|
||
status: {
|
||
Active: 'Active',
|
||
Disabled: 'Disabled',
|
||
Expired: 'Expired',
|
||
LimitReached: 'Limit reached',
|
||
Revoked: 'Revoked',
|
||
},
|
||
},
|
||
|
||
instructions: {
|
||
title: 'Connection instructions',
|
||
intro: 'Get connected in three steps, on any device.',
|
||
step1: 'Install the app for your OS from the list below.',
|
||
step2: 'On the "My configs" page, copy the link or open the QR code for the config you want.',
|
||
step3: 'Import the link or scan the QR code in the app — done.',
|
||
appsTitle: 'Apps',
|
||
noApps: 'The app catalog is empty right now.',
|
||
os: {
|
||
IOS: 'iOS',
|
||
Android: 'Android',
|
||
Windows: 'Windows',
|
||
MacOS: 'macOS',
|
||
Linux: 'Linux',
|
||
},
|
||
},
|
||
|
||
news: {
|
||
title: 'News',
|
||
empty: 'No news yet.',
|
||
},
|
||
|
||
support: {
|
||
title: 'Support',
|
||
empty: 'You have no tickets yet.',
|
||
reportBug: 'Report a bug',
|
||
requestRole: 'Request a role',
|
||
submit: 'Submit',
|
||
messageLabel: 'Describe the issue or suggestion',
|
||
attachmentsLabel: 'Screenshots (optional, up to 5)',
|
||
filesSelected: '{{count}} file(s) selected',
|
||
existingRole: 'Existing role',
|
||
newRole: 'New role',
|
||
selectRole: 'Select a role',
|
||
newRoleName: 'Role name',
|
||
newRoleMaxConfigs: 'Max configs (-1 = unlimited)',
|
||
newRoleMaxIpLimit: 'Max IPs (-1 = unlimited)',
|
||
justification: 'Justification',
|
||
ticketCreated: 'Ticket submitted.',
|
||
roleRequestPending: 'You already have a pending role request.',
|
||
reply: 'Reply',
|
||
replyPlaceholder: 'Write a comment…',
|
||
reopen: 'Reopen',
|
||
reopened: 'Ticket reopened.',
|
||
lastActivity: 'Last activity: {{date}}',
|
||
requestedExistingRole: 'Requested role: {{role}}',
|
||
requestedNewRole: 'Requested new role "{{name}}" (configs: {{configs}}, IPs: {{ip}})',
|
||
type: {
|
||
BugReport: 'Bug/suggestion',
|
||
RoleRequest: 'Role request',
|
||
},
|
||
status: {
|
||
Open: 'Open',
|
||
Resolved: 'Resolved',
|
||
Closed: 'Closed',
|
||
},
|
||
},
|
||
|
||
settings: {
|
||
changePassword: 'Change password',
|
||
currentPassword: 'Current password',
|
||
currentPasswordRequired: 'Enter your current password.',
|
||
currentPasswordInvalid: 'Current password is incorrect.',
|
||
newPassword: 'New password',
|
||
passwordChanged: 'Password changed.',
|
||
changeUserName: 'Change username',
|
||
changeUserNameHint: 'Current username: {{current}}.',
|
||
newUserName: 'New username',
|
||
userNameChanged: 'Username changed.',
|
||
telegramHint: 'Linking Telegram enables passwordless login and account recovery.',
|
||
telegramLinkedStatus: 'Linked',
|
||
link: 'Link Telegram',
|
||
unlink: 'Unlink',
|
||
confirmUnlink: 'Unlink Telegram from your account?',
|
||
telegramLinked: 'Telegram linked.',
|
||
telegramUnlinked: 'Telegram unlinked.',
|
||
waitingForLink: 'Waiting for confirmation in Telegram…',
|
||
deleteAccount: 'Delete account',
|
||
deleteAccountHint: 'Revokes all configs and permanently deletes your account.',
|
||
confirmDelete: 'Are you sure? This cannot be undone.',
|
||
confirmDeleteYes: 'Yes, delete',
|
||
cancel: 'Cancel',
|
||
},
|
||
|
||
admin: {
|
||
prev: 'Previous',
|
||
next: 'Next',
|
||
tabs: {
|
||
overview: 'Overview',
|
||
activation: 'Activation requests',
|
||
users: 'Users',
|
||
configs: 'All configs',
|
||
roles: 'Roles',
|
||
nodes: 'Nodes',
|
||
apps: 'Apps',
|
||
news: 'News',
|
||
support: 'Tickets',
|
||
audit: 'Audit',
|
||
maintenance: 'Maintenance',
|
||
},
|
||
users: {
|
||
searchPlaceholder: 'Search by username',
|
||
userName: 'Username',
|
||
role: 'Role',
|
||
statusLabel: 'Status',
|
||
manage: 'Manage',
|
||
empty: 'No users found.',
|
||
total: 'Total: {{count}}',
|
||
status: {
|
||
blocked: 'Blocked',
|
||
active: 'Active',
|
||
pending: 'Not activated',
|
||
},
|
||
block: 'Block',
|
||
unblock: 'Unblock',
|
||
confirmBlock: 'Block this user? All their configs will be disabled.',
|
||
blocked: 'User blocked.',
|
||
unblocked: 'User unblocked.',
|
||
roleChanged: 'Role changed.',
|
||
resetPassword: 'Reset password',
|
||
reset: 'Reset',
|
||
passwordReset: 'Password reset.',
|
||
configs: 'Configs',
|
||
delete: 'Delete user',
|
||
confirmDelete: 'Delete this user? All their configs will be revoked in 3x-ui — this cannot be undone.',
|
||
deleted: 'User deleted.',
|
||
},
|
||
configs: {
|
||
searchPlaceholder: 'Search by panel email or label',
|
||
owner: 'Owner',
|
||
node: 'Node',
|
||
location: 'Location',
|
||
protocol: 'Protocol',
|
||
traffic: 'Traffic',
|
||
statusLabel: 'Status',
|
||
allStatuses: 'All statuses',
|
||
created: 'Created',
|
||
empty: 'No configs found.',
|
||
total: 'Total: {{count}}',
|
||
},
|
||
activation: {
|
||
empty: 'No pending activation requests.',
|
||
approved: 'User activated.',
|
||
rejected: 'Request rejected.',
|
||
approve: 'Approve',
|
||
reject: 'Reject',
|
||
},
|
||
roles: {
|
||
create: 'Create role',
|
||
name: 'Name',
|
||
maxConfigs: 'Config quota',
|
||
maxConfigsHint: '−1 = unlimited.',
|
||
maxIpLimit: 'IP limit per config',
|
||
maxIpLimitHint: '−1 = unlimited. Applies to new configs only.',
|
||
system: 'system',
|
||
edit: 'Edit',
|
||
save: 'Save',
|
||
delete: 'Delete',
|
||
confirmDelete: 'Delete this role? This cannot be undone.',
|
||
created: 'Role created.',
|
||
updated: 'Quota updated.',
|
||
deleted: 'Role deleted.',
|
||
},
|
||
nodes: {
|
||
create: 'Add node',
|
||
name: 'Name',
|
||
baseAddress: 'Panel address',
|
||
username: 'Username',
|
||
password: 'Password',
|
||
location: 'Location',
|
||
empty: 'No nodes added yet.',
|
||
created: 'Node added.',
|
||
updated: 'Node updated.',
|
||
deleted: 'Node deleted.',
|
||
confirmDelete: 'Delete this node? Existing configs on it will stop syncing.',
|
||
edit: 'Edit',
|
||
delete: 'Delete',
|
||
probe: 'Probe',
|
||
sync: 'Sync',
|
||
probeSuccess: 'Node is reachable.',
|
||
probeFailure: 'Node unreachable: {{message}}',
|
||
syncSuccess: 'Synced inbounds: {{count}}',
|
||
status: {
|
||
Unknown: 'Unknown',
|
||
Online: 'Online',
|
||
Offline: 'Offline',
|
||
},
|
||
enabled: 'Enabled',
|
||
disabled: 'Disabled',
|
||
inbounds: 'Inbounds',
|
||
noInbounds: 'No inbounds found — click "Sync".',
|
||
publish: 'Publishing',
|
||
published: 'Published',
|
||
unpublished: 'Not published',
|
||
publishSaved: 'Publishing settings saved.',
|
||
displayName: 'Display name',
|
||
maxClients: 'Client limit (optional)',
|
||
allowedRoles: 'Allowed for roles',
|
||
isPublishedLabel: 'Publish inbound',
|
||
optional: 'optional',
|
||
},
|
||
apps: {
|
||
create: 'Add app',
|
||
name: 'Name',
|
||
downloadUrl: 'Download link',
|
||
os: 'OS',
|
||
description: 'Description',
|
||
iconUrl: 'Icon URL',
|
||
sortOrder: 'Sort order',
|
||
enabled: 'Enabled',
|
||
disabled: 'disabled',
|
||
empty: 'The app catalog is empty.',
|
||
created: 'App added.',
|
||
updated: 'App updated.',
|
||
deleted: 'App deleted.',
|
||
confirmDelete: 'Remove this app from the catalog?',
|
||
},
|
||
news: {
|
||
create: 'Add post',
|
||
title: 'Title',
|
||
body: 'Body (Markdown)',
|
||
preview: 'Preview',
|
||
empty: 'No news yet.',
|
||
created: 'Post published.',
|
||
updated: 'Post updated.',
|
||
deleted: 'Post deleted.',
|
||
confirmDelete: 'Delete this post?',
|
||
},
|
||
support: {
|
||
empty: 'No tickets yet.',
|
||
resolve: 'Resolve',
|
||
resolved: 'Ticket marked as resolved.',
|
||
close: 'Close',
|
||
closed: 'Ticket closed.',
|
||
approve: 'Approve',
|
||
approved: 'Request approved, role granted.',
|
||
reject: 'Reject',
|
||
rejected: 'Request rejected.',
|
||
},
|
||
audit: {
|
||
time: 'Time',
|
||
action: 'Action',
|
||
target: 'Target',
|
||
source: 'Source',
|
||
empty: 'The audit log is empty.',
|
||
},
|
||
maintenance: {
|
||
closedTickets: {
|
||
title: 'Closed tickets',
|
||
description: 'Delete all closed tickets along with their comments and attachments (files). This cannot be undone.',
|
||
action: 'Delete closed tickets',
|
||
confirm: 'Delete all closed tickets and their files? This cannot be undone.',
|
||
deleted: 'Deleted tickets: {{count}}.',
|
||
},
|
||
audit: {
|
||
title: 'Audit log',
|
||
description: 'Delete audit log entries older than the given number of days. This cannot be undone.',
|
||
olderThan: 'Older than',
|
||
days: 'days',
|
||
action: 'Delete old entries',
|
||
confirm: 'Delete audit entries older than {{days}} days? This cannot be undone.',
|
||
deleted: 'Deleted audit entries: {{count}}.',
|
||
},
|
||
apps: {
|
||
title: 'Disabled apps',
|
||
description: 'Delete all disabled apps from the catalog. This cannot be undone.',
|
||
action: 'Delete disabled apps',
|
||
confirm: 'Delete all disabled apps? This cannot be undone.',
|
||
deleted: 'Deleted apps: {{count}}.',
|
||
},
|
||
factoryReset: {
|
||
dangerZone: 'Danger zone',
|
||
title: 'Full panel reset',
|
||
description: 'Return the panel to a freshly-deployed state — deletes all users, configs, nodes and data. Your current account is kept.',
|
||
action: 'Reset panel',
|
||
dialogTitle: 'Full panel reset',
|
||
warningIntro: 'This will permanently delete:',
|
||
warningItems: [
|
||
'All users except your current account',
|
||
'All VPN configs (revoked on the 3x-ui nodes first)',
|
||
'All nodes and inbounds — 3x-ui server registrations and stored credentials',
|
||
'All support tickets with their comments and attachments',
|
||
'All news posts',
|
||
'The entire audit log',
|
||
'All roles except the system admin/user roles',
|
||
'The app catalog (will be re-seeded from the default set)',
|
||
],
|
||
irreversible: 'This cannot be undone.',
|
||
confirmPhrase: 'RESET',
|
||
confirmInputLabel: 'Type "{{phrase}}" to confirm',
|
||
confirmButton: 'Reset the panel permanently',
|
||
done: 'The panel has been reset to its initial state.',
|
||
},
|
||
},
|
||
stats: {
|
||
totalUsers: 'Total users',
|
||
activatedUsers: 'Activated',
|
||
pendingActivationRequests: 'Pending activation',
|
||
totalNodes: 'Total nodes',
|
||
onlineNodes: 'Nodes online',
|
||
totalConfigs: 'Total configs',
|
||
activeConfigs: 'Active configs',
|
||
totalTraffic: 'Total traffic',
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
const STORAGE_KEY = 'pnv-lang'
|
||
const stored = typeof localStorage !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null
|
||
|
||
void i18n.use(initReactI18next).init({
|
||
resources,
|
||
lng: stored ?? 'ru',
|
||
fallbackLng: 'ru',
|
||
interpolation: { escapeValue: false },
|
||
})
|
||
|
||
export function setLanguage(lng: string) {
|
||
localStorage.setItem(STORAGE_KEY, lng)
|
||
void i18n.changeLanguage(lng)
|
||
}
|
||
|
||
export default i18n
|