Двуязычный интерфейс (RU/EN) с автоопределением + документация на двух языках

Панель:
- engine/main/lang.php: класс Lang, автоопределение языка (?lang= -> cookie -> Accept-Language -> конфиг)
- перевод применяется к готовому ответу через ob_start(): покрывает всю панель, админку и письма,
  не требуя правки 200 файлов шаблонов; отсутствующая фраза остаётся русской
- замена только на границах слов, иначе короткий ключ портил длинные слова (Модуль -> Modуль)
- AJAX-ответы переводятся отдельно (json_encode экранирует кириллицу в \uXXXX)
- application/lang/en.php: 657 переводов; ru.php как точка расширения
- переключатель RU/EN в шапке кабинета, админки и в подвале страницы входа
- 'lang' в config.php — язык по умолчанию
- проверено обходом 20 разделов: 0 непереведённых фраз, 0 мешанины языков, 0 фаталов

Документация — теперь на русском и английском:
- README.en.md, CHANGES.en.md, SECURITY.en.md, GUIDE.en.md
- переключатели языка в начале каждого документа
- раздел «Язык интерфейса» в инструкции: как работает, как добавить свой язык
This commit is contained in:
2026-07-30 02:25:34 +00:00
parent 6ee2744e3a
commit e0b96d3501
16 changed files with 2145 additions and 11 deletions
+220
View File
@@ -0,0 +1,220 @@
[Русский](ИЗМЕНЕНИЯ.md) · **English**
# What changed compared to the original
Original: **HostinPL 5.6** as a "nulled" build (the `Xopowblu-4EJlOBEK/HostinPL-5.6` fork),
written for Debian 9 and PHP 7.0.
Below is everything we changed, with file names and line numbers. Panel code changes live in `panel/`.
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
---
## 1. The panel now runs on modern PHP
The original targets PHP 7.0. On PHP 8 parts of it died outright. It now runs on **PHP 8.4**:
walking all 21 sections (home, servers, news, status, tickets, web hosting and the whole admin area)
returns **200 on every one and zero errors in the log**.
| File | Before | After |
|------|--------|-------|
| `engine/engine_ftp/elFinder.class.php:4497` | `utf8_encode()` — removed in PHP 8.2, the file manager died with a fatal error | Wrapped in `function_exists()`, falls back to `mb_convert_encoding($str,'UTF-8','ISO-8859-1')` |
| `application/views/admin/checksys/index.php:22` | `apache_get_modules()` — only exists under Apache with mod_php, so the "System check" page returned 500 under nginx | Wrapped in `function_exists()`, otherwise checked via `REQUEST_URI` |
| `application/views/admin/index.php:215` | `$item['invoice_ammount']` used after the `foreach`, producing an Undefined variable when there were no invoices | `isset()` check |
| `application/models/users.php:199-206` | `$city[1]`, `$country[1]`, `$countryCode[1]` used without checking that the regex matched | `isset()` checks |
| `application/controllers/common/loginheader.php:35` | `$_GET['ref']` without `isset` | `isset()` plus a cast to `(int)` |
### What turned out to be a false alarm
The bundled **phpseclib 1.x** uses `create_function()`, which PHP 8 removed. It looked like a blocker,
but checking showed that **phpseclib is not included anywhere in the panel** — it is dead code.
Communication with game nodes goes through the native `php-ssh2` extension
(`engine/libs/ssh2.php``ssh2_exec`). Nothing needed fixing.
`get_magic_quotes_gpc()` in `elFinderConnector.class.php:320` is equally harmless: it sits behind
`version_compare(PHP_VERSION,'5.4','<') && ...`, so on PHP 8 it is never reached.
---
## 2. The captcha is off and managed from the admin area
The captcha used to be hard-wired: without valid Google keys **it was impossible to log in**,
and the form displayed "ERROR for site owner: Invalid site key". There is now a `captcha_enable`
flag in `application/config.php`, defaulting to `0` (off).
**Backend.** All four validators received the line
`if($this->config->captcha_enable != '1') return $result;` before the captcha check:
* `application/controllers/account/login.php` — login
* `application/controllers/common/loginheader.php` — registration and the contact form (2 places)
* `application/controllers/tickets/create.php` — ticket creation
**Markup.** The five captcha widgets (4 in `views/common/loginheader.php`, 1 in
`views/tickets/create.php`) are wrapped in `<?php if(@$captcha_enable == '1'): ?>`. Google's `api.js`
is only loaded when the captcha is on. Every `grecaptcha.reset(...)` call became
`window.grecaptcha && grecaptcha.reset(...)` — otherwise, with the captcha off, JavaScript threw
inside the error handlers and the forms stopped responding.
**Admin area.** `views/admin/settings.php`, the "Other settings" tab, gained a
"Bot protection (reCAPTCHA v2)" block: an Off/On selector plus Site key and Secret key fields.
Verified both ways: with the captcha off, login and registration succeed and the user is really
created in the database; with it on, a fresh session gets "Confirm that you are not a robot!"
and the widget returns to the page.
> **Careful when adding your own settings.** The settings writer matches configuration lines
> **by substring** (`strpos`). That is why the flag is called `captcha_enable` and not `captcha`:
> the string `captcha` occurs inside `recaptcha` and `secret_recaptcha`, and saving would have
> overwritten the wrong parameter. New keys must not be substrings of existing ones.
---
## 3. Vulnerabilities fixed
In brief (full detail with code in [SECURITY.en.md](SECURITY.en.md)):
* **Two pre-authentication SQL injections** in `application/models/users.php` (`createAuthLog()`):
the login journal received the password from the form and the IP from the `CF-Connecting-IP`
header — which was not validated at all — without escaping. Every value now goes through
`$this->db->escape()` or a cast to `(int)`, and the header is validated with
`filter_var(..., FILTER_VALIDATE_IP)`.
* **Plaintext passwords**: `login.php` wrote the real password into the `authlog` table on every
login attempt, including failed ones. Removed.
* **XSS** in the hidden `ref` field of the registration form — `htmlspecialchars()` added.
* The request to the `ip-api.com` geolocation service now only runs for a valid IP and uses `urlencode()`.
---
## 4. The installer was rewritten from scratch
The original `install` was dangerous on any current system:
* `echo "deb ... stretch main" > /etc/apt/sources.list`**wiped the repository list** and replaced
it with Debian 9, after which apt was broken
* installed `php7.0` and hard-coded edits to `/etc/php/7.0/apache2/php.ini`
* switched MariaDB to `bind-address = 0.0.0.0` without a word of warning
* only ran when `/etc/issue.net` said `Debian9`, and otherwise refused to start
* generated passwords and tokens but saved them nowhere — you could only copy them off the screen
* carried on after any failure: every command ended in `> /dev/null 2>&1`
The new scripts:
**`install-panel.sh`** — nginx, PHP 8.x (version detected automatically), MariaDB, a database with a
random password, configuration, scheduler, autostart watchdog, administrator creation, and a final
check that the login page really is served with its form. Idempotent: running it again does not wipe
a database that is already loaded. Credentials are written to `/root/.redl-panel-credentials`
(chmod 600). With `set -euo pipefail` the script stops on error instead of pretending all is well.
**`install-node.sh`** — Docker from the official repository, image build, the
`/home/cp/gameservers/files` layout, the `gameservers` group, MariaDB for game server databases,
SteamCMD, ProFTPD, and sshd configuration that rolls back if `sshd -t` fails. **Before doing anything
it checks whether Docker can work on this machine at all** (`unshare -Ur`) and says so plainly if it
cannot. At the end it prints ready-to-use location details and firewall commands.
Neither script touches `/etc/apt/sources.list`.
---
## 5. The game server image was rebuilt
The original `docker/Dockerfile.original-stretch` is based on `debian:stretch`, and the Debian 9
repositories were shut down in 2023 — **that image no longer builds**, `apt-get update` inside it fails.
The new `docker/Dockerfile` is based on Debian 12 (bookworm) but **must still be tagged
`debian:stretch`**: that name is hard-coded in the panel
(`application/models/servers.php:642`, `docker create ... debian:stretch`) and cannot be changed
without editing the panel.
What is inside: 32-bit libraries (SA-MP, CRMP, MTA and older CS builds are i386), `screen` for
consoles, Java for Minecraft, Node.js 20 for RAGE:MP, and `gdb` for crash analysis.
Node.js comes from NodeSource **over HTTPS with repository key verification**, unlike before.
---
## 6. Working without systemd
The panel assumes systemd is present. Container VPSes do not have it, so watchdogs were added on
cron: `/usr/local/bin/hostinpl-guard` (panel) and `/usr/local/bin/gamenode-guard` (node).
Once a minute (every 2 minutes on a node) they check MariaDB, PHP-FPM, nginx, Docker and cron and
restart whatever died, plus an `@reboot` job to bring everything up after a restart.
Panel liveness is determined by an **HTTP request** to the login page rather than by searching for a
process name — a pattern search would have matched the watchdog's own command line.
---
## 7. Scheduler
The original pointed its 9 jobs at the panel's public domain. They now go to `127.0.0.1`
(independent of DNS and external reachability) and carry `-m` timeouts so that a hung request does
not pile up processes.
> **Token pitfall.** You cannot extract the scheduler token from the config with a plain
> `grep "'token'"` — the line `'yk_password1' => 'token'` matches the same pattern, two values end
> up in the URL separated by a newline, and the jobs then fail silently. The installer writes the
> token directly when it generates the configuration.
---
## 8. Interface languages: Russian and English
The original was Russian-only, with every string hard-coded in the templates. The build now has a
translation layer:
* `engine/main/lang.php` — the `Lang` class and the global `t()` helper
* `application/lang/ru.php`, `application/lang/en.php` — dictionaries
* Language selection order: `?lang=` parameter → `lang` cookie → **the browser's `Accept-Language`
header** → panel default
* A switcher (RU / EN) in the header of the client area and on the login page
* Anything not yet translated falls back to the original Russian text, so nothing can disappear
from the interface
Details and current coverage: see [GUIDE.en.md](GUIDE.en.md), the "Interface language" section.
---
## 9. Branding and dates
* The year in footers: `2020©``2026©` (`views/common/footer.php`, `views/common/loginheader.php`)
* Name and description are REDL.IO: `application/config.php` (`description`, `keywords`,
`mail_sender`, `mail_from`), footers, the "System check" page
* Links to the previous build owner's sites (`hostinpl.ru`, `osmp.ga`) replaced with `redl.io`
* "VK community" links pointing at somebody else's community replaced with `redl.io`
(`footer.php`, `views/main/index.php`, `views/offline/index.php`)
* All 15 e-mail templates: a "REDL.IO / AI-powered hosting" header and the signature
"Sincerely, the REDL.IO team"
**The authors' copyright headers were not removed** — they remain in every file that had them.
---
## 10. phpMyAdmin
The panel links to `/phpmyadmin` from the admin area. The original installer set phpMyAdmin up
through Apache, which did not work under nginx. It is now installed from the distribution repository
and served by nginx itself.
> **Reverse proxy pitfall.** For the address `/phpmyadmin` (without a trailing slash) nginx replies
> with a redirect and by default puts **its own port** into it, for example
> `http://panel.example.com:8095/phpmyadmin/`. If the panel sits behind a reverse proxy that address
> is unreachable from outside and the admin link does not open. The cure is
> `absolute_redirect off; port_in_redirect off;`, already present in the configuration the installer
> generates.
---
## What we did NOT do
* We did not rewrite password hashing. The panel stores passwords as **unsalted MD5**
(`md5($password)`, a `varchar(32)` column). Moving to `password_hash()` affects login,
registration, recovery and password changes, and requires migrating existing users.
* We did not move game server creation away from Docker.
* We did not delete `panel/application/public/js/proxy/proxy.php` — the file is defanged but kept as
evidence (see [SECURITY.en.md](SECURITY.en.md)).
* We did not test the payment gateways with real payments, nor VK login.
* We did not verify the game servers themselves: that needs a node with Docker.
---
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
+396
View File
@@ -0,0 +1,396 @@
[Русский](ИНСТРУКЦИЯ.md) · **English**
# Complete guide
From an empty server to a hosting service that can take clients.
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
Contents:
1. [How it all fits together](#1-how-it-all-fits-together)
2. [Installing the panel](#2-installing-the-panel)
3. [Installing a game node](#3-installing-a-game-node)
4. [Connecting the node to the panel](#4-connecting-the-node-to-the-panel)
5. [Game server files](#5-game-server-files)
6. [Configuring the panel](#6-configuring-the-panel)
7. [Interface language](#7-interface-language)
8. [Captcha](#8-captcha)
9. [HTTPS and domain](#9-https-and-domain)
10. [Maintenance](#10-maintenance)
11. [Troubleshooting](#11-troubleshooting)
---
## 1. How it all fits together
Two roles, usually on separate machines:
**The panel** — the website, the database, client areas, tickets, payments. It does not need Docker
and runs happily on a cheap VPS.
**A game node (location)** — where the game servers themselves run. The panel connects to it
**over SSH** and creates a separate Docker container for every ordered server:
```
docker create --tty --rm --name=gs<ID> --network=host \
--cpus="<cores>" --memory=<RAM>M \
--volume="/home/gs<ID>/:/home/container/" \
--workdir=/home/container debian:stretch
```
Hence two hard requirements for a node: **working Docker** and **a local image tagged
`debian:stretch`** (the name is hard-coded in the panel). Server files live on the node in
`/home/gs<ID>` and appear inside the container as `/home/container`.
You can have several nodes — they are added to the panel as separate locations, and the client
picks one when ordering.
> **Important about nodes.** Docker does not start inside container-based VPSes (LXC, OpenVZ and
> similar) because the kernel forbids nested namespaces. A node needs **a dedicated server or KVM**.
> The installer checks this first and warns you.
---
## 2. Installing the panel
Ubuntu 24.04 / 22.04 or Debian 12 / 13. Minimum: 1 core, 1 GB RAM, 10 GB disk.
```bash
apt-get update && apt-get install -y git
git clone https://github.com/RedlHosting/redl-gamepanel.git
cd redl-gamepanel
sudo bash install-panel.sh
```
The script asks three things:
* **the panel's domain or IP** — it goes into the configuration as the base address, and all links
and e-mails are built from it;
* **the administrator e-mail** — also the login;
* **the administrator password** — at least 6 characters.
Everything after that is automatic: packages, a database with a random password, the schema
(27 tables), configuration, permissions, PHP, nginx, the scheduler, an autostart watchdog, an
administrator with full access, and a final check that the login page opens.
It takes 25 minutes. The panel address and login are printed at the end.
**A non-standard port** (if 80 is taken, or the panel sits behind a reverse proxy):
```bash
sudo PORT=8095 bash install-panel.sh
```
**Where things end up:**
| What | Path |
|------|------|
| Panel code | `/var/www/hostinpl` |
| Configuration | `/var/www/hostinpl/application/config.php` |
| Database password and scheduler token | `/root/.redl-panel-credentials` |
| nginx configuration | `/etc/nginx/sites-available/hostinpl` |
| Scheduler | `/etc/cron.d/hostinpl` |
| Autostart watchdog | `/usr/local/bin/hostinpl-guard` |
| Database | MariaDB, database `hostin` |
**phpMyAdmin** (optional; the panel links to it from the admin area):
```bash
sudo apt-get install -y phpmyadmin # do NOT let it configure a web server, decline
sudo ln -sfn /usr/share/phpmyadmin /var/www/hostinpl/phpmyadmin
```
Log in with the database user from `/root/.redl-panel-credentials`. Remember that phpMyAdmin will be
reachable by anyone who knows the address: either restrict it by IP in nginx, or do not install it.
---
## 3. Installing a game node
You need **a dedicated server or KVM** (not LXC/OpenVZ). Minimum: 2 cores, 2 GB RAM, 100 GB disk,
counting 12 GB per game server.
```bash
apt-get update && apt-get install -y git
git clone https://github.com/RedlHosting/redl-gamepanel.git
cd redl-gamepanel
sudo bash install-node.sh
```
The script asks:
* **this node's IP** — how the panel will reach it;
* **whether to allow root SSH login with a password** — the panel can only connect with a login and
password, and its commands require root. Answer `yes` and the script generates a strong root
password and shows it at the end.
What it does: checks that the machine is suitable, installs Docker from the official repository,
builds the `debian:stretch` image (Debian 12 inside, plus 32-bit libraries, Java, Node.js and
screen), creates `/home/cp/gameservers/files` and the `gameservers` group, installs MariaDB for game
server databases, SteamCMD and ProFTPD, configures sshd, and installs a Docker watchdog.
The first run takes 515 minutes, mostly building the image.
**Close the ports immediately afterwards.** The script prints ready-made commands; substitute the
panel's address:
```bash
ufw allow from PANEL_IP to any port 22 proto tcp
ufw allow from PANEL_IP to any port 3306 proto tcp
ufw deny 3306
# leave the game ports open: 7777 (SA-MP), 22005 (RAGE:MP), 25565 (Minecraft) and so on
```
If you also connect to the node over SSH yourself, **allow your own IP first** or you will lock
yourself out:
```bash
ufw allow from YOUR_IP to any port 22 proto tcp
```
---
## 4. Connecting the node to the panel
1. Sign in to the panel as an administrator
2. Go to **Admin → Locations → Add location**
3. Fill in:
| Field | What to enter |
|-------|---------------|
| Name | Anything meaningful: "Frankfurt", "Helsinki" |
| IP | The node's IP |
| User | `root` |
| Password | The root password printed by `install-node.sh` |
| Games | Tick the games available on this node |
4. Save and open **Admin → Locations** — if the panel connected, the location shows its real cores,
RAM and disk (refreshed hourly by the scheduler).
If it does not connect, check from the panel machine:
```bash
ssh root@NODE_IP # does the password work?
php -m | grep ssh2 # is the php-ssh2 extension installed?
```
If the extension is missing: `apt-get install -y php-ssh2`, then restart PHP-FPM.
---
## 5. Game server files
**They are not included.** The original installer downloaded them from third-party sites over plain
HTTP with no signature checking, so we removed that (details in [SECURITY.en.md](SECURITY.en.md)).
Lay the files out on the node like this:
```
/home/cp/gameservers/files/<game_code>/
```
Game codes are listed under **Admin → Games** (for example `samp`, `crmp`, `mta`, `minecraft`,
`cs`, `css`, `ragemp`). When a server is created, the panel copies the contents of that directory
into `/home/gs<ID>/`.
Only take builds from primary sources: SA-MP from `sa-mp.com`, MTA from `mtasa.com`,
Minecraft (Paper) from `papermc.io`, RAGE:MP from `rage.mp`, Steam games via SteamCMD
(already installed in `/root/steamcmd`).
Then test the whole chain: order a server as a test client and confirm that it is created and starts.
---
## 6. Configuring the panel
**Admin → Settings**, four tabs:
* **General settings** — name, description, contacts, logo
* **Payment gateways** — Unitpay, Enot, AnyPay, LiteKassa, RoboKassa, FreeKassa, YooKassa, QIWI.
You need your own merchant accounts. We never tested a gateway with a real payment — test each one
with your own money before taking clients.
* **Other settings** — trial period, e-mail confirmation, **captcha**, maintenance mode, VK login
* **Information and bonuses** — top-up bonuses, referral percentages
Plans and games live under **Admin → Games** (versions, RAM and core limits, prices).
> When you save settings the panel rewrites `config.php` entirely. It matches configuration lines
> **by substring**, so do not create parameters whose names are contained in other names.
> After editing the file by hand, restart PHP-FPM **fully** (`restart`, not `reload`) — otherwise a
> cached copy is served.
---
## 7. Interface language
The panel speaks **Russian and English**. The language is chosen in this order:
1. `?lang=en` or `?lang=ru` in the URL — an explicit choice, remembered in a cookie for a year
2. the `lang` cookie — the previous choice
3. **the browser's `Accept-Language` header** — automatic detection
4. the `lang` value in `application/config.php` (default `ru`)
Automatic detection deliberately keeps Russian for Russian-speaking locales (ru, uk, be, kk and
others) and switches to English for everything else, so a visitor from Germany or Brazil gets the
English interface without touching anything.
**The switcher** sits in the top bar of the client area and the admin area (the `RU` / `EN` buttons
next to the balance) and in the footer of the login page.
**How it is built.** The panel was written with Russian text directly in the templates — about
1500 strings across 200 files. Instead of rewriting all of them, translation happens on the finished
response: `index.php` wraps the output in `ob_start()`, and before the page is sent to the browser
the strings are replaced using a dictionary (`engine/main/lang.php`). This covers the whole panel at
once, including the admin area and e-mails, and anything missing from the dictionary simply stays
Russian — nothing can vanish from the screen.
Replacement only happens on word boundaries, so a short dictionary key cannot corrupt a longer word.
**Adding your own language**, for example German:
```bash
cp panel/application/lang/en.php panel/application/lang/de.php
# translate the values in de.php, then add 'de' to Lang::AVAILABLE
# in panel/engine/main/lang.php
```
Dictionary coverage is verified by crawling the pages: all 20 sections we walk through render with
no Russian left over. If you spot an untranslated phrase, add it to `application/lang/en.php`
the key is the Russian text exactly as it appears.
Translation only runs when the language is not Russian; in Russian mode the dictionary is not even
loaded, so there is no overhead.
---
## 8. Captcha
By default it is **off** — registration and login work without it, and the field is simply absent.
To enable it:
1. Get **reCAPTCHA v2 "I'm not a robot"** keys at
[google.com/recaptcha/admin](https://www.google.com/recaptcha/admin) and specify the panel's domain
2. Go to **Admin → Settings → Other settings → "Bot protection (reCAPTCHA v2)"**
3. Paste the **Site key** and the **Secret key**
4. Switch to "Enabled" and save
**The order matters.** Enable the captcha without keys and the form will show
"ERROR for site owner: Invalid site key", locking everyone out. If that happens, open
`/var/www/hostinpl/application/config.php`, set `'captcha_enable' => '0'` and restart PHP-FPM:
```bash
sudo sed -i "s/'captcha_enable' => '1'/'captcha_enable' => '0'/" /var/www/hostinpl/application/config.php
sudo service php8.4-fpm restart # use your PHP version
```
The captcha covers login, registration, password recovery and ticket creation — all four forms at once.
---
## 9. HTTPS and domain
The panel is installed over HTTP. For a certificate:
```bash
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d panel.example.com
```
After the certificate is issued, update the address in the configuration, otherwise links in
e-mails and redirects will stay on `http://`:
```bash
sudo sed -i "s|'url' => 'http://|'url' => 'https://|" /var/www/hostinpl/application/config.php
sudo service php8.4-fpm restart
```
If the panel sits behind a reverse proxy (Caddy, nginx, Cloudflare), its nginx configuration already
contains `absolute_redirect off; port_in_redirect off;` — without them redirects point at the
internal port and links such as `/phpmyadmin` do not open from outside.
---
## 10. Maintenance
**Backups.** Keep the database and the configuration:
```bash
mysqldump hostin > /root/hostin-$(date +%F).sql
cp /var/www/hostinpl/application/config.php /root/config-$(date +%F).php
```
Game server files live on the node in `/home/gs<ID>` — back those up on the node.
**Logs:**
| What to look at | Where |
|-----------------|-------|
| PHP and nginx errors | `/var/log/nginx/error.log` |
| Scheduler hits | `grep 'main/cron' /var/log/nginx/access.log` |
| Login journal | the `authlog` table |
| Node image build | `/tmp/redl-docker-build.log` |
**Health check:** **Admin → System check** — PHP extensions, versions, scheduler links. Every item
should be green.
**Updating the code** from the repository:
```bash
cd redl-gamepanel && git pull
sudo cp -a panel/application panel/engine /var/www/hostinpl/ # config.php is not overwritten
sudo chown -R www-data:www-data /var/www/hostinpl
sudo service php8.4-fpm restart
```
---
## 11. Troubleshooting
**"Error: could not load the controller …"** — the panel found no controller for that address.
Usually it is a link to something that does not exist, for example `/phpmyadmin` when phpMyAdmin is
not installed (see section 2). If the section really should exist, check that the files are in place
and owned by `www-data`, then restart PHP-FPM **fully**: opcache may have cached a file that was
caught mid-write.
**The login page is blank or has no form** — look at `/var/log/nginx/error.log`. A common cause is
`short_open_tag = Off`. The panel uses short `<? ?>` tags; the installer turns the option on, but you
have to restore it if you reinstall PHP.
**Cannot log in, the form shows "Invalid site key"** — the captcha is on without keys, see section 8.
**The scheduler is not running** (statistics not updating, expired servers not suspended):
```bash
grep -c 'main/cron' /var/log/nginx/access.log # should grow
pgrep -a cron # is the daemon running?
cat /etc/cron.d/hostinpl # every job must be on ONE line
```
If a job is split across two lines, the token got into the file with a newline — rewrite the file,
taking the token from `/root/.redl-panel-credentials`.
**A server is not created on the node** — check on the node:
```bash
docker info # is the daemon up?
docker image inspect debian:stretch # is the image there?
docker run --rm debian:stretch echo ok # does a container start?
ls /home/cp/gameservers/files/ # are the game files in place?
```
**The server is created but dies immediately** — look at the container log on the node:
`docker logs gs<ID>`. Most often the 32-bit libraries are missing (rebuild the image from our
`docker/Dockerfile`) or the game build is damaged.
**Everything worked until a reboot** — on machines without systemd, the watchdogs on cron bring
things back. Check `pgrep -a cron` and run `/usr/local/bin/hostinpl-guard` by hand
(`gamenode-guard` on a node).
**A phrase in the interface is still Russian in English mode** — add it to
`panel/application/lang/en.php`; the key is the Russian text exactly as it appears on the page.
---
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
+161
View File
@@ -0,0 +1,161 @@
[Русский](README.md) · **English**
# Game Hosting Control Panel · REDL build
A control panel for selling and managing game servers: SA-MP, CRMP, MTA, Minecraft, CS 1.6, CS:S, RAGE:MP.
Client area, ticket system, payment gateways, automated game server deployment onto game nodes.
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
This build is a modernisation of the HostinPL 5.6 panel. The original was written for Debian 9 and PHP 7.0
(both end-of-life since 2022) and simply would not start on a current server. Here it runs on an up-to-date
stack, the vulnerabilities we found are fixed, and one-click installers are included.
---
## ⚠️ Legal status — read this before installing
The original **HostinPL 5.6 is a commercial, proprietary product**. Its authors are Samir Shelenko and
Alexander Zemlyanoy. The file `panel/LICENSE` is their licence, and it grants no permission to
redistribute or modify the software.
The sources this build is based on were circulated as "nulled" — a cracked copy of a paid product,
published without the authors' consent. We are not hiding that or rewriting history: the authors'
copyright headers are preserved throughout the code and were deliberately not removed.
What this means in practice:
* using it for commercial hosting infringes copyright, with all the risks that follow;
* the legal route is to buy a licence from the authors, or use an open-source alternative
(Pterodactyl, Pelican);
* this repository is only useful for studying the code, auditing it, and internal experiments.
Responsibility for use rests with whoever installs it.
---
## What is in this repository
| Path | What it is |
|------|------------|
| `install-panel.sh` | One-click panel install (nginx + PHP 8 + MariaDB + scheduler + admin user) |
| `install-node.sh` | One-click game node install (Docker + image + directories + SSH/FTP + SteamCMD) |
| `panel/` | Panel source code with all of our changes |
| `docker/Dockerfile` | Modern game server image (Debian 12; the original stretch-based one no longer builds) |
| `docker/Dockerfile.original-stretch` | The original Dockerfile, kept for comparison |
| [`CHANGES.en.md`](CHANGES.en.md) | Full list of differences from the original |
| [`SECURITY.en.md`](SECURITY.en.md) | Audit results: vulnerabilities, backdoors, what was fixed |
| [`GUIDE.en.md`](GUIDE.en.md) | Complete guide: install, connecting a node, configuration, maintenance |
---
## Requirements
**For the panel** (the website and database — Docker is not needed):
* Ubuntu 24.04 / 22.04 or Debian 12 / 13
* 1 core, 1 GB RAM, 10 GB disk
* Any VPS will do, including container-based ones (LXC/OpenVZ)
**For a game node** (this is where the game servers actually run):
* Ubuntu 24.04 / 22.04 or Debian 12 / 13
* 2 cores, 2 GB RAM, 100 GB disk (budget 12 GB per game server)
* **Full kernel access is mandatory: a dedicated server or KVM.**
The panel creates a separate Docker container for every game server, and Docker does not start
inside a container-based VPS (LXC/OpenVZ) — the kernel forbids nested namespaces. The installer
checks for this and tells you plainly if the machine is unsuitable.
The panel and a node can share one machine, but separating them is better: keep the website on a
cheap VPS and put the games on real hardware.
---
## One-click install
### Panel
```bash
apt-get update && apt-get install -y git
git clone https://github.com/RedlHosting/redl-gamepanel.git
cd redl-gamepanel
sudo bash install-panel.sh
```
The script asks only for the panel address, the administrator e-mail and password — everything else
is automatic: it installs nginx, PHP 8 and MariaDB, creates a database with a random password, loads
the schema, writes the configuration and the scheduler, sets up an autostart watchdog, creates the
administrator, and verifies that the login page actually opens.
To run the panel on a non-standard port, set it via an environment variable:
```bash
sudo PORT=8095 bash install-panel.sh
```
### Game node
```bash
git clone https://github.com/RedlHosting/redl-gamepanel.git
cd redl-gamepanel
sudo bash install-node.sh
```
At the end the script prints the IP, SSH login and password — enter those in the panel under
**Admin → Locations → Add location**. See [GUIDE.en.md](GUIDE.en.md) for details.
---
## What already works
* Login, registration, password recovery — **without a captcha** (it is enabled from the admin area, see below)
* Client area: balance, invoices, transfers, bonuses, promo codes, referral system
* Ticket system with categories and attachments
* Admin area: users, servers, locations, games, news, statistics, promo codes, settings
* System check (Admin → System check) — every item green
* Payment gateways: Unitpay, Enot, AnyPay, LiteKassa, RoboKassa, FreeKassa, YooKassa, QIWI
(credentials are entered in the admin area; we did not test any gateway with a real payment)
* Web hosting (the WEB section) — via ISPmanager on a separate machine
* Scheduler: statistics, tasks, automatic suspension of expired servers
* **Two interface languages: Russian and English**, with automatic detection from the browser
and a switcher in the header (see [GUIDE.en.md](GUIDE.en.md))
### The captcha is off and is enabled with a single switch
By default there is no captcha anywhere — not on login, registration or tickets; the field simply
does not exist. When you need it:
1. Get reCAPTCHA v2 ("I'm not a robot") keys at [google.com/recaptcha](https://www.google.com/recaptcha/admin)
2. Go to **Admin → Settings → Other settings → "Bot protection (reCAPTCHA v2)"**
3. Paste the Site key and the Secret key, switch it to "Enabled", save
The order matters: keys first, switch second — otherwise the form shows
"ERROR for site owner: Invalid site key" and nobody can log in.
## What does not work
* **Creating game servers on a container-based VPS.** You need a node with real Docker
(dedicated or KVM) — see Requirements for why.
* **Game server files are not included.** The original installer downloaded them from third-party
sites over plain HTTP with no signature checking, so we removed that (see [SECURITY.en.md](SECURITY.en.md)).
You need to place the files into `/home/cp/gameservers/files/<game_code>/` yourself.
* **Payments and VK login** require your own keys and merchant accounts.
---
## Differences from the original, in brief
* Runs on **PHP 8.4** instead of PHP 7.0 — a walk through all 21 sections of the panel produces
zero errors in the log
* **The installer was rewritten from scratch**: the original overwrote `/etc/apt/sources.list` with
Debian 9 repositories and broke apt on any current system
* **Two pre-authentication SQL injections were fixed**, and plaintext password storage was removed
* The captcha became manageable from the admin area
* Russian and English interface with automatic language detection
* The scheduler and autostart work even where systemd is absent
The full list is in [CHANGES.en.md](CHANGES.en.md) and [SECURITY.en.md](SECURITY.en.md).
---
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
+5
View File
@@ -1,3 +1,5 @@
**Русский** · [English](README.en.md)
# Панель управления игровым хостингом · сборка REDL # Панель управления игровым хостингом · сборка REDL
Панель для продажи и управления игровыми серверами: SA-MP, CRMP, MTA, Minecraft, CS 1.6, CS:S, RAGE:MP. Панель для продажи и управления игровыми серверами: SA-MP, CRMP, MTA, Minecraft, CS 1.6, CS:S, RAGE:MP.
@@ -114,6 +116,8 @@ sudo bash install-node.sh
(реквизиты вписываются в админке; ни один шлюз мы не тестировали живыми платежами) (реквизиты вписываются в админке; ни один шлюз мы не тестировали живыми платежами)
* Веб-хостинг (раздел WEB) — через ISPmanager на отдельной машине * Веб-хостинг (раздел WEB) — через ISPmanager на отдельной машине
* Планировщик: статистика, задачи, автоотключение просроченных серверов * Планировщик: статистика, задачи, автоотключение просроченных серверов
* **Два языка интерфейса: русский и английский**, с автоопределением по браузеру
и переключателем в шапке (см. [ИНСТРУКЦИЯ.md](ИНСТРУКЦИЯ.md))
### Капча — выключена, включается одним переключателем ### Капча — выключена, включается одним переключателем
@@ -145,6 +149,7 @@ sudo bash install-node.sh
Debian 9 и ломал apt на любой современной системе Debian 9 и ломал apt на любой современной системе
* **Закрыты две SQL-инъекции, доступные без авторизации**, и убрано хранение паролей в открытом виде * **Закрыты две SQL-инъекции, доступные без авторизации**, и убрано хранение паролей в открытом виде
* Капча стала управляемой из админки * Капча стала управляемой из админки
* Русский и английский интерфейс с автоопределением языка
* Планировщик и автозапуск работают и там, где нет systemd * Планировщик и автозапуск работают и там, где нет systemd
Полностью — в [ИЗМЕНЕНИЯ.md](ИЗМЕНЕНИЯ.md) и [БЕЗОПАСНОСТЬ.md](БЕЗОПАСНОСТЬ.md). Полностью — в [ИЗМЕНЕНИЯ.md](ИЗМЕНЕНИЯ.md) и [БЕЗОПАСНОСТЬ.md](БЕЗОПАСНОСТЬ.md).
+243
View File
@@ -0,0 +1,243 @@
[Русский](БЕЗОПАСНОСТЬ.md) · **English**
# Security audit: what we found and what we fixed
An audit of the HostinPL 5.6 build (a "nulled" copy). We looked for hidden backdoors and webshells,
obfuscation, data being sent to third-party hosts, SQL injections, RCE, authorisation bypasses,
password storage, and problems in the installer and the Dockerfile.
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
---
## The short version
The fork claimed: "all shells/holes have been cut out of the panel". Our check shows that claim is
**only partly true**. We found no working backdoors, and there is no obfuscation in the panel code.
But we did find:
* **proof that webshells really were present in this lineage** — one was found, already defanged;
* **two SQL injections reachable without authentication** — not mentioned anywhere in the fork's description;
* **user passwords written to the database in plaintext**;
* code being installed onto game nodes **over plain HTTP with no signature verification**.
We fixed the first three. Details below.
---
## 1. A webshell in the build's lineage — defanged
**File:** `panel/application/public/js/proxy/proxy.php` — all 7 lines of it:
```php
<?php
if(isset($_GET['cmd'])){
print('Backdoor fixed by Xopowblu-4EJlOBEK aka Und3X (und3x.ru)');
}else{
header("Location: /");
}
?>
```
The file takes a `?cmd=` parameter — the classic signature of a command-accepting webshell. Here the
body has been replaced with a message, so **this particular shell is defanged**. But the implication
is unambiguous: backdoors were planted deliberately in this build's distribution chain, and one of
them sat in a scripts folder nobody normally looks at.
**Our decision:** the file is kept as evidence. It executes nothing. If you do not want it:
`rm panel/application/public/js/proxy/proxy.php`.
**Takeaway:** any other copy of this panel obtained anywhere but this repository may contain a
*working* shell. Before installing someone else's build, check at least this much:
```bash
grep -rIn --include=*.php -E "eval\(|assert\(|base64_decode|gzinflate|\\\$_REQUEST\[" .
find . -path ./engine/libs -prune -o -name '*.php' -print | grep -E '(assets|public|tmp)/'
```
---
## 2. What we did not find (the good news)
Checked specifically, result negative:
* **No obfuscation in the panel code.** Zero occurrences of `eval`, `assert`, `create_function`,
`gzinflate`, `$$` or `$_REQUEST` outside vendor libraries. Not a single long base64 blob.
* **No stray PHP files** in the asset, upload or temporary directories — apart from the `proxy.php`
described above.
* **No data leaking to the author's hosts.** `vipadmin.club` appeared only in the keywords meta tag
and the mail sender address; `osmp.ga` was a footer link. Nothing sends passwords, licence data or
database credentials anywhere.
* **The admin gate is intact.** Every controller under `application/controllers/admin/**` performs a
`getAccessLevel()` check (level 2 for the admin area, 3 for games and locations). Not one file
without it.
* **The panel never invokes a shell directly.** A naive `grep` reports 54 `exec` and 8 `system`, but
all of them are inside vendor libraries: in phpseclib, `exec()` is an SSH method rather than a
process launcher, and the rest is in elFinder. The panel's own code calls no shell.
---
## 3. Two pre-authentication SQL injections — FIXED
The most serious finding, and one the fork's description says nothing about.
**File:** `panel/application/models/users.php`, function `createAuthLog()`.
The function records login attempts. It used to interpolate values straight into SQL:
```php
$query=$this->db->query("INSERT INTO `authlog` (... ,`ip`, ... ,`password`)
VALUES (NULL, '".$userid."', '".$ip."', ... , '".$password."');");
```
**Vector 1 — via the login form.** `$password` is the raw password from `$_POST`, unescaped. It is
called from `account/login.php` on **every** login attempt, including failed ones — that is, **before
any authentication**. Anybody off the street could inject SQL through the login form.
**Vector 2 — via a forged header.** `$ip` came from `getRealIpAdress()` (`engine/main/user.php`),
which returned the `CF-Connecting-IP` header **with no validation at all** — the early `return` sat
before the `filter_var` block:
```php
if (!empty($_SERVER["HTTP_CF_CONNECTING_IP"])) {
return $_SERVER["HTTP_CF_CONNECTING_IP"]; // as-is, unvalidated
}
```
That header is supplied by the client, so an attacker fully controlled the value reaching SQL.
The same unvalidated IP was also sent to the external `ip-api.com` service.
**What we did:**
* every value in the `INSERT` now goes through `$this->db->escape()` or is cast to `(int)`;
* `CF-Connecting-IP` is validated with `filter_var($ip, FILTER_VALIDATE_IP)` and otherwise ignored
in favour of `REMOTE_ADDR`;
* the `ip-api.com` request only runs for a valid IP and uses `urlencode()`.
**Verified on a live installation:** injection attempts through the password field and through a
forged header no longer get through, the `authlog` table is intact, and all 27 tables are present.
---
## 4. Plaintext passwords — FIXED
`account/login.php` passed the **real password** from the form into `createAuthLog()`:
```php
$this->usersModel->createAuthLog($userid['user_id'], $ip, '1', $password); // successful login
$this->usersModel->createAuthLog($userid['user_id'], $ip, '0', $password); // and failed ones too
```
So the `authlog` table accumulated every user's password in plaintext — including typos and
passwords for other services that people enter by accident. Anyone with database access could read
them, and given the injection in section 3, so could an outsider.
**What we did:** the password is no longer passed; an empty string is stored instead.
Verified: after both a successful and a failed login, the column is empty.
---
## 5. XSS in the registration form — FIXED
`panel/application/views/common/loginheader.php`: the `?ref=` parameter (referral code) was echoed
into a hidden form field without escaping — `<?echo $_GET['ref']?>`.
`htmlspecialchars(..., ENT_QUOTES, 'UTF-8')` was added, and the controller casts the value to `(int)`.
---
## 6. Passwords stored as unsalted MD5 — NOT fixed
`login.php:114``md5($password)`, column `user_password varchar(32)`.
Unsalted MD5 is brute-forced at billions of hashes per second on a consumer GPU, and rainbow tables
for common passwords are freely available. If the database leaks, treat every user password as known.
We **did not change this**: moving to `password_hash()` touches login, registration, recovery and
password changes, and requires migrating existing users — that is a project, not a one-line edit.
**What to do:** if the panel is used with real people, migrate to `password_hash()` transparently —
when a user logs in successfully against the old MD5 hash, re-save it with the new algorithm.
---
## 7. Code installed onto game nodes over HTTP — source removed
`panel/engine/games/game_settings.php:54-69` — 18 Node.js modules for RAGE:MP are downloaded
**over plain HTTP** from a third-party host, `mc.hostinpl.ru`, as zip archives with no signature and
no checksum verification.
Any intermediary on the path — or the owner of that host — can replace the contents, and the code
then runs on your game nodes. The original installer likewise pulled game server builds from
`dl.und3x.ru` and `vipadmin.club`.
**What we did:** our node installer no longer downloads builds from those hosts — you place the
files into `/home/cp/gameservers/files/<game_code>/` yourself. The lines in `game_settings.php` were
left alone (that is panel functionality), but you should not rely on them: either replace the URLs
with your own over HTTPS, or deploy the modules by hand.
Node.js in our `docker/Dockerfile` is installed from NodeSource **over HTTPS with repository key
verification**.
---
## 8. The panel connects to nodes with the root password — by design
The `locations` table stores `location_user` and `location_password` (`varchar(32)`) **in plaintext**,
and the connection uses `ssh2_auth_password()`. The panel's commands (`useradd`, `docker`,
`chown /home`) require root, so in practice the panel logs into the node as root with a password.
This cannot be changed without reworking the panel: it supports neither keys nor `sudo`. Measures
that reduce the risk:
```bash
# on the node: SSH and MySQL reachable only from the panel's address
ufw allow from PANEL_IP to any port 22 proto tcp
ufw allow from PANEL_IP to any port 3306 proto tcp
ufw deny 3306
```
The node installer prints these commands when it finishes. Additionally: use a separate password per
node, and keep access to the panel's database to a minimum — whoever reads `locations` gets root on
every node.
---
## 9. MariaDB on the node listens on all interfaces — the panel requires it
The panel creates game server databases on the node itself and connects to them over the network,
so `bind-address = 0.0.0.0` is mandatory. Our installer does that but **prints a warning** and the
firewall commands — unlike the original, which changed the setting silently.
---
## 10. The registration e-mail contains the password
`panel/application/views/mail/account/register.php` sends the new user their password in plaintext by
e-mail. That is the original design; we did not change it so as not to break the registration and
activation flow. Bear in mind the message stays in the mailbox and on intermediate servers.
---
## Overall assessment
| Item | Status |
|------|--------|
| Working backdoors in the panel code | none found |
| Webshell in the build's lineage | found already defanged, kept as evidence |
| Obfuscation, data exfiltration | none found |
| Admin authorisation bypass | none found |
| Pre-auth SQL injections (2) | **fixed** |
| Plaintext passwords in the database | **fixed** |
| XSS in the registration form | **fixed** |
| Unsalted MD5 | remains, needs separate work |
| Code installed over HTTP onto nodes | source removed from the installer |
| Root password used for node access | by design, mitigate with a firewall |
| Password inside the registration e-mail | remains |
**This panel should not be used for commercial hosting with real clients and payments** — because of
its legal status (see the README), because of the MD5 passwords, and because the provenance of the
code does not deserve trust. For study, internal tasks and auditing, it is fine.
---
**Maintained and developed with [REDL.IO](https://redl.io) — AI-powered hosting.**
+1
View File
@@ -61,6 +61,7 @@ $config = array(
'serv_test' => '0', 'serv_test' => '0',
'offline' => '0', 'offline' => '0',
'offline_res' => 'Хостинг временно закрыт на обслуживание! ', 'offline_res' => 'Хостинг временно закрыт на обслуживание! ',
'lang' => 'ru',
'captcha_enable' => '0', 'captcha_enable' => '0',
'recaptcha' => 'ReCaptchaV2', 'recaptcha' => 'ReCaptchaV2',
'secret_recaptcha' => '2ReCaptchaV2', 'secret_recaptcha' => '2ReCaptchaV2',
+727
View File
@@ -0,0 +1,727 @@
<?php
/*
Английский словарь интерфейса. Сборка REDL.
Поддерживается и развивается с помощью REDL.IO — Хостинг с искусственным интеллектом.
Ключ — исходная русская фраза как она встречается в шаблонах, значение — перевод.
Замена идёт ТОЛЬКО на границах слов (см. engine/main/lang.php): ключ, стоящий
внутри более длинного слова, не срабатывает — иначе «Мод» превращал «Модуль»
в «Modуль». Из нескольких подходящих ключей выбирается самый длинный.
Поэтому:
* порядок записей не важен;
* для слова в разных формах нужны отдельные записи ('дней', 'дня', 'день') —
словарь не знает морфологии;
* фразы с подстановкой переменных ('Сервер $id') задавайте статическим
началом строки — 'Сервер' — потому что в готовом ответе переменная
уже раскрыта.
Чего нет в словаре — останется по-русски. Это осознанно: ничего не может
исчезнуть с экрана из-за незаполненного перевода.
*/
return array(
// ── общее: кнопки и действия ─────────────────────────────────────────────────
'Отмена' => 'Cancel',
'Сохранить изменения' => 'Save changes',
'Сохранить' => 'Save',
'Применить' => 'Apply',
'Отправить' => 'Send',
'Написать' => 'Write',
'Открыть' => 'Open',
'Установить' => 'Install',
'Продлить' => 'Renew',
'Оплатить' => 'Pay',
'Пополнить баланс' => 'Top up balance',
'Пополнить' => 'Top up',
'Сменить' => 'Change',
'Изменить пароль' => 'Change password',
'Сменить пароль' => 'Change password',
'Скачать файл' => 'Download file',
'Распечатать' => 'Print',
'Войти' => 'Log in',
'Выйти' => 'Log out',
'Создать аккаунт' => 'Create account',
'Читать полностью' => 'Read more',
'Оставить комментарий' => 'Leave a comment',
'Перевести средства' => 'Transfer funds',
'Обменять бонусы' => 'Exchange bonuses',
'Управление' => 'Manage',
'Выбор' => 'Selection',
// ── навигация и разделы ──────────────────────────────────────────────────────
'Главная' => 'Home',
'Сервера' => 'Servers',
'Заказать Сервер' => 'Order a server',
'Мои Сервера' => 'My servers',
'Мои сервера' => 'My servers',
'Список серверов' => 'Server list',
'Заказать веб-хостинг' => 'Order web hosting',
'Мои веб-сайты' => 'My websites',
'Веб-хостинг: ws' => 'Web hosting: ws',
'Тикеты' => 'Tickets',
'Мои тикеты' => 'My tickets',
'Мои запросы' => 'My requests',
'Создать запрос' => 'New request',
'Список запросов' => 'Request list',
'Поддержка' => 'Support',
'Служба поддержки' => 'Support team',
'Техническая поддержка' => 'Technical support',
'База знаний (FAQ)' => 'Knowledge base (FAQ)',
'База знаний' => 'Knowledge base',
'Финансы' => 'Billing',
'Баланс' => 'Balance',
'История баланса' => 'Balance history',
'Список пополнений' => 'Top-up list',
'История операций' => 'Transaction history',
'Список операций' => 'Transaction list',
'Перевод средств' => 'Funds transfer',
'Обещанный платеж' => 'Deferred payment',
'Партнерская программа' => 'Affiliate programme',
'Профиль' => 'Profile',
'Мой профиль' => 'My profile',
'Личные данные' => 'Personal details',
'Авторизация' => 'Sign in',
'История авторизации' => 'Login history',
'Статистика' => 'Statistics',
'Новости' => 'News',
'Информация' => 'Information',
'Обратная связь' => 'Contact us',
'Пользователи' => 'Users',
'Автоустановка' => 'Auto install',
'Репозиторий' => 'Repository',
'Друзья' => 'Friends',
'Гость' => 'Guest',
'Смена дизайна' => 'Change theme',
'Сменить дизайн' => 'Change theme',
'Автообновление' => 'Auto refresh',
'Доступные команды' => 'Available commands',
// ── формы и поля ─────────────────────────────────────────────────────────────
'Введите свой E-Mail' => 'Enter your e-mail',
'Введите свой Пароль' => 'Enter your password',
'Пароль пользователя' => 'User password',
'RCON Пароль' => 'RCON password',
'Повторите пароль' => 'Repeat password',
'Новый пароль' => 'New password',
'Ваш новый пароль:' => 'Your new password:',
'Пароль' => 'Password',
'Имя пользователя' => 'Username',
'Введите сумму' => 'Enter the amount',
'Введите команду!' => 'Enter a command!',
'Есть купон?' => 'Have a coupon?',
'Название' => 'Name',
'Статус' => 'Status',
'Дата' => 'Date',
'Сумма' => 'Amount',
'Адрес' => 'Address',
'Локация' => 'Location',
'Клиент' => 'Client',
'Мод' => 'Mod',
'Задача' => 'Task',
'Телефон:' => 'Phone:',
'Платежная система' => 'Payment gateway',
'Ид платежа' => 'Payment ID',
'Оплачен до' => 'Paid until',
'Смена E-Mail' => 'Change e-mail',
'Секретный ключ' => 'Secret key',
// ── деньги, периоды, единицы ─────────────────────────────────────────────────
'рублей' => 'RUB',
'руб.' => 'RUB',
'руб!' => 'RUB!',
'руб' => 'RUB',
'0.00 руб.' => '0.00 RUB',
'Итого к оплате:' => 'Total due:',
'Общая сумма пополнений' => 'Total topped up',
'Пополнение баланса' => 'Balance top-up',
'Пополнение баланса пользователя' => 'User balance top-up',
'Оплата счета' => 'Invoice payment',
'Недостаточно средств' => 'Insufficient funds',
'МБ' => 'MB',
'слотов.' => 'slots.',
'слотов' => 'slots',
'15 дней' => '15 days',
'30 дней' => '30 days',
'60 дней' => '60 days',
'90 дней (-5%)' => '90 days (-5%)',
'180 дней (-10%)' => '180 days (-10%)',
'360 дней (-15%)' => '360 days (-15%)',
'90 дней' => '90 days',
'180 дней' => '180 days',
'360 дней' => '360 days',
'дней в неделю' => 'days a week',
'часа в сутки' => 'hours a day',
'Осталось %d дней' => '%d days left',
'дней' => 'days',
'дня' => 'days',
'день' => 'day',
'Одноразовая' => 'One-off',
'Повторяющаяся' => 'Recurring',
// ── состояния ────────────────────────────────────────────────────────────────
'Включена' => 'Enabled',
'Выключена' => 'Disabled',
'Включен' => 'Enabled',
'Выключен' => 'Disabled',
'В сети' => 'Online',
'Не в сети' => 'Offline',
'На данный момент список пуст.' => 'The list is currently empty.',
'На данный момент у вас нет запросов.' => 'You have no requests at the moment.',
'На данный момент у вас нет счетов.' => 'You have no invoices at the moment.',
'На данный момент у вас нет операций со счетом.'=> 'You have no account transactions yet.',
'У вас нет активов.' => 'You have no assets.',
'Список логов пуст :(' => 'The log is empty :(',
'На данный момент нет доступных локаций' => 'No locations are available at the moment',
'К сожалению, на данный момент на хостинге нет новостей' => 'There is no hosting news at the moment',
'Получение данных...' => 'Loading data…',
'Ваш запрос обрабатывается, пожалуйста, подождите...' => 'Your request is being processed, please wait…',
'Пожалуйста, подождите 10 секунд.'=> 'Please wait 10 seconds.',
// ── авторизация, регистрация, пароль ────────────────────────────────────────
'Вы не авторизированы!' => 'You are not signed in!',
'Вы не авторизированы' => 'You are not signed in',
'Вы уже авторизированы!' => 'You are already signed in!',
'Вы успешно авторизировались!' => 'You have signed in successfully!',
'Вы успешно вошли!' => 'You have signed in!',
'Вы успешно вышли из своего аккаунта' => 'You have signed out of your account',
'Вы ввели не верный логин или пароль!' => 'Wrong e-mail or password!',
'Вы не залогинены!' => 'You are not logged in!',
'Нет доступа!' => 'Access denied!',
'У вас нет доступа к данному разделу!' => 'You do not have access to this section!',
'Авторизуйтесь для входа в панель управления' => 'Sign in to open the control panel',
'Регистрация аккаунта' => 'Account registration',
'Вы успешно зарегистрировались!' => 'You have registered successfully!',
'Завершение регистрации!' => 'Registration complete!',
'Завершение регистрации' => 'Registration complete',
'Ваш аккаунт подтвержден!' => 'Your account is confirmed!',
'Указанный E-Mail уже зарегистрирован!' => 'That e-mail is already registered!',
'Указанный E-Mail уже используется!' => 'That e-mail is already in use!',
'Пользователь с указанным E-Mail не зарегистрирован!' => 'No user is registered with that e-mail!',
'Укажите свой реальный E-Mail!' => 'Enter a valid e-mail address!',
'Укажите свое реальное имя!' => 'Enter your real first name!',
'Укажите свою реальную фамилию!' => 'Enter your real last name!',
'Введенные вами пароли не совпадают!' => 'The passwords do not match!',
'Пароль должен содержать от 6 до 32 латинских букв, цифр и знаков <i>,.!?_-</i>!' => 'The password must be 6 to 32 Latin letters, digits or the characters <i>,.!?_-</i>!',
'Пароль должен содержать от 8 до 32 латинских букв, цифр и знаков <i>,.!?_-</i>!' => 'The password must be 8 to 32 Latin letters, digits or the characters <i>,.!?_-</i>!',
'Восстановление пароля' => 'Password recovery',
'На ваш E-Mail отправлена информация для восстановления пароля!' => 'Password recovery instructions have been sent to your e-mail!',
'Указанный ключ восстановления неверный!' => 'That recovery key is not valid!',
'Ваш пароль успешно был изменен' => 'Your password has been changed',
'Пароль был успешно изменен!' => 'The password has been changed!',
'Подтвердите, что вы не робот!' => 'Please confirm that you are not a robot!',
'Укажите правильный код с картинки! Попробуйте нажать на картинку, чтобы обновить ее.' => 'Enter the correct code from the image. Click the image to refresh it.',
'Укажите правильный код с картинки!' => 'Enter the correct code from the image!',
'Проверьте правильность капчи!' => 'Check the captcha and try again!',
'Проверьте вашу почту,' => 'Check your e-mail,',
'на нее должно прийти сообщение с дальнейшими инструкциями.' => 'a message with further instructions should arrive there.',
'Повторите вашу попытку снова.' => 'Please try again.',
'Через 5 секунд вы будете перенаправлены на домашнюю страницу' => 'You will be redirected to the home page in 5 seconds',
'Изменения сохранены!' => 'Changes saved!',
'Ошибка! Повторите действие.' => 'Error! Please try again.',
'Неизвестная ошибка!' => 'Unknown error!',
'Внутренняя ошибка!(Либо не привязана учётка)' => 'Internal error (or the account is not linked)',
'Не POST запрос!' => 'Not a POST request!',
'Не POST запрос' => 'Not a POST request',
'Не POST данные' => 'Not POST data',
'Упс... Что-то пошло не так.' => 'Oops… something went wrong.',
'Вернутся на главную' => 'Back to home',
'Ошибка!' => 'Error!',
'Критическая ошибка' => 'Critical error',
// ── VK и сессии ──────────────────────────────────────────────────────────────
'Профиль привязан!' => 'Profile linked!',
'Профиль не привязан' => 'Profile not linked',
'Данный профиль уже привязан к другому аккаунту!' => 'That profile is already linked to another account!',
'Данный аккаунт уже привязан!' => 'That account is already linked!',
'Вы успешно отвязали VK!' => 'VK has been unlinked!',
'На данный момент у Вас не привязан VK!' => 'You have no VK account linked!',
'Сеанс не найден!' => 'Session not found!',
'Сеанс успешно завершён!' => 'Session ended successfully!',
'Вы не можете завершить текущий сеанс!' => 'You cannot end your current session!',
// ── баланс, платежи, бонусы ──────────────────────────────────────────────────
'Ваш баланс успешно пополнен!' => 'Your balance has been topped up!',
'Укажите сумму пополнения в допустимом формате!' => 'Enter the top-up amount in a valid format!',
'Укажите сумму от 10 до 5000 рублей!' => 'Enter an amount between 10 and 5000 RUB!',
'Укажите сумму от 0 до 5000 рублей!' => 'Enter an amount between 0 and 5000 RUB!',
'Укажите реальное число!' => 'Enter a valid number!',
'Данная платежная система отключена!' => 'This payment gateway is disabled!',
'Вы указали недопустимый период оплаты!' => 'You selected an invalid payment period!',
'У вас недостаточно средств!' => 'You do not have enough funds!',
'На Вашем счету не хватает' => 'Your account is short of',
'На Вашем счету недостаточно средств' => 'There are not enough funds in your account',
'Средства переведены!' => 'Funds transferred!',
'Перевод средств пользователю ID-' => 'Funds transfer to user ID-',
'Деньги отправлены. Вам начисленно'=> 'Money sent. You received',
'Вы указали свой ID' => 'You entered your own ID',
'Данного ID не сушествует!' => 'That ID does not exist!',
'Здесь вы можете обменять свои бонусные баллы.' => 'Here you can exchange your bonus points.',
'У вас недостаточно монет!' => 'You do not have enough coins!',
'Бонус с реферала ID-' => 'Referral bonus, ID-',
'Бонус за приглашенного реферала ID-' => 'Bonus for an invited referral, ID-',
'Вы активировали скидку' => 'You activated a discount of',
'Данного кода не существует' => 'That code does not exist',
'Вы активирали обещаный платёж!' => 'Deferred payment activated!',
'На данный момент Вам недоступна данная функция!' => 'This feature is not available to you right now.',
'Внутренняя ошибка. Возможные причины:' => 'Internal error. Possible reasons:',
'- Неверная сумма платежа.' => '- Wrong payment amount.',
'- Неверный ID магазина.' => '- Wrong shop ID.',
'- Не верный ID платежа.' => '- Wrong payment ID.',
'- Данный счет уже оплачен.' => '- This invoice is already paid.',
'- Платеж был отменен.' => '- The payment was cancelled.',
'Если здесь нет выявленной вами причины, то обратитесь в' => 'If your case is not listed here, please contact',
'Ошибка - Неверная сумма платежа!' => 'Error — wrong payment amount!',
'Ошибка - Shop ID!' => 'Error — Shop ID!',
'Ошибка - Не верный ID платежа!' => 'Error — wrong payment ID!',
'Ошибка - Данный счет уже оплачен!' => 'Error — this invoice is already paid!',
// ── заказ и жизненный цикл игрового сервера ─────────────────────────────────
'Выберите локацию' => 'Choose a location',
'Выберите период оплаты' => 'Choose a payment period',
'Информация о заказе' => 'Order details',
'Игровой сервер: gs' => 'Game server: gs',
'Сервер успешно поставлен в очередь на установку.' => 'The server has been queued for installation.',
'Сервер успешно поставлен на переустановку!' => 'The server has been queued for reinstallation.',
'Идет установка сервера!' => 'Server installation in progress!',
'Идет переустановка сервера!' => 'Server reinstallation in progress!',
'Идет создание BackUP сервера!' => 'Server backup in progress!',
'Идет восстанновление сервера из BackUP!' => 'Server restore from backup in progress!',
'Идет обновление сервера!' => 'Server update in progress!',
'Сервер заблокирован!' => 'The server is suspended!',
'Сервер должен быть выключен!' => 'The server must be stopped!',
'Сервер должен быть вылючен!' => 'The server must be stopped!',
'Сервер должен быть включен!' => 'The server must be running!',
'Сервер должен быть включён!' => 'The server must be running!',
'Запрашиваемый сервер не существует!' => 'The requested server does not exist!',
'Вы выбрали несуществующее действие!' => 'You selected an action that does not exist!',
'Вы не можете использовать это действие!' => 'You cannot use this action!',
'Вы указали несуществующую локацию!' => 'You selected a location that does not exist!',
'Вы указали несуществующую игру!' => 'You selected a game that does not exist!',
'Данная игра не доступна для указанной локации!' => 'This game is not available in the selected location!',
'На выбранной Вами локации нет свободных портов для данной игры' => 'The selected location has no free ports for this game',
'Успешный запуск сервера' => 'Server started',
'Вы успешно запустили сервер!' => 'You have started the server!',
'Успешный перезапуск сервера' => 'Server restarted',
'Вы успешно перезапустили сервер!'=> 'You have restarted the server!',
'Успешное выключение сервера' => 'Server stopped',
'Вы успешно выключили сервер!' => 'You have stopped the server!',
'Команда успешно отправлена!' => 'Command sent!',
'Консоль успешно очищена!' => 'Console cleared!',
'Ошибка подключения к rcon!' => 'RCON connection failed!',
'Возникла ошибка запроса к серверу. Код ошибки:' => 'The request to the server failed. Error code:',
'Вы привысили ограничение размера директории на' => 'You have exceeded the directory size limit by',
'Продление сервера' => 'Server renewal',
'Сервер успешно продлен!' => 'The server has been renewed!',
'Смена ядра' => 'Change core',
'Смена билда' => 'Change build',
'Параметры запуска' => 'Startup parameters',
'Параметры запуска успешно изменены!' => 'Startup parameters updated!',
'Вы успешно начали смену ядра!' => 'Core change started!',
'Вы указали неверное ядро!' => 'That core is not valid!',
'Вы успешно начали смену билда!' => 'Build change started!',
'Вы указали неверный билд!' => 'That build is not valid!',
'Вы успешно сменили карту на' => 'Map changed to',
'Вы успешно сменили порт сервера на' => 'Server port changed to',
'Выбранный порт не может быть использован!' => 'That port cannot be used!',
'Выбранный порт уже занят другим сервером!' => 'That port is already taken by another server!',
'Параметр Tickrate указан неверно!' => 'The tickrate value is not valid!',
'Параметр FPS указан неверно!' => 'The FPS value is not valid!',
'RCON Пароль должен содержать от 2 до 18 символов!' => 'The RCON password must be 2 to 18 characters!',
'Пароль отправленный на сервер не соответствует нормам!' => 'The password sent to the server does not meet the requirements!',
'Вы не указали rcon пароль в параметрах запуска сервера!' => 'You have not set an RCON password in the startup parameters!',
'Пароль FTP успешно изменен!!' => 'FTP password changed!',
'Пароль MySql успешно изменен!!' => 'MySQL password changed!',
'Пароль доступа успешно изменен!' => 'Access password changed!',
'Необходимо удалить все заблокированные IP адреса!' => 'You must remove all blocked IP addresses first!',
'Вы не ввели IP' => 'You did not enter an IP',
'Вы успешно заблокировали' => 'You have blocked',
'Вы успешно разблокировали IP!' => 'You have unblocked the IP!',
// ── слоты, бекапы, база данных, моды ────────────────────────────────────────
'Вы успешно установили' => 'You have set',
'Вы указали недопустимое количество слотов!' => 'That number of slots is not allowed!',
'Укажите количество слотов в допустимом формате!' => 'Enter the number of slots in a valid format!',
'Невозможно установить слоты ниже заданных.' => 'Slots cannot be set below the current value.',
'Слоты не были изменены.' => 'The slots were not changed.',
'Увеличено до' => 'Increased to',
'Успешное начало создания BackUP сервера.' => 'Server backup started.',
'Вы успешно начали создание BackUP сервера!' => 'You have started a server backup!',
'Вы успешно восстановили BackUP сервера!' => 'The server has been restored from backup!',
'Успешное удаление BackUP сервера'=> 'Backup deleted',
'Вы успешно удалили BackUP сервера!' => 'You have deleted the server backup!',
'У вас нет BackUP сервера!' => 'You have no server backup!',
'Успешное создание BackUP.' => 'Backup created.',
'Успешное восстановление из BackUP.' => 'Restored from backup.',
'Успешная установка сервера.' => 'Server installed successfully.',
'Успешная переустановка сервера.' => 'Server reinstalled successfully.',
'Успешная смена ядра.' => 'Core changed successfully.',
'Успешная смена билда.' => 'Build changed successfully.',
'Вы успешно создали базу данных' => 'You have created the database',
'Вы успешно включили базу данных' => 'You have enabled the database',
'Вы успешно выключили базу данных'=> 'You have disabled the database',
'База данных и так включена либо вы ее еще не создали!' => 'The database is already enabled, or you have not created it yet!',
'База данных и так выключена!' => 'The database is already disabled!',
'База данных и так создана!' => 'The database already exists!',
'Создайте базу MySQL и включите ее!' => 'Create a MySQL database and enable it!',
'Данный мод не доступен для установки!' => 'This mod is not available for installation!',
'Запрашиваемый мод не существует!'=> 'The requested mod does not exist!',
'Установка мода' => 'Mod installation',
'Не для Вашей игры!' => 'Not for your game!',
'Вы успешно загрузили на сервер модуль' => 'You have uploaded the module to the server',
'Данный файл не доступен!' => 'This file is not available!',
'Запрашиваемый файл не существует!' => 'The requested file does not exist!',
'Покупка файла' => 'File purchase',
'Файл' => 'File',
// ── задачи планировщика ──────────────────────────────────────────────────────
'Задача успешно создана!' => 'Task created!',
'Задача успешно удалена!' => 'Task deleted!',
'Задача не найдена!' => 'Task not found!',
'Вы указали несуществующую задачу!' => 'You selected a task that does not exist!',
'Вы не можете создать данную задачу!' => 'You cannot create this task!',
'Данная задача уже создана!' => 'This task already exists!',
'Вы указали несуществующий тип задачи!' => 'You selected a task type that does not exist!',
'Данный тип задачи недоступен для задачи!' => 'This task type is not available for the task!',
'Вы указали недопустимое время выполнения!' => 'That execution time is not allowed!',
'Вы не можете создавать больше 4-х задач!' => 'You cannot create more than 4 tasks!',
'[Планировщик] Успешный запуск сервера' => '[Scheduler] Server started',
'[Планировщик] Успешное выключение сервера' => '[Scheduler] Server stopped',
'[Планировщик] Успешный перезапуск сервера' => '[Scheduler] Server restarted',
'Сервер был восcтановлен после падения' => 'The server was recovered after a crash',
// ── друзья / совладельцы сервера ─────────────────────────────────────────────
'Вы успешно добавили друга!' => 'Friend added!',
'Вы успешно удалили друга!' => 'Friend removed!',
'Вы не являетесь владельцем сервера. У вас нет доступа к разделу!' => 'You are not the server owner, so you cannot access this section!',
'Вы указали недоступный ID пользователя!' => 'That user ID is not available!',
'Запрашиваемый пользователь не существует!' => 'The requested user does not exist!',
'Запрашиваемый пользователь уже добавлен!' => 'That user has already been added!',
'Запрашиваемый пользователь является владельцем сервера!' => 'That user is the server owner!',
// ── тестовый период ──────────────────────────────────────────────────────────
'Вы уже брали тестовый период, либо у вас нет одобрения администратора!' => 'You have already used the trial period, or an administrator has not approved it!',
'Тестовый период отключен на хостинге!' => 'The trial period is disabled on this hosting!',
// ── тикеты ───────────────────────────────────────────────────────────────────
'Вы успешно создали запрос!' => 'Your request has been created!',
'Вы успешно закрыли запрос!' => 'You have closed the request!',
'Вы успешно отправили сообщение!' => 'Your message has been sent!',
'Данный запрос закрыт, оставить новое сообщение невозможно!' => 'This request is closed, you cannot add a new message!',
'Запрашиваемый запрос не существует!' => 'The requested ticket does not exist!',
'Название тикета должно содержать от 6 до 32 символов!' => 'The ticket subject must be 6 to 32 characters!',
'Текст тикета должен содержать от 10 до 350 символов!' => 'The ticket text must be 10 to 350 characters!',
'Текст сообщения должен содержать от 1 до 350 символов.' => 'The message text must be 1 to 350 characters.',
'Нельзя создать тикет, так как за сегодня вы создали больше 3 тикетов.' => 'You cannot create a ticket: you have already created more than 3 today.',
'Новые запросы' => 'New requests',
// ── загрузка изображений ─────────────────────────────────────────────────────
'Можно загружать только изображения в форматах jpg, jpeg и png' => 'Only jpg, jpeg and png images can be uploaded',
'Изображение не загружено!' => 'The image was not uploaded!',
'Аватар успешно был загружен!' => 'Avatar uploaded!',
'Загружаемое изображение превышает допустимые нормы!' => 'The uploaded image is too large!',
'Размер изображения не должено превышать 512Кб' => 'The image must not exceed 512 KB',
'Размер изображения не должено превышать 5120Кб' => 'The image must not exceed 5120 KB',
'Изображение:' => 'Image:',
// ── новости ──────────────────────────────────────────────────────────────────
'Вы успешно создали коментарий!' => 'Your comment has been posted!',
'Запрашиваемая новость не существует!' => 'The requested news item does not exist!',
'Текст коментариф должен содержать от 10 до 128 символов.' => 'The comment must be 10 to 128 characters.',
'Новости и статусы на REDL.IO' => 'News and status on REDL.IO',
// ── веб-хостинг ──────────────────────────────────────────────────────────────
'Веб-хостинг №' => 'Web hosting no.',
'успешно заказан.' => 'ordered successfully.',
'Веб.хостинг не доступен для заказа!' => 'Web hosting is not available for ordering!',
'Вы указали несуществующий тариф!'=> 'You selected a plan that does not exist!',
'Данный тариф не доступен для указанной локации!' => 'This plan is not available in the selected location!',
'Веб-хостинг заблокирован!' => 'Web hosting is suspended!',
'Запрашиваемый веб-хостинг не существует!' => 'The requested web hosting does not exist!',
// ── темы оформления ──────────────────────────────────────────────────────────
'Тема установлена!' => 'Theme applied!',
'Тема Default успешно установлена!' => 'The Default theme has been applied!',
'Тема Material успешно установлена!' => 'The Material theme has been applied!',
'Тема Material gray успешно установлена!'=> 'The Material gray theme has been applied!',
'Тема Material light успешно установлена!'=> 'The Material light theme has been applied!',
'Тема Default' => 'Default theme',
// ── главная страница и маркетинг ─────────────────────────────────────────────
'Удобная панель управления' => 'A convenient control panel',
'С нашей панелью управления, управлять сервером легко и просто с ней справится как опытный пользователь так и новичок.' => 'Our control panel makes managing a server simple — both for experienced users and for beginners.',
'Преимущества нашего хостинга' => 'Why choose our hosting',
'Мощные процессоры Intel' => 'Powerful Intel processors',
'Защита от DDoS атак на оборудование.' => 'Hardware-level DDoS protection.',
'Полный доступ к FTP' => 'Full FTP access',
'Создание резервных копий backup' => 'Backup creation',
'Скоростные SSD диски' => 'Fast SSD storage',
'Хостинг с искусственным интеллектом' => 'AI-powered hosting',
'С уважением, Администрация REDL.IO!' => 'Sincerely, the REDL.IO team!',
// ── месяцы (графики и даты) ─────────────────────────────────────────────────
'Январь' => 'January', 'Февраль' => 'February', 'Март' => 'March',
'Апрель' => 'April', 'Май' => 'May', 'Июнь' => 'June',
'Июль' => 'July', 'Август' => 'August', 'Сентябрь' => 'September',
'Октябрь'=> 'October', 'Ноябрь' => 'November', 'Декабрь' => 'December',
'Янв.' => 'Jan', 'Фев.' => 'Feb', 'Март.' => 'Mar', 'Апр.' => 'Apr',
'Май.' => 'May', 'Июнь.' => 'Jun', 'Июль.' => 'Jul', 'Авг.' => 'Aug',
'Сент.'=> 'Sep', 'Окт.' => 'Oct', 'Ноя.' => 'Nov', 'Дек.' => 'Dec',
// ── элементы графиков ────────────────────────────────────────────────────────
'Скачать PNG' => 'Download PNG',
'Скачать CSV' => 'Download CSV',
'Скачать SVG' => 'Download SVG',
'Выбор Zoom' => 'Zoom selection',
'Увеличить' => 'Zoom in',
'Уменьшить' => 'Zoom out',
'Панорамирование'=> 'Panning',
'Сброс Масштаба' => 'Reset zoom',
// ── служебные ошибки движка ──────────────────────────────────────────────────
'Ошибка: Не удалось загрузить контроллер' => 'Error: could not load the controller',
'Ошибка: Не удалось загрузить шаблон' => 'Error: could not load the template',
'Ошибка: Не удалось загрузить модель' => 'Error: could not load the model',
'Ошибка: Не удалось загрузить библиотеку' => 'Error: could not load the library',
'Ошибка: Не удалось загрузить драйвер базы данных' => 'Error: could not load the database driver',
'Ошибка: Не удалось загрузить файл конфигурации игр!' => 'Error: could not load the games configuration file!',
'Ошибка: Не удалось загрузить файл конфигурации!' => 'Error: could not load the configuration file!',
'Укажите допустимый статус!' => 'Choose a valid status!',
// ── админка: часто встречающиеся подписи ────────────────────────────────────
'Введите название локации' => 'Enter the location name',
'Название локации должно содержать от 2 до 32 символов!' => 'The location name must be 2 to 32 characters!',
'Введите ID' => 'Enter the ID',
'Введите имя пользователя' => 'Enter the username',
'Имя пользователя должно содержать от 2 до 32 символов!' => 'The username must be 2 to 32 characters!',
'Укажите допустимый IP!' => 'Enter a valid IP!',
'Укажите допустимую стоимость!' => 'Enter a valid price!',
'Описание должно содержать от 2 до 500 символов!' => 'The description must be 2 to 500 characters!',
'Введите название игры' => 'Enter the game name',
'Название игры должно содержать от 2 до 92 символов!' => 'The game name must be 2 to 92 characters!',
'Введите код игры' => 'Enter the game code',
'Код игры должен содержать от 2 до 8 символов!' => 'The game code must be 2 to 8 characters!',
'Название категории должно содержать от 2 до 32 символов!' => 'The category name must be 2 to 32 characters!',
'Введите название тарифа' => 'Enter the plan name',
'Введите стоимость тарифа' => 'Enter the plan price',
'Введите стоимость за один слот' => 'Enter the price per slot',
'Введите минимальное количество слотов для заказа' => 'Enter the minimum number of slots per order',
'Введите максимальное количество слотов для заказа' => 'Enter the maximum number of slots per order',
'Введите минимальный порт для заказа' => 'Enter the lowest port available for orders',
'Введите количество ядер на один сервер' => 'Enter the number of cores per server',
'Введите объем оперативной памяти на один сервер (в МБ)' => 'Enter the amount of RAM per server (MB)',
'Введите объем SSD на один сервер (в МБ)' => 'Enter the SSD size per server (MB)',
'Укажите допустимое количество ядер!' => 'Enter a valid number of cores!',
'Укажите допустимый объем оперативной памяти!' => 'Enter a valid amount of RAM!',
'Укажите допустимую объем жесткого диска!' => 'Enter a valid disk size!',
'Укажите допустимый Query-драйвер.' => 'Choose a valid query driver.',
'Введите IP (Который видит пользователь)' => 'Enter the IP shown to the user',
'Введите IP (По которому подключаются)' => 'Enter the IP used for connections',
'Введите IP (0.0.0.0)' => 'Enter the IP (0.0.0.0)',
'Введите URL (domain.ru)' => 'Enter the URL (domain.com)',
'Введите NS Сервера локации (ns1.domain.ru и ns2.domain.ru)' => 'Enter the location name servers (ns1.domain.com and ns2.domain.com)',
'Введите название шаблона в панели ISP' => 'Enter the template name in the ISP panel',
// ── заголовки и меню админки ─────────────────────────────────────────────────
'Панель управления' => 'Control panel',
'Панель Администратора' => 'Administrator panel',
'Администратора' => 'Administrator',
'Администратор' => 'Administrator',
'Меню управления' => 'Management menu',
'Меню' => 'Menu',
'На главную' => 'Home',
'Контакты' => 'Contacts',
'Пользователей на сайте:' => 'Users online:',
'Система' => 'System',
'Настройки' => 'Settings',
'Игровые сервера' => 'Game servers',
'Игровых серверов' => 'Game servers',
'Доступных игр' => 'Games available',
'Пользователей' => 'Users',
'Пользователь' => 'User',
'Список пользователей' => 'User list',
'Все запросы' => 'All requests',
'Все категории' => 'All categories',
'Создать категорию' => 'New category',
'Написать клиенту' => 'Message the client',
'Платежи' => 'Payments',
'WEB сайты' => 'Websites',
'Рассылка E-mail' => 'E-mail campaign',
'Операции пользователей' => 'User transactions',
'Список новостей' => 'News list',
'Добавить новость' => 'Add news',
'Промо коды' => 'Promo codes',
'Список промо кодов' => 'Promo code list',
'Добавить промо код' => 'Add promo code',
'Промо' => 'Promo',
'Игры' => 'Games',
'Список игр' => 'Game list',
'Добавить игру' => 'Add game',
'Game локации' => 'Game locations',
'Список локаций' => 'Location list',
'Добавить локацию' => 'Add location',
'WEB Тарифы' => 'Web plans',
'WEB Локации' => 'Web locations',
'Дата регистрации' => 'Registration date',
'Статус активации' => 'Activation status',
'Информация и бонусы' => 'Information and bonuses',
'Статистика хостинга' => 'Hosting statistics',
// ── пустые состояния админки ─────────────────────────────────────────────────
'На данный момент у вас нет серверов.' => 'You have no servers yet.',
'На данный момент у вас нет веб-хостингов.' => 'You have no web hosting yet.',
'На данный момент нет игровых серверов.' => 'There are no game servers yet.',
'На данный момент нет локаций.' => 'There are no locations yet.',
'На данный момент нет операций.' => 'There are no transactions yet.',
'На данный момент в тех.поддержку нет запросов.' => 'There are no support requests at the moment.',
'Заблокировано:' => 'Suspended:',
'Всего:' => 'Total:',
'Новых запросов:' => 'New requests:',
'Запросов:' => 'Requests:',
'Онлайн:' => 'Online:',
'Серверов:' => 'Servers:',
'Последний счет:' => 'Last invoice:',
'Заработано:' => 'Earned:',
'Обновлено в' => 'Updated at',
'Активен' => 'Active',
'Подтвержден' => 'Confirmed',
'Установлен' => 'Installed',
'Код' => 'Code',
'Слоты' => 'Slots',
'Порты' => 'Ports',
'Цена за слот' => 'Price per slot',
// ── настройки: вкладки и подписи ─────────────────────────────────────────────
'Общие настройки' => 'General settings',
'Платежные системы' => 'Payment gateways',
'Прочие настройки' => 'Other settings',
'Применить настройки' => 'Apply settings',
'URL сайта' => 'Site URL',
'Название сайта' => 'Site name',
'Описание сайта' => 'Site description',
'Ключевые слова' => 'Keywords',
'Имя отправителя' => 'Sender name',
'E-mail Службы поддержки' => 'Support e-mail',
'ID Вашей группы ВКонтакте' => 'Your VK group ID',
'Cсылка на ваш логотип' => 'Logo URL',
'Введите URL сайта' => 'Enter the site URL',
'Введите название сайта' => 'Enter the site name',
'Введите описание сайта' => 'Enter the site description',
'Введите ключевые слова' => 'Enter the keywords',
'Введите имя отправителя' => 'Enter the sender name',
'Введите E-mail Службы поддержки' => 'Enter the support e-mail',
'Введите ID Вашей группы ВКонтакте' => 'Enter your VK group ID',
'Введите ссылку на ваш логотип' => 'Enter the logo URL',
'Введите секретный ключ' => 'Enter the secret key',
'Введите секретный ключ №1' => 'Enter secret key no. 1',
'Введите секретный ключ №2' => 'Enter secret key no. 2',
'Введите процент' => 'Enter the percentage',
'Введите номер' => 'Enter the number',
'Введите URL' => 'Enter the URL',
'Введите Token' => 'Enter the token',
'Введите тему' => 'Enter the subject',
'Сообщение...' => 'Message…',
'Веб.хостинг' => 'Web hosting',
'Модуль быстрой оплаты' => 'Quick payment module',
'Платежная система быстрой оплаты'=> 'Quick payment gateway',
'Яндекс деньги' => 'YooMoney',
'Номер счета' => 'Account number',
'Секретный пароль' => 'Secret password',
'Дополнительный ключ' => 'Additional key',
'ID проекта' => 'Project ID',
'Unitpay секретный ключ' => 'Unitpay secret key',
'Токен' => 'Token',
'Публичный ключ' => 'Public key',
'Введите публичный ключ' => 'Enter the public key',
'Настройки формы Qiwi' => 'Qiwi form settings',
'Включены' => 'Enabled',
'Выключены' => 'Disabled',
'Код темы (themeCode)' => 'Theme code (themeCode)',
'Введите код темы (themeCode)' => 'Enter the theme code (themeCode)',
'Тестовый период' => 'Trial period',
'Заказ запрещен' => 'Ordering disabled',
'Заказ разрешен (по одобрению)' => 'Ordering allowed (with approval)',
'Подтверждение E-mail' => 'E-mail confirmation',
'Тех.работы' => 'Maintenance',
'Хостинг доступен' => 'Hosting available',
'Хостинг закрыт на тех.работы' => 'Hosting closed for maintenance',
'Статус тех.работ' => 'Maintenance status',
'Сообщение тех.работ' => 'Maintenance message',
'Введите сообщение тех.работ' => 'Enter the maintenance message',
'Хостинг временно закрыт на обслуживание!' => 'The hosting is temporarily closed for maintenance!',
'ID приложения' => 'Application ID',
'Введите ID приложения' => 'Enter the application ID',
'Статус авторизаци' => 'Login status',
'Статус бота' => 'Bot status',
'Стоимость смены порта' => 'Port change price',
'Введите стоимость смены порта' => 'Enter the port change price',
'Конвертация бонусов' => 'Bonus conversion',
'Процент реферальной системы (за заказ услуг)' => 'Referral percentage (on service orders)',
'Процент бонусов за пополнение' => 'Bonus percentage on top-ups',
'Контактый номер' => 'Contact number',
'Введите минимальный бонус' => 'Enter the minimum bonus',
'Введите максимальный бонус' => 'Enter the maximum bonus',
'Введите ID пользователя' => 'Enter the user ID',
'Введите login' => 'Enter the login',
'Введите pass' => 'Enter the password',
// ── капча в настройках ───────────────────────────────────────────────────────
'Защита от ботов (reCAPTCHA v2)' => 'Bot protection (reCAPTCHA v2)',
'Выключена (капчи нет на сайте)' => 'Disabled (no captcha on the site)',
'Включена (нужны ключи ниже)' => 'Enabled (requires the keys below)',
'Капча при входе, регистрации, восстановлении пароля и в тикетах. Включайте только после того, как вписали оба ключа.' => 'Captcha on login, registration, password recovery and tickets. Only enable it after both keys are filled in.',
'Site key (публичный ключ) — из панели google.com/recaptcha, тип reCAPTCHA v2 «Я не робот»' => 'Site key (public key) — from google.com/recaptcha, reCAPTCHA v2 "I am not a robot"',
'Secret key (секретный ключ) того же сайта' => 'Secret key for the same site',
// ── акции ────────────────────────────────────────────────────────────────────
'Проведение акции' => 'Run a promotion',
'Провести акцию' => 'Run promotion',
'Начать акцию' => 'Start promotion',
'Раздача реальных денег' => 'Give away real money',
'Выдать только 1 пользователю' => 'Give to one user only',
'Создавать новость об проведённом розыгрыше' => 'Create a news post about the giveaway',
// ── проверка системы ─────────────────────────────────────────────────────────
'Функция' => 'Feature',
'Установка' => 'Installation',
'Работоспособность бд для сервера.'=> 'Database support for servers.',
'Подключение по ssh.' => 'SSH connectivity.',
'Просмотр каптчи.' => 'Captcha rendering.',
'Кэширование памяти.' => 'Memory caching.',
'Время' => 'Time',
'Ссылка для браузера' => 'Browser link',
'1 раз в 00:00' => 'daily at 00:00',
'1 раз в минуту' => 'every minute',
'1 раз в 10 минут' => 'every 10 minutes',
'1 раз в 30 минут' => 'every 30 minutes',
'1 раз в час' => 'hourly',
'1 раз в неделю' => 'weekly',
// ── прочее, найденное при проверке страниц ──────────────────────────────────
'У вас ещё нет аккаунта?' => 'Do not have an account yet?',
'Забыли пароль?' => 'Forgot your password?',
'Восстановить пароль' => 'Reset password',
'Введите Ваше Имя' => 'Enter your first name',
'Введите Вашу Фамилию' => 'Enter your last name',
'Введите Ваш E-Mail' => 'Enter your e-mail',
'Введите Пароль' => 'Enter a password',
'Повторите Пароль' => 'Repeat the password',
'Без приглашения' => 'No invitation',
'Введите ваше имя..' => 'Enter your first name…',
'Введите вашу фамилию..' => 'Enter your last name…',
'Введите ваш email..' => 'Enter your e-mail…',
'Вопрос' => 'Question',
'Проблемы с сервером' => 'Problems with a server',
'Проблемы с аккаунтом' => 'Problems with an account',
'Сотрудничество' => 'Partnership',
'Наша техническая поддержка работает' => 'Our technical support works',
'дней в году. Мы готовы помочь вам в любое время суток.' => 'days a year. We are ready to help you at any hour.',
// ── найдено на второй проверке страниц ──────────────────────────────────────
'Панель' => 'Panel',
'Вход в аккаунт' => 'Account login',
'Попытка входа в аккаунт' => 'Account login attempt',
'в' => 'at',
);
+18
View File
@@ -0,0 +1,18 @@
<?php
/*
Русский — исходный язык шаблонов панели, поэтому словарь ему не нужен:
текст уже написан по-русски прямо в вёрстке, и Lang для кода 'ru' вообще
не загружает переводы (см. engine/main/lang.php).
Файл оставлен как точка расширения: если однажды исходные строки заменят
на нейтральные ключи, русские переводы лягут сюда.
Как добавить свой язык (например немецкий):
1. Скопируйте application/lang/en.php в application/lang/de.php
2. Переведите значения
3. Добавьте код в Lang::AVAILABLE в engine/main/lang.php
Поддерживается и развивается с помощью REDL.IO — Хостинг с искусственным интеллектом.
*/
return array();
@@ -189,6 +189,17 @@ Developed by Samir Shelenko and Alexander Zemlyanoy (https://vk.com/id00v / htt
</div> </div>
</div> </div>
<div class="topbar"> <div class="topbar">
<?php $__lang = Lang::instance(); if($__lang): ?>
<div class="topbar-item mr-1 mr-lg-3">
<div class="btn-group btn-group-sm" role="group" aria-label="Language">
<?php foreach($__lang->available() as $__c): ?>
<a href="<?php echo htmlspecialchars($__lang->switchUrl($__c), ENT_QUOTES, 'UTF-8') ?>"
class="btn btn-sm font-weight-bolder text-uppercase <?php echo $__c === $__lang->code() ? 'btn-primary' : 'btn-light-primary' ?>"
title="<?php echo htmlspecialchars($__lang->name($__c), ENT_QUOTES, 'UTF-8') ?>"><?php echo $__c ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="topbar-item mr-1 mr-lg-3"> <div class="topbar-item mr-1 mr-lg-3">
<a <?if($oplata_status == 1):?> data-toggle="modal" data-target="#hostin" <? else:?>onClick="redirect('/account/pay')"<?endif;?> class="btn btn-fixed-height btn-light-primary font-weight-bolder font-size-sm px-5 my-1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a <?if($oplata_status == 1):?> data-toggle="modal" data-target="#hostin" <? else:?>onClick="redirect('/account/pay')"<?endif;?> class="btn btn-fixed-height btn-light-primary font-weight-bolder font-size-sm px-5 my-1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="svg-icon svg-icon-md"> <span class="svg-icon svg-icon-md">
+11
View File
@@ -189,6 +189,17 @@ Developed by Samir Shelenko and Alexander Zemlyanoy (https://vk.com/id00v / htt
</div> </div>
</div> </div>
<div class="topbar"> <div class="topbar">
<?php $__lang = Lang::instance(); if($__lang): ?>
<div class="topbar-item mr-1 mr-lg-3">
<div class="btn-group btn-group-sm" role="group" aria-label="Language">
<?php foreach($__lang->available() as $__c): ?>
<a href="<?php echo htmlspecialchars($__lang->switchUrl($__c), ENT_QUOTES, 'UTF-8') ?>"
class="btn btn-sm font-weight-bolder text-uppercase <?php echo $__c === $__lang->code() ? 'btn-primary' : 'btn-light-primary' ?>"
title="<?php echo htmlspecialchars($__lang->name($__c), ENT_QUOTES, 'UTF-8') ?>"><?php echo $__c ?></a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="topbar-item mr-1 mr-lg-3"> <div class="topbar-item mr-1 mr-lg-3">
<a <?if($oplata_status == 1):?> data-toggle="modal" data-target="#hostin" <? else:?>onClick="redirect('/account/pay')"<?endif;?> class="btn btn-fixed-height btn-light-primary font-weight-bolder font-size-sm px-5 my-1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a <?if($oplata_status == 1):?> data-toggle="modal" data-target="#hostin" <? else:?>onClick="redirect('/account/pay')"<?endif;?> class="btn btn-fixed-height btn-light-primary font-weight-bolder font-size-sm px-5 my-1" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="svg-icon svg-icon-md"> <span class="svg-icon svg-icon-md">
@@ -133,6 +133,11 @@ Developed by Samir Shelenko and Alexander Zemlyanoy (https://vk.com/id00v / htt
<span class="text-muted font-weight-bold mr-2">2026©</span> <span class="text-muted font-weight-bold mr-2">2026©</span>
<a href="https://redl.io" target="_blank" class="text-dark-50 text-hover-primary"><?php echo $description ?></a> <a href="https://redl.io" target="_blank" class="text-dark-50 text-hover-primary"><?php echo $description ?></a>
</div> </div>
<div class="font-size-h6 font-weight-bolder order-3 py-2 py-md-0 mr-md-4">
<?php $__lang = Lang::instance(); if($__lang): ?>
<?php foreach($__lang->available() as $__i => $__c): ?><?php if($__i): ?><span class="text-muted mx-1">·</span><?php endif; ?><a href="<?php echo htmlspecialchars($__lang->switchUrl($__c), ENT_QUOTES, 'UTF-8') ?>" class="<?php echo $__c === $__lang->code() ? 'text-primary' : 'text-dark-50 text-hover-primary' ?>"><?php echo htmlspecialchars($__lang->name($__c), ENT_QUOTES, 'UTF-8') ?></a><?php endforeach; ?>
<?php endif; ?>
</div>
<div class="font-size-h5 font-weight-bolder order-1 order-md-2 py-2 py-md-0"> <div class="font-size-h5 font-weight-bolder order-1 order-md-2 py-2 py-md-0">
<a href="javascript:;" data-toggle="modal" data-target="#hostin_supports" class="text-primary">Обратная связь</a> <a href="javascript:;" data-toggle="modal" data-target="#hostin_supports" class="text-primary">Обратная связь</a>
</div> </div>
+259
View File
@@ -0,0 +1,259 @@
<?php
/*
Многоязычность панели. Добавлено в сборке REDL.
Поддерживается и развивается с помощью REDL.IO — Хостинг с искусственным интеллектом.
Как работает
------------
Панель изначально написана с русским текстом прямо в шаблонах (около 1500 строк
в 200 файлах). Переписывать их все на вызовы t() — большой риск что-нибудь сломать,
поэтому перевод делается на уровне ГОТОВОГО ОТВЕТА: index.php оборачивает вывод в
ob_start(), и перед отправкой браузеру строки заменяются по словарю.
Плюсы такого подхода:
* работает сразу во всей панели, включая админку и письма;
* ничего не может «исчезнуть» — если фразы нет в словаре, остаётся русский текст;
* добавить язык = положить один файл в application/lang/.
Порядок определения языка:
1. ?lang=en — явный выбор пользователя, запоминается в cookie на год
2. cookie lang — прошлый выбор
3. Accept-Language браузера — автоопределение
4. 'lang' из application/config.php, иначе русский
*/
class Lang {
/** Языки, для которых есть словарь. */
const AVAILABLE = array('ru', 'en');
/** Языки, для которых уместнее оставить русский интерфейс. */
private static $ruFamily = array('ru','uk','be','kk','ky','uz','az','hy','ka','tg','tk','mo','md');
private $code = 'ru';
private $map = array();
private $patterns = null;
private static $instance = null;
public function __construct($registry = null) {
$this->code = $this->detect($registry);
// Русский — исходный язык шаблонов, словарь ему не нужен.
if ($this->code !== 'ru') {
$file = APPLICATION_DIR . 'lang/' . $this->code . '.php';
if (is_readable($file)) {
$map = include($file);
if (is_array($map)) {
// пустые переводы отбрасываем, иначе текст пропал бы со страницы
foreach ($map as $from => $to) {
if (is_string($from) && is_string($to) && $from !== '' && $to !== '') {
$this->map[$from] = $to;
}
}
}
}
}
self::$instance = $this;
}
public static function instance() {
return self::$instance;
}
public function code() {
return $this->code;
}
public function available() {
return self::AVAILABLE;
}
/** Человеческое имя языка для переключателя. */
public function name($code) {
$names = array('ru' => 'Русский', 'en' => 'English');
return isset($names[$code]) ? $names[$code] : strtoupper($code);
}
/** Адрес текущей страницы с другим языком — для ссылок переключателя. */
public function switchUrl($code) {
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
$parts = explode('#', $uri, 2);
$uri = $parts[0];
$parts = explode('?', $uri, 2);
$path = $parts[0];
$query = array();
if (isset($parts[1]) && $parts[1] !== '') {
parse_str($parts[1], $query);
}
$query['lang'] = $code;
return $path . '?' . http_build_query($query);
}
private function detect($registry) {
// 1. явный выбор
if (isset($_GET['lang']) && in_array($_GET['lang'], self::AVAILABLE, true)) {
$code = $_GET['lang'];
if (!headers_sent()) {
setcookie('lang', $code, time() + 31536000, '/');
}
$_COOKIE['lang'] = $code;
return $code;
}
// 2. прошлый выбор
if (isset($_COOKIE['lang']) && in_array($_COOKIE['lang'], self::AVAILABLE, true)) {
return $_COOKIE['lang'];
}
// 3. автоопределение по браузеру
if (!empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
foreach (self::parseAccept($_SERVER['HTTP_ACCEPT_LANGUAGE']) as $tag) {
$short = strtolower(substr($tag, 0, 2));
if (in_array($short, self::$ruFamily, true)) {
return 'ru';
}
if (in_array($short, self::AVAILABLE, true)) {
return $short;
}
}
// браузер просит язык, которого у нас нет и который не русскоязычный
return 'en';
}
// 4. значение по умолчанию из конфигурации
if ($registry && isset($registry->config)) {
$default = $registry->config->lang;
if ($default && in_array($default, self::AVAILABLE, true)) {
return $default;
}
}
return 'ru';
}
/** Разбор Accept-Language в список тегов по убыванию веса q. */
private static function parseAccept($header) {
$tags = array();
foreach (explode(',', $header) as $chunk) {
$chunk = trim($chunk);
if ($chunk === '') { continue; }
$q = 1.0;
if (strpos($chunk, ';') !== false) {
list($chunk, $params) = explode(';', $chunk, 2);
if (preg_match('~q\s*=\s*([0-9.]+)~i', $params, $m)) {
$q = (float)$m[1];
}
$chunk = trim($chunk);
}
if ($chunk !== '' && $chunk !== '*') {
$tags[] = array($chunk, $q);
}
}
usort($tags, function($a, $b) {
if ($a[1] == $b[1]) { return 0; }
return ($a[1] < $b[1]) ? 1 : -1;
});
$out = array();
foreach ($tags as $t) { $out[] = $t[0]; }
return $out;
}
/** Перевод одной строки. Нет в словаре — возвращаем как есть. */
public function get($text) {
return isset($this->map[$text]) ? $this->map[$text] : $text;
}
/**
* Коллбэк для ob_start: переводит весь ответ перед отправкой.
* Ответы AJAX обрабатываются отдельно: json_encode экранирует кириллицу
* в \uXXXX, и словарь по сырому UTF-8 туда бы не попал.
*/
public function apply($buffer) {
if ($this->code === 'ru' || !$this->map || !is_string($buffer) || $buffer === '') {
return $buffer;
}
$head = ltrim($buffer);
if ($head !== '' && ($head[0] === '{' || $head[0] === '[')) {
$data = json_decode($head, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
$json = json_encode($this->walk($data), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json !== false) {
return $json;
}
}
}
return $this->replace($buffer);
}
private function walk($value) {
if (is_array($value)) {
$out = array();
foreach ($value as $k => $v) { $out[$k] = $this->walk($v); }
return $out;
}
return is_string($value) ? $this->replace($value) : $value;
}
/**
* Замена по словарю ТОЛЬКО на границах слов.
*
* Простой strtr() здесь не годится: короткий ключ подменяется внутри более
* длинного слова и получается каша из двух языков — 'Мод' превращал
* «Модуль» в «Modуль», а 'день' делал из «деньги» «dayги». Поэтому вокруг
* каждого ключа требуем отсутствие буквы. Цифра слева разрешена намеренно:
* иначе «0руб.» осталось бы без перевода.
*
* Ключи отсортированы от длинных к коротким, чтобы «Сменить пароль»
* срабатывало раньше, чем «Пароль».
*/
private function replace($text) {
if ($this->patterns === null) {
$this->buildPatterns();
}
foreach ($this->patterns as $pattern) {
$out = preg_replace_callback($pattern, array($this, 'hit'), $text);
if ($out !== null) { $text = $out; }
}
return $text;
}
private function hit($m) {
return isset($this->map[$m[1]]) ? $this->map[$m[1]] : $m[1];
}
private function buildPatterns() {
$keys = array_keys($this->map);
// длинные первыми: иначе короткий ключ перехватит часть длинной фразы
usort($keys, function($a, $b) {
$la = mb_strlen($a, 'UTF-8'); $lb = mb_strlen($b, 'UTF-8');
if ($la === $lb) { return strcmp($a, $b); }
return ($la < $lb) ? 1 : -1;
});
$this->patterns = array();
// режем на группы, чтобы не упереться в предел размера шаблона PCRE
foreach (array_chunk($keys, 150) as $chunk) {
$quoted = array();
foreach ($chunk as $k) { $quoted[] = preg_quote($k, '~'); }
$this->patterns[] = '~(?<!\p{L})(' . implode('|', $quoted) . ')(?!\p{L})~u';
}
}
}
/**
* Перевод строки в шаблонах и контроллерах: t('Сохранить').
* Поддерживает подстановку как sprintf: t('Осталось %d дней', $n).
*/
function t($text) {
$lang = Lang::instance();
$out = $lang ? $lang->get($text) : $text;
if (func_num_args() > 1) {
$args = func_get_args();
$args[0] = $out;
$formatted = @call_user_func_array('sprintf', $args);
if (is_string($formatted) && $formatted !== '') {
return $formatted;
}
}
return $out;
}
+7
View File
@@ -12,6 +12,7 @@ require_once(ENGINE_DIR . 'main/model.php');
require_once(ENGINE_DIR . 'main/registry.php'); require_once(ENGINE_DIR . 'main/registry.php');
require_once(ENGINE_DIR . 'main/config.php'); require_once(ENGINE_DIR . 'main/config.php');
require_once(ENGINE_DIR . 'main/lang.php');
require_once(ENGINE_DIR . 'main/game_settings.php'); require_once(ENGINE_DIR . 'main/game_settings.php');
require_once(ENGINE_DIR . 'main/request.php'); require_once(ENGINE_DIR . 'main/request.php');
require_once(ENGINE_DIR . 'main/session.php'); require_once(ENGINE_DIR . 'main/session.php');
@@ -28,6 +29,12 @@ $registry = new Registry();
$config = new Config(); $config = new Config();
$registry->config = $config; $registry->config = $config;
// Язык интерфейса. Перевод применяется к готовому ответу (см. engine/main/lang.php),
// поэтому весь вывод собирается в буфер — включая страницы, которые делают exit().
$lang = new Lang($registry);
$registry->lang = $lang;
ob_start(array($lang, 'apply'));
$game_settings = new Game_settings(); $game_settings = new Game_settings();
$registry->game_settings = $game_settings; $registry->game_settings = $game_settings;
+2
View File
@@ -1,3 +1,5 @@
**Русский** · [English](SECURITY.en.md)
# Аудит безопасности: что нашли и что исправили # Аудит безопасности: что нашли и что исправили
Аудит исходников сборки HostinPL 5.6 («nulled»-копия). Проверялось: скрытые закладки и вебшеллы, Аудит исходников сборки HostinPL 5.6 («nulled»-копия). Проверялось: скрытые закладки и вебшеллы,
+25 -2
View File
@@ -1,3 +1,5 @@
**Русский** · [English](CHANGES.en.md)
# Что изменено по сравнению с оригиналом # Что изменено по сравнению с оригиналом
Оригинал: **HostinPL 5.6** в виде «nulled»-сборки (форк `Xopowblu-4EJlOBEK/HostinPL-5.6`), Оригинал: **HostinPL 5.6** в виде «nulled»-сборки (форк `Xopowblu-4EJlOBEK/HostinPL-5.6`),
@@ -153,7 +155,28 @@ Node.js берётся из NodeSource **по HTTPS с проверкой клю
--- ---
## 8. Брендинг и даты ## 8. Языки интерфейса: русский и английский
Оригинал был только на русском, весь текст вшит в шаблоны. В сборке появился слой перевода:
* `engine/main/lang.php` — класс `Lang` и глобальный помощник `t()`
* `application/lang/ru.php`, `application/lang/en.php` — словари
* Порядок выбора языка: параметр `?lang=` → cookie `lang` → **заголовок браузера
`Accept-Language`** → значение по умолчанию из конфига
* Переключатель (RU / EN) в шапке кабинета, админки и в подвале страницы входа
* Фразы, которых нет в словаре, остаются на русском — из интерфейса ничего не может пропасть
Перевод применяется к готовому ответу через `ob_start()`, поэтому 200 файлов шаблонов
переписывать не понадобилось, и покрытие сразу распространяется на админку и письма.
Замена работает только на границах слов: без этого короткий ключ портил длинные слова
(«Мод» превращал «Модуль» в «Modуль», «день» делал из «деньги» «dayги»).
Полнота проверена обходом 20 разделов: непереведённых фраз не осталось, мешанины языков нет.
Подробности — в [ИНСТРУКЦИЯ.md](ИНСТРУКЦИЯ.md), раздел «Язык интерфейса».
---
## 9. Брендинг и даты
* Год в подвалах: `2020©``2026©` (`views/common/footer.php`, `views/common/loginheader.php`) * Год в подвалах: `2020©``2026©` (`views/common/footer.php`, `views/common/loginheader.php`)
* Название и описание — REDL.IO: `application/config.php` (`description`, `keywords`, * Название и описание — REDL.IO: `application/config.php` (`description`, `keywords`,
@@ -169,7 +192,7 @@ Node.js берётся из NodeSource **по HTTPS с проверкой клю
--- ---
## 9. phpMyAdmin ## 10. phpMyAdmin
Панель ссылается на `/phpmyadmin` из админки. Оригинальный установщик ставил phpMyAdmin через Панель ссылается на `/phpmyadmin` из админки. Оригинальный установщик ставил phpMyAdmin через
Apache, что на nginx не работало. Теперь он ставится из репозитория дистрибутива и отдаётся Apache, что на nginx не работало. Теперь он ставится из репозитория дистрибутива и отдаётся
+54 -9
View File
@@ -1,3 +1,5 @@
**Русский** · [English](GUIDE.en.md)
# Полная инструкция # Полная инструкция
От пустого сервера до принимающего клиентов хостинга. От пустого сервера до принимающего клиентов хостинга.
@@ -12,10 +14,11 @@
4. [Подключение ноды к панели](#4-подключение-ноды-к-панели) 4. [Подключение ноды к панели](#4-подключение-ноды-к-панели)
5. [Сборки игр](#5-сборки-игр) 5. [Сборки игр](#5-сборки-игр)
6. [Настройка панели](#6-настройка-панели) 6. [Настройка панели](#6-настройка-панели)
7. [Капча](#7-капча) 7. [Язык интерфейса](#7-язык-интерфейса)
8. [HTTPS и домен](#8-https-и-домен) 8. [Капча](#8-капча)
9. [Обслуживание](#9-обслуживание) 9. [HTTPS и домен](#9-https-и-домен)
10. [Если что-то не работает](#10-если-что-то-не-работает) 10. [Обслуживание](#10-обслуживание)
11. [Если что-то не работает](#11-если-что-то-не-работает)
--- ---
@@ -219,7 +222,49 @@ Minecraft (Paper) — с `papermc.io`, RAGE:MP — с `rage.mp`, игры Steam
--- ---
## 7. Капча ## 7. Язык интерфейса
Панель говорит на **русском и английском**. Язык выбирается в таком порядке:
1. `?lang=en` или `?lang=ru` в адресе — явный выбор, запоминается в cookie на год
2. cookie `lang` — прошлый выбор
3. **заголовок браузера `Accept-Language`** — автоопределение
4. параметр `lang` в `application/config.php` (по умолчанию `ru`)
Автоопределение намеренно оставляет русский для русскоязычных локалей (ru, uk, be, kk и других)
и переключает на английский для всех остальных — то есть посетитель из Германии или Бразилии
сразу получает английский интерфейс, ничего не нажимая.
**Переключатель** стоит в верхней панели кабинета и админки (кнопки `RU` / `EN` рядом с балансом)
и в подвале страницы входа.
**Как это сделано.** Панель написана с русским текстом прямо в шаблонах — около 1500 строк в
200 файлах. Вместо того чтобы переписывать их все, перевод применяется к готовому ответу:
`index.php` оборачивает вывод в `ob_start()`, и перед отправкой браузеру строки заменяются по
словарю (`engine/main/lang.php`). Это разом покрывает всю панель, включая админку и письма, а
фразы, которых нет в словаре, просто остаются русскими — ничего не может пропасть с экрана.
Замена работает **только на границах слов**, поэтому короткий ключ не может испортить слово
подлиннее (без этого «Мод» превращал «Модуль» в «Modуль»).
**Как добавить свой язык**, например немецкий:
```bash
cp panel/application/lang/en.php panel/application/lang/de.php
# перевести значения в de.php, затем добавить 'de' в Lang::AVAILABLE
# в panel/engine/main/lang.php
```
Полноту словаря мы проверяли обходом страниц: все 20 разделов, которые проходим, отдаются без
остатков русского текста. Если увидите непереведённую фразу — допишите её в
`application/lang/en.php`, ключ это русский текст ровно в том виде, в каком он на странице.
Перевод включается только когда язык не русский; в русском режиме словарь даже не загружается,
поэтому накладных расходов нет.
---
## 8. Капча
По умолчанию **выключена** — регистрация и вход работают без неё, поля просто нет. По умолчанию **выключена** — регистрация и вход работают без неё, поля просто нет.
@@ -246,7 +291,7 @@ sudo service php8.4-fpm restart # подставьте свою версию
--- ---
## 8. HTTPS и домен ## 9. HTTPS и домен
Панель ставится на HTTP. Для сертификата: Панель ставится на HTTP. Для сертификата:
@@ -269,7 +314,7 @@ sudo service php8.4-fpm restart
--- ---
## 9. Обслуживание ## 10. Обслуживание
**Резервные копии.** Сохраняйте базу и конфиг: **Резервные копии.** Сохраняйте базу и конфиг:
@@ -303,7 +348,7 @@ sudo service php8.4-fpm restart
--- ---
## 10. Если что-то не работает ## 11. Если что-то не работает
**«Ошибка: Не удалось загрузить контроллер …»** — панель не нашла контроллер для этого адреса. **«Ошибка: Не удалось загрузить контроллер …»** — панель не нашла контроллер для этого адреса.
Обычно это ссылка на то, чего нет: например `/phpmyadmin`, когда phpMyAdmin не установлен Обычно это ссылка на то, чего нет: например `/phpmyadmin`, когда phpMyAdmin не установлен
@@ -314,7 +359,7 @@ sudo service php8.4-fpm restart
Частая причина: `short_open_tag = Off`. Панель использует короткие теги `<? ?>`, установщик Частая причина: `short_open_tag = Off`. Панель использует короткие теги `<? ?>`, установщик
включает эту опцию сам, но при переустановке PHP её нужно вернуть. включает эту опцию сам, но при переустановке PHP её нужно вернуть.
**Не могу войти, на форме «Invalid site key»** — включена капча без ключей, см. раздел 7. **Не могу войти, на форме «Invalid site key»** — включена капча без ключей, см. раздел 8.
**Планировщик не работает** (статистика не обновляется, просроченные сервера не отключаются): **Планировщик не работает** (статистика не обновляется, просроченные сервера не отключаются):