Files
PnvPanel/frontend/src/shared/lib/i18n.ts
T
Leonid Pershin b6637a1c03
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s
Add news feature with CRUD operations and real-time notifications
- Implemented news management functionality, allowing admins to create, read, update, and delete news posts.
- Introduced a new SignalR event for broadcasting news updates to all connected clients.
- Updated API documentation to include new endpoints for news management.
- Enhanced frontend with a dedicated news page and admin interface for managing news posts.
- Added necessary localization for news-related terms in both Russian and English.
2026-07-03 15:28:33 +03:00

597 lines
25 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 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: 'Новости',
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: 'Ссылка-подписка (для клиента):',
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: 'Пока нет новостей.',
},
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: 'Пользователи',
roles: 'Роли',
nodes: 'Ноды',
apps: 'Приложения',
news: 'Новости',
audit: 'Аудит',
},
users: {
searchPlaceholder: 'Поиск по имени пользователя',
userName: 'Имя пользователя',
role: 'Роль',
statusLabel: 'Статус',
manage: 'Управление',
empty: 'Пользователи не найдены.',
total: 'Всего: {{count}}',
status: {
blocked: 'Заблокирован',
active: 'Активен',
pending: 'Не активирован',
},
block: 'Заблокировать',
unblock: 'Разблокировать',
confirmBlock: 'Заблокировать пользователя? Все его конфиги будут отключены.',
blocked: 'Пользователь заблокирован.',
unblocked: 'Пользователь разблокирован.',
roleChanged: 'Роль изменена.',
resetPassword: 'Сбросить пароль',
reset: 'Сбросить',
passwordReset: 'Пароль сброшен.',
configs: 'Конфиги',
},
activation: {
empty: 'Нет ожидающих запросов на активацию.',
approved: 'Пользователь активирован.',
rejected: 'Запрос отклонён.',
approve: 'Активировать',
reject: 'Отклонить',
},
roles: {
create: 'Создать роль',
name: 'Название',
maxConfigs: 'Квота конфигов',
maxConfigsHint: '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: 'Удалить новость?',
},
audit: {
time: 'Время',
action: 'Действие',
target: 'Объект',
source: 'Источник',
empty: 'Журнал аудита пуст.',
},
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',
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):',
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.',
},
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',
roles: 'Roles',
nodes: 'Nodes',
apps: 'Apps',
news: 'News',
audit: 'Audit',
},
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',
},
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.',
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?',
},
audit: {
time: 'Time',
action: 'Action',
target: 'Target',
source: 'Source',
empty: 'The audit log is empty.',
},
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