Merge pull request #2 from Sora39831/dev

Enhance user registration, dashboard, and MariaDB support
This commit is contained in:
Sora39 2026-04-03 10:04:19 +08:00 committed by GitHub
commit e74ae4ef1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
61 changed files with 9521 additions and 8927 deletions

View file

@ -4,6 +4,7 @@ package config
import (
_ "embed"
"encoding/json"
"fmt"
"io"
"os"
@ -159,3 +160,121 @@ func init() {
}
_ = copyFile(oldDBPath, newDBPath) // ignore error
}
// GetDBTypeFromJSON reads the dbType setting directly from the JSON config file.
// This is needed before the database is initialized. Falls back to "sqlite".
func GetDBTypeFromJSON() string {
data, err := os.ReadFile(GetSettingPath())
if err != nil {
return "sqlite"
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return "sqlite"
}
// Check nested format: "other" group contains "dbType"
if other, ok := settings["other"].(map[string]any); ok {
if dbType, ok := other["dbType"].(string); ok && dbType != "" {
return dbType
}
}
// Check flat format: top-level "dbType"
if dbType, ok := settings["dbType"].(string); ok && dbType != "" {
return dbType
}
return "sqlite"
}
// DBConfig holds MariaDB connection settings read from the JSON config file.
type DBConfig struct {
Type string
Host string
Port string
User string
Password string
Name string
}
// GetDBConfigFromJSON reads all MariaDB connection settings from the JSON config file.
func GetDBConfigFromJSON() DBConfig {
data, err := os.ReadFile(GetSettingPath())
if err != nil {
return DBConfig{Type: "sqlite", Host: "127.0.0.1", Port: "3306", Name: "3xui"}
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return DBConfig{Type: "sqlite", Host: "127.0.0.1", Port: "3306", Name: "3xui"}
}
// readString extracts a value from either nested (group.key) or flat format
readString := func(nestedGroup, flatKey string) string {
if group, ok := settings[nestedGroup].(map[string]any); ok {
if v, ok := group[flatKey].(string); ok {
return v
}
}
if v, ok := settings[flatKey].(string); ok {
return v
}
return ""
}
// Read dbType from the same parsed settings
dbType := "sqlite"
if other, ok := settings["other"].(map[string]any); ok {
if t, ok := other["dbType"].(string); ok && t != "" {
dbType = t
}
} else if t, ok := settings["dbType"].(string); ok && t != "" {
dbType = t
}
return DBConfig{
Type: dbType,
Host: readString("other", "dbHost"),
Port: readString("other", "dbPort"),
User: readString("other", "dbUser"),
Password: readString("other", "dbPassword"),
Name: readString("other", "dbName"),
}
}
// WriteSettingToJSON writes a single setting key to the JSON config file.
// It reads the existing file, updates the value, and writes back.
func WriteSettingToJSON(key, value string) error {
path := GetSettingPath()
data, err := os.ReadFile(path)
if err != nil {
return err
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return err
}
// Check if the key lives in a nested group
groupMap := map[string]string{
"dbType": "other", "dbHost": "other", "dbPort": "other",
"dbUser": "other", "dbPassword": "other", "dbName": "other",
}
if group, ok := groupMap[key]; ok {
if _, exists := settings[group]; !exists {
settings[group] = make(map[string]any)
}
settings[group].(map[string]any)[key] = value
} else {
settings[key] = value
}
out, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, out, 0644)
}

131
config/config_test.go Normal file
View file

@ -0,0 +1,131 @@
package config
import (
"strings"
"testing"
)
func TestGetVersion(t *testing.T) {
v := GetVersion()
if v == "" {
// version file might be empty in test, that's ok
t.Log("version is empty (expected in test environment)")
}
}
func TestGetName(t *testing.T) {
n := GetName()
if n == "" {
t.Fatal("name should not be empty")
}
if strings.TrimSpace(n) != n {
t.Error("name should be trimmed")
}
}
func TestIsDebugDefault(t *testing.T) {
t.Setenv("XUI_DEBUG", "")
if IsDebug() {
t.Error("IsDebug should return false by default")
}
}
func TestIsDebugTrue(t *testing.T) {
t.Setenv("XUI_DEBUG", "true")
if !IsDebug() {
t.Error("IsDebug should return true when XUI_DEBUG=true")
}
}
func TestIsDebugFalse(t *testing.T) {
t.Setenv("XUI_DEBUG", "false")
if IsDebug() {
t.Error("IsDebug should return false when XUI_DEBUG=false")
}
}
func TestGetLogLevelDefault(t *testing.T) {
t.Setenv("XUI_DEBUG", "")
t.Setenv("XUI_LOG_LEVEL", "")
if GetLogLevel() != Info {
t.Errorf("default log level should be Info, got %s", GetLogLevel())
}
}
func TestGetLogLevelDebug(t *testing.T) {
t.Setenv("XUI_DEBUG", "true")
if GetLogLevel() != Debug {
t.Errorf("log level should be Debug when XUI_DEBUG=true, got %s", GetLogLevel())
}
}
func TestGetLogLevelCustom(t *testing.T) {
t.Setenv("XUI_DEBUG", "")
t.Setenv("XUI_LOG_LEVEL", "warning")
if GetLogLevel() != Warning {
t.Errorf("log level should be Warning, got %s", GetLogLevel())
}
}
func TestGetBinFolderPathDefault(t *testing.T) {
t.Setenv("XUI_BIN_FOLDER", "")
if GetBinFolderPath() != "bin" {
t.Errorf("default bin folder should be 'bin', got %s", GetBinFolderPath())
}
}
func TestGetBinFolderPathCustom(t *testing.T) {
t.Setenv("XUI_BIN_FOLDER", "/custom/bin")
if GetBinFolderPath() != "/custom/bin" {
t.Errorf("bin folder should be '/custom/bin', got %s", GetBinFolderPath())
}
}
func TestGetDBFolderPathDefault(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", "")
folder := GetDBFolderPath()
// On Linux without env var, should be "/etc/x-ui"
if folder != "/etc/x-ui" {
t.Errorf("default DB folder should be '/etc/x-ui', got %s", folder)
}
}
func TestGetDBFolderPathCustom(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", "/tmp/test-db")
if GetDBFolderPath() != "/tmp/test-db" {
t.Errorf("DB folder should be '/tmp/test-db', got %s", GetDBFolderPath())
}
}
func TestGetDBPath(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", "/tmp/test")
dbPath := GetDBPath()
expected := "/tmp/test/" + GetName() + ".db"
if dbPath != expected {
t.Errorf("GetDBPath() = %q, want %q", dbPath, expected)
}
}
func TestGetSettingPath(t *testing.T) {
t.Setenv("XUI_DB_FOLDER", "/tmp/test")
settingPath := GetSettingPath()
expected := "/tmp/test/" + GetName() + ".json"
if settingPath != expected {
t.Errorf("GetSettingPath() = %q, want %q", settingPath, expected)
}
}
func TestGetLogFolderDefault(t *testing.T) {
t.Setenv("XUI_LOG_FOLDER", "")
folder := GetLogFolder()
if folder != "/var/log/x-ui" {
t.Errorf("default log folder should be '/var/log/x-ui', got %s", folder)
}
}
func TestGetLogFolderCustom(t *testing.T) {
t.Setenv("XUI_LOG_FOLDER", "/custom/logs")
if GetLogFolder() != "/custom/logs" {
t.Errorf("log folder should be '/custom/logs', got %s", GetLogFolder())
}
}

View file

@ -1,5 +1,5 @@
// Package database provides database initialization, migration, and management utilities
// for the 3x-ui panel using GORM with SQLite.
// for the 3x-ui panel using GORM with SQLite or MariaDB.
package database
import (
@ -17,6 +17,8 @@ import (
"github.com/mhsanaei/3x-ui/v2/util/crypto"
"github.com/mhsanaei/3x-ui/v2/xray"
mysql2 "github.com/go-sql-driver/mysql"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
@ -68,7 +70,15 @@ func initUser() error {
Password: hashedPassword,
Role: "admin",
}
return db.Create(user).Error
if err := db.Create(user).Error; err != nil {
return err
}
// Mark password hashing seeder as done since initUser already uses bcrypt
hashSeeder := &model.HistoryOfSeeders{
SeederName: "UserPasswordHash",
}
return db.Create(hashSeeder).Error
}
return nil
}
@ -106,7 +116,26 @@ func runSeeders(isUsersEmpty bool) error {
hashSeeder := &model.HistoryOfSeeders{
SeederName: "UserPasswordHash",
}
return db.Create(hashSeeder).Error
if err := db.Create(hashSeeder).Error; err != nil {
return err
}
}
if !slices.Contains(seedersHistory, "RemoveClientTrafficEmailUnique") {
// Drop the old unique index on client_traffics.email to allow
// the same email across multiple inbounds
dbType := config.GetDBTypeFromJSON()
if dbType == "mariadb" {
db.Exec("DROP INDEX IF EXISTS idx_client_traffics_email ON client_traffics")
} else {
db.Exec("DROP INDEX IF EXISTS idx_client_traffics_email")
}
uniqueSeeder := &model.HistoryOfSeeders{
SeederName: "RemoveClientTrafficEmailUnique",
}
if err := db.Create(uniqueSeeder).Error; err != nil {
return err
}
}
}
@ -121,7 +150,61 @@ func isTableEmpty(tableName string) (bool, error) {
}
// InitDB sets up the database connection, migrates models, and runs seeders.
func InitDB(dbPath string) error {
// It reads the dbType from the JSON config to determine whether to use SQLite or MariaDB.
func InitDB() error {
CloseDB() // close any existing connection before re-initializing
dbType := config.GetDBTypeFromJSON()
var err error
switch dbType {
case "mariadb":
err = initMariaDB()
default:
err = initSQLite(config.GetDBPath())
}
if err != nil {
return err
}
if err := initModels(); err != nil {
return err
}
if err := initUser(); err != nil {
return err
}
isUsersEmpty, err := isTableEmpty("users")
if err != nil {
return err
}
return runSeeders(isUsersEmpty)
}
// InitDBWithPath is a convenience function for tests and migrations that need
// to open a specific SQLite file.
func InitDBWithPath(dbPath string) error {
CloseDB() // close any existing connection before re-initializing
if err := initSQLite(dbPath); err != nil {
return err
}
if err := initModels(); err != nil {
return err
}
if err := initUser(); err != nil {
return err
}
isUsersEmpty, err := isTableEmpty("users")
if err != nil {
return err
}
return runSeeders(isUsersEmpty)
}
// initSQLite opens a SQLite database connection and runs model migrations.
func initSQLite(dbPath string) error {
dir := path.Dir(dbPath)
err := os.MkdirAll(dir, fs.ModePerm)
if err != nil {
@ -144,19 +227,49 @@ func InitDB(dbPath string) error {
return err
}
if err := initModels(); err != nil {
return err
return nil
}
// buildMariaDBDSN constructs a MariaDB DSN from the given config using
// go-sql-driver/mysql's Config to properly escape special characters in credentials.
func buildMariaDBDSN(dbConfig config.DBConfig) string {
cfg := mysql2.Config{
User: dbConfig.User,
Passwd: dbConfig.Password,
Net: "tcp",
Addr: dbConfig.Host + ":" + dbConfig.Port,
DBName: dbConfig.Name,
Params: map[string]string{
"charset": "utf8mb4",
"parseTime": "True",
"loc": "Local",
},
}
return cfg.FormatDSN()
}
// initMariaDB opens a MariaDB connection and runs model migrations.
func initMariaDB() error {
dbConfig := config.GetDBConfigFromJSON()
dsn := buildMariaDBDSN(dbConfig)
var gormLogger logger.Interface
if config.IsDebug() {
gormLogger = logger.Default
} else {
gormLogger = logger.Discard
}
isUsersEmpty, err := isTableEmpty("users")
var err error
c := &gorm.Config{
Logger: gormLogger,
}
db, err = gorm.Open(mysql.Open(dsn), c)
if err != nil {
return err
}
if err := initUser(); err != nil {
return err
}
return runSeeders(isUsersEmpty)
return nil
}
// CloseDB closes the database connection if it exists.
@ -193,13 +306,12 @@ func IsSQLiteDB(file io.ReaderAt) (bool, error) {
}
// Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
// For MariaDB, this is a no-op.
func Checkpoint() error {
// Update WAL
err := db.Exec("PRAGMA wal_checkpoint;").Error
if err != nil {
return err
if config.GetDBTypeFromJSON() != "sqlite" {
return nil
}
return nil
return db.Exec("PRAGMA wal_checkpoint;").Error
}
// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection

214
database/db_test.go Normal file
View file

@ -0,0 +1,214 @@
package database
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v2/database/model"
)
func setupTestDB(t *testing.T) {
t.Helper()
tmpDir := t.TempDir()
t.Setenv("XUI_DEBUG", "")
dbPath := filepath.Join(tmpDir, "test.db")
if err := InitDBWithPath(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
t.Cleanup(func() {
CloseDB()
})
}
func TestIsSQLiteDB_ValidFile(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "valid.db")
if err := InitDBWithPath(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
defer CloseDB()
f, err := os.Open(dbPath)
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer f.Close()
ok, err := IsSQLiteDB(f)
if err != nil {
t.Fatalf("IsSQLiteDB error: %v", err)
}
if !ok {
t.Error("IsSQLiteDB should return true for a valid SQLite file")
}
}
func TestIsSQLiteDB_InvalidFile(t *testing.T) {
tmpDir := t.TempDir()
notADB := filepath.Join(tmpDir, "notdb.txt")
if err := os.WriteFile(notADB, []byte("this is not a database"), 0644); err != nil {
t.Fatal(err)
}
f, err := os.Open(notADB)
if err != nil {
t.Fatal(err)
}
defer f.Close()
ok, err := IsSQLiteDB(f)
if err != nil {
t.Fatalf("IsSQLiteDB error: %v", err)
}
if ok {
t.Error("IsSQLiteDB should return false for a non-SQLite file")
}
}
func TestIsSQLiteDB_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
empty := filepath.Join(tmpDir, "empty.db")
if err := os.WriteFile(empty, []byte{}, 0644); err != nil {
t.Fatal(err)
}
f, err := os.Open(empty)
if err != nil {
t.Fatal(err)
}
defer f.Close()
ok, err := IsSQLiteDB(f)
// Empty file returns EOF since there aren't enough bytes to read
if err == nil && ok {
t.Error("IsSQLiteDB should return false for an empty file")
}
}
func TestIsSQLiteDB_WrongContent(t *testing.T) {
// File with 16 bytes (matching SQLite header length) but wrong signature content
r := bytes.NewReader([]byte("SQLite for !!"))
ok, err := IsSQLiteDB(r)
if err != nil {
t.Fatalf("IsSQLiteDB error: %v", err)
}
if ok {
t.Error("IsSQLiteDB should return false for wrong signature content")
}
}
func TestInitDB_CreatesTables(t *testing.T) {
setupTestDB(t)
// Verify all tables exist by querying them
tables := []string{"users", "inbounds", "outbound_traffics", "settings", "inbound_client_ips", "client_traffics", "history_of_seeders"}
for _, table := range tables {
var count int64
if err := db.Table(table).Count(&count).Error; err != nil {
t.Errorf("table %q should exist but got error: %v", table, err)
}
}
}
func TestInitDB_CreatesDefaultUser(t *testing.T) {
setupTestDB(t)
var user model.User
if err := db.First(&user).Error; err != nil {
t.Fatalf("should have a default user: %v", err)
}
if user.Username != "admin" {
t.Errorf("default username should be 'admin', got %q", user.Username)
}
if user.Role != "admin" {
t.Errorf("default role should be 'admin', got %q", user.Role)
}
// Password should be a bcrypt hash, not plaintext
if user.Password == "admin" {
t.Error("default password should be hashed, not plaintext")
}
}
func TestInitDB_Idempotent(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("XUI_DEBUG", "")
dbPath := filepath.Join(tmpDir, "idempotent.db")
// First init
if err := InitDBWithPath(dbPath); err != nil {
t.Fatalf("first InitDB failed: %v", err)
}
CloseDB()
// Second init on the same file should not fail
if err := InitDBWithPath(dbPath); err != nil {
t.Fatalf("second InitDB failed: %v", err)
}
defer CloseDB()
// Should still have exactly one default user
var count int64
db.Model(&model.User{}).Count(&count)
if count != 1 {
t.Errorf("expected 1 user after second init, got %d", count)
}
}
func TestValidateSQLiteDB_ValidDB(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "valid.db")
if err := InitDBWithPath(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
CloseDB()
if err := ValidateSQLiteDB(dbPath); err != nil {
t.Errorf("ValidateSQLiteDB should pass for valid DB: %v", err)
}
}
func TestValidateSQLiteDB_NonExistent(t *testing.T) {
err := ValidateSQLiteDB("/tmp/does-not-exist-12345.db")
if err == nil {
t.Error("ValidateSQLiteDB should fail for non-existent file")
}
}
func TestValidateSQLiteDB_CorruptDB(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "corrupt.db")
// Write garbage that looks like SQLite header but is corrupt
garbage := make([]byte, 4096)
copy(garbage, []byte("SQLite format 3\x00"))
if err := os.WriteFile(dbPath, garbage, 0644); err != nil {
t.Fatal(err)
}
err := ValidateSQLiteDB(dbPath)
if err == nil {
t.Error("ValidateSQLiteDB should fail for corrupt DB")
}
}
func TestIsNotFound(t *testing.T) {
if IsNotFound(nil) {
t.Error("IsNotFound should return false for nil")
}
}
func TestInitUser_OnlyOnce(t *testing.T) {
setupTestDB(t)
// initUser should not create a second user when table is not empty
if err := initUser(); err != nil {
t.Fatalf("initUser error: %v", err)
}
var count int64
db.Model(&model.User{}).Count(&count)
if count != 1 {
t.Errorf("expected 1 user, got %d", count)
}
}

174
database/migrate.go Normal file
View file

@ -0,0 +1,174 @@
package database
import (
"fmt"
"log"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/xray"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// allModels returns the list of all model structs for migration.
func allModels() []any {
return []any{
&model.User{},
&model.Inbound{},
&model.OutboundTraffics{},
&model.Setting{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
&model.HistoryOfSeeders{},
}
}
// tableNames returns the GORM table names for all models, in dependency order.
func tableNames() []string {
return []string{
"users",
"inbounds",
"outbound_traffics",
"settings",
"inbound_client_ips",
"client_traffics",
"history_of_seeders",
}
}
// openSQLite opens a SQLite connection for migration.
func openSQLite(dbPath string) (*gorm.DB, error) {
return gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
}
// openMariaDB opens a MariaDB connection for migration.
func openMariaDB() (*gorm.DB, error) {
dbConfig := config.GetDBConfigFromJSON()
dsn := buildMariaDBDSN(dbConfig)
return gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Discard})
}
// closeDB safely closes the underlying SQL connection.
func closeDB(gdb *gorm.DB) {
if gdb == nil {
return
}
sqlDB, err := gdb.DB()
if err != nil {
return
}
sqlDB.Close()
}
// migrateTable copies all rows from src table to dst table using the given model slice.
// It returns the number of rows migrated.
func migrateTable[T any](src, dst *gorm.DB, tableName string) (int64, error) {
var rows []T
if err := src.Table(tableName).Find(&rows).Error; err != nil {
return 0, fmt.Errorf("reading from %s: %w", tableName, err)
}
if len(rows) == 0 {
return 0, nil
}
if err := dst.Table(tableName).CreateInBatches(&rows, 100).Error; err != nil {
return 0, fmt.Errorf("writing to %s: %w", tableName, err)
}
return int64(len(rows)), nil
}
// migrateAllTables copies all data between two database connections within a transaction.
func migrateAllTables(src, dst *gorm.DB) error {
// Truncate destination tables and migrate within a transaction
return dst.Transaction(func(tx *gorm.DB) error {
// Clear destination tables in reverse dependency order
for i := len(tableNames()) - 1; i >= 0; i-- {
name := tableNames()[i]
if err := tx.Exec("DELETE FROM " + name).Error; err != nil {
return fmt.Errorf("failed to clear %s: %w", name, err)
}
}
total := int64(0)
for _, name := range tableNames() {
var count int64
var err error
switch name {
case "users":
count, err = migrateTable[model.User](src, tx, name)
case "inbounds":
count, err = migrateTable[model.Inbound](src, tx, name)
case "outbound_traffics":
count, err = migrateTable[model.OutboundTraffics](src, tx, name)
case "settings":
count, err = migrateTable[model.Setting](src, tx, name)
case "inbound_client_ips":
count, err = migrateTable[model.InboundClientIps](src, tx, name)
case "client_traffics":
count, err = migrateTable[xray.ClientTraffic](src, tx, name)
case "history_of_seeders":
count, err = migrateTable[model.HistoryOfSeeders](src, tx, name)
}
if err != nil {
return fmt.Errorf("migration failed for %s: %w", name, err)
}
total += count
log.Printf("Migrated %d rows from %s", count, name)
}
log.Printf("Migration complete: %d total rows", total)
return nil
})
}
// MigrateSQLiteToMariaDB copies all data from the SQLite database to MariaDB.
// The SQLite file is kept as a backup.
func MigrateSQLiteToMariaDB() error {
srcDB, err := openSQLite(config.GetDBPath())
if err != nil {
return fmt.Errorf("failed to open SQLite source: %w", err)
}
defer closeDB(srcDB)
dstDB, err := openMariaDB()
if err != nil {
return fmt.Errorf("failed to open MariaDB destination: %w", err)
}
defer closeDB(dstDB)
// AutoMigrate all tables on destination
for _, m := range allModels() {
if err := dstDB.AutoMigrate(m); err != nil {
return fmt.Errorf("failed to migrate table on MariaDB: %w", err)
}
}
return migrateAllTables(srcDB, dstDB)
}
// MigrateMariaDBToSQLite copies all data from MariaDB to the SQLite database.
// A new SQLite file is created (or overwritten) at the configured path.
func MigrateMariaDBToSQLite() error {
srcDB, err := openMariaDB()
if err != nil {
return fmt.Errorf("failed to open MariaDB source: %w", err)
}
defer closeDB(srcDB)
dstDB, err := openSQLite(config.GetDBPath())
if err != nil {
return fmt.Errorf("failed to open SQLite destination: %w", err)
}
defer closeDB(dstDB)
// AutoMigrate all tables on destination
for _, m := range allModels() {
if err := dstDB.AutoMigrate(m); err != nil {
return fmt.Errorf("failed to migrate table on SQLite: %w", err)
}
}
return migrateAllTables(srcDB, dstDB)
}

181
database/migrate_test.go Normal file
View file

@ -0,0 +1,181 @@
package database
import (
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/xray"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// openTestSQLiteDB opens an in-memory or file-based SQLite database for testing.
func openTestSQLiteDB(t *testing.T, dbPath string) *gorm.DB {
t.Helper()
gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Discard})
if err != nil {
t.Fatalf("failed to open test SQLite DB: %v", err)
}
t.Cleanup(func() {
sqlDB, _ := gdb.DB()
if sqlDB != nil {
sqlDB.Close()
}
})
return gdb
}
// createTestTables runs AutoMigrate on the given DB for all models.
func createTestTables(t *testing.T, gdb *gorm.DB) {
t.Helper()
models := []any{
&model.User{},
&model.Inbound{},
&model.OutboundTraffics{},
&model.Setting{},
&model.InboundClientIps{},
&xray.ClientTraffic{},
&model.HistoryOfSeeders{},
}
for _, m := range models {
if err := gdb.AutoMigrate(m); err != nil {
t.Fatalf("AutoMigrate failed: %v", err)
}
}
}
func TestMigrateAllTables_EmoprySource(t *testing.T) {
srcDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "src.db"))
dstDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "dst.db"))
createTestTables(t, srcDB)
createTestTables(t, dstDB)
err := migrateAllTables(srcDB, dstDB)
if err != nil {
t.Fatalf("migrateAllTables on empty source should succeed: %v", err)
}
// Verify destination is still empty
for _, name := range tableNames() {
var count int64
dstDB.Table(name).Count(&count)
if count != 0 {
t.Errorf("table %s should be empty, got %d rows", name, count)
}
}
}
func TestMigrateAllTables_WithData(t *testing.T) {
srcDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "src.db"))
dstDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "dst.db"))
createTestTables(t, srcDB)
createTestTables(t, dstDB)
// Insert test data into source
srcDB.Create(&model.User{Username: "testuser", Password: "testpass", Role: "admin"})
srcDB.Create(&model.Setting{Key: "testkey", Value: "testvalue"})
err := migrateAllTables(srcDB, dstDB)
if err != nil {
t.Fatalf("migrateAllTables failed: %v", err)
}
// Verify data was copied
var userCount int64
dstDB.Table("users").Count(&userCount)
if userCount != 1 {
t.Errorf("expected 1 user in dst, got %d", userCount)
}
var settingCount int64
dstDB.Table("settings").Count(&settingCount)
if settingCount != 1 {
t.Errorf("expected 1 setting in dst, got %d", settingCount)
}
}
func TestMigrateAllTables_OverwritesExisting(t *testing.T) {
srcDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "src.db"))
dstDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "dst.db"))
createTestTables(t, srcDB)
createTestTables(t, dstDB)
// Insert existing data in destination that should be cleared
dstDB.Create(&model.User{Username: "olduser", Password: "oldpass", Role: "admin"})
dstDB.Create(&model.Setting{Key: "oldkey", Value: "oldvalue"})
// Insert new data in source
srcDB.Create(&model.User{Username: "newuser", Password: "newpass", Role: "admin"})
err := migrateAllTables(srcDB, dstDB)
if err != nil {
t.Fatalf("migrateAllTables failed: %v", err)
}
// Verify old data was replaced
var userCount int64
dstDB.Table("users").Count(&userCount)
if userCount != 1 {
t.Errorf("expected 1 user in dst after overwrite, got %d", userCount)
}
var user model.User
dstDB.Table("users").First(&user)
if user.Username != "newuser" {
t.Errorf("expected username 'newuser', got '%s'", user.Username)
}
// Settings should be empty since source has no settings
var settingCount int64
dstDB.Table("settings").Count(&settingCount)
if settingCount != 0 {
t.Errorf("expected 0 settings in dst after overwrite, got %d", settingCount)
}
}
func TestMigrateTable_Generic(t *testing.T) {
srcDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "src.db"))
dstDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "dst.db"))
createTestTables(t, srcDB)
createTestTables(t, dstDB)
// Insert test users
srcDB.Create(&model.User{Username: "user1", Password: "pass1", Role: "admin"})
srcDB.Create(&model.User{Username: "user2", Password: "pass2", Role: "admin"})
count, err := migrateTable[model.User](srcDB, dstDB, "users")
if err != nil {
t.Fatalf("migrateTable failed: %v", err)
}
if count != 2 {
t.Errorf("expected 2 rows migrated, got %d", count)
}
var dstCount int64
dstDB.Table("users").Count(&dstCount)
if dstCount != 2 {
t.Errorf("expected 2 users in dst, got %d", dstCount)
}
}
func TestMigrateTable_EmptyTable(t *testing.T) {
srcDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "src.db"))
dstDB := openTestSQLiteDB(t, filepath.Join(t.TempDir(), "dst.db"))
createTestTables(t, srcDB)
createTestTables(t, dstDB)
count, err := migrateTable[model.User](srcDB, dstDB, "users")
if err != nil {
t.Fatalf("migrateTable on empty table should succeed: %v", err)
}
if count != 0 {
t.Errorf("expected 0 rows migrated, got %d", count)
}
}

View file

@ -0,0 +1,61 @@
package model
import (
"testing"
)
func TestGenXrayInboundConfig_EmptyListen(t *testing.T) {
in := &Inbound{
Listen: "",
Port: 443,
Protocol: VLESS,
Settings: `{"clients":[]}`,
Tag: "test-inbound",
Sniffing: `{"enabled":true}`,
}
cfg := in.GenXrayInboundConfig()
if cfg == nil {
t.Fatal("GenXrayInboundConfig should not return nil")
}
// Empty listen should default to 0.0.0.0
expected := `"0.0.0.0"`
if string(cfg.Listen) != expected {
t.Errorf("Listen should default to %s, got %s", expected, string(cfg.Listen))
}
if cfg.Port != 443 {
t.Errorf("Port should be 443, got %d", cfg.Port)
}
if cfg.Protocol != "vless" {
t.Errorf("Protocol should be vless, got %q", cfg.Protocol)
}
if cfg.Tag != "test-inbound" {
t.Errorf("Tag should be test-inbound, got %q", cfg.Tag)
}
}
func TestGenXrayInboundConfig_CustomListen(t *testing.T) {
in := &Inbound{
Listen: "127.0.0.1",
Port: 8080,
Protocol: VMESS,
Tag: "custom",
}
cfg := in.GenXrayInboundConfig()
expected := `"127.0.0.1"`
if string(cfg.Listen) != expected {
t.Errorf("Listen should be %s, got %s", expected, string(cfg.Listen))
}
}
func TestGenXrayInboundConfig_EmptySettings(t *testing.T) {
in := &Inbound{
Port: 443,
Protocol: Trojan,
}
cfg := in.GenXrayInboundConfig()
if cfg == nil {
t.Fatal("GenXrayInboundConfig should not return nil")
}
// Empty string Settings produces a nil RawMessage since json_util.RawMessage("") may be nil
// Just verify no panic occurred
}

1109
docs/API-DB-MariaDB.md Normal file

File diff suppressed because it is too large Load diff

346
docs/API-DB.md Normal file
View file

@ -0,0 +1,346 @@
# 3x-ui 数据库相关接口
> 以下接口涉及数据库的读写操作(增删改查)或数据库文件的导入导出。
---
## 目录
- [1. 入站管理](#1-入站管理)
- [2. 客户端管理](#2-客户端管理)
- [3. 流量管理](#3-流量管理)
- [4. IP 记录管理](#4-ip-记录管理)
- [5. 面板配置](#5-面板配置)
- [6. 用户管理](#6-用户管理)
- [7. 数据库导入导出](#7-数据库导入导出)
---
## 1. 入站管理
### `GET /panel/api/inbounds/list`
查询数据库,获取当前用户的所有入站记录。
**响应 (`obj`)** `[]Inbound`
---
### `GET /panel/api/inbounds/get/:id`
根据 ID 从数据库查询单条入站记录。
**URL 参数:** `:id`int
**响应 (`obj`)** `Inbound` 对象。
---
### `POST /panel/api/inbounds/add`
向数据库写入一条新的入站记录。
**请求体:** `Inbound` 对象JSON 或表单)。
**响应 (`obj`)** 创建的 `Inbound` 对象。
---
### `POST /panel/api/inbounds/del/:id`
从数据库删除指定入站记录及其关联的客户端流量数据。
**URL 参数:** `:id`int
**响应 (`obj`)** 被删除的入站 IDint
---
### `POST /panel/api/inbounds/update/:id`
更新数据库中指定入站记录。
**URL 参数:** `:id`int
**请求体:** `Inbound` 对象JSON 或表单)。
**响应 (`obj`)** 更新后的 `Inbound` 对象。
---
### `POST /panel/api/inbounds/import`
通过 JSON 数据导入,向数据库写入一条新的入站记录。
**请求体**(表单):
| 字段 | 类型 | 必填 |
|---|---|---|
| `data` | string (JSON) | 是 |
`data` 字段为 JSON 序列化的 `Inbound` 对象。
**响应 (`obj`)** 创建的 `Inbound` 对象。
---
## 2. 客户端管理
### `GET /panel/api/inbounds/getClientTraffics/:email`
根据邮箱从数据库查询客户端流量记录。
**URL 参数:** `:email`string
**响应 (`obj`)** `[]ClientTraffic`
---
### `GET /panel/api/inbounds/getClientTrafficsById/:id`
根据客户端 ID 从数据库查询流量记录。
**URL 参数:** `:id`string
**响应 (`obj`)** `[]ClientTraffic`
---
### `POST /panel/api/inbounds/addClient`
向数据库写入新客户端记录,更新入站的 `Settings` 字段。
**请求体:** 包含新客户端信息的 `Inbound` 对象。
**响应:**
```json
{ "success": true, "msg": "Client added successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/:id/delClient/:clientId`
从数据库删除指定客户端记录,更新入站的 `Settings` 字段。
**URL 参数:** `:id`int、`:clientId`string
**响应:**
```json
{ "success": true, "msg": "Client deleted successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/updateClient/:clientId`
更新数据库中指定客户端的配置。
**URL 参数:** `:clientId`string
**请求体:** 包含更新后客户端设置的 `Inbound` 对象。
**响应:**
```json
{ "success": true, "msg": "Client configuration updated successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/:id/delClientByEmail/:email`
根据邮箱从数据库删除客户端记录。
**URL 参数:** `:id`int、`:email`string
**响应:**
```json
{ "success": true, "msg": "Client deleted successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/delDepletedClients/:id`
批量删除数据库中指定入站下所有流量耗尽的客户端记录。
**URL 参数:** `:id`int
**响应:**
```json
{ "success": true, "msg": "Depleted clients deleted successfully", "obj": null }
```
---
## 3. 流量管理
### `POST /panel/api/inbounds/:id/resetClientTraffic/:email`
将数据库中指定客户端的上行、下行流量重置为 0。
**URL 参数:** `:id`int、`:email`string
**响应:**
```json
{ "success": true, "msg": "Client traffic reset successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/resetAllTraffics`
将数据库中所有入站的上行、下行流量重置为 0。
**响应:**
```json
{ "success": true, "msg": "All traffic reset successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/resetAllClientTraffics/:id`
将数据库中指定入站下所有客户端的上行、下行流量重置为 0。
**URL 参数:** `:id`int
**响应:**
```json
{ "success": true, "msg": "All client traffic reset successfully", "obj": null }
```
---
### `POST /panel/api/inbounds/updateClientTraffic/:email`
手动修改数据库中指定客户端的流量数值。
**URL 参数:** `:email`string
**请求体**JSON
```json
{
"upload": 0,
"download": 0
}
```
**响应:**
```json
{ "success": true, "msg": "Client configuration updated successfully", "obj": null }
```
---
## 4. IP 记录管理
### `POST /panel/api/inbounds/clientIps/:email`
从数据库查询客户端关联的 IP 地址记录。
**URL 参数:** `:email`string
**响应 (`obj`)**
- `[]string`,格式为 `"IP (YYYY-MM-DD HH:MM:SS)"`(含时间戳时)
- `[]string`,纯 IP 字符串(旧格式)
- `"No IP Record"`(无数据时)
---
### `POST /panel/api/inbounds/clearClientIps/:email`
清除数据库中指定客户端的 IP 记录。
**URL 参数:** `:email`string
**响应:**
```json
{ "success": true, "msg": "Log cleanup successful", "obj": null }
```
---
## 5. 面板配置
### `POST /panel/setting/all`
从数据库查询所有面板配置项。
**响应 (`obj`)** `AllSetting` 对象。
---
### `POST /panel/setting/update`
将配置写入数据库(批量更新面板设置)。
**请求体:** `AllSetting` 对象JSON 或表单)。
**响应:**
```json
{ "success": true, "msg": "Settings modified successfully", "obj": null }
```
---
## 6. 用户管理
### `POST /panel/setting/updateUser`
修改数据库中的管理员用户名和密码。
**请求体**JSON 或表单):
```json
{
"oldUsername": "string",
"oldPassword": "string",
"newUsername": "string",
"newPassword": "string"
}
```
**成功响应:**
```json
{ "success": true, "msg": "User modified successfully", "obj": null }
```
**错误响应:**
- `msg: "User modification failed: original username/password incorrect"`
- `msg: "User modification failed: username and password cannot be empty"`
---
## 7. 数据库导入导出
### `GET /panel/api/server/getDb`
导出整个 SQLite 数据库文件(`x-ui.db`)。
**响应:** 二进制文件下载(`application/octet-stream`,文件名 `x-ui.db`)。不使用 `Msg` 包装格式。
---
### `POST /panel/api/server/importDB`
导入数据库备份文件,覆盖当前数据库。导入后自动重启 Xray 服务。
**请求体:** multipart 文件上传(字段名 `db`)。
**响应 (`obj`)** `"Database imported successfully"`

1059
docs/API.md Normal file

File diff suppressed because it is too large Load diff

485
docs/install-logic.md Normal file
View file

@ -0,0 +1,485 @@
# install.sh 逻辑文档
## 概述
`install.sh` 是 3x-ui 面板的安装脚本,负责在 Linux 服务器上完成以下工作:
1. 安装系统依赖包
2. 下载并解压 3x-ui 发行版
3. 配置 systemd / OpenRC 服务
4. 生成随机凭据用户名、密码、端口、Web 路径)
5. 配置 SSL 证书Let's Encrypt 域名证书、IP 证书、或自定义证书)
6. 显示安装结果和访问信息
---
## 全局配置
### 颜色变量
| 变量 | 值 | 用途 |
|---------|----------------|------------|
| `red` | `\033[0;31m` | 红色文本 |
| `green` | `\033[0;32m` | 绿色文本 |
| `blue` | `\033[0;34m` | 蓝色文本 |
| `yellow`| `\033[0;33m` | 黄色文本 |
| `plain` | `\033[0m` | 重置颜色 |
### 路径变量
| 变量 | 默认值 | 说明 |
|-----------------|-------------------------|--------------------------|
| `xui_folder` | `/usr/local/x-ui` | x-ui 安装目录 |
| `xui_service` | `/etc/systemd/system` | systemd 服务文件目录 |
可通过环境变量 `XUI_MAIN_FOLDER``XUI_SERVICE` 覆盖。
---
## 入口流程
```
install.sh 被执行
├─ 检查 root 权限
├─ 检测操作系统发行版
├─ 检测 CPU 架构
├─ install_base() ← 安装系统依赖
└─ install_x-ui($1) ← 主安装逻辑($1 为可选的版本号)
```
---
## 函数详解
### 1. root 权限检查(第 14-15 行)
检查 `$EUID` 是否为 0。非 root 用户直接退出并提示使用 root 权限。
### 2. 操作系统检测(第 17-28 行)
读取 `/etc/os-release``/usr/lib/os-release`,将 `$ID` 赋值给 `release` 变量。
支持的发行版:
| 包管理器 | 发行版 |
|----------|--------|
| `apt` | ubuntu, debian, armbian |
| `dnf` | fedora, amzn, virtuozzo, rhel, almalinux, rocky, ol |
| `yum` | centos 7 |
| `pacman` | arch, manjaro, parch |
| `zypper` | opensuse-tumbleweed, opensuse-leap |
| `apk` | alpine |
### 3. `arch()` — CPU 架构检测(第 30-41 行)
通过 `uname -m` 映射到标准架构标识:
| `uname -m` 输出 | 返回值 |
|------------------------|----------|
| x86_64, x64, amd64 | `amd64` |
| i*86, x86 | `386` |
| armv8*, arm64, aarch64 | `arm64` |
| armv7*, arm | `armv7` |
| armv6* | `armv6` |
| armv5* | `armv5` |
| s390x | `s390x` |
| 其他 | 退出报错 |
### 4. IP/域名验证函数(第 46-57 行)
| 函数 | 逻辑 |
|---------------|---------------------------------------------------|
| `is_ipv4()` | 正则匹配 `数字.数字.数字.数字` 格式 |
| `is_ipv6()` | 检查字符串是否包含 `:` |
| `is_ip()` | 调用 `is_ipv4``is_ipv6` |
| `is_domain()` | 正则匹配标准域名格式(含国际化域名 `xn--` 支持) |
### 5. `is_port_in_use()` — 端口占用检测(第 60-74 行)
按优先级尝试三种方式:
1. `ss -ltn` — 检查监听端口
2. `netstat -lnt` — 回退方案
3. `lsof -nP -iTCP:端口 -sTCP:LISTEN` — 最后手段
任一命中即返回 0端口被占用
### 6. `install_base()` — 安装基础依赖(第 76-104 行)
根据 `$release` 使用对应的包管理器安装以下公共依赖:
```
curl, tar, tzdata, socat, ca-certificates, openssl
```
额外安装 `cron`(用于 acme.sh 自动续期,仅 apt 系列)。
- CentOS 7 使用 `yum`,其他版本使用 `dnf`
- 未识别的发行版默认回退到 `apt-get`
### 7. `gen_random_string(length)` — 随机字符串生成(第 106-111 行)
```
openssl rand -base64(length*2) → 过滤 a-zA-Z0-9 → 截取前 length 个字符
```
用于生成用户名、密码、Web 路径等随机值。
### 8. `install_acme()` — 安装 acme.sh第 113-124 行)
```bash
curl -s https://get.acme.sh | sh
```
安装到 `~/.acme.sh/` 目录。失败返回 1。
---
## SSL 证书管理
### 9. `setup_ssl_certificate(domain, server_ip, port, webBasePath)` — 域名 SSL第 126-191 行)
**用途**:为域名签发 Let's Encrypt 证书。
**流程**
```
检查 acme.sh 是否已安装
├─ 未安装 → 调用 install_acme()
└─ 已安装 → 继续
创建证书目录:/root/cert/${domain}/
签发证书:
acme.sh --set-default-ca --server letsencrypt
acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80
↳ 失败 → 清理并返回 1
安装证书:
acme.sh --installcert
--key-file /root/cert/${domain}/privkey.pem
--fullchain-file /root/cert/${domain}/fullchain.pem
--reloadcmd "systemctl restart x-ui"
启用自动续期acme.sh --upgrade --auto-upgrade
设置文件权限:
privkey.pem → 600仅所有者可读
fullchain.pem → 644
配置面板证书路径:
x-ui cert -webCert fullchain.pem -webCertKey privkey.pem
```
**前提条件**80 端口必须可从外网访问。
### 10. `setup_ip_certificate(ipv4, ipv6)` — IP 证书(第 195-343 行)
**用途**:为 IP 地址签发 Let's Encrypt 短期证书(约 6 天有效期)。
**流程**
```
检查 acme.sh
验证 IPv4 地址格式
创建证书目录:/root/cert/ip/
选择 HTTP-01 监听端口:
└─ 默认 80用户可自定义
└─ 循环检测端口占用,被占用则提示换端口
签发证书:
acme.sh --issue
-d ${ipv4} [-d ${ipv6}]
--standalone
--server letsencrypt
--certificate-profile shortlived
--days 6
--httpport ${WebPort}
安装证书:
acme.sh --installcert
--key-file /root/cert/ip/privkey.pem
--fullchain-file /root/cert/ip/fullchain.pem
--reloadcmd "systemctl restart x-ui || rc-service x-ui restart"
↳ 通过检查文件是否存在(而非退出码)判断成功
启用自动续期
设置文件权限
配置面板证书路径
```
**关键特性**
- 使用 `--certificate-profile shortlived` 配置文件,证书有效期约 6 天
- acme.sh cron 任务会在到期前自动续期
- 不依赖退出码判断安装成功(因为 reloadcmd 失败会导致非零退出)
- 支持 IPv4 + IPv6 双栈
### 11. `ssl_cert_issue()` — 手动 SSL 证书签发(第 346-509 行)
**用途**:交互式域名证书签发,提供更多自定义选项。
**流程**
```
读取当前面板的 webBasePath 和 port
检查 acme.sh不存在则安装
获取并验证用户输入的域名:
└─ 循环直到输入有效域名
└─ 检查是否已存在该域名的证书
创建证书目录:/root/cert/${domain}/
选择端口(默认 80
临时停止面板(释放端口)
签发证书:
acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort}
设置 reloadcmd证书续期后执行的命令
├─ 默认systemctl restart x-ui || rc-service x-ui restart
├─ 选项 1systemctl reload nginx ; systemctl restart x-ui
├─ 选项 2自定义命令
└─ 选项 0保持默认
安装证书并启用自动续期
启动面板
询问是否将证书应用到面板:
└─ 是 → x-ui cert -webCert ... -webCertKey ...
└─ 否 → 跳过
```
**特点**
- 签发前会停止面板以释放端口
- 支持自定义 reloadcmd例如先 reload nginx 再重启 x-ui
- 签发失败会自动重新启动面板
### 12. `prompt_and_setup_ssl(panel_port, web_base_path, server_ip)` — SSL 选择菜单(第 513-638 行)
**用途**:安装时的统一 SSL 配置入口,提供三种选择。
**菜单**
```
1. Let's Encrypt 域名证书90 天有效期,自动续期)
└─ 调用 ssl_cert_issue()
└─ 从 acme.sh 列表提取域名作为 SSL_HOST
2. Let's Encrypt IP 证书6 天有效期,自动续期) ← 默认选项
└─ 可选输入 IPv6 地址
└─ 停止面板释放 80 端口
└─ 调用 setup_ip_certificate(server_ip, ipv6)
└─ SSL_HOST = server_ip
3. 自定义 SSL 证书(指定已有文件路径)
└─ 输入域名
└─ 循环验证证书文件(存在、可读、非空)
└─ 循环验证私钥文件(存在、可读、非空)
└─ x-ui cert -webCert ... -webCertKey ...
└─ 提示用户自行管理续期
```
**全局变量**:设置 `SSL_HOST` 供后续显示访问地址使用。
---
## 安装后配置
### 13. `config_after_install()` — 安装后配置(第 640-760 行)
**用途**首次安装后的凭据生成、端口设置、Web 路径生成、SSL 配置。
**流程图**
```
读取当前面板设置:
- hasDefaultCredential是否为默认凭据
- webBasePath
- port
- cert证书路径
获取服务器公网 IP
└─ 依次尝试 6 个 API
1. api4.ipify.org
2. ipv4.icanhazip.com
3. v4.api.ipinfo.io/ip
4. ipv4.myexternalip.com/raw
5. 4.ident.me
6. check-host.net/ip
判断 webBasePath 是否足够长≥4 字符):
┌─ webBasePath 过短
│ ├─ hasDefaultCredential == true首次安装
│ │ ├─ 生成随机 webBasePath18 位)
│ │ ├─ 生成随机用户名10 位)
│ │ ├─ 生成随机密码10 位)
│ │ ├─ 询问是否自定义端口
│ │ │ ├─ 是 → 用户输入端口
│ │ │ └─ 否 → 随机生成 1024-62000 范围端口
│ │ ├─ 应用设置x-ui setting -username ... -password ... -port ... -webBasePath ...
│ │ ├─ prompt_and_setup_ssl() ← 必需
│ │ └─ 显示完整凭据和访问地址
│ │
│ └─ hasDefaultCredential != true非首次安装
│ ├─ 生成新 webBasePath
│ ├─ 检查是否有证书:
│ │ ├─ 无 → prompt_and_setup_ssl()(推荐)
│ │ └─ 有 → 显示 HTTP 访问地址
│ └─ 结束
└─ webBasePath 正常≥4 字符)
├─ hasDefaultCredential == true
│ ├─ 生成随机用户名和密码
│ ├─ 应用新凭据
│ └─ 显示凭据
└─ hasDefaultCredential != true
└─ 提示凭据已正确设置
再次检查证书:
├─ 无证书 → prompt_and_setup_ssl()(推荐)
└─ 有证书 → 跳过
最后执行x-ui migrate数据库迁移
```
---
## 主安装逻辑
### 14. `install_x-ui(version)` — 主安装函数(第 762-958 行)
**参数**`$1` 可选,指定安装版本号(如 `v2.3.5`)。
**流程**
```
cd /usr/local/
┌─ 无版本参数(安装最新版)
│ ├─ 从 GitHub API 获取最新版本号
│ │ └─ IPv4 失败时重试 curl -4
│ └─ 下载x-ui-linux-${arch}.tar.gz
└─ 有版本参数
├─ 验证版本号 ≥ v2.3.5
└─ 下载指定版本
同时下载 x-ui.sh 到 /usr/bin/x-ui-temp
停止已有 x-ui 服务并删除旧安装目录
解压 tar.gz设置执行权限
ARM 架构特殊处理:
armv5/armv6/armv7 → 重命名为 xray-linux-arm
安装 x-ui.sh 到 /usr/bin/x-ui
创建日志目录 /var/log/x-ui/
调用 config_after_install() ← 生成凭据 + SSL
etckeeper 兼容:
└─ 如果 /etc/.git 存在,将 x-ui.db 加入 .gitignore
┌─ Alpine Linux
│ ├─ 下载 OpenRC 脚本 x-ui.rc → /etc/init.d/x-ui
│ ├─ rc-update add x-ui启用开机自启
│ └─ rc-service x-ui start
└─ 其他系统systemd
├─ 优先使用 tar.gz 中的服务文件
│ ├─ x-ui.service ← 通用
│ ├─ x-ui.service.debian ← Ubuntu/Debian
│ ├─ x-ui.service.arch ← Arch/Manjaro
│ └─ x-ui.service.rhel ← 其他CentOS/Fedora 等)
├─ 如果 tar.gz 中没有,从 GitHub 下载对应文件
└─ 配置服务:
chown root:root x-ui.service
chmod 644 x-ui.service
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
显示安装完成信息和子命令用法
```
**子命令列表**(安装完成后显示):
| 命令 | 功能 |
|-------------------|--------------------|
| `x-ui` | 打开管理菜单 |
| `x-ui start` | 启动面板 |
| `x-ui stop` | 停止面板 |
| `x-ui restart` | 重启面板 |
| `x-ui status` | 查看状态 |
| `x-ui settings` | 查看当前设置 |
| `x-ui enable` | 设置开机自启 |
| `x-ui disable` | 取消开机自启 |
| `x-ui log` | 查看日志 |
| `x-ui banlog` | 查看 Fail2ban 日志 |
| `x-ui update` | 更新 |
| `x-ui legacy` | 安装旧版本 |
| `x-ui install` | 安装 |
| `x-ui uninstall` | 卸载 |
---
## 调用关系总结
```
install.sh
├─ install_base()
│ └─ 根据发行版安装 curl, tar, tzdata, socat, ca-certificates, openssl
└─ install_x-ui($1)
├─ 下载 x-ui 发行版和 x-ui.sh
├─ 解压、设置权限
├─ config_after_install()
│ ├─ gen_random_string() × 3用户名/密码/Web路径
│ ├─ 获取公网 IP
│ ├─ prompt_and_setup_ssl()
│ │ ├─ [选项1] ssl_cert_issue()
│ │ │ ├─ install_acme()
│ │ │ └─ acme.sh 签发/安装/续期域名证书
│ │ ├─ [选项2] setup_ip_certificate()
│ │ │ ├─ install_acme()
│ │ │ └─ acme.sh 签发/安装/续期 IP 短期证书
│ │ └─ [选项3] 用户提供自定义证书路径
│ └─ x-ui migrate
└─ 配置系统服务systemd 或 OpenRC
```
---
## 关键设计决策
1. **强制 SSL**:首次安装时必须配置 SSL 证书(三种方式选一),确保面板通过 HTTPS 访问。
2. **随机化安全**用户名、密码、端口、Web 路径全部随机生成,避免使用默认凭据。
3. **多 OS 兼容**:通过 `case` 语句适配 7 大包管理器体系Alpine 使用 OpenRC其余使用 systemd。
4. **IP 证书支持**:利用 Let's Encrypt 的 shortlived profile为无域名场景提供 SSL 支持6 天有效期,自动续期)。
5. **优雅降级**
- GitHub API 失败时用 `curl -4` 重试
- `ss` 不可用时回退到 `netstat`,再回退到 `lsof`
- tar.gz 中无服务文件时从 GitHub 下载
- acme.sh reloadcmd 失败不阻止证书安装
6. **etckeeper 兼容**:自动将数据库文件加入 `/etc/.gitignore`,避免 etckeeper 追踪频繁变化的数据库。

View file

@ -0,0 +1,927 @@
# Panel Settings JSON Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- []`) syntax for tracking.
**Goal:** Extract panel settings from the SQLite `settings` table into a standalone `x-ui.json` file, keeping `xrayTemplateConfig` in the database.
**Architecture:** Replace the database-backed `getSetting`/`saveSetting` in `SettingService` with JSON file read/write. All public `Get*`/`Set*` methods keep their signatures unchanged so controllers, CLI, and sub package need zero changes. `xrayTemplateConfig` gets dedicated DB helper methods to bypass the JSON path.
**Tech Stack:** Go, GORM/SQLite (retained for xrayTemplateConfig only), `encoding/json`, `os`
---
## File Map
| File | Action | Purpose |
|------|--------|---------|
| `config/config.go` | Modify | Add `GetSettingPath()` |
| `web/service/setting.go` | Modify | Replace DB-backed internals with JSON file I/O |
| `web/service/xray_setting.go` | Modify | Use direct DB helpers for xrayTemplateConfig |
| `web/service/setting_test.go` | Create | Unit tests for JSON settings |
No changes needed: `main.go`, `database/db.go`, `database/model/model.go`, `web/entity/entity.go`, any controller, `sub/`, `xray/`.
---
### Task 1: Add `GetSettingPath()` to `config/config.go`
**Files:**
- Modify: `config/config.go:100`
- [ ] **Step 1: Add `GetSettingPath()` function**
Add after the existing `GetDBPath()` function at line 101:
```go
// GetSettingPath returns the full path to the panel settings JSON file.
func GetSettingPath() string {
return fmt.Sprintf("%s/%s.json", GetDBFolderPath(), GetName())
}
```
- [ ] **Step 2: Verify it compiles**
Run: `cd /usr/x-ui/3x-ui && go build ./config/`
Expected: no errors
- [ ] **Step 3: Commit**
```bash
git add config/config.go
git commit -m "feat(config): add GetSettingPath for JSON settings file"
```
---
### Task 2: Add JSON file I/O helpers to `web/service/setting.go`
**Files:**
- Modify: `web/service/setting.go`
- [ ] **Step 1: Add imports**
Add `"os"` and `"github.com/mhsanaei/3x-ui/v2/config"` to the import block. The existing imports `"github.com/mhsanaei/3x-ui/v2/database"` and `"github.com/mhsanaei/3x-ui/v2/database/model"` will be kept for now (removed later when `getSetting`/`saveSetting` are replaced and `GetAllSetting` no longer queries DB).
The import block becomes:
```go
import (
_ "embed"
"encoding/json"
"errors"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/common"
"github.com/mhsanaei/3x-ui/v2/util/random"
"github.com/mhsanaei/3x-ui/v2/util/reflect_util"
"github.com/mhsanaei/3x-ui/v2/web/entity"
"github.com/mhsanaei/3x-ui/v2/xray"
)
```
- [ ] **Step 2: Add `loadSettings()` and `saveSettings()` functions**
Add these package-level functions before the `SettingService` struct (after `defaultValueMap`, around line 106):
```go
// loadSettings reads the JSON settings file into a map.
// If the file doesn't exist, it creates one from defaultValueMap (excluding xrayTemplateConfig).
func loadSettings() (map[string]string, error) {
path := config.GetSettingPath()
data, err := os.ReadFile(path)
if os.IsNotExist(err) {
settings := make(map[string]string)
for k, v := range defaultValueMap {
if k == "xrayTemplateConfig" {
continue
}
settings[k] = v
}
return settings, saveSettings(settings)
}
if err != nil {
return nil, err
}
var settings map[string]string
if err := json.Unmarshal(data, &settings); err != nil {
return nil, fmt.Errorf("failed to parse settings file %s: %w", path, err)
}
return settings, nil
}
// saveSettings writes the settings map to the JSON file.
func saveSettings(settings map[string]string) error {
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
return os.WriteFile(config.GetSettingPath(), data, 0644)
}
```
- [ ] **Step 3: Verify it compiles**
Run: `cd /usr/x-ui/3x-ui && go build ./web/service/`
Expected: no errors (existing code still compiles with old + new functions coexisting)
- [ ] **Step 4: Commit**
```bash
git add web/service/setting.go
git commit -m "feat(service): add JSON file I/O helpers for settings"
```
---
### Task 3: Replace `getSetting`/`saveSetting` with JSON-based implementations
**Files:**
- Modify: `web/service/setting.go:205-229`
- [ ] **Step 1: Replace `getSetting`**
Replace lines 205-213:
```go
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
db := database.GetDB()
setting := &model.Setting{}
err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error
if err != nil {
return nil, err
}
return setting, nil
}
```
With:
```go
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
settings, err := loadSettings()
if err != nil {
return nil, err
}
value, ok := settings[key]
if !ok {
return nil, fmt.Errorf("setting key %q not found", key)
}
return &model.Setting{Key: key, Value: value}, nil
}
```
- [ ] **Step 2: Replace `saveSetting`**
Replace lines 215-229:
```go
func (s *SettingService) saveSetting(key string, value string) error {
setting, err := s.getSetting(key)
db := database.GetDB()
if database.IsNotFound(err) {
return db.Create(&model.Setting{
Key: key,
Value: value,
}).Error
} else if err != nil {
return err
}
setting.Key = key
setting.Value = value
return db.Save(setting).Error
}
```
With:
```go
func (s *SettingService) saveSetting(key string, value string) error {
settings, err := loadSettings()
if err != nil {
return err
}
settings[key] = value
return saveSettings(settings)
}
```
- [ ] **Step 3: Replace `getString` to use JSON directly**
Replace lines 231-243:
```go
func (s *SettingService) getString(key string) (string, error) {
setting, err := s.getSetting(key)
if database.IsNotFound(err) {
value, ok := defaultValueMap[key]
if !ok {
return "", common.NewErrorf("key <%v> not in defaultValueMap", key)
}
return value, nil
} else if err != nil {
return "", err
}
return setting.Value, nil
}
```
With:
```go
func (s *SettingService) getString(key string) (string, error) {
settings, err := loadSettings()
if err != nil {
return "", err
}
value, ok := settings[key]
if !ok {
defaultValue, hasDefault := defaultValueMap[key]
if !hasDefault {
return "", common.NewErrorf("key <%v> not in defaultValueMap", key)
}
return defaultValue, nil
}
return value, nil
}
```
- [ ] **Step 4: Replace `ResetSettings`**
Replace lines 195-203:
```go
func (s *SettingService) ResetSettings() error {
db := database.GetDB()
err := db.Where("1 = 1").Delete(model.Setting{}).Error
if err != nil {
return err
}
return db.Model(model.User{}).
Where("1 = 1").Error
}
```
With:
```go
func (s *SettingService) ResetSettings() error {
// Delete the JSON settings file
err := os.Remove(config.GetSettingPath())
if err != nil && !os.IsNotExist(err) {
return err
}
// Clear users table
db := database.GetDB()
return db.Where("1 = 1").Delete(model.User{}).Error
}
```
- [ ] **Step 5: Verify it compiles**
Run: `cd /usr/x-ui/3x-ui && go build ./web/service/`
Expected: no errors
- [ ] **Step 6: Commit**
```bash
git add web/service/setting.go
git commit -m "feat(service): replace DB-backed settings with JSON file operations"
```
---
### Task 4: Update `GetAllSetting` and `UpdateAllSetting` to use JSON
**Files:**
- Modify: `web/service/setting.go:120-193, 691-710`
- [ ] **Step 1: Replace `GetAllSetting`**
Replace lines 120-193:
```go
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
db := database.GetDB()
settings := make([]*model.Setting, 0)
err := db.Model(model.Setting{}).Not("key = ?", "xrayTemplateConfig").Find(&settings).Error
if err != nil {
return nil, err
}
allSetting := &entity.AllSetting{}
t := reflect.TypeFor[entity.AllSetting]()
v := reflect.ValueOf(allSetting).Elem()
fields := reflect_util.GetFields(t)
setSetting := func(key, value string) (err error) {
defer func() {
panicErr := recover()
if panicErr != nil {
err = errors.New(fmt.Sprint(panicErr))
}
}()
var found bool
var field reflect.StructField
for _, f := range fields {
if f.Tag.Get("json") == key {
field = f
found = true
break
}
}
if !found {
// Some settings are automatically generated, no need to return to the front end to modify the user
return nil
}
fieldV := v.FieldByName(field.Name)
switch t := fieldV.Interface().(type) {
case int:
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
fieldV.SetInt(n)
case string:
fieldV.SetString(value)
case bool:
fieldV.SetBool(value == "true")
default:
return common.NewErrorf("unknown field %v type %v", key, t)
}
return
}
keyMap := map[string]bool{}
for _, setting := range settings {
err := setSetting(setting.Key, setting.Value)
if err != nil {
return nil, err
}
keyMap[setting.Key] = true
}
for key, value := range defaultValueMap {
if keyMap[key] {
continue
}
err := setSetting(key, value)
if err != nil {
return nil, err
}
}
return allSetting, nil
}
```
With:
```go
func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
settings, err := loadSettings()
if err != nil {
return nil, err
}
allSetting := &entity.AllSetting{}
t := reflect.TypeFor[entity.AllSetting]()
v := reflect.ValueOf(allSetting).Elem()
fields := reflect_util.GetFields(t)
setSetting := func(key, value string) (err error) {
defer func() {
panicErr := recover()
if panicErr != nil {
err = errors.New(fmt.Sprint(panicErr))
}
}()
var found bool
var field reflect.StructField
for _, f := range fields {
if f.Tag.Get("json") == key {
field = f
found = true
break
}
}
if !found {
return nil
}
fieldV := v.FieldByName(field.Name)
switch t := fieldV.Interface().(type) {
case int:
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
fieldV.SetInt(n)
case string:
fieldV.SetString(value)
case bool:
fieldV.SetBool(value == "true")
default:
return common.NewErrorf("unknown field %v type %v", key, t)
}
return
}
keyMap := map[string]bool{}
for key, value := range settings {
err := setSetting(key, value)
if err != nil {
return nil, err
}
keyMap[key] = true
}
for key, value := range defaultValueMap {
if key == "xrayTemplateConfig" {
continue
}
if keyMap[key] {
continue
}
err := setSetting(key, value)
if err != nil {
return nil, err
}
}
return allSetting, nil
}
```
- [ ] **Step 2: Replace `UpdateAllSetting`**
Replace lines 691-710:
```go
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err
}
v := reflect.ValueOf(allSetting).Elem()
t := reflect.TypeFor[entity.AllSetting]()
fields := reflect_util.GetFields(t)
errs := make([]error, 0)
for _, field := range fields {
key := field.Tag.Get("json")
fieldV := v.FieldByName(field.Name)
value := fmt.Sprint(fieldV.Interface())
err := s.saveSetting(key, value)
if err != nil {
errs = append(errs, err)
}
}
return common.Combine(errs...)
}
```
With:
```go
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err
}
settings, err := loadSettings()
if err != nil {
return err
}
v := reflect.ValueOf(allSetting).Elem()
t := reflect.TypeFor[entity.AllSetting]()
fields := reflect_util.GetFields(t)
for _, field := range fields {
key := field.Tag.Get("json")
fieldV := v.FieldByName(field.Name)
settings[key] = fmt.Sprint(fieldV.Interface())
}
return saveSettings(settings)
}
```
- [ ] **Step 3: Verify it compiles**
Run: `cd /usr/x-ui/3x-ui && go build ./web/service/`
Expected: no errors
- [ ] **Step 4: Commit**
```bash
git add web/service/setting.go
git commit -m "feat(service): migrate GetAllSetting/UpdateAllSetting to JSON"
```
---
### Task 5: Handle `xrayTemplateConfig` — dedicated DB accessors
**Files:**
- Modify: `web/service/setting.go:273-274`
- Modify: `web/service/xray_setting.go:17-21`
- [ ] **Step 1: Add dedicated DB accessor for xrayTemplateConfig**
Add a new private function in `setting.go` (after the `saveSettings` function):
```go
// getXrayTemplateConfigFromDB reads xrayTemplateConfig directly from the database.
func getXrayTemplateConfigFromDB() (string, error) {
db := database.GetDB()
setting := &model.Setting{}
err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(setting).Error
if err != nil {
return "", err
}
return setting.Value, nil
}
// saveXrayTemplateConfigToDB writes xrayTemplateConfig directly to the database.
func saveXrayTemplateConfigToDB(value string) error {
db := database.GetDB()
setting := &model.Setting{}
err := db.Model(model.Setting{}).Where("key = ?", "xrayTemplateConfig").First(setting).Error
if database.IsNotFound(err) {
return db.Create(&model.Setting{
Key: "xrayTemplateConfig",
Value: value,
}).Error
}
if err != nil {
return err
}
setting.Value = value
return db.Save(setting).Error
}
```
- [ ] **Step 2: Update `GetXrayConfigTemplate` to use DB directly**
Replace line 273-274:
```go
func (s *SettingService) GetXrayConfigTemplate() (string, error) {
return s.getString("xrayTemplateConfig")
}
```
With:
```go
func (s *SettingService) GetXrayConfigTemplate() (string, error) {
config, err := getXrayTemplateConfigFromDB()
if err != nil {
// If not in DB, return the embedded default
return xrayTemplateConfig, nil
}
return config, nil
}
```
- [ ] **Step 3: Update `XraySettingService.SaveXraySetting` to use DB directly**
Replace line 17-21 in `xray_setting.go`:
```go
func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
if err := s.CheckXrayConfig(newXraySettings); err != nil {
return err
}
return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings)
}
```
With:
```go
func (s *XraySettingService) SaveXraySetting(newXraySettings string) error {
if err := s.CheckXrayConfig(newXraySettings); err != nil {
return err
}
return saveXrayTemplateConfigToDB(newXraySettings)
}
```
- [ ] **Step 4: Verify it compiles**
Run: `cd /usr/x-ui/3x-ui && go build ./web/service/`
Expected: no errors
- [ ] **Step 5: Commit**
```bash
git add web/service/setting.go web/service/xray_setting.go
git commit -m "feat(service): use direct DB access for xrayTemplateConfig"
```
---
### Task 6: Clean up unused imports
**Files:**
- Modify: `web/service/setting.go`
- [ ] **Step 1: Remove `database` and `model` imports if no longer needed**
Check if `database` and `model` packages are still referenced in `setting.go` after all changes. `database` is still used by `ResetSettings()` (for `database.GetDB()` to clear users table). `model` is no longer needed in `setting.go` since `getSetting`/`saveSetting` no longer use `model.Setting`, and `ResetSettings` uses `model.User` which... actually check: `ResetSettings` references `model.User{}`.
So `database` and `model` are still needed in `setting.go` for:
- `ResetSettings()``database.GetDB()` + `model.User{}`
- `getXrayTemplateConfigFromDB()` / `saveXrayTemplateConfigToDB()``database` + `model.Setting{}`
No import cleanup needed. Skip this step.
- [ ] **Step 2: Verify full build**
Run: `cd /usr/x-ui/3x-ui && go build ./...`
Expected: no errors
- [ ] **Step 3: Commit (only if changes were made)**
```bash
git add web/service/setting.go
git commit -m "chore(service): clean up unused imports"
```
---
### Task 7: Write unit tests
**Files:**
- Create: `web/service/setting_test.go`
- [ ] **Step 1: Write tests for JSON settings**
```go
package service
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v2/config"
)
func setupTestSettings(t *testing.T) func() {
t.Helper()
tmpDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", tmpDir)
return func() {}
}
func TestLoadSettingsCreatesDefaults(t *testing.T) {
setupTestSettings(t)
settings, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings() error: %v", err)
}
// Should contain default values
if settings["webPort"] != "2053" {
t.Errorf("expected webPort=2053, got %s", settings["webPort"])
}
if settings["webBasePath"] != "/" {
t.Errorf("expected webBasePath=/, got %s", settings["webBasePath"])
}
// Should NOT contain xrayTemplateConfig
if _, exists := settings["xrayTemplateConfig"]; exists {
t.Error("xrayTemplateConfig should not be in JSON settings")
}
// File should exist on disk
path := config.GetSettingPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("settings file %s should have been created", path)
}
}
func TestSaveAndLoadSettings(t *testing.T) {
setupTestSettings(t)
settings := map[string]string{
"webPort": "8080",
"webListen": "0.0.0.0",
}
err := saveSettings(settings)
if err != nil {
t.Fatalf("saveSettings() error: %v", err)
}
loaded, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings() error: %v", err)
}
if loaded["webPort"] != "8080" {
t.Errorf("expected webPort=8080, got %s", loaded["webPort"])
}
if loaded["webListen"] != "0.0.0.0" {
t.Errorf("expected webListen=0.0.0.0, got %s", loaded["webListen"])
}
}
func TestSettingServiceGetString(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
// Should return default value when key not set
val, err := svc.getString("webPort")
if err != nil {
t.Fatalf("getString error: %v", err)
}
if val != "2053" {
t.Errorf("expected 2053, got %s", val)
}
}
func TestSettingServiceSetAndGetString(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
err := svc.setString("webPort", "9090")
if err != nil {
t.Fatalf("setString error: %v", err)
}
val, err := svc.getString("webPort")
if err != nil {
t.Fatalf("getString error: %v", err)
}
if val != "9090" {
t.Errorf("expected 9090, got %s", val)
}
}
func TestResetSettingsDeletesFile(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
// Create settings first
_, err := svc.getString("webPort")
if err != nil {
t.Fatalf("getString error: %v", err)
}
path := config.GetSettingPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Fatal("settings file should exist before reset")
}
// Note: ResetSettings also needs DB for users table.
// For this unit test, we just verify the JSON file deletion part works.
// Full integration test would need a test DB.
err = os.Remove(path)
if err != nil {
t.Fatalf("remove error: %v", err)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Error("settings file should not exist after reset")
}
// Re-loading should recreate defaults
settings, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings after reset error: %v", err)
}
if settings["webPort"] != "2053" {
t.Errorf("expected default webPort=2053 after reset, got %s", settings["webPort"])
}
}
func TestSettingsFileFormat(t *testing.T) {
setupTestSettings(t)
settings, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings error: %v", err)
}
path := config.GetSettingPath()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile error: %v", err)
}
// Verify it's valid JSON
var parsed map[string]string
if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("settings file is not valid JSON: %v", err)
}
// Verify pretty-printed (has newlines)
if !contains(data, '\n') {
t.Error("settings file should be pretty-printed with newlines")
}
// Verify key count matches
if len(parsed) != len(settings) {
t.Errorf("parsed key count %d != loaded key count %d", len(parsed), len(settings))
}
_ = filepath.Base(path) // just to use the import
}
func contains(data []byte, b byte) bool {
for _, d := range data {
if d == b {
return true
}
}
return false
}
```
- [ ] **Step 2: Run tests**
Run: `cd /usr/x-ui/3x-ui && go test ./web/service/ -run TestLoadSettings -v`
Expected: PASS
Run: `cd /usr/x-ui/3x-ui && go test ./web/service/ -run TestSaveAndLoad -v`
Expected: PASS
Run: `cd /usr/x-ui/3x-ui && go test ./web/service/ -run TestSettingService -v`
Expected: PASS
Run: `cd /usr/x-ui/3x-ui && go test ./web/service/ -run TestReset -v`
Expected: PASS
Run: `cd /usr/x-ui/3x-ui && go test ./web/service/ -run TestSettingsFile -v`
Expected: PASS
- [ ] **Step 3: Run all tests**
Run: `cd /usr/x-ui/3x-ui && go test ./web/service/ -v`
Expected: all PASS
- [ ] **Step 4: Commit**
```bash
git add web/service/setting_test.go
git commit -m "test(service): add unit tests for JSON settings"
```
---
### Task 8: Full build verification
- [ ] **Step 1: Build entire project**
Run: `cd /usr/x-ui/3x-ui && go build ./...`
Expected: no errors
- [ ] **Step 2: Run `go vet`**
Run: `cd /usr/x-ui/3x-ui && go vet ./...`
Expected: no issues
- [ ] **Step 3: Final commit (only if fixes needed)**
```bash
git add -A
git commit -m "chore: fix build issues from settings migration"
```
---
## Self-Review
**1. Spec coverage:**
- Panel settings in flat key-value JSON: Tasks 2-4
- xrayTemplateConfig stays in DB: Task 5
- All new installations (no migration): Task 2 Step 1 (auto-create from defaults)
- JSON file path: Task 1 (`GetSettingPath`)
- JSON auto-created on first run: Task 2 Step 1 (`loadSettings`)
- CLI compatibility: No changes to main.go, works via unchanged `SettingService` API
- Tests: Task 7
**2. Placeholder scan:** No TBD/TODO found. All code blocks contain complete implementations.
**3. Type consistency:**
- `getSetting` still returns `(*model.Setting, error)` — reused by `getString` which checks `database.IsNotFound(err)`. After the change, `getSetting` returns a custom error when key not found (not `gorm.ErrRecordNotFound`). Need to verify: `getString` checks `database.IsNotFound(err)` which tests for `gorm.ErrRecordNotFound`. The new `getSetting` returns `fmt.Errorf(...)` which is NOT a gorm error. This means `getString` would NOT fall through to the default — it would return the error instead.
**FIX:** `getString` must not rely on `database.IsNotFound`. The rewritten `getString` in Task 3 Step 3 already handles this correctly — it reads the map directly and checks `ok`, no longer calling `getSetting` or checking `database.IsNotFound`. Good.

View file

@ -0,0 +1,193 @@
# Pre-release Install/Update Selection Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Let users choose between the latest Stable or Pre-release when installing or updating 3x-ui.
**Architecture:** Replace the hardcoded `/releases/latest` API call with a `/releases` call that parses both stable and pre-release tags. Add an interactive prompt in both `install.sh` and `update.sh` so users pick which version to install. Functions are duplicated across files (matching existing conventions — no shared library).
**Tech Stack:** Bash, GitHub REST API, grep/sed/awk for JSON parsing (no jq).
---
### Task 1: Add `get_releases` helper + prompt to `install.sh`
**Files:**
- Modify: `install.sh:874-911`
- [ ] **Step 1: Add `get_releases` function before `install_x-ui()`**
Insert this function before `install_x-ui()` (around line 874). It fetches all releases and parses out the latest stable and pre-release tags:
```bash
get_releases() {
local releases_json
releases_json=$(curl -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases")
if [[ -z "$releases_json" ]]; then
echo -e "${yellow}正在尝试通过 IPv4 获取版本...${plain}"
releases_json=$(curl -4 -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases")
if [[ -z "$releases_json" ]]; then
echo -e "${red}获取 x-ui 版本失败,可能是 GitHub API 限制,请稍后重试${plain}"
exit 1
fi
fi
# Parse first non-prerelease tag_name
latest_stable=$(echo "$releases_json" | tr '{' '\n' | grep '"prerelease":false' | head -1 | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
# Parse first prerelease tag_name
latest_prerelease=$(echo "$releases_json" | tr '{' '\n' | grep '"prerelease":true' | head -1 | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ -z "$latest_stable" && -z "$latest_prerelease" ]]; then
echo -e "${red}获取 x-ui 版本失败${plain}"
exit 1
fi
}
select_version() {
if [[ -n "$latest_stable" && -n "$latest_prerelease" ]]; then
echo ""
echo -e "${green}请选择要安装的版本:${plain}"
echo -e "${green}1)${plain} 最新稳定版: ${latest_stable}"
echo -e "${green}2)${plain} 最新预发布版: ${latest_prerelease}"
read -rp "请输入选择 [1-2]: " version_choice
while [[ "$version_choice" != "1" && "$version_choice" != "2" ]]; do
read -rp "无效输入,请重新输入 [1-2]: " version_choice
done
if [[ "$version_choice" == "1" ]]; then
tag_version="$latest_stable"
else
tag_version="$latest_prerelease"
fi
elif [[ -n "$latest_stable" ]]; then
tag_version="$latest_stable"
else
tag_version="$latest_prerelease"
fi
}
```
- [ ] **Step 2: Replace the no-argument release fetch block in `install_x-ui()`**
Replace lines 879-888:
```bash
tag_version=$(curl -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}正在尝试通过 IPv4 获取版本...${plain}"
tag_version=$(curl -4 -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
echo -e "${red}获取 x-ui 版本失败,可能是 GitHub API 限制,请稍后重试${plain}"
exit 1
fi
fi
echo -e "获取到 x-ui 最新版本:${tag_version},开始安装..."
```
With:
```bash
get_releases
select_version
echo -e "获取到 x-ui 版本:${tag_version},开始安装..."
```
- [ ] **Step 3: Verify the script still works for the specific-version path**
Read the full `install_x-ui()` function and confirm the `else` branch (lines 894-911, where `$1` is provided) is untouched.
- [ ] **Step 4: Commit**
```bash
git add install.sh
git commit -m "feat(install): add pre-release version selection prompt"
```
### Task 2: Add `get_releases` helper + prompt to `update.sh`
**Files:**
- Modify: `update.sh:748-767`
- [ ] **Step 1: Add `get_releases` and `select_version` functions before `update_x-ui()`**
Insert the same two functions before `update_x-ui()` (around line 748). Identical logic to install.sh except the prompt text says "更新" (update) instead of "安装" (install):
```bash
get_releases() {
local releases_json
releases_json=$(${curl_bin} -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases" 2>/dev/null)
if [[ -z "$releases_json" ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
releases_json=$(${curl_bin} -4 -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases" 2>/dev/null)
if [[ -z "$releases_json" ]]; then
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
fi
fi
latest_stable=$(echo "$releases_json" | tr '{' '\n' | grep '"prerelease":false' | head -1 | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
latest_prerelease=$(echo "$releases_json" | tr '{' '\n' | grep '"prerelease":true' | head -1 | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ -z "$latest_stable" && -z "$latest_prerelease" ]]; then
_fail "ERROR: Failed to fetch x-ui version"
fi
}
select_version() {
if [[ -n "$latest_stable" && -n "$latest_prerelease" ]]; then
echo ""
echo -e "${green}Which version do you want to update to?${plain}"
echo -e "${green}1)${plain} Latest Stable: ${latest_stable}"
echo -e "${green}2)${plain} Latest Pre-release: ${latest_prerelease}"
read -rp "Please enter your choice [1-2]: " version_choice
while [[ "$version_choice" != "1" && "$version_choice" != "2" ]]; do
read -rp "Invalid input, please re-enter [1-2]: " version_choice
done
if [[ "$version_choice" == "1" ]]; then
tag_version="$latest_stable"
else
tag_version="$latest_prerelease"
fi
elif [[ -n "$latest_stable" ]]; then
tag_version="$latest_stable"
else
tag_version="$latest_prerelease"
fi
}
```
Note: `update.sh` uses `${curl_bin}` instead of bare `curl` — the helper respects this.
- [ ] **Step 2: Replace the release fetch block in `update_x-ui()`**
Replace lines 760-768:
```bash
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/Sora39831/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
fi
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
```
With:
```bash
get_releases
select_version
echo -e "Got x-ui version: ${tag_version}, beginning the installation..."
```
- [ ] **Step 3: Verify the rest of `update_x-ui()` is unchanged**
Confirm lines 769+ (download, cleanup, install) remain intact.
- [ ] **Step 4: Commit**
```bash
git add update.sh
git commit -m "feat(update): add pre-release version selection prompt"
```

View file

@ -0,0 +1,134 @@
# Panel Settings JSON Migration Design
## Overview
Extract panel settings from the SQLite `settings` table into a standalone JSON file (`x-ui.json`) located in the same directory as the database (`/etc/x-ui/` by default). The `xrayTemplateConfig` remains in the database.
## Requirements
- Panel settings (webPort, tgBot*, sub*, ldap*, etc.) stored in a flat key-value JSON file
- `xrayTemplateConfig` stays in the database `settings` table
- All new installations (no migration from existing DB)
- JSON file path: `<DB_FOLDER>/x-ui.json` (same directory as `x-ui.db`)
- JSON file auto-created on first run with default values
## Architecture
### File Layout
```
/etc/x-ui/
x-ui.db # SQLite: users, inbounds, client_traffics, xrayTemplateConfig
x-ui.json # Panel settings (flat key-value JSON)
```
### JSON Format
```json
{
"webListen": "",
"webPort": "2053",
"webCertFile": "",
"webKeyFile": "",
"secret": "random32chars...",
"webBasePath": "/",
"sessionMaxAge": "360",
"tgBotEnable": "false",
"tgBotToken": "",
"subEnable": "true",
"ldapEnable": "false",
...
}
```
All values are strings (consistent with current DB storage). No `xrayTemplateConfig` key.
## Changes
### 1. `config/config.go`
Add `GetSettingPath()` function:
```go
func GetSettingPath() string {
return fmt.Sprintf("%s/%s.json", GetDBFolderPath(), GetName())
}
```
### 2. `web/service/setting.go`
Replace database-backed `getSetting`/`saveSetting` with JSON file operations:
- **`loadSettings()`** — reads JSON file into `map[string]string`; creates file from `defaultValueMap` if not exists
- **`saveSettings(settings)`** — writes `map[string]string` to JSON file
- **`getSetting(key)`** → read from JSON map
- **`saveSetting(key, value)`** → update key in JSON map, write back
- **`getString(key)`** → `getSetting(key)` with fallback to `defaultValueMap`
- **`GetAllSetting()`** → load JSON map, populate `AllSetting` struct via reflection (same as current, data source changes)
- **`UpdateAllSetting()`** → reflect fields into map, save to JSON
- **`ResetSettings()`** → delete JSON file + clear users table
Remove `import "github.com/mhsanaei/3x-ui/v2/database"` and `model` imports (no longer needed for settings operations).
### 3. `web/service/xray_setting.go`
`XraySettingService.SaveXraySetting()` and related methods continue using the database directly for `xrayTemplateConfig`:
- Replace `s.SettingService.saveSetting("xrayTemplateConfig", ...)` with direct DB operation via `database.GetDB()`
- Add a private helper `saveXraySettingToDB()` / `getXraySettingFromDB()` for direct DB access
### 4. `database/db.go`
Keep `model.Setting{}` in `initModels()` — the `settings` table still stores `xrayTemplateConfig`.
### 5. `main.go`
No changes needed. CLI commands use `SettingService` which handles JSON internally.
The only change: `resetSetting()` calls `settingService.ResetSettings()` which now deletes the JSON file instead of DB rows. The `users` table clearing logic is preserved.
## Data Flow
### Reading
```
Controller/CLI → SettingService.GetString("webPort")
→ loadSettings() [reads x-ui.json]
→ returns "2053" (or default if missing)
```
### Writing
```
Controller/CLI → SettingService.SetPort(8080)
→ setInt("webPort", 8080)
→ setString("webPort", "8080")
→ saveSetting("webPort", "8080")
→ loadSettings() → update map["webPort"] = "8080" → saveSettings()
```
### Xray Config (unchanged path)
```
XraySettingService.SaveXraySetting(config)
→ validate config
→ database.GetDB().Where("key = ?", "xrayTemplateConfig").Save(...)
```
## Error Handling
- JSON file read failure: return error (panel cannot start without settings)
- JSON file write failure: return error (settings update fails, no silent data loss)
- JSON file not found: auto-create from defaults (first run)
- Malformed JSON: return error with clear message
- Concurrent access: Go's single-goroutine web server model means no concurrent write issues for settings
## Testing
- Verify first run creates `x-ui.json` with correct defaults
- Verify `GetAllSetting()` returns correct values from JSON
- Verify `UpdateAllSetting()` writes all fields to JSON
- Verify CLI `x-ui setting -port 8080` updates JSON file
- Verify CLI `x-ui setting -reset` deletes JSON file and recreates on next access
- Verify `xrayTemplateConfig` still works via database
- Verify `x-ui setting -show` reads from JSON file correctly

View file

@ -0,0 +1,79 @@
# Pre-release Install/Update Selection
## Summary
Add interactive prompts to `install.sh` and `update.sh` so users can choose between the latest **Stable** release or the latest **Pre-release** when installing or updating 3x-ui.
## Current State
- `install.sh` and `update.sh` both hardcode `GET /repos/Sora39831/3x-ui/releases/latest`, which only returns stable releases.
- No mechanism exists to install or update to a pre-release version through the automated flow.
## Design
### 1. GitHub API Fetch Helper
A shared function (duplicated in both `install.sh` and `update.sh`, matching existing script conventions) that:
- Calls `GET https://api.github.com/repos/Sora39831/3x-ui/releases` (returns all releases)
- Parses the JSON response to extract:
- `latest_stable_tag` — first entry with `"prerelease": false`
- `latest_prerelease_tag` — first entry with `"prerelease": true` (empty if none exists)
- Uses `grep`/`sed`/`awk` (no `jq` dependency, consistent with existing parsing patterns)
- Falls back to `curl -4` on IPv6 failure, matching existing retry pattern
### 2. Interactive Prompt
Both scripts display a menu after fetching release info:
```
Which version do you want to install/update?
1) Latest Stable: v2.x.x
2) Latest Pre-release: v2.x.x-beta
Please enter your choice [1-2]:
```
Behavior:
- Show actual version tags so the user knows what they're selecting
- If no pre-release exists: skip prompt, use stable automatically
- If no stable release exists (edge case): skip prompt, use pre-release automatically
- Invalid input re-prompts
### 3. install.sh Changes
In `install_x-ui()`, the no-argument path (line ~879):
**Before:** Calls `/releases/latest`, parses single tag, downloads.
**After:**
1. Call fetch helper to get both tags
2. Show interactive prompt
3. Set `tag_version` from user choice
4. Download as before (existing logic unchanged)
The specific-version path (`$1` argument) is unchanged.
### 4. update.sh Changes
In `update_x-ui()`, same pattern:
**Before:** Calls `/releases/latest`, parses single tag, downloads.
**After:**
1. Call fetch helper to get both tags
2. Show interactive prompt
3. Set `tag_version` from user choice
4. Continue existing update logic (unchanged)
`x-ui.sh` is **not modified** — it delegates to `update.sh` already.
## Files Modified
- `install.sh` — add fetch helper + prompt in `install_x-ui()`
- `update.sh` — add fetch helper + prompt in `update_x-ui()`
## Out of Scope
- Persisting user's choice across updates (always prompt each time)
- CLI flags like `--pre-release` for non-interactive use
- Changes to `x-ui.sh` (delegation is already in place)

View file

@ -0,0 +1,332 @@
# MariaDB Support for 3x-ui
## Summary
Add MariaDB as an alternative database backend to SQLite. Users switch between SQLite and MariaDB via the `x-ui.sh` management script (option 27). Data is automatically migrated during the switch. MariaDB connection credentials are stored in `/etc/x-ui/x-ui.json`.
## Requirements
- Support both SQLite and MariaDB as database backends
- Switch via `x-ui.sh` with interactive prompts for MariaDB credentials (IP, port, username, password, database name)
- Auto-migrate data when switching between SQLite and MariaDB
- Keep old database as backup after migration
- MariaDB has core feature parity (CRUD, migrations, seeders) but skips SQLite-specific features (WAL checkpoint, file export/import)
- Credentials stored in `/etc/x-ui/x-ui.json`
## Architecture: Approach A — Driver-agnostic `InitDB`
Refactor `database.InitDB()` to read config from the JSON settings file, determine the driver type, and open the appropriate GORM connection. The package-level `var db *gorm.DB` singleton stays unchanged — all callers continue using `database.GetDB()`.
---
## Section 1: Configuration
### New settings in `web/service/setting.go`
Add to `defaultValueMap`:
| Key | Default | Description |
|-----|---------|-------------|
| `dbType` | `"sqlite"` | `"sqlite"` or `"mariadb"` |
| `dbHost` | `"127.0.0.1"` | MariaDB host |
| `dbPort` | `"3306"` | MariaDB port |
| `dbUser` | `""` | MariaDB username |
| `dbPassword` | `""` | MariaDB password |
| `dbName` | `"3xui"` | MariaDB database name |
Add getter/setter methods: `GetDBType()`, `SetDBType()`, `GetDBHost()`, `SetDBHost()`, `GetDBPort()`, `SetDBPort()`, `GetDBUser()`, `SetDBUser()`, `GetDBPassword()`, `SetDBPassword()`, `GetDBName()`, `SetDBName()`.
### Config reading before DB init
Problem: settings are stored IN the database, but we need `dbType` BEFORE opening the DB.
Solution: `config/config.go` gets a `GetDBTypeFromJSON()` function that reads `/etc/x-ui/x-ui.json` directly (falls back to `"sqlite"` if file doesn't exist or key is missing). This is called before `database.InitDB()`.
### New CLI flags in `main.go`
Add `-dbType`, `-dbHost`, `-dbPort`, `-dbUser`, `-dbPassword`, `-dbName` flags to the `setting` subcommand. These write directly to the JSON config file (not via the DB) using `config.WriteSettingToJSON(key, value)`.
New `config/config.go` helper: `WriteSettingToJSON(key, value string)` — reads the JSON file, updates the key, writes back.
---
## Section 2: Database Layer (`database/db.go`)
### Refactored `InitDB()`
```go
func InitDB() error {
dbType := config.GetDBTypeFromJSON()
switch dbType {
case "mariadb":
return initMariaDB()
default: // "sqlite"
return initSQLite(config.GetDBPath())
}
}
```
### `initSQLite(path string) error`
Existing logic unchanged — opens SQLite with `gorm.io/driver/sqlite`, runs `initModels()`, `initUser()`, `runSeeders()`.
### `initMariaDB() error`
1. Read host, port, user, password, dbName from JSON config.
2. Build DSN: `user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=True&loc=Local`
3. Open with `gorm.io/driver/mysql`.
4. Run `initModels()`, `initUser()`, `runSeeders()` (same as SQLite).
### Adapted functions
- `Checkpoint()` — if MariaDB, return `nil`. If SQLite, existing WAL logic.
- `IsSQLiteDB()` — unchanged, only called for SQLite.
- `ValidateSQLiteDB()` — unchanged, only called for SQLite.
### New dependency
`gorm.io/driver/mysql` added to `go.mod`.
---
## Section 3: Data Migration (`database/migrate.go`)
New file with two functions:
### `MigrateSQLiteToMariaDB() error`
1. Open SQLite connection from `config.GetDBPath()`.
2. Open MariaDB connection from JSON settings.
3. For each table (users, inbounds, outbound_traffics, settings, inbound_client_ips, client_traffics, history_of_seeders):
- AutoMigrate the model on MariaDB.
- `SELECT *` from SQLite → `INSERT` into MariaDB using GORM raw SQL.
4. On success: close connections (SQLite file kept as backup).
5. On failure: return error with context.
### `MigrateMariaDBToSQLite() error`
Reverse of above:
1. Open MariaDB connection from JSON settings.
2. Open/create SQLite connection at `config.GetDBPath()`.
3. For each table: read from MariaDB, write to SQLite.
4. On success: close connections.
5. On failure: return error.
Row transfer approach: Use the existing model structs explicitly. For each table, query all rows from source into a `[]Model` slice, then batch-insert into destination. This avoids raw SQL differences between SQLite and MySQL. Example for users:
```go
var users []model.User
srcDB.Find(&users)
dstDB.CreateInBatches(&users, 100)
```
This pattern repeats for each of the 7 tables.
---
## Section 4: `main.go` Changes
### Updated callers
All `database.InitDB(config.GetDBPath())` calls change to `database.InitDB()`:
- `runWebServer()` (line 49)
- `resetSetting()` (line 134)
- `updateTgbotSetting()` (line 221)
- `updateSetting()` (line 259)
- `updateCert()` (line 318)
- `migrateDb()` (line 395)
### New `migrate-db` subcommand
```go
case "migrate-db":
migrateDbBetweenDrivers()
```
`migrateDbBetweenDrivers()`:
1. Read `dbType` from JSON config.
2. If `dbType == "mariadb"`: call `database.MigrateSQLiteToMariaDB()`.
3. If `dbType == "sqlite"`: call `database.MigrateMariaDBToSQLite()`.
4. Print success/failure message.
### New CLI flags
Add to `setting` subcommand:
- `-dbType string` — set database type
- `-dbHost string` — set MariaDB host
- `-dbPort string` — set MariaDB port
- `-dbUser string` — set MariaDB username
- `-dbPassword string` — set MariaDB password
- `-dbName string` — set MariaDB database name
These call `config.WriteSettingToJSON()` to write directly to the JSON file. Only the 6 DB-related settings use `WriteSettingToJSON()` — all other settings (port, username, etc.) continue to use the existing `SettingService` methods that write through the database.
---
## Section 5: `web/service/server.go` Changes
### `GetDb()`
Add check at the top:
```go
dbType, _ := s.GetDBType()
if dbType == "mariadb" {
return nil, common.NewError("Database export is not supported for MariaDB")
}
```
Existing SQLite logic unchanged.
### `ImportDB()`
Add check at the top:
```go
dbType, _ := s.GetDBType()
if dbType == "mariadb" {
return common.NewError("Database import is not supported for MariaDB")
}
```
Existing SQLite logic unchanged.
---
## Section 6: `x-ui.sh` Changes
### New menu option 27
Add to `show_menu`:
```
│────────────────────────────────────────────────│
│ ${green}27.${plain} 数据库管理 │
```
Add to the case statement:
```bash
27)
check_install && db_menu
;;
```
Update prompt: `请输入选择 [0-27]`
### `db_menu()` function
```bash
db_menu() {
# Read current dbType from JSON
local current_type=$(read_json_dbtype)
echo -e "
╔────────────────────────────────────────────────╗
│ ${green}数据库管理${plain} │
│ ${green}0.${plain} 返回主菜单 │
│ ${green}1.${plain} 查看当前数据库类型(当前: ${current_type}
│ ${green}2.${plain} 切换到 MariaDB │
│ ${green}3.${plain} 切换到 SQLite │
╚────────────────────────────────────────────────╝
"
read -rp "请输入选择 [0-3]" num
case "${num}" in
0) show_menu ;;
1) db_show_status && db_menu ;;
2) db_switch_to_mariadb ;;
3) db_switch_to_sqlite ;;
*) echo "无效选项" && db_menu ;;
esac
}
```
### `db_switch_to_mariadb()`
```bash
db_switch_to_mariadb() {
echo "请输入 MariaDB 连接信息(直接回车使用默认值):"
read -rp "MariaDB IP默认 127.0.0.1: " db_host
db_host=${db_host:-127.0.0.1}
read -rp "MariaDB 端口(默认 3306: " db_port
db_port=${db_port:-3306}
read -rp "MariaDB 用户名: " db_user
if [ -z "$db_user" ]; then
echo -e "${red}用户名不能为空${plain}"
db_menu
return
fi
read -rsp "MariaDB 密码: " db_pass
echo
if [ -z "$db_pass" ]; then
echo -e "${red}密码不能为空${plain}"
db_menu
return
fi
read -rp "数据库名(默认 3xui: " db_name
db_name=${db_name:-3xui}
# Write settings to JSON config
/usr/local/x-ui/x-ui setting -dbType mariadb -dbHost "$db_host" -dbPort "$db_port" -dbUser "$db_user" -dbPassword "$db_pass" -dbName "$db_name"
# Migrate data
echo "正在迁移数据从 SQLite 到 MariaDB..."
/usr/local/x-ui/x-ui migrate-db
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
restart
else
echo -e "${red}数据迁移失败,正在回滚到 SQLite...${plain}"
/usr/local/x-ui/x-ui setting -dbType sqlite
restart
fi
}
```
### `db_switch_to_sqlite()`
```bash
db_switch_to_sqlite() {
/usr/local/x-ui/x-ui setting -dbType sqlite
echo "正在迁移数据从 MariaDB 到 SQLite..."
/usr/local/x-ui/x-ui migrate-db
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
restart
else
echo -e "${red}数据迁移失败${plain}"
fi
}
```
### Helper functions in x-ui.sh
- `read_json_dbtype()` — reads `dbType` from `/etc/x-ui/x-ui.json` using `grep`/`sed` or Python if available.
- `db_show_status()` — displays current DB type and connection info.
---
## Files Changed
| File | Changes |
|------|---------|
| `go.mod` | Add `gorm.io/driver/mysql` |
| `config/config.go` | Add `GetDBTypeFromJSON()`, `WriteSettingToJSON()` |
| `database/db.go` | Refactor `InitDB()` to be driver-agnostic, add `initMariaDB()`, adapt `Checkpoint()` |
| `database/migrate.go` | **New file**`MigrateSQLiteToMariaDB()`, `MigrateMariaDBToSQLite()` |
| `main.go` | Update all `InitDB` calls, add `migrate-db` subcommand, add setting CLI flags |
| `web/service/setting.go` | Add 6 new settings + getter/setter methods |
| `web/service/server.go` | Guard `GetDb()`/`ImportDB()` for MariaDB |
| `x-ui.sh` | Add option 27, `db_menu()`, `db_switch_to_mariadb()`, `db_switch_to_sqlite()`, helpers |
## Testing
1. Fresh install with SQLite (default) — verify panel works as before
2. Switch to MariaDB via x-ui.sh — verify data migrates and panel starts
3. Switch back to SQLite — verify data migrates back
4. Verify MariaDB CRUD operations (create inbound, modify settings, etc.)
5. Verify GetDb/ImportDB return appropriate errors when using MariaDB
6. Verify invalid MariaDB credentials show error and rollback to SQLite

View file

@ -0,0 +1,444 @@
# x-panel (xeefei/x-panel) 设备限制功能分析
> 本文档整理了 x-panel 的设备限制(IP限制)相关逻辑代码和接口,供后续修改 3x-ui IP 限制功能参考。
## 目录
1. [架构概览](#架构概览)
2. [数据模型](#数据模型)
3. [核心任务CheckDeviceLimitJob](#核心任务checkdevicelimitjob)
4. [封禁/解封机制](#封禁解封机制)
5. [观察期防误封逻辑](#观察期防误封逻辑)
6. [TTL 过期清理](#ttl-过期清理)
7. [遗留任务CheckClientIpJob](#遗留任务checkclientipjob)
8. [前端 UI](#前端-ui)
9. [主程序启动与依赖注入](#主程序启动与依赖注入)
10. [关键日志路径](#关键日志路径)
11. [与 3x-ui 的差异总结](#与-3x-ui-的差异总结)
---
## 架构概览
x-panel 有两套 IP 限制机制并行运行:
| 任务 | 来源 | 执行方式 | 核心思路 |
|------|------|----------|----------|
| `CheckDeviceLimitJob` | 新增 | `main.go` 中 goroutine + 10s Ticker | 内存跟踪活跃 IP超限通过 Xray API 替换 UUID 封禁 |
| `CheckClientIpJob` | 遗留(同 3x-ui) | cron 每 10s | 解析 access.log超限 IP 写入 Fail2ban 日志 |
**CheckDeviceLimitJob 工作流程(每 10 秒一次):**
```
Run()
├─ 1. cleanupExpiredIPs() // 清理 3 分钟不活跃的 IP
├─ 2. parseAccessLog() // 增量读取 access.log更新活跃 IP 表
└─ 3. checkAllClientsLimit() // 检查所有用户,超限封禁,恢复解封
```
---
## 数据模型
**源文件:** `database/model/model.go`
### Inbound 结构体(新增字段)
```go
type Inbound struct {
// ... 原有字段 ...
// 设备限制字段per-inbound 级别(不是 per-client
DeviceLimit int `json:"deviceLimit" form:"deviceLimit" gorm:"column:device_limit;default:0"`
}
```
- `device_limit > 0` 表示该入站规则启用了设备限制
- 这是**入站级别**的限制,不是客户端级别的
### Client 结构体
```go
type Client struct {
ID string `json:"id"`
Security string `json:"security"`
Password string `json:"password"`
SpeedLimit int `json:"speedLimit" form:"speedLimit"` // KB/s0=不限速
Flow string `json:"flow"`
Email string `json:"email"`
LimitIP int `json:"limitIp"` // 遗留字段Fail2ban 用
TotalGB int64 `json:"totalGB"`
ExpiryTime int64 `json:"expiryTime"`
Enable bool `json:"enable"`
TgID int64 `json:"tgId"`
SubID string `json:"subId"`
Comment string `json:"comment"`
Reset int `json:"reset"`
}
```
### InboundClientIps与 3x-ui 相同)
```go
type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" gorm:"unique"`
Ips string `json:"ips"` // JSON 数组字符串
}
```
### 内存状态结构
```go
// 活跃 IP 跟踪TTL 机制)
// map[用户email] -> map[IP地址] -> 最后活跃时间
var ActiveClientIPs = make(map[string]map[string]time.Time)
var activeClientsLock sync.RWMutex
// 用户封禁状态跟踪
// map[用户email] -> 是否被封禁(true/false)
var ClientStatus = make(map[string]bool)
var clientStatusLock sync.RWMutex
```
---
## 核心任务CheckDeviceLimitJob
**源文件:** `web/job/check_client_ip_job.go`
### 结构体
```go
type CheckDeviceLimitJob struct {
inboundService service.InboundService
xrayService *service.XrayService
xrayApi xray.XrayAPI
lastPosition int64 // access.log 增量读取位置
telegramService service.TelegramService // TG 通知(可为 nil
violationStartTime map[string]time.Time // 观察期开始时间
triggerLock sync.Mutex // 保护 violationStartTime
}
```
### 构造函数
```go
func NewCheckDeviceLimitJob(xrayService *service.XrayService, telegramService service.TelegramService) *CheckDeviceLimitJob
```
### Run() 主循环
```go
func (j *CheckDeviceLimitJob) Run() {
if !j.xrayService.IsXrayRunning() {
return
}
j.cleanupExpiredIPs()
j.parseAccessLog()
j.checkAllClientsLimit()
}
```
### cleanupExpiredIPs() — 清理过期 IP
- TTL 窗口:**3 分钟**
- 超过 3 分钟未出现的 IP 被删除
- 用户所有 IP 都过期后,用户条目也从 map 中移除
```go
const activeTTL = 3 * time.Minute
for email, ips := range ActiveClientIPs {
for ip, lastSeen := range ips {
if now.Sub(lastSeen) > activeTTL {
delete(ActiveClientIPs[email], ip)
}
}
if len(ActiveClientIPs[email]) == 0 {
delete(ActiveClientIPs, email)
}
}
```
### parseAccessLog() — 增量解析日志
- 使用 `file.Seek(j.lastPosition, 0)` 实现增量读取
- 正则提取 email 和 IP
```go
emailRegex := regexp.MustCompile(`email: ([^ ]+)`)
ipRegex := regexp.MustCompile(`from (?:tcp:|udp:)?\[?([0-9a-fA-F\.:]+)\]?:\d+ accepted`)
```
- 忽略 `127.0.0.1``::1`
- 读取完毕后记录当前位置;如果文件被截断(当前位置 < 上次位置重置为 0
### checkAllClientsLimit() — 核心检查逻辑
```go
// 查询启用了设备限制且正在运行的入站
db.Where("device_limit > 0 AND enable = ?", true).Find(&inbounds)
// 获取 Xray API 端口
apiPort := j.xrayService.GetApiPort()
j.xrayApi.Init(apiPort)
defer j.xrayApi.Close()
```
**第一步:处理在线用户**
- 遍历 `ActiveClientIPs`
- 通过 `inboundService.GetClientTrafficByEmail(email)` 关联到入站
- 检查活跃 IP 数 vs `device_limit`
- 超限 → 进入观察期逻辑 → 封禁
- 恢复 → 解封
**第二步:处理已封禁但已下线的用户**
- 遍历 `ClientStatus`
- 已封禁但不在 `ActiveClientIPs` 中的用户 → 解封
---
## 封禁/解封机制
### banUser() — 封禁UUID 替换)
```go
func (j *CheckDeviceLimitJob) banUser(email string, activeIPCount int, info *struct{...}) {
// 1. 从数据库获取原始客户端信息
_, client, err := j.inboundService.GetClientByEmail(email)
// 2. 发送 Telegram 通知(异步 goroutine
go func() {
j.telegramService.SendMessage(tgMessage)
}()
// 3. 从 Xray-Core 中删除该用户
j.xrayApi.RemoveUser(info.Tag, email)
// 4. 等待 5 秒,解决竞态条件
time.Sleep(5000 * time.Millisecond)
// 5. 创建临时客户端,替换 UUID/Password
tempClient := *client
if tempClient.ID != "" { tempClient.ID = RandomUUID() }
if tempClient.Password != "" { tempClient.Password = RandomUUID() }
// 6. 用错误的 UUID/Password 添加回去 → 客户端无法通过验证
j.xrayApi.AddUser(string(info.Protocol), info.Tag, clientMap)
// 7. 标记为已封禁
ClientStatus[email] = true
}
```
### unbanUser() — 解封(恢复原始 UUID
```go
func (j *CheckDeviceLimitJob) unbanUser(email string, activeIPCount int, info *struct{...}) {
// 1. 从数据库获取原始客户端信息
_, client, err := j.inboundService.GetClientByEmail(email)
// 2. 删除封禁用的临时用户
j.xrayApi.RemoveUser(info.Tag, email)
// 3. 等待 5 秒
time.Sleep(5000 * time.Millisecond)
// 4. 用原始正确的 UUID/Password 添加回去
j.xrayApi.AddUser(string(info.Protocol), info.Tag, clientMap)
// 5. 移除封禁标记
delete(ClientStatus, email)
}
```
### RandomUUID() — 生成随机 UUID
```go
func RandomUUID() string {
uuid := make([]byte, 16)
rand.Read(uuid)
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
return hex.EncodeToString(uuid[0:4]) + "-" + hex.EncodeToString(uuid[4:6]) + "-" +
hex.EncodeToString(uuid[6:8]) + "-" + hex.EncodeToString(uuid[8:10]) + "-" +
hex.EncodeToString(uuid[10:16])
}
```
### 关键依赖接口
| 接口 | 说明 |
|------|------|
| `j.inboundService.GetClientByEmail(email)` | 从数据库获取客户端原始配置(含 UUID/Password |
| `j.xrayApi.RemoveUser(tag, email)` | 通过 gRPC 从 Xray-Core 移除用户 |
| `j.xrayApi.AddUser(protocol, tag, clientMap)` | 通过 gRPC 向 Xray-Core 添加用户 |
| `j.xrayService.GetApiPort()` | 获取 Xray API 端口号 |
| `j.xrayService.IsXrayRunning()` | 检查 Xray 是否运行中 |
| `j.telegramService.SendMessage(msg)` | 发送 Telegram 通知 |
---
## 观察期防误封逻辑
**目的:** 解决用户切换网络时产生临时双 IP 导致误封的问题。
```
场景 A用户设备数超限且当前未被封禁
├─ 首次发现超限 → 记录时间,进入 3 分钟观察期,不封禁
├─ 观察期内仍超限但未满 3 分钟 → 继续观察
└─ 观察期满 3 分钟仍超限 → 确认封禁
场景 B用户恢复正常IP 数 ≤ 限制)
├─ 之前在观察名单中 → 移除观察记录,皆大欢喜
└─ 之前被封禁 → 执行解封
```
核心代码:
```go
if activeIPCount > info.Limit && !isBanned {
startTime, exists := j.violationStartTime[email]
if !exists {
// 首次超限,开始观察
j.violationStartTime[email] = time.Now()
continue
}
if time.Since(startTime) < 3*time.Minute {
// 还在观察期,暂不封禁
continue
}
// 观察期结束,确认封禁
delete(j.violationStartTime, email)
j.banUser(email, activeIPCount, &info)
}
```
---
## TTL 过期清理
- **活跃窗口:** 3 分钟
- 每 10 秒执行一次清理
- IP 在 `ActiveClientIPs` 中的 `lastSeen` 时间超过 3 分钟则删除
- 用户所有 IP 被清理后,用户条目也移除
- 被清理的已封禁用户在 `checkAllClientsLimit` 第二步中会被解封
---
## 遗留任务CheckClientIpJob
**源文件:** `web/job/check_client_ip_job.go` (lines 416-714)
与 3x-ui 的实现完全一致:
1. 解析 access.log提取每个 email 的所有 IP
2. 与数据库中 `InboundClientIps` 记录对比
3. 超过 `LimitIP` 的 IP 写入 `3xipl.log`
4. 依赖 Fail2ban 读取日志进行 iptables 封禁
5. 每小时清理 access.log
此任务由 cron 调度,与 `CheckDeviceLimitJob` 独立运行。
---
## 前端 UI
**源文件:** `web/html/form/client.html`
### 入站级别
`DeviceLimit` 字段不在 client 表单中显示,而是在入站配置中设置(具体 UI 未在提供的文件中)。
### 客户端级别
| 字段 | 行号 | 说明 |
|------|------|------|
| `client.limitIp` | 108 | IP 数量限制遗留Fail2ban 用) |
| `client.speedLimit` | 85-92 | 独立限速,单位 KB/s0=不限速 |
| `client._totalGB` | 150 | 总流量限制 |
| `client._expiryTime` | 179-182 | 过期时间 |
| `client.reset` | 193 | 续期天数 |
---
## 主程序启动与依赖注入
**源文件:** `main.go`
### 服务初始化runWebServer 函数)
```go
// 1. 创建服务实例
xrayService := service.XrayService{}
settingService := service.SettingService{}
serverService := service.ServerService{}
inboundService := service.InboundService{}
// 2. 创建 Xray API 实例并注入
xrayApi := xray.XrayAPI{}
xrayService.SetXrayAPI(xrayApi)
inboundService.SetXrayAPI(xrayApi)
// 3. 初始化 Telegram Bot如已启用
if tgEnable {
tgBot := service.NewTgBot(...)
tgBotService = tgBot
}
// 4. 注入 Telegram 服务
serverService.SetTelegramService(tgBotService)
inboundService.SetTelegramService(tgBotService)
```
### 设备限制定时任务启动
```go
go func() {
time.Sleep(10 * time.Second) // 等待面板和 Xray 稳定
ticker := time.NewTicker(10 * time.Second) // 每 10 秒执行
defer ticker.Stop()
// 创建 Telegram 服务(可为 nil
var tgBotService service.TelegramService
if tgEnable {
tgBotService = new(service.Tgbot)
}
// 创建任务实例
checkJob := job.NewCheckDeviceLimitJob(&xrayService, tgBotService)
// 无限循环
for {
<-ticker.C
checkJob.Run()
}
}()
```
---
## 关键日志路径
| 路径 | 说明 |
|------|------|
| `config.GetLogFolder() + "/3xipl.log"` | IP 限制日志(遗留 Fail2ban 用) |
| `config.GetLogFolder() + "/3xipl-banned.log"` | 封禁日志 |
| `config.GetLogFolder() + "/3xipl-ap.log"` | 持久化访问日志 |
| Xray access log配置中指定 | 用户连接日志,设备限制解析源 |
| `config.GetBinFolderPath() + "/core_crash_*.log"` | 崩溃报告 |
---
## 与 3x-ui 的差异总结
| 特性 | 3x-ui | x-panel |
|------|-------|---------|
| IP 限制级别 | per-client (`LimitIP`) | per-inbound (`DeviceLimit`) + per-client 遗留 |
| 封禁方式 | Fail2ban + iptables | Xray API UUID 替换 |
| 活跃 IP 跟踪 | 无(全量日志分析) | 内存 map + 3 分钟 TTL |
| 误封防护 | 无 | 3 分钟观察期 |
| 解封机制 | Fail2ban unban | 恢复原始 UUID |
| 通知 | 无 | Telegram Bot 集成 |
| 限速 | 无 | per-client `SpeedLimit` (KB/s) |
| 调度方式 | cron 10s | goroutine + Ticker 10s |
| 依赖 | Fail2ban, iptables | Xray gRPC API |

937
docs/x-ui-logic.md Normal file
View file

@ -0,0 +1,937 @@
# x-ui.sh 逻辑文档
## 概述
`x-ui.sh` 是 3x-ui 面板的管理脚本,提供 26 个交互式菜单选项和 15 个子命令涵盖面板的安装、更新、卸载、凭据管理、服务控制、SSL 证书、防火墙、Fail2ban IP 限制、BBR 加速、Geo 文件更新等功能。
---
## 全局配置
### 颜色变量
| 变量 | 值 | 用途 |
|---------|----------------|----------|
| `red` | `\033[0;31m` | 红色 |
| `green` | `\033[0;32m` | 绿色 |
| `blue` | `\033[0;34m` | 蓝色 |
| `yellow`| `\033[0;33m` | 黄色 |
| `plain` | `\033[0m` | 重置 |
### 日志函数
| 函数 | 前缀 | 用途 |
|---------|-----------|------------|
| `LOGD()` | `[调试]` | 调试信息 |
| `LOGE()` | `[错误]` | 错误信息 |
| `LOGI()` | `[信息]` | 普通信息 |
### 路径变量
| 变量 | 默认值 | 说明 |
|-------------------------|---------------------------|-------------------------|
| `xui_folder` | `/usr/local/x-ui` | x-ui 安装目录 |
| `xui_service` | `/etc/systemd/system` | systemd 服务文件目录 |
| `log_folder` | `/var/log/x-ui` | 日志目录 |
| `iplimit_log_path` | `.../3xipl.log` | IP 限制日志 |
| `iplimit_banned_log_path`| `.../3xipl-banned.log` | IP 封禁日志 |
### 辅助函数
| 函数 | 功能 |
|-----------------------|----------------------------------------------|
| `confirm()` | 通用确认提示,支持自定义默认值 |
| `confirm_restart()` | 确认后重启面板(重启 x-ui 也会重启 xray |
| `before_show_menu()` | 按回车返回主菜单 |
| `gen_random_string()` | 通过 openssl 生成指定长度的随机字母数字字符串 |
| `is_port_in_use()` | 端口占用检测ss → netstat → lsof |
| `is_ipv4/is_ipv6/is_ip/is_domain()` | IP/域名格式验证 |
---
## 入口流程
```
x-ui.sh 被执行
├─ 检查 root 权限
├─ 检测操作系统发行版和版本号
├─ 初始化路径和日志目录
├─ 有命令行参数 → 执行对应子命令(不显示菜单)
└─ 无参数 → 显示交互式菜单 show_menu()
├─ 显示当前状态(运行/停止/未安装 + 开机自启 + xray 状态)
├─ 读取用户输入 [0-26]
└─ 根据选择调用对应功能
```
---
## 主菜单 (show_menu)
```
╔────────────────────────────────────────────────╗
│ 0. 退出脚本 │
│────────────────────────────────────────────────│
│ 1. 安装 2. 更新 3. 更新菜单 │
│ 4. 安装旧版本 5. 卸载 │
│────────────────────────────────────────────────│
│ 6. 重置用户名和密码 7. 重置 Web 路径 │
│ 8. 重置设置 9. 修改端口 │
│ 10. 查看当前设置 │
│────────────────────────────────────────────────│
│ 11. 启动 12. 停止 13. 重启 │
│ 14. 重启 Xray 15. 查看状态 │
│ 16. 日志管理 │
│────────────────────────────────────────────────│
│ 17. 设置开机自启 18. 取消开机自启 │
│────────────────────────────────────────────────│
│ 19. SSL 证书管理 20. Cloudflare SSL │
│ 21. IP 限制管理 22. 防火墙管理 │
│ 23. SSH 端口转发管理 │
│────────────────────────────────────────────────│
│ 24. BBR 管理 25. 更新 Geo 文件 │
│ 26. 网速测试 (Speedtest) │
╚────────────────────────────────────────────────╝
```
大部分选项在执行前调用 `check_install`(检查面板是否已安装)或 `check_uninstall`(检查面板是否未安装),防止误操作。
---
## 状态检测函数
| 函数 | 返回值 | 逻辑 |
|------------------------|---------------------------|-------------------------------------------|
| `check_status()` | 0=运行中, 1=未运行, 2=未安装 | Alpine 检查 init.d其他检查 systemd |
| `check_enabled()` | 0=已启用, 1=未启用 | Alpine 检查 rc-update其他检查 systemctl |
| `check_xray_status()` | 0=运行中, 1=未运行 | ps 查找 xray-linux 进程 |
| `check_install()` | 前置检查 | 未安装则提示并返回菜单 |
| `check_uninstall()` | 前置检查 | 已安装则提示"勿重复安装"并返回菜单 |
---
## 菜单选项详解
### 选项 0退出脚本
```bash
exit 0
```
直接退出,无额外逻辑。
---
### 选项 1安装
**函数**`install()`
```
下载并执行 install.sh从 GitHub raw 文件)
└─ 成功后自动调用 start()
```
- 执行 `bash <(curl -Ls https://raw.githubusercontent.com/Sora39831/3x-ui/main/install.sh)`
- 安装成功后自动启动面板
---
### 选项 2更新
**函数**`update()`
```
确认提示:"更新所有 x-ui 组件到最新版本,数据不会丢失"
├─ 取消 → 返回菜单
└─ 确认 → 执行 update.sh从 GitHub 下载)
└─ 成功 → "更新完成,面板已自动重启"
```
---
### 选项 3更新菜单
**函数**`update_menu()`
```
确认提示
└─ 确认 → 下载最新 x-ui.sh 到 /usr/bin/x-ui
└─ 成功 → "更新成功" 并 exit 0
```
仅更新管理脚本自身,不影响面板程序。
---
### 选项 4安装旧版本
**函数**`legacy_version()`
```
提示用户输入版本号(如 2.4.0
├─ 空 → 退出
└─ 有效 → 执行对应版本的 install.sh传入版本参数
```
- 下载指定 tag 的 install.sh`v$tag_version/install.sh`
- 传入参数 `v$tag_version` 进行安装
- install.sh 内部会验证版本 ≥ v2.3.5
---
### 选项 5卸载
**函数**`uninstall()`
```
确认:"卸载面板xray 也会被卸载!"(默认 n
├─ 取消 → 返回菜单
└─ 确认 →
Alpine: rc-service stop → rc-update del → rm init.d
其他: systemctl stop → disable → rm service → daemon-reload → reset-failed
删除 /etc/x-ui/ 和 ${xui_folder}/
显示重装命令
删除脚本自身trap SIGTERM → rm $0
```
---
### 选项 6重置用户名和密码
**函数**`reset_user()`
```
确认提示(默认 n
└─ 确认 →
输入用户名(默认随机 10 位)
输入密码(默认随机 18 位)
询问是否禁用双因素认证
├─ 是 → -resetTwoFactor true
└─ 否 → -resetTwoFactor false
应用设置x-ui setting -username ... -password ...
确认后重启面板
```
---
### 选项 7重置 Web 路径
**函数**`reset_webbasepath()`
```
确认提示
└─ 确认 → 生成随机 18 位字符串
应用x-ui setting -webBasePath ...
重启面板
```
---
### 选项 8重置设置
**函数**`reset_config()`
```
确认:"重置所有面板设置?账户数据不会丢失,用户名和密码不会改变"(默认 n
└─ 确认 → x-ui setting -reset
重启面板
```
仅重置面板配置,不影响账户数据库。
---
### 选项 9修改端口
**函数**`set_port()`
```
输入端口号 [1-65535]
├─ 空 → 取消
└─ 有效 → x-ui setting -port ${port}
确认后重启面板
```
---
### 选项 10查看当前设置
**函数**`check_config()`
```
获取面板设置x-ui setting -show true
获取公网 IPapi.ipify.org → 4.ident.me
检查是否有证书:
├─ 有证书 → 从证书路径提取域名,显示 https://域名:端口/路径
└─ 无证书 →
显示警告
询问是否为 IP 生成 SSL 证书
├─ 是 → 停止面板 → ssl_cert_issue_for_ip() → 启动面板
└─ 否 → 显示 http://IP:端口/路径,建议使用选项 19
```
---
### 选项 11启动
**函数**`start()`
```
检查当前状态
├─ 运行中 → "面板正在运行,无需重复启动"
└─ 未运行 →
Alpine: rc-service x-ui start
其他: systemctl start x-ui
等待 2 秒后再次检查状态
├─ 成功 → "x-ui 启动成功"
└─ 失败 → "面板启动失败,可能是因为启动时间超过两秒"
```
---
### 选项 12停止
**函数**`stop()`
```
检查当前状态
├─ 已停止 → "面板已停止,无需重复停止!"
└─ 运行中 →
Alpine: rc-service x-ui stop
其他: systemctl stop x-ui
等待 2 秒后检查状态
├─ 成功 → "x-ui 和 xray 已停止"
└─ 失败 → "面板停止失败"
```
---
### 选项 13重启
**函数**`restart()`
```
Alpine: rc-service x-ui restart
其他: systemctl restart x-ui
等待 2 秒后检查状态
├─ 成功 → "x-ui 和 xray 重启成功"
└─ 失败 → "面板重启失败"
```
---
### 选项 14重启 Xray
**函数**`restart_xray()`
```
systemctl reload x-ui ← 发送 reload 信号,不重启面板本身
"已发送重启信号,请查看日志确认"
等待 2 秒 → 显示 xray 运行状态
```
与选项 13 的区别:选项 13 重启整个 x-ui 服务,选项 14 仅重载 xray-core。
---
### 选项 15查看状态
**函数**`status()`
```
Alpine: rc-service x-ui status
其他: systemctl status x-ui -l
```
显示完整的 systemd/服务状态信息。
---
### 选项 16日志管理
**函数**`show_log()`
```
Alpine:
1. 调试日志 → grep 'x-ui[' /var/log/messages
0. 返回
其他 (systemd):
1. 调试日志 → journalctl -u x-ui -e --no-pager -f -p debug
2. 清除所有日志 → journalctl --rotate → --vacuum-time=1s → 重启面板
0. 返回
```
---
### 选项 17设置开机自启
**函数**`enable()`
```
Alpine: rc-update add x-ui default
其他: systemctl enable x-ui
```
---
### 选项 18取消开机自启
**函数**`disable()`
```
Alpine: rc-update del x-ui
其他: systemctl disable x-ui
```
---
### 选项 19SSL 证书管理
**函数**`ssl_cert_issue_main()` — 子菜单入口
#### 子菜单
```
1. 获取 SSL域名
2. 吊销证书
3. 强制续期
4. 查看已有域名
5. 为面板设置证书路径
6. 为 IP 地址获取 SSL6 天证书,自动续期)
0. 返回主菜单
```
#### 子选项 1获取 SSL域名证书
**函数**`ssl_cert_issue()`
```
检查/安装 acme.sh
按发行版安装 socat
获取并验证域名(循环直到有效)
检查是否已有该域名的证书acme.sh --list
创建证书目录 /root/cert/${domain}/
选择端口(默认 80
签发证书:
acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force
↳ 失败 → 清理并退出
设置 reloadcmd
默认x-ui restart
可选systemctl reload nginx ; x-ui restart
可选:自定义命令
安装证书:
acme.sh --installcert
--key-file /root/cert/${domain}/privkey.pem
--fullchain-file /root/cert/${domain}/fullchain.pem
--reloadcmd ${reloadCmd}
启用自动续期acme.sh --upgrade --auto-upgrade
设置文件权限privkey.pem → 600, fullchain.pem → 644
询问是否为面板设置证书:
├─ 是 → x-ui cert -webCert ... -webCertKey ... → 重启
└─ 否 → 跳过
```
#### 子选项 2吊销证书
```
列出 /root/cert/ 下所有域名目录
选择域名 → acme.sh --revoke -d ${domain}
```
#### 子选项 3强制续期
```
列出所有域名
选择域名 → acme.sh --renew -d ${domain} --force
```
#### 子选项 4查看已有域名
```
遍历 /root/cert/ 下的域名目录
显示每个域名的 fullchain.pem 和 privkey.pem 路径
缺失文件的标记为"证书或密钥缺失"
```
#### 子选项 5为面板设置证书路径
```
列出所有域名
选择域名 → 验证文件存在
x-ui cert -webCert ... -webCertKey ...
重启面板
```
#### 子选项 6为 IP 地址获取 SSL
**函数**`ssl_cert_issue_for_ip()`
```
获取服务器公网 IPapi.ipify.org → 4.ident.me
询问是否包含 IPv6 地址
检查/安装 acme.sh
按发行版安装 socat
创建证书目录 /root/cert/ip/
构建域名参数:-d ${server_ip} [-d ${ipv6}]
选择 HTTP-01 监听端口(默认 80
└─ 循环检测端口占用,被占用则提示换端口
签发证书:
acme.sh --issue
-d ${server_ip} [-d ${ipv6}]
--standalone --server letsencrypt
--certificate-profile shortlived
--days 6 --httpport ${WebPort} --force
安装证书(不依赖退出码,通过检查文件判断成功)
启用自动续期
设置文件权限
为面板设置证书路径 → 显示 https://IP:端口/路径 → 重启面板
```
---
### 选项 20Cloudflare SSL 证书
**函数**`ssl_cert_issue_CF()`
```
显示使用说明(需要:邮箱、全局 API 密钥、域名)
确认提示
检查/安装 acme.sh
输入域名 (CF_Domain)
输入 API 密钥 (CF_GlobalKey)
输入注册邮箱 (CF_AccountEmail)
设置 CA 为 Let's Encrypt
导出环境变量CF_Key, CF_Email
签发通配符证书:
acme.sh --issue --dns dns_cf -d ${domain} -d *.${domain} --force
↳ 使用 Cloudflare DNS 验证
创建证书目录 /root/cert/${domain}/
设置 reloadcmd同域名证书流程
安装证书(含 *.${domain} 通配符)
启用自动续期
询问是否为面板设置证书 → 同域名证书流程
```
**特点**:支持通配符证书 `*.domain.com`,不需要开放 80 端口(使用 DNS 验证)。
---
### 选项 21IP 限制管理Fail2ban
**函数**`iplimit_main()` — 子菜单入口
#### 子菜单
```
1. 安装 Fail2ban 并配置 IP 限制
2. 修改封禁时长
3. 解封所有人
4. 封禁日志
5. 封禁指定 IP 地址
6. 解封指定 IP 地址
7. 实时日志
8. 服务状态
9. 重启服务
10. 卸载 Fail2ban 和 IP 限制
0. 返回主菜单
```
#### 子选项 1安装 Fail2ban
**函数**`install_iplimit()`
```
检查 Fail2ban 是否已安装
└─ 未安装 → 按发行版安装:
Ubuntu ≥ 24: 额外安装 python3-pip + pyasynchat
Debian ≥ 12: 额外安装 python3-systemd
CentOS 7: 先装 epel-release
清除 jail 配置冲突iplimit_remove_conflicts
创建日志文件3xipl.log, 3xipl-banned.log
创建 jail 配置create_iplimit_jails
启动并启用 Fail2ban 服务
```
**Jail 配置详情** (`create_iplimit_jails`)
```ini
# /etc/fail2ban/jail.d/3x-ipl.conf
[3x-ipl]
enabled=true
backend=auto
filter=3x-ipl
action=3x-ipl
logpath=/var/log/x-ui/3xipl.log
maxretry=2
findtime=32
bantime=30m # 默认 30 分钟,可通过子选项 2 修改
```
**过滤器**:匹配 `[LIMIT_IP] Email=... || Disconnecting OLD IP=... || Timestamp=...` 格式的日志行。
**动作**:使用 iptables 封禁/解封 IP同时写入封禁日志文件。
#### 子选项 2修改封禁时长
```
输入新的封禁时长(分钟)
重新生成 jail 配置 → 重启 Fail2ban
```
#### 子选项 3解封所有人
```
fail2ban-client reload --restart --unban 3x-ipl
清空封禁日志文件
```
#### 子选项 5/6手动封禁/解封 IP
```
输入 IP 地址 → 正则验证IPv4/IPv6
fail2ban-client set 3x-ipl banip/unbanip "$ip"
```
#### 子选项 10卸载
```
选项 1仅移除 IP 限制配置(保留 Fail2ban
删除 filter.d/3x-ipl.conf, action.d/3x-ipl.conf, jail.d/3x-ipl.conf
重启 Fail2ban
选项 2完全卸载
删除 /etc/fail2ban
停止服务
按发行版卸载 fail2ban 包 + autoremove
```
---
### 选项 22防火墙管理
**函数**`firewall_menu()` — 子菜单入口(基于 UFW
#### 子菜单
```
1. 安装防火墙
2. 端口列表 [带编号]
3. 开放端口
4. 删除列表中的端口
5. 启用防火墙
6. 禁用防火墙
7. 防火墙状态
0. 返回主菜单
```
#### 子选项 1安装防火墙
**函数**`install_firewall()`
```
检查 ufw 是否安装 → 未安装则 apt-get install ufw
检查防火墙是否激活 → 未激活则:
ufw allow ssh
ufw allow http
ufw allow https
ufw allow 2053/tcp ← webPort
ufw allow 2096/tcp ← subport
ufw --force enable
```
#### 子选项 3开放端口
**函数**`open_ports()`
```
输入端口(逗号分隔或范围,如 80,443,2053 或 400-500
验证输入格式
逐个处理:
范围 → ufw allow start:end/tcp + ufw allow start:end/udp
单端口 → ufw allow port
确认显示已开放的端口
```
#### 子选项 4删除端口
**函数**`delete_ports()`
```
显示当前规则ufw status numbered
选择删除方式:
1. 按规则编号删除 → ufw delete $number
2. 按端口号删除 → ufw delete allow $port
确认显示已删除的端口
```
**注意**:原始代码中选项 4 有一个已知 bug`firewall_wall_menu` 应为 `firewall_menu`),这会导致删除端口后不返回菜单。
---
### 选项 23SSH 端口转发管理
**函数**`SSH_port_forwarding()`
```
获取服务器公网 IP多 API 轮询)
读取当前面板设置:
- webBasePath, port, listenIP, cert, key
判断状态:
├─ 已有证书+密钥 → "面板已配置 SSL安全" → 返回
├─ 无证书且 listenIP 为空或 0.0.0.0 → "面板不安全" 警告
└─ listenIP 已设置且非 0.0.0.0 → 显示 SSH 转发命令
子菜单:
1. 设置监听 IP
├─ 默认 127.0.0.1 或自定义
├─ x-ui setting -listenIP ${ip}
└─ 显示 SSH 转发命令:
ssh -L 2222:${listenIP}:${port} root@${server_ip}
访问 http://localhost:2222${webBasePath}
2. 清除监听 IP
└─ x-ui setting -listenIP 0.0.0.0 → 重启
0. 返回
```
**用途**:将面板绑定到 127.0.0.1,只能通过 SSH 隧道访问,提高安全性。
---
### 选项 24BBR 管理
**函数**`bbr_menu()` — 子菜单入口
#### 子菜单
```
1. 启用 BBR
2. 禁用 BBR
0. 返回主菜单
```
#### 启用 BBR
**函数**`enable_bbr()`
```
检查是否已启用tcp_congestion_control == bbr 且 default_qdisc 为 fq/cake
├─ 已启用 → 直接返回
└─ 未启用 →
有 /etc/sysctl.d/ →
创建 /etc/sysctl.d/99-bbr-x-ui.conf
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
注释 sysctl.conf 中的旧设置
sysctl --system
无 /etc/sysctl.d/ →
直接修改 /etc/sysctl.conf
sysctl -p
验证tcp_congestion_control == bbr → "BBR 已成功启用"
```
**特性**:启用前会备份当前设置(写入注释行 `#旧qdisc:旧拥塞控制`),以便禁用时恢复。
#### 禁用 BBR
**函数**`disable_bbr()`
```
检查是否已启用 → 未启用则返回
有 99-bbr-x-ui.conf →
读取备份的旧设置
恢复 net.core.default_qdisc 和 net.ipv4.tcp_congestion_control
删除配置文件
sysctl --system
无 99-bbr-x-ui.conf →
将 sysctl.conf 中的 fq→pfifo_fast, bbr→cubic
sysctl -p
验证tcp_congestion_control != bbr → "BBR 已成功替换为 CUBIC"
```
---
### 选项 25更新 Geo 文件
**函数**`update_geo()` — 子菜单入口
#### 子菜单
```
1. Loyalsoldier (geoip.dat, geosite.dat)
2. chocolate4u (geoip_IR.dat, geosite_IR.dat)
3. runetfreedom (geoip_RU.dat, geosite_RU.dat)
4. 全部更新
0. 返回主菜单
```
#### 数据源
| 选项 | 数据源 | 文件 | 用途 |
|------|---------------------------------------|------------------------------|------------------|
| 1 | Loyalsoldier/v2ray-rules-dat | geoip.dat, geosite.dat | 通用规则 |
| 2 | chocolate4u/Iran-v2ray-rules | geoip_IR.dat, geosite_IR.dat | 伊朗规则 |
| 3 | runetfreedom/russia-v2ray-rules-dat | geoip_RU.dat, geosite_RU.dat | 俄罗斯规则 |
| 4 | 以上全部 | 全部 6 个文件 | 一键更新 |
**下载逻辑** (`update_geofiles`)
```
每个文件:
curl -fLRo ${xui_folder}/bin/${dat}.dat
-z ${xui_folder}/bin/${dat}.dat ← 仅在远程更新时下载
https://github.com/${source}/releases/latest/download/${remote_file}.dat
```
`-z` 参数确保只有远程文件比本地新时才下载,节省带宽。
更新后自动重启面板以加载新规则。
---
### 选项 26网速测试 (Speedtest)
**函数**`run_speedtest()`
```
检查 speedtest 命令是否存在
└─ 不存在 →
有 snap → snap install speedtest
无 snap → 按包管理器安装:
dnf/yum → rpm 包源
apt-get/apt → deb 包源
curl 安装脚本 → 包管理器安装
执行 speedtest
```
---
## 子命令(命令行模式)
当脚本以参数调用时(如 `x-ui start`),跳过交互菜单直接执行:
| 子命令 | 对应菜单 | 附加行为 |
|------------------------|----------|-------------------------------|
| `start` | 11 | 执行后不返回菜单 |
| `stop` | 12 | 执行后不返回菜单 |
| `restart` | 13 | 执行后不返回菜单 |
| `restart-xray` | 14 | 执行后不返回菜单 |
| `status` | 15 | 执行后不返回菜单 |
| `settings` | 10 | 执行后不返回菜单 |
| `enable` | 17 | 执行后不返回菜单 |
| `disable` | 18 | 执行后不返回菜单 |
| `log` | 16 | 执行后不返回菜单 |
| `banlog` | 4(限制) | 执行后不返回菜单 |
| `update` | 2 | 执行后不返回菜单 |
| `legacy` | 4 | 执行后不返回菜单 |
| `install` | 1 | 使用 check_uninstall 前置检查 |
| `uninstall` | 5 | 执行后不返回菜单 |
| `update-all-geofiles` | 25-4 | 更新后自动重启 |
| 无效参数 | — | 显示用法帮助 |
所有子命令传递参数 `0` 给功能函数,使其执行后不调用 `before_show_menu()` 返回菜单。
---
## 调用关系总览
```
x-ui.sh
├─ show_menu()
│ ├─ show_status() → check_status() + show_enable_status() + show_xray_status()
│ ├─ 0: exit
│ ├─ 1: install() → install.sh → start()
│ ├─ 2: update() → update.sh
│ ├─ 3: update_menu() → 下载 x-ui.sh
│ ├─ 4: legacy_version() → install.sh v$version
│ ├─ 5: uninstall() → 停止服务 + 删除文件
│ ├─ 6: reset_user() → x-ui setting -username/-password
│ ├─ 7: reset_webbasepath() → x-ui setting -webBasePath
│ ├─ 8: reset_config() → x-ui setting -reset
│ ├─ 9: set_port() → x-ui setting -port
│ ├─ 10: check_config() → x-ui setting -show + ssl_cert_issue_for_ip()
│ ├─ 11: start() → systemctl/rc-service start
│ ├─ 12: stop() → systemctl/rc-service stop
│ ├─ 13: restart() → systemctl/rc-service restart
│ ├─ 14: restart_xray() → systemctl reload
│ ├─ 15: status() → systemctl/rc-service status
│ ├─ 16: show_log() → journalctl/grep messages
│ ├─ 17: enable() → systemctl/rc-update enable
│ ├─ 18: disable() → systemctl/rc-update disable
│ ├─ 19: ssl_cert_issue_main()
│ │ ├─ 1: ssl_cert_issue() → acme.sh 域名证书
│ │ ├─ 2: 吊销证书 → acme.sh --revoke
│ │ ├─ 3: 强制续期 → acme.sh --renew --force
│ │ ├─ 4: 查看已有域名
│ │ ├─ 5: 设置面板证书路径
│ │ └─ 6: ssl_cert_issue_for_ip() → acme.sh IP 短期证书
│ ├─ 20: ssl_cert_issue_CF() → acme.sh Cloudflare DNS 通配符证书
│ ├─ 21: iplimit_main()
│ │ ├─ 1: install_iplimit() → install fail2ban + create_iplimit_jails()
│ │ ├─ 2: 修改封禁时长
│ │ ├─ 3: 解封所有人
│ │ ├─ 4: show_banlog()
│ │ ├─ 5/6: 手动封禁/解封 IP
│ │ ├─ 7: tail -f fail2ban.log
│ │ ├─ 8/9: 服务状态/重启
│ │ └─ 10: remove_iplimit()
│ ├─ 22: firewall_menu() → UFW 防火墙管理
│ ├─ 23: SSH_port_forwarding() → 设置 listenIP 为 127.0.0.1
│ ├─ 24: bbr_menu() → enable_bbr() / disable_bbr()
│ ├─ 25: update_geo() → update_geofiles() → 下载 geoip/geosite .dat
│ └─ 26: run_speedtest() → speedtest
└─ 子命令模式($# > 0
└─ case $1 in "start"|"stop"|... → 对应函数 0
```
---
## 关键设计决策
1. **Alpine 兼容**:所有服务管理操作都区分 Alpine (OpenRC) 和其他系统 (systemd),通过 `$release` 变量判断。
2. **操作确认**:危险操作(卸载、重置凭据等)默认为 "n",防止误操作。安全操作(更新等)默认为 "y"。
3. **子命令模式**:支持 `x-ui start` 等非交互式调用,传递参数 `0` 抑制 `before_show_menu()` 的回车等待。
4. **状态前置检查**:大多数菜单选项先调用 `check_install``check_uninstall`,确保操作的前提条件满足。
5. **等待机制**start/stop/restart 后等待 2 秒再检查状态,给 systemd/init.d 足够时间完成操作。
6. **Geo 文件条件下载**:使用 `curl -z` 参数,仅在远程文件比本地新时才下载,节省带宽和时间。
7. **BBR 备份恢复**:启用 BBR 前将当前设置备份到注释行中,禁用时精确恢复原始值。
8. **Fail2ban jail 隔离**IP 限制使用独立的 `3x-ipl` jail与系统默认 jail 分离,互不影响。

3
go.mod
View file

@ -26,11 +26,13 @@ require (
golang.org/x/sys v0.42.0
golang.org/x/text v0.35.0
google.golang.org/grpc v1.80.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
@ -47,6 +49,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect

6
go.sum
View file

@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
@ -56,6 +58,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
@ -271,6 +275,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=

View file

@ -786,18 +786,16 @@ config_after_install() {
# 全新安装:用户输入或随机生成凭据
echo -e "${yellow}设置面板凭据(输入 rd 或留空将自动生成):${plain}"
read -rp "请输入用户名" config_username
read -rp "请输入用户名 [默认 admin]" config_username
config_username="${config_username// /}"
if [[ -z "$config_username" || "$config_username" == "rd" ]]; then
config_username=$(gen_random_string 10)
echo -e "${green}已生成随机用户名:${config_username}${plain}"
config_username="admin"
fi
read -rp "请输入密码" config_password
read -rp "请输入密码 [默认 admin]" config_password
config_password="${config_password// /}"
if [[ -z "$config_password" || "$config_password" == "rd" ]]; then
config_password=$(gen_random_string 10)
echo -e "${green}已生成随机密码:${config_password}${plain}"
config_password="admin"
fi
read -rp "请输入 Web 路径(不含前导 /" config_webBasePath

127
main.go
View file

@ -46,7 +46,7 @@ func runWebServer() {
godotenv.Load()
err := database.InitDB(config.GetDBPath())
err := database.InitDB()
if err != nil {
log.Fatalf("Error initializing database: %v", err)
}
@ -131,7 +131,7 @@ func runWebServer() {
// resetSetting resets all panel settings to their default values.
func resetSetting() {
err := database.InitDB(config.GetDBPath())
err := database.InitDB()
if err != nil {
fmt.Println("Failed to initialize database:", err)
return
@ -218,7 +218,7 @@ func updateTgbotEnableSts(status bool) {
// updateTgbotSetting updates Telegram bot settings including token, chat ID, and runtime schedule.
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath())
err := database.InitDB()
if err != nil {
fmt.Println("Error initializing database:", err)
return
@ -256,7 +256,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
err := database.InitDB(config.GetDBPath())
err := database.InitDB()
if err != nil {
fmt.Println("Database initialization failed:", err)
return
@ -315,7 +315,7 @@ func updateSetting(port int, username string, password string, webBasePath strin
// updateCert updates the SSL certificate files for the panel.
func updateCert(publicKey string, privateKey string) {
err := database.InitDB(config.GetDBPath())
err := database.InitDB()
if err != nil {
fmt.Println(err)
return
@ -392,7 +392,7 @@ func GetListenIP(getListen bool) {
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
err := database.InitDB()
if err != nil {
log.Fatal(err)
}
@ -401,6 +401,44 @@ func migrateDb() {
fmt.Println("Migration done!")
}
// migrateDbBetweenDrivers migrates data between SQLite and MariaDB.
// The direction can be specified via --direction flag, otherwise it falls back to dbType from config.
func migrateDbBetweenDrivers(direction string) {
switch direction {
case "sqlite-to-mariadb":
fmt.Println("Migrating data from SQLite to MariaDB...")
if err := database.MigrateSQLiteToMariaDB(); err != nil {
log.Fatal("Migration failed: ", err)
}
fmt.Println("Migration to MariaDB completed successfully.")
case "mariadb-to-sqlite":
fmt.Println("Migrating data from MariaDB to SQLite...")
if err := database.MigrateMariaDBToSQLite(); err != nil {
log.Fatal("Migration failed: ", err)
}
fmt.Println("Migration to SQLite completed successfully.")
default:
// Fall back to inferring from dbType config
dbType := config.GetDBTypeFromJSON()
switch dbType {
case "mariadb":
fmt.Println("Migrating data from SQLite to MariaDB...")
if err := database.MigrateSQLiteToMariaDB(); err != nil {
log.Fatal("Migration failed: ", err)
}
fmt.Println("Migration to MariaDB completed successfully.")
case "sqlite":
fmt.Println("Migrating data from MariaDB to SQLite...")
if err := database.MigrateMariaDBToSQLite(); err != nil {
log.Fatal("Migration failed: ", err)
}
fmt.Println("Migration to SQLite completed successfully.")
default:
log.Fatalf("Unknown dbType: %s", dbType)
}
}
}
// main is the entry point of the 3x-ui application.
// It parses command-line arguments to run the web server, migrate database, or update settings.
func main() {
@ -447,6 +485,29 @@ func main() {
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "Set cron time for Telegram bot notifications")
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "Set chat ID for Telegram bot notifications")
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "Enable notifications via Telegram bot")
var dbTypeFlag string
var dbHost string
var dbPort string
var dbUser string
var dbPassword string
var dbName string
var showDbType bool
settingCmd.StringVar(&dbTypeFlag, "dbType", "", "Set database type (sqlite or mariadb)")
settingCmd.StringVar(&dbHost, "dbHost", "", "Set MariaDB host")
settingCmd.StringVar(&dbPort, "dbPort", "", "Set MariaDB port")
settingCmd.StringVar(&dbUser, "dbUser", "", "Set MariaDB username")
settingCmd.StringVar(&dbPassword, "dbPassword", "", "Set MariaDB password")
settingCmd.StringVar(&dbName, "dbName", "", "Set MariaDB database name")
settingCmd.BoolVar(&showDbType, "showDbType", false, "Print current database type and exit")
migrateDbCmd := flag.NewFlagSet("migrate-db", flag.ExitOnError)
var migrateDirection string
migrateDbCmd.StringVar(&migrateDirection, "direction", "", "Migration direction: sqlite-to-mariadb or mariadb-to-sqlite")
// Allow dbPassword to be passed via env var to avoid leaking it in process args
if p := os.Getenv("XUI_DB_PASSWORD"); p != "" {
dbPassword = p
}
oldUsage := flag.Usage
flag.Usage = func() {
@ -455,6 +516,7 @@ func main() {
fmt.Println("Commands:")
fmt.Println(" run run web panel")
fmt.Println(" migrate migrate form other/old x-ui")
fmt.Println(" migrate-db migrate data between SQLite and MariaDB")
fmt.Println(" setting set settings")
}
@ -474,12 +536,23 @@ func main() {
runWebServer()
case "migrate":
migrateDb()
case "migrate-db":
err := migrateDbCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
migrateDbBetweenDrivers(migrateDirection)
case "setting":
err := settingCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
if showDbType {
fmt.Println(config.GetDBTypeFromJSON())
return
}
if reset {
resetSetting()
} else {
@ -500,6 +573,48 @@ func main() {
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
if dbTypeFlag != "" {
if err := config.WriteSettingToJSON("dbType", dbTypeFlag); err != nil {
fmt.Println("Failed to set dbType:", err)
} else {
fmt.Println("dbType set to:", dbTypeFlag)
}
}
if dbHost != "" {
if err := config.WriteSettingToJSON("dbHost", dbHost); err != nil {
fmt.Println("Failed to set dbHost:", err)
} else {
fmt.Println("dbHost set to:", dbHost)
}
}
if dbPort != "" {
if err := config.WriteSettingToJSON("dbPort", dbPort); err != nil {
fmt.Println("Failed to set dbPort:", err)
} else {
fmt.Println("dbPort set to:", dbPort)
}
}
if dbUser != "" {
if err := config.WriteSettingToJSON("dbUser", dbUser); err != nil {
fmt.Println("Failed to set dbUser:", err)
} else {
fmt.Println("dbUser set to:", dbUser)
}
}
if dbPassword != "" {
if err := config.WriteSettingToJSON("dbPassword", dbPassword); err != nil {
fmt.Println("Failed to set dbPassword:", err)
} else {
fmt.Println("dbPassword set")
}
}
if dbName != "" {
if err := config.WriteSettingToJSON("dbName", dbName); err != nil {
fmt.Println("Failed to set dbName:", err)
} else {
fmt.Println("dbName set to:", dbName)
}
}
case "cert":
err := settingCmd.Parse(os.Args[2:])
if err != nil {

162
util/common/common_test.go Normal file
View file

@ -0,0 +1,162 @@
package common
import (
"errors"
"strings"
"testing"
)
func TestNewErrorf(t *testing.T) {
err := NewErrorf("invalid port: %d", 8080)
if err == nil {
t.Fatal("NewErrorf should return non-nil error")
}
expected := "invalid port: 8080"
if err.Error() != expected {
t.Errorf("NewErrorf returned %q, expected %q", err.Error(), expected)
}
}
func TestNewError(t *testing.T) {
err := NewError("something", " went wrong")
if err == nil {
t.Fatal("NewError should return non-nil error")
}
if !strings.Contains(err.Error(), "something") {
t.Errorf("NewError should contain 'something', got %q", err.Error())
}
}
func TestRecoverWithoutPanic(t *testing.T) {
recovered := Recover("")
if recovered != nil {
t.Errorf("Recover should return nil when no panic occurred, got %v", recovered)
}
}
func TestFormatTrafficBytes(t *testing.T) {
tests := []struct {
input int64
expected string
}{
{0, "0.00B"},
{1, "1.00B"},
{512, "512.00B"},
{1023, "1023.00B"},
}
for _, tt := range tests {
result := FormatTraffic(tt.input)
if result != tt.expected {
t.Errorf("FormatTraffic(%d) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestFormatTrafficKB(t *testing.T) {
tests := []struct {
input int64
expected string
}{
{1024, "1.00KB"},
{1536, "1.50KB"},
{2048, "2.00KB"},
}
for _, tt := range tests {
result := FormatTraffic(tt.input)
if result != tt.expected {
t.Errorf("FormatTraffic(%d) = %q, want %q", tt.input, result, tt.expected)
}
}
}
func TestFormatTrafficMB(t *testing.T) {
result := FormatTraffic(1048576) // 1 MB
expected := "1.00MB"
if result != expected {
t.Errorf("FormatTraffic(1048576) = %q, want %q", result, expected)
}
}
func TestFormatTrafficGB(t *testing.T) {
result := FormatTraffic(1073741824) // 1 GB
expected := "1.00GB"
if result != expected {
t.Errorf("FormatTraffic(1073741824) = %q, want %q", result, expected)
}
}
func TestFormatTrafficTB(t *testing.T) {
result := FormatTraffic(1099511627776) // 1 TB
expected := "1.00TB"
if result != expected {
t.Errorf("FormatTraffic(1099511627776) = %q, want %q", result, expected)
}
}
func TestFormatTrafficPB(t *testing.T) {
result := FormatTraffic(1125899906842624) // 1 PB
expected := "1.00PB"
if result != expected {
t.Errorf("FormatTraffic(1125899906842624) = %q, want %q", result, expected)
}
}
func TestFormatTrafficLargePB(t *testing.T) {
// Value exceeding PB should stay in PB
result := FormatTraffic(11258999068426240) // 10 PB
if !strings.HasSuffix(result, "PB") {
t.Errorf("FormatTraffic should cap at PB, got %q", result)
}
}
func TestCombineAllNil(t *testing.T) {
err := Combine(nil, nil, nil)
if err != nil {
t.Errorf("Combine(nil, nil, nil) should return nil, got %v", err)
}
}
func TestCombineNoArgs(t *testing.T) {
err := Combine()
if err != nil {
t.Errorf("Combine() should return nil, got %v", err)
}
}
func TestCombineSingleError(t *testing.T) {
input := errors.New("test error")
err := Combine(input)
if err == nil {
t.Fatal("Combine should return non-nil when an error is present")
}
if !strings.Contains(err.Error(), "test error") {
t.Errorf("Combine should contain 'test error', got %q", err.Error())
}
}
func TestCombineMultipleErrors(t *testing.T) {
err1 := errors.New("error 1")
err2 := errors.New("error 2")
combined := Combine(err1, nil, err2)
if combined == nil {
t.Fatal("Combine should return non-nil when errors are present")
}
s := combined.Error()
if !strings.Contains(s, "error 1") {
t.Errorf("Combined error should contain 'error 1', got %q", s)
}
if !strings.Contains(s, "error 2") {
t.Errorf("Combined error should contain 'error 2', got %q", s)
}
}
func TestCombineFiltersNils(t *testing.T) {
err1 := errors.New("real error")
combined := Combine(nil, err1, nil)
if combined == nil {
t.Fatal("Combine should return non-nil when at least one error is present")
}
if !strings.Contains(combined.Error(), "real error") {
t.Errorf("Combined error should contain 'real error', got %q", combined.Error())
}
}

View file

@ -0,0 +1,72 @@
package crypto
import (
"strings"
"testing"
)
func TestHashPasswordAsBcrypt(t *testing.T) {
hash, err := HashPasswordAsBcrypt("password123")
if err != nil {
t.Fatalf("HashPasswordAsBcrypt failed: %v", err)
}
if hash == "" {
t.Fatal("hash should not be empty")
}
if hash == "password123" {
t.Fatal("hash should not equal the plaintext password")
}
if !strings.HasPrefix(hash, "$2a$") && !strings.HasPrefix(hash, "$2b$") {
t.Fatalf("hash should have bcrypt prefix, got: %s", hash[:4])
}
}
func TestHashAndCheckRoundTrip(t *testing.T) {
passwords := []string{
"password123",
"",
"very-long-password-with-special-chars-!@#$%^&*()",
"unicode-密码-test",
}
for _, pw := range passwords {
hash, err := HashPasswordAsBcrypt(pw)
if err != nil {
t.Fatalf("HashPasswordAsBcrypt(%q) failed: %v", pw, err)
}
if !CheckPasswordHash(hash, pw) {
t.Errorf("CheckPasswordHash should return true for correct password %q", pw)
}
}
}
func TestCheckPasswordHashWrongPassword(t *testing.T) {
hash, err := HashPasswordAsBcrypt("correct-password")
if err != nil {
t.Fatalf("HashPasswordAsBcrypt failed: %v", err)
}
if CheckPasswordHash(hash, "wrong-password") {
t.Error("CheckPasswordHash should return false for wrong password")
}
}
func TestCheckPasswordHashInvalidHash(t *testing.T) {
if CheckPasswordHash("not-a-valid-hash", "password") {
t.Error("CheckPasswordHash should return false for invalid hash")
}
}
func TestDifferentPasswordsProduceDifferentHashes(t *testing.T) {
hash1, _ := HashPasswordAsBcrypt("password1")
hash2, _ := HashPasswordAsBcrypt("password2")
if hash1 == hash2 {
t.Error("different passwords should produce different hashes")
}
}
func TestSamePasswordProducesDifferentHashes(t *testing.T) {
hash1, _ := HashPasswordAsBcrypt("same-password")
hash2, _ := HashPasswordAsBcrypt("same-password")
if hash1 == hash2 {
t.Error("bcrypt should use different salts, producing different hashes for same password")
}
}

View file

@ -0,0 +1,67 @@
package random
import (
"testing"
)
func TestSeqLength(t *testing.T) {
lengths := []int{0, 1, 10, 32, 64, 128}
for _, n := range lengths {
s := Seq(n)
if len([]rune(s)) != n {
t.Errorf("Seq(%d) returned string of length %d", n, len([]rune(s)))
}
}
}
func TestSeqCharset(t *testing.T) {
allChars := make(map[rune]bool)
for _, r := range allSeq {
allChars[r] = true
}
s := Seq(1000)
for i, r := range s {
if !allChars[r] {
t.Errorf("Seq produced invalid character %q at index %d", r, i)
}
}
}
func TestSeqUniqueness(t *testing.T) {
// Generate several strings and check they're not all identical
seen := make(map[string]bool)
for range 10 {
seen[Seq(32)] = true
}
if len(seen) < 2 {
t.Error("Seq(32) produced identical strings across 10 calls")
}
}
func TestSeqEmpty(t *testing.T) {
s := Seq(0)
if s != "" {
t.Errorf("Seq(0) should return empty string, got %q", s)
}
}
func TestNumRange(t *testing.T) {
for _, n := range []int{1, 5, 10, 100, 1000} {
for range 100 {
r := Num(n)
if r < 0 || r >= n {
t.Errorf("Num(%d) returned %d, expected [0, %d)", n, r, n)
}
}
}
}
func TestNumOne(t *testing.T) {
for range 50 {
r := Num(1)
if r != 0 {
t.Errorf("Num(1) should always return 0, got %d", r)
}
}
}

View file

@ -719,70 +719,15 @@ class URLBuilder {
class LanguageManager {
static supportedLanguages = [
{
name: "العربية",
value: "ar-EG",
icon: "🇪🇬",
},
{
name: "English",
value: "en-US",
icon: "🇺🇸",
},
{
name: "فارسی",
value: "fa-IR",
icon: "🇮🇷",
},
{
name: "简体中文",
value: "zh-CN",
icon: "🇨🇳",
},
{
name: "繁體中文",
value: "zh-TW",
icon: "🇹🇼",
},
{
name: "日本語",
value: "ja-JP",
icon: "🇯🇵",
},
{
name: "Русский",
value: "ru-RU",
icon: "🇷🇺",
},
{
name: "Tiếng Việt",
value: "vi-VN",
icon: "🇻🇳",
},
{
name: "Español",
value: "es-ES",
icon: "🇪🇸",
},
{
name: "Indonesian",
value: "id-ID",
icon: "🇮🇩",
},
{
name: "Український",
value: "uk-UA",
icon: "🇺🇦",
},
{
name: "Türkçe",
value: "tr-TR",
icon: "🇹🇷",
},
{
name: "Português",
value: "pt-BR",
icon: "🇧🇷",
}
]
@ -793,25 +738,6 @@ class LanguageManager {
if (window.navigator) {
lang = window.navigator.language || window.navigator.userLanguage;
const simularLangs = [
["ar", this.supportedLanguages[0].value],
["fa", this.supportedLanguages[2].value],
["ja", this.supportedLanguages[5].value],
["ru", this.supportedLanguages[6].value],
["vi", this.supportedLanguages[7].value],
["es", this.supportedLanguages[8].value],
["id", this.supportedLanguages[9].value],
["uk", this.supportedLanguages[10].value],
["tr", this.supportedLanguages[11].value],
["pt", this.supportedLanguages[12].value],
]
simularLangs.forEach((pair) => {
if (lang === pair[0]) {
lang = pair[1];
}
});
if (LanguageManager.isSupportLanguage(lang)) {
CookieManager.setCookie("lang", lang, 150);
} else {

View file

@ -48,6 +48,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
g.POST("/import", a.importInbound)
g.GET("/userInfo", a.getUserInfo)
g.POST("/onlines", a.onlines)
g.POST("/lastOnline", a.lastOnline)
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
@ -454,3 +455,14 @@ func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
a.xrayService.SetToNeedRestart()
}
}
// getUserInfo returns client traffic information for the logged-in user.
func (a *InboundController) getUserInfo(c *gin.Context) {
user := session.GetLoginUser(c)
traffic, err := a.inboundService.GetClientTrafficByEmail(user.Username)
if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
return
}
jsonObj(c, traffic, nil)
}

View file

@ -1,6 +1,7 @@
package controller
import (
"errors"
"fmt"
"net/http"
"strings"
@ -8,6 +9,7 @@ import (
"time"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/web/middleware"
"github.com/mhsanaei/3x-ui/v2/web/service"
"github.com/mhsanaei/3x-ui/v2/web/session"
@ -35,6 +37,7 @@ type IndexController struct {
settingService service.SettingService
userService service.UserService
inboundService service.InboundService
tgbot service.Tgbot
}
@ -51,7 +54,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/logout", a.logout)
g.POST("/login", a.login)
g.POST("/register", a.register)
g.POST("/register", middleware.RateLimitMiddleware(5, time.Minute), a.register)
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
g.POST("/getTurnstileSiteKey", a.getTurnstileSiteKey)
}
@ -59,7 +62,12 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
func (a *IndexController) index(c *gin.Context) {
if session.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, "panel/")
user := session.GetLoginUser(c)
if user.Role == "admin" {
c.Redirect(http.StatusTemporaryRedirect, "panel/")
} else {
c.Redirect(http.StatusTemporaryRedirect, "panel/user")
}
return
}
html(c, "login.html", "pages.login.title", nil)
@ -129,6 +137,11 @@ func (a *IndexController) register(c *gin.Context) {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData"))
return
}
// Trim whitespace
form.Username = strings.TrimSpace(form.Username)
form.Password = strings.TrimSpace(form.Password)
if form.Username == "" {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyUsername"))
return
@ -137,6 +150,14 @@ func (a *IndexController) register(c *gin.Context) {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyPassword"))
return
}
if len(form.Username) < 3 || len(form.Username) > 64 {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidUsername"))
return
}
if len(form.Password) < 8 || len(form.Password) > 128 {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidPassword"))
return
}
// Verify Turnstile token if site key is configured
turnstileSecretKey, err := a.settingService.GetTurnstileSecretKey()
@ -151,10 +172,9 @@ func (a *IndexController) register(c *gin.Context) {
}
}
err = a.userService.RegisterUser(form.Username, form.Password)
err = a.userService.RegisterUser(form.Username, form.Password, &a.inboundService)
if err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "already exists") {
if errors.Is(err, service.ErrUsernameAlreadyExists) {
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.userExists"))
return
}

View file

@ -1,6 +1,10 @@
package controller
import (
"net/http"
"github.com/mhsanaei/3x-ui/v2/web/session"
"github.com/gin-gonic/gin"
)
@ -25,6 +29,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
g.Use(a.checkLogin)
g.GET("/", a.index)
g.GET("/user", a.user)
g.GET("/inbounds", a.inbounds)
g.GET("/settings", a.settings)
g.GET("/xray", a.xraySettings)
@ -33,11 +38,21 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
a.xraySettingController = NewXraySettingController(g)
}
// index renders the main panel index page.
// index renders the main panel index page. Non-admin users are redirected to the user dashboard.
func (a *XUIController) index(c *gin.Context) {
user := session.GetLoginUser(c)
if user.Role != "admin" {
c.Redirect(http.StatusTemporaryRedirect, "user")
return
}
html(c, "index.html", "pages.index.title", nil)
}
// user renders the user dashboard page.
func (a *XUIController) user(c *gin.Context) {
html(c, "user.html", "pages.user.title", nil)
}
// inbounds renders the inbounds management page.
func (a *XUIController) inbounds(c *gin.Context) {
html(c, "inbounds.html", "pages.inbounds.title", nil)

View file

@ -104,9 +104,17 @@ type AllSetting struct {
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
// Database settings
DBType string `json:"dbType" form:"dbType"`
DBHost string `json:"dbHost" form:"dbHost"`
DBPort string `json:"dbPort" form:"dbPort"`
DBUser string `json:"dbUser" form:"dbUser"`
DBPassword string `json:"-" form:"dbPassword"`
DBName string `json:"dbName" form:"dbName"`
// Registration settings
TurnstileSiteKey string `json:"turnstileSiteKey" form:"turnstileSiteKey"`
TurnstileSecretKey string `json:"turnstileSecretKey" form:"turnstileSecretKey"`
TurnstileSecretKey string `json:"-" form:"-"` // server-side only, never sent to frontend
}
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
@ -176,5 +184,27 @@ func (s *AllSetting) CheckValid() error {
return common.NewError("time location not exist:", s.TimeLocation)
}
// Validate database settings
if s.DBType != "" && s.DBType != "sqlite" && s.DBType != "mariadb" {
return common.NewError("db type must be sqlite or mariadb, got:", s.DBType)
}
if s.DBType == "mariadb" {
if s.DBHost == "" {
return common.NewError("db host is required for MariaDB")
}
if s.DBPort != "" {
port := 0
for _, c := range s.DBPort {
if c < '0' || c > '9' {
return common.NewError("db port is not a valid number:", s.DBPort)
}
port = port*10 + int(c-'0')
}
if port <= 0 || port > math.MaxUint16 {
return common.NewError("db port is not a valid port:", s.DBPort)
}
}
}
return nil
}

View file

@ -1,4 +1,5 @@
{{ template "page/head_start" .}}
<script data-cfasync="false" src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
{{ template "page/head_end" .}}
{{ template "page/body_start" .}}
@ -118,7 +119,7 @@
</a-form-item>
<a-form-item>
<div class="cf-turnstile-wrapper">
<div class="cf-turnstile" :data-sitekey="turnstileSiteKey" data-callback="onTurnstileCallback" :data-size="turnstileSize"></div>
<div class="cf-turnstile" id="turnstile-widget"></div>
</div>
</a-form-item>
<a-form-item>
@ -149,10 +150,6 @@
<script>
var turnstileToken = '';
function onTurnstileCallback(token) {
turnstileToken = token;
}
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
@ -172,7 +169,7 @@
this.twoFactorEnable = await this.getTwoFactorEnable();
this.turnstileSiteKey = await this.getTurnstileSiteKey();
if (this.turnstileSiteKey) {
this.loadTurnstileScript();
this.renderTurnstile();
}
},
computed: {
@ -214,17 +211,25 @@
this.regUser = { username: "", password: "", confirmPassword: "" };
turnstileToken = '';
if (window.turnstile) {
turnstile.reset('.cf-turnstile');
var container = document.getElementById('turnstile-widget');
if (container) {
turnstile.reset(container);
}
}
}
this.loadingStates.registerSpinning = false;
},
loadTurnstileScript() {
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
script.async = true;
script.defer = true;
document.head.appendChild(script);
renderTurnstile() {
if (window.turnstile) {
var container = document.getElementById('turnstile-widget');
if (container) {
turnstile.render(container, {
sitekey: this.turnstileSiteKey,
callback: function(token) { turnstileToken = token; },
size: this.turnstileSize,
});
}
}
},
async getTurnstileSiteKey() {
const msg = await HttpUtil.post('/getTurnstileSiteKey');

169
web/html/user.html Normal file
View file

@ -0,0 +1,169 @@
{{ template "page/head_start" .}}
{{ template "page/head_end" .}}
{{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
<transition name="list" appear>
<a-layout-content class="under min-h-0">
<div class="waves-header">
<div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
<defs>
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
</defs>
<g class="parallax">
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
</g>
</svg>
</div>
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
<a-col :xs="22" :sm="16" :md="12" :lg="10" :xl="8" :xxl="6" class="my-3rem">
<template v-if="loading">
<div class="text-center">
<a-spin size="large" />
</div>
</template>
<template v-else>
<a-card :class="themeSwitcher.currentTheme" class="user-card">
<div class="setting-section">
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
placement="bottomRight" trigger="click">
<template slot="content">
<a-space direction="vertical" :size="10">
<a-theme-switch-login></a-theme-switch-login>
<span>{{ i18n "pages.settings.language" }}</span>
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-space>
</template>
<a-button shape="circle" icon="setting"></a-button>
</a-popover>
</div>
<div class="text-center mb-24">
<a-icon type="user" style="font-size: 48px; color: #008771;" />
<h2 class="mt-8">[[ username ]]</h2>
</div>
<a-divider />
<a-descriptions :column="1" bordered size="small">
<a-descriptions-item label='{{ i18n "pages.user.username" }}'>
[[ username ]]
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.upload" }}'>
[[ traffic ? SizeFormatter.sizeFormat(traffic.up) : '-' ]]
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.download" }}'>
[[ traffic ? SizeFormatter.sizeFormat(traffic.down) : '-' ]]
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.totalTraffic" }}'>
<template v-if="traffic">
<template v-if="traffic.total > 0">
[[ SizeFormatter.sizeFormat(traffic.up + traffic.down) ]] / [[ SizeFormatter.sizeFormat(traffic.total) ]]
<a-progress :percent="traffic.total > 0 ? NumberFormatter.toFixed((traffic.up + traffic.down) / traffic.total * 100, 1) : 0" size="small" :status="((traffic.up + traffic.down) / traffic.total * 100) >= 90 ? 'exception' : 'normal'" />
</template>
<template v-else>
{{ i18n "unlimited" }}
</template>
</template>
<template v-else>-</template>
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.remained" }}'>
<template v-if="traffic && traffic.total > 0">
<span :class="{ 'text-red': (traffic.total - traffic.up - traffic.down) <= 0 }">
[[ SizeFormatter.sizeFormat(Math.max(traffic.total - traffic.up - traffic.down, 0)) ]]
</span>
</template>
<template v-else-if="traffic">{{ i18n "unlimited" }}</template>
<template v-else>-</template>
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.expiryTime" }}'>
<template v-if="traffic">
<template v-if="traffic.expiryTime > 0">
<span :class="{ 'text-red': traffic.expiryTime < Date.now() }">
[[ formatExpiryTime(traffic.expiryTime) ]]
</span>
</template>
<template v-else>
{{ i18n "unlimited" }}
</template>
</template>
<template v-else>-</template>
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.lastOnline" }}'>
<template v-if="traffic && traffic.lastOnline > 0">
[[ new Date(traffic.lastOnline * 1000).toLocaleString() ]]
</template>
<template v-else>-</template>
</a-descriptions-item>
<a-descriptions-item label='{{ i18n "pages.user.status" }}'>
<a-badge v-if="traffic" :status="traffic.enable ? 'processing' : 'default'" :text="traffic.enable ? '{{ i18n 'enabled' }}' : '{{ i18n 'disabled' }}'" />
<template v-else>-</template>
</a-descriptions-item>
</a-descriptions>
<div class="mt-24 text-center">
<a-button type="primary" icon="logout" @click="logout">
{{ i18n "menu.logout" }}
</a-button>
</div>
</a-card>
</template>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout>
{{template "page/body_scripts" .}}
{{template "component/aThemeSwitch" .}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
el: '#app',
data: {
themeSwitcher,
loading: true,
username: '',
traffic: null,
lang: '',
},
async mounted() {
this.lang = LanguageManager.getLanguage();
await this.loadUserInfo();
},
methods: {
async loadUserInfo() {
try {
const msg = await HttpUtil.get('/panel/api/inbounds/userInfo');
if (msg.success) {
this.username = msg.obj?.email || '';
this.traffic = msg.obj;
}
} catch (e) {
console.error("Failed to get user info:", e);
}
this.loading = false;
},
formatExpiryTime(timestamp) {
if (timestamp <= 0) return '{{ i18n "unlimited" }}';
const date = new Date(timestamp);
const now = Date.now();
if (timestamp < now) {
return date.toLocaleString() + ' ({{ i18n "depleted" }})';
}
const diffDays = Math.ceil((timestamp - now) / (1000 * 60 * 60 * 24));
return date.toLocaleString() + ' (' + diffDays + ' {{ i18n "day" }})';
},
logout() {
location.href = basePath + 'logout/';
},
},
});
</script>
{{ template "page/body_end" .}}

View file

@ -0,0 +1,320 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
)
func init() {
gin.SetMode(gin.TestMode)
}
func TestRedirectMiddleware_XUIToPanel(t *testing.T) {
r := gin.New()
r.Use(RedirectMiddleware("/"))
r.GET("/panel/*path", func(c *gin.Context) {
c.String(http.StatusOK, "panel")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/xui/settings", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusMovedPermanently {
t.Errorf("expected 301, got %d", w.Code)
}
loc := w.Header().Get("Location")
if loc != "/panel/settings" {
t.Errorf("expected redirect to /panel/settings, got %q", loc)
}
}
func TestRedirectMiddleware_XUIAPIToPanelAPI(t *testing.T) {
r := gin.New()
r.Use(RedirectMiddleware("/"))
r.GET("/panel/api/*path", func(c *gin.Context) {
c.String(http.StatusOK, "api")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/xui/API/inbounds", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusMovedPermanently {
t.Errorf("expected 301, got %d", w.Code)
}
loc := w.Header().Get("Location")
if loc != "/panel/api/inbounds" {
t.Errorf("expected redirect to /panel/api/inbounds, got %q", loc)
}
}
func TestRedirectMiddleware_PanelAPICase(t *testing.T) {
r := gin.New()
r.Use(RedirectMiddleware("/"))
r.GET("/panel/api/*path", func(c *gin.Context) {
c.String(http.StatusOK, "api")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/panel/API/list", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusMovedPermanently {
t.Errorf("expected 301, got %d", w.Code)
}
loc := w.Header().Get("Location")
if loc != "/panel/api/list" {
t.Errorf("expected redirect to /panel/api/list, got %q", loc)
}
}
func TestRedirectMiddleware_NoRedirect(t *testing.T) {
r := gin.New()
r.Use(RedirectMiddleware("/"))
r.GET("/panel/settings", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/panel/settings", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestRedirectMiddleware_WithBasePath(t *testing.T) {
r := gin.New()
r.Use(RedirectMiddleware("/base/"))
r.GET("/base/panel/*path", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/base/xui/settings", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusMovedPermanently {
t.Errorf("expected 301, got %d", w.Code)
}
loc := w.Header().Get("Location")
if loc != "/base/panel/settings" {
t.Errorf("expected redirect to /base/panel/settings, got %q", loc)
}
}
func TestDomainValidatorMiddleware_MatchingDomain(t *testing.T) {
r := gin.New()
r.Use(DomainValidatorMiddleware("example.com"))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Host = "example.com"
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestDomainValidatorMiddleware_MatchingDomainWithPort(t *testing.T) {
r := gin.New()
r.Use(DomainValidatorMiddleware("example.com"))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Host = "example.com:8443"
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200 for matching domain with port, got %d", w.Code)
}
}
func TestDomainValidatorMiddleware_NonMatchingDomain(t *testing.T) {
r := gin.New()
r.Use(DomainValidatorMiddleware("example.com"))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Host = "evil.com"
r.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403, got %d", w.Code)
}
}
func TestDomainValidatorMiddleware_Subdomain(t *testing.T) {
r := gin.New()
r.Use(DomainValidatorMiddleware("example.com"))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Host = "sub.example.com"
r.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403 for subdomain, got %d", w.Code)
}
}
func TestRateLimitMiddleware_FirstRequest(t *testing.T) {
r := gin.New()
r.Use(RateLimitMiddleware(5, time.Minute))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.RemoteAddr = "192.168.1.1:12345"
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200 for first request, got %d", w.Code)
}
}
func TestRateLimitMiddleware_WithinLimit(t *testing.T) {
r := gin.New()
r.Use(RateLimitMiddleware(3, time.Minute))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
for i := range 3 {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.RemoteAddr = "192.168.1.2:12345"
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("request %d: expected 200, got %d", i+1, w.Code)
}
}
}
func TestRateLimitMiddleware_ExceedsLimit(t *testing.T) {
r := gin.New()
r.Use(RateLimitMiddleware(2, time.Minute))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
// First 2 should pass
for range 2 {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.RemoteAddr = "192.168.1.3:12345"
r.ServeHTTP(w, req)
}
// 3rd should be rate limited
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.RemoteAddr = "192.168.1.3:12345"
r.ServeHTTP(w, req)
if w.Code != http.StatusTooManyRequests {
t.Errorf("expected 429, got %d", w.Code)
}
}
func TestRateLimitMiddleware_XRealIP(t *testing.T) {
r := gin.New()
r.Use(RateLimitMiddleware(2, time.Minute))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
for range 2 {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Real-IP", "10.0.0.1")
r.ServeHTTP(w, req)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Real-IP", "10.0.0.1")
r.ServeHTTP(w, req)
if w.Code != http.StatusTooManyRequests {
t.Errorf("expected 429 with X-Real-IP, got %d", w.Code)
}
}
func TestRateLimitMiddleware_XForwardedFor(t *testing.T) {
r := gin.New()
r.Use(RateLimitMiddleware(2, time.Minute))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
for range 2 {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Forwarded-For", "10.0.0.2, 10.0.0.3")
r.ServeHTTP(w, req)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Forwarded-For", "10.0.0.2, 10.0.0.3")
r.ServeHTTP(w, req)
if w.Code != http.StatusTooManyRequests {
t.Errorf("expected 429 with X-Forwarded-For, got %d", w.Code)
}
}
func TestRateLimitMiddleware_DifferentIPsIndependent(t *testing.T) {
r := gin.New()
r.Use(RateLimitMiddleware(1, time.Minute))
r.GET("/test", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
// Exhaust limit for IP 1
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Real-IP", "10.0.0.10")
r.ServeHTTP(w, req)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Real-IP", "10.0.0.10")
r.ServeHTTP(w, req)
if w.Code != http.StatusTooManyRequests {
t.Errorf("IP 1 second request should be 429, got %d", w.Code)
}
// IP 2 should still be allowed
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/test", nil)
req.Header.Set("X-Real-IP", "10.0.0.20")
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("different IP should get 200, got %d", w.Code)
}
}

View file

@ -0,0 +1,82 @@
package middleware
import (
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/mhsanaei/3x-ui/v2/web/entity"
)
type rateEntry struct {
count int
lastSeen time.Time
}
// RateLimitMiddleware returns a Gin middleware that limits requests per IP.
// maxRequests is the maximum number of requests allowed within the window.
func RateLimitMiddleware(maxRequests int, window time.Duration) gin.HandlerFunc {
var mu sync.Mutex
entries := make(map[string]*rateEntry)
// Periodically evict stale entries to prevent unbounded memory growth
go func() {
ticker := time.NewTicker(window)
defer ticker.Stop()
for range ticker.C {
mu.Lock()
cutoff := time.Now().Add(-window * 2)
for ip, e := range entries {
if e.lastSeen.Before(cutoff) {
delete(entries, ip)
}
}
mu.Unlock()
}
}()
return func(c *gin.Context) {
ip := c.GetHeader("X-Real-IP")
if ip == "" {
ip = c.GetHeader("X-Forwarded-For")
if ip != "" {
// Take the first IP from X-Forwarded-For
if idx := len(ip); idx > 0 {
for i, ch := range ip {
if ch == ',' {
ip = ip[:i]
break
}
}
}
}
}
if ip == "" {
ip = c.Request.RemoteAddr
}
mu.Lock()
now := time.Now()
e, exists := entries[ip]
if !exists || now.Sub(e.lastSeen) > window {
entries[ip] = &rateEntry{count: 1, lastSeen: now}
mu.Unlock()
c.Next()
return
}
e.lastSeen = now
e.count++
if e.count > maxRequests {
mu.Unlock()
c.JSON(http.StatusTooManyRequests, entity.Msg{
Success: false,
Msg: "Too many requests",
})
c.Abort()
return
}
mu.Unlock()
c.Next()
}
}

View file

@ -11,17 +11,20 @@ import (
// It provides backward compatibility by redirecting old '/xui' paths to new '/panel' paths,
// including API endpoints. The middleware performs permanent redirects (301) for SEO purposes.
func RedirectMiddleware(basePath string) gin.HandlerFunc {
return func(c *gin.Context) {
// Redirect from old '/xui' path to '/panel'
redirects := map[string]string{
"panel/API": "panel/api",
"xui/API": "panel/api",
"xui": "panel",
}
// Use a slice to guarantee longest-prefix-first matching order.
// A map would have nondeterministic iteration, causing "/xui/API" to
// sometimes match the shorter "/xui" rule instead.
redirects := []struct{ from, to string }{
{"panel/API", "panel/api"},
{"xui/API", "panel/api"},
{"xui", "panel"},
}
return func(c *gin.Context) {
path := c.Request.URL.Path
for from, to := range redirects {
from, to = basePath+from, basePath+to
for _, r := range redirects {
from := basePath + r.from
to := basePath + r.to
if strings.HasPrefix(path, from) {
newPath := to + path[len(from):]

View file

@ -165,8 +165,34 @@ func (s *InboundService) contains(slice []string, str string) bool {
return false
}
func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {
allEmails, err := s.getAllEmails()
// checkEmailsExistForClients 检查一批客户端中是否有重复的 email包括列表内部重复和与数据库已有 email 冲突)
// 当前未使用,保留供后续功能(如批量添加客户端时的邮箱校验)使用
// func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (string, error) {
// // 获取数据库中所有已存在的 email用于检测冲突
// allEmails, err := s.getAllEmails()
// if err != nil {
// return "", err
// }
// var emails []string // 用于记录当前批次中已遍历过的 email检测列表内部重复
// for _, client := range clients {
// if client.Email != "" {
// // 检查是否在当前批次中重复出现
// if s.contains(emails, client.Email) {
// return client.Email, nil
// }
// // 检查是否与数据库中已有 email 冲突
// if s.contains(allEmails, client.Email) {
// return client.Email, nil
// }
// emails = append(emails, client.Email)
// }
// }
// // 全部检查通过,无重复
// return "", nil
// }
func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {
clients, err := s.GetClients(inbound)
if err != nil {
return "", err
}
@ -176,37 +202,25 @@ func (s *InboundService) checkEmailsExistForClients(clients []model.Client) (str
if s.contains(emails, client.Email) {
return client.Email, nil
}
if s.contains(allEmails, client.Email) {
return client.Email, nil
}
emails = append(emails, client.Email)
}
}
return "", nil
}
func (s *InboundService) checkEmailExistForInbound(inbound *model.Inbound) (string, error) {
// checkEmailExistInInbound checks if an email already exists in a specific inbound's clients.
func (s *InboundService) checkEmailExistInInbound(inbound *model.Inbound, email string) (bool, error) {
clients, err := s.GetClients(inbound)
if err != nil {
return "", err
return false, err
}
allEmails, err := s.getAllEmails()
if err != nil {
return "", err
}
var emails []string
lowerEmail := strings.ToLower(email)
for _, client := range clients {
if client.Email != "" {
if s.contains(emails, client.Email) {
return client.Email, nil
}
if s.contains(allEmails, client.Email) {
return client.Email, nil
}
emails = append(emails, client.Email)
if strings.ToLower(client.Email) == lowerEmail {
return true, nil
}
}
return "", nil
return false, nil
}
// AddInbound creates a new inbound configuration.
@ -582,19 +596,25 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
interfaceClients[i] = cm
}
}
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return false, err
}
if existEmail != "" {
return false, common.NewError("Duplicate email:", existEmail)
}
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return false, err
}
// Check email uniqueness within this inbound only
for _, client := range clients {
if client.Email == "" {
continue
}
exists, err := s.checkEmailExistInInbound(oldInbound, client.Email)
if err != nil {
return false, err
}
if exists {
return false, common.NewError("Duplicate email in this inbound:", client.Email)
}
}
// Secure client ID
for _, client := range clients {
switch oldInbound.Protocol {
@ -818,12 +838,23 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
}
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients)
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return false, err
}
if existEmail != "" {
return false, common.NewError("Duplicate email:", existEmail)
// Check email uniqueness within this inbound only
for _, client := range clients {
if client.Email == "" {
continue
}
exists, err := s.checkEmailExistInInbound(oldInbound, client.Email)
if err != nil {
return false, err
}
if exists {
return false, common.NewError("Duplicate email in this inbound:", client.Email)
}
}
}

View file

@ -882,6 +882,9 @@ func (s *ServerService) GetConfigJson() (any, error) {
}
func (s *ServerService) GetDb() ([]byte, error) {
if config.GetDBTypeFromJSON() == "mariadb" {
return nil, common.NewError("database export is not supported when using MariaDB — use mysqldump instead")
}
// Update by manually trigger a checkpoint operation
err := database.Checkpoint()
if err != nil {
@ -904,6 +907,9 @@ func (s *ServerService) GetDb() ([]byte, error) {
}
func (s *ServerService) ImportDB(file multipart.File) error {
if config.GetDBTypeFromJSON() == "mariadb" {
return common.NewError("database import is not supported when using MariaDB — use mysql restore instead")
}
// Check if the file is a SQLite database
isValidDb, err := database.IsSQLiteDB(file)
if err != nil {
@ -1009,7 +1015,7 @@ func (s *ServerService) ImportDB(file multipart.File) error {
}
// Open & migrate new DB
if err = database.InitDB(config.GetDBPath()); err != nil {
if err = database.InitDBWithPath(config.GetDBPath()); err != nil {
if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
}

100
web/service/server_test.go Normal file
View file

@ -0,0 +1,100 @@
package service
import (
"testing"
)
func TestIsValidGeofileName_Valid(t *testing.T) {
svc := &ServerService{}
valid := []string{
"geoip.dat",
"geosite.dat",
"geoip_IR.dat",
"geoip_RU.dat",
"geosite_IR.dat",
"custom-file_v2.dat",
}
for _, name := range valid {
if !svc.IsValidGeofileName(name) {
t.Errorf("IsValidGeofileName(%q) should return true", name)
}
}
}
func TestIsValidGeofileName_PathTraversal(t *testing.T) {
svc := &ServerService{}
invalid := []string{
"../geoip.dat",
"../../etc/passwd",
"subdir/geoip.dat",
"geoip.dat/../../../etc",
"..\\geoip.dat",
}
for _, name := range invalid {
if svc.IsValidGeofileName(name) {
t.Errorf("IsValidGeofileName(%q) should return false (path traversal)", name)
}
}
}
func TestIsValidGeofileName_Empty(t *testing.T) {
svc := &ServerService{}
if svc.IsValidGeofileName("") {
t.Error("IsValidGeofileName(\"\") should return false")
}
}
func TestIsValidGeofileName_NoDatExtension(t *testing.T) {
svc := &ServerService{}
invalid := []string{
"geoip.txt",
"geosite",
"file.exe",
"script.sh",
}
for _, name := range invalid {
if svc.IsValidGeofileName(name) {
t.Errorf("IsValidGeofileName(%q) should return false (no .dat extension)", name)
}
}
}
func TestIsValidGeofileName_SpecialChars(t *testing.T) {
svc := &ServerService{}
invalid := []string{
"geoip$.dat",
"geoip!.dat",
"geoip;.dat",
"geoip .dat",
"geoip@attack.dat",
}
for _, name := range invalid {
if svc.IsValidGeofileName(name) {
t.Errorf("IsValidGeofileName(%q) should return false (special chars)", name)
}
}
}
func TestLogEntryContains(t *testing.T) {
tests := []struct {
line string
suffixes []string
want bool
}{
// The implementation checks strings.Contains(line, sfx+"]")
{"line with freedom]", []string{"freedom"}, true},
{"line with blackhole]", []string{"blackhole"}, true},
{"freedom outbound", []string{"freedom"}, false},
{"blackhole outbound", []string{"blackhole"}, false},
{"freedom outbound", []string{"blackhole"}, false},
{"some log line", []string{}, false},
{"line with freedom] and blackhole]", []string{"freedom", "blackhole"}, true},
{"line with freedom] and blackhole]", []string{"other"}, false},
}
for _, tt := range tests {
got := logEntryContains(tt.line, tt.suffixes)
if got != tt.want {
t.Errorf("logEntryContains(%q, %v) = %v, want %v", tt.line, tt.suffixes, got, tt.want)
}
}
}

View file

@ -108,6 +108,14 @@ var defaultValueMap = map[string]string{
// Registration settings
"turnstileSiteKey": "",
"turnstileSecretKey": "",
// Database settings
"dbType": "sqlite",
"dbHost": "127.0.0.1",
"dbPort": "3306",
"dbUser": "",
"dbPassword": "",
"dbName": "3xui",
}
// settingGroups defines the nested JSON structure for on-disk settings.
@ -198,6 +206,12 @@ var settingGroups = map[string]map[string]string{
"secret": "secret",
"warp": "warp",
"xrayOutboundTestUrl": "xrayOutboundTestUrl",
"dbType": "dbType",
"dbHost": "dbHost",
"dbPort": "dbPort",
"dbUser": "dbUser",
"dbPassword": "dbPassword",
"dbName": "dbName",
},
}
@ -959,6 +973,10 @@ func (s *SettingService) GetTurnstileSecretKey() (string, error) {
return s.getString("turnstileSecretKey")
}
func (s *SettingService) SetTurnstileSecretKey(value string) error {
return s.setString("turnstileSecretKey", value)
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err
@ -974,9 +992,16 @@ func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
fields := reflect_util.GetFields(t)
for _, field := range fields {
key := field.Tag.Get("json")
if key == "-" || key == "" {
continue
}
fieldV := v.FieldByName(field.Name)
settings[key] = fmt.Sprint(fieldV.Interface())
}
// DBPassword uses json:"-" to avoid leaking to frontend, handle it via form tag
if allSetting.DBPassword != "" {
settings["dbPassword"] = allSetting.DBPassword
}
return saveSettings(settings)
}
@ -1086,3 +1111,51 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
return result, nil
}
func (s *SettingService) GetDBType() (string, error) {
return s.getString("dbType")
}
func (s *SettingService) SetDBType(value string) error {
return s.setString("dbType", value)
}
func (s *SettingService) GetDBHost() (string, error) {
return s.getString("dbHost")
}
func (s *SettingService) SetDBHost(value string) error {
return s.setString("dbHost", value)
}
func (s *SettingService) GetDBPort() (string, error) {
return s.getString("dbPort")
}
func (s *SettingService) SetDBPort(value string) error {
return s.setString("dbPort", value)
}
func (s *SettingService) GetDBUser() (string, error) {
return s.getString("dbUser")
}
func (s *SettingService) SetDBUser(value string) error {
return s.setString("dbUser", value)
}
func (s *SettingService) GetDBPassword() (string, error) {
return s.getString("dbPassword")
}
func (s *SettingService) SetDBPassword(value string) error {
return s.setString("dbPassword", value)
}
func (s *SettingService) GetDBName() (string, error) {
return s.getString("dbName")
}
func (s *SettingService) SetDBName(value string) error {
return s.setString("dbName", value)
}

297
web/service/setting_test.go Normal file
View file

@ -0,0 +1,297 @@
package service
import (
"encoding/json"
"os"
"testing"
"github.com/mhsanaei/3x-ui/v2/config"
)
func setupTestSettings(t *testing.T) func() {
t.Helper()
tmpDir := t.TempDir()
t.Setenv("XUI_DB_FOLDER", tmpDir)
return func() {}
}
func TestLoadSettingsCreatesDefaults(t *testing.T) {
setupTestSettings(t)
settings, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings() error: %v", err)
}
// Should contain default values
if settings["webPort"] != "2053" {
t.Errorf("expected webPort=2053, got %s", settings["webPort"])
}
if settings["webBasePath"] != "/" {
t.Errorf("expected webBasePath=/, got %s", settings["webBasePath"])
}
// Should NOT contain xrayTemplateConfig
if _, exists := settings["xrayTemplateConfig"]; exists {
t.Error("xrayTemplateConfig should not be in JSON settings")
}
// File should exist on disk
path := config.GetSettingPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("settings file %s should have been created", path)
}
}
func TestSaveAndLoadSettings(t *testing.T) {
setupTestSettings(t)
settings := map[string]string{
"webPort": "8080",
"webListen": "0.0.0.0",
}
err := saveSettings(settings)
if err != nil {
t.Fatalf("saveSettings() error: %v", err)
}
loaded, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings() error: %v", err)
}
if loaded["webPort"] != "8080" {
t.Errorf("expected webPort=8080, got %s", loaded["webPort"])
}
if loaded["webListen"] != "0.0.0.0" {
t.Errorf("expected webListen=0.0.0.0, got %s", loaded["webListen"])
}
}
func TestSettingServiceGetString(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
// Should return default value when key not set
val, err := svc.getString("webPort")
if err != nil {
t.Fatalf("getString error: %v", err)
}
if val != "2053" {
t.Errorf("expected 2053, got %s", val)
}
}
func TestSettingServiceSetAndGetString(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
err := svc.setString("webPort", "9090")
if err != nil {
t.Fatalf("setString error: %v", err)
}
val, err := svc.getString("webPort")
if err != nil {
t.Fatalf("getString error: %v", err)
}
if val != "9090" {
t.Errorf("expected 9090, got %s", val)
}
}
func TestResetSettingsDeletesFile(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
// Create settings first
_, err := svc.getString("webPort")
if err != nil {
t.Fatalf("getString error: %v", err)
}
path := config.GetSettingPath()
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Fatal("settings file should exist before reset")
}
// Manually delete to simulate the file removal part of ResetSettings
err = os.Remove(path)
if err != nil {
t.Fatalf("remove error: %v", err)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Error("settings file should not exist after reset")
}
// Re-loading should recreate defaults
settings, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings after reset error: %v", err)
}
if settings["webPort"] != "2053" {
t.Errorf("expected default webPort=2053 after reset, got %s", settings["webPort"])
}
}
func TestSettingsFileFormat(t *testing.T) {
setupTestSettings(t)
settings, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings error: %v", err)
}
path := config.GetSettingPath()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile error: %v", err)
}
// Verify it's valid JSON
var parsed map[string]any
if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("settings file is not valid JSON: %v", err)
}
// Verify nested format: should contain group objects
for _, group := range []string{"web", "tgBot", "sub", "ldap", "other"} {
val, exists := parsed[group]
if !exists {
t.Errorf("expected group %q in nested JSON", group)
continue
}
if _, isMap := val.(map[string]any); !isMap {
t.Errorf("expected group %q to be an object, got %T", group, val)
}
}
// Verify pretty-printed (has newlines)
hasNewline := false
for _, b := range data {
if b == '\n' {
hasNewline = true
break
}
}
if !hasNewline {
t.Error("settings file should be pretty-printed with newlines")
}
// Verify round-trip: flatten nested back to flat should match loaded settings
flattened := flattenNested(parsed)
if len(flattened) != len(settings) {
t.Errorf("flattened key count %d != loaded key count %d", len(flattened), len(settings))
}
for k, v := range settings {
if fv, ok := flattened[k]; !ok {
t.Errorf("key %q missing after flatten", k)
} else if fv != v {
t.Errorf("key %q: expected %q, got %q", k, v, fv)
}
}
}
func TestLegacyFlatFormatBackwardCompat(t *testing.T) {
setupTestSettings(t)
// Write a flat JSON file (legacy format)
flat := map[string]string{
"webPort": "8080",
"webListen": "0.0.0.0",
"subEnable": "false",
"ldapHost": "ldap.example.com",
}
data, err := json.MarshalIndent(flat, "", " ")
if err != nil {
t.Fatalf("MarshalIndent error: %v", err)
}
path := config.GetSettingPath()
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatalf("WriteFile error: %v", err)
}
// loadSettings should parse it as flat and merge defaults
loaded, err := loadSettings()
if err != nil {
t.Fatalf("loadSettings error: %v", err)
}
if loaded["webPort"] != "8080" {
t.Errorf("expected webPort=8080, got %s", loaded["webPort"])
}
if loaded["webListen"] != "0.0.0.0" {
t.Errorf("expected webListen=0.0.0.0, got %s", loaded["webListen"])
}
if loaded["subEnable"] != "false" {
t.Errorf("expected subEnable=false, got %s", loaded["subEnable"])
}
if loaded["ldapHost"] != "ldap.example.com" {
t.Errorf("expected ldapHost=ldap.example.com, got %s", loaded["ldapHost"])
}
// Defaults should be merged for missing keys
if loaded["webBasePath"] != "/" {
t.Errorf("expected webBasePath=/, got %s", loaded["webBasePath"])
}
}
func TestRoundTripNestedFormat(t *testing.T) {
setupTestSettings(t)
svc := &SettingService{}
// Set some values
if err := svc.setString("webPort", "9090"); err != nil {
t.Fatalf("setString error: %v", err)
}
if err := svc.setString("tgBotEnable", "true"); err != nil {
t.Fatalf("setString error: %v", err)
}
if err := svc.setString("ldapHost", "ldap.test.com"); err != nil {
t.Fatalf("setString error: %v", err)
}
// Read back
val, err := svc.getString("webPort")
if err != nil || val != "9090" {
t.Errorf("expected webPort=9090, got %s (err: %v)", val, err)
}
val, err = svc.getString("tgBotEnable")
if err != nil || val != "true" {
t.Errorf("expected tgBotEnable=true, got %s (err: %v)", val, err)
}
val, err = svc.getString("ldapHost")
if err != nil || val != "ldap.test.com" {
t.Errorf("expected ldapHost=ldap.test.com, got %s (err: %v)", val, err)
}
// Verify on-disk format is nested
path := config.GetSettingPath()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("ReadFile error: %v", err)
}
var parsed map[string]any
if err := json.Unmarshal(data, &parsed); err != nil {
t.Fatalf("settings file is not valid JSON: %v", err)
}
if webGroup, ok := parsed["web"].(map[string]any); ok {
if port, ok := webGroup["port"].(string); !ok || port != "9090" {
t.Errorf("expected web.port=9090 in nested JSON, got %v", webGroup["port"])
}
} else {
t.Error("expected 'web' group in nested JSON")
}
if tgGroup, ok := parsed["tgBot"].(map[string]any); ok {
if enable, ok := tgGroup["enable"].(string); !ok || enable != "true" {
t.Errorf("expected tgBot.enable=true in nested JSON, got %v", tgGroup["enable"])
}
} else {
t.Error("expected 'tgBot' group in nested JSON")
}
}

View file

@ -6,10 +6,14 @@ import (
"net/http"
"net/url"
"time"
"github.com/mhsanaei/3x-ui/v2/logger"
)
const turnstileVerifyURL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
var turnstileClient = &http.Client{Timeout: 10 * time.Second}
type turnstileResponse struct {
Success bool `json:"success"`
}
@ -24,20 +28,22 @@ func VerifyTurnstile(secretKey, token, remoteIP string) bool {
form.Set("remoteip", remoteIP)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.PostForm(turnstileVerifyURL, form)
resp, err := turnstileClient.PostForm(turnstileVerifyURL, form)
if err != nil {
logger.Warning("Turnstile verification request failed (network error):", err)
return false
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
if err != nil {
logger.Warning("Turnstile verification failed to read response:", err)
return false
}
var result turnstileResponse
if err := json.Unmarshal(body, &result); err != nil {
logger.Warning("Turnstile verification failed to parse response:", err)
return false
}
return result.Success

View file

@ -1,9 +1,11 @@
package service
import (
"encoding/json"
"errors"
"strings"
"github.com/google/uuid"
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
@ -13,6 +15,9 @@ import (
"gorm.io/gorm"
)
// ErrUsernameAlreadyExists is returned when a user tries to register with a taken username.
var ErrUsernameAlreadyExists = errors.New("username already exists")
// UserService provides business logic for user management and authentication.
// It handles user creation, login, password management, and 2FA operations.
type UserService struct {
@ -127,7 +132,7 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
Error
}
func (s *UserService) RegisterUser(username string, password string) error {
func (s *UserService) RegisterUser(username string, password string, inboundService *InboundService) error {
if username == "" {
return errors.New("username can not be empty")
}
@ -141,19 +146,90 @@ func (s *UserService) RegisterUser(username string, password string) error {
}
db := database.GetDB()
user := &model.User{
Username: username,
Password: hashedPassword,
Role: "user",
}
if err := db.Create(user).Error; err != nil {
// Check for unique constraint violation
if strings.Contains(err.Error(), "UNIQUE constraint failed") || strings.Contains(err.Error(), "Duplicate") {
return errors.New("username already exists")
// Create user and add as client to all inbounds in a single transaction
return db.Transaction(func(tx *gorm.DB) error {
user := &model.User{
Username: username,
Password: hashedPassword,
Role: "user",
}
return err
}
return nil
if err := tx.Create(user).Error; err != nil {
errMsg := err.Error()
if strings.Contains(errMsg, "UNIQUE constraint failed") || strings.Contains(errMsg, "Duplicate") {
return ErrUsernameAlreadyExists
}
return err
}
// Add the new user as a disabled client to all existing inbounds
inbounds, err := inboundService.GetAllInbounds()
if err != nil {
return err
}
for _, inbound := range inbounds {
clientID := uuid.New().String()
client := model.Client{
ID: clientID,
Email: username,
Enable: false,
SubID: uuid.New().String()[:8],
Comment: "auto-added on registration",
}
// Build the client JSON entry based on protocol
clientEntry := map[string]any{
"email": client.Email,
"enable": client.Enable,
"totalGB": 0,
"expiryTime": 0,
"limitIp": 0,
"subId": client.SubID,
"comment": client.Comment,
"created_at": 0,
"updated_at": 0,
}
switch inbound.Protocol {
case "trojan":
clientEntry["password"] = clientID
case "shadowsocks":
clientEntry["password"] = clientID
default:
clientEntry["id"] = clientID
}
// Parse inbound settings and append the new client
var settings map[string]any
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
return err
}
clientsRaw, ok := settings["clients"].([]any)
if !ok {
clientsRaw = []any{}
}
clientsRaw = append(clientsRaw, clientEntry)
settings["clients"] = clientsRaw
newSettings, err := json.Marshal(settings)
if err != nil {
return err
}
inbound.Settings = string(newSettings)
// Save the updated inbound settings
if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).Update("settings", inbound.Settings).Error; err != nil {
return err
}
// Create ClientTraffic record for this inbound
if err := inboundService.AddClientStat(tx, inbound.Id, &client); err != nil {
return err
}
}
return nil
})
}
func (s *UserService) UpdateFirstUser(username string, password string) error {
@ -174,11 +250,13 @@ func (s *UserService) UpdateFirstUser(username string, password string) error {
if database.IsNotFound(err) {
user.Username = username
user.Password = hashedPassword
user.Role = "admin"
return db.Model(model.User{}).Create(user).Error
} else if err != nil {
return err
}
user.Username = username
user.Password = hashedPassword
user.Role = "admin"
return db.Save(user).Error
}

147
web/service/user_test.go Normal file
View file

@ -0,0 +1,147 @@
package service
import (
"os"
"path/filepath"
"testing"
"github.com/mhsanaei/3x-ui/v2/database"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/util/crypto"
)
func setupTestDB(t *testing.T) {
t.Helper()
tmpDir := t.TempDir()
t.Setenv("XUI_DEBUG", "")
t.Setenv("XUI_DB_FOLDER", tmpDir)
dbPath := filepath.Join(tmpDir, "test.db")
if err := database.InitDBWithPath(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
t.Cleanup(func() {
database.CloseDB()
})
}
func TestGetFirstUser(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
user, err := svc.GetFirstUser()
if err != nil {
t.Fatalf("GetFirstUser error: %v", err)
}
if user.Username != "admin" {
t.Errorf("expected username 'admin', got %q", user.Username)
}
}
func TestCheckUser_ValidCredentials(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
user, err := svc.CheckUser("admin", "admin", "")
if err != nil {
t.Fatalf("CheckUser error: %v", err)
}
if user.Username != "admin" {
t.Errorf("expected username 'admin', got %q", user.Username)
}
}
func TestCheckUser_WrongPassword(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
_, err := svc.CheckUser("admin", "wrongpassword", "")
if err == nil {
t.Error("CheckUser should fail with wrong password")
}
}
func TestCheckUser_NonExistentUser(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
_, err := svc.CheckUser("nonexistent", "password", "")
if err == nil {
t.Error("CheckUser should fail for non-existent user")
}
}
func TestUpdateFirstUser(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
err := svc.UpdateFirstUser("newadmin", "newpassword123")
if err != nil {
t.Fatalf("UpdateFirstUser error: %v", err)
}
// Verify login with new credentials
user, err := svc.CheckUser("newadmin", "newpassword123", "")
if err != nil {
t.Fatalf("CheckUser with new credentials error: %v", err)
}
if user.Username != "newadmin" {
t.Errorf("expected username 'newadmin', got %q", user.Username)
}
}
func TestUpdateFirstUser_EmptyUsername(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
err := svc.UpdateFirstUser("", "password")
if err == nil {
t.Error("UpdateFirstUser should fail with empty username")
}
}
func TestUpdateFirstUser_EmptyPassword(t *testing.T) {
setupTestDB(t)
svc := &UserService{}
err := svc.UpdateFirstUser("admin", "")
if err == nil {
t.Error("UpdateFirstUser should fail with empty password")
}
}
func TestUpdateFirstUser_CreateWhenNone(t *testing.T) {
// Use a fresh temp dir so no users table data exists
tmpDir := t.TempDir()
os.Setenv("XUI_DEBUG", "")
os.Setenv("XUI_DB_FOLDER", tmpDir)
defer func() {
os.Unsetenv("XUI_DEBUG")
os.Unsetenv("XUI_DB_FOLDER")
}()
dbPath := filepath.Join(tmpDir, "empty.db")
if err := database.InitDBWithPath(dbPath); err != nil {
t.Fatalf("InitDB failed: %v", err)
}
defer database.CloseDB()
// Delete all users to simulate empty table
database.GetDB().Where("1 = 1").Delete(&model.User{})
svc := &UserService{}
err := svc.UpdateFirstUser("firstadmin", "firstpass")
if err != nil {
t.Fatalf("UpdateFirstUser should create user when table is empty: %v", err)
}
user, err := svc.GetFirstUser()
if err != nil {
t.Fatalf("GetFirstUser error: %v", err)
}
if user.Username != "firstadmin" {
t.Errorf("expected username 'firstadmin', got %q", user.Username)
}
if !crypto.CheckPasswordHash(user.Password, "firstpass") {
t.Error("password hash should match 'firstpass'")
}
}

View file

@ -1,795 +0,0 @@
"username" = "اسم المستخدم"
"password" = "الباسورد"
"login" = "تسجيل الدخول"
"confirm" = "تأكيد"
"cancel" = "إلغاء"
"close" = "إغلاق"
"create" = "إنشاء"
"update" = "تحديث"
"copy" = "نسخ"
"copied" = "اتنسخ"
"download" = "تحميل"
"remark" = "ملاحظة"
"enable" = "مفعل"
"protocol" = "بروتوكول"
"search" = "بحث"
"filter" = "فلترة"
"loading" = "جاري التحميل..."
"second" = "ثانية"
"minute" = "دقيقة"
"hour" = "ساعة"
"day" = "يوم"
"check" = "شيك"
"indefinite" = "غير محدد"
"unlimited" = "غير محدود"
"none" = "مفيش"
"qrCode" = "كود QR"
"info" = "معلومات أكتر"
"edit" = "تعديل"
"delete" = "مسح"
"reset" = "إعادة ضبط"
"noData" = "لا توجد بيانات."
"copySuccess" = "اتنسخ بنجاح"
"sure" = "متأكد؟"
"encryption" = "تشفير"
"useIPv4ForHost" = "استخدم IPv4 للمضيف"
"transmission" = "نقل"
"host" = "المستضيف"
"path" = "مسار"
"camouflage" = "تمويه"
"status" = "الحالة"
"enabled" = "مفعل"
"disabled" = "معطل"
"depleted" = "خلص"
"depletingSoon" = "هينتهي قريب"
"offline" = "أوفلاين"
"online" = "أونلاين"
"domainName" = "اسم الدومين"
"monitor" = "المسمع IP"
"certificate" = "شهادة رقمية"
"fail" = "فشل"
"comment" = "تعليق"
"success" = "تم بنجاح"
"lastOnline" = "آخر متصل"
"getVersion" = "جيب النسخة"
"install" = "تثبيت"
"clients" = "عملاء"
"usage" = "استخدام"
"twoFactorCode" = "الكود"
"remained" = "المتبقي"
"security" = "أمان"
"secAlertTitle" = "تنبيه أمني"
"secAlertSsl" = "الاتصال ده مش آمن. ابعد عن إدخال معلومات حساسة لغاية ما تشغل TLS لحماية البيانات."
"secAlertConf" = "بعض الإعدادات معرضة لهجمات. ينصح بتعزيز بروتوكولات الأمان عشان تمنع الاختراقات المحتملة."
"secAlertSSL" = "البانل مش مؤمن. حمّل شهادة TLS لحماية البيانات."
"secAlertPanelPort" = "بورت البانل الافتراضي معرض للخطر. ياريت تغير لبورت عشوائي أو محدد."
"secAlertPanelURI" = "مسار URI الافتراضي للبانل مش آمن. ياريت تضبط مسار URI معقد."
"secAlertSubURI" = "مسار URI الافتراضي للاشتراك مش آمن. ياريت تضبط مسار URI معقد."
"secAlertSubJsonURI" = "مسار URI الافتراضي لاشتراك JSON مش آمن. ياريت تضبط مسار URI معقد."
"emptyDnsDesc" = "مفيش سيرفر DNS مضاف."
"emptyFakeDnsDesc" = "مفيش سيرفر Fake DNS مضاف."
"emptyBalancersDesc" = "مفيش موازن تحميل مضاف."
"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف."
"somethingWentWrong" = "حدث خطأ ما"
[subscription]
"title" = "معلومات الاشتراك"
"subId" = "معرّف الاشتراك"
"status" = "الحالة"
"downloaded" = "التنزيل"
"uploaded" = "الرفع"
"expiry" = "تاريخ الانتهاء"
"totalQuota" = "الحصة الإجمالية"
"individualLinks" = "روابط فردية"
"active" = "نشط"
"inactive" = "غير نشط"
"unlimited" = "غير محدود"
"noExpiry" = "بدون انتهاء"
[menu]
"theme" = "الثيم"
"dark" = "داكن"
"ultraDark" = "داكن جدًا"
"dashboard" = "نظرة عامة"
"inbounds" = "الإدخالات"
"settings" = "إعدادات البانل"
"xray" = "إعدادات Xray"
"logout" = "تسجيل خروج"
"link" = "إدارة"
[pages.login]
"hello" = "أهلا"
"title" = "أهلاً وسهلاً"
"loginAgain" = "انتهت صلاحية الجلسة، سجل دخول تاني"
[pages.login.toasts]
"invalidFormData" = "تنسيق البيانات المدخلة مش صحيح."
"emptyUsername" = "اسم المستخدم مطلوب"
"emptyPassword" = "الباسورد مطلوب"
"wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح."
"successLogin" = "لقد تم تسجيل الدخول إلى حسابك بنجاح."
"successRegister" = "تم التسجيل بنجاح، يرجى تسجيل الدخول."
"userExists" = "اسم المستخدم موجود بالفعل"
"errorRegister" = "فشل التسجيل"
[pages.index]
"title" = "نظرة عامة"
"cpu" = "المعالج"
"logicalProcessors" = "المعالجات المنطقية"
"frequency" = "التردد"
"swap" = "Swap"
"storage" = "تخزين"
"memory" = "رام"
"threads" = "خيوط المعالجة"
"xrayStatus" = "Xray"
"stopXray" = "إيقاف"
"restartXray" = "إعادة تشغيل"
"xraySwitch" = "النسخة"
"xraySwitchClick" = "اختار النسخة اللي عايز تتحول لها."
"xraySwitchClickDesk" = "اختار بحذر، النسخ القديمة ممكن ما تتوافقش مع الإعدادات الحالية."
"xrayStatusUnknown" = "مش معروف"
"xrayStatusRunning" = "شغالة"
"xrayStatusStop" = "متوقفة"
"xrayStatusError" = "فيها غلطة"
"xrayErrorPopoverTitle" = "حصل خطأ أثناء تشغيل Xray"
"operationHours" = "مدة التشغيل"
"systemLoad" = "تحميل النظام"
"systemLoadDesc" = "متوسط تحميل النظام في الدقائق 1, 5, و15"
"connectionCount" = "إحصائيات الاتصال"
"ipAddresses" = "عناوين IP"
"toggleIpVisibility" = "بدل إظهار IP"
"overallSpeed" = "السرعة الكلية"
"upload" = "رفع"
"download" = "تنزيل"
"totalData" = "إجمالي البيانات"
"sent" = "مرسل"
"received" = "مستقبل"
"documentation" = "التوثيق"
"xraySwitchVersionDialog" = "هل تريد حقًا تغيير إصدار Xray؟"
"xraySwitchVersionDialogDesc" = "سيؤدي هذا إلى تغيير إصدار Xray إلى #version#."
"xraySwitchVersionPopover" = "تم تحديث Xray بنجاح"
"geofileUpdateDialog" = "هل تريد حقًا تحديث ملف الجغرافيا؟"
"geofileUpdateDialogDesc" = "سيؤدي هذا إلى تحديث ملف #filename#."
"geofilesUpdateDialogDesc" = "سيؤدي هذا إلى تحديث كافة الملفات."
"geofilesUpdateAll" = "تحديث الكل"
"geofileUpdatePopover" = "تم تحديث ملف الجغرافيا بنجاح"
"dontRefresh" = "التثبيت شغال، متعملش Refresh للصفحة"
"logs" = "السجلات"
"config" = "الإعدادات"
"backup" = "نسخة احتياطية"
"backupTitle" = "نسخة احتياطية واسترجاع قاعدة البيانات"
"exportDatabase" = "اخزن نسخة"
"exportDatabaseDesc" = "اضغط عشان تحمل ملف .db يحتوي على نسخة احتياطية لقاعدة البيانات الحالية على جهازك."
"importDatabase" = "استرجاع"
"importDatabaseDesc" = "اضغط عشان تختار وتحمل ملف .db من جهازك لاسترجاع قاعدة البيانات من نسخة احتياطية."
"importDatabaseSuccess" = "تم استيراد قاعدة البيانات بنجاح"
"importDatabaseError" = "حدث خطأ أثناء استيراد قاعدة البيانات"
"readDatabaseError" = "حدث خطأ أثناء قراءة قاعدة البيانات"
"getDatabaseError" = "حدث خطأ أثناء استرجاع قاعدة البيانات"
"getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
[pages.inbounds]
"allTimeTraffic" = "إجمالي حركة المرور"
"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت"
"title" = "الإدخالات"
"totalDownUp" = "إجمالي المرسل/المستقبل"
"totalUsage" = "إجمالي الاستخدام"
"inboundCount" = "عدد الإدخالات"
"operate" = "القائمة"
"enable" = "مفعل"
"remark" = "ملاحظة"
"protocol" = "بروتوكول"
"port" = "بورت"
"portMap" = "خريطة البورت"
"traffic" = "الترافيك"
"details" = "تفاصيل"
"transportConfig" = "نقل"
"expireDate" = "المدة"
"createdAt" = "تاريخ الإنشاء"
"updatedAt" = "تاريخ التحديث"
"resetTraffic" = "إعادة ضبط الترافيك"
"addInbound" = "أضف إدخال"
"generalActions" = "إجراءات عامة"
"autoRefresh" = "تحديث تلقائي"
"autoRefreshInterval" = "الفاصل"
"modifyInbound" = "تعديل الإدخال"
"deleteInbound" = "حذف الإدخال"
"deleteInboundContent" = "متأكد إنك عايز تحذف الإدخال؟"
"deleteClient" = "حذف العميل"
"deleteClientContent" = "متأكد إنك عايز تحذف العميل؟"
"resetTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك؟"
"copyLink" = "انسخ الرابط"
"address" = "العنوان"
"network" = "الشبكة"
"destinationPort" = "بورت الوجهة"
"targetAddress" = "عنوان الهدف"
"monitorDesc" = "سيبها فاضية لو عايز تستمع على كل الـ IPs"
"meansNoLimit" = "= غير محدود. (الوحدة: جيجابايت)"
"totalFlow" = "إجمالي التدفق"
"leaveBlankToNeverExpire" = "سيبها فاضية عشان ماتنتهيش"
"noRecommendKeepDefault" = "ننصح باستخدام الافتراضي"
"certificatePath" = "مسار الملف"
"certificateContent" = "محتوى الملف"
"publicKey" = "المفتاح العام"
"privatekey" = "المفتاح الخاص"
"clickOnQRcode" = "اضغط على كود QR للنسخ"
"client" = "عميل"
"export" = "تصدير كل الروابط"
"clone" = "استنساخ"
"cloneInbound" = "استنساخ الإدخال"
"cloneInboundContent" = "كل إعدادات الإدخال ده، غير البورت، IP الاستماع، والعملاء، هتتطبق على الاستنساخ."
"cloneInboundOk" = "استنساخ"
"resetAllTraffic" = "إعادة ضبط ترافيك كل الإدخالات"
"resetAllTrafficTitle" = "إعادة ضبط ترافيك كل الإدخالات"
"resetAllTrafficContent" = "متأكد إنك عايز تعيد ضبط الترافيك لكل الإدخالات؟"
"resetInboundClientTraffics" = "إعادة ضبط ترافيك العملاء"
"resetInboundClientTrafficTitle" = "إعادة ضبط ترافيك العملاء"
"resetInboundClientTrafficContent" = "متأكد إنك عايز تعيد ضبط ترافيك عملاء الإدخال ده؟"
"resetAllClientTraffics" = "إعادة ضبط ترافيك كل العملاء"
"resetAllClientTrafficTitle" = "إعادة ضبط ترافيك كل العملاء"
"resetAllClientTrafficContent" = "متأكد إنك عايز تعيد ضبط ترافيك كل العملاء؟"
"delDepletedClients" = "حذف العملاء اللي خلصت"
"delDepletedClientsTitle" = "حذف العملاء اللي خلصت"
"delDepletedClientsContent" = "متأكد إنك عايز تحذف كل العملاء اللي خلصت؟"
"email" = "الإيميل"
"emailDesc" = "ادخل إيميل فريد."
"IPLimit" = "تحديد IP"
"IPLimitDesc" = "بيعطل الإدخال لو العدد زاد عن القيمة المحددة. (0 = تعطيل)"
"IPLimitlog" = "سجل IP"
"IPLimitlogDesc" = "سجل تاريخ الـ IPs. (عشان تفعل الإدخال بعد التعطيل، امسح السجل)"
"IPLimitlogclear" = "امسح السجل"
"setDefaultCert" = "استخدم شهادة البانل"
"telegramDesc" = "ادخل ID شات Telegram. (استخدم '/id' في البوت) أو (@userinfobot)"
"subscriptionDesc" = "عشان تلاقي رابط الاشتراك، ادخل على 'التفاصيل'. وكمان ممكن تستخدم نفس الاسم لعدة عملاء."
"info" = "معلومات"
"same" = "نفسه"
"inboundData" = "بيانات الإدخال"
"exportInbound" = "تصدير الإدخال"
"import" = "استيراد"
"importInbound" = "استيراد إدخال"
"periodicTrafficResetTitle" = "إعادة تعيين حركة المرور"
"periodicTrafficResetDesc" = "إعادة تعيين عداد حركة المرور تلقائيًا في فترات محددة"
"lastReset" = "آخر إعادة تعيين"
[pages.client]
"add" = "أضف عميل"
"edit" = "تعديل عميل"
"submitAdd" = "أضف العميل"
"submitEdit" = "احفظ التعديلات"
"clientCount" = "عدد العملاء"
"bulk" = "إضافة بالجملة"
"method" = "طريقة"
"first" = "أول واحد"
"last" = "آخر واحد"
"prefix" = "بادئة"
"postfix" = "لاحقة"
"delayedStart" = "ابدأ بعد أول استخدام"
"expireDays" = "المدة"
"days" = "يوم/أيام"
"renew" = "تجديد تلقائي"
"renewDesc" = "تجديد تلقائي بعد انتهاء الصلاحية. (0 = تعطيل)(الوحدة: يوم)"
[pages.inbounds.periodicTrafficReset]
"never" = "أبداً"
"daily" = "يومياً"
"weekly" = "أسبوعياً"
"monthly" = "شهرياً"
[pages.inbounds.toasts]
"obtain" = "تم الحصول عليه"
"updateSuccess" = "تم التحديث بنجاح"
"logCleanSuccess" = "تم مسح السجل"
"inboundsUpdateSuccess" = "تم تحديث الواردات بنجاح"
"inboundUpdateSuccess" = "تم تحديث الوارد بنجاح"
"inboundCreateSuccess" = "تم إنشاء الوارد بنجاح"
"inboundDeleteSuccess" = "تم حذف الوارد بنجاح"
"inboundClientAddSuccess" = "تمت إضافة عميل(عملاء) وارد"
"inboundClientDeleteSuccess" = "تم حذف عميل وارد"
"inboundClientUpdateSuccess" = "تم تحديث عميل وارد"
"delDepletedClientsSuccess" = "تم حذف جميع العملاء المستنفذين"
"resetAllClientTrafficSuccess" = "تم إعادة تعيين كل حركة المرور من العميل"
"resetAllTrafficSuccess" = "تم إعادة تعيين كل حركة المرور"
"resetInboundClientTrafficSuccess" = "تم إعادة تعيين حركة المرور"
"trafficGetError" = "خطأ في الحصول على حركات المرور"
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
"getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc."
[pages.inbounds.stream.general]
"request" = "طلب"
"response" = "رد"
"name" = "اسم"
"value" = "قيمة"
[pages.inbounds.stream.tcp]
"version" = "نسخة"
"method" = "طريقة"
"path" = "مسار"
"status" = "الحالة"
"statusDescription" = "وصف الحالة"
"requestHeader" = "رأس الطلب"
"responseHeader" = "رأس الرد"
[pages.settings]
"title" = "إعدادات البانل"
"save" = "حفظ"
"infoDesc" = "كل تغيير هتعمله هنا لازم يتخزن. ياريت تعيد تشغيل البانل عشان التعديلات تتفعل."
"restartPanel" = "إعادة تشغيل البانل"
"restartPanelDesc" = "متأكد إنك عايز تعيد تشغيل البانل؟ لو ماقدرتش تدخل بعد إعادة التشغيل، شوف سجل البانل على السيرفر."
"restartPanelSuccess" = "تم إعادة تشغيل اللوحة بنجاح"
"actions" = "إجراءات"
"resetDefaultConfig" = "استرجاع الافتراضي"
"panelSettings" = "عام"
"securitySettings" = "المصادقة"
"TGBotSettings" = "بوت Telegram"
"panelListeningIP" = "IP الاستماع"
"panelListeningIPDesc" = "عنوان IP للبانل. (سيبه فاضي عشان يستمع على كل الـ IPs)"
"panelListeningDomain" = "دومين الاستماع"
"panelListeningDomainDesc" = "اسم الدومين للبانل. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)"
"panelPort" = "بورت الاستماع"
"panelPortDesc" = "رقم البورت للبانل. (لازم يكون بورت فاضي)"
"publicKeyPath" = "مسار المفتاح العام"
"publicKeyPathDesc" = "مسار ملف المفتاح العام للبانل. (يبدأ بـ '/')"
"privateKeyPath" = "مسار المفتاح الخاص"
"privateKeyPathDesc" = "مسار ملف المفتاح الخاص للبانل. (يبدأ بـ '/')"
"panelUrlPath" = "مسار URI"
"panelUrlPathDesc" = "مسار URI للبانل. (يبدأ بـ '/' وبينتهي بـ '/')"
"pageSize" = "حجم الصفحة"
"pageSizeDesc" = "حدد حجم الصفحة لجدول الإدخالات. (0 = تعطيل)"
"remarkModel" = "نموذج الملاحظة وحرف الفصل"
"datepicker" = "نوع التقويم"
"datepickerPlaceholder" = "اختار التاريخ"
"datepickerDescription" = "المهام المجدولة هتشتغل بناءً على التقويم ده."
"sampleRemark" = "مثال للملاحظة"
"oldUsername" = "اسم المستخدم الحالي"
"currentPassword" = "الباسورد الحالي"
"newUsername" = "اسم المستخدم الجديد"
"newPassword" = "الباسورد الجديد"
"telegramBotEnable" = "تفعيل بوت Telegram"
"telegramBotEnableDesc" = "يفعل بوت Telegram."
"telegramToken" = "توكن Telegram"
"telegramTokenDesc" = "توكن البوت اللي جبت من '@BotFather'."
"telegramProxy" = "بروكسي SOCKS"
"telegramProxyDesc" = "يفعل بروكسي SOCKS5 للاتصال بـ Telegram. (اضبط الإعدادات حسب الدليل)"
"telegramAPIServer" = "سيرفر Telegram API"
"telegramAPIServerDesc" = "سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي."
"telegramChatId" = "ID شات الأدمن"
"telegramChatIdDesc" = "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من @userinfobot) أو (استخدم '/id' في البوت)"
"telegramNotifyTime" = "وقت الإشعار"
"telegramNotifyTimeDesc" = "وقت إشعار البوت للتقارير الدورية. (استخدم صيغة وقت crontab)"
"tgNotifyBackup" = "نسخة احتياطية لقاعدة البيانات"
"tgNotifyBackupDesc" = "ابعت ملف النسخة الاحتياطية لقاعدة البيانات مع التقرير."
"tgNotifyLogin" = "إشعار بتسجيل الدخول"
"tgNotifyLoginDesc" = "استقبل إشعار بكل محاولة تسجيل دخول للبانل مع اسم المستخدم، الـ IP، والوقت."
"sessionMaxAge" = "مدة الجلسة"
"sessionMaxAgeDesc" = "المدة اللي تفضل فيها مسجل دخول. (الوحدة: دقيقة)"
"expireTimeDiff" = "تنبيه بتاريخ الانتهاء"
"expireTimeDiffDesc" = "استقبل تنبيه قبل ما توصل لتاريخ الانتهاء بالمدة المحددة. (الوحدة: يوم)"
"trafficDiff" = "تنبيه حد الترافيك"
"trafficDiffDesc" = "استقبل تنبيه عند وصول الترافيك للحد المحدد. (الوحدة: جيجابايت)"
"tgNotifyCpu" = "تنبيه حمل المعالج"
"tgNotifyCpuDesc" = "استقبل تنبيه لو حمل المعالج عدى الحد المحدد. (الوحدة: %)"
"timeZone" = "المنطقة الزمنية"
"timeZoneDesc" = "المهام المجدولة هتشتغل بناءً على المنطقة الزمنية دي."
"subSettings" = "الاشتراك"
"subEnable" = "تفعيل خدمة الاشتراك"
"subEnableDesc" = "يفعل خدمة الاشتراك."
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
"subTitle" = "عنوان الاشتراك"
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
"subSupportUrl" = "رابط الدعم"
"subSupportUrlDesc" = "رابط الدعم الفني المعروض في عميل VPN"
"subProfileUrl" = "رابط الملف الشخصي"
"subProfileUrlDesc" = "رابط لموقعك الإلكتروني يظهر في عميل VPN"
"subAnnounce" = "إعلان"
"subAnnounceDesc" = "نص الإعلان المعروض في عميل VPN"
"subEnableRouting" = "تفعيل التوجيه"
"subEnableRoutingDesc" = "إعداد عام لتمكين التوجيه (Routing) في عميل VPN. (فقط لـ Happ)"
"subRoutingRules" = "قواعد التوجيه"
"subRoutingRulesDesc" = "قواعد التوجيه العامة لعميل VPN. (فقط لـ Happ)"
"subListen" = "IP الاستماع"
"subListenDesc" = "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)"
"subPort" = "بورت الاستماع"
"subPortDesc" = "رقم البورت لخدمة الاشتراك. (لازم يكون بورت فاضي)"
"subCertPath" = "مسار المفتاح العام"
"subCertPathDesc" = "مسار ملف المفتاح العام لخدمة الاشتراك. (يبدأ بـ '/')"
"subKeyPath" = "مسار المفتاح الخاص"
"subKeyPathDesc" = "مسار ملف المفتاح الخاص لخدمة الاشتراك. (يبدأ بـ '/')"
"subPath" = "مسار URI"
"subPathDesc" = "مسار URI لخدمة الاشتراك. (يبدأ بـ '/' وبينتهي بـ '/')"
"subDomain" = "دومين الاستماع"
"subDomainDesc" = "اسم الدومين لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الدومينات والـ IPs)"
"subUpdates" = "فترات التحديث"
"subUpdatesDesc" = "فترات تحديث رابط الاشتراك في تطبيقات العملاء. (الوحدة: ساعة)"
"subEncrypt" = "تشفير"
"subEncryptDesc" = "المحتوى اللي هيترجع من خدمة الاشتراك هيكون مشفر بـ Base64."
"subShowInfo" = "اظهر معلومات الاستخدام"
"subShowInfoDesc" = "هيظهر الترافيك المتبقي والتاريخ في تطبيقات العملاء."
"subURI" = "مسار البروكسي العكسي"
"subURIDesc" = "مسار URI لرابط الاشتراك عشان تستخدمه ورا البروكسي."
"externalTrafficInformEnable" = "تنبيه الترافيك الخارجي"
"externalTrafficInformEnableDesc" = "يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك."
"externalTrafficInformURI" = "مسار تنبيه الترافيك الخارجي"
"externalTrafficInformURIDesc" = "تحديثات الترافيك هتتبعت للمسار ده."
"fragment" = "تجزئة"
"fragmentDesc" = "يفعل تجزئة لحزمة TLS hello."
"fragmentSett" = "إعدادات التجزئة"
"noisesDesc" = "يفعل التشويش."
"noisesSett" = "إعدادات التشويش"
"mux" = "MUX"
"muxDesc" = "ينقل أكثر من تيار بيانات مستقل خلال تيار بيانات واحد قائم."
"muxSett" = "إعدادات MUX"
"direct" = "اتصال مباشر"
"directDesc" = "ينشئ اتصال مباشر مع الدومينات أو نطاقات IP لدولة معينة."
"notifications" = "الإشعارات"
"certs" = "الشهادات"
"externalTraffic" = "الترافيك الخارجي"
"dateAndTime" = "التاريخ والوقت"
"proxyAndServer" = "البروكسي والسيرفر"
"intervals" = "الفترات"
"information" = "المعلومات"
"language" = "اللغة"
"telegramBotLanguage" = "لغة بوت Telegram"
[pages.xray]
"title" = "إعدادات Xray"
"save" = "احفظ"
"restart" = "أعد تشغيل Xray"
"restartSuccess" = "تم إعادة تشغيل Xray بنجاح"
"stopSuccess" = "تم إيقاف Xray بنجاح"
"restartError" = "حدث خطأ أثناء إعادة تشغيل Xray."
"stopError" = "حدث خطأ أثناء إيقاف Xray."
"basicTemplate" = "أساسي"
"advancedTemplate" = "متقدم"
"generalConfigs" = "إعدادات عامة"
"generalConfigsDesc" = "الخيارات دي هتحدد التعديلات العامة."
"logConfigs" = "السجلات"
"logConfigsDesc" = "السجلات ممكن تأثر على كفاءة السيرفر. ننصح بتفعيلها بحكمة لما تكون محتاجها."
"blockConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على بروتوكولات ومواقع محددة."
"basicRouting" = "توجيه أساسي"
"blockConnectionsConfigsDesc" = "الخيارات دي هتحجب الترافيك بناءً على الدولة المطلوبة."
"directConnectionsConfigsDesc" = "الاتصال المباشر بيضمن إن الترافيك المعين مايمرش من سيرفر تاني."
"blockips" = "حظر IPs"
"blockdomains" = "حظر دومينات"
"directips" = "اتصالات مباشرة لـ IPs"
"directdomains" = "اتصالات مباشرة للدومينات"
"ipv4Routing" = "توجيه IPv4"
"ipv4RoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر IPv4."
"warpRouting" = "توجيه WARP"
"warpRoutingDesc" = "الخيارات دي هتوجه الترافيك بناءً على وجهة معينة عبر WARP."
"Template" = "قالب إعدادات Xray المتقدم"
"TemplateDesc" = "ملف إعدادات Xray النهائي هيتولد بناءً على القالب ده."
"FreedomStrategy" = "استراتيجية بروتوكول الحرية"
"FreedomStrategyDesc" = "اختار استراتيجية المخرجات للشبكة في بروتوكول الحرية."
"RoutingStrategy" = "استراتيجية التوجيه العامة"
"RoutingStrategyDesc" = "حدد استراتيجية التوجيه الإجمالية لحل كل الطلبات."
"outboundTestUrl" = "رابط اختبار المخرج"
"outboundTestUrlDesc" = "الرابط المستخدم عند اختبار اتصال المخرج"
"Torrent" = "حظر بروتوكول التورنت"
"Inbounds" = "الإدخالات"
"InboundsDesc" = "قبول العملاء المعينين."
"Outbounds" = "المخرجات"
"Balancers" = "موازنات التحميل"
"OutboundsDesc" = "حدد مسار الترافيك الصادر."
"Routings" = "قواعد التوجيه"
"RoutingsDesc" = "أولوية كل قاعدة مهمة جداً!"
"completeTemplate" = "الكل"
"logLevel" = "مستوى السجلات"
"logLevelDesc" = "مستوى السجل الخاص بالأخطاء، اللي بيوضح المعلومات المطلوبة للتسجيل."
"accessLog" = "سجل الوصول"
"accessLogDesc" = "مسار ملف سجل الوصول. القيمة الخاصة 'none' بتعطل سجل الوصول."
"errorLog" = "سجل الأخطاء"
"errorLogDesc" = "مسار ملف سجل الأخطاء. القيمة الخاصة 'none' بتعطل سجل الأخطاء."
"dnsLog" = "سجل DNS"
"dnsLogDesc" = "لو هتسجل استعلامات DNS."
"maskAddress" = "إخفاء العنوان"
"maskAddressDesc" = "إخفاء عنوان الـ IP؛ لو مفعل، هيستبدل تلقائياً عنوان IP اللي بيظهر في السجل."
"statistics" = "إحصائيات"
"statsInboundUplink" = "إحصائيات رفع الإدخال"
"statsInboundUplinkDesc" = "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من الإدخالات."
"statsInboundDownlink" = "إحصائيات تنزيل الإدخال"
"statsInboundDownlinkDesc" = "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من الإدخالات."
"statsOutboundUplink" = "إحصائيات رفع المخرجات"
"statsOutboundUplinkDesc" = "تفعيل جمع الإحصائيات لترافيك الرفع لكل بروكسي من المخرجات."
"statsOutboundDownlink" = "إحصائيات تنزيل المخرجات"
"statsOutboundDownlinkDesc" = "تفعيل جمع الإحصائيات لترافيك التنزيل لكل بروكسي من المخرجات."
[pages.xray.rules]
"first" = "أول"
"last" = "آخر"
"up" = "فوق"
"down" = "تحت"
"source" = "المصدر"
"dest" = "الوجهة"
"inbound" = "إدخال"
"outbound" = "مخرج"
"balancer" = "موازن"
"info" = "معلومات"
"add" = "أضف قاعدة"
"edit" = "عدل القاعدة"
"useComma" = "عناصر مفصولة بفواصل"
[pages.xray.outbound]
"addOutbound" = "أضف مخرج"
"addReverse" = "أضف عكسي"
"editOutbound" = "عدل المخرج"
"editReverse" = "عدل العكسي"
"tag" = "تاج"
"tagDesc" = "تاج فريد"
"address" = "العنوان"
"reverse" = "عكسي"
"domain" = "دومين"
"type" = "النوع"
"bridge" = "جسر"
"portal" = "بوابة"
"link" = "رابط"
"intercon" = "تواصل"
"settings" = "إعدادات"
"accountInfo" = "معلومات الحساب"
"outboundStatus" = "حالة المخرج"
"sendThrough" = "أرسل من خلال"
"test" = "اختبار"
"testResult" = "نتيجة الاختبار"
"testing" = "جاري اختبار الاتصال..."
"testSuccess" = "الاختبار ناجح"
"testFailed" = "فشل الاختبار"
"testError" = "فشل اختبار المخرج"
[pages.xray.balancer]
"addBalancer" = "أضف موازن تحميل"
"editBalancer" = "عدل موازن التحميل"
"balancerStrategy" = "استراتيجية الموازن"
"balancerSelectors" = "المحددات"
"tag" = "تاج"
"tagDesc" = "تاج فريد"
"balancerDesc" = "ماينفعش تستخدم balancerTag و outboundTag مع بعض. لو اتستخدموا مع بعض، outboundTag هو اللي هيشتغل."
[pages.xray.wireguard]
"secretKey" = "المفتاح السري"
"publicKey" = "المفتاح العام"
"allowedIPs" = "عناوين IP المسموح بها"
"endpoint" = "النهاية"
"psk" = "المفتاح المشترك"
"domainStrategy" = "استراتيجية الدومين"
[pages.xray.tun]
"nameDesc" = "اسم واجهة TUN. القيمة الافتراضية هي 'xray0'"
"mtuDesc" = "وحدة النقل الأقصى. الحد الأقصى لحجم حزم البيانات. القيمة الافتراضية هي 1500"
"userLevel" = "مستوى المستخدم"
"userLevelDesc" = "ستستخدم جميع الاتصالات المُرسلة عبر هذا الإدخال مستوى المستخدم هذا. القيمة الافتراضية هي 0"
[pages.xray.dns]
"enable" = "فعل DNS"
"enableDesc" = "فعل سيرفر DNS المدمج"
"tag" = "تاج إدخال DNS"
"tagDesc" = "التاج ده هيبقى متاح كإدخال في قواعد التوجيه."
"clientIp" = "IP العميل"
"clientIpDesc" = "بيحدد موقع العميل خلال استعلامات DNS"
"disableCache" = "تعطيل الكاش"
"disableCacheDesc" = "بيعطل تخزين نتائج DNS مؤقتاً"
"disableFallback" = "تعطيل النسخ الاحتياطي"
"disableFallbackDesc" = "بيعطل استعلامات DNS الاحتياطية"
"disableFallbackIfMatch" = "تعطيل النسخ الاحتياطي عند التطابق"
"disableFallbackIfMatchDesc" = "بيعطل استعلامات DNS الاحتياطية لما يتحقق تطابق مع قائمة الدومينات"
"enableParallelQuery" = "تفعيل الاستعلام المتوازي"
"enableParallelQueryDesc" = "تفعيل استعلامات DNS المتوازية لعدة خوادم لحل أسرع"
"strategy" = "استراتيجية الاستعلام"
"strategyDesc" = "الاستراتيجية العامة لحل أسماء الدومين"
"add" = "أضف سيرفر"
"edit" = "عدل السيرفر"
"domains" = "الدومينات"
"expectIPs" = "العناوين المتوقعة"
"unexpectIPs" = "عناوين IP غير متوقعة"
"useSystemHosts" = "استخدام ملف Hosts الخاص بالنظام"
"useSystemHostsDesc" = "استخدام ملف hosts من نظام مثبت"
"usePreset" = "استخدام النموذج"
"dnsPresetTitle" = "قوالب DNS"
"dnsPresetFamily" = "العائلي"
[pages.xray.fakedns]
"add" = "أضف Fake DNS"
"edit" = "عدل Fake DNS"
"ipPool" = "نطاق IP Pool"
"poolSize" = "حجم المجموعة"
[pages.settings.security]
"admin" = "بيانات الأدمن"
"twoFactor" = "المصادقة الثنائية"
"twoFactorEnable" = "تفعيل المصادقة الثنائية"
"twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان."
"twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية"
"twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية"
"twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:"
"twoFactorModalFirstStep" = "1. امسح رمز QR هذا في تطبيق المصادقة أو انسخ الرمز الموجود بجانب رمز QR والصقه في التطبيق"
"twoFactorModalSecondStep" = "2. أدخل الرمز من التطبيق"
"twoFactorModalRemoveStep" = "أدخل الرمز من التطبيق لإزالة المصادقة الثنائية."
"twoFactorModalChangeCredentialsTitle" = "تغيير بيانات الاعتماد"
"twoFactorModalChangeCredentialsStep" = "أدخل الرمز من التطبيق لتغيير بيانات اعتماد المسؤول."
"twoFactorModalSetSuccess" = "تم إنشاء المصادقة الثنائية بنجاح"
"twoFactorModalDeleteSuccess" = "تم حذف المصادقة الثنائية بنجاح"
"twoFactorModalError" = "رمز خاطئ"
[pages.settings.toasts]
"modifySettings" = "تم تغيير المعلمات."
"getSettings" = "حدث خطأ أثناء استرداد المعلمات."
"modifyUserError" = "حدث خطأ أثناء تغيير بيانات اعتماد المسؤول."
"modifyUser" = "لقد قمت بتغيير بيانات اعتماد المسؤول بنجاح."
"originalUserPassIncorrect" = "اسم المستخدم أو الباسورد الحالي غير صحيح"
"userPassMustBeNotEmpty" = "اسم المستخدم والباسورد الجديدين فاضيين"
"getOutboundTrafficError" = "خطأ في الحصول على حركات المرور الصادرة"
"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
[tgbot]
"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
"noResult" = "❗ لا يوجد نتائج!"
"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
"wentWrong" = "❌ حدث خطأ ما!"
"noIpRecord" = "❗ لا يوجد سجل IP!"
"noInbounds" = "❗ لم يتم العثور على أي وارد!"
"unlimited" = "♾ غير محدود (إعادة تعيين)"
"add" = "إضافة"
"month" = "شهر"
"months" = "أشهر"
"day" = "يوم"
"days" = "أيام"
"hours" = "ساعات"
"minutes" = "دقائق"
"unknown" = "غير معروف"
"inbounds" = "الواردات"
"clients" = "العملاء"
"offline" = "🔴 غير متصل"
"online" = "🟢 متصل"
[tgbot.commands]
"unknown" = "❗ أمر مش معروف."
"pleaseChoose" = "👇 من فضلك اختار:\r\n"
"help" = "🤖 أهلا بيك في البوت! البوت ده معمول عشان يديك بيانات معينة من البانل ويسمحلك بالتعديلات."
"start" = "👋 أهلا <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 أهلا بيك في بوت إدارة <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ البوت شغال!"
"usage" = "❗ من فضلك ادخل نص للتبحث عنه!"
"getID" = "🆔 الـ ID بتاعك: <code>{{ .ID }}</code>"
"helpAdminCommands" = "عشان تعيد تشغيل Xray Core:\r\n<code>/restart</code>\r\n\r\nعشان تدور على إيميل عميل:\r\n<code>/usage [Email]</code>\r\n\r\nعشان تدور على إدخالات (مع إحصائيات العملاء):\r\n<code>/inbound [Remark]</code>\r\n\r\nID شات Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "عشان تدور على الإحصائيات، استخدم الأمر ده:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nID شات Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ العملية نجحت!"
"restartFailed" = "❗ حصل خطأ في العملية.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core مش شغال."
"startDesc" = "عرض القائمة الرئيسية"
"helpDesc" = "مساعدة البوت"
"statusDesc" = "التحقق من حالة البوت"
"idDesc" = "عرض معرف Telegram الخاص بك"
[tgbot.messages]
"cpuThreshold" = "🔴 حمل المعالج {{ .Percent }}% عدى الحد المسموح ({{ .Threshold }}%)"
"selectUserFailed" = "❌ حصل خطأ في اختيار المستخدم!"
"userSaved" = "✅ حفظت بيانات مستخدم Telegram."
"loginSuccess" = "✅ تسجيل الدخول للبانل تم بنجاح.\r\n"
"loginFailed" = "❗️فشل محاولة تسجيل الدخول للبانل.\r\n"
"2faFailed" = "فشل 2FA"
"report" = "🕰 التقارير المجدولة: {{ .RunTime }}\r\n"
"datetime" = "⏰ التاريخ والوقت: {{ .DateTime }}\r\n"
"hostname" = "💻 السيرفر: {{ .Hostname }}\r\n"
"version" = "🚀 نسخة 3X-UI: {{ .Version }}\r\n"
"xrayVersion" = "📡 نسخة Xray: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 عناوين IP:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ وقت التشغيل: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 تحميل النظام: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 الرام: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 الترافيك: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " الحالة: {{ .State }}\r\n"
"username" = "👤 اسم المستخدم: {{ .Username }}\r\n"
"password" = "👤 الباسورد: {{ .Password }}\r\n"
"time" = "⏰ الوقت: {{ .Time }}\r\n"
"inbound" = "📍 الإدخال: {{ .Remark }}\r\n"
"port" = "🔌 البورت: {{ .Port }}\r\n"
"expire" = "📅 تاريخ الانتهاء: {{ .Time }}\r\n"
"expireIn" = "📅 هيخلص بعد: {{ .Time }}\r\n"
"active" = "💡 مفعل: {{ .Enable }}\r\n"
"enabled" = "🚨 مفعل: {{ .Enable }}\r\n"
"online" = "🌐 حالة الاتصال: {{ .Status }}\r\n"
"lastOnline" = "🔙 آخر متصل: {{ .Time }}\r\n"
"email" = "📧 الإيميل: {{ .Email }}\r\n"
"upload" = "🔼 رفع: ↑{{ .Upload }}\r\n"
"download" = "🔽 تنزيل: ↓{{ .Download }}\r\n"
"total" = "📊 الإجمالي: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 مستخدم Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 نفذ {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 عدد النفاذ لـ {{ .Type }}:\r\n"
"onlinesCount" = "🌐 العملاء الأونلاين: {{ .Count }}\r\n"
"disabled" = "🛑 معطل: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 هينتهي قريب: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 وقت النسخة الاحتياطية: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 اتحدّث في: {{ .Time }}\r\n\r\n"
"yes" = "✅ أيوه"
"no" = "❌ لأ"
"received_id" = "🔑📥 الـ ID اتحدث."
"received_password" = "🔑📥 الباسورد اتحدث."
"received_email" = "📧📥 الإيميل اتحدث."
"received_comment" = "💬📥 التعليق اتحدث."
"id_prompt" = "🔑 الـ ID الافتراضي: {{ .ClientId }}\n\nادخل الـ ID بتاعك."
"pass_prompt" = "🔑 الباسورد الافتراضي: {{ .ClientPassword }}\n\nادخل الباسورد بتاعك."
"email_prompt" = "📧 الإيميل الافتراضي: {{ .ClientEmail }}\n\nادخل الإيميل بتاعك."
"comment_prompt" = "💬 التعليق الافتراضي: {{ .ClientComment }}\n\nادخل تعليقك."
"inbound_client_data_id" = "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 المعرف: {{ .ClientId }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!"
"inbound_client_data_pass" = "🔄 الدخول: {{ .InboundRemark }}\n\n🔑 كلمة المرور: {{ .ClientPass }}\n📧 البريد الإلكتروني: {{ .ClientEmail }}\n📊 الترافيك: {{ .ClientTraffic }}\n📅 تاريخ الانتهاء: {{ .ClientExp }}\n🌐 حدّ IP: {{ .IpLimit }}\n💬 تعليق: {{ .ClientComment }}\n\nدلوقتي تقدر تضيف العميل على الدخول!"
"cancel" = "❌ العملية اتلغت! \n\nممكن تبدأ من /start في أي وقت. 🔄"
"error_add_client" = "⚠️ حصل خطأ:\n\n {{ .error }}"
"using_default_value" = "تمام، هشيل على القيمة الافتراضية. 😊"
"incorrect_input" = "المدخلات مش صحيحة.\nالكلمات لازم تكون متصلة من غير فراغات.\nمثال صحيح: aaaaaa\nمثال غلط: aaa aaa 🚫"
"AreYouSure" = "إنت متأكد؟ 🤔"
"SuccessResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ✅ تم بنجاح"
"FailedResetTraffic" = "📧 البريد الإلكتروني: {{ .ClientEmail }}\n🏁 النتيجة: ❌ فشل \n\n🛠 الخطأ: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 عملية إعادة ضبط الترافيك خلصت لكل العملاء."
[tgbot.buttons]
"closeKeyboard" = "❌ اقفل الكيبورد"
"cancel" = "❌ إلغاء"
"cancelReset" = "❌ إلغاء إعادة الضبط"
"cancelIpLimit" = "❌ إلغاء حد الـ IP"
"confirmResetTraffic" = "✅ تأكيد إعادة ضبط الترافيك؟"
"confirmClearIps" = "✅ تأكيد مسح الـ IPs؟"
"confirmRemoveTGUser" = "✅ تأكيد حذف مستخدم Telegram؟"
"confirmToggle" = "✅ تأكيد تفعيل/تعطيل المستخدم؟"
"dbBackup" = "احصل على نسخة DB"
"serverUsage" = "استخدام السيرفر"
"getInbounds" = "احصل على الإدخالات"
"depleteSoon" = "هينتهي قريب"
"clientUsage" = "استخدام العميل"
"onlines" = "العملاء الأونلاين"
"commands" = "الأوامر"
"refresh" = "🔄 تجديد"
"clearIPs" = "❌ مسح الـ IPs"
"removeTGUser" = "❌ حذف مستخدم Telegram"
"selectTGUser" = "👤 اختار مستخدم Telegram"
"selectOneTGUser" = "👤 اختار مستخدم Telegram:"
"resetTraffic" = "📈 إعادة ضبط الترافيك"
"resetExpire" = "📅 تغيير تاريخ الانتهاء"
"ipLog" = "🔢 سجل الـ IP"
"ipLimit" = "🔢 حد الـ IP"
"setTGUser" = "👤 ضبط مستخدم Telegram"
"toggle" = "🔘 تفعيل / تعطيل"
"custom" = "🔢 مخصص"
"confirmNumber" = "✅ تأكيد: {{ .Num }}"
"confirmNumberAdd" = "✅ تأكيد إضافة: {{ .Num }}"
"limitTraffic" = "🚧 حد الترافيك"
"getBanLogs" = "احصل على سجلات الحظر"
"allClients" = "كل العملاء"
"addClient" = "إضافة عميل"
"submitDisable" = "إرسال كمعطّل ☑️"
"submitEnable" = "إرسال كمفعّل ✅"
"use_default" = "🏷️ استخدام الإعدادات الافتراضية"
"change_id" = "⚙️🔑 المعرّف"
"change_password" = "⚙️🔑 كلمة السر"
"change_email" = "⚙️📧 البريد الإلكتروني"
"change_comment" = "⚙️💬 تعليق"
"ResetAllTraffics" = "إعادة ضبط جميع الترافيك"
"SortedTrafficUsageReport" = "تقرير استخدام الترافيك المرتب"
[tgbot.answers]
"successfulOperation" = "✅ العملية نجحت!"
"errorOperation" = "❗ حصل خطأ في العملية."
"getInboundsFailed" = "❌ فشل الحصول على الإدخالات."
"getClientsFailed" = "❌ فشل الحصول على العملاء."
"canceled" = "❌ {{ .Email }}: العملية اتلغت."
"clientRefreshSuccess" = "✅ {{ .Email }}: العميل اتحدث بنجاح."
"IpRefreshSuccess" = "✅ {{ .Email }}: الـ IPs اتحدثت بنجاح."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: مستخدم Telegram اتحدث بنجاح."
"resetTrafficSuccess" = "✅ {{ .Email }}: الترافيك اتظبط بنجاح."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: حد الترافيك اتسجل بنجاح."
"expireResetSuccess" = "✅ {{ .Email }}: أيام الانتهاء اتظبطت بنجاح."
"resetIpSuccess" = "✅ {{ .Email }}: حد الـ IP ({{ .Count }}) اتسجل بنجاح."
"clearIpSuccess" = "✅ {{ .Email }}: الـ IPs اتمسحت بنجاح."
"getIpLog" = "✅ {{ .Email }}: سجل الـ IP اتجاب."
"getUserInfo" = "✅ {{ .Email }}: بيانات مستخدم Telegram اتجاب."
"removedTGUserSuccess" = "✅ {{ .Email }}: مستخدم Telegram اتحذف بنجاح."
"enableSuccess" = "✅ {{ .Email }}: اتفعل بنجاح."
"disableSuccess" = "✅ {{ .Email }}: اتعطل بنجاح."
"askToAddUserId" = "مافيش إعدادات ليك!\r\nاطلب من الأدمن يضيف الـ Telegram ChatID الخاص بيك في إعداداتك.\r\n\r\nالـ ChatID بتاعك: <code>{{ .TgUserID }}</code>"
"chooseClient" = "اختار عميل للإدخال {{ .Inbound }}"
"chooseInbound" = "اختار الإدخال"

View file

@ -116,6 +116,8 @@
"successRegister" = "Registration successful, please log in."
"userExists" = "Username already exists"
"errorRegister" = "Registration failed"
"invalidUsername" = "Username must be 3-64 characters"
"invalidPassword" = "Password must be 8-128 characters"
[pages.index]
"title" = "Overview"
@ -315,6 +317,17 @@
"requestHeader" = "Request Header"
"responseHeader" = "Response Header"
[pages.user]
"title" = "User Dashboard"
"username" = "Username"
"upload" = "Upload"
"download" = "Download"
"totalTraffic" = "Total Traffic"
"expiryTime" = "Expiry Time"
"lastOnline" = "Last Online"
"remained" = "Remained"
"status" = "Status"
[pages.settings]
"title" = "Panel Settings"
"save" = "Save"

View file

@ -1,795 +0,0 @@
"username" = "Nombre de Usuario"
"password" = "Contraseña"
"login" = "Acceder"
"confirm" = "Confirmar"
"cancel" = "Cancelar"
"close" = "Cerrar"
"create" = "Crear"
"update" = "Actualizar"
"copy" = "Copiar"
"copied" = "Copiado"
"download" = "Descargar"
"remark" = "Notas"
"enable" = "Habilitar"
"protocol" = "Protocolo"
"search" = "Buscar"
"filter" = "Filtrar"
"loading" = "Cargando..."
"second" = "Segundo"
"minute" = "Minuto"
"hour" = "Hora"
"day" = "Día"
"check" = "Verificar"
"indefinite" = "Indefinido"
"unlimited" = "Ilimitado"
"none" = "None"
"qrCode" = "Código QR"
"info" = "Más Información"
"edit" = "Editar"
"delete" = "Eliminar"
"reset" = "Restablecer"
"noData" = "Sin datos"
"copySuccess" = "Copiado exitosamente"
"sure" = "Seguro"
"encryption" = "Encriptación"
"useIPv4ForHost" = "Usar IPv4 para el host"
"transmission" = "Transmisión"
"host" = "Host"
"path" = "Path"
"camouflage" = "Camuflaje"
"status" = "Estado"
"enabled" = "Habilitado"
"disabled" = "Deshabilitado"
"depleted" = "Agotado"
"depletingSoon" = "Agotándose"
"offline" = "fuera de línea"
"online" = "en línea"
"domainName" = "Nombre de dominio"
"monitor" = "Listening IP"
"certificate" = "Certificado Digital"
"fail" = "Falló"
"comment" = "Comentario"
"success" = "Éxito"
"lastOnline" = "Última conexión"
"getVersion" = "Obtener versión"
"install" = "Instalar"
"clients" = "Clientes"
"usage" = "Uso"
"twoFactorCode" = "Código"
"remained" = "Restante"
"security" = "Seguridad"
"secAlertTitle" = "Alerta de Seguridad"
"secAlertSsl" = "Esta conexión no es segura. Por favor, evite ingresar información sensible hasta que se active TLS para la protección de datos."
"secAlertConf" = "Ciertas configuraciones son vulnerables a ataques. Se recomienda reforzar los protocolos de seguridad para prevenir posibles violaciones."
"secAlertSSL" = "El panel carece de una conexión segura. Por favor, instale un certificado TLS para la protección de datos."
"secAlertPanelPort" = "El puerto predeterminado del panel es vulnerable. Por favor, configure un puerto aleatorio o específico."
"secAlertPanelURI" = "La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja."
"secAlertSubURI" = "La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja."
"secAlertSubJsonURI" = "La ruta URI JSON predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja."
"emptyDnsDesc" = "No hay servidores DNS añadidos."
"emptyFakeDnsDesc" = "No hay servidores Fake DNS añadidos."
"emptyBalancersDesc" = "No hay balanceadores añadidos."
"emptyReverseDesc" = "No hay proxies inversos añadidos."
"somethingWentWrong" = "Algo salió mal"
[subscription]
"title" = "Información de suscripción"
"subId" = "ID de suscripción"
"status" = "Estado"
"downloaded" = "Descargado"
"uploaded" = "Subido"
"expiry" = "Caducidad"
"totalQuota" = "Cuota total"
"individualLinks" = "Enlaces individuales"
"active" = "Activo"
"inactive" = "Inactivo"
"unlimited" = "Ilimitado"
"noExpiry" = "Sin caducidad"
[menu]
"theme" = "Tema"
"dark" = "Oscuro"
"ultraDark" = "Ultra Oscuro"
"dashboard" = "Estado del Sistema"
"inbounds" = "Entradas"
"settings" = "Configuraciones"
"xray" = "Ajustes Xray"
"logout" = "Cerrar Sesión"
"link" = "Gestionar"
[pages.login]
"hello" = "Hola"
"title" = "Bienvenido"
"loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente."
[pages.login.toasts]
"invalidFormData" = "El formato de los datos de entrada es inválido."
"emptyUsername" = "Por favor ingresa el nombre de usuario."
"emptyPassword" = "Por favor ingresa la contraseña."
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
"successLogin" = "Has iniciado sesión en tu cuenta correctamente."
"successRegister" = "Registro exitoso, por favor inicia sesión."
"userExists" = "El nombre de usuario ya existe"
"errorRegister" = "Error en el registro"
[pages.index]
"title" = "Estado del Sistema"
"cpu" = "CPU"
"logicalProcessors" = "Procesadores lógicos"
"frequency" = "Frecuencia"
"swap" = "Memoria Virtual"
"storage" = "Almacenamiento"
"memory" = "RAM"
"threads" = "Hilos"
"xrayStatus" = "Xray"
"stopXray" = "Detener"
"restartXray" = "Reiniciar"
"xraySwitch" = "Versión"
"xraySwitchClick" = "Elige la versión a la que deseas cambiar."
"xraySwitchClickDesk" = "Elige sabiamente, ya que las versiones anteriores pueden no ser compatibles con las configuraciones actuales."
"xrayStatusUnknown" = "Desconocido"
"xrayStatusRunning" = "En ejecución"
"xrayStatusStop" = "Detenido"
"xrayStatusError" = "Error"
"xrayErrorPopoverTitle" = "Se produjo un error al ejecutar Xray"
"operationHours" = "Tiempo de Funcionamiento"
"systemLoad" = "Carga del Sistema"
"systemLoadDesc" = "promedio de carga del sistema en los últimos 1, 5 y 15 minutos"
"connectionCount" = "Número de Conexiones"
"ipAddresses" = "Direcciones IP"
"toggleIpVisibility" = "Alternar visibilidad de la IP"
"overallSpeed" = "Velocidad general"
"upload" = "Subida"
"download" = "Descarga"
"totalData" = "Datos totales"
"sent" = "Enviado"
"received" = "Recibido"
"documentation" = "Documentación"
"xraySwitchVersionDialog" = "¿Realmente deseas cambiar la versión de Xray?"
"xraySwitchVersionDialogDesc" = "Esto cambiará la versión de Xray a #version#."
"xraySwitchVersionPopover" = "Xray se actualizó correctamente"
"geofileUpdateDialog" = "¿Realmente deseas actualizar el geofichero?"
"geofileUpdateDialogDesc" = "Esto actualizará el archivo #filename#."
"geofilesUpdateDialogDesc" = "Esto actualizará todos los archivos."
"geofilesUpdateAll" = "Actualizar todo"
"geofileUpdatePopover" = "Geofichero actualizado correctamente"
"dontRefresh" = "La instalación está en progreso, por favor no actualices esta página."
"logs" = "Registros"
"config" = "Configuración"
"backup" = "Сopia de Seguridad"
"backupTitle" = "Copia de Seguridad y Restauración de la Base de Datos"
"exportDatabase" = "Copia de seguridad"
"exportDatabaseDesc" = "Haz clic para descargar un archivo .db que contiene una copia de seguridad de tu base de datos actual en tu dispositivo."
"importDatabase" = "Restaurar"
"importDatabaseDesc" = "Haz clic para seleccionar y cargar un archivo .db desde tu dispositivo para restaurar tu base de datos desde una copia de seguridad."
"importDatabaseSuccess" = "La base de datos se ha importado correctamente"
"importDatabaseError" = "Ocurrió un error al importar la base de datos"
"readDatabaseError" = "Ocurrió un error al leer la base de datos"
"getDatabaseError" = "Ocurrió un error al obtener la base de datos"
"getConfigError" = "Ocurrió un error al obtener el archivo de configuración"
[pages.inbounds]
"allTimeTraffic" = "Tráfico Total"
"allTimeTrafficUsage" = "Uso de datos histórico"
"title" = "Entradas"
"totalDownUp" = "Subidas/Descargas Totales"
"totalUsage" = "Uso Total"
"inboundCount" = "Número de Entradas"
"operate" = "Menú"
"enable" = "Habilitar"
"remark" = "Notas"
"protocol" = "Protocolo"
"port" = "Puerto"
"portMap" = "Puertos de Destino"
"traffic" = "Tráfico"
"details" = "Detalles"
"transportConfig" = "Transporte"
"expireDate" = "Fecha de Expiración"
"createdAt" = "Creado"
"updatedAt" = "Actualizado"
"resetTraffic" = "Restablecer Tráfico"
"addInbound" = "Agregar Entrada"
"generalActions" = "Acciones Generales"
"autoRefresh" = "Auto-actualizar"
"autoRefreshInterval" = "Intervalo"
"modifyInbound" = "Modificar Entrada"
"deleteInbound" = "Eliminar Entrada"
"deleteInboundContent" = "¿Confirmar eliminación de entrada?"
"deleteClient" = "Eliminar cliente"
"deleteClientContent" = "¿Está seguro de que desea eliminar el cliente?"
"resetTrafficContent" = "¿Confirmar restablecimiento de tráfico?"
"copyLink" = "Copiar Enlace"
"address" = "Dirección"
"network" = "Red"
"destinationPort" = "Puerto de Destino"
"targetAddress" = "Dirección de Destino"
"monitorDesc" = "Dejar en blanco por defecto"
"meansNoLimit" = " = illimitata. (unidad: GB)"
"totalFlow" = "Flujo Total"
"leaveBlankToNeverExpire" = "Dejar en Blanco para Nunca Expirar"
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
"certificatePath" = "Ruta Cert"
"certificateContent" = "Datos Cert"
"publicKey" = "Clave Pública"
"privatekey" = "Clave Privada"
"clickOnQRcode" = "Haz clic en el Código QR para Copiar"
"client" = "Cliente"
"export" = "Exportar Enlaces"
"clone" = "Clonar"
"cloneInbound" = "Clonar Entradas"
"cloneInboundContent" = "Se aplicarán todas las configuraciones de esta entrada, excepto el Puerto, la IP de Escucha y los Clientes, al clon."
"cloneInboundOk" = "Clonar"
"resetAllTraffic" = "Restablecer Tráfico de Todas las Entradas"
"resetAllTrafficTitle" = "Restablecer tráfico de todas las entradas"
"resetAllTrafficContent" = "¿Estás seguro de que deseas restablecer el tráfico de todas las entradas?"
"resetInboundClientTraffics" = "Restablecer Tráfico de Clientes"
"resetInboundClientTrafficTitle" = "Restablecer todo el tráfico de clientes"
"resetInboundClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para los clientes de esta entrada?"
"resetAllClientTraffics" = "Restablecer Tráfico de Todos los Clientes"
"resetAllClientTrafficTitle" = "Restablecer todo el tráfico de clientes"
"resetAllClientTrafficContent" = "¿Estás seguro de que deseas restablecer todo el tráfico para todos los clientes?"
"delDepletedClients" = "Eliminar Clientes Agotados"
"delDepletedClientsTitle" = "Eliminar clientes agotados"
"delDepletedClientsContent" = "¿Estás seguro de que deseas eliminar todos los clientes agotados?"
"email" = "Email"
"emailDesc" = "Por favor proporciona una dirección de correo electrónico única."
"IPLimit" = "Límite de IP"
"IPLimitDesc" = "Desactiva la entrada si la cantidad supera el valor ingresado (ingresa 0 para desactivar el límite de IP)."
"IPLimitlog" = "Registro de IP"
"IPLimitlogDesc" = "Registro de historial de IPs (antes de habilitar la entrada después de que haya sido desactivada por el límite de IP, debes borrar el registro)."
"IPLimitlogclear" = "Limpiar el Registro"
"setDefaultCert" = "Establecer certificado desde el panel"
"telegramDesc" = "Por favor, proporciona el ID de Chat de Telegram. (usa el comando '/id' en el bot) o (@userinfobot)"
"subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones."
"info" = "Info"
"same" = "misma"
"inboundData" = "Datos de entrada"
"exportInbound" = "Exportación entrante"
"import" = "Importar"
"importInbound" = "Importar un entrante"
"periodicTrafficResetTitle" = "Reset de Tráfico"
"periodicTrafficResetDesc" = "Reiniciar automáticamente el contador de tráfico en intervalos especificados"
"lastReset" = "Último reinicio"
[pages.client]
"add" = "Agregar Cliente"
"edit" = "Editar Cliente"
"submitAdd" = "Agregar Cliente"
"submitEdit" = "Guardar Cambios"
"clientCount" = "Número de Clientes"
"bulk" = "Agregar en Lote"
"method" = "Método"
"first" = "Primero"
"last" = "Último"
"prefix" = "Prefijo"
"postfix" = "Sufijo"
"delayedStart" = "Iniciar después del primer uso"
"expireDays" = "Duración"
"days" = "Día(s)"
"renew" = "Renovación automática"
"renewDesc" = "Renovación automática después de la expiración. (0 = desactivar) (unidad: día)"
[pages.inbounds.periodicTrafficReset]
"never" = "Nunca"
"daily" = "Diariamente"
"weekly" = "Semanalmente"
"monthly" = "Mensualmente"
[pages.inbounds.toasts]
"obtain" = "Recibir"
"updateSuccess" = "La actualización fue exitosa"
"logCleanSuccess" = "El registro ha sido limpiado"
"inboundsUpdateSuccess" = "Entradas actualizadas correctamente"
"inboundUpdateSuccess" = "Entrada actualizada correctamente"
"inboundCreateSuccess" = "Entrada creada correctamente"
"inboundDeleteSuccess" = "Entrada eliminada correctamente"
"inboundClientAddSuccess" = "Cliente(s) de entrada añadido(s)"
"inboundClientDeleteSuccess" = "Cliente de entrada eliminado"
"inboundClientUpdateSuccess" = "Cliente de entrada actualizado"
"delDepletedClientsSuccess" = "Todos los clientes con tráfico agotado fueron eliminados"
"resetAllClientTrafficSuccess" = "Todo el tráfico del cliente ha sido reiniciado"
"resetAllTrafficSuccess" = "Todo el tráfico ha sido reiniciado"
"resetInboundClientTrafficSuccess" = "El tráfico ha sido reiniciado"
"trafficGetError" = "Error al obtener los tráficos"
"getNewX25519CertError" = "Error al obtener el certificado X25519."
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
"getNewVlessEncError" = "Error al obtener el certificado VlessEnc."
[pages.inbounds.stream.general]
"request" = "Pedido"
"response" = "Respuesta"
"name" = "Nombre"
"value" = "Valor"
[pages.inbounds.stream.tcp]
"version" = "Versión"
"method" = "Método"
"path" = "Camino"
"status" = "Estado"
"statusDescription" = "Descripción de la Situación"
"requestHeader" = "Encabezado de solicitud"
"responseHeader" = "Encabezado de respuesta"
[pages.settings]
"title" = "Configuraciones"
"save" = "Guardar"
"infoDesc" = "Cada cambio realizado aquí debe ser guardado. Por favor, reinicie el panel para aplicar los cambios."
"restartPanel" = "Reiniciar Panel"
"restartPanelDesc" = "¿Está seguro de que desea reiniciar el panel? Haga clic en Aceptar para reiniciar después de 3 segundos. Si no puede acceder al panel después de reiniciar, por favor, consulte la información de registro del panel en el servidor."
"restartPanelSuccess" = "El panel se reinició correctamente"
"actions" = "Acciones"
"resetDefaultConfig" = "Restablecer a Configuración Predeterminada"
"panelSettings" = "Configuraciones del Panel"
"securitySettings" = "Configuraciones de Seguridad"
"TGBotSettings" = "Configuraciones de Bot de Telegram"
"panelListeningIP" = "IP de Escucha del Panel"
"panelListeningIPDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"panelListeningDomain" = "Dominio de Escucha del Panel"
"panelListeningDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs."
"panelPort" = "Puerto del Panel"
"panelPortDesc" = "El puerto utilizado para mostrar este panel."
"publicKeyPath" = "Ruta del Archivo de Clave Pública del Certificado del Panel"
"publicKeyPathDesc" = "Complete con una ruta absoluta que comience con."
"privateKeyPath" = "Ruta del Archivo de Clave Privada del Certificado del Panel"
"privateKeyPathDesc" = "Complete con una ruta absoluta que comience con."
"panelUrlPath" = "Ruta Raíz de la URL del Panel"
"panelUrlPathDesc" = "Debe empezar con '/' y terminar con."
"pageSize" = "Tamaño de paginación"
"pageSizeDesc" = "Defina el tamaño de página para la tabla de entradas. Establezca 0 para desactivar"
"remarkModel" = "Modelo de observación y carácter de separación"
"datepicker" = "selector de fechas"
"datepickerPlaceholder" = "Seleccionar fecha"
"datepickerDescription" = "El tipo de calendario selector especifica la fecha de vencimiento"
"sampleRemark" = "Observación de muestra"
"oldUsername" = "Nombre de Usuario Actual"
"currentPassword" = "Contraseña Actual"
"newUsername" = "Nuevo Nombre de Usuario"
"newPassword" = "Nueva Contraseña"
"telegramBotEnable" = "Habilitar bot de Telegram"
"telegramBotEnableDesc" = "Conéctese a las funciones de este panel a través del bot de Telegram."
"telegramToken" = "Token de Telegram"
"telegramTokenDesc" = "Debe obtener el token del administrador de bots de Telegram @botfather."
"telegramProxy" = "Socks5 Proxy"
"telegramProxyDesc" = "Si necesita el proxy Socks5 para conectarse a Telegram. Ajuste su configuración según la guía."
"telegramAPIServer" = "API Server de Telegram"
"telegramAPIServerDesc" = "El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado."
"telegramChatId" = "IDs de Chat de Telegram para Administradores"
"telegramChatIdDesc" = "IDs de Chat múltiples separados por comas. Use @userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat."
"telegramNotifyTime" = "Hora de Notificación del Bot de Telegram"
"telegramNotifyTimeDesc" = "Usar el formato de tiempo de Crontab."
"tgNotifyBackup" = "Respaldo de Base de Datos"
"tgNotifyBackupDesc" = "Incluir archivo de respaldo de base de datos con notificación de informe."
"tgNotifyLogin" = "Notificación de Inicio de Sesión"
"tgNotifyLoginDesc" = "Muestra el nombre de usuario, dirección IP y hora cuando alguien intenta iniciar sesión en su panel."
"sessionMaxAge" = "Edad Máxima de Sesión"
"sessionMaxAgeDesc" = "La duración de una sesión de inicio de sesión (unidad: minutos)."
"expireTimeDiff" = "Umbral de Expiración para Notificación"
"expireTimeDiffDesc" = "Reciba notificaciones sobre la expiración de la cuenta antes del umbral (unidad: días)."
"trafficDiff" = "Umbral de Tráfico para Notificación"
"trafficDiffDesc" = "Reciba notificaciones sobre el agotamiento del tráfico antes de alcanzar el umbral (unidad: GB)."
"tgNotifyCpu" = "Umbral de Alerta de Porcentaje de CPU"
"tgNotifyCpuDesc" = "Reciba notificaciones si el uso de la CPU supera este umbral (unidad: %)."
"timeZone" = "Zona Horaria"
"timeZoneDesc" = "Las tareas programadas se ejecutan de acuerdo con la hora en esta zona horaria."
"subSettings" = "Suscripción"
"subEnable" = "Habilitar Servicio"
"subEnableDesc" = "Función de suscripción con configuración separada."
"subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente."
"subTitle" = "Título de la Suscripción"
"subTitleDesc" = "Título mostrado en el cliente VPN"
"subSupportUrl" = "URL de soporte"
"subSupportUrlDesc" = "Enlace de soporte técnico mostrado en el cliente VPN"
"subProfileUrl" = "URL del perfil"
"subProfileUrlDesc" = "Un enlace a tu sitio web mostrado en el cliente VPN"
"subAnnounce" = "Anuncio"
"subAnnounceDesc" = "El texto del anuncio mostrado en el cliente VPN"
"subEnableRouting" = "Habilitar enrutamiento"
"subEnableRoutingDesc" = "Configuración global para habilitar el enrutamiento en el cliente VPN. (Solo para Happ)"
"subRoutingRules" = "Reglas de enrutamiento"
"subRoutingRulesDesc" = "Reglas de enrutamiento globales para el cliente VPN. (Solo para Happ)"
"subListen" = "Listening IP"
"subListenDesc" = "Dejar en blanco por defecto para monitorear todas las IPs."
"subPort" = "Puerto de Suscripción"
"subPortDesc" = "El número de puerto para el servicio de suscripción debe estar sin usar en el servidor."
"subCertPath" = "Ruta del Archivo de Clave Pública del Certificado de Suscripción"
"subCertPathDesc" = "Complete con una ruta absoluta que comience con '/'"
"subKeyPath" = "Ruta del Archivo de Clave Privada del Certificado de Suscripción"
"subKeyPathDesc" = "Complete con una ruta absoluta que comience con '/'"
"subPath" = "Ruta Raíz de la URL de Suscripción"
"subPathDesc" = "Debe empezar con '/' y terminar con '/'"
"subDomain" = "Dominio de Escucha"
"subDomainDesc" = "Dejar en blanco por defecto para monitorear todos los dominios e IPs."
"subUpdates" = "Intervalos de Actualización de Suscripción"
"subUpdatesDesc" = "Horas de intervalo entre actualizaciones en la aplicación del cliente."
"subEncrypt" = "Encriptar configuraciones"
"subEncryptDesc" = "Encriptar las configuraciones devueltas en la suscripción."
"subShowInfo" = "Mostrar información de uso"
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
"subURI" = "URI de proxy inverso"
"externalTrafficInformEnable" = "Informe de tráfico externo"
"externalTrafficInformEnableDesc" = "Informar a la API externa sobre cada actualización de tráfico."
"externalTrafficInformURI" = "URI de información de tráfico externo"
"externalTrafficInformURIDesc" = "Las actualizaciones de tráfico se envían a este URI."
"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
"fragment" = "Fragmentación"
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS"
"fragmentSett" = "Configuración de Fragmentación"
"noisesDesc" = "Activar Sonidos"
"noisesSett" = "Configuración de Sonidos"
"mux" = "Mux"
"muxDesc" = "Transmite múltiples flujos de datos independientes dentro de un flujo de datos establecido."
"muxSett" = "Configuración Mux"
"direct" = "Conexión Directa"
"directDesc" = "Establece conexiones directas con dominios o rangos de IP de un país específico."
"notifications" = "Notificaciones"
"certs" = "Certificados"
"externalTraffic" = "Tráfico Externo"
"dateAndTime" = "Fecha y Hora"
"proxyAndServer" = "Proxy y Servidor"
"intervals" = "Intervalos"
"information" = "Información"
"language" = "Idioma"
"telegramBotLanguage" = "Idioma del Bot de Telegram"
[pages.xray]
"title" = "Xray Configuración"
"save" = "Guardar configuración"
"restart" = "Reiniciar Xray"
"restartSuccess" = "Xray se ha reiniciado correctamente"
"stopSuccess" = "Xray se ha detenido correctamente"
"restartError" = "Ocurrió un error al reiniciar Xray."
"stopError" = "Ocurrió un error al detener Xray."
"basicTemplate" = "Perfil Básico"
"advancedTemplate" = "Perfil Avanzado"
"generalConfigs" = "Configuraciones Generales"
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
"logConfigs" = "Registro"
"logConfigsDesc" = "Los registros pueden afectar la eficiencia de su servidor. Se recomienda habilitarlos sabiamente solo en caso de sus necesidades."
"blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos."
"basicRouting" = "Enrutamiento Básico"
"blockConnectionsConfigsDesc" = "Estas opciones bloquearán el tráfico según el país solicitado específico."
"directConnectionsConfigsDesc" = "Una conexión directa asegura que el tráfico específico no sea enrutado a través de otro servidor."
"blockips" = "Bloquear IPs"
"blockdomains" = "Bloquear Dominios"
"directips" = "IPs Directas"
"directdomains" = "Dominios Directos"
"ipv4Routing" = "Enrutamiento IPv4"
"ipv4RoutingDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4."
"warpRouting" = "Enrutamiento WARP"
"warpRoutingDesc" = "Precaución: Antes de usar estas opciones, instale WARP en modo de proxy socks5 en su servidor siguiendo los pasos en el GitHub del panel. WARP enrutará el tráfico a los sitios web a través de los servidores de Cloudflare."
"Template" = "Plantilla de Configuración de Xray"
"TemplateDesc" = "Genera el archivo de configuración final de Xray basado en esta plantilla."
"FreedomStrategy" = "Configurar Estrategia para el Protocolo Freedom"
"FreedomStrategyDesc" = "Establece la estrategia de salida de la red en el Protocolo Freedom."
"RoutingStrategy" = "Configurar Estrategia de Enrutamiento de Dominios"
"RoutingStrategyDesc" = "Establece la estrategia general de enrutamiento para la resolución de DNS."
"outboundTestUrl" = "URL de prueba de outbound"
"outboundTestUrlDesc" = "URL usada al probar la conectividad del outbound"
"Torrent" = "Prohibir Uso de BitTorrent"
"Inbounds" = "Entrante"
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
"Outbounds" = "Salidas"
"Balancers" = "Equilibradores"
"OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
"Routings" = "Reglas de enrutamiento"
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
"completeTemplate" = "Todos"
"logLevel" = "Nivel de registro"
"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse."
"accessLog" = "Registro de acceso"
"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso"
"errorLog" = "Registro de Errores"
"errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'none' desactiva los registros de errores."
"dnsLog" = "Registro DNS"
"dnsLogDesc" = "Si habilitar los registros de consulta DNS"
"maskAddress" = "Enmascarar Dirección"
"maskAddressDesc" = "Máscara de dirección IP, cuando se habilita, reemplazará automáticamente la dirección IP que aparece en el registro."
"statistics" = "Estadísticas"
"statsInboundUplink" = "Estadísticas de Subida de Entrada"
"statsInboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de entrada."
"statsInboundDownlink" = "Estadísticas de Bajada de Entrada"
"statsInboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de entrada."
"statsOutboundUplink" = "Estadísticas de Subida de Salida"
"statsOutboundUplinkDesc" = "Habilita la recopilación de estadísticas para el tráfico ascendente de todos los proxies de salida."
"statsOutboundDownlink" = "Estadísticas de Bajada de Salida"
"statsOutboundDownlinkDesc" = "Habilita la recopilación de estadísticas para el tráfico descendente de todos los proxies de salida."
[pages.xray.rules]
"first" = "Primero"
"last" = "Último"
"up" = "Arriba"
"down" = "Abajo"
"source" = "Fuente"
"dest" = "Destino"
"inbound" = "Entrante"
"outbound" = "Saliente"
"balancer" = "Equilibrador"
"info" = "Información"
"add" = "Agregar Regla"
"edit" = "Editar Regla"
"useComma" = "Elementos separados por comas"
[pages.xray.outbound]
"addOutbound" = "Agregar salida"
"addReverse" = "Agregar reverso"
"editOutbound" = "Editar salida"
"editReverse" = "Editar reverso"
"tag" = "Etiqueta"
"tagDesc" = "etiqueta única"
"address" = "Dirección"
"reverse" = "Reverso"
"domain" = "Dominio"
"type" = "Tipo"
"bridge" = "puente"
"portal" = "portal"
"link" = "Enlace"
"intercon" = "Interconexión"
"settings" = "Configuración"
"accountInfo" = "Información de la Cuenta"
"outboundStatus" = "Estado de Salida"
"sendThrough" = "Enviar a través de"
"test" = "Probar"
"testResult" = "Resultado de la prueba"
"testing" = "Probando conexión..."
"testSuccess" = "Prueba exitosa"
"testFailed" = "Prueba fallida"
"testError" = "Error al probar la salida"
[pages.xray.balancer]
"addBalancer" = "Agregar equilibrador"
"editBalancer" = "Editar balanceador"
"balancerStrategy" = "Estrategia"
"balancerSelectors" = "Selectores"
"tag" = "Etiqueta"
"tagDesc" = "etiqueta única"
"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag."
[pages.xray.wireguard]
"secretKey" = "Llave secreta"
"publicKey" = "Llave pública"
"allowedIPs" = "IP permitidas"
"endpoint" = "Punto final"
"psk" = "Clave precompartida"
"domainStrategy" = "Estrategia de dominio"
[pages.xray.tun]
"nameDesc" = "El nombre de la interfaz TUN. El valor predeterminado es 'xray0'"
"mtuDesc" = "Unidad Máxima de Transmisión. El tamaño máximo de los paquetes de datos. El valor predeterminado es 1500"
"userLevel" = "Nivel de Usuario"
"userLevelDesc" = "Todas las conexiones realizadas a través de este entrada utilizarán este nivel de usuario. El valor predeterminado es 0"
[pages.xray.dns]
"enable" = "Habilitar DNS"
"enableDesc" = "Habilitar servidor DNS incorporado"
"tag" = "Etiqueta de Entrada DNS"
"tagDesc" = "Esta etiqueta estará disponible como una etiqueta de entrada en las reglas de enrutamiento."
"clientIp" = "IP del cliente"
"clientIpDesc" = "Se utiliza para notificar al servidor la ubicación IP especificada durante las consultas DNS"
"disableCache" = "Desactivar caché"
"disableCacheDesc" = "Desactiva el almacenamiento en caché de DNS"
"disableFallback" = "Desactivar respaldo"
"disableFallbackDesc" = "Desactiva las consultas DNS de respaldo"
"disableFallbackIfMatch" = "Desactivar respaldo si coincide"
"disableFallbackIfMatchDesc" = "Desactiva las consultas DNS de respaldo cuando se acierta en la lista de dominios coincidentes del servidor DNS"
"enableParallelQuery" = "Habilitar consulta paralela"
"enableParallelQueryDesc" = "Habilitar consultas DNS paralelas a múltiples servidores para una resolución más rápida"
"strategy" = "Estrategia de Consulta"
"strategyDesc" = "Estrategia general para resolver nombres de dominio"
"add" = "Agregar Servidor"
"edit" = "Editar Servidor"
"domains" = "Dominios"
"expectIPs" = "IPs esperadas"
"unexpectIPs" = "IPs inesperadas"
"useSystemHosts" = "Usar Hosts del sistema"
"useSystemHostsDesc" = "Usar el archivo hosts de un sistema instalado"
"usePreset" = "Usar plantilla"
"dnsPresetTitle" = "Plantillas DNS"
"dnsPresetFamily" = "Familiar"
[pages.xray.fakedns]
"add" = "Agregar DNS Falso"
"edit" = "Editar DNS Falso"
"ipPool" = "Subred del grupo de IP"
"poolSize" = "Tamaño del grupo"
[pages.settings.security]
"admin" = "Credenciales de administrador"
"twoFactor" = "Autenticación de dos factores"
"twoFactorEnable" = "Habilitar 2FA"
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
"twoFactorModalSetTitle" = "Activar autenticación de dos factores"
"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
"twoFactorModalFirstStep" = "1. Escanea este código QR en la aplicación de autenticación o copia el token cerca del código QR y pégalo en la aplicación"
"twoFactorModalSecondStep" = "2. Ingresa el código de la aplicación"
"twoFactorModalRemoveStep" = "Ingresa el código de la aplicación para eliminar la autenticación de dos factores."
"twoFactorModalChangeCredentialsTitle" = "Cambiar credenciales"
"twoFactorModalChangeCredentialsStep" = "Ingrese el código de la aplicación para cambiar las credenciales del administrador."
"twoFactorModalSetSuccess" = "La autenticación de dos factores se ha establecido con éxito"
"twoFactorModalDeleteSuccess" = "La autenticación de dos factores se ha eliminado con éxito"
"twoFactorModalError" = "Código incorrecto"
[pages.settings.toasts]
"modifySettings" = "Los parámetros han sido modificados."
"getSettings" = "Ocurrió un error al obtener los parámetros."
"modifyUserError" = "Ocurrió un error al cambiar las credenciales del administrador."
"modifyUser" = "Has cambiado exitosamente las credenciales del administrador."
"originalUserPassIncorrect" = "Nombre de usuario o contraseña original incorrectos"
"userPassMustBeNotEmpty" = "El nuevo nombre de usuario y la nueva contraseña no pueden estar vacíos"
"getOutboundTrafficError" = "Error al obtener el tráfico saliente"
"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
[tgbot]
"keyboardClosed" = "❌ Teclado cerrado!"
"noResult" = "❗ ¡Sin resultados!"
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando nuevamente!"
"wentWrong" = "❌ ¡Algo salió mal!"
"noIpRecord" = "❗ ¡No hay registro de IP!"
"noInbounds" = "❗ ¡No se encontraron entradas!"
"unlimited" = "♾ Ilimitado (Restablecer)"
"add" = "Añadir"
"month" = "Mes"
"months" = "Meses"
"day" = "Día"
"days" = "Días"
"hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconocido"
"inbounds" = "Entradas"
"clients" = "Clientes"
"offline" = "🔴 Desconectado"
"online" = "🟢 En línea"
[tgbot.commands]
"unknown" = "❗ Comando desconocido"
"pleaseChoose" = "👇 Por favor elige:\r\n"
"help" = "🤖 ¡Bienvenido a este bot! Está diseñado para ofrecerte datos específicos del servidor y te permite hacer modificaciones según sea necesario.\r\n\r\n"
"start" = "👋 Hola <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Bienvenido al bot de gestión de <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ ¡El bot está bien!"
"usage" = "❗ ¡Por favor proporciona un texto para buscar!"
"getID" = "🆔 Tu ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Para reiniciar Xray Core:\r\n<code>/restart</code>\r\n\r\nPara buscar un correo electrónico de cliente:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nPara buscar entradas (con estadísticas de cliente):\r\n<code>/inbound [Observación]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Para buscar estadísticas, utiliza el siguiente comando:\r\n<code>/usage [Correo electrónico]</code>\r\n\r\nID de Chat de Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ ¡Operación exitosa!"
"restartFailed" = "❗ Error en la operación.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core no está en ejecución."
"startDesc" = "Mostrar el menú principal"
"helpDesc" = "Ayuda del bot"
"statusDesc" = "Comprobar el estado del bot"
"idDesc" = "Mostrar tu ID de Telegram"
[tgbot.messages]
"cpuThreshold" = "🔴 El uso de CPU {{ .Percent }}% es mayor que el umbral {{ .Threshold }}%"
"selectUserFailed" = "❌ ¡Error al seleccionar usuario!"
"userSaved" = "✅ Usuario de Telegram guardado."
"loginSuccess" = "✅ Has iniciado sesión en el panel con éxito.\r\n"
"loginFailed" = "❗️ Falló el inicio de sesión en el panel.\r\n"
"2faFailed" = "Error de 2FA"
"report" = "🕰 Informes programados: {{ .RunTime }}\r\n"
"datetime" = "⏰ Fecha y Hora: {{ .DateTime }}\r\n"
"hostname" = "💻 Nombre del Host: {{ .Hostname }}\r\n"
"version" = "🚀 Versión de X-UI: {{ .Version }}\r\n"
"xrayVersion" = "📡 Versión de Xray: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Tiempo de actividad del servidor: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Carga del servidor: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Memoria del servidor: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Conteo de TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 Conteo de UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Tráfico: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Estado de Xray: {{ .State }}\r\n"
"username" = "👤 Nombre de usuario: {{ .Username }}\r\n"
"password" = "👤 Contraseña: {{ .Password }}\r\n"
"time" = "⏰ Hora: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Puerto: {{ .Port }}\r\n"
"expire" = "📅 Fecha de Vencimiento: {{ .Time }}\r\n"
"expireIn" = "📅 Vence en: {{ .Time }}\r\n"
"active" = "💡 Activo: {{ .Enable }}\r\n"
"enabled" = "🚨 Habilitado: {{ .Enable }}\r\n"
"online" = "🌐 Estado de conexión: {{ .Status }}\r\n"
"lastOnline" = "🔙 Última conexión: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Subida: ↑{{ .Upload }}\r\n"
"download" = "🔽 Bajada: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Usuario de Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Agotado {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Cantidad de Agotados {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Clientes en línea: {{ .Count }}\r\n"
"disabled" = "🛑 Desactivado: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Se agotará pronto: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Hora de la Copia de Seguridad: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Actualizado en: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sí"
"no" = "❌ No"
"received_id" = "🔑📥 ID actualizado."
"received_password" = "🔑📥 Contraseña actualizada."
"received_email" = "📧📥 Correo electrónico actualizado."
"received_comment" = "💬📥 Comentario actualizado."
"id_prompt" = "🔑 ID predeterminado: {{ .ClientId }}\n\nIntroduce tu ID."
"pass_prompt" = "🔑 Contraseña predeterminada: {{ .ClientPassword }}\n\nIntroduce tu contraseña."
"email_prompt" = "📧 Correo electrónico predeterminado: {{ .ClientEmail }}\n\nIntroduce tu correo electrónico."
"comment_prompt" = "💬 Comentario predeterminado: {{ .ClientComment }}\n\nIntroduce tu comentario."
"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!"
"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Contraseña: {{ .ClientPass }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!"
"cancel" = "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄"
"error_add_client" = "⚠️ Error:\n\n {{ .error }}"
"using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊"
"incorrect_input" = "Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫"
"AreYouSure" = "¿Estás seguro? 🤔"
"SuccessResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito"
"FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠 Error: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Proceso de reinicio de tráfico finalizado para todos los clientes."
[tgbot.buttons]
"closeKeyboard" = "❌ Cerrar Teclado"
"cancel" = "❌ Cancelar"
"cancelReset" = "❌ Cancelar Reinicio"
"cancelIpLimit" = "❌ Cancelar Límite de IP"
"confirmResetTraffic" = "✅ ¿Confirmar Reinicio de Tráfico?"
"confirmClearIps" = "✅ ¿Confirmar Limpiar IPs?"
"confirmRemoveTGUser" = "✅ ¿Confirmar Eliminar Usuario de Telegram?"
"confirmToggle" = "✅ ¿Confirmar habilitar/deshabilitar usuario?"
"dbBackup" = "Obtener Copia de Seguridad de BD"
"serverUsage" = "Uso del Servidor"
"getInbounds" = "Obtener Entradas"
"depleteSoon" = "Pronto se Agotará"
"clientUsage" = "Obtener Uso"
"onlines" = "Clientes en línea"
"commands" = "Comandos"
"refresh" = "🔄 Actualizar"
"clearIPs" = "❌ Limpiar IPs"
"removeTGUser" = "❌ Eliminar Usuario de Telegram"
"selectTGUser" = "👤 Seleccionar Usuario de Telegram"
"selectOneTGUser" = "👤 Selecciona un usuario de telegram:"
"resetTraffic" = "📈 Reiniciar Tráfico"
"resetExpire" = "📅 Cambiar fecha de Vencimiento"
"ipLog" = "🔢 Registro de IP"
"ipLimit" = "🔢 Límite de IP"
"setTGUser" = "👤 Establecer Usuario de Telegram"
"toggle" = "🔘 Habilitar / Deshabilitar"
"custom" = "🔢 Costumbre"
"confirmNumber" = "✅ Confirmar: {{ .Num }}"
"confirmNumberAdd" = "✅ Confirmar agregando: {{ .Num }}"
"limitTraffic" = "🚧 Límite de tráfico"
"getBanLogs" = "Registros de prohibición"
"allClients" = "Todos los Clientes"
"addClient" = "Añadir cliente"
"submitDisable" = "Enviar como deshabilitado ☑️"
"submitEnable" = "Enviar como habilitado ✅"
"use_default" = "🏷️ Usar por defecto"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Contraseña"
"change_email" = "⚙️📧 Correo electrónico"
"change_comment" = "⚙️💬 Comentario"
"ResetAllTraffics" = "Reiniciar todo el tráfico"
"SortedTrafficUsageReport" = "Informe de uso de tráfico ordenado"
[tgbot.answers]
"successfulOperation" = "✅ ¡Exitosa!"
"errorOperation" = "❗ Error en la Operación."
"getInboundsFailed" = "❌ Error al obtener las entradas"
"getClientsFailed" = "❌ No se pudo obtener los clientes."
"canceled" = "❌ {{ .Email }} : Operación cancelada."
"clientRefreshSuccess" = "✅ {{ .Email }} : Cliente actualizado exitosamente."
"IpRefreshSuccess" = "✅ {{ .Email }} : IPs actualizadas exitosamente."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Usuario de Telegram del cliente actualizado exitosamente."
"resetTrafficSuccess" = "✅ {{ .Email }} : Tráfico reiniciado exitosamente."
"setTrafficLimitSuccess" = "✅ {{ .Email }} : Límite de Tráfico guardado exitosamente."
"expireResetSuccess" = "✅ {{ .Email }} : Días de vencimiento reiniciados exitosamente."
"resetIpSuccess" = "✅ {{ .Email }} : Límite de IP {{ .Count }} guardado exitosamente."
"clearIpSuccess" = "✅ {{ .Email }} : IPs limpiadas exitosamente."
"getIpLog" = "✅ {{ .Email }} : Obtener Registro de IP."
"getUserInfo" = "✅ {{ .Email }} : Obtener Información de Usuario de Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }} : Usuario de Telegram eliminado exitosamente."
"enableSuccess" = "✅ {{ .Email }} : Habilitado exitosamente."
"disableSuccess" = "✅ {{ .Email }} : Deshabilitado exitosamente."
"askToAddUserId" = "¡No se encuentra su configuración!\r\nPor favor, pídale a su administrador que use su ChatID de usuario de Telegram en su(s) configuración(es).\r\n\r\nSu ChatID de usuario: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Elige un Cliente para Inbound {{ .Inbound }}"
"chooseInbound" = "Elige un Inbound"

View file

@ -1,795 +0,0 @@
"username" = "نام‌کاربری"
"password" = "رمزعبور"
"login" = "ورود"
"confirm" = "تایید"
"cancel" = "انصراف"
"close" = "بستن"
"create" = "ایجاد"
"update" = "به‌روزرسانی"
"copy" = "کپی"
"copied" = "کپی شد"
"download" = "دانلود"
"remark" = "نام"
"enable" = "فعال"
"protocol" = "پروتکل"
"search" = "جستجو"
"filter" = "فیلتر"
"loading" = "...در حال بارگذاری"
"second" = "ثانیه"
"minute" = "دقیقه"
"hour" = "ساعت"
"day" = "روز"
"check" = "چک کردن"
"indefinite" = "نامحدود"
"unlimited" = "نامحدود"
"none" = "هیچ"
"qrCode" = "QRکد"
"info" = "اطلاعات بیشتر"
"edit" = "ویرایش"
"delete" = "حذف"
"reset" = "ریست"
"noData" = "داده‌ای وجود ندارد."
"copySuccess" = "باموفقیت کپی‌شد"
"sure" = "مطمئن"
"encryption" = "رمزگذاری"
"useIPv4ForHost" = "از IPv4 برای میزبان استفاده کنید"
"transmission" = "راه‌اتصال"
"host" = "آدرس"
"path" = "مسیر"
"camouflage" = "مبهم‌سازی"
"status" = "وضعیت"
"enabled" = "فعال"
"disabled" = "غیرفعال"
"depleted" = "منقضی"
"depletingSoon" = "در‌حال‌انقضا"
"offline" = "آفلاین"
"online" = "آنلاین"
"domainName" = "آدرس دامنه"
"monitor" = "آی‌پی اتصال"
"certificate" = "گواهی دیجیتال"
"fail" = "ناموفق"
"comment" = "توضیحات"
"success" = "موفق"
"lastOnline" = "آخرین فعالیت"
"getVersion" = "دریافت نسخه"
"install" = "نصب"
"clients" = "کاربران"
"usage" = "استفاده"
"twoFactorCode" = "کد"
"remained" = "باقی‌مانده"
"security" = "امنیت"
"secAlertTitle" = "هشدار‌امنیتی"
"secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید"
"secAlertConf" = "تنظیمات خاصی در برابر حملات آسیب پذیر هستند. توصیه می‌شود پروتکل‌های امنیتی را برای جلوگیری از نفوذ احتمالی تقویت کنید"
"secAlertSSL" = "پنل فاقد ارتباط امن است. لطفاً یک گواهینامه تی‌ال‌اس برای محافظت از داده‌ها نصب کنید"
"secAlertPanelPort" = "استفاده از پورت پیش‌فرض پنل ناامن است. لطفاً یک پورت تصادفی یا خاص تنظیم کنید"
"secAlertPanelURI" = "مسیر پیش‌فرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"secAlertSubURI" = "مسیر پیش‌فرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"secAlertSubJsonURI" = "مسیر پیش‌فرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
"emptyDnsDesc" = "هیچ سرور DNS اضافه نشده است."
"emptyFakeDnsDesc" = "هیچ سرور Fake DNS اضافه نشده است."
"emptyBalancersDesc" = "هیچ بالانسر اضافه نشده است."
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
"somethingWentWrong" = "مشکلی پیش آمد"
[subscription]
"title" = "اطلاعات سابسکریپشن"
"subId" = "شناسه اشتراک"
"status" = "وضعیت"
"downloaded" = "دانلود"
"uploaded" = "آپلود"
"expiry" = "تاریخ پایان"
"totalQuota" = "حجم کلی"
"individualLinks" = "لینک‌های تکی"
"active" = "فعال"
"inactive" = "غیرفعال"
"unlimited" = "نامحدود"
"noExpiry" = "بدون انقضا"
[menu]
"theme" = "تم"
"dark" = "تیره"
"ultraDark" = "فوق تیره"
"dashboard" = "نمای کلی"
"inbounds" = "ورودی‌ها"
"settings" = "تنظیمات پنل"
"xray" = "پیکربندی ایکس‌ری"
"logout" = "خروج"
"link" = "مدیریت"
[pages.login]
"hello" = "سلام"
"title" = "خوش‌آمدید"
"loginAgain" = "مدت زمان استفاده به‌اتمام‌رسیده، لطفا دوباره وارد شوید"
[pages.login.toasts]
"invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است"
"emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌"
"emptyPassword" = "لطفا یک رمزعبور وارد کنید"
"wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است."
"successLogin" = "شما با موفقیت به حساب کاربری خود وارد شدید."
"successRegister" = "ثبت‌نام با موفقیت انجام شد، لطفاً وارد شوید."
"userExists" = "نام کاربری از قبل وجود دارد"
"errorRegister" = "ثبت نام ناموفق بود"
[pages.index]
"title" = "نمای کلی"
"cpu" = "پردازنده"
"logicalProcessors" = "پردازنده‌های منطقی"
"frequency" = "فرکانس"
"swap" = "سواپ"
"storage" = "ذخیره‌سازی"
"memory" = "حافظه رم"
"threads" = "رشته‌ها"
"xrayStatus" = "ایکس‌ری"
"stopXray" = "توقف"
"restartXray" = "شروع‌مجدد"
"xraySwitch" = "‌نسخه"
"xraySwitchClick" = "نسخه مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا بادقت انتخاب کنید. درصورت انتخاب نسخه قدیمی‌تر، امکان ناهماهنگی با پیکربندی فعلی وجود دارد"
"xrayStatusUnknown" = "ناشناخته"
"xrayStatusRunning" = "در حال اجرا"
"xrayStatusStop" = "متوقف"
"xrayStatusError" = "خطا"
"xrayErrorPopoverTitle" = "خطا در هنگام اجرای Xray رخ داد"
"operationHours" = "مدت‌کارکرد"
"systemLoad" = "بارسیستم"
"systemLoadDesc" = "میانگین بار سیستم برای 1، 5 و 15 دقیقه گذشته"
"connectionCount" = "تعداد کانکشن ها"
"ipAddresses" = "آدرس‌های IP"
"toggleIpVisibility" = "تغییر وضعیت نمایش IP"
"overallSpeed" = "سرعت کلی"
"upload" = "آپلود"
"download" = "دانلود"
"totalData" = "داده‌های کل"
"sent" = "ارسال شده"
"received" = "دریافت شده"
"documentation" = "مستندات"
"xraySwitchVersionDialog" = "آیا واقعاً می‌خواهید نسخه Xray را تغییر دهید؟"
"xraySwitchVersionDialogDesc" = "این کار نسخه Xray را به #version# تغییر می‌دهد."
"xraySwitchVersionPopover" = "Xray با موفقیت به‌روز شد"
"geofileUpdateDialog" = "آیا واقعاً می‌خواهید فایل جغرافیایی را به‌روز کنید؟"
"geofileUpdateDialogDesc" = "این عمل فایل #filename# را به‌روز می‌کند."
"geofilesUpdateDialogDesc" = "با این کار همه فایل‌ها به‌روزرسانی می‌شوند."
"geofilesUpdateAll" = "همه را به‌روزرسانی کنید"
"geofileUpdatePopover" = "فایل جغرافیایی با موفقیت به‌روز شد"
"dontRefresh" = "در حال نصب، لطفا صفحه را رفرش نکنید"
"logs" = "گزارش‌ها"
"config" = "پیکربندی"
"backup" = "پشتیبان‌گیری"
"backupTitle" = "پشتیبان‌گیری دیتابیس"
"exportDatabase" = "پشتیبان‌گیری"
"exportDatabaseDesc" = "برای دانلود یک فایل .db حاوی پشتیبان از پایگاه داده فعلی خود به دستگاهتان کلیک کنید."
"importDatabase" = "بازیابی"
"importDatabaseDesc" = "برای انتخاب و آپلود یک فایل .db از دستگاهتان و بازیابی پایگاه داده از یک پشتیبان کلیک کنید."
"importDatabaseSuccess" = "پایگاه داده با موفقیت وارد شد"
"importDatabaseError" = "خطا در وارد کردن پایگاه داده"
"readDatabaseError" = "خطا در خواندن پایگاه داده"
"getDatabaseError" = "خطا در دریافت پایگاه داده"
"getConfigError" = "خطا در دریافت فایل پیکربندی"
[pages.inbounds]
"allTimeTraffic" = "کل ترافیک"
"allTimeTrafficUsage" = "کل استفاده در تمام مدت"
"title" = "کاربران"
"totalDownUp" = "دریافت/ارسال کل"
"totalUsage" = "‌‌‌مصرف کل"
"inboundCount" = "کل ورودی‌ها"
"operate" = "عملیات"
"enable" = "فعال"
"remark" = "نام"
"protocol" = "پروتکل"
"port" = "پورت"
"portMap" = "پورت‌های نظیر"
"traffic" = "ترافیک"
"details" = "توضیحات"
"transportConfig" = "نحوه اتصال"
"expireDate" = "مدت زمان"
"createdAt" = "ایجاد"
"updatedAt" = "به‌روزرسانی"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "افزودن ورودی"
"generalActions" = "عملیات کلی"
"autoRefresh" = "تازه‌سازی خودکار"
"autoRefreshInterval" = "فاصله"
"modifyInbound" = "ویرایش ورودی"
"deleteInbound" = "حذف ورودی"
"deleteInboundContent" = "آیا مطمئن به حذف ورودی هستید؟"
"deleteClient" = "حذف کاربر"
"deleteClientContent" = "آیا مطمئن به حذف کاربر هستید؟"
"resetTrafficContent" = "آیا مطمئن به ریست ترافیک هستید؟"
"copyLink" = "کپی لینک"
"address" = "آدرس"
"network" = "شبکه"
"destinationPort" = "پورت مقصد"
"targetAddress" = "آدرس مقصد"
"monitorDesc" = "به‌طور پیش‌فرض خالی‌بگذارید"
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
"totalFlow" = "ترافیک کل"
"leaveBlankToNeverExpire" = "برای منقضی‌نشدن خالی‌بگذارید"
"noRecommendKeepDefault" = "توصیه‌می‌شود به‌طور پیش‌فرض حفظ‌شود"
"certificatePath" = "مسیر فایل"
"certificateContent" = "محتوای فایل"
"publicKey" = "کلید عمومی"
"privatekey" = "کلید خصوصی"
"clickOnQRcode" = "برای کپی بر روی کدتصویری کلیک کنید"
"client" = "کاربر"
"export" = "استخراج لینک‌ها"
"clone" = "شبیه‌سازی"
"cloneInbound" = "شبیه‌سازی ورودی"
"cloneInboundContent" = "همه موارد این ورودی بجز پورت، آی‌پی و کاربر‌ها شبیه‌سازی خواهند شد"
"cloneInboundOk" = "ساختن شبیه ساز"
"resetAllTraffic" = "ریست ترافیک کل ورودی‌ها"
"resetAllTrafficTitle" = "ریست ترافیک کل ورودی‌ها"
"resetAllTrafficContent" = "آیا مطمئن به ریست ترافیک تمام ورودی‌ها هستید؟"
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران این‌ ورودی هستید؟"
"resetAllClientTraffics" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن به ریست ترافیک تمام کاربران هستید؟"
"delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن به حذف تمام کاربران منقضی‌شده ‌هستید؟"
"email" = "ایمیل"
"emailDesc" = "باید یک ایمیل یکتا باشد"
"IPLimit" = "محدودیت آی‌پی"
"IPLimitDesc" = "(اگر تعداد از مقدار تنظیم شده بیشتر شود، ورودی را غیرفعال می کند. (0 = غیرفعال"
"IPLimitlog" = "گزارش‌ها"
"IPLimitlogDesc" = "گزارش تاریخچه آی‌پی. برای فعال کردن ورودی پس از غیرفعال شدن، گزارش را پاک کنید"
"IPLimitlogclear" = "پاک کردن گزارش‌ها"
"setDefaultCert" = "استفاده از گواهی پنل"
"telegramDesc" = "لطفا شناسه گفتگوی تلگرام را وارد کنید. (از دستور '/id' در ربات استفاده کنید) یا (@userinfobot)"
"subscriptionDesc" = "شما می‌توانید لینک سابسکربپشن خودرا در 'جزئیات' پیدا کنید، همچنین می‌توانید از همین نام برای چندین کاربر استفاده‌کنید"
"info" = "اطلاعات"
"same" = "همسان"
"inboundData" = "داده‌های ورودی"
"exportInbound" = "استخراج ورودی"
"import" = "افزودن"
"importInbound" = "افزودن یک ورودی"
"periodicTrafficResetTitle" = "بازنشانی ترافیک"
"periodicTrafficResetDesc" = "بازنشانی خودکار شمارنده ترافیک در فواصل زمانی مشخص"
"lastReset" = "آخرین بازنشانی"
[pages.client]
"add" = "کاربر جدید"
"edit" = "ویرایش کاربر"
"submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات"
"clientCount" = "تعداد کاربران"
"bulk" = "انبوه‌سازی"
"method" = "روش"
"first" = "از"
"last" = "تا"
"prefix" = "پیشوند"
"postfix" = "پسوند"
"delayedStart" = "شروع‌پس‌از‌اولین‌استفاده"
"expireDays" = "مدت زمان"
"days" = "(روز)"
"renew" = "تمدید خودکار"
"renewDesc" = "تمدید خودکار پس‌از ‌انقضا. (0 = غیرفعال)(واحد: روز)"
[pages.inbounds.periodicTrafficReset]
"never" = "هرگز"
"daily" = "روزانه"
"weekly" = "هفتگی"
"monthly" = "ماهانه"
[pages.inbounds.toasts]
"obtain" = "فراهم‌سازی"
"updateSuccess" = "بروزرسانی با موفقیت انجام شد"
"logCleanSuccess" = "لاگ پاکسازی شد"
"inboundsUpdateSuccess" = "ورودی‌ها با موفقیت به‌روزرسانی شدند"
"inboundUpdateSuccess" = "ورودی با موفقیت به‌روزرسانی شد"
"inboundCreateSuccess" = "ورودی با موفقیت ایجاد شد"
"inboundDeleteSuccess" = "ورودی با موفقیت حذف شد"
"inboundClientAddSuccess" = "کلاینت(های) ورودی اضافه شدند"
"inboundClientDeleteSuccess" = "کلاینت ورودی حذف شد"
"inboundClientUpdateSuccess" = "کلاینت ورودی به‌روزرسانی شد"
"delDepletedClientsSuccess" = "تمام کلاینت‌های مصرف شده حذف شدند"
"resetAllClientTrafficSuccess" = "تمام ترافیک کلاینت بازنشانی شد"
"resetAllTrafficSuccess" = "تمام ترافیک‌ها بازنشانی شدند"
"resetInboundClientTrafficSuccess" = "ترافیک بازنشانی شد"
"trafficGetError" = "خطا در دریافت ترافیک‌ها"
"getNewX25519CertError" = "خطا در دریافت گواهی X25519."
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
"getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc."
[pages.inbounds.stream.general]
"request" = "درخواست"
"response" = "پاسخ"
"name" = "نام"
"value" = "مقدار"
[pages.inbounds.stream.tcp]
"version" = "نسخه"
"method" = "متد"
"path" = "مسیر"
"status" = "وضعیت"
"statusDescription" = "توضیحات وضعیت"
"requestHeader" = "سربرگ درخواست"
"responseHeader" = "سربرگ پاسخ"
[pages.settings]
"title" = "تنظیمات پنل"
"save" = "ذخیره"
"infoDesc" = "برای اعمال تغییرات در این بخش باید پس از ذخیره کردن، پنل را ریستارت کنید"
"restartPanel" = "ریستارت پنل"
"restartPanelDesc" = "آیا مطمئن به ریستارت پنل هستید؟ اگر پس‌از ریستارت نمی‌توانید به پنل دسترسی پیدا کنید، لطفاً گزارش‌های موجود در اسکریپت پنل را بررسی کنید"
"restartPanelSuccess" = "پنل با موفقیت راه‌اندازی مجدد شد"
"actions" = "عملیات ها"
"resetDefaultConfig" = "برگشت به پیش‌فرض"
"panelSettings" = "پیکربندی"
"securitySettings" = "احرازهویت"
"TGBotSettings" = "ربات تلگرام"
"panelListeningIP" = "آدرس آی‌پی"
"panelListeningIPDesc" = "آدرس آی‌پی برای وب پنل. برای گوش‌دادن به‌تمام آی‌پی‌ها خالی‌بگذارید"
"panelListeningDomain" = "نام دامنه"
"panelListeningDomainDesc" = "آدرس دامنه برای وب پنل. برای گوش دادن به‌تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید"
"panelPort" = "پورت"
"panelPortDesc" = "شماره پورت برای وب پنل. باید پورت استفاده نشده‌باشد"
"publicKeyPath" = "مسیر کلید عمومی"
"publicKeyPathDesc" = "مسیر فایل کلیدعمومی برای وب پنل. با '/' شروع‌می‌شود"
"privateKeyPath" = "مسیر کلید خصوصی"
"privateKeyPathDesc" = "مسیر فایل کلیدخصوصی برای وب پنل. با '/' شروع‌می‌شود"
"panelUrlPath" = "URI مسیر"
"panelUrlPathDesc" = "برای وب پنل. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر"
"pageSize" = "اندازه صفحه بندی جدول"
"pageSizeDesc" = "(اندازه صفحه برای جدول ورودی‌ها.(0 = غیرفعال"
"remarkModel" = "نام‌کانفیگ و جداکننده"
"datepicker" = "نوع تقویم"
"datepickerPlaceholder" = "انتخاب تاریخ"
"datepickerDescription" = "وظایف برنامه ریزی شده بر اساس این تقویم اجرا می‌شود"
"sampleRemark" = "نمونه‌نام"
"oldUsername" = "نام‌کاربری فعلی"
"currentPassword" = "رمز‌عبور فعلی"
"newUsername" = "نام‌کاربری جدید"
"newPassword" = "رمزعبور جدید"
"telegramBotEnable" = "فعال‌سازی ربات تلگرام"
"telegramBotEnableDesc" = "ربات تلگرام را فعال می‌کند"
"telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "دریافت کنید @botfather توکن را می‌توانید از"
"telegramProxy" = "SOCKS پراکسی"
"telegramProxyDesc" = "را برای اتصال به تلگرام فعال می کند SOCKS5 پراکسی"
"telegramAPIServer" = "سرور API تلگرام"
"telegramAPIServerDesc" = "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید"
"telegramChatId" = "آی‌دی چت مدیر"
"telegramChatIdDesc" = "دریافت ‌کنید ('/id'یا (دستور (@userinfobot) آی‌دی(های) چت تلگرام مدیر، از"
"telegramNotifyTime" = "زمان نوتیفیکیشن"
"telegramNotifyTimeDesc" = "زمان‌اطلاع‌رسانی ربات تلگرام برای گزارش های دوره‌ای. از فرمت زمانبندی لینوکس استفاده‌کنید‌"
"tgNotifyBackup" = "پشتیبان‌گیری از دیتابیس"
"tgNotifyBackupDesc" = "فایل پشتیبان‌دیتابیس را به‌همراه گزارش ارسال می‌کند"
"tgNotifyLogin" = "اعلان ورود"
"tgNotifyLoginDesc" = "نام‌کاربری، آدرس آی‌پی، و زمان ورود، فردی که سعی می‌کند وارد پنل شود را نمایش می‌دهد"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "(بیشینه زمانی که می‌توانید لاگین بمانید. (واحد: دقیقه"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به زمان انقضا. (واحد: روز"
"trafficDiff" = "آستانه ترافیک باقی مانده"
"trafficDiffDesc" = "(فاصله زمانی هشدار تا رسیدن به اتمام ترافیک. (واحد: گیگابایت"
"tgNotifyCpu" = "آستانه هشدار بار پردازنده"
"tgNotifyCpuDesc" = "(اگر بار روی پردازنده ازاین آستانه فراتر رفت، برای شما پیام ارسال می‌شود. (واحد: درصد"
"timeZone" = "منطقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه‌زمانی اجرا می‌شود"
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال‌سازی سرویس سابسکریپشن"
"subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند"
"subJsonEnable" = "فعال/غیرفعال‌سازی مستقل نقطه دسترسی سابسکریپشن JSON."
"subTitle" = "عنوان اشتراک"
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
"subSupportUrl" = "آدرس پشتیبانی"
"subSupportUrlDesc" = "لینک پشتیبانی فنی که در کلاینت VPN نمایش داده می‌شود"
"subProfileUrl" = "آدرس پروفایل"
"subProfileUrlDesc" = "لینک وب‌سایت شما که در کلاینت VPN نمایش داده می‌شود"
"subAnnounce" = "اعلان"
"subAnnounceDesc" = "متن اعلانی که در کلاینت VPN نمایش داده می‌شود"
"subEnableRouting" = "فعال‌سازی مسیریابی"
"subEnableRoutingDesc" = "تنظیمات سراسری برای فعال‌سازی مسیریابی در کلاینت VPN. (فقط برای Happ)"
"subRoutingRules" = "قوانین مسیریابی"
"subRoutingRulesDesc" = "قوانین مسیریابی سراسری برای کلاینت VPN. (فقط برای Happ)"
"subListen" = "آدرس آی‌پی"
"subListenDesc" = "آدرس آی‌پی برای سرویس سابسکریپشن. برای گوش دادن به‌تمام آی‌پی‌ها خالی‌بگذارید"
"subPort" = "پورت"
"subPortDesc" = "شماره پورت برای سرویس سابسکریپشن. باید پورت استفاده نشده‌باشد"
"subCertPath" = "مسیر کلید عمومی"
"subCertPathDesc" = "مسیر فایل کلیدعمومی برای سرویس سابیکریپشن. با '/' شروع‌می‌شود"
"subKeyPath" = "مسیر کلید خصوصی"
"subKeyPathDesc" = "مسیر فایل کلیدخصوصی برای سرویس سابسکریپشن. با '/' شروع‌می‌شود"
"subPath" = "URI مسیر"
"subPathDesc" = "برای سرویس سابسکریپشن. با '/' شروع‌ و با '/' خاتمه‌ می‌یابد URI مسیر"
"subDomain" = "نام دامنه"
"subDomainDesc" = "آدرس دامنه برای سرویس سابسکریپشن. برای گوش دادن به تمام دامنه‌ها و آی‌پی‌ها خالی‌بگذارید‌"
"subUpdates" = "فاصله بروزرسانی‌ سابسکریپشن"
"subUpdatesDesc" = "(فاصله مابین بروزرسانی در برنامه‌های کاربری. (واحد: ساعت"
"subEncrypt" = "کدگذاری"
"subEncryptDesc" = "کدگذاری خواهدشد Base64 محتوای برگشتی سرویس سابسکریپشن برپایه"
"subShowInfo" = "نمایش اطلاعات مصرف"
"subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
"subURI" = "پروکسی معکوس URI مسیر"
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
"externalTrafficInformEnable" = "اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"fragment" = "فرگمنت"
"fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس"
"fragmentSett" = "تنظیمات فرگمنت"
"noisesDesc" = "فعال کردن Noises."
"noisesSett" = "تنظیمات Noises"
"mux" = "ماکس"
"muxDesc" = "چندین جریان داده مستقل را در یک جریان داده ثابت منتقل می کند"
"muxSett" = "تنظیمات ماکس"
"direct" = "اتصال مستقیم"
"directDesc" = "به طور مستقیم با دامنه ها یا محدوده آی‌پی یک کشور خاص ارتباط برقرار می کند"
"notifications" = "اعلان‌ها"
"certs" = "گواهی‌ها"
"externalTraffic" = "ترافیک خارجی"
"dateAndTime" = "تاریخ و زمان"
"proxyAndServer" = "پراکسی و سرور"
"intervals" = "فواصل"
"information" = "اطلاعات"
"language" = "زبان"
"telegramBotLanguage" = "زبان ربات تلگرام"
[pages.xray]
"title" = "پیکربندی ایکس‌ری"
"save" = "ذخیره"
"restart" = "ریستارت ایکس‌ری"
"restartSuccess" = "Xray با موفقیت راه‌اندازی مجدد شد"
"stopSuccess" = "Xray با موفقیت متوقف شد"
"restartError" = "خطا در راه‌اندازی مجدد Xray."
"stopError" = "خطا در توقف Xray."
"basicTemplate" = "پایه"
"advancedTemplate" = "پیشرفته"
"generalConfigs" = "استراتژی‌ کلی"
"generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند"
"logConfigs" = "گزارش"
"logConfigsDesc" = "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید"
"blockConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند"
"basicRouting" = "مسیریابی پایه"
"blockConnectionsConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس کشور درخواست‌شده خاص مسدود می‌کنند."
"directConnectionsConfigsDesc" = "یک اتصال مستقیم تضمین می‌کند که ترافیک خاص از طریق سرور دیگری مسیریابی نشود."
"blockips" = "مسدود کردن آی‌پی‌ها"
"blockdomains" = "مسدود کردن دامنه‌ها"
"directips" = "آی‌پی‌های مستقیم"
"directdomains" = "دامنه‌های مستقیم"
"ipv4Routing" = "IPv4 مسیریابی"
"ipv4RoutingDesc" = "این گزینه‌ها ترافیک را از طریق آی‌پی نسخه4 سرور، به مقصد هدایت می‌کند"
"warpRouting" = "WARP مسیریابی"
"warpRoutingDesc" = "این گزینه‌ها ترافیک‌ را از طریق وارپ کلادفلر به مقصد هدایت می‌کند"
"Template" = "‌پیکربندی پیشرفته الگو ایکس‌ری"
"TemplateDesc" = "فایل پیکربندی نهایی ایکس‌ری بر اساس این الگو ایجاد می‌شود"
"FreedomStrategy" = "Freedom استراتژی پروتکل"
"FreedomStrategyDesc" = "تعیین می‌کند Freedom استراتژی خروجی شبکه را برای پروتکل"
"RoutingStrategy" = "استراتژی کلی مسیریابی"
"RoutingStrategyDesc" = "استراتژی کلی مسیریابی برای حل تمام درخواست‌ها را تعیین می‌کند"
"outboundTestUrl" = "آدرس تست خروجی"
"outboundTestUrlDesc" = "آدرسی که برای تست اتصال خروجی استفاده می‌شود."
"Torrent" = "مسدودسازی پروتکل بیت‌تورنت"
"Inbounds" = "ورودی‌ها"
"InboundsDesc" = "پذیرش کلاینت خاص"
"Outbounds" = "خروجی‌ها"
"Balancers" = "بالانسرها"
"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
"Routings" = "قوانین مسیریابی"
"RoutingsDesc" = "اولویت هر قانون مهم است"
"completeTemplate" = "کامل"
"logLevel" = "سطح گزارش"
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
"accessLog" = "مسیر گزارش"
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند."
"errorLog" = "گزارش خطا"
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
"dnsLog" = "گزارش DNS"
"dnsLogDesc" = "آیا ثبت‌های درخواست DNS را فعال کنید"
"maskAddress" = "پنهان کردن آدرس"
"maskAddressDesc" = "پوشش آدرس IP، هنگامی که فعال می‌شود، به طور خودکار آدرس IP که در لاگ ظاهر می‌شود را جایگزین می‌کند."
"statistics" = "آمار"
"statsInboundUplink" = "آمار آپلود ورودی"
"statsInboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های ورودی را فعال می‌کند."
"statsInboundDownlink" = "آمار دانلود ورودی"
"statsInboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های ورودی را فعال می‌کند."
"statsOutboundUplink" = "آمار آپلود خروجی"
"statsOutboundUplinkDesc" = "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند."
"statsOutboundDownlink" = "آمار دانلود خروجی"
"statsOutboundDownlinkDesc" = "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند."
[pages.xray.rules]
"first" = "اولین"
"last" = "آخرین"
"up" = "بالا"
"down" = "پایین"
"source" = "مبدا"
"dest" = "مقصد"
"inbound" = "ورودی"
"outbound" = "خروجی"
"balancer" = "بالانسر"
"info" = "اطلاعات"
"add" = "افزودن قانون"
"edit" = "ویرایش قانون"
"useComma" = "موارد جدا شده با کاما"
[pages.xray.outbound]
"addOutbound" = "افزودن خروجی"
"addReverse" = "افزودن معکوس"
"editOutbound" = "ویرایش خروجی"
"editReverse" = "ویرایش معکوس"
"tag" = "برچسب"
"tagDesc" = "برچسب یگانه"
"address" = "آدرس"
"reverse" = "معکوس"
"domain" = "دامنه"
"type" = "نوع"
"bridge" = "پل"
"portal" = "پورتال"
"link" = "لینک"
"intercon" = "اتصال میانی"
"settings" = "تنظیمات"
"accountInfo" = "اطلاعات حساب"
"outboundStatus" = "وضعیت خروجی"
"sendThrough" = "ارسال با"
"test" = "تست"
"testResult" = "نتیجه تست"
"testing" = "در حال تست اتصال..."
"testSuccess" = "تست موفقیت‌آمیز"
"testFailed" = "تست ناموفق"
"testError" = "خطا در تست خروجی"
[pages.xray.balancer]
"addBalancer" = "افزودن بالانسر"
"editBalancer" = "ویرایش بالانسر"
"balancerStrategy" = "استراتژی"
"balancerSelectors" = "انتخاب‌گرها"
"tag" = "برچسب"
"tagDesc" = "برچسب یگانه"
"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
[pages.xray.wireguard]
"secretKey" = "کلید شخصی"
"publicKey" = "کلید عمومی"
"allowedIPs" = "آی‌پی‌های مجاز"
"endpoint" = "نقطه پایانی"
"psk" = "کلید مشترک"
"domainStrategy" = "استراتژی حل دامنه"
[pages.xray.tun]
"nameDesc" = "نام رابط TUN. مقدار پیش‌فرض 'xray0' است"
"mtuDesc" = "واحد انتقال حداکثر. بیشترین اندازه بسته‌های داده. مقدار پیش‌فرض 1500 است"
"userLevel" = "سطح کاربر"
"userLevelDesc" = "تمام اتصالات انجام‌شده از طریق این ورودی از این سطح کاربری استفاده خواهند کرد. مقدار پیش‌فرض 0 است"
[pages.xray.dns]
"enable" = "فعال کردن حل دامنه"
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
"tag" = "برچسب"
"tagDesc" = "این برچسب در قوانین مسیریابی به عنوان یک برچسب ورودی قابل استفاده خواهد بود"
"clientIp" = "آی‌پی کلاینت"
"clientIpDesc" = "برای اطلاع‌رسانی به سرور درباره مکان IP مشخص‌شده در طول درخواست‌های DNS استفاده می‌شود"
"disableCache" = "غیرفعال‌سازی کش"
"disableCacheDesc" = "کش DNS را غیرفعال می‌کند"
"disableFallback" = "غیرفعال‌سازی Fallback"
"disableFallbackDesc" = "درخواست‌های DNS Fallback را غیرفعال می‌کند"
"disableFallbackIfMatch" = "غیرفعال‌سازی Fallback در صورت تطابق"
"disableFallbackIfMatchDesc" = "درخواست‌های DNS Fallback را زمانی که لیست دامنه‌های مطابقت‌یافته سرور DNS فعال است، غیرفعال می‌کند"
"enableParallelQuery" = "فعال‌سازی پرس‌وجوی موازی"
"enableParallelQueryDesc" = "فعال‌سازی پرس‌وجوهای DNS موازی به چندین سرور برای وضوح سریع‌تر"
"strategy" = "استراتژی پرس‌وجو"
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
"add" = "افزودن سرور"
"edit" = "ویرایش سرور"
"domains" = "دامنه‌ها"
"expectIPs" = "آی‌پی‌های مورد انتظار"
"unexpectIPs" = "آی‌پی‌های غیرمنتظره"
"useSystemHosts" = "استفاده از Hosts سیستم"
"useSystemHostsDesc" = "استفاده از فایل hosts یک سیستم نصب‌شده"
"usePreset" = "استفاده از پیش‌تنظیم"
"dnsPresetTitle" = "پیش‌تنظیم‌های DNS"
"dnsPresetFamily" = "خانوادگی"
[pages.xray.fakedns]
"add" = "افزودن دی‌ان‌اس جعلی"
"edit" = "ویرایش دی‌ان‌اس جعلی"
"ipPool" = "زیرشبکه استخر آی‌پی"
"poolSize" = "اندازه استخر"
[pages.settings.security]
"admin" = "اعتبارنامه‌های ادمین"
"twoFactor" = "احراز هویت دو مرحله‌ای"
"twoFactorEnable" = "فعال‌سازی 2FA"
"twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند."
"twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای"
"twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای"
"twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:"
"twoFactorModalFirstStep" = "1. این کد QR را در برنامه احراز هویت اسکن کنید یا توکن کنار کد QR را کپی کرده و در برنامه بچسبانید"
"twoFactorModalSecondStep" = "2. کد را از برنامه وارد کنید"
"twoFactorModalRemoveStep" = "برای حذف احراز هویت دو مرحله‌ای، کد را از برنامه وارد کنید."
"twoFactorModalChangeCredentialsTitle" = "تغییر اعتبارنامه‌ها"
"twoFactorModalChangeCredentialsStep" = "برای تغییر اعتبارنامه‌های مدیر، کد را از برنامه وارد کنید."
"twoFactorModalSetSuccess" = "احراز هویت دو مرحله‌ای با موفقیت برقرار شد"
"twoFactorModalDeleteSuccess" = "احراز هویت دو مرحله‌ای با موفقیت حذف شد"
"twoFactorModalError" = "کد نادرست"
[pages.settings.toasts]
"modifySettings" = "پارامترها تغییر کرده‌اند."
"getSettings" = "خطا در دریافت پارامترها"
"modifyUserError" = "خطا در تغییر اعتبارنامه‌های مدیر سیستم."
"modifyUser" = "شما با موفقیت اعتبارنامه‌های مدیر سیستم را تغییر دادید."
"originalUserPassIncorrect" = "نام‌کاربری یا رمزعبور فعلی اشتباه‌است"
"userPassMustBeNotEmpty" = "نام‌کاربری یا رمزعبور جدید خالی‌است"
"getOutboundTrafficError" = "خطا در دریافت ترافیک خروجی"
"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
[tgbot]
"keyboardClosed" = "❌ صفحه کلید بسته شد!"
"noResult" = "❗ نتیجه ای یافت نشد!"
"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
"wentWrong" = "❌ مشکلی پیش آمد!"
"noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ نامحدود(ریست)"
"add" = "افزودن"
"month" = "ماه"
"months" = "ماه"
"day" = "روز"
"days" = "روز"
"hours" = "ساعت"
"minutes" = "دقیقه"
"unknown" = "نامشخص"
"inbounds" = "ورودی ها"
"clients" = "کاربران"
"offline" = "🔴 آفلاین"
"online" = "🟢 آنلاین"
[tgbot.commands]
"unknown" = "❗ دستور ناشناخته"
"pleaseChoose" = "👇 لطفاً انتخاب کنید:\r\n"
"help" = "🤖 به این ربات خوش آمدید! این ربات برای ارائه داده‌های خاص از سرور طراحی شده است و به شما امکان تغییرات لازم را می‌دهد.\r\n\r\n"
"start" = "👋 سلام <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
"status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای راه‌اندازی مجدد Xray Core:\r\n<code>/restart</code>\r\n\r\nبرای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n\r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیحات]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>"
"helpClientCommands" = "برای جستجوی آمار، از دستور زیر استفاده کنید:\r\n<code>/usage [ایمیل]</code>\r\n\r\nشناسه گفتگوی تلگرام:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ عملیات با موفقیت انجام شد!"
"restartFailed" = "❗ خطا در عملیات.\r\n\r\n<code>خطا: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core در حال اجرا نیست."
"startDesc" = "نمایش منوی اصلی"
"helpDesc" = "راهنمای ربات"
"statusDesc" = "بررسی وضعیت ربات"
"idDesc" = "نمایش شناسه تلگرام شما"
[tgbot.messages]
"cpuThreshold" = "🔴 بار ‌پردازنده {{ .Percent }}% بیشتر از آستانه است {{ .Threshold }}%"
"selectUserFailed" = "❌ خطا در انتخاب کاربر!"
"userSaved" = "✅ کاربر تلگرام ذخیره شد."
"loginSuccess" = "✅ با موفقیت به پنل وارد شدید.\r\n"
"loginFailed" = "❗️ ورود به پنل ناموفق‌بود \r\n"
"2faFailed" = "خطای 2FA"
"report" = "🕰 گزارشات‌زمان‌بندی‌شده: {{ .RunTime }}\r\n"
"datetime" = "⏰ تاریخ‌وزمان: {{ .DateTime }}\r\n"
"hostname" = "💻 نام‌میزبان: {{ .Hostname }}\r\n"
"version" = "🚀 نسخه‌پنل: {{ .Version }}\r\n"
"xrayVersion" = "📡 نسخه‌هسته: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 آدرس‌آی‌پی: {{ .IP }}\r\n"
"ips" = "🔢 آدرس‌های آی‌پی:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ مدت‌کارکردسیستم: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 بارسیستم: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 ترافیک: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " وضعیت‌ایکس‌ری: {{ .State }}\r\n"
"username" = "👤 نام‌کاربری: {{ .Username }}\r\n"
"password" = "👤 رمز عبور: {{ .Password }}\r\n"
"time" = "⏰ زمان: {{ .Time }}\r\n"
"inbound" = "📍 نام‌ورودی: {{ .Remark }}\r\n"
"port" = "🔌 پورت: {{ .Port }}\r\n"
"expire" = "📅 تاریخ‌انقضا: {{ .Time }}\r\n\r\n"
"expireIn" = "📅 باقی‌ مانده‌ تا انقضا: {{ .Time }}\r\n\r\n"
"active" = "💡 فعال: {{ .Enable }}\r\n"
"enabled" = "🚨 وضعیت: {{ .Enable }}\r\n"
"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
"lastOnline" = "🔙 آخرین فعالیت: {{ .Time }}\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
"total" = "🔄 کل: {{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 کاربر تلگرام: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} به‌اتمام‌رسیده‌است:\r\n"
"exhaustedCount" = "🚨 تعداد {{ .Type }} به‌اتمام‌رسیده‌است:\r\n"
"onlinesCount" = "🌐 کاربران‌آنلاین: {{ .Count }}\r\n"
"disabled" = "🛑 غیرفعال: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 به‌زودی‌به‌پایان‌خواهدرسید: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 زمان‌پشتیبان‌گیری: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 تازه‌سازی شده در: {{ .Time }}\r\n\r\n"
"yes" = "✅ بله"
"no" = "❌ خیر"
"received_id" = "🔑📥 شناسه به‌روزرسانی شد."
"received_password" = "🔑📥 رمز عبور به‌روزرسانی شد."
"received_email" = "📧📥 ایمیل به‌روزرسانی شد."
"received_comment" = "💬📥 نظر به‌روزرسانی شد."
"id_prompt" = "🔑 شناسه پیش‌فرض: {{ .ClientId }}\n\nشناسه خود را وارد کنید."
"pass_prompt" = "🔑 رمز عبور پیش‌فرض: {{ .ClientPassword }}\n\nرمز عبور خود را وارد کنید."
"email_prompt" = "📧 ایمیل پیش‌فرض: {{ .ClientEmail }}\n\nایمیل خود را وارد کنید."
"comment_prompt" = "💬 نظر پیش‌فرض: {{ .ClientComment }}\n\nنظر خود را وارد کنید."
"inbound_client_data_id" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 شناسه: {{ .ClientId }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!"
"inbound_client_data_pass" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 رمز عبور: {{ .ClientPass }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!"
"cancel" = "❌ فرآیند لغو شد! \n\nمیتوانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄"
"error_add_client" = "⚠️ خطا:\n\n {{ .error }}"
"using_default_value" = "باشه، از مقدار پیش‌فرض استفاده می‌کنم. 😊"
"incorrect_input" = "ورودی شما معتبر نیست.\nعبارتها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫"
"AreYouSure" = "مطمئنی؟ 🤔"
"SuccessResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیت‌آمیز"
"FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠 خطا: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 فرآیند بازنشانی ترافیک برای همه مشتریان به پایان رسید."
[tgbot.buttons]
"closeKeyboard" = "❌ بستن کیبورد"
"cancel" = "❌ لغو"
"cancelReset" = "❌ لغو تنظیم مجدد"
"cancelIpLimit" = "❌ لغو محدودیت آی‌پی"
"confirmResetTraffic" = "✅ تأیید تنظیم مجدد ترافیک؟"
"confirmClearIps" = "✅ تأیید پاک‌سازی آدرس‌های آی‌پی؟"
"confirmRemoveTGUser" = "✅ تأیید حذف کاربر تلگرام؟"
"confirmToggle" = "✅ تایید فعال/غیرفعال کردن کاربر؟"
"dbBackup" = "دریافت پشتیبان"
"serverUsage" = "استفاده از سیستم"
"getInbounds" = "دریافت ورودی‌ها"
"depleteSoon" = "به‌زودی به پایان خواهد رسید"
"clientUsage" = "دریافت آمار کاربر"
"onlines" = "کاربران آنلاین"
"commands" = "دستورات"
"refresh" = "🔄 تازه‌سازی"
"clearIPs" = "❌ پاک‌سازی آدرس‌ها"
"removeTGUser" = "❌ حذف کاربر تلگرام"
"selectTGUser" = "👤 انتخاب کاربر تلگرام"
"selectOneTGUser" = "👤 یک کاربر تلگرام را انتخاب کنید:"
"resetTraffic" = "📈 تنظیم مجدد ترافیک"
"resetExpire" = "📅 تنظیم مجدد تاریخ انقضا"
"ipLog" = "🔢 لاگ آدرس‌های IP"
"ipLimit" = "🔢 محدودیت IP"
"setTGUser" = "👤 تنظیم کاربر تلگرام"
"toggle" = "🔘 فعال / غیرفعال"
"custom" = "🔢 سفارشی"
"confirmNumber" = "✅ تایید: {{ .Num }}"
"confirmNumberAdd" = "✅ تایید اضافه کردن: {{ .Num }}"
"limitTraffic" = "🚧 محدودیت ترافیک"
"getBanLogs" = "گزارش های بلوک را دریافت کنید"
"allClients" = "همه مشتریان"
"addClient" = "افزودن مشتری"
"submitDisable" = "ارسال به عنوان غیرفعال ☑️"
"submitEnable" = "ارسال به عنوان فعال ✅"
"use_default" = "🏷️ استفاده از پیش‌فرض"
"change_id" = "⚙️🔑 شناسه"
"change_password" = "⚙️🔑 گذرواژه"
"change_email" = "⚙️📧 ایمیل"
"change_comment" = "⚙️💬 نظر"
"ResetAllTraffics" = "بازنشانی همه ترافیک‌ها"
"SortedTrafficUsageReport" = "گزارش استفاده از ترافیک مرتب‌شده"
[tgbot.answers]
"successfulOperation" = "✅ انجام شد!"
"errorOperation" = "❗ خطا در عملیات."
"getInboundsFailed" = "❌ دریافت ورودی‌ها با خطا مواجه شد."
"getClientsFailed" = "❌ دریافت مشتریان با شکست مواجه شد."
"canceled" = "❌ {{ .Email }} : عملیات لغو شد."
"clientRefreshSuccess" = "✅ {{ .Email }} : کلاینت با موفقیت تازه‌سازی شد."
"IpRefreshSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت تازه‌سازی شدند."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : کاربر تلگرام کلاینت با موفقیت تازه‌سازی شد."
"resetTrafficSuccess" = "✅ {{ .Email }} : ترافیک با موفقیت تنظیم مجدد شد."
"setTrafficLimitSuccess" = "✅ {{ .Email }} : محدودیت ترافیک با موفقیت ذخیره شد."
"expireResetSuccess" = "✅ {{ .Email }} : تاریخ انقضا با موفقیت تنظیم مجدد شد."
"resetIpSuccess" = "✅ {{ .Email }} : محدودیت آدرس IP {{ .Count }} با موفقیت ذخیره شد."
"clearIpSuccess" = "✅ {{ .Email }} : آدرس‌ها با موفقیت پاک‌سازی شدند."
"getIpLog" = "✅ {{ .Email }} : دریافت لاگ آدرس‌های IP."
"getUserInfo" = "✅ {{ .Email }} : دریافت اطلاعات کاربر تلگرام."
"removedTGUserSuccess" = "✅ {{ .Email }} : کاربر تلگرام با موفقیت حذف شد."
"enableSuccess" = "✅ {{ .Email }} : با موفقیت فعال شد."
"disableSuccess" = "✅ {{ .Email }} : با موفقیت غیرفعال شد."
"askToAddUserId" = "پیکربندی شما یافت نشد!\r\nلطفاً از مدیر خود بخواهید که شناسه کاربر تلگرام خود را در پیکربندی (های) خود استفاده کند.\r\n\r\nشناسه کاربری شما: <code>{{ .TgUserID }}</code>"
"chooseClient" = "یک مشتری برای ورودی {{ .Inbound }} انتخاب کنید"
"chooseInbound" = "یک ورودی انتخاب کنید"

View file

@ -1,795 +0,0 @@
"username" = "Nama Pengguna"
"password" = "Kata Sandi"
"login" = "Masuk"
"confirm" = "Konfirmasi"
"cancel" = "Batal"
"close" = "Tutup"
"create" = "Buat"
"update" = "Perbarui"
"copy" = "Salin"
"copied" = "Tersalin"
"download" = "Unduh"
"remark" = "Catatan"
"enable" = "Aktifkan"
"protocol" = "Protokol"
"search" = "Cari"
"filter" = "Filter"
"loading" = "Memuat..."
"second" = "Detik"
"minute" = "Menit"
"hour" = "Jam"
"day" = "Hari"
"check" = "Centang"
"indefinite" = "Tak Terbatas"
"unlimited" = "Tanpa Batas"
"none" = "None"
"qrCode" = "Kode QR"
"info" = "Informasi Lebih Lanjut"
"edit" = "Edit"
"delete" = "Hapus"
"reset" = "Reset"
"noData" = "Tidak ada data."
"copySuccess" = "Berhasil Disalin"
"sure" = "Yakin"
"encryption" = "Enkripsi"
"useIPv4ForHost" = "Gunakan IPv4 untuk host"
"transmission" = "Transmisi"
"host" = "Host"
"path" = "Jalur"
"camouflage" = "Obfuscation"
"status" = "Status"
"enabled" = "Aktif"
"disabled" = "Nonaktif"
"depleted" = "Habis"
"depletingSoon" = "Akan Habis"
"offline" = "Offline"
"online" = "Online"
"domainName" = "Nama Domain"
"monitor" = "IP Pemantauan"
"certificate" = "Sertifikat Digital"
"fail" = "Gagal"
"comment" = "Komentar"
"success" = "Berhasil"
"lastOnline" = "Terakhir online"
"getVersion" = "Dapatkan Versi"
"install" = "Instal"
"clients" = "Klien"
"usage" = "Penggunaan"
"twoFactorCode" = "Kode"
"remained" = "Tersisa"
"security" = "Keamanan"
"secAlertTitle" = "Peringatan keamanan"
"secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data."
"secAlertConf" = "Beberapa pengaturan rentan terhadap serangan. Disarankan untuk memperkuat protokol keamanan guna mencegah pelanggaran potensial."
"secAlertSSL" = "Panel kekurangan koneksi yang aman. Harap instal sertifikat TLS untuk perlindungan data."
"secAlertPanelPort" = "Port default panel rentan. Harap konfigurasi port acak atau tertentu."
"secAlertPanelURI" = "Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks."
"secAlertSubURI" = "Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks."
"secAlertSubJsonURI" = "Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks."
"emptyDnsDesc" = "Tidak ada server DNS yang ditambahkan."
"emptyFakeDnsDesc" = "Tidak ada server Fake DNS yang ditambahkan."
"emptyBalancersDesc" = "Tidak ada penyeimbang yang ditambahkan."
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
"somethingWentWrong" = "Terjadi kesalahan"
[subscription]
"title" = "Info langganan"
"subId" = "ID langganan"
"status" = "Status"
"downloaded" = "Diunduh"
"uploaded" = "Diunggah"
"expiry" = "Kedaluwarsa"
"totalQuota" = "Kuota total"
"individualLinks" = "Tautan individual"
"active" = "Aktif"
"inactive" = "Nonaktif"
"unlimited" = "Tanpa batas"
"noExpiry" = "Tanpa kedaluwarsa"
[menu]
"theme" = "Tema"
"dark" = "Gelap"
"ultraDark" = "Sangat Gelap"
"dashboard" = "Ikhtisar"
"inbounds" = "Masuk"
"settings" = "Pengaturan Panel"
"xray" = "Konfigurasi Xray"
"logout" = "Keluar"
"link" = "Kelola"
[pages.login]
"hello" = "Halo"
"title" = "Selamat Datang"
"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali"
[pages.login.toasts]
"invalidFormData" = "Format data input tidak valid."
"emptyUsername" = "Nama Pengguna diperlukan"
"emptyPassword" = "Kata Sandi diperlukan"
"wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid."
"successLogin" = "Anda telah berhasil masuk ke akun Anda."
"successRegister" = "Pendaftaran berhasil, silakan masuk."
"userExists" = "Nama pengguna sudah ada"
"errorRegister" = "Pendaftaran gagal"
[pages.index]
"title" = "Ikhtisar"
"cpu" = "CPU"
"logicalProcessors" = "Prosesor logis"
"frequency" = "Frekuensi"
"swap" = "Swap"
"storage" = "Penyimpanan"
"memory" = "RAM"
"threads" = "Thread"
"xrayStatus" = "Xray"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Versi"
"xraySwitchClick" = "Pilih versi yang ingin Anda pindah."
"xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini."
"xrayStatusUnknown" = "Tidak diketahui"
"xrayStatusRunning" = "Berjalan"
"xrayStatusStop" = "Berhenti"
"xrayStatusError" = "Kesalahan"
"xrayErrorPopoverTitle" = "Terjadi kesalahan saat menjalankan Xray"
"operationHours" = "Waktu Aktif"
"systemLoad" = "Beban Sistem"
"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir"
"connectionCount" = "Statistik Koneksi"
"ipAddresses" = "Alamat IP"
"toggleIpVisibility" = "Alihkan visibilitas IP"
"overallSpeed" = "Kecepatan keseluruhan"
"upload" = "Unggah"
"download" = "Unduh"
"totalData" = "Total data"
"sent" = "Dikirim"
"received" = "Diterima"
"documentation" = "Dokumentasi"
"xraySwitchVersionDialog" = "Apakah Anda yakin ingin mengubah versi Xray?"
"xraySwitchVersionDialogDesc" = "Ini akan mengubah versi Xray ke #version#."
"xraySwitchVersionPopover" = "Xray berhasil diperbarui"
"geofileUpdateDialog" = "Apakah Anda yakin ingin memperbarui geofile?"
"geofileUpdateDialogDesc" = "Ini akan memperbarui file #filename#."
"geofilesUpdateDialogDesc" = "Ini akan memperbarui semua berkas."
"geofilesUpdateAll" = "Perbarui semua"
"geofileUpdatePopover" = "Geofile berhasil diperbarui"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log"
"config" = "Konfigurasi"
"backup" = "Cadangan"
"backupTitle" = "Cadangan & Pulihkan Database"
"exportDatabase" = "Cadangkan"
"exportDatabaseDesc" = "Klik untuk mengunduh file .db yang berisi cadangan dari database Anda saat ini ke perangkat Anda."
"importDatabase" = "Pulihkan"
"importDatabaseDesc" = "Klik untuk memilih dan mengunggah file .db dari perangkat Anda untuk memulihkan database dari cadangan."
"importDatabaseSuccess" = "Database berhasil diimpor"
"importDatabaseError" = "Terjadi kesalahan saat mengimpor database"
"readDatabaseError" = "Terjadi kesalahan saat membaca database"
"getDatabaseError" = "Terjadi kesalahan saat mengambil database"
"getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi"
[pages.inbounds]
"allTimeTraffic" = "Total Lalu Lintas"
"allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu"
"title" = "Masuk"
"totalDownUp" = "Total Terkirim/Diterima"
"totalUsage" = "Penggunaan Total"
"inboundCount" = "Total Masuk"
"operate" = "Menu"
"enable" = "Aktifkan"
"remark" = "Catatan"
"protocol" = "Protokol"
"port" = "Port"
"portMap" = "Port Mapping"
"traffic" = "Traffic"
"details" = "Rincian"
"transportConfig" = "Transport"
"expireDate" = "Durasi"
"createdAt" = "Dibuat"
"updatedAt" = "Diperbarui"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum"
"autoRefresh" = "Pembaruan otomatis"
"autoRefreshInterval" = "Interval"
"modifyInbound" = "Ubah Masuk"
"deleteInbound" = "Hapus Masuk"
"deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?"
"deleteClient" = "Hapus Klien"
"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?"
"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?"
"copyLink" = "Salin URL"
"address" = "Alamat"
"network" = "Jaringan"
"destinationPort" = "Port Tujuan"
"targetAddress" = "Alamat Target"
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
"meansNoLimit" = "= Unlimited. (unit: GB)"
"totalFlow" = "Total Aliran"
"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
"certificatePath" = "Path Berkas"
"certificateContent" = "Konten Berkas"
"publicKey" = "Kunci Publik"
"privatekey" = "Kunci Pribadi"
"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin"
"client" = "Klien"
"export" = "Ekspor Semua URL"
"clone" = "Duplikat"
"cloneInbound" = "Duplikat"
"cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat."
"cloneInboundOk" = "Duplikat"
"resetAllTraffic" = "Reset Semua Traffic Masuk"
"resetAllTrafficTitle" = "Reset Semua Traffic Masuk"
"resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?"
"resetInboundClientTraffics" = "Reset Traffic Klien Masuk"
"resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk"
"resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?"
"resetAllClientTraffics" = "Reset Traffic Semua Klien"
"resetAllClientTrafficTitle" = "Reset Traffic Semua Klien"
"resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?"
"delDepletedClients" = "Hapus Klien Habis"
"delDepletedClientsTitle" = "Hapus Klien Habis"
"delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?"
"email" = "Email"
"emailDesc" = "Harap berikan alamat email yang unik."
"IPLimit" = "Batas IP"
"IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)"
"IPLimitlog" = "Log IP"
"IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)"
"IPLimitlogclear" = "Hapus Log"
"setDefaultCert" = "Atur Sertifikat dari Panel"
"telegramDesc" = "Harap berikan ID Obrolan Telegram. (gunakan perintah '/id' di bot) atau (@userinfobot)"
"subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien."
"info" = "Info"
"same" = "Sama"
"inboundData" = "Data Masuk"
"exportInbound" = "Ekspor Masuk"
"import" = "Impor"
"importInbound" = "Impor Masuk"
"periodicTrafficResetTitle" = "Reset Trafik Berkala"
"periodicTrafficResetDesc" = "Reset otomatis penghitung trafik pada interval tertentu"
"lastReset" = "Reset Terakhir"
[pages.client]
"add" = "Tambah Klien"
"edit" = "Edit Klien"
"submitAdd" = "Tambah Klien"
"submitEdit" = "Simpan Perubahan"
"clientCount" = "Jumlah Klien"
"bulk" = "Tambahkan Massal"
"method" = "Metode"
"first" = "Pertama"
"last" = "Terakhir"
"prefix" = "Awalan"
"postfix" = "Akhiran"
"delayedStart" = "Mulai Awal"
"expireDays" = "Durasi"
"days" = "Hari"
"renew" = "Perpanjang Otomatis"
"renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)"
[pages.inbounds.periodicTrafficReset]
"never" = "Tidak Pernah"
"daily" = "Harian"
"weekly" = "Mingguan"
"monthly" = "Bulanan"
[pages.inbounds.toasts]
"obtain" = "Dapatkan"
"updateSuccess" = "Pembaruan berhasil"
"logCleanSuccess" = "Log telah dibersihkan"
"inboundsUpdateSuccess" = "Inbound berhasil diperbarui"
"inboundUpdateSuccess" = "Inbound berhasil diperbarui"
"inboundCreateSuccess" = "Inbound berhasil dibuat"
"inboundDeleteSuccess" = "Inbound berhasil dihapus"
"inboundClientAddSuccess" = "Klien inbound telah ditambahkan"
"inboundClientDeleteSuccess" = "Klien inbound telah dihapus"
"inboundClientUpdateSuccess" = "Klien inbound telah diperbarui"
"delDepletedClientsSuccess" = "Semua klien yang habis telah dihapus"
"resetAllClientTrafficSuccess" = "Semua lalu lintas klien telah direset"
"resetAllTrafficSuccess" = "Semua lalu lintas telah direset"
"resetInboundClientTrafficSuccess" = "Lalu lintas telah direset"
"trafficGetError" = "Gagal mendapatkan data lalu lintas"
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
"getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
[pages.inbounds.stream.general]
"request" = "Permintaan"
"response" = "Respons"
"name" = "Nama"
"value" = "Nilai"
[pages.inbounds.stream.tcp]
"version" = "Versi"
"method" = "Metode"
"path" = "Path"
"status" = "Status"
"statusDescription" = "Deskripsi Status"
"requestHeader" = "Header Permintaan"
"responseHeader" = "Header Respons"
[pages.settings]
"title" = "Pengaturan Panel"
"save" = "Simpan"
"infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan."
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server."
"restartPanelSuccess" = "Panel berhasil dimulai ulang"
"actions" = "Tindakan"
"resetDefaultConfig" = "Reset ke Default"
"panelSettings" = "Umum"
"securitySettings" = "Otentikasi"
"TGBotSettings" = "Bot Telegram"
"panelListeningIP" = "IP Pendengar"
"panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)"
"panelListeningDomain" = "Domain Pendengar"
"panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)"
"panelPort" = "Port Pendengar"
"panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)"
"publicKeyPath" = "Path Kunci Publik"
"publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan /)"
"privateKeyPath" = "Path Kunci Privat"
"privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan /)"
"panelUrlPath" = "URI Path"
"panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan / dan diakhiri dengan /)"
"pageSize" = "Ukuran Halaman"
"pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)"
"remarkModel" = "Model Catatan & Karakter Pemisah"
"datepicker" = "Jenis Kalender"
"datepickerPlaceholder" = "Pilih tanggal"
"datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini."
"sampleRemark" = "Contoh Catatan"
"oldUsername" = "Username Saat Ini"
"currentPassword" = "Kata Sandi Saat Ini"
"newUsername" = "Username Baru"
"newPassword" = "Kata Sandi Baru"
"telegramBotEnable" = "Aktifkan Bot Telegram"
"telegramBotEnableDesc" = "Mengaktifkan bot Telegram."
"telegramToken" = "Token Telegram"
"telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'."
"telegramProxy" = "Proxy SOCKS"
"telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)"
"telegramAPIServer" = "Telegram API Server"
"telegramAPIServerDesc" = "Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default."
"telegramChatId" = "ID Obrolan Admin"
"telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
"telegramNotifyTime" = "Waktu Notifikasi"
"telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)"
"tgNotifyBackup" = "Cadangan Database"
"tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan."
"tgNotifyLogin" = "Notifikasi Login"
"tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda."
"sessionMaxAge" = "Durasi Sesi"
"sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)"
"expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa"
"expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)"
"trafficDiff" = "Notifikasi Batas Traffic"
"trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)"
"tgNotifyCpu" = "Notifikasi Beban CPU"
"tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)"
"timeZone" = "Zone Waktu"
"timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini."
"subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan."
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
"subTitle" = "Judul Langganan"
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
"subSupportUrl" = "URL Dukungan"
"subSupportUrlDesc" = "Tautan dukungan teknis yang ditampilkan di klien VPN"
"subProfileUrl" = "URL Profil"
"subProfileUrlDesc" = "Tautan ke situs web Anda yang ditampilkan di klien VPN"
"subAnnounce" = "Pengumuman"
"subAnnounceDesc" = "Teks pengumuman yang ditampilkan di klien VPN"
"subEnableRouting" = "Aktifkan perutean"
"subEnableRoutingDesc" = "Pengaturan global untuk mengaktifkan perutean (routing) di klien VPN. (Hanya untuk Happ)"
"subRoutingRules" = "Aturan routing"
"subRoutingRulesDesc" = "Aturan routing global untuk klien VPN. (Hanya untuk Happ)"
"subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar"
"subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)"
"subCertPath" = "Path Kunci Publik"
"subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan /)"
"subKeyPath" = "Path Kunci Privat"
"subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan /)"
"subPath" = "URI Path"
"subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan / dan diakhiri dengan /)"
"subDomain" = "Domain Pendengar"
"subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)"
"subUpdates" = "Interval Pembaruan"
"subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)"
"subEncrypt" = "Encode"
"subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64."
"subShowInfo" = "Tampilkan Info Penggunaan"
"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
"subURI" = "URI Proxy Terbalik"
"subURIDesc" = "Path URI dari URL langganan untuk digunakan di belakang proxy."
"externalTrafficInformEnable" = "Informasikan API eksternal pada setiap pembaruan lalu lintas."
"externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
"externalTrafficInformURI" = "Lalu Lintas Eksternal Menginformasikan URI"
"externalTrafficInformURIDesc" = "Pembaruan lalu lintas dikirim ke URI ini."
"fragment" = "Fragmentasi"
"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
"fragmentSett" = "Pengaturan Fragmentasi"
"noisesDesc" = "Aktifkan Noises."
"noisesSett" = "Pengaturan Noises"
"mux" = "Mux"
"muxDesc" = "Mengirimkan beberapa aliran data independen dalam aliran data yang sudah ada."
"muxSett" = "Pengaturan Mux"
"direct" = "Koneksi langsung"
"directDesc" = "Secara langsung membuat koneksi dengan domain atau rentang IP negara tertentu."
"notifications" = "Notifikasi"
"certs" = "Sertifikat"
"externalTraffic" = "Lalu Lintas Eksternal"
"dateAndTime" = "Tanggal dan Waktu"
"proxyAndServer" = "Proxy dan Server"
"intervals" = "Interval"
"information" = "Informasi"
"language" = "Bahasa"
"telegramBotLanguage" = "Bahasa Bot Telegram"
[pages.xray]
"title" = "Konfigurasi Xray"
"save" = "Simpan"
"restart" = "Restart Xray"
"restartSuccess" = "Xray berhasil diluncurkan ulang"
"stopSuccess" = "Xray telah berhasil dihentikan"
"restartError" = "Terjadi kesalahan saat memulai ulang Xray."
"stopError" = "Terjadi kesalahan saat menghentikan Xray."
"basicTemplate" = "Dasar"
"advancedTemplate" = "Lanjutan"
"generalConfigs" = "Strategi Umum"
"generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum."
"logConfigs" = "Catatan"
"logConfigsDesc" = "Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan"
"blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta."
"basicRouting" = "Perutean Dasar"
"blockConnectionsConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta."
"directConnectionsConfigsDesc" = "Koneksi langsung memastikan bahwa lalu lintas tertentu tidak dialihkan melalui server lain."
"blockips" = "Blokir IP"
"blockdomains" = "Blokir Domain"
"directips" = "IP Langsung"
"directdomains" = "Domain Langsung"
"ipv4Routing" = "Perutean IPv4"
"ipv4RoutingDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4."
"warpRouting" = "Perutean WARP"
"warpRoutingDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP."
"Template" = "Template Konfigurasi Xray Lanjutan"
"TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini."
"FreedomStrategy" = "Strategi Protokol Freedom"
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
"outboundTestUrl" = "URL tes outbound"
"outboundTestUrlDesc" = "URL yang digunakan saat menguji konektivitas outbound"
"Torrent" = "Blokir Protokol BitTorrent"
"Inbounds" = "Masuk"
"InboundsDesc" = "Menerima klien tertentu."
"Outbounds" = "Keluar"
"Balancers" = "Penyeimbang"
"OutboundsDesc" = "Atur jalur lalu lintas keluar."
"Routings" = "Aturan Pengalihan"
"RoutingsDesc" = "Prioritas setiap aturan penting!"
"completeTemplate" = "Semua"
"logLevel" = "Tingkat Log"
"logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat."
"accessLog" = "Log Akses"
"accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses"
"errorLog" = "Catatan eror"
"errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan"
"dnsLog" = "Log DNS"
"dnsLogDesc" = "Apakah akan mengaktifkan log kueri DNS"
"maskAddress" = "Alamat Masker"
"maskAddressDesc" = "Masker alamat IP, ketika diaktifkan, akan secara otomatis mengganti alamat IP yang muncul di log."
"statistics" = "Statistik"
"statsInboundUplink" = "Statistik Unggah Masuk"
"statsInboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy masuk."
"statsInboundDownlink" = "Statistik Unduh Masuk"
"statsInboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy masuk."
"statsOutboundUplink" = "Statistik Unggah Keluar"
"statsOutboundUplinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unggah dari semua proxy keluar."
"statsOutboundDownlink" = "Statistik Unduh Keluar"
"statsOutboundDownlinkDesc" = "Mengaktifkan pengumpulan statistik untuk lalu lintas unduh dari semua proxy keluar."
[pages.xray.rules]
"first" = "Pertama"
"last" = "Terakhir"
"up" = "Naik"
"down" = "Turun"
"source" = "Sumber"
"dest" = "Tujuan"
"inbound" = "Masuk"
"outbound" = "Keluar"
"balancer" = "Pengimbang"
"info" = "Info"
"add" = "Tambahkan Aturan"
"edit" = "Edit Aturan"
"useComma" = "Item yang dipisahkan koma"
[pages.xray.outbound]
"addOutbound" = "Tambahkan Keluar"
"addReverse" = "Tambahkan Revers"
"editOutbound" = "Edit Keluar"
"editReverse" = "Edit Revers"
"tag" = "Tag"
"tagDesc" = "Tag Unik"
"address" = "Alamat"
"reverse" = "Revers"
"domain" = "Domain"
"type" = "Tipe"
"bridge" = "Jembatan"
"portal" = "Portal"
"link" = "Tautan"
"intercon" = "Interkoneksi"
"settings" = "Pengaturan"
"accountInfo" = "Informasi Akun"
"outboundStatus" = "Status Keluar"
"sendThrough" = "Kirim Melalui"
"test" = "Tes"
"testResult" = "Hasil Tes"
"testing" = "Menguji koneksi..."
"testSuccess" = "Tes berhasil"
"testFailed" = "Tes gagal"
"testError" = "Gagal menguji outbound"
[pages.xray.balancer]
"addBalancer" = "Tambahkan Penyeimbang"
"editBalancer" = "Sunting Penyeimbang"
"balancerStrategy" = "Strategi"
"balancerSelectors" = "Penyeleksi"
"tag" = "Menandai"
"tagDesc" = "Label Unik"
"balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi."
[pages.xray.wireguard]
"secretKey" = "Kunci Rahasia"
"publicKey" = "Kunci Publik"
"allowedIPs" = "IP yang Diizinkan"
"endpoint" = "Titik Akhir"
"psk" = "Kunci Pra-Bagi"
"domainStrategy" = "Strategi Domain"
[pages.xray.tun]
"nameDesc" = "Nama antarmuka TUN. Standar adalah 'xray0'"
"mtuDesc" = "Unit Transmisi Maksimum. Ukuran maksimum paket data. Standar adalah 1500"
"userLevel" = "Level Pengguna"
"userLevelDesc" = "Semua koneksi yang dibuat melalui inbound ini akan menggunakan level pengguna ini. Standar adalah 0"
[pages.xray.dns]
"enable" = "Aktifkan DNS"
"enableDesc" = "Aktifkan server DNS bawaan"
"tag" = "Tanda DNS Masuk"
"tagDesc" = "Tanda ini akan tersedia sebagai tanda masuk dalam aturan penataan."
"clientIp" = "IP Klien"
"clientIpDesc" = "Digunakan untuk memberi tahu server tentang lokasi IP yang ditentukan selama kueri DNS"
"disableCache" = "Nonaktifkan cache"
"disableCacheDesc" = "Menonaktifkan caching DNS"
"disableFallback" = "Nonaktifkan Fallback"
"disableFallbackDesc" = "Menonaktifkan kueri DNS fallback"
"disableFallbackIfMatch" = "Nonaktifkan Fallback Jika Cocok"
"disableFallbackIfMatchDesc" = "Menonaktifkan kueri DNS fallback ketika daftar domain yang cocok dari server DNS terpenuhi"
"enableParallelQuery" = "Aktifkan Kueri Paralel"
"enableParallelQueryDesc" = "Aktifkan kueri DNS paralel ke beberapa server untuk resolusi yang lebih cepat"
"strategy" = "Strategi Kueri"
"strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain"
"add" = "Tambahkan Server"
"edit" = "Sunting Server"
"domains" = "Domains"
"expectIPs" = "IP yang Diharapkan"
"unexpectIPs" = "IP tak terduga"
"useSystemHosts" = "Gunakan Hosts Sistem"
"useSystemHostsDesc" = "Gunakan file hosts dari sistem yang terinstal"
"usePreset" = "Gunakan templat"
"dnsPresetTitle" = "Templat DNS"
"dnsPresetFamily" = "Keluarga"
[pages.xray.fakedns]
"add" = "Tambahkan DNS Palsu"
"edit" = "Edit DNS Palsu"
"ipPool" = "Subnet Kumpulan IP"
"poolSize" = "Ukuran Kolam"
[pages.settings.security]
"admin" = "Kredensial admin"
"twoFactor" = "Autentikasi dua faktor"
"twoFactorEnable" = "Aktifkan 2FA"
"twoFactorEnableDesc" = "Menambahkan lapisan autentikasi tambahan untuk keamanan lebih."
"twoFactorModalSetTitle" = "Aktifkan autentikasi dua faktor"
"twoFactorModalDeleteTitle" = "Nonaktifkan autentikasi dua faktor"
"twoFactorModalSteps" = "Untuk menyiapkan autentikasi dua faktor, lakukan beberapa langkah:"
"twoFactorModalFirstStep" = "1. Pindai kode QR ini di aplikasi autentikasi atau salin token di dekat kode QR dan tempelkan ke aplikasi"
"twoFactorModalSecondStep" = "2. Masukkan kode dari aplikasi"
"twoFactorModalRemoveStep" = "Masukkan kode dari aplikasi untuk menghapus autentikasi dua faktor."
"twoFactorModalChangeCredentialsTitle" = "Ubah kredensial"
"twoFactorModalChangeCredentialsStep" = "Masukkan kode dari aplikasi untuk mengubah kredensial administrator."
"twoFactorModalSetSuccess" = "Autentikasi dua faktor telah berhasil dibuat"
"twoFactorModalDeleteSuccess" = "Autentikasi dua faktor telah berhasil dihapus"
"twoFactorModalError" = "Kode salah"
[pages.settings.toasts]
"modifySettings" = "Parameter telah diubah."
"getSettings" = "Terjadi kesalahan saat mengambil parameter."
"modifyUserError" = "Terjadi kesalahan saat mengubah kredensial administrator."
"modifyUser" = "Anda telah berhasil mengubah kredensial administrator."
"originalUserPassIncorrect" = "Username atau password saat ini tidak valid"
"userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong"
"getOutboundTrafficError" = "Gagal mendapatkan lalu lintas keluar"
"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
[tgbot]
"keyboardClosed" = "❌ Keyboard ditutup!"
"noResult" = "❗ Tidak ada hasil!"
"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
"wentWrong" = "❌ Terjadi kesalahan!"
"noIpRecord" = "❗ Tidak ada Catatan IP!"
"noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
"unlimited" = "♾ Tidak terbatas (Reset)"
"add" = "Tambah"
"month" = "Bulan"
"months" = "Bulan"
"day" = "Hari"
"days" = "Hari"
"hours" = "Jam"
"minutes" = "Menit"
"unknown" = "Tidak diketahui"
"inbounds" = "Inbound"
"clients" = "Klien"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
[tgbot.commands]
"unknown" = "❗ Perintah tidak dikenal."
"pleaseChoose" = "👇 Harap pilih:\r\n"
"help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n"
"start" = "👋 Halo <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Selamat datang di <b>{{.Hostname }}</b> bot managemen.\r\n"
"status" = "✅ Bot dalam keadaan baik!"
"usage" = "❗ Harap berikan teks untuk mencari!"
"getID" = "🆔 ID Anda: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Untuk memulai ulang Xray Core:\r\n<code>/restart</code>\r\n\r\nUntuk mencari email klien:\r\n<code>/usage [Email]</code>\r\n\r\nUntuk mencari inbound (dengan statistik klien):\r\n<code>/inbound [Catatan]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n<code>/usage [Email]</code>\r\n\r\nID Obrolan Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Operasi berhasil!"
"restartFailed" = "❗ Kesalahan dalam operasi.\r\n\r\n<code>Error: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core tidak berjalan."
"startDesc" = "Tampilkan menu utama"
"helpDesc" = "Bantuan bot"
"statusDesc" = "Periksa status bot"
"idDesc" = "Tampilkan ID Telegram Anda"
[tgbot.messages]
"cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%"
"selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!"
"userSaved" = "✅ Pengguna Telegram tersimpan."
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
"2faFailed" = "2FA Gagal"
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
"version" = "🚀 Versi 3X-UI: {{ .Version }}\r\n"
"xrayVersion" = "📡 Versi Xray: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Status: {{ .State }}\r\n"
"username" = "👤 Nama Pengguna: {{ .Username }}\r\n"
"password" = "👤 Kata Sandi: {{ .Password }}\r\n"
"time" = "⏰ Waktu: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n"
"expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n"
"active" = "💡 Aktif: {{ .Enable }}\r\n"
"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n"
"online" = "🌐 Status Koneksi: {{ .Status }}\r\n"
"lastOnline" = "🔙 Terakhir online: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n"
"download" = "🔽 Unduh: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n"
"disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n"
"yes" = "✅ Ya"
"no" = "❌ Tidak"
"received_id" = "🔑📥 ID diperbarui."
"received_password" = "🔑📥 Kata sandi diperbarui."
"received_email" = "📧📥 Email diperbarui."
"received_comment" = "💬📥 Komentar diperbarui."
"id_prompt" = "🔑 ID Default: {{ .ClientId }}\n\nMasukkan ID Anda."
"pass_prompt" = "🔑 Kata Sandi Default: {{ .ClientPassword }}\n\nMasukkan kata sandi Anda."
"email_prompt" = "📧 Email Default: {{ .ClientEmail }}\n\nMasukkan email Anda."
"comment_prompt" = "💬 Komentar Default: {{ .ClientComment }}\n\nMasukkan komentar Anda."
"inbound_client_data_id" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!"
"inbound_client_data_pass" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!"
"cancel" = "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄"
"error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}"
"using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊"
"incorrect_input" = "Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫"
"AreYouSure" = "Apakah kamu yakin? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠 Kesalahan: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Proses reset traffic selesai untuk semua klien."
[tgbot.buttons]
"closeKeyboard" = "❌ Tutup Papan Ketik"
"cancel" = "❌ Batal"
"cancelReset" = "❌ Batal Reset"
"cancelIpLimit" = "❌ Batal Batas IP"
"confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?"
"confirmClearIps" = "✅ Konfirmasi Hapus IPs?"
"confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?"
"confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?"
"dbBackup" = "Dapatkan Cadangan DB"
"serverUsage" = "Penggunaan Server"
"getInbounds" = "Dapatkan Inbounds"
"depleteSoon" = "Habis Sebentar"
"clientUsage" = "Dapatkan Penggunaan"
"onlines" = "Klien Online"
"commands" = "Perintah"
"refresh" = "🔄 Perbarui"
"clearIPs" = "❌ Hapus IPs"
"removeTGUser" = "❌ Hapus Pengguna Telegram"
"selectTGUser" = "👤 Pilih Pengguna Telegram"
"selectOneTGUser" = "👤 Pilih Pengguna Telegram:"
"resetTraffic" = "📈 Reset Lalu Lintas"
"resetExpire" = "📅 Ubah Tanggal Kadaluarsa"
"ipLog" = "🔢 Log IP"
"ipLimit" = "🔢 Batas IP"
"setTGUser" = "👤 Set Pengguna Telegram"
"toggle" = "🔘 Aktifkan / Nonaktifkan"
"custom" = "🔢 Kustom"
"confirmNumber" = "✅ Konfirmasi: {{ .Num }}"
"confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}"
"limitTraffic" = "🚧 Batas Lalu Lintas"
"getBanLogs" = "Dapatkan Log Pemblokiran"
"allClients" = "Semua Klien"
"addClient" = "Tambah Klien"
"submitDisable" = "Kirim Sebagai Nonaktif ☑️"
"submitEnable" = "Kirim Sebagai Aktif ✅"
"use_default" = "🏷️ Gunakan Default"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Kata Sandi"
"change_email" = "⚙️📧 Email"
"change_comment" = "⚙️💬 Komentar"
"ResetAllTraffics" = "Reset Semua Lalu Lintas"
"SortedTrafficUsageReport" = "Laporan Penggunaan Lalu Lintas yang Terurut"
[tgbot.answers]
"successfulOperation" = "✅ Operasi berhasil!"
"errorOperation" = "❗ Kesalahan dalam operasi."
"getInboundsFailed" = "❌ Gagal mendapatkan inbounds."
"getClientsFailed" = "❌ Gagal mendapatkan klien."
"canceled" = "❌ {{ .Email }}: Operasi dibatalkan."
"clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil."
"resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil."
"expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil."
"resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil."
"clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil."
"getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP."
"getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil."
"enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil."
"disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil."
"askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ChatID Telegram Anda dalam konfigurasi Anda.\r\n\r\nChatID Pengguna Anda: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Pilih Klien untuk Inbound {{ .Inbound }}"
"chooseInbound" = "Pilih Inbound"

View file

@ -1,795 +0,0 @@
"username" = "ユーザー名"
"password" = "パスワード"
"login" = "ログイン"
"confirm" = "確認"
"cancel" = "キャンセル"
"close" = "閉じる"
"create" = "作成"
"update" = "更新"
"copy" = "コピー"
"copied" = "コピー済み"
"download" = "ダウンロード"
"remark" = "備考"
"enable" = "有効化"
"protocol" = "プロトコル"
"search" = "検索"
"filter" = "フィルター"
"loading" = "読み込み中..."
"second" = "秒"
"minute" = "分"
"hour" = "時間"
"day" = "日"
"check" = "確認"
"indefinite" = "無期限"
"unlimited" = "無制限"
"none" = "なし"
"qrCode" = "QRコード"
"info" = "詳細情報"
"edit" = "編集"
"delete" = "削除"
"reset" = "リセット"
"noData" = "データなし。"
"copySuccess" = "コピー成功"
"sure" = "確定"
"encryption" = "暗号化"
"useIPv4ForHost" = "ホストにIPv4を使用"
"transmission" = "伝送"
"host" = "ホスト"
"path" = "パス"
"camouflage" = "偽装"
"status" = "ステータス"
"enabled" = "有効"
"disabled" = "無効"
"depleted" = "消耗済み"
"depletingSoon" = "間もなく消耗"
"offline" = "オフライン"
"online" = "オンライン"
"domainName" = "ドメイン名"
"monitor" = "監視"
"certificate" = "証明書"
"fail" = "失敗"
"comment" = "コメント"
"success" = "成功"
"lastOnline" = "最終オンライン"
"getVersion" = "バージョン取得"
"install" = "インストール"
"clients" = "クライアント"
"usage" = "利用状況"
"twoFactorCode" = "コード"
"remained" = "残り"
"security" = "セキュリティ"
"secAlertTitle" = "セキュリティアラート"
"secAlertSsl" = "この接続は安全ではありません。TLSを有効にしてデータ保護を行うまで、機密情報を入力しないでください。"
"secAlertConf" = "一部の設定は脆弱です。潜在的な脆弱性を防ぐために、セキュリティプロトコルを強化することをお勧めします。"
"secAlertSSL" = "セキュアな接続がありません。データ保護のためにTLS証明書をインストールしてください。"
"secAlertPanelPort" = "デフォルトのポートにはセキュリティリスクがあります。ランダムなポートまたは特定のポートを設定してください。"
"secAlertPanelURI" = "デフォルトのURIパスは安全ではありません。複雑なURIパスを設定してください。"
"secAlertSubURI" = "サブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。"
"secAlertSubJsonURI" = "JSONサブスクリプションのデフォルトURIパスは安全ではありません。複雑なURIパスを設定してください。"
"emptyDnsDesc" = "追加されたDNSサーバーはありません。"
"emptyFakeDnsDesc" = "追加されたFake DNSサーバーはありません。"
"emptyBalancersDesc" = "追加されたバランサーはありません。"
"emptyReverseDesc" = "追加されたリバースプロキシはありません。"
"somethingWentWrong" = "エラーが発生しました"
[subscription]
"title" = "サブスクリプション情報"
"subId" = "サブスクリプションID"
"status" = "ステータス"
"downloaded" = "ダウンロード"
"uploaded" = "アップロード"
"expiry" = "有効期限"
"totalQuota" = "合計クォータ"
"individualLinks" = "個別リンク"
"active" = "有効"
"inactive" = "無効"
"unlimited" = "無制限"
"noExpiry" = "期限なし"
[menu]
"theme" = "テーマ"
"dark" = "ダーク"
"ultraDark" = "ウルトラダーク"
"dashboard" = "ダッシュボード"
"inbounds" = "インバウンド一覧"
"settings" = "パネル設定"
"xray" = "Xray設定"
"logout" = "ログアウト"
"link" = "リンク管理"
[pages.login]
"hello" = "こんにちは"
"title" = "ようこそ"
"loginAgain" = "ログインセッションが切れました。再度ログインしてください。"
[pages.login.toasts]
"invalidFormData" = "データ形式エラー"
"emptyUsername" = "ユーザー名を入力してください"
"emptyPassword" = "パスワードを入力してください"
"wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。"
"successLogin" = "アカウントに正常にログインしました。"
"successRegister" = "登録が完了しました。ログインしてください。"
"userExists" = "ユーザー名は既に存在します"
"errorRegister" = "登録に失敗しました"
[pages.index]
"title" = "システムステータス"
"cpu" = "CPU"
"logicalProcessors" = "論理プロセッサ"
"frequency" = "周波数"
"swap" = "スワップ"
"storage" = "ストレージ"
"memory" = "RAM"
"threads" = "スレッド"
"xrayStatus" = "Xray"
"stopXray" = "停止"
"restartXray" = "再起動"
"xraySwitch" = "バージョン"
"xraySwitchClick" = "切り替えるバージョンを選択してください"
"xraySwitchClickDesk" = "慎重に選択してください。古いバージョンは現在の設定と互換性がない可能性があります。"
"xrayStatusUnknown" = "不明"
"xrayStatusRunning" = "実行中"
"xrayStatusStop" = "停止"
"xrayStatusError" = "エラー"
"xrayErrorPopoverTitle" = "Xrayの実行中にエラーが発生しました"
"operationHours" = "システム稼働時間"
"systemLoad" = "システム負荷"
"systemLoadDesc" = "過去1、5、15分間のシステム平均負荷"
"connectionCount" = "接続数"
"ipAddresses" = "IPアドレス"
"toggleIpVisibility" = "IPの表示を切り替える"
"overallSpeed" = "全体の速度"
"upload" = "アップロード"
"download" = "ダウンロード"
"totalData" = "総データ量"
"sent" = "送信"
"received" = "受信"
"documentation" = "ドキュメント"
"xraySwitchVersionDialog" = "Xrayのバージョンを本当に変更しますか"
"xraySwitchVersionDialogDesc" = "Xrayのバージョンが#version#に変更されます。"
"xraySwitchVersionPopover" = "Xrayの更新が成功しました"
"geofileUpdateDialog" = "ジオファイルを本当に更新しますか?"
"geofileUpdateDialogDesc" = "これにより#filename#ファイルが更新されます。"
"geofilesUpdateDialogDesc" = "これにより、すべてのファイルが更新されます。"
"geofilesUpdateAll" = "すべて更新"
"geofileUpdatePopover" = "ジオファイルの更新が成功しました"
"dontRefresh" = "インストール中、このページをリロードしないでください"
"logs" = "ログ"
"config" = "設定"
"backup" = "バックアップ"
"backupTitle" = "データベースのバックアップと復元"
"exportDatabase" = "バックアップ"
"exportDatabaseDesc" = "クリックして、現在のデータベースのバックアップを含む .db ファイルをデバイスにダウンロードします。"
"importDatabase" = "復元"
"importDatabaseDesc" = "クリックして、デバイスから .db ファイルを選択し、アップロードしてバックアップからデータベースを復元します。"
"importDatabaseSuccess" = "データベースのインポートに成功しました"
"importDatabaseError" = "データベースのインポート中にエラーが発生しました"
"readDatabaseError" = "データベースの読み取り中にエラーが発生しました"
"getDatabaseError" = "データベースの取得中にエラーが発生しました"
"getConfigError" = "設定ファイルの取得中にエラーが発生しました"
[pages.inbounds]
"allTimeTraffic" = "総トラフィック"
"allTimeTrafficUsage" = "これまでの総使用量"
"title" = "インバウンド一覧"
"totalDownUp" = "総アップロード / ダウンロード"
"totalUsage" = "総使用量"
"inboundCount" = "インバウンド数"
"operate" = "メニュー"
"enable" = "有効化"
"remark" = "備考"
"protocol" = "プロトコル"
"port" = "ポート"
"portMap" = "ポートマッピング"
"traffic" = "トラフィック"
"details" = "詳細情報"
"transportConfig" = "トランスポート設定"
"expireDate" = "有効期限"
"createdAt" = "作成"
"updatedAt" = "更新"
"resetTraffic" = "トラフィックリセット"
"addInbound" = "インバウンド追加"
"generalActions" = "一般操作"
"autoRefresh" = "自動更新"
"autoRefreshInterval" = "間隔"
"modifyInbound" = "インバウンド修正"
"deleteInbound" = "インバウンド削除"
"deleteInboundContent" = "インバウンドを削除してもよろしいですか?"
"deleteClient" = "クライアント削除"
"deleteClientContent" = "クライアントを削除してもよろしいですか?"
"resetTrafficContent" = "トラフィックをリセットしてもよろしいですか?"
"copyLink" = "リンクをコピー"
"address" = "アドレス"
"network" = "ネットワーク"
"destinationPort" = "宛先ポート"
"targetAddress" = "宛先アドレス"
"monitorDesc" = "空白にするとすべてのIPを監視"
"meansNoLimit" = "= 無制限単位GB"
"totalFlow" = "総トラフィック"
"leaveBlankToNeverExpire" = "空白にすると期限なし"
"noRecommendKeepDefault" = "デフォルト値を保持することをお勧めします"
"certificatePath" = "ファイルパス"
"certificateContent" = "ファイル内容"
"publicKey" = "公開鍵"
"privatekey" = "秘密鍵"
"clickOnQRcode" = "QRコードをクリックしてコピー"
"client" = "クライアント"
"export" = "リンクエクスポート"
"clone" = "複製"
"cloneInbound" = "複製"
"cloneInboundContent" = "このインバウンドルールは、ポートPort、リスニングIPListening IP、クライアントClientsを除くすべての設定がクローンされます"
"cloneInboundOk" = "クローン作成"
"resetAllTraffic" = "すべてのインバウンドトラフィックをリセット"
"resetAllTrafficTitle" = "すべてのインバウンドトラフィックをリセット"
"resetAllTrafficContent" = "すべてのインバウンドトラフィックをリセットしてもよろしいですか?"
"resetInboundClientTraffics" = "クライアントトラフィックをリセット"
"resetInboundClientTrafficTitle" = "すべてのクライアントトラフィックをリセット"
"resetInboundClientTrafficContent" = "このインバウンドクライアントのすべてのトラフィックをリセットしてもよろしいですか?"
"resetAllClientTraffics" = "すべてのクライアントトラフィックをリセット"
"resetAllClientTrafficTitle" = "すべてのクライアントトラフィックをリセット"
"resetAllClientTrafficContent" = "すべてのクライアントのトラフィックをリセットしてもよろしいですか?"
"delDepletedClients" = "トラフィックが尽きたクライアントを削除"
"delDepletedClientsTitle" = "トラフィックが尽きたクライアントを削除"
"delDepletedClientsContent" = "トラフィックが尽きたすべてのクライアントを削除してもよろしいですか?"
"email" = "メールアドレス"
"emailDesc" = "メールアドレスは一意でなければなりません"
"IPLimit" = "IP制限"
"IPLimitDesc" = "設定値を超えるとインバウンドトラフィックが無効になります。0 = 無効)"
"IPLimitlog" = "IPログ"
"IPLimitlogDesc" = "IP履歴ログ無効なインバウンドトラフィックを有効にするには、ログをクリアしてください"
"IPLimitlogclear" = "ログをクリア"
"setDefaultCert" = "パネル設定から証明書を設定"
"telegramDesc" = "TelegramチャットIDを提供してください。ボットで'/id'コマンドを使用)または(@userinfobot"
"subscriptionDesc" = "サブスクリプションURLを見つけるには、“詳細情報”に移動してください。また、複数のクライアントに同じ名前を使用することができます。"
"info" = "情報"
"same" = "同じ"
"inboundData" = "インバウンドデータ"
"exportInbound" = "インバウンドルールをエクスポート"
"import" = "インポート"
"importInbound" = "インバウンドルールをインポート"
"periodicTrafficResetTitle" = "トラフィックリセット"
"periodicTrafficResetDesc" = "指定された間隔でトラフィックカウンタを自動的にリセット"
"lastReset" = "最後のリセット"
[pages.client]
"add" = "クライアント追加"
"edit" = "クライアント編集"
"submitAdd" = "クライアント追加"
"submitEdit" = "変更を保存"
"clientCount" = "クライアント数"
"bulk" = "一括作成"
"method" = "方法"
"first" = "最初"
"last" = "最後"
"prefix" = "プレフィックス"
"postfix" = "サフィックス"
"delayedStart" = "初回使用後に開始"
"expireDays" = "期間"
"days" = "日"
"renew" = "自動更新"
"renewDesc" = "期限が切れた後に自動更新。0 = 無効)(単位:日)"
[pages.inbounds.periodicTrafficReset]
"never" = "なし"
"daily" = "毎日"
"weekly" = "毎週"
"monthly" = "毎月"
[pages.inbounds.toasts]
"obtain" = "取得"
"updateSuccess" = "更新が成功しました"
"logCleanSuccess" = "ログがクリアされました"
"inboundsUpdateSuccess" = "インバウンドが正常に更新されました"
"inboundUpdateSuccess" = "インバウンドが正常に更新されました"
"inboundCreateSuccess" = "インバウンドが正常に作成されました"
"inboundDeleteSuccess" = "インバウンドが正常に削除されました"
"inboundClientAddSuccess" = "インバウンドクライアントが追加されました"
"inboundClientDeleteSuccess" = "インバウンドクライアントが削除されました"
"inboundClientUpdateSuccess" = "インバウンドクライアントが更新されました"
"delDepletedClientsSuccess" = "すべての枯渇したクライアントが削除されました"
"resetAllClientTrafficSuccess" = "クライアントのすべてのトラフィックがリセットされました"
"resetAllTrafficSuccess" = "すべてのトラフィックがリセットされました"
"resetInboundClientTrafficSuccess" = "トラフィックがリセットされました"
"trafficGetError" = "トラフィックの取得中にエラーが発生しました"
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
"getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。"
[pages.inbounds.stream.general]
"request" = "リクエスト"
"response" = "レスポンス"
"name" = "名前"
"value" = "値"
[pages.inbounds.stream.tcp]
"version" = "バージョン"
"method" = "方法"
"path" = "パス"
"status" = "ステータス"
"statusDescription" = "ステータス説明"
"requestHeader" = "リクエストヘッダー"
"responseHeader" = "レスポンスヘッダー"
[pages.settings]
"title" = "パネル設定"
"save" = "保存"
"infoDesc" = "ここでのすべての変更は、保存してパネルを再起動する必要があります"
"restartPanel" = "パネル再起動"
"restartPanelDesc" = "パネルを再起動してもよろしいですか?再起動後にパネルにアクセスできない場合は、サーバーでパネルログを確認してください"
"restartPanelSuccess" = "パネルの再起動に成功しました"
"actions" = "操作"
"resetDefaultConfig" = "デフォルト設定にリセット"
"panelSettings" = "一般"
"securitySettings" = "セキュリティ設定"
"TGBotSettings" = "Telegramボット設定"
"panelListeningIP" = "パネル監視IP"
"panelListeningIPDesc" = "デフォルトではすべてのIPを監視する"
"panelListeningDomain" = "パネル監視ドメイン"
"panelListeningDomainDesc" = "デフォルトで空白の場合、すべてのドメインとIPアドレスを監視する"
"panelPort" = "パネル監視ポート"
"panelPortDesc" = "再起動で有効"
"publicKeyPath" = "パネル証明書公開鍵ファイルパス"
"publicKeyPathDesc" = "'/'で始まる絶対パスを入力"
"privateKeyPath" = "パネル証明書秘密鍵ファイルパス"
"privateKeyPathDesc" = "'/'で始まる絶対パスを入力"
"panelUrlPath" = "パネルURLルートパス"
"panelUrlPathDesc" = "'/'で始まり、'/'で終わる必要があります"
"pageSize" = "ページサイズ"
"pageSizeDesc" = "インバウンドテーブルのページサイズを定義します。0を設定すると無効化されます"
"remarkModel" = "備考モデルと区切り記号"
"datepicker" = "日付ピッカー"
"datepickerPlaceholder" = "日付を選択"
"datepickerDescription" = "日付選択カレンダーで有効期限を指定する"
"sampleRemark" = "備考の例"
"oldUsername" = "旧ユーザー名"
"currentPassword" = "旧パスワード"
"newUsername" = "新しいユーザー名"
"newPassword" = "新しいパスワード"
"telegramBotEnable" = "Telegramボットを有効にする"
"telegramBotEnableDesc" = "Telegramボット機能を有効にする"
"telegramToken" = "Telegramボットトークン"
"telegramTokenDesc" = "'@BotFather'から取得したTelegramボットトークン"
"telegramProxy" = "SOCKS5プロキシ"
"telegramProxyDesc" = "SOCKS5プロキシを有効にしてTelegramに接続するガイドに従って設定を調整"
"telegramAPIServer" = "Telegram APIサーバー"
"telegramAPIServerDesc" = "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する"
"telegramChatId" = "管理者チャットID"
"telegramChatIdDesc" = "Telegram管理者チャットID複数の場合はカンマで区切る@userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する"
"telegramNotifyTime" = "通知時間"
"telegramNotifyTimeDesc" = "定期的なTelegramボット通知時間を設定するcrontab時間形式を使用"
"tgNotifyBackup" = "データベースバックアップ"
"tgNotifyBackupDesc" = "レポート付きのデータベースバックアップファイルを送信"
"tgNotifyLogin" = "ログイン通知"
"tgNotifyLoginDesc" = "誰かがパネルにログインしようとしたときに、ユーザー名、IPアドレス、時間を表示する"
"sessionMaxAge" = "セッション期間"
"sessionMaxAgeDesc" = "ログイン状態を保持する期間(単位:分)"
"expireTimeDiff" = "有効期限通知のしきい値"
"expireTimeDiffDesc" = "このしきい値に達した場合、有効期限に関する通知を受け取る(単位:日)"
"trafficDiff" = "トラフィック消耗しきい値"
"trafficDiffDesc" = "このしきい値に達した場合、トラフィック消耗に関する通知を受け取る単位GB"
"tgNotifyCpu" = "CPU負荷通知しきい値"
"tgNotifyCpuDesc" = "CPU負荷がこのしきい値を超えた場合、通知を受け取る単位%"
"timeZone" = "タイムゾーン"
"timeZoneDesc" = "定時タスクはこのタイムゾーンの時間に従って実行される"
"subSettings" = "サブスクリプション設定"
"subEnable" = "サブスクリプションサービスを有効にする"
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
"subTitle" = "サブスクリプションタイトル"
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
"subSupportUrl" = "サポートURL"
"subSupportUrlDesc" = "VPNクライアントに表示されるテクニカルサポートへのリンク"
"subProfileUrl" = "プロフィールURL"
"subProfileUrlDesc" = "VPNクライアントに表示されるWebサイトへのリンク"
"subAnnounce" = "お知らせ"
"subAnnounceDesc" = "VPNクライアントに表示されるお知らせのテキスト"
"subEnableRouting" = "ルーティングを有効化"
"subEnableRoutingDesc" = "VPNクライアントでルーティングを有効にするためのグローバル設定。(Happのみ)"
"subRoutingRules" = "ルーティングルール"
"subRoutingRulesDesc" = "VPNクライアントのグローバルルーティングルール。(Happのみ)"
"subListen" = "監視IP"
"subListenDesc" = "サブスクリプションサービスが監視するIPアドレス空白にするとすべてのIPを監視"
"subPort" = "監視ポート"
"subPortDesc" = "サブスクリプションサービスが監視するポート番号(使用されていないポートである必要があります)"
"subCertPath" = "公開鍵パス"
"subCertPathDesc" = "サブスクリプションサービスで使用する公開鍵ファイルのパス('/'で始まる)"
"subKeyPath" = "秘密鍵パス"
"subKeyPathDesc" = "サブスクリプションサービスで使用する秘密鍵ファイルのパス('/'で始まる)"
"subPath" = "URIパス"
"subPathDesc" = "サブスクリプションサービスで使用するURIパス'/'で始まり、'/'で終わる)"
"subDomain" = "監視ドメイン"
"subDomainDesc" = "サブスクリプションサービスが監視するドメイン空白にするとすべてのドメインとIPを監視"
"subUpdates" = "更新間隔"
"subUpdatesDesc" = "クライアントアプリケーションでサブスクリプションURLの更新間隔単位時間"
"subEncrypt" = "エンコード"
"subEncryptDesc" = "サブスクリプションサービスが返す内容をBase64エンコードする"
"subShowInfo" = "利用情報を表示"
"subShowInfoDesc" = "クライアントアプリで残りのトラフィックと日付情報を表示する"
"subURI" = "リバースプロキシURI"
"subURIDesc" = "プロキシ後ろのサブスクリプションURLのURIパスに使用する"
"externalTrafficInformEnable" = "外部トラフィック情報"
"externalTrafficInformEnableDesc" = "トラフィックの更新ごとに外部 API に通知します。"
"externalTrafficInformURI" = "外部トラフィック通知 URI"
"externalTrafficInformURIDesc" = "トラフィックの更新ごとに外部 API に通知します。"
"fragment" = "フラグメント"
"fragmentDesc" = "TLS helloパケットのフラグメントを有効にする"
"fragmentSett" = "設定"
"noisesDesc" = "Noisesを有効にする"
"noisesSett" = "Noises設定"
"mux" = "マルチプレクサ"
"muxDesc" = "確立されたストリーム内で複数の独立したストリームを伝送する"
"muxSett" = "マルチプレクサ設定"
"direct" = "直接接続"
"directDesc" = "特定の国のドメインまたはIP範囲に直接接続する"
"notifications" = "通知"
"certs" = "証明書"
"externalTraffic" = "外部トラフィック"
"dateAndTime" = "日付と時刻"
"proxyAndServer" = "プロキシとサーバー"
"intervals" = "間隔"
"information" = "情報"
"language" = "言語"
"telegramBotLanguage" = "Telegram Botの言語"
[pages.xray]
"title" = "Xray 設定"
"save" = "保存"
"restart" = "Xray 再起動"
"restartSuccess" = "Xrayの再起動に成功しました"
"stopSuccess" = "Xrayが正常に停止しました"
"restartError" = "Xrayの再起動中にエラーが発生しました。"
"stopError" = "Xrayの停止中にエラーが発生しました。"
"basicTemplate" = "基本設定"
"advancedTemplate" = "高度な設定"
"generalConfigs" = "一般設定"
"generalConfigsDesc" = "これらのオプションは一般設定を決定します"
"logConfigs" = "ログ"
"logConfigsDesc" = "ログはサーバーのパフォーマンスに影響を与える可能性があるため、必要な場合にのみ有効にすることをお勧めします"
"blockConfigsDesc" = "これらのオプションは、特定のプロトコルやウェブサイトへのユーザー接続をブロックします"
"basicRouting" = "基本ルーティング"
"blockConnectionsConfigsDesc" = "これらのオプションにより、特定のリクエスト元の国に基づいてトラフィックをブロックします。"
"directConnectionsConfigsDesc" = "直接接続により、特定のトラフィックが他のサーバーを経由しないようにします。"
"blockips" = "IPをブロック"
"blockdomains" = "ドメインをブロック"
"directips" = "直接IP"
"directdomains" = "直接ドメイン"
"ipv4Routing" = "IPv4 ルーティング"
"ipv4RoutingDesc" = "このオプションはIPv4のみを介してターゲットドメインへルーティングします"
"warpRouting" = "WARP ルーティング"
"warpRoutingDesc" = "注意これらのオプションを使用する前に、パネルのGitHubの手順に従って、サーバーにsocks5プロキシモードでWARPをインストールしてください。WARPはCloudflareサーバー経由でトラフィックをウェブサイトにルーティングします。"
"Template" = "高度なXray設定テンプレート"
"TemplateDesc" = "最終的なXray設定ファイルはこのテンプレートに基づいて生成されます"
"FreedomStrategy" = "Freedom プロトコル戦略"
"FreedomStrategyDesc" = "Freedomプロトコル内のネットワークの出力戦略を設定する"
"RoutingStrategy" = "ルーティングドメイン戦略設定"
"RoutingStrategyDesc" = "DNS解決の全体的なルーティング戦略を設定する"
"outboundTestUrl" = "アウトバウンドテスト URL"
"outboundTestUrlDesc" = "アウトバウンド接続テストに使用する URL。既定値"
"Torrent" = "BitTorrent プロトコルをブロック"
"Inbounds" = "インバウンドルール"
"InboundsDesc" = "特定のクライアントからのトラフィックを受け入れる"
"Outbounds" = "アウトバウンドルール"
"Balancers" = "負荷分散"
"OutboundsDesc" = "アウトバウンドトラフィックの送信方法を設定する"
"Routings" = "ルーティングルール"
"RoutingsDesc" = "各ルールの優先順位が重要です"
"completeTemplate" = "すべて"
"logLevel" = "ログレベル"
"logLevelDesc" = "エラーログのレベルを指定し、記録する情報を示します"
"accessLog" = "アクセスログ"
"accessLogDesc" = "アクセスログのファイルパス。特殊値 'none' はアクセスログを無効にします"
"errorLog" = "エラーログ"
"errorLogDesc" = "エラーログのファイルパス。特殊値 'none' はエラーログを無効にします"
"dnsLog" = "DNS ログ"
"dnsLogDesc" = "DNSクエリのログを有効にするかどうか"
"maskAddress" = "アドレスをマスク"
"maskAddressDesc" = "IPアドレスをマスクし、有効にするとログに表示されるIPアドレスを自動的に置き換えます"
"statistics" = "統計"
"statsInboundUplink" = "インバウンドアップロード統計"
"statsInboundUplinkDesc" = "すべてのインバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。"
"statsInboundDownlink" = "インバウンドダウンロード統計"
"statsInboundDownlinkDesc" = "すべてのインバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。"
"statsOutboundUplink" = "アウトバウンドアップロード統計"
"statsOutboundUplinkDesc" = "すべてのアウトバウンドプロキシのアップストリームトラフィックの統計収集を有効にします。"
"statsOutboundDownlink" = "アウトバウンドダウンロード統計"
"statsOutboundDownlinkDesc" = "すべてのアウトバウンドプロキシのダウンストリームトラフィックの統計収集を有効にします。"
[pages.xray.rules]
"first" = "最初"
"last" = "最後"
"up" = "上へ"
"down" = "下へ"
"source" = "ソース"
"dest" = "宛先アドレス"
"inbound" = "インバウンド"
"outbound" = "アウトバウンド"
"balancer" = "負荷分散"
"info" = "情報"
"add" = "ルール追加"
"edit" = "ルール編集"
"useComma" = "カンマ区切りの項目"
[pages.xray.outbound]
"addOutbound" = "アウトバウンド追加"
"addReverse" = "リバース追加"
"editOutbound" = "アウトバウンド編集"
"editReverse" = "リバース編集"
"tag" = "タグ"
"tagDesc" = "一意のタグ"
"address" = "アドレス"
"reverse" = "リバース"
"domain" = "ドメイン"
"type" = "タイプ"
"bridge" = "ブリッジ"
"portal" = "ポータル"
"link" = "リンク"
"intercon" = "インターコネクション"
"settings" = "設定"
"accountInfo" = "アカウント情報"
"outboundStatus" = "アウトバウンドステータス"
"sendThrough" = "送信経路"
"test" = "テスト"
"testResult" = "テスト結果"
"testing" = "接続をテスト中..."
"testSuccess" = "テスト成功"
"testFailed" = "テスト失敗"
"testError" = "アウトバウンドのテストに失敗しました"
[pages.xray.balancer]
"addBalancer" = "負荷分散追加"
"editBalancer" = "負荷分散編集"
"balancerStrategy" = "戦略"
"balancerSelectors" = "セレクター"
"tag" = "タグ"
"tagDesc" = "一意のタグ"
"balancerDesc" = "balancerTagとoutboundTagは同時に使用できません。同時に使用された場合、outboundTagのみが有効になります。"
[pages.xray.wireguard]
"secretKey" = "シークレットキー"
"publicKey" = "公開鍵"
"allowedIPs" = "許可されたIP"
"endpoint" = "エンドポイント"
"psk" = "共有キー"
"domainStrategy" = "ドメイン戦略"
[pages.xray.tun]
"nameDesc" = "TUN インターフェースの名前。デフォルトは 'xray0' です"
"mtuDesc" = "最大伝送単位。データパケットの最大サイズ。デフォルトは 1500 です"
"userLevel" = "ユーザーレベル"
"userLevelDesc" = "このインバウンドを通じて確立されたすべての接続は、このユーザーレベルを使用します。デフォルトは 0 です"
[pages.xray.dns]
"enable" = "DNSを有効にする"
"enableDesc" = "組み込みDNSサーバーを有効にする"
"tag" = "DNSインバウンドタグ"
"tagDesc" = "このタグはルーティングルールでインバウンドタグとして使用できます"
"clientIp" = "クライアントIP"
"clientIpDesc" = "DNSクエリ中に指定されたIPの位置をサーバーに通知するために使用されます"
"disableCache" = "キャッシュを無効にする"
"disableCacheDesc" = "DNSキャッシュを無効にします"
"disableFallback" = "フォールバックを無効にする"
"disableFallbackDesc" = "フォールバックDNSクエリを無効にします"
"disableFallbackIfMatch" = "一致した場合にフォールバックを無効にする"
"disableFallbackIfMatchDesc" = "DNSサーバーの一致するドメインリストにヒットした場合、フォールバックDNSクエリを無効にします"
"enableParallelQuery" = "並列クエリを有効にする"
"enableParallelQueryDesc" = "複数のサーバーへの並列DNSクエリを有効にして、より高速な解決を実現"
"strategy" = "クエリ戦略"
"strategyDesc" = "ドメイン名解決の全体的な戦略"
"add" = "サーバー追加"
"edit" = "サーバー編集"
"domains" = "ドメイン"
"expectIPs" = "期待されるIP"
"unexpectIPs" = "予期しないIP"
"useSystemHosts" = "システムのHostsを使用"
"useSystemHostsDesc" = "インストール済みシステムのhostsファイルを使用する"
"usePreset" = "テンプレートを使用"
"dnsPresetTitle" = "DNSテンプレート"
"dnsPresetFamily" = "ファミリー"
[pages.xray.fakedns]
"add" = "フェイクDNS追加"
"edit" = "フェイクDNS編集"
"ipPool" = "IPプールサブネット"
"poolSize" = "プールサイズ"
[pages.settings.security]
"admin" = "管理者の資格情報"
"twoFactor" = "二段階認証"
"twoFactorEnable" = "2FAを有効化"
"twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。"
"twoFactorModalSetTitle" = "二段階認証を有効にする"
"twoFactorModalDeleteTitle" = "二段階認証を無効にする"
"twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:"
"twoFactorModalFirstStep" = "1. 認証アプリでこのQRコードをスキャンするか、QRコード近くのトークンをコピーしてアプリに貼り付けます"
"twoFactorModalSecondStep" = "2. アプリからコードを入力してください"
"twoFactorModalRemoveStep" = "二段階認証を削除するには、アプリからコードを入力してください。"
"twoFactorModalChangeCredentialsTitle" = "認証情報の変更"
"twoFactorModalChangeCredentialsStep" = "管理者の認証情報を変更するには、アプリケーションからコードを入力してください。"
"twoFactorModalSetSuccess" = "二要素認証が正常に設定されました"
"twoFactorModalDeleteSuccess" = "二要素認証が正常に削除されました"
"twoFactorModalError" = "コードが間違っています"
[pages.settings.toasts]
"modifySettings" = "パラメーターが変更されました。"
"getSettings" = "パラメーターの取得中にエラーが発生しました"
"modifyUserError" = "管理者認証情報の変更中にエラーが発生しました。"
"modifyUser" = "管理者の認証情報を正常に変更しました。"
"originalUserPassIncorrect" = "旧ユーザー名または旧パスワードが間違っています"
"userPassMustBeNotEmpty" = "新しいユーザー名と新しいパスワードは空にできません"
"getOutboundTrafficError" = "送信トラフィックの取得エラー"
"resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
[tgbot]
"keyboardClosed" = "❌ キーボードを閉じました!"
"noResult" = "❗ 結果がありません!"
"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!"
"wentWrong" = "❌ 何かがうまくいかなかった!"
"noIpRecord" = "❗ IPレコードがありません"
"noInbounds" = "❗ インバウンドが見つかりません!"
"unlimited" = "♾ 無制限(リセット)"
"add" = "追加"
"month" = "月"
"months" = "ヶ月"
"day" = "日"
"days" = "日間"
"hours" = "時間"
"minutes" = "分"
"unknown" = "不明"
"inbounds" = "インバウンド"
"clients" = "クライアント"
"offline" = "🔴 オフライン"
"online" = "🟢 オンライン"
[tgbot.commands]
"unknown" = "❗ 不明なコマンド"
"pleaseChoose" = "👇 選択してください:\r\n"
"help" = "🤖 このボットをご利用いただきありがとうございます!サーバーから特定のデータを提供し、必要な変更を行うことができます。\r\n\r\n"
"start" = "👋 こんにちは、<i>{{ .Firstname }}</i>。\r\n"
"welcome" = "🤖 <b>{{ .Hostname }}</b> 管理ボットへようこそ。\r\n"
"status" = "✅ ボットは正常に動作しています!"
"usage" = "❗ 検索するテキストを入力してください!"
"getID" = "🆔 あなたのIDは<code>{{ .ID }}</code>"
"helpAdminCommands" = "Xray Coreを再起動するには\r\n<code>/restart</code>\r\n\r\nクライアントの電子メールを検索するには\r\n<code>/usage [電子メール]</code>\r\n\r\nインバウンドクライアントの統計情報を含むを検索するには\r\n<code>/inbound [備考]</code>\r\n\r\nTelegramチャットID\r\n<code>/id</code>"
"helpClientCommands" = "統計情報を検索するには、次のコマンドを使用してください:\r\n<code>/usage [電子メール]</code>\r\n\r\nTelegramチャットID\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ 操作成功!"
"restartFailed" = "❗ 操作エラー。\r\n\r\n<code>エラー: {{ .Error }}</code>"
"xrayNotRunning" = "❗ Xray Core は動作していません。"
"startDesc" = "メインメニューを表示"
"helpDesc" = "ボットのヘルプ"
"statusDesc" = "ボットの状態を確認"
"idDesc" = "Telegram IDを表示"
[tgbot.messages]
"cpuThreshold" = "🔴 CPU使用率は{{ .Percent }}%、しきい値{{ .Threshold }}%を超えました"
"selectUserFailed" = "❌ ユーザーの選択に失敗しました!"
"userSaved" = "✅ Telegramユーザーが保存されました。"
"loginSuccess" = "✅ パネルに正常にログインしました。\r\n"
"loginFailed" = "❗️ パネルのログインに失敗しました。\r\n"
"2faFailed" = "2FAエラー"
"report" = "🕰 定期報告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日時:{{ .DateTime }}\r\n"
"hostname" = "💻 ホスト名:{{ .Hostname }}\r\n"
"version" = "🚀 X-UI バージョン:{{ .Version }}\r\n"
"xrayVersion" = "📡 Xray バージョン: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6{{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4{{ .IPv4 }}\r\n"
"ip" = "🌐 IP{{ .IP }}\r\n"
"ips" = "🔢 IPアドレス\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ サーバー稼働時間:{{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 サーバー負荷:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 サーバーメモリ:{{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP接続数{{ .Count }}\r\n"
"udpCount" = "🔸 UDP接続数{{ .Count }}\r\n"
"traffic" = "🚦 トラフィック:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Xrayステータス{{ .State }}\r\n"
"username" = "👤 ユーザー名:{{ .Username }}\r\n"
"password" = "👤 パスワード: {{ .Password }}\r\n"
"time" = "⏰ 時間:{{ .Time }}\r\n"
"inbound" = "📍 インバウンド:{{ .Remark }}\r\n"
"port" = "🔌 ポート:{{ .Port }}\r\n"
"expire" = "📅 有効期限:{{ .Time }}\r\n"
"expireIn" = "📅 残り時間:{{ .Time }}\r\n"
"active" = "💡 有効:{{ .Enable }}\r\n"
"enabled" = "🚨 有効化済み:{{ .Enable }}\r\n"
"online" = "🌐 接続ステータス:{{ .Status }}\r\n"
"lastOnline" = "🔙 最終オンライン: {{ .Time }}\r\n"
"email" = "📧 メール:{{ .Email }}\r\n"
"upload" = "🔼 アップロード↑:{{ .Upload }}\r\n"
"download" = "🔽 ダウンロード↓:{{ .Download }}\r\n"
"total" = "📊 合計:{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Telegramユーザー{{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 消耗済みの {{ .Type }}\r\n"
"exhaustedCount" = "🚨 消耗済みの {{ .Type }} 数量:\r\n"
"onlinesCount" = "🌐 オンラインクライアント:{{ .Count }}\r\n"
"disabled" = "🛑 無効化:{{ .Disabled }}\r\n"
"depleteSoon" = "🔜 間もなく消耗:{{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 バックアップ時間:{{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 更新時間:{{ .Time }}\r\n\r\n"
"yes" = "✅ はい"
"no" = "❌ いいえ"
"received_id" = "🔑📥 IDが更新されました。"
"received_password" = "🔑📥 パスワードが更新されました。"
"received_email" = "📧📥 メールが更新されました。"
"received_comment" = "💬📥 コメントが更新されました。"
"id_prompt" = "🔑 デフォルトID: {{ .ClientId }}\n\nIDを入力してください。"
"pass_prompt" = "🔑 デフォルトパスワード: {{ .ClientPassword }}\n\nパスワードを入力してください。"
"email_prompt" = "📧 デフォルトメール: {{ .ClientEmail }}\n\nメールを入力してください。"
"comment_prompt" = "💬 デフォルトコメント: {{ .ClientComment }}\n\nコメントを入力してください。"
"inbound_client_data_id" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます"
"inbound_client_data_pass" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 パスワード: {{ .ClientPass }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます"
"cancel" = "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄"
"error_add_client" = "⚠️ エラー:\n\n {{ .error }}"
"using_default_value" = "わかりました、デフォルト値を使用します。 😊"
"incorrect_input" = "入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫"
"AreYouSure" = "本当にいいですか?🤔"
"SuccessResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功"
"FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 エラー: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 すべてのクライアントのトラフィックリセットが完了しました。"
[tgbot.buttons]
"closeKeyboard" = "❌ キーボードを閉じる"
"cancel" = "❌ キャンセル"
"cancelReset" = "❌ リセットをキャンセル"
"cancelIpLimit" = "❌ IP制限をキャンセル"
"confirmResetTraffic" = "✅ トラフィックをリセットしますか?"
"confirmClearIps" = "✅ IPをクリアしますか"
"confirmRemoveTGUser" = "✅ Telegramユーザーを削除しますか"
"confirmToggle" = "✅ ユーザーを有効/無効にしますか?"
"dbBackup" = "データベースバックアップを取得"
"serverUsage" = "サーバーの使用状況"
"getInbounds" = "インバウンド情報を取得"
"depleteSoon" = "間もなく消耗"
"clientUsage" = "使用状況を取得"
"onlines" = "オンラインクライアント"
"commands" = "コマンド"
"refresh" = "🔄 更新"
"clearIPs" = "❌ IPをクリア"
"removeTGUser" = "❌ Telegramユーザーを削除"
"selectTGUser" = "👤 Telegramユーザーを選択"
"selectOneTGUser" = "👤 1人のTelegramユーザーを選択"
"resetTraffic" = "📈 トラフィックをリセット"
"resetExpire" = "📅 有効期限を変更"
"ipLog" = "🔢 IPログ"
"ipLimit" = "🔢 IP制限"
"setTGUser" = "👤 Telegramユーザーを設定"
"toggle" = "🔘 有効/無効"
"custom" = "🔢 カスタム"
"confirmNumber" = "✅ 確認: {{ .Num }}"
"confirmNumberAdd" = "✅ 追加を確認:{{ .Num }}"
"limitTraffic" = "🚧 トラフィック制限"
"getBanLogs" = "禁止ログ"
"allClients" = "すべてのクライアント"
"addClient" = "クライアントを追加"
"submitDisable" = "無効として送信 ☑️"
"submitEnable" = "有効として送信 ✅"
"use_default" = "🏷️ デフォルトを使用"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 パスワード"
"change_email" = "⚙️📧 メールアドレス"
"change_comment" = "⚙️💬 コメント"
"ResetAllTraffics" = "すべてのトラフィックをリセット"
"SortedTrafficUsageReport" = "ソートされたトラフィック使用レポート"
[tgbot.answers]
"successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作エラー。"
"getInboundsFailed" = "❌ インバウンド情報の取得に失敗しました。"
"getClientsFailed" = "❌ クライアントの取得に失敗しました。"
"canceled" = "❌ {{ .Email }}:操作がキャンセルされました。"
"clientRefreshSuccess" = "✅ {{ .Email }}:クライアントが正常に更新されました。"
"IpRefreshSuccess" = "✅ {{ .Email }}IPが正常に更新されました。"
"TGIdRefreshSuccess" = "✅ {{ .Email }}クライアントのTelegramユーザーが正常に更新されました。"
"resetTrafficSuccess" = "✅ {{ .Email }}:トラフィックが正常にリセットされました。"
"setTrafficLimitSuccess" = "✅ {{ .Email }}:トラフィック制限が正常に保存されました。"
"expireResetSuccess" = "✅ {{ .Email }}:有効期限の日数が正常にリセットされました。"
"resetIpSuccess" = "✅ {{ .Email }}IP制限数が正常に保存されました{{ .Count }}。"
"clearIpSuccess" = "✅ {{ .Email }}IPが正常にクリアされました。"
"getIpLog" = "✅ {{ .Email }}IPログの取得。"
"getUserInfo" = "✅ {{ .Email }}Telegramユーザー情報の取得。"
"removedTGUserSuccess" = "✅ {{ .Email }}Telegramユーザーが正常に削除されました。"
"enableSuccess" = "✅ {{ .Email }}:正常に有効化されました。"
"disableSuccess" = "✅ {{ .Email }}:正常に無効化されました。"
"askToAddUserId" = "設定が見つかりませんでした!\r\n管理者に問い合わせて、設定にTelegramユーザーのChatIDを使用してください。\r\n\r\nあなたのユーザーChatID<code>{{ .TgUserID }}</code>"
"chooseClient" = "インバウンド {{ .Inbound }} のクライアントを選択"
"chooseInbound" = "インバウンドを選択"

View file

@ -1,795 +0,0 @@
"username" = "Nome de Usuário"
"password" = "Senha"
"login" = "Entrar"
"confirm" = "Confirmar"
"cancel" = "Cancelar"
"close" = "Fechar"
"create" = "Criar"
"update" = "Atualizar"
"copy" = "Copiar"
"copied" = "Copiado"
"download" = "Baixar"
"remark" = "Observação"
"enable" = "Ativado"
"protocol" = "Protocolo"
"search" = "Pesquisar"
"filter" = "Filtrar"
"loading" = "Carregando..."
"second" = "Segundo"
"minute" = "Minuto"
"hour" = "Hora"
"day" = "Dia"
"check" = "Verificar"
"indefinite" = "Indeterminado"
"unlimited" = "Ilimitado"
"none" = "Nada"
"qrCode" = "Código QR"
"info" = "Mais Informações"
"edit" = "Editar"
"delete" = "Excluir"
"reset" = "Redefinir"
"noData" = "Sem dados."
"copySuccess" = "Copiado com Sucesso"
"sure" = "Certo"
"encryption" = "Criptografia"
"useIPv4ForHost" = "Usar IPv4 para o host"
"transmission" = "Transmissão"
"host" = "Servidor"
"path" = "Caminho"
"camouflage" = "Ofuscação"
"status" = "Status"
"enabled" = "Ativado"
"disabled" = "Desativado"
"depleted" = "Encerrado"
"depletingSoon" = "Esgotando"
"offline" = "Offline"
"online" = "Online"
"domainName" = "Nome de Domínio"
"monitor" = "IP de Escuta"
"certificate" = "Certificado Digital"
"fail" = "Falhou"
"comment" = "Comentário"
"success" = "Com Sucesso"
"lastOnline" = "Última vez online"
"getVersion" = "Obter Versão"
"install" = "Instalar"
"clients" = "Clientes"
"usage" = "Uso"
"twoFactorCode" = "Código"
"remained" = "Restante"
"security" = "Segurança"
"secAlertTitle" = "Alerta de Segurança"
"secAlertSsl" = "Esta conexão não é segura. Evite inserir informações confidenciais até que o TLS seja ativado para proteção de dados."
"secAlertConf" = "Algumas configurações estão vulneráveis a ataques. Recomenda-se reforçar os protocolos de segurança para evitar possíveis violações."
"secAlertSSL" = "O painel não possui uma conexão segura. Instale o certificado TLS para proteção de dados."
"secAlertPanelPort" = "A porta padrão do painel é vulnerável. Configure uma porta aleatória ou específica."
"secAlertPanelURI" = "O caminho URI padrão do painel não é seguro. Configure um caminho URI complexo."
"secAlertSubURI" = "O caminho URI padrão de inscrição não é seguro. Configure um caminho URI complexo."
"secAlertSubJsonURI" = "O caminho URI JSON de inscrição padrão não é seguro. Configure um caminho URI complexo."
"emptyDnsDesc" = "Nenhum servidor DNS adicionado."
"emptyFakeDnsDesc" = "Nenhum servidor Fake DNS adicionado."
"emptyBalancersDesc" = "Nenhum balanceador adicionado."
"emptyReverseDesc" = "Nenhum proxy reverso adicionado."
"somethingWentWrong" = "Algo deu errado"
[subscription]
"title" = "Informações da assinatura"
"subId" = "ID da assinatura"
"status" = "Status"
"downloaded" = "Baixado"
"uploaded" = "Enviado"
"expiry" = "Validade"
"totalQuota" = "Cota total"
"individualLinks" = "Links individuais"
"active" = "Ativo"
"inactive" = "Inativo"
"unlimited" = "Ilimitado"
"noExpiry" = "Sem validade"
[menu]
"theme" = "Tema"
"dark" = "Escuro"
"ultraDark" = "Ultra Escuro"
"dashboard" = "Visão Geral"
"inbounds" = "Inbounds"
"settings" = "Panel Settings"
"xray" = "Xray Configs"
"logout" = "Sair"
"link" = "Gerenciar"
[pages.login]
"hello" = "Olá"
"title" = "Bem-vindo"
"loginAgain" = "Sua sessão expirou, faça login novamente"
[pages.login.toasts]
"invalidFormData" = "O formato dos dados de entrada é inválido."
"emptyUsername" = "Nome de usuário é obrigatório"
"emptyPassword" = "Senha é obrigatória"
"wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido."
"successLogin" = "Você entrou na sua conta com sucesso."
"successRegister" = "Registro bem-sucedido, por favor faça login."
"userExists" = "Nome de usuário já existe"
"errorRegister" = "Falha no registro"
[pages.index]
"title" = "Visão Geral"
"cpu" = "CPU"
"logicalProcessors" = "Processadores lógicos"
"frequency" = "Frequência"
"swap" = "Swap"
"storage" = "Armazenamento"
"memory" = "RAM"
"threads" = "Threads"
"xrayStatus" = "Xray"
"stopXray" = "Parar"
"restartXray" = "Reiniciar"
"xraySwitch" = "Versão"
"xraySwitchClick" = "Escolha a versão para a qual deseja alternar."
"xraySwitchClickDesk" = "Escolha com cuidado, pois versões mais antigas podem não ser compatíveis com as configurações atuais."
"xrayStatusUnknown" = "Desconhecido"
"xrayStatusRunning" = "Em execução"
"xrayStatusStop" = "Parado"
"xrayStatusError" = "Erro"
"xrayErrorPopoverTitle" = "Ocorreu um erro ao executar o Xray"
"operationHours" = "Tempo de Atividade"
"systemLoad" = "Carga do Sistema"
"systemLoadDesc" = "Média de carga do sistema nos últimos 1, 5 e 15 minutos"
"connectionCount" = "Estatísticas de Conexão"
"ipAddresses" = "Endereços IP"
"toggleIpVisibility" = "Alternar visibilidade do IP"
"overallSpeed" = "Velocidade geral"
"upload" = "Upload"
"download" = "Download"
"totalData" = "Dados totais"
"sent" = "Enviado"
"received" = "Recebido"
"documentation" = "Documentação"
"xraySwitchVersionDialog" = "Você realmente deseja alterar a versão do Xray?"
"xraySwitchVersionDialogDesc" = "Isso mudará a versão do Xray para #version#."
"xraySwitchVersionPopover" = "Xray atualizado com sucesso"
"geofileUpdateDialog" = "Você realmente deseja atualizar o geofile?"
"geofileUpdateDialogDesc" = "Isso atualizará o arquivo #filename#."
"geofilesUpdateDialogDesc" = "Isso atualizará todos os arquivos."
"geofilesUpdateAll" = "Atualizar tudo"
"geofileUpdatePopover" = "Geofile atualizado com sucesso"
"dontRefresh" = "Instalação em andamento, por favor não atualize a página"
"logs" = "Logs"
"config" = "Configuração"
"backup" = "Backup"
"backupTitle" = "Backup e Restauração do Banco de Dados"
"exportDatabase" = "Backup"
"exportDatabaseDesc" = "Clique para baixar um arquivo .db contendo um backup do seu banco de dados atual para o seu dispositivo."
"importDatabase" = "Restaurar"
"importDatabaseDesc" = "Clique para selecionar e enviar um arquivo .db do seu dispositivo para restaurar seu banco de dados a partir de um backup."
"importDatabaseSuccess" = "O banco de dados foi importado com sucesso"
"importDatabaseError" = "Ocorreu um erro ao importar o banco de dados"
"readDatabaseError" = "Ocorreu um erro ao ler o banco de dados"
"getDatabaseError" = "Ocorreu um erro ao recuperar o banco de dados"
"getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração"
[pages.inbounds]
"allTimeTraffic" = "Tráfego Total"
"allTimeTrafficUsage" = "Uso total de todos os tempos"
"title" = "Inbounds"
"totalDownUp" = "Total Enviado/Recebido"
"totalUsage" = "Uso Total"
"inboundCount" = "Total de Inbounds"
"operate" = "Menu"
"enable" = "Ativado"
"remark" = "Observação"
"protocol" = "Protocolo"
"port" = "Porta"
"portMap" = "Porta Mapeada"
"traffic" = "Tráfego"
"details" = "Detalhes"
"transportConfig" = "Transporte"
"expireDate" = "Duração"
"createdAt" = "Criado"
"updatedAt" = "Atualizado"
"resetTraffic" = "Redefinir Tráfego"
"addInbound" = "Adicionar Inbound"
"generalActions" = "Ações Gerais"
"autoRefresh" = "Atualização automática"
"autoRefreshInterval" = "Intervalo"
"modifyInbound" = "Modificar Inbound"
"deleteInbound" = "Excluir Inbound"
"deleteInboundContent" = "Tem certeza de que deseja excluir o inbound?"
"deleteClient" = "Excluir Cliente"
"deleteClientContent" = "Tem certeza de que deseja excluir o cliente?"
"resetTrafficContent" = "Tem certeza de que deseja redefinir o tráfego?"
"copyLink" = "Copiar URL"
"address" = "Endereço"
"network" = "Rede"
"destinationPort" = "Porta de Destino"
"targetAddress" = "Endereço de Destino"
"monitorDesc" = "Deixe em branco para ouvir todos os IPs"
"meansNoLimit" = "= Ilimitado. (unidade: GB)"
"totalFlow" = "Fluxo Total"
"leaveBlankToNeverExpire" = "Deixe em branco para nunca expirar"
"noRecommendKeepDefault" = "Recomenda-se manter o padrão"
"certificatePath" = "Caminho"
"certificateContent" = "Conteúdo"
"publicKey" = "Chave Pública"
"privatekey" = "Chave Privada"
"clickOnQRcode" = "Clique no Código QR para Copiar"
"client" = "Cliente"
"export" = "Exportar Todos os URLs"
"clone" = "Clonar"
"cloneInbound" = "Clonar"
"cloneInboundContent" = "Todas as configurações deste inbound, exceto Porta, IP de Escuta e Clientes, serão aplicadas ao clone."
"cloneInboundOk" = "Clonar"
"resetAllTraffic" = "Redefinir Tráfego de Todos os Inbounds"
"resetAllTrafficTitle" = "Redefinir Tráfego de Todos os Inbounds"
"resetAllTrafficContent" = "Tem certeza de que deseja redefinir o tráfego de todos os inbounds?"
"resetInboundClientTraffics" = "Redefinir Tráfego dos Clientes"
"resetInboundClientTrafficTitle" = "Redefinir Tráfego dos Clientes"
"resetInboundClientTrafficContent" = "Tem certeza de que deseja redefinir o tráfego dos clientes deste inbound?"
"resetAllClientTraffics" = "Redefinir Tráfego de Todos os Clientes"
"resetAllClientTrafficTitle" = "Redefinir Tráfego de Todos os Clientes"
"resetAllClientTrafficContent" = "Tem certeza de que deseja redefinir o tráfego de todos os clientes?"
"delDepletedClients" = "Excluir Clientes Esgotados"
"delDepletedClientsTitle" = "Excluir Clientes Esgotados"
"delDepletedClientsContent" = "Tem certeza de que deseja excluir todos os clientes esgotados?"
"email" = "Email"
"emailDesc" = "Por favor, forneça um endereço de e-mail único."
"IPLimit" = "Limite de IP"
"IPLimitDesc" = "Desativa o inbound se o número ultrapassar o valor definido. (0 = desativar)"
"IPLimitlog" = "Log de IP"
"IPLimitlogDesc" = "O histórico de IPs. (para ativar o inbound após a desativação, limpe o log)"
"IPLimitlogclear" = "Limpar o Log"
"setDefaultCert" = "Definir Certificado pelo Painel"
"telegramDesc" = "Por favor, forneça o ID do Chat do Telegram. (use o comando '/id' no bot) ou (@userinfobot)"
"subscriptionDesc" = "Para encontrar seu URL de assinatura, navegue até 'Detalhes'. Além disso, você pode usar o mesmo nome para vários clientes."
"info" = "Informações"
"same" = "Igual"
"inboundData" = "Dados do Inbound"
"exportInbound" = "Exportar Inbound"
"import" = "Importar"
"importInbound" = "Importar um Inbound"
"periodicTrafficResetTitle" = "Reset de Tráfego"
"periodicTrafficResetDesc" = "Reinicia automaticamente o contador de tráfego em intervalos especificados"
"lastReset" = "Último Reset"
[pages.client]
"add" = "Adicionar Cliente"
"edit" = "Editar Cliente"
"submitAdd" = "Adicionar Cliente"
"submitEdit" = "Salvar Alterações"
"clientCount" = "Número de Clientes"
"bulk" = "Adicionar Vários"
"method" = "Método"
"first" = "Primeiro"
"last" = "Último"
"prefix" = "Prefixo"
"postfix" = "Sufixo"
"delayedStart" = "Iniciar Após Primeiro Uso"
"expireDays" = "Duração"
"days" = "Dia(s)"
"renew" = "Renovação Automática"
"renewDesc" = "Renovação automática após expiração. (0 = desativado)(unidade: dia)"
[pages.inbounds.periodicTrafficReset]
"never" = "Nunca"
"daily" = "Diariamente"
"weekly" = "Semanalmente"
"monthly" = "Mensalmente"
[pages.inbounds.toasts]
"obtain" = "Obter"
"updateSuccess" = "A atualização foi bem-sucedida"
"logCleanSuccess" = "O log foi limpo"
"inboundsUpdateSuccess" = "Entradas atualizadas com sucesso"
"inboundUpdateSuccess" = "Entrada atualizada com sucesso"
"inboundCreateSuccess" = "Entrada criada com sucesso"
"inboundDeleteSuccess" = "Entrada excluída com sucesso"
"inboundClientAddSuccess" = "Cliente(s) de entrada adicionado(s)"
"inboundClientDeleteSuccess" = "Cliente de entrada excluído"
"inboundClientUpdateSuccess" = "Cliente de entrada atualizado"
"delDepletedClientsSuccess" = "Todos os clientes esgotados foram excluídos"
"resetAllClientTrafficSuccess" = "Todo o tráfego do cliente foi reiniciado"
"resetAllTrafficSuccess" = "Todo o tráfego foi reiniciado"
"resetInboundClientTrafficSuccess" = "O tráfego foi reiniciado"
"trafficGetError" = "Erro ao obter tráfegos"
"getNewX25519CertError" = "Erro ao obter o certificado X25519."
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
"getNewVlessEncError" = "Erro ao obter o certificado VlessEnc."
[pages.inbounds.stream.general]
"request" = "Requisição"
"response" = "Resposta"
"name" = "Nome"
"value" = "Valor"
[pages.inbounds.stream.tcp]
"version" = "Versão"
"method" = "Método"
"path" = "Caminho"
"status" = "Status"
"statusDescription" = "Descrição do Status"
"requestHeader" = "Cabeçalho da Requisição"
"responseHeader" = "Cabeçalho da Resposta"
[pages.settings]
"title" = "Configurações do Painel"
"save" = "Salvar"
"infoDesc" = "Toda alteração feita aqui precisa ser salva. Reinicie o painel para aplicar as alterações."
"restartPanel" = "Reiniciar Painel"
"restartPanelDesc" = "Tem certeza de que deseja reiniciar o painel? Se não conseguir acessar o painel após reiniciar, consulte os logs do painel no servidor."
"restartPanelSuccess" = "O painel foi reiniciado com sucesso"
"actions" = "Ações"
"resetDefaultConfig" = "Redefinir para Padrão"
"panelSettings" = "Geral"
"securitySettings" = "Autenticação"
"TGBotSettings" = "Bot do Telegram"
"panelListeningIP" = "IP de Escuta"
"panelListeningIPDesc" = "O endereço IP para o painel web. (deixe em branco para escutar em todos os IPs)"
"panelListeningDomain" = "Domínio de Escuta"
"panelListeningDomainDesc" = "O nome de domínio para o painel web. (deixe em branco para escutar em todos os domínios e IPs)"
"panelPort" = "Porta de Escuta"
"panelPortDesc" = "O número da porta para o painel web. (deve ser uma porta não usada)"
"publicKeyPath" = "Caminho da Chave Pública"
"publicKeyPathDesc" = "O caminho do arquivo de chave pública para o painel web. (começa com /)"
"privateKeyPath" = "Caminho da Chave Privada"
"privateKeyPathDesc" = "O caminho do arquivo de chave privada para o painel web. (começa com /)"
"panelUrlPath" = "Caminho URI"
"panelUrlPathDesc" = "O caminho URI para o painel web. (começa com / e termina com /)"
"pageSize" = "Tamanho da Paginação"
"pageSizeDesc" = "Definir o tamanho da página para a tabela de entradas. (0 = desativado)"
"remarkModel" = "Modelo de Observação & Caractere de Separação"
"datepicker" = "Tipo de Calendário"
"datepickerPlaceholder" = "Selecionar data"
"datepickerDescription" = "Tarefas agendadas serão executadas com base neste calendário."
"sampleRemark" = "Exemplo de Observação"
"oldUsername" = "Nome de Usuário Atual"
"currentPassword" = "Senha Atual"
"newUsername" = "Novo Nome de Usuário"
"newPassword" = "Nova Senha"
"telegramBotEnable" = "Ativar Bot do Telegram"
"telegramBotEnableDesc" = "Ativa o bot do Telegram."
"telegramToken" = "Token do Telegram"
"telegramTokenDesc" = "O token do bot do Telegram obtido de '@BotFather'."
"telegramProxy" = "Proxy SOCKS"
"telegramProxyDesc" = "Ativa o proxy SOCKS5 para conectar ao Telegram. (ajuste as configurações conforme o guia)"
"telegramAPIServer" = "API Server do Telegram"
"telegramAPIServerDesc" = "O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão."
"telegramChatId" = "ID de Chat do Administrador"
"telegramChatIdDesc" = "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui @userinfobot) ou (use o comando '/id' no bot)"
"telegramNotifyTime" = "Hora da Notificação"
"telegramNotifyTimeDesc" = "O horário de notificação do bot do Telegram configurado para relatórios periódicos. (use o formato de tempo do crontab)"
"tgNotifyBackup" = "Backup do Banco de Dados"
"tgNotifyBackupDesc" = "Enviar arquivo de backup do banco de dados junto com o relatório."
"tgNotifyLogin" = "Notificação de Login"
"tgNotifyLoginDesc" = "Receba notificações sobre o nome de usuário, endereço IP e horário sempre que alguém tentar fazer login no seu painel web."
"sessionMaxAge" = "Duração da Sessão"
"sessionMaxAgeDesc" = "A duração pela qual você pode permanecer logado. (unidade: minuto)"
"expireTimeDiff" = "Notificação de Expiração"
"expireTimeDiffDesc" = "Receba notificações sobre a data de expiração ao atingir esse limite. (unidade: dia)"
"trafficDiff" = "Notificação de Limite de Tráfego"
"trafficDiffDesc" = "Receba notificações sobre o limite de tráfego ao atingir esse limite. (unidade: GB)"
"tgNotifyCpu" = "Notificação de Carga da CPU"
"tgNotifyCpuDesc" = "Receba notificações se a carga da CPU ultrapassar esse limite. (unidade: %)"
"timeZone" = "Fuso Horário"
"timeZoneDesc" = "As tarefas agendadas serão executadas com base nesse fuso horário."
"subSettings" = "Assinatura"
"subEnable" = "Ativar Serviço de Assinatura"
"subEnableDesc" = "Ativa o serviço de assinatura."
"subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente."
"subTitle" = "Título da Assinatura"
"subTitleDesc" = "Título exibido no cliente VPN"
"subSupportUrl" = "URL de Suporte"
"subSupportUrlDesc" = "Link de suporte técnico exibido no cliente VPN"
"subProfileUrl" = "URL de Perfil"
"subProfileUrlDesc" = "Um link para o seu site exibido no cliente VPN"
"subAnnounce" = "Anúncio"
"subAnnounceDesc" = "O texto do anúncio exibido no cliente VPN"
"subEnableRouting" = "Ativar roteamento"
"subEnableRoutingDesc" = "Configuração global para habilitar o roteamento no cliente VPN. (Apenas para Happ)"
"subRoutingRules" = "Regras de roteamento"
"subRoutingRulesDesc" = "Regras de roteamento globais para o cliente VPN. (Apenas para Happ)"
"subListen" = "IP de Escuta"
"subListenDesc" = "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)"
"subPort" = "Porta de Escuta"
"subPortDesc" = "O número da porta para o serviço de assinatura. (deve ser uma porta não usada)"
"subCertPath" = "Caminho da Chave Pública"
"subCertPathDesc" = "O caminho do arquivo de chave pública para o serviço de assinatura. (começa com /)"
"subKeyPath" = "Caminho da Chave Privada"
"subKeyPathDesc" = "O caminho do arquivo de chave privada para o serviço de assinatura. (começa com /)"
"subPath" = "Caminho URI"
"subPathDesc" = "O caminho URI para o serviço de assinatura. (começa com / e termina com /)"
"subDomain" = "Domínio de Escuta"
"subDomainDesc" = "O nome de domínio para o serviço de assinatura. (deixe em branco para escutar em todos os domínios e IPs)"
"subUpdates" = "Intervalos de Atualização"
"subUpdatesDesc" = "Os intervalos de atualização da URL de assinatura nos aplicativos de cliente. (unidade: hora)"
"subEncrypt" = "Codificar"
"subEncryptDesc" = "O conteúdo retornado pelo serviço de assinatura será codificado em Base64."
"subShowInfo" = "Mostrar Informações de Uso"
"subShowInfoDesc" = "O tráfego restante e a data serão exibidos nos aplicativos de cliente."
"subURI" = "URI de Proxy Reverso"
"subURIDesc" = "O caminho URI da URL de assinatura para uso por trás de proxies."
"externalTrafficInformEnable" = "Informações de tráfego externo"
"externalTrafficInformEnableDesc" = "Informar a API externa sobre cada atualização de tráfego."
"externalTrafficInformURI" = "URI de informação de tráfego externo"
"externalTrafficInformURIDesc" = "As atualizações de tráfego são enviadas para este URI."
"fragment" = "Fragmentação"
"fragmentDesc" = "Ativa a fragmentação para o pacote TLS hello."
"fragmentSett" = "Configurações de Fragmentação"
"noisesDesc" = "Ativar Noises."
"noisesSett" = "Configurações de Noises"
"mux" = "Mux"
"muxDesc" = "Transmitir múltiplos fluxos de dados independentes dentro de um fluxo de dados estabelecido."
"muxSett" = "Configurações de Mux"
"direct" = "Conexão Direta"
"directDesc" = "Estabelece conexões diretamente com domínios ou intervalos de IP de um país específico."
"notifications" = "Notificações"
"certs" = "Certificados"
"externalTraffic" = "Tráfego Externo"
"dateAndTime" = "Data e Hora"
"proxyAndServer" = "Proxy e Servidor"
"intervals" = "Intervalos"
"information" = "Informação"
"language" = "Idioma"
"telegramBotLanguage" = "Idioma do Bot do Telegram"
[pages.xray]
"title" = "Configurações Xray"
"save" = "Salvar"
"restart" = "Reiniciar Xray"
"restartSuccess" = "Xray foi reiniciado com sucesso"
"stopSuccess" = "Xray foi interrompido com sucesso"
"restartError" = "Ocorreu um erro ao reiniciar o Xray."
"stopError" = "Ocorreu um erro ao parar o Xray."
"basicTemplate" = "Básico"
"advancedTemplate" = "Avançado"
"generalConfigs" = "Geral"
"generalConfigsDesc" = "Essas opções determinam ajustes gerais."
"logConfigs" = "Log"
"logConfigsDesc" = "Os logs podem afetar a eficiência do servidor. É recomendável habilitá-los com sabedoria apenas se necessário."
"blockConfigsDesc" = "Essas opções bloqueiam tráfego com base em protocolos e sites específicos solicitados."
"basicRouting" = "Roteamento Básico"
"blockConnectionsConfigsDesc" = "Essas opções bloquearão o tráfego com base no país solicitado."
"directConnectionsConfigsDesc" = "Uma conexão direta garante que o tráfego específico não seja roteado por outro servidor."
"blockips" = "Bloquear IPs"
"blockdomains" = "Bloquear Domínios"
"directips" = "IPs Diretos"
"directdomains" = "Domínios Diretos"
"ipv4Routing" = "Roteamento IPv4"
"ipv4RoutingDesc" = "Essas opções roteam o tráfego para um destino específico via IPv4."
"warpRouting" = "Roteamento WARP"
"warpRoutingDesc" = "Essas opções roteam o tráfego para um destino específico via WARP."
"Template" = "Modelo de Configuração Avançada do Xray"
"TemplateDesc" = "O arquivo final de configuração do Xray será gerado com base neste modelo."
"FreedomStrategy" = "Estratégia do Protocolo Freedom"
"FreedomStrategyDesc" = "Definir a estratégia de saída para a rede no Protocolo Freedom."
"RoutingStrategy" = "Estratégia Geral de Roteamento"
"RoutingStrategyDesc" = "Definir a estratégia geral de roteamento de tráfego para resolver todas as solicitações."
"outboundTestUrl" = "URL de teste de outbound"
"outboundTestUrlDesc" = "URL usada ao testar conectividade do outbound"
"Torrent" = "Bloquear Protocolo BitTorrent"
"Inbounds" = "Inbounds"
"InboundsDesc" = "Aceitar clientes específicos."
"Outbounds" = "Outbounds"
"Balancers" = "Balanceadores"
"OutboundsDesc" = "Definir o caminho de saída do tráfego."
"Routings" = "Regras de Roteamento"
"RoutingsDesc" = "A prioridade de cada regra é importante!"
"completeTemplate" = "Todos"
"logLevel" = "Nível de Log"
"logLevelDesc" = "O nível de log para erros, indicando a informação que precisa ser registrada."
"accessLog" = "Log de Acesso"
"accessLogDesc" = "O caminho do arquivo para o log de acesso. O valor especial 'none' desativa os logs de acesso."
"errorLog" = "Log de Erros"
"errorLogDesc" = "O caminho do arquivo para o log de erros. O valor especial 'none' desativa os logs de erro."
"dnsLog" = "Log DNS"
"dnsLogDesc" = "Se ativar logs de consulta DNS"
"maskAddress" = "Mascarar Endereço"
"maskAddressDesc" = "Máscara de endereço IP, quando ativado, substitui automaticamente o endereço IP que aparece no log."
"statistics" = "Estatísticas"
"statsInboundUplink" = "Estatísticas de Upload de Entrada"
"statsInboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de entrada."
"statsInboundDownlink" = "Estatísticas de Download de Entrada"
"statsInboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de entrada."
"statsOutboundUplink" = "Estatísticas de Upload de Saída"
"statsOutboundUplinkDesc" = "Habilita a coleta de estatísticas para o tráfego de upload de todos os proxies de saída."
"statsOutboundDownlink" = "Estatísticas de Download de Saída"
"statsOutboundDownlinkDesc" = "Habilita a coleta de estatísticas para o tráfego de download de todos os proxies de saída."
[pages.xray.rules]
"first" = "Primeiro"
"last" = "Último"
"up" = "Cima"
"down" = "Baixo"
"source" = "Fonte"
"dest" = "Destino"
"inbound" = "Entrada"
"outbound" = "Saída"
"balancer" = "Balanceador"
"info" = "Info"
"add" = "Adicionar Regra"
"edit" = "Editar Regra"
"useComma" = "Itens separados por vírgula"
[pages.xray.outbound]
"addOutbound" = "Adicionar Saída"
"addReverse" = "Adicionar Reverso"
"editOutbound" = "Editar Saída"
"editReverse" = "Editar Reverso"
"tag" = "Tag"
"tagDesc" = "Tag Única"
"address" = "Endereço"
"reverse" = "Reverso"
"domain" = "Domínio"
"type" = "Tipo"
"bridge" = "Ponte"
"portal" = "Portal"
"link" = "Link"
"intercon" = "Interconexão"
"settings" = "Configurações"
"accountInfo" = "Informações da Conta"
"outboundStatus" = "Status de Saída"
"sendThrough" = "Enviar Através de"
"test" = "Testar"
"testResult" = "Resultado do teste"
"testing" = "Testando conexão..."
"testSuccess" = "Teste bem-sucedido"
"testFailed" = "Teste falhou"
"testError" = "Falha ao testar saída"
[pages.xray.balancer]
"addBalancer" = "Adicionar Balanceador"
"editBalancer" = "Editar Balanceador"
"balancerStrategy" = "Estratégia"
"balancerSelectors" = "Seletores"
"tag" = "Tag"
"tagDesc" = "Tag Única"
"balancerDesc" = "Não é possível usar balancerTag e outboundTag ao mesmo tempo. Se usados simultaneamente, apenas outboundTag funcionará."
[pages.xray.wireguard]
"secretKey" = "Chave Secreta"
"publicKey" = "Chave Pública"
"allowedIPs" = "IPs Permitidos"
"endpoint" = "Ponto Final"
"psk" = "Chave Pré-Compartilhada"
"domainStrategy" = "Estratégia de Domínio"
[pages.xray.tun]
"nameDesc" = "O nome da interface TUN. O padrão é 'xray0'"
"mtuDesc" = "Unidade Máxima de Transmissão. O tamanho máximo dos pacotes de dados. O padrão é 1500"
"userLevel" = "Nível do Usuário"
"userLevelDesc" = "Todas as conexões feitas através deste inbound usarão este nível de usuário. O padrão é 0"
[pages.xray.dns]
"enable" = "Ativar DNS"
"enableDesc" = "Ativar o servidor DNS integrado"
"tag" = "Tag de Entrada DNS"
"tagDesc" = "Esta tag estará disponível como uma tag de Entrada nas regras de roteamento."
"clientIp" = "IP do Cliente"
"clientIpDesc" = "Usado para notificar o servidor sobre a localização IP especificada durante consultas DNS"
"disableCache" = "Desativar cache"
"disableCacheDesc" = "Desativa o cache de DNS"
"disableFallback" = "Desativar Fallback"
"disableFallbackDesc" = "Desativa consultas DNS de fallback"
"disableFallbackIfMatch" = "Desativar Fallback Se Corresponder"
"disableFallbackIfMatchDesc" = "Desativa consultas DNS de fallback quando a lista de domínios correspondentes do servidor DNS é atingida"
"enableParallelQuery" = "Habilitar Consulta Paralela"
"enableParallelQueryDesc" = "Habilitar consultas DNS paralelas para múltiplos servidores para resolução mais rápida"
"strategy" = "Estratégia de Consulta"
"strategyDesc" = "Estratégia geral para resolver nomes de domínio"
"add" = "Adicionar Servidor"
"edit" = "Editar Servidor"
"domains" = "Domínios"
"expectIPs" = "IPs Esperadas"
"unexpectIPs" = "IPs inesperados"
"useSystemHosts" = "Usar Hosts do sistema"
"useSystemHostsDesc" = "Usar o arquivo hosts de um sistema instalado"
"usePreset" = "Usar modelo"
"dnsPresetTitle" = "Modelos DNS"
"dnsPresetFamily" = "Familiar"
[pages.xray.fakedns]
"add" = "Adicionar Fake DNS"
"edit" = "Editar Fake DNS"
"ipPool" = "Sub-rede do Pool de IP"
"poolSize" = "Tamanho do Pool"
[pages.settings.security]
"admin" = "Credenciais de administrador"
"twoFactor" = "Autenticação de dois fatores"
"twoFactorEnable" = "Ativar 2FA"
"twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança."
"twoFactorModalSetTitle" = "Ativar autenticação de dois fatores"
"twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores"
"twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:"
"twoFactorModalFirstStep" = "1. Escaneie este QR code no aplicativo de autenticação ou copie o token próximo ao QR code e cole no aplicativo"
"twoFactorModalSecondStep" = "2. Digite o código do aplicativo"
"twoFactorModalRemoveStep" = "Digite o código do aplicativo para remover a autenticação de dois fatores."
"twoFactorModalChangeCredentialsTitle" = "Alterar credenciais"
"twoFactorModalChangeCredentialsStep" = "Insira o código do aplicativo para alterar as credenciais do administrador."
"twoFactorModalSetSuccess" = "A autenticação de dois fatores foi estabelecida com sucesso"
"twoFactorModalDeleteSuccess" = "A autenticação de dois fatores foi excluída com sucesso"
"twoFactorModalError" = "Código incorreto"
[pages.settings.toasts]
"modifySettings" = "Os parâmetros foram alterados."
"getSettings" = "Ocorreu um erro ao recuperar os parâmetros."
"modifyUserError" = "Ocorreu um erro ao alterar as credenciais do administrador."
"modifyUser" = "Você alterou com sucesso as credenciais do administrador."
"originalUserPassIncorrect" = "O nome de usuário ou senha atual é inválido"
"userPassMustBeNotEmpty" = "O novo nome de usuário e senha não podem estar vazios"
"getOutboundTrafficError" = "Erro ao obter tráfego de saída"
"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
[tgbot]
"keyboardClosed" = "❌ Teclado fechado!"
"noResult" = "❗ Nenhum resultado!"
"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
"wentWrong" = "❌ Algo deu errado!"
"noIpRecord" = "❗ Nenhum registro de IP!"
"noInbounds" = "❗ Nenhum inbound encontrado!"
"unlimited" = "♾ Ilimitado (Reset)"
"add" = "Adicionar"
"month" = "Mês"
"months" = "Meses"
"day" = "Dia"
"days" = "Dias"
"hours" = "Horas"
"minutes" = "Minutos"
"unknown" = "Desconhecido"
"inbounds" = "Inbounds"
"clients" = "Clientes"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
[tgbot.commands]
"unknown" = "❗ Comando desconhecido."
"pleaseChoose" = "👇 Escolha:\r\n"
"help" = "🤖 Bem-vindo a este bot! Ele foi projetado para oferecer dados específicos do painel da web e permite que você faça as modificações necessárias.\r\n\r\n"
"start" = "👋 Olá <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Bem-vindo ao bot de gerenciamento do <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Bot está OK!"
"usage" = "❗ Por favor, forneça um texto para pesquisar!"
"getID" = "🆔 Seu ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Para reiniciar o Xray Core:\r\n<code>/restart</code>\r\n\r\nPara pesquisar por um email de cliente:\r\n<code>/usage [Email]</code>\r\n\r\nPara pesquisar por inbounds (com estatísticas do cliente):\r\n<code>/inbound [Remark]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"helpClientCommands" = "Para pesquisar por estatísticas, use o seguinte comando:\r\n\r\n<code>/usage [Email]</code>\r\n\r\nTelegram Chat ID:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Operação bem-sucedida!"
"restartFailed" = "❗ Erro na operação.\r\n\r\n<code>Erro: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core não está em execução."
"startDesc" = "Mostrar menu principal"
"helpDesc" = "Ajuda do bot"
"statusDesc" = "Verificar status do bot"
"idDesc" = "Mostrar seu ID do Telegram"
[tgbot.messages]
"cpuThreshold" = "🔴 A carga da CPU {{ .Percent }}% excede o limite de {{ .Threshold }}%"
"selectUserFailed" = "❌ Erro na seleção do usuário!"
"userSaved" = "✅ Usuário do Telegram salvo."
"loginSuccess" = "✅ Conectado ao painel com sucesso.\r\n"
"loginFailed" = "❗Tentativa de login no painel falhou.\r\n"
"2faFailed" = "Falha no 2FA"
"report" = "🕰 Relatórios agendados: {{ .RunTime }}\r\n"
"datetime" = "⏰ Data&Hora: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
"version" = "🚀 Versão 3X-UI: {{ .Version }}\r\n"
"xrayVersion" = "📡 Versão Xray: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IPs:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Tempo de atividade: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Carga do sistema: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Tráfego: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Status: {{ .State }}\r\n"
"username" = "👤 Nome de usuário: {{ .Username }}\r\n"
"password" = "👤 Senha: {{ .Password }}\r\n"
"time" = "⏰ Hora: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Porta: {{ .Port }}\r\n"
"expire" = "📅 Data de expiração: {{ .Time }}\r\n"
"expireIn" = "📅 Expira em: {{ .Time }}\r\n"
"active" = "💡 Ativo: {{ .Enable }}\r\n"
"enabled" = "🚨 Ativado: {{ .Enable }}\r\n"
"online" = "🌐 Status da conexão: {{ .Status }}\r\n"
"lastOnline" = "🔙 Última vez online: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Usuário do Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 {{ .Type }} esgotado:\r\n"
"exhaustedCount" = "🚨 Contagem de {{ .Type }} esgotado:\r\n"
"onlinesCount" = "🌐 Clientes online: {{ .Count }}\r\n"
"disabled" = "🛑 Desativado: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Esgotar em breve: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Hora do backup: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Atualizado em: {{ .Time }}\r\n\r\n"
"yes" = "✅ Sim"
"no" = "❌ Não"
"received_id" = "🔑📥 ID atualizado."
"received_password" = "🔑📥 Senha atualizada."
"received_email" = "📧📥 E-mail atualizado."
"received_comment" = "💬📥 Comentário atualizado."
"id_prompt" = "🔑 ID Padrão: {{ .ClientId }}\n\nDigite seu ID."
"pass_prompt" = "🔑 Senha Padrão: {{ .ClientPassword }}\n\nDigite sua senha."
"email_prompt" = "📧 E-mail Padrão: {{ .ClientEmail }}\n\nDigite seu e-mail."
"comment_prompt" = "💬 Comentário Padrão: {{ .ClientComment }}\n\nDigite seu comentário."
"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!"
"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Senha: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!"
"cancel" = "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄"
"error_add_client" = "⚠️ Erro:\n\n {{ .error }}"
"using_default_value" = "Tudo bem, vou manter o valor padrão. 😊"
"incorrect_input" = "Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫"
"AreYouSure" = "Você tem certeza? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠 Erro: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Processo de redefinição de tráfego concluído para todos os clientes."
[tgbot.buttons]
"closeKeyboard" = "❌ Fechar teclado"
"cancel" = "❌ Cancelar"
"cancelReset" = "❌ Cancelar redefinição"
"cancelIpLimit" = "❌ Cancelar limite de IP"
"confirmResetTraffic" = "✅ Confirmar redefinição de tráfego?"
"confirmClearIps" = "✅ Confirmar limpar IPs?"
"confirmRemoveTGUser" = "✅ Confirmar remover usuário do Telegram?"
"confirmToggle" = "✅ Confirmar ativar/desativar usuário?"
"dbBackup" = "Obter backup do DB"
"serverUsage" = "Uso do servidor"
"getInbounds" = "Obter Inbounds"
"depleteSoon" = "Esgotar em breve"
"clientUsage" = "Obter uso"
"onlines" = "Clientes online"
"commands" = "Comandos"
"refresh" = "🔄 Atualizar"
"clearIPs" = "❌ Limpar IPs"
"removeTGUser" = "❌ Remover usuário do Telegram"
"selectTGUser" = "👤 Selecionar usuário do Telegram"
"selectOneTGUser" = "👤 Selecione um usuário do Telegram:"
"resetTraffic" = "📈 Redefinir tráfego"
"resetExpire" = "📅 Alterar data de expiração"
"ipLog" = "🔢 Log de IP"
"ipLimit" = "🔢 Limite de IP"
"setTGUser" = "👤 Definir usuário do Telegram"
"toggle" = "🔘 Ativar / Desativar"
"custom" = "🔢 Personalizado"
"confirmNumber" = "✅ Confirmar: {{ .Num }}"
"confirmNumberAdd" = "✅ Confirmar adicionar: {{ .Num }}"
"limitTraffic" = "🚧 Limite de tráfego"
"getBanLogs" = "Obter logs de banimento"
"allClients" = "Todos os clientes"
"addClient" = "Adicionar Cliente"
"submitDisable" = "Enviar como Desativado ☑️"
"submitEnable" = "Enviar como Ativado ✅"
"use_default" = "🏷️ Usar padrão"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Senha"
"change_email" = "⚙️📧 E-mail"
"change_comment" = "⚙️💬 Comentário"
"ResetAllTraffics" = "Redefinir Todo o Tráfego"
"SortedTrafficUsageReport" = "Relatório de Uso de Tráfego Ordenado"
[tgbot.answers]
"successfulOperation" = "✅ Operação bem-sucedida!"
"errorOperation" = "❗ Erro na operação."
"getInboundsFailed" = "❌ Falha ao obter inbounds."
"getClientsFailed" = "❌ Falha ao obter clientes."
"canceled" = "❌ {{ .Email }}: Operação cancelada."
"clientRefreshSuccess" = "✅ {{ .Email }}: Cliente atualizado com sucesso."
"IpRefreshSuccess" = "✅ {{ .Email }}: IPs atualizados com sucesso."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Usuário do Telegram do cliente atualizado com sucesso."
"resetTrafficSuccess" = "✅ {{ .Email }}: Tráfego redefinido com sucesso."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Limite de tráfego salvo com sucesso."
"expireResetSuccess" = "✅ {{ .Email }}: Dias de expiração redefinidos com sucesso."
"resetIpSuccess" = "✅ {{ .Email }}: Limite de IP {{ .Count }} salvo com sucesso."
"clearIpSuccess" = "✅ {{ .Email }}: IPs limpos com sucesso."
"getIpLog" = "✅ {{ .Email }}: Obter log de IP."
"getUserInfo" = "✅ {{ .Email }}: Obter informações do usuário do Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Usuário do Telegram removido com sucesso."
"enableSuccess" = "✅ {{ .Email }}: Ativado com sucesso."
"disableSuccess" = "✅ {{ .Email }}: Desativado com sucesso."
"askToAddUserId" = "Sua configuração não foi encontrada!\r\nPeça ao seu administrador para usar seu Telegram ChatID em suas configurações.\r\n\r\nSeu ChatID: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Escolha um cliente para Inbound {{ .Inbound }}"
"chooseInbound" = "Escolha um Inbound"

View file

@ -1,795 +0,0 @@
"username" = "Имя пользователя"
"password" = "Пароль"
"login" = "Войти"
"confirm" = "Подтвердить"
"cancel" = "Отмена"
"close" = "Закрыть"
"create" = "Создать"
"update" = "Обновить"
"copy" = "Копировать"
"copied" = "Скопировано"
"download" = "Скачать"
"remark" = "Примечание"
"enable" = "Включить"
"protocol" = "Протокол"
"search" = "Поиск"
"filter" = "Фильтр"
"loading" = "Загрузка..."
"second" = "Секунда"
"minute" = "Минута"
"hour" = "Час"
"day" = "День"
"check" = "Проверить"
"indefinite" = "Бесконечно"
"unlimited" = "Безлимит"
"none" = "Пусто"
"qrCode" = "QR-код"
"info" = "Информация"
"edit" = "Изменить"
"delete" = "Удалить"
"reset" = "Сбросить"
"noData" = "Нет данных."
"copySuccess" = "Скопировано"
"sure" = "Да"
"encryption" = "Шифрование"
"useIPv4ForHost" = "Использовать IPv4 для подключения к хосту"
"transmission" = "Транспорт"
"host" = "Хост"
"path" = "Путь"
"camouflage" = "Маскировка"
"status" = "Статус"
"enabled" = "Включено"
"disabled" = "Отключено"
"depleted" = "Исчерпано"
"depletingSoon" = "Почти исчерпано"
"offline" = "Офлайн"
"online" = "Онлайн"
"domainName" = "Домен"
"monitor" = "Мониторинг IP"
"certificate" = "SSL-сертификат"
"fail" = "Сбой"
"comment" = "Комментарий"
"success" = "Успешно"
"lastOnline" = "Был(а) в сети"
"getVersion" = "Узнать версию"
"install" = "Установка"
"clients" = "Клиенты"
"usage" = "Использование"
"twoFactorCode" = "Код 2FA"
"remained" = "Остаток"
"security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата."
"secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак."
"secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных."
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт."
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI."
"secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
"secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
"emptyDnsDesc" = "Нет добавленных DNS-серверов."
"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов."
"emptyBalancersDesc" = "Нет добавленных балансировщиков."
"emptyReverseDesc" = "Нет добавленных реверс-прокси."
"somethingWentWrong" = "Что-то пошло не так"
[subscription]
"title" = "Информация о подписке"
"subId" = "ID подписки"
"status" = "Статус"
"downloaded" = "Загружено"
"uploaded" = "Отправлено"
"expiry" = "Срок действия"
"totalQuota" = "Общий лимит"
"individualLinks" = "Индивидуальные ссылки"
"active" = "Активна"
"inactive" = "Неактивна"
"unlimited" = "Неограниченно"
"noExpiry" = "Бессрочно"
[menu]
"theme" = "Тема"
"dark" = "Темная"
"ultraDark" = "Очень темная"
"dashboard" = "Дашборд"
"inbounds" = "Подключения"
"settings" = "Настройки"
"xray" = "Настройки Xray"
"logout" = "Выход"
"link" = "Управление"
[pages.login]
"hello" = "Привет!"
"title" = "Добро пожаловать!"
"loginAgain" = "Сессия истекла. Войдите в систему снова"
[pages.login.toasts]
"invalidFormData" = "Недопустимый формат данных"
"emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверные данные учетной записи."
"successLogin" = "Вход выполнен успешно"
"successRegister" = "Регистрация прошла успешно, пожалуйста, войдите."
"userExists" = "Имя пользователя уже существует"
"errorRegister" = "Ошибка регистрации"
[pages.index]
"title" = "Дашборд"
"cpu" = "ЦП"
"logicalProcessors" = "Логические процессоры"
"frequency" = "Частота"
"swap" = "Файл подкачки"
"storage" = "Диск"
"memory" = "ОЗУ"
"threads" = "Потоки"
"xrayStatus" = "Xray"
"stopXray" = "Остановить"
"restartXray" = "Перезапустить"
"xraySwitch" = "Выбор версии"
"xraySwitchClick" = "Выберите нужную версию"
"xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки"
"xrayStatusUnknown" = "Неизвестно"
"xrayStatusRunning" = "Запущен"
"xrayStatusStop" = "Остановлен"
"xrayStatusError" = "Ошибка"
"xrayErrorPopoverTitle" = "Ошибка при запуске Xray"
"operationHours" = "Время работы системы"
"systemLoad" = "Нагрузка на систему"
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionCount" = "Количество соединений"
"ipAddresses" = "IP-адреса сервера"
"toggleIpVisibility" = "Скрыть или показать IP-адреса сервера"
"overallSpeed" = "Общая скорость передачи трафика"
"upload" = "Отправка"
"download" = "Загрузка"
"totalData" = "Общий объем трафика"
"sent" = "Отправлено"
"received" = "Получено"
"documentation" = "Документация"
"xraySwitchVersionDialog" = "Переключить версию Xray"
"xraySwitchVersionDialogDesc" = "Вы точно хотите сменить версию Xray?"
"xraySwitchVersionPopover" = "Xray успешно обновлён"
"geofileUpdateDialog" = "Вы действительно хотите обновить геофайл?"
"geofileUpdateDialogDesc" = "Это обновит файл #filename#."
"geofilesUpdateDialogDesc" = "Это обновит все геофайлы."
"geofilesUpdateAll" = "Обновить все"
"geofileUpdatePopover" = "Геофайлы успешно обновлены"
"dontRefresh" = "Установка в процессе. Не обновляйте страницу"
"logs" = "Журнал"
"config" = "Конфигурация"
"backup" = "Резервная копия"
"backupTitle" = "Резервная копия базы данных"
"exportDatabase" = "Экспорт базы данных"
"exportDatabaseDesc" = "Нажмите, чтобы скачать файл .db, содержащий резервную копию вашей текущей базы данных на ваше устройство."
"importDatabase" = "Импорт базы данных"
"importDatabaseDesc" = "Нажмите, чтобы выбрать и загрузить файл .db с вашего устройства для восстановления базы данных из резервной копии."
"importDatabaseSuccess" = "База данных успешно импортирована"
"importDatabaseError" = "Произошла ошибка при импорте базы данных"
"readDatabaseError" = "Произошла ошибка при чтении базы данных"
"getDatabaseError" = "Произошла ошибка при получении базы данных"
"getConfigError" = "Произошла ошибка при получении конфигурационного файла"
[pages.inbounds]
"allTimeTraffic" = "Общий трафик"
"allTimeTrafficUsage" = "Общее использование за все время"
"title" = "Подключения"
"totalDownUp" = "Отправлено/получено"
"totalUsage" = "Всего трафика"
"inboundCount" = "Всего подключений"
"operate" = "Меню"
"enable" = "Включить"
"remark" = "Примечание"
"protocol" = "Протокол"
"port" = "Порт"
"portMap" = "Порт-маппинг"
"traffic" = "Трафик"
"details" = "Подробнее"
"transportConfig" = "Транспорт"
"expireDate" = "Дата окончания"
"createdAt" = "Создано"
"updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика"
"addInbound" = "Создать подключение"
"generalActions" = "Общие действия"
"autoRefresh" = "Автообновление"
"autoRefreshInterval" = "Интервал"
"modifyInbound" = "Изменить подключение"
"deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
"deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
"copyLink" = "Копировать ссылку"
"address" = "Адрес"
"network" = "Сеть"
"destinationPort" = "Порт назначения"
"targetAddress" = "Целевой адрес"
"monitorDesc" = "Оставьте пустым для прослушивания всех IP-адресов"
"meansNoLimit" = "= Без ограничений (значение: ГБ)"
"totalFlow" = "Общий расход"
"leaveBlankToNeverExpire" = "Оставьте пустым, чтобы было бесконечным"
"noRecommendKeepDefault" = "Рекомендуется оставить настройки по умолчанию"
"certificatePath" = "Путь к сертификату"
"certificateContent" = "Содержимое сертификата"
"publicKey" = "Публичный ключ"
"privatekey" = "Приватный ключ"
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
"client" = "Клиент"
"export" = "Экспорт ссылок"
"clone" = "Клонировать"
"cloneInbound" = "Клонировать"
"cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Сброс трафика всех подключений"
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
"resetInboundClientTraffics" = "Сброс трафика клиента"
"resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
"resetAllClientTraffics" = "Сброс трафика всех клиентов"
"resetAllClientTrafficTitle" = "Сброс трафика всех клиентов"
"resetAllClientTrafficContent" = "Вы уверены, что хотите сбросить трафик всех клиентов?"
"delDepletedClients" = "Удалить отключенных клиентов"
"delDepletedClientsTitle" = "Удаление отключенных клиентов"
"delDepletedClientsContent" = "Вы уверены, что хотите удалить всех отключенных клиентов?"
"email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "Лимит по количеству IP"
"IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 отключить)"
"IPLimitlog" = "Лог IP-адресов"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)"
"IPLimitlogclear" = "Очистить лог"
"setDefaultCert" = "Установить сертификат панели"
"telegramDesc" = "Пожалуйста, укажите Chat ID Telegram. (используйте команду '/id' в боте) или (@userinfobot)"
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'"
"info" = "Информация"
"same" = "Тот же"
"inboundData" = "Данные подключений"
"exportInbound" = "Экспорт подключений"
"import" = "Импортировать"
"importInbound" = "Импорт подключений"
"periodicTrafficResetTitle" = "Сброс трафика"
"periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы"
"lastReset" = "Последний сброс"
[pages.client]
"add" = "Добавить клиента"
"edit" = "Редактировать клиента"
"submitAdd" = "Добавить"
"submitEdit" = "Сохранить изменения"
"clientCount" = "Количество клиентов"
"bulk" = "Добавить несколько"
"method" = "Метод"
"first" = "Первый"
"last" = "Последний"
"prefix" = "Префикс"
"postfix" = "Постфикс"
"delayedStart" = "Начало использования"
"expireDays" = "Длительность"
"days" = "дней"
"renew" = "Автопродление"
"renewDesc" = "Автопродление после истечения срока действия. (0 = отключить)(единица: день)"
[pages.inbounds.periodicTrafficReset]
"never" = "Никогда"
"daily" = "Ежедневно"
"weekly" = "Еженедельно"
"monthly" = "Ежемесячно"
[pages.inbounds.toasts]
"obtain" = "Получить"
"updateSuccess" = "Обновление прошло успешно"
"logCleanSuccess" = "Лог был очищен"
"inboundsUpdateSuccess" = "Подключения успешно обновлены"
"inboundUpdateSuccess" = "Подключение успешно обновлено"
"inboundCreateSuccess" = "Подключение успешно создано"
"inboundDeleteSuccess" = "Подключение успешно удалено"
"inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)"
"inboundClientDeleteSuccess" = "Клиент подключения удалён"
"inboundClientUpdateSuccess" = "Клиент подключения обновлён"
"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
"resetAllTrafficSuccess" = "Весь трафик сброшен"
"resetInboundClientTrafficSuccess" = "Трафик сброшен"
"trafficGetError" = "Ошибка получения данных о трафике"
"getNewX25519CertError" = "Ошибка при получении сертификата X25519."
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
"getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc."
[pages.inbounds.stream.general]
"request" = "Запрос"
"response" = "Ответ"
"name" = "Имя"
"value" = "Значение"
[pages.inbounds.stream.tcp]
"version" = "Версия"
"method" = "Метод"
"path" = "Путь"
"status" = "Статус"
"statusDescription" = "Описание статуса"
"requestHeader" = "Заголовок запроса"
"responseHeader" = "Заголовок ответа"
[pages.settings]
"title" = "Настройки"
"save" = "Сохранить"
"infoDesc" = "Сохраните изменения и перезапустите панель для их применения."
"restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
"restartPanelSuccess" = "Панель успешно перезапущена"
"actions" = "Действия"
"resetDefaultConfig" = "Восстановить настройки по умолчанию"
"panelSettings" = "Панель"
"securitySettings" = "Учетная запись"
"TGBotSettings" = "Telegram-Бот"
"panelListeningIP" = "IP-адрес для управления панелью"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен панели"
"panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP."
"panelPort" = "Порт панели"
"panelPortDesc" = "Порт, на котором работает панель"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
"publicKeyPathDesc" = "Введите полный путь, начинающийся с '/'"
"privateKeyPath" = "Путь к файлу приватного ключа сертификата панели"
"privateKeyPathDesc" = "Введите полный путь, начинающийся с '/'"
"panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
"pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения"
"datepicker" = "Тип календаря"
"datepickerPlaceholder" = "Выберите дату"
"datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем."
"sampleRemark" = "Пример примечания"
"oldUsername" = "Текущий логин"
"currentPassword" = "Текущий пароль"
"newUsername" = "Новый логин"
"newPassword" = "Новый пароль"
"telegramBotEnable" = "Включить Telegram бота"
"telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота"
"telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramProxy" = "Прокси-сервер Socks5"
"telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству."
"telegramAPIServer" = "API-сервер Telegram"
"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
"telegramChatId" = "User ID администратора бота"
"telegramChatIdDesc" = "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте @userinfobot или команду '/id' в боте."
"telegramNotifyTime" = "Частота уведомлений для администраторов от бота"
"telegramNotifyTimeDesc" = "Укажите интервал уведомлений в формате Crontab"
"tgNotifyBackup" = "Резервное копирование базы данных"
"tgNotifyBackupDesc" = "Отправлять уведомление с файлом резервной копии базы данных"
"tgNotifyLogin" = "Уведомление о входе"
"tgNotifyLoginDesc" = "Отображает имя пользователя, IP-адрес и время, когда кто-то пытается войти в вашу панель."
"sessionMaxAge" = "Продолжительность сессии"
"sessionMaxAgeDesc" = "Продолжительность сессии в системе (значение: минута)"
"expireTimeDiff" = "Задержка уведомления об истечении сессии"
"expireTimeDiffDesc" = "Получение уведомления об истечении срока действия сессии до достижения порогового значения (значение: день)"
"trafficDiff" = "Порог трафика для уведомления"
"trafficDiffDesc" = "Получение уведомления об исчерпании трафика до достижения порога (значение: ГБ)"
"tgNotifyCpu" = "Порог нагрузки на ЦП для уведомления"
"tgNotifyCpuDesc" = "Уведомление администраторов в Telegram, если нагрузка на ЦП превышает этот порог (значение: %)"
"timeZone" = "Часовой пояс"
"timeZoneDesc" = "Запланированные задачи выполняются в соответствии со временем в этом часовом поясе"
"subSettings" = "Подписка"
"subEnable" = "Включить подписку"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
"subTitle" = "Заголовок подписки"
"subTitleDesc" = "Название подписки, которое видит клиент в VPN-клиенте"
"subSupportUrl" = "URL поддержки"
"subSupportUrlDesc" = "Ссылка на техническую поддержку, отображаемая в VPN-клиенте"
"subProfileUrl" = "URL профиля"
"subProfileUrlDesc" = "Ссылка на ваш сайт, отображаемая в VPN-клиенте"
"subAnnounce" = "Объявление"
"subAnnounceDesc" = "Текст объявления, отображаемый в VPN-клиенте"
"subEnableRouting" = "Включить маршрутизацию"
"subEnableRoutingDesc" = "Глобальная настройка для включения маршрутизации в VPN-клиенте. (Только для Happ)"
"subRoutingRules" = "Правила маршрутизации"
"subRoutingRulesDesc" = "Глобальные правила маршрутизации для VPN-клиента. (Только для Happ)"
"subListen" = "Прослушивание IP"
"subListenDesc" = "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса"
"subPort" = "Порт подписки"
"subPortDesc" = "Номер порта для обслуживания службы подписки не должен использоваться на сервере"
"subCertPath" = "Путь к файлу публичного ключа сертификата подписки"
"subCertPathDesc" = "Введите полный путь, начинающийся с '/'"
"subKeyPath" = "Путь к файлу приватного ключа сертификата подписки"
"subKeyPathDesc" = "Введите полный путь, начинающийся с '/'"
"subPath" = "Корневой путь URL-адреса подписки"
"subPathDesc" = "Должен начинаться с '/' и заканчиваться на '/'"
"subDomain" = "Домен прослушивания"
"subDomainDesc" = "Оставьте пустым по умолчанию, чтобы слушать все домены и IP-адреса"
"subUpdates" = "Интервалы обновления подписки"
"subUpdatesDesc" = "Интервал между обновлениями в клиентском приложении (в часах)"
"subEncrypt" = "Шифровать конфиги"
"subEncryptDesc" = "Шифровать возвращенные конфиги в подписке"
"subShowInfo" = "Показать информацию об использовании"
"subShowInfoDesc" = "Отображать остаток трафика и дату окончания после имени конфигурации"
"subURI" = "URI обратного прокси"
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
"externalTrafficInformEnable" = "Информация о внешнем трафике"
"externalTrafficInformEnableDesc" = "Информировать внешний API о каждом обновлении трафика"
"externalTrafficInformURI" = "URI информации о внешнем трафике"
"externalTrafficInformURIDesc" = "Обновления трафика отправляются на этот URI"
"fragment" = "Фрагментация"
"fragmentDesc" = "Включить фрагментацию TLS-хэндшейка"
"fragmentSett" = "Настройки фрагментации"
"noisesDesc" = "Включить Noises."
"noisesSett" = "Настройки Noises"
"mux" = "Mux"
"muxDesc" = "Передача нескольких независимых потоков данных в одном соединении."
"muxSett" = "Настройки Mux"
"direct" = "Прямое подключение"
"directDesc" = "Устанавливает прямые соединения с доменами или IP-адресами определённой страны."
"notifications" = "Уведомления"
"certs" = "Сертификаты"
"externalTraffic" = "Внешний трафик"
"dateAndTime" = "Дата и время"
"proxyAndServer" = "Прокси и сервер"
"intervals" = "Интервалы"
"information" = "Информация"
"language" = "Язык интерфейса"
"telegramBotLanguage" = "Язык Telegram-бота"
[pages.xray]
"title" = "Настройки Xray"
"save" = "Сохранить"
"restart" = "Перезапуск Xray"
"restartSuccess" = "Xray успешно перезапущен"
"stopSuccess" = "Xray успешно остановлен"
"restartError" = "Произошла ошибка при перезапуске Xray."
"stopError" = "Произошла ошибка при остановке Xray."
"basicTemplate" = "Основное"
"advancedTemplate" = "Расширенный шаблон"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
"logConfigs" = "Логи"
"logConfigsDesc" = "Логи могут замедлять работу сервера. Включайте только нужные вам виды логов при необходимости!"
"blockConfigsDesc" = "Настройте, чтобы клиенты не имели доступа к определенным протоколам"
"basicRouting" = "Базовые соединения"
"blockConnectionsConfigsDesc" = "Эти параметры будут блокировать трафик в зависимости от страны назначения."
"directConnectionsConfigsDesc" = "Прямое соединение означает, что определенный трафик не будет перенаправлен через другой сервер."
"blockips" = "Заблокированные IP-адреса"
"blockdomains" = "Заблокированные домены"
"directips" = "Прямые IP-адреса"
"directdomains" = "Прямые домены"
"ipv4Routing" = "Правила IPv4"
"ipv4RoutingDesc" = "Эти параметры позволят клиентам маршрутизироваться к целевым доменам только через IPv4"
"warpRouting" = "Правила WARP"
"warpRoutingDesc" = " Эти опции будут направлять трафик в зависимости от конкретного пункта назначения через WARP."
"Template" = "Шаблон конфигурации Xray"
"TemplateDesc" = "На основе шаблона создаётся конфигурационный файл Xray."
"FreedomStrategy" = "Настройка стратегии протокола Freedom"
"FreedomStrategyDesc" = "Установка стратегии вывода сети в протоколе Freedom"
"RoutingStrategy" = "Настройка маршрутизации доменов"
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"outboundTestUrl" = "URL для теста исходящего"
"outboundTestUrlDesc" = "URL для проверки подключения исходящего"
"Torrent" = "Заблокировать BitTorrent"
"Inbounds" = "Входящие подключения"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
"Outbounds" = "Исходящие подключения"
"Balancers" = "Балансировщик"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера"
"Routings" = "Маршрутизация"
"RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все"
"logLevel" = "Уровень логов"
"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
"accessLog" = "Логи доступа"
"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключает логи доступа."
"errorLog" = "Логи ошибок"
"errorLogDesc" = "Путь к файлу логов ошибок. Специальное значение «none» отключает логи ошибок."
"dnsLog" = "Логи DNS"
"dnsLogDesc" = "Включить логи запросов DNS"
"maskAddress" = "Маскировка адреса"
"maskAddressDesc" = "При активации реальный IP-адрес заменяется на маскировочный в логах."
"statistics" = "Статистика"
"statsInboundUplink" = "Статистика входящего аплинка"
"statsInboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех входящих прокси."
"statsInboundDownlink" = "Статистика входящего даунлинка"
"statsInboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех входящих прокси."
"statsOutboundUplink" = "Статистика исходящего аплинка"
"statsOutboundUplinkDesc" = "Включает сбор статистики для исходящего трафика всех исходящих прокси."
"statsOutboundDownlink" = "Статистика исходящего даунлинка"
"statsOutboundDownlinkDesc" = "Включает сбор статистики для входящего трафика всех исходящих прокси."
[pages.xray.rules]
"first" = "Первый"
"last" = "Последний"
"up" = "Поднять вверх"
"down" = "Опустить вниз"
"source" = "Источник"
"dest" = "Пункт назначения"
"inbound" = "Входящее подключение"
"outbound" = "Исходящее подключение"
"balancer" = "Балансировщик"
"info" = "Информация"
"add" = "Создать правило"
"edit" = "Редактировать правило"
"useComma" = "Элементы, разделённые запятыми"
[pages.xray.outbound]
"addOutbound" = "Создать исходящее подключение"
"addReverse" = "Создать реверс-прокси"
"editOutbound" = "Изменить исходящее подключение"
"editReverse" = "Редактировать реверс-прокси"
"tag" = "Тег"
"tagDesc" = "Уникальный тег"
"address" = "Адрес"
"reverse" = "Реверс-прокси"
"domain" = "Домен"
"type" = "Тип"
"bridge" = "Мост"
"portal" = "Портал"
"link" = "Ссылка"
"intercon" = "Соединение"
"settings" = "Настройки"
"accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Статус исходящего подключения"
"sendThrough" = "Отправить через"
"test" = "Тест"
"testResult" = "Результат теста"
"testing" = "Тестирование соединения..."
"testSuccess" = "Тест успешен"
"testFailed" = "Тест не пройден"
"testError" = "Не удалось протестировать исходящее подключение"
[pages.xray.balancer]
"addBalancer" = "Создать балансировщик"
"editBalancer" = "Редактировать балансировщик"
"balancerStrategy" = "Стратегия"
"balancerSelectors" = "Селекторы"
"tag" = "Тег"
"tagDesc" = "Уникальный тег"
"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
[pages.xray.wireguard]
"secretKey" = "Секретный ключ"
"publicKey" = "Публичный ключ"
"allowedIPs" = "Разрешенные IP-адреса"
"endpoint" = "Конечная точка"
"psk" = "Общий ключ"
"domainStrategy" = "Стратегия домена"
[pages.xray.tun]
"nameDesc" = "Имя интерфейса TUN. Значение по умолчанию - 'xray0'"
"mtuDesc" = "Максимальная единица передачи. Максимальный размер пакетов данных. Значение по умолчанию - 1500"
"userLevel" = "Уровень пользователя"
"userLevelDesc" = "Все соединения, установленные через этот входящий поток, будут использовать этот уровень пользователя. Значение по умолчанию - 0"
[pages.xray.dns]
"enable" = "Включить DNS"
"enableDesc" = "Включить встроенный DNS-сервер"
"tag" = "Название тега DNS"
"tagDesc" = "Этот тег будет доступен как входящий тег в правилах маршрутизации."
"clientIp" = "IP клиента"
"clientIpDesc" = "Используется для уведомления сервера о указанном местоположении IP во время DNS-запросов"
"disableCache" = "Отключить кэш"
"disableCacheDesc" = "Отключает кэширование DNS"
"disableFallback" = "Отключить резервный DNS"
"disableFallbackDesc" = "Отключает резервные DNS-запросы"
"disableFallbackIfMatch" = "Отключить резервный DNS при совпадении"
"disableFallbackIfMatchDesc" = "Отключает резервные DNS-запросы при совпадении списка доменов DNS-сервера"
"enableParallelQuery" = "Включить параллельные запросы"
"enableParallelQueryDesc" = "Включить параллельные DNS-запросы к нескольким серверам для более быстрого разрешения"
"strategy" = "Стратегия запроса"
"strategyDesc" = "Общая стратегия разрешения доменных имен"
"add" = "Создать DNS"
"edit" = "Редактировать DNS"
"domains" = "Домены"
"expectIPs" = "Ожидаемые IP"
"unexpectIPs" = "Неожидаемые IP"
"useSystemHosts" = "Использовать системные Hosts"
"useSystemHostsDesc" = "Использовать файл hosts из установленной системы"
"usePreset" = "Использовать шаблон"
"dnsPresetTitle" = "Шаблоны DNS"
"dnsPresetFamily" = "Семейный"
[pages.xray.fakedns]
"add" = "Создать Fake DNS"
"edit" = "Редактировать Fake DNS"
"ipPool" = "Подсеть пула IP"
"poolSize" = "Размер пула"
[pages.settings.security]
"admin" = "Учетные данные администратора"
"twoFactor" = "Двухфакторная аутентификация"
"twoFactorEnable" = "Включить 2FA"
"twoFactorEnableDesc" = "Добавляет дополнительный уровень аутентификации для повышения безопасности."
"twoFactorModalSetTitle" = "Включить двухфакторную аутентификацию"
"twoFactorModalDeleteTitle" = "Отключить двухфакторную аутентификацию"
"twoFactorModalSteps" = "Для настройки двухфакторной аутентификации выполните несколько шагов:"
"twoFactorModalFirstStep" = "1. Отсканируйте этот QR-код в приложении для аутентификации или скопируйте токен рядом с QR-кодом и вставьте его в приложение"
"twoFactorModalSecondStep" = "2. Введите код из приложения"
"twoFactorModalRemoveStep" = "Введите код из приложения, чтобы отключить двухфакторную аутентификацию."
"twoFactorModalChangeCredentialsTitle" = "Изменить учетные данные"
"twoFactorModalChangeCredentialsStep" = "Введите код из приложения, чтобы изменить учетные данные администратора."
"twoFactorModalSetSuccess" = "Двухфакторная аутентификация была успешно установлена"
"twoFactorModalDeleteSuccess" = "Двухфакторная аутентификация была успешно удалена"
"twoFactorModalError" = "Неверный код"
[pages.settings.toasts]
"modifySettings" = "Настройки изменены"
"getSettings" = "Произошла ошибка при получении параметров."
"modifyUserError" = "Произошла ошибка при изменении учетных данных администратора."
"modifyUser" = "Вы успешно изменили учетные данные администратора."
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
"getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения"
"resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения"
[tgbot]
"keyboardClosed" = "❌ Клавиатура закрыта."
"noResult" = "❗ Нет результатов."
"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
"wentWrong" = "❌ Что-то пошло не так..."
"noIpRecord" = "❗ Нет записей об IP-адресе."
"noInbounds" = "❗ У вас не настроено ни одного входящего подключения."
"unlimited" = "♾ Безлимит"
"add" = "Добавить"
"month" = "Месяц"
"months" = "Месяцев"
"day" = "День"
"days" = "Дней"
"hours" = "Часов"
"minutes" = "Минуты"
"unknown" = "Неизвестно"
"inbounds" = "Входящие подключения"
"clients" = "Клиенты"
"offline" = "🔴 Офлайн"
"online" = "🟢 Онлайн"
[tgbot.commands]
"unknown" = "❗ Неизвестная команда"
"pleaseChoose" = "👇 Пожалуйста, выберите:\r\n"
"help" = "🤖 Добро пожаловать! Этот бот предназначен для предоставления вам данных с сервера и позволяет вносить изменения на него.\r\n\r\n"
"start" = "👋 Привет, <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>!\r\n"
"status" = "✅ Бот функционирует нормально."
"usage" = "❗ Пожалуйста, укажите email для поиска."
"getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Ядро Xray успешно перезапущено."
"restartFailed" = "❗ Ошибка при перезапуске Xray-core.\r\n\r\n<code>Ошибка: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core не запущен."
"startDesc" = "Показать главное меню"
"helpDesc" = "Справка по боту"
"statusDesc" = "Проверить статус бота"
"idDesc" = "Показать ваш Telegram ID"
[tgbot.messages]
"cpuThreshold" = "🔴 Загрузка процессора составляет {{ .Percent }}%, что превышает пороговое значение {{ .Threshold }}%"
"selectUserFailed" = "❌ Ошибка при выборе пользователя."
"userSaved" = "✅ Пользователь Telegram сохранен."
"loginSuccess" = "✅ Успешный вход в панель.\r\n"
"loginFailed" = "❗️ Ошибка входа в панель.\r\n"
"2faFailed" = "Ошибка 2FA"
"report" = "🕰 Запланированные отчеты: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата и время: {{ .DateTime }}\r\n"
"hostname" = "💻 Имя хоста: {{ .Hostname }}\r\n"
"version" = "🚀 Версия X-UI: {{ .Version }}\r\n"
"xrayVersion" = "📡 Версия Xray: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP-адреса:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Время работы сервера: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Нагрузка сервера: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 ОЗУ сервера: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Количество TCP-соединений: {{ .Count }}\r\n"
"udpCount" = "🔸 Количество UDP-соединений: {{ .Count }}\r\n"
"traffic" = "🚦 Трафик: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Состояние Xray: {{ .State }}\r\n"
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
"password" = "👤 Пароль: {{ .Password }}\r\n"
"time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .Time }}\r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
"active" = "💡 Активен: {{ .Enable }}\r\n"
"enabled" = "🚨 Активен: {{ .Enable }}\r\n"
"online" = "🌐 Статус соединения: {{ .Status }}\r\n"
"lastOnline" = "🔙 Был(а) в сети: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n"
"download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n"
"total" = "📊 Всего: ↑↓{{ .UpDown }} из {{ .Total }}\r\n"
"TGUser" = "👤 Telegram User ID: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Исчерпаны {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Количество исчерпанных {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Клиентов онлайн: {{ .Count }}\r\n"
"disabled" = "🛑 Отключено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Клиенты, у которых скоро исчерпание: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Время резервного копирования: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Обновлено: {{ .Time }}\r\n\r\n"
"yes" = "✅ Да"
"no" = "❌ Нет"
"received_id" = "🔑📥 ID обновлён."
"received_password" = "🔑📥 Пароль обновлён."
"received_email" = "📧📥 Email обновлен."
"received_comment" = "💬📥 Комментарий обновлён."
"id_prompt" = "🔑 Стандартный ID: {{ .ClientId }}\n\nВведите ваш ID."
"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль."
"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email."
"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий."
"inbound_client_data_id" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
"inbound_client_data_pass" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄"
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
"using_default_value" = "Используется значение по умолчанию👌"
"incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
"AreYouSure" = "Вы уверены? 🤔"
"SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно"
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Сброс трафика завершён для всех клиентов."
[tgbot.buttons]
"closeKeyboard" = "❌ Закрыть клавиатуру"
"cancel" = "❌ Отмена"
"cancelReset" = "❌ Отменить сброс"
"cancelIpLimit" = "❌ Отменить лимит IP"
"confirmResetTraffic" = "✅ Подтвердить сброс трафика?"
"confirmClearIps" = "✅ Подтвердить очистку IP?"
"confirmRemoveTGUser" = "✅ Подтвердить удаление пользователя Telegram?"
"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
"dbBackup" = "📂 Бэкап БД"
"serverUsage" = "💻 Состояние сервера"
"getInbounds" = "🔌 Входящие подключения"
"depleteSoon" = "⚠️ Скоро конец"
"clientUsage" = "Статистика клиента"
"onlines" = "🟢 Онлайн"
"commands" = "🖱️ Команды"
"refresh" = "🔄 Обновить"
"clearIPs" = "❌ Очистить IP"
"removeTGUser" = "❌ Удалить пользователя Telegram"
"selectTGUser" = "👤 Выбрать пользователя Telegram"
"selectOneTGUser" = "👤 Выберите пользователя Telegram:"
"resetTraffic" = "📈 Сбросить трафик"
"resetExpire" = "📅 Изменить дату окончания"
"ipLog" = "🔢 Лог IP"
"ipLimit" = "🔢 Лимит IP"
"setTGUser" = "👤 Установить пользователя Telegram"
"toggle" = "🔘 Вкл./Выкл."
"custom" = "🔢 Свой"
"confirmNumber" = "✅ Подтвердить: {{ .Num }}"
"confirmNumberAdd" = "✅ Подтвердить добавление: {{ .Num }}"
"limitTraffic" = "🚧 Лимит трафика"
"getBanLogs" = "📄 Лог банов"
"allClients" = "👥 Все клиенты"
"addClient" = " Новый клиент"
"submitDisable" = "Добавить отключенным ☑️"
"submitEnable" = "Добавить включенным ✅"
"use_default" = "🏷️ Использовать по умолчанию"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Пароль"
"change_email" = "⚙️📧 Email"
"change_comment" = "⚙️💬 Комментарий"
"ResetAllTraffics" = "Сбросить весь трафик"
"SortedTrafficUsageReport" = "Отсортированный отчет об использовании трафика"
[tgbot.answers]
"successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить входящие подключения."
"getClientsFailed" = "❌ Не удалось получить клиентов."
"canceled" = "❌ {{ .Email }}: Операция отменена."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреса успешно обновлены."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Пользователь Telegram клиента успешно обновлен."
"resetTrafficSuccess" = "✅ {{ .Email }}: Трафик успешно сброшен."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Лимит трафика успешно установлен."
"expireResetSuccess" = "✅ {{ .Email }}: Срок действия успешно сброшен."
"resetIpSuccess" = "✅ {{ .Email }}: Лимит IP ({{ .Count }}) успешно сохранен."
"clearIpSuccess" = "✅ {{ .Email }}: IP-адреса успешно очищены."
"getIpLog" = "✅ {{ .Email }}: Получен лог IP."
"getUserInfo" = "✅ {{ .Email }}: Получена информация о пользователе Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Пользователь Telegram успешно удален."
"enableSuccess" = "✅ {{ .Email }}: Включено успешно."
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}"
"chooseInbound" = "Выберите входящее подключение"

View file

@ -1,795 +0,0 @@
"username" = "Kullanıcı Adı"
"password" = "Şifre"
"login" = "Giriş Yap"
"confirm" = "Onayla"
"cancel" = "İptal"
"close" = "Kapat"
"create" = "Oluştur"
"update" = "Güncelle"
"copy" = "Kopyala"
"copied" = "Kopyalandı"
"download" = "İndir"
"remark" = "Açıklama"
"enable" = "Etkin"
"protocol" = "Protokol"
"search" = "Ara"
"filter" = "Filtrele"
"loading" = "Yükleniyor..."
"second" = "Saniye"
"minute" = "Dakika"
"hour" = "Saat"
"day" = "Gün"
"check" = "Kontrol Et"
"indefinite" = "Belirsiz"
"unlimited" = "Sınırsız"
"none" = "Hiçbiri"
"qrCode" = "QR Kod"
"info" = "Daha Fazla Bilgi"
"edit" = "Düzenle"
"delete" = "Sil"
"reset" = "Sıfırla"
"noData" = "Veri yok."
"copySuccess" = "Başarıyla Kopyalandı"
"sure" = "Emin misiniz"
"encryption" = "Şifreleme"
"useIPv4ForHost" = "Ana bilgisayar için IPv4 kullan"
"transmission" = "İletim"
"host" = "Sunucu"
"path" = "Yol"
"camouflage" = "Kandırma"
"status" = "Durum"
"enabled" = "Etkin"
"disabled" = "Devre Dışı"
"depleted" = "Bitti"
"depletingSoon" = "Bitmek Üzere"
"offline" = "Çevrimdışı"
"online" = "Çevrimiçi"
"domainName" = "Alan Adı"
"monitor" = "Dinleme IP"
"certificate" = "Dijital Sertifika"
"fail" = "Başarısız"
"comment" = "Yorum"
"success" = "Başarılı"
"lastOnline" = "Son çevrimiçi"
"getVersion" = "Sürümü Al"
"install" = "Yükle"
"clients" = "Müşteriler"
"usage" = "Kullanım"
"twoFactorCode" = "Kod"
"remained" = "Kalan"
"security" = "Güvenlik"
"secAlertTitle" = "Güvenlik Uyarısı"
"secAlertSsl" = "Bu bağlantı güvenli değil. Verilerin korunması için TLS etkinleştirilene kadar hassas bilgiler girmekten kaçının."
"secAlertConf" = "Bazı ayarlar saldırılara açıktır. Olası ihlalleri önlemek için güvenlik protokollerini güçlendirmeniz önerilir."
"secAlertSSL" = "Panelde güvenli bağlantı yok. Verilerin korunması için TLS sertifikası yükleyin."
"secAlertPanelPort" = "Panel varsayılan portu savunmasız. Rastgele veya belirli bir port yapılandırın."
"secAlertPanelURI" = "Panel varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın."
"secAlertSubURI" = "Abonelik varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın."
"secAlertSubJsonURI" = "Abonelik JSON varsayılan URI yolu güvensiz. Karmaşık bir URI yolu yapılandırın."
"emptyDnsDesc" = "Eklenmiş DNS sunucusu yok."
"emptyFakeDnsDesc" = "Eklenmiş Fake DNS sunucusu yok."
"emptyBalancersDesc" = "Eklenmiş dengeleyici yok."
"emptyReverseDesc" = "Eklenmiş ters proxy yok."
"somethingWentWrong" = "Bir şeyler yanlış gitti"
[subscription]
"title" = "Abonelik Bilgisi"
"subId" = "Abonelik Kimliği"
"status" = "Durum"
"downloaded" = "İndirilen"
"uploaded" = "Yüklenen"
"expiry" = "Son Kullanma"
"totalQuota" = "Toplam Kota"
"individualLinks" = "Bireysel Bağlantılar"
"active" = "Aktif"
"inactive" = "Pasif"
"unlimited" = "Sınırsız"
"noExpiry" = "Süresiz"
[menu]
"theme" = "Tema"
"dark" = "Koyu"
"ultraDark" = "Ultra Koyu"
"dashboard" = "Genel Bakış"
"inbounds" = "Gelenler"
"settings" = "Panel Ayarları"
"xray" = "Xray Yapılandırmaları"
"logout" = ıkış Yap"
"link" = "Yönet"
[pages.login]
"hello" = "Merhaba"
"title" = "Hoş Geldiniz"
"loginAgain" = "Oturum süreniz doldu, lütfen tekrar giriş yapın"
[pages.login.toasts]
"invalidFormData" = "Girdi verisi formatı geçersiz."
"emptyUsername" = "Kullanıcı adı gerekli"
"emptyPassword" = "Şifre gerekli"
"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu."
"successLogin" = "Hesabınıza başarıyla giriş yaptınız."
"successRegister" = "Kayıt başarılı, lütfen giriş yapın."
"userExists" = "Kullanıcı adı zaten mevcut"
"errorRegister" = "Kayıt başarısız"
[pages.index]
"title" = "Genel Bakış"
"cpu" = "İşlemci"
"logicalProcessors" = "Mantıksal işlemciler"
"frequency" = "Frekans"
"swap" = "Takas"
"storage" = "Depolama"
"memory" = "RAM"
"threads" = "İş parçacıkları"
"xrayStatus" = "Xray"
"stopXray" = "Durdur"
"restartXray" = "Yeniden Başlat"
"xraySwitch" = "Sürüm"
"xraySwitchClick" = "Geçiş yapmak istediğiniz sürümü seçin."
"xraySwitchClickDesk" = "Dikkatli seçin, eski sürümler mevcut yapılandırmalarla uyumlu olmayabilir."
"xrayStatusUnknown" = "Bilinmiyor"
"xrayStatusRunning" = "Çalışıyor"
"xrayStatusStop" = "Durduruldu"
"xrayStatusError" = "Hata"
"xrayErrorPopoverTitle" = "Xray çalıştırılırken bir hata oluştu"
"operationHours" = "Çalışma Süresi"
"systemLoad" = "Sistem Yükü"
"systemLoadDesc" = "Geçmiş 1, 5 ve 15 dakika için sistem yük ortalaması"
"connectionCount" = "Bağlantı İstatistikleri"
"ipAddresses" = "IP adresleri"
"toggleIpVisibility" = "IP görünürlüğünü değiştir"
"overallSpeed" = "Genel hız"
"upload" = "Yükleme"
"download" = "İndirme"
"totalData" = "Toplam veri"
"sent" = "Gönderilen"
"received" = "Alınan"
"documentation" = "Dokümantasyon"
"xraySwitchVersionDialog" = "Xray sürümünü gerçekten değiştirmek istiyor musunuz?"
"xraySwitchVersionDialogDesc" = "Bu işlem Xray sürümünü #version# olarak değiştirecektir."
"xraySwitchVersionPopover" = "Xray başarıyla güncellendi"
"geofileUpdateDialog" = "Geofile'ı gerçekten güncellemek istiyor musunuz?"
"geofileUpdateDialogDesc" = "Bu işlem #filename# dosyasını güncelleyecektir."
"geofilesUpdateDialogDesc" = "Bu, tüm dosyaları güncelleyecektir."
"geofilesUpdateAll" = "Tümünü güncelle"
"geofileUpdatePopover" = "Geofile başarıyla güncellendi"
"dontRefresh" = "Kurulum devam ediyor, lütfen bu sayfayı yenilemeyin"
"logs" = "Günlükler"
"config" = "Yapılandırma"
"backup" = "Yedek"
"backupTitle" = "Veritabanı Yedekleme & Geri Yükleme"
"exportDatabase" = "Yedekle"
"exportDatabaseDesc" = "Mevcut veritabanınızın yedeğini içeren bir .db dosyasını cihazınıza indirmek için tıklayın."
"importDatabase" = "Geri Yükle"
"importDatabaseDesc" = "Cihazınızdan bir .db dosyası seçip yükleyerek veritabanınızı yedekten geri yüklemek için tıklayın."
"importDatabaseSuccess" = "Veritabanı başarıyla içe aktarıldı"
"importDatabaseError" = "Veritabanı içe aktarılırken bir hata oluştu"
"readDatabaseError" = "Veritabanı okunurken bir hata oluştu"
"getDatabaseError" = "Veritabanı alınırken bir hata oluştu"
"getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu"
[pages.inbounds]
"allTimeTraffic" = "Toplam Trafik"
"allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı"
"title" = "Gelenler"
"totalDownUp" = "Toplam Gönderilen/Alınan"
"totalUsage" = "Toplam Kullanım"
"inboundCount" = "Toplam Gelen"
"operate" = "Menü"
"enable" = "Etkin"
"remark" = "Açıklama"
"protocol" = "Protokol"
"port" = "Port"
"portMap" = "Port Atama"
"traffic" = "Trafik"
"details" = "Detaylar"
"transportConfig" = "Taşıma"
"expireDate" = "Süre"
"createdAt" = "Oluşturuldu"
"updatedAt" = "Güncellendi"
"resetTraffic" = "Trafiği Sıfırla"
"addInbound" = "Gelen Ekle"
"generalActions" = "Genel Eylemler"
"autoRefresh" = "Otomatik yenileme"
"autoRefreshInterval" = "Aralık"
"modifyInbound" = "Geleni Düzenle"
"deleteInbound" = "Geleni Sil"
"deleteInboundContent" = "Geleni silmek istediğinizden emin misiniz?"
"deleteClient" = "Müşteriyi Sil"
"deleteClientContent" = "Müşteriyi silmek istediğinizden emin misiniz?"
"resetTrafficContent" = "Trafiği sıfırlamak istediğinizden emin misiniz?"
"copyLink" = "URL'yi Kopyala"
"address" = "Adres"
"network" = "Ağ"
"destinationPort" = "Hedef Port"
"targetAddress" = "Hedef Adres"
"monitorDesc" = "Tüm IP'leri dinlemek için boş bırakın"
"meansNoLimit" = "= Sınırsız. (birim: GB)"
"totalFlow" = "Toplam Akış"
"leaveBlankToNeverExpire" = "Hiçbir zaman sona ermemesi için boş bırakın"
"noRecommendKeepDefault" = "Varsayılanı korumanız önerilir"
"certificatePath" = "Dosya Yolu"
"certificateContent" = "Dosya İçeriği"
"publicKey" = "Genel Anahtar"
"privatekey" = "Özel Anahtar"
"clickOnQRcode" = "Kopyalamak için QR Kodu Tıklayın"
"client" = "Müşteri"
"export" = "Tüm URL'leri Dışa Aktar"
"clone" = "Klonla"
"cloneInbound" = "Klonla"
"cloneInboundContent" = "Bu gelenin tüm ayarları, Port, Dinleme IP ve Müşteriler hariç, klona uygulanacaktır."
"cloneInboundOk" = "Klonla"
"resetAllTraffic" = "Tüm Gelen Trafiğini Sıfırla"
"resetAllTrafficTitle" = "Tüm Gelen Trafiğini Sıfırla"
"resetAllTrafficContent" = "Tüm gelenlerin trafiğini sıfırlamak istediğinizden emin misiniz?"
"resetInboundClientTraffics" = "Müşteri Trafiklerini Sıfırla"
"resetInboundClientTrafficTitle" = "Müşteri Trafiklerini Sıfırla"
"resetInboundClientTrafficContent" = "Bu gelenin müşterilerinin trafiğini sıfırlamak istediğinizden emin misiniz?"
"resetAllClientTraffics" = "Tüm Müşteri Trafiklerini Sıfırla"
"resetAllClientTrafficTitle" = "Tüm Müşteri Trafiklerini Sıfırla"
"resetAllClientTrafficContent" = "Tüm müşterilerin trafiğini sıfırlamak istediğinizden emin misiniz?"
"delDepletedClients" = "Bitmiş Müşterileri Sil"
"delDepletedClientsTitle" = "Bitmiş Müşterileri Sil"
"delDepletedClientsContent" = "Tüm bitmiş müşterileri silmek istediğinizden emin misiniz?"
"email" = "E-posta"
"emailDesc" = "Lütfen benzersiz bir e-posta adresi sağlayın."
"IPLimit" = "IP Limiti"
"IPLimitDesc" = "Sayının aşılması durumunda gelen devre dışı bırakılır. (0 = devre dışı)"
"IPLimitlog" = "IP Günlüğü"
"IPLimitlogDesc" = "IP geçmiş günlüğü. (devre dışı bırakıldıktan sonra gelini etkinleştirmek için günlüğü temizleyin)"
"IPLimitlogclear" = "Günlüğü Temizle"
"setDefaultCert" = "Panelden Sertifikayı Ayarla"
"telegramDesc" = "Lütfen Telegram Sohbet Kimliği sağlayın. (botta '/id' komutunu kullanın) veya (@userinfobot)"
"subscriptionDesc" = "Abonelik URL'inizi bulmak için 'Detaylar'a gidin. Ayrıca, aynı adı birden fazla müşteri için kullanabilirsiniz."
"info" = "Bilgi"
"same" = "Aynı"
"inboundData" = "Gelenin Verileri"
"exportInbound" = "Geleni Dışa Aktar"
"import" = "İçe Aktar"
"importInbound" = "Bir Gelen İçe Aktar"
"periodicTrafficResetTitle" = "Trafik Sıfırlama"
"periodicTrafficResetDesc" = "Belirtilen aralıklarla trafik sayacını otomatik olarak sıfırla"
"lastReset" = "Son Sıfırlama"
[pages.client]
"add" = "Müşteri Ekle"
"edit" = "Müşteriyi Düzenle"
"submitAdd" = "Müşteri Ekle"
"submitEdit" = "Değişiklikleri Kaydet"
"clientCount" = "Müşteri Sayısı"
"bulk" = "Toplu Ekle"
"method" = "Yöntem"
"first" = "İlk"
"last" = "Son"
"prefix" = "Önek"
"postfix" = "Sonek"
"delayedStart" = "İlk Kullanımdan Sonra Başlat"
"expireDays" = "Süre"
"days" = "Gün"
"renew" = "Otomatik Yenile"
"renewDesc" = "Süresi dolduktan sonra otomatik yenileme. (0 = devre dışı)(birim: gün)"
[pages.inbounds.periodicTrafficReset]
"never" = "Asla"
"daily" = "Günlük"
"weekly" = "Haftalık"
"monthly" = "Aylık"
[pages.inbounds.toasts]
"obtain" = "Elde Et"
"updateSuccess" = "Güncelleme başarılı oldu"
"logCleanSuccess" = "Günlük temizlendi"
"inboundsUpdateSuccess" = "Gelen bağlantılar başarıyla güncellendi"
"inboundUpdateSuccess" = "Gelen bağlantı başarıyla güncellendi"
"inboundCreateSuccess" = "Gelen bağlantı başarıyla oluşturuldu"
"inboundDeleteSuccess" = "Gelen bağlantı başarıyla silindi"
"inboundClientAddSuccess" = "Gelen bağlantı istemci(leri) eklendi"
"inboundClientDeleteSuccess" = "Gelen bağlantı istemcisi silindi"
"inboundClientUpdateSuccess" = "Gelen bağlantı istemcisi güncellendi"
"delDepletedClientsSuccess" = "Tüm tükenmiş istemciler silindi"
"resetAllClientTrafficSuccess" = "İstemcinin tüm trafiği sıfırlandı"
"resetAllTrafficSuccess" = "Tüm trafik sıfırlandı"
"resetInboundClientTrafficSuccess" = "Trafik sıfırlandı"
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
"getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu."
[pages.inbounds.stream.general]
"request" = "İstek"
"response" = "Yanıt"
"name" = "Ad"
"value" = "Değer"
[pages.inbounds.stream.tcp]
"version" = "Sürüm"
"method" = "Yöntem"
"path" = "Yol"
"status" = "Durum"
"statusDescription" = "Durum Açıklaması"
"requestHeader" = "İstek Başlığı"
"responseHeader" = "Yanıt Başlığı"
[pages.settings]
"title" = "Panel Ayarları"
"save" = "Kaydet"
"infoDesc" = "Burada yapılan her değişikliğin kaydedilmesi gerekir. Değişikliklerin uygulanması için paneli yeniden başlatın."
"restartPanel" = "Paneli Yeniden Başlat"
"restartPanelDesc" = "Paneli yeniden başlatmak istediğinizden emin misiniz? Yeniden başlattıktan sonra panele erişemezseniz, sunucudaki panel günlük bilgilerini görüntüleyin."
"restartPanelSuccess" = "Panel başarıyla yeniden başlatıldı"
"actions" = "Eylemler"
"resetDefaultConfig" = "Varsayılana Sıfırla"
"panelSettings" = "Genel"
"securitySettings" = "Kimlik Doğrulama"
"TGBotSettings" = "Telegram Bot"
"panelListeningIP" = "Dinleme IP"
"panelListeningIPDesc" = "Web paneli için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"panelListeningDomain" = "Dinleme Alan Adı"
"panelListeningDomainDesc" = "Web paneli için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)"
"panelPort" = "Dinleme Portu"
"panelPortDesc" = "Web paneli için port numarası. (kullanılmayan bir port olmalıdır)"
"publicKeyPath" = "Genel Anahtar Yolu"
"publicKeyPathDesc" = "Web paneli için genel anahtar dosya yolu. ('/' ile başlar)"
"privateKeyPath" = "Özel Anahtar Yolu"
"privateKeyPathDesc" = "Web paneli için özel anahtar dosya yolu. ('/' ile başlar)"
"panelUrlPath" = "URI Yolu"
"panelUrlPathDesc" = "Web paneli için URI yolu. ('/' ile başlar ve '/' ile biter)"
"pageSize" = "Sayfa Boyutu"
"pageSizeDesc" = "Gelenler tablosu için sayfa boyutunu belirleyin. (0 = devre dışı)"
"remarkModel" = "Açıklama Modeli & Ayırma Karakteri"
"datepicker" = "Takvim Türü"
"datepickerPlaceholder" = "Tarih Seçin"
"datepickerDescription" = "Planlanmış görevler bu takvime göre çalışacaktır."
"sampleRemark" = "Örnek Açıklama"
"oldUsername" = "Mevcut Kullanıcı Adı"
"currentPassword" = "Mevcut Şifre"
"newUsername" = "Yeni Kullanıcı Adı"
"newPassword" = "Yeni Şifre"
"telegramBotEnable" = "Telegram Botunu Etkinleştir"
"telegramBotEnableDesc" = "Telegram botunu etkinleştirir."
"telegramToken" = "Telegram Token"
"telegramTokenDesc" = "'@BotFather'dan alınan Telegram bot token."
"telegramProxy" = "SOCKS Proxy"
"telegramProxyDesc" = "Telegram'a bağlanmak için SOCKS5 proxy'sini etkinleştirir. (ayarları kılavuzda belirtilen şekilde ayarlayın)"
"telegramAPIServer" = "Telegram API Server"
"telegramAPIServerDesc" = "Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın."
"telegramChatId" = "Yönetici Sohbet Kimliği"
"telegramChatIdDesc" = "Telegram Yönetici Sohbet Kimliği(leri). (virgülle ayrılmış)(buradan alın @userinfobot) veya (botta '/id' komutunu kullanın)"
"telegramNotifyTime" = "Bildirim Zamanı"
"telegramNotifyTimeDesc" = "Periyodik raporlar için ayarlanan Telegram bot bildirim zamanı. (crontab zaman formatını kullanın)"
"tgNotifyBackup" = "Veritabanı Yedeği"
"tgNotifyBackupDesc" = "Bir rapor ile birlikte veritabanı yedek dosyasını gönder."
"tgNotifyLogin" = "Giriş Bildirimi"
"tgNotifyLoginDesc" = "Birisi web panelinize giriş yapmaya çalıştığında kullanıcı adı, IP adresi ve zaman hakkında bildirim alın."
"sessionMaxAge" = "Oturum Süresi"
"sessionMaxAgeDesc" = "Giriş yaptıktan sonra oturum süresi. (birim: dakika)"
"expireTimeDiff" = "Son Kullanma Tarihi Bildirimi"
"expireTimeDiffDesc" = "Bu eşik seviyesine ulaşıldığında son kullanma tarihi hakkında bildirim alın. (birim: gün)"
"trafficDiff" = "Trafik Sınırı Bildirimi"
"trafficDiffDesc" = "Bu eşik seviyesine ulaşıldığında trafik sınırı hakkında bildirim alın. (birim: GB)"
"tgNotifyCpu" = "CPU Yükü Bildirimi"
"tgNotifyCpuDesc" = "CPU yükü bu eşik seviyesini aşarsa bildirim alın. (birim: %)"
"timeZone" = "Saat Dilimi"
"timeZoneDesc" = "Planlanmış görevler bu saat dilimine göre çalışacaktır."
"subSettings" = "Abonelik"
"subEnable" = "Abonelik Hizmetini Etkinleştir"
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
"subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak."
"subTitle" = "Abonelik Başlığı"
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
"subSupportUrl" = "Destek URL'si"
"subSupportUrlDesc" = "VPN istemcisinde gösterilen teknik destek bağlantısı"
"subProfileUrl" = "Profil URL'si"
"subProfileUrlDesc" = "VPN istemcisinde görüntülenen web sitenize giden bağlantı"
"subAnnounce" = "Duyuru"
"subAnnounceDesc" = "VPN istemcisinde görüntülenen duyuru metni"
"subEnableRouting" = "Yönlendirmeyi etkinleştir"
"subEnableRoutingDesc" = "VPN istemcisinde yönlendirmeyi etkinleştirmek için genel ayar. (Yalnızca Happ için)"
"subRoutingRules" = "Yönlendirme kuralları"
"subRoutingRulesDesc" = "VPN istemcisi için genel yönlendirme kuralları. (Yalnızca Happ için)"
"subListen" = "Dinleme IP"
"subListenDesc" = "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)"
"subPort" = "Dinleme Portu"
"subPortDesc" = "Abonelik hizmeti için port numarası. (kullanılmayan bir port olmalıdır)"
"subCertPath" = "Genel Anahtar Yolu"
"subCertPathDesc" = "Abonelik hizmeti için genel anahtar dosya yolu. ('/' ile başlar)"
"subKeyPath" = "Özel Anahtar Yolu"
"subKeyPathDesc" = "Abonelik hizmeti için özel anahtar dosya yolu. ('/' ile başlar)"
"subPath" = "URI Yolu"
"subPathDesc" = "Abonelik hizmeti için URI yolu. ('/' ile başlar ve '/' ile biter)"
"subDomain" = "Dinleme Alan Adı"
"subDomainDesc" = "Abonelik hizmeti için alan adı. (tüm alan adlarını ve IP'leri dinlemek için boş bırakın)"
"subUpdates" = "Güncelleme Aralıkları"
"subUpdatesDesc" = "Müşteri uygulamalarındaki abonelik URL'sinin güncelleme aralıkları. (birim: saat)"
"subEncrypt" = "Şifrele"
"subEncryptDesc" = "Abonelik hizmetinin döndürülen içeriği Base64 ile şifrelenir."
"subShowInfo" = "Kullanım Bilgisini Göster"
"subShowInfoDesc" = "Kalan trafik ve tarih müşteri uygulamalarında görüntülenir."
"subURI" = "Ters Proxy URI"
"subURIDesc" = "Proxy arkasında kullanılacak abonelik URL'sinin URI yolu."
"externalTrafficInformEnable" = "Harici Trafik Bilgisi"
"externalTrafficInformEnableDesc" = "Her trafik güncellemesinde harici API'yi bilgilendirin."
"externalTrafficInformURI" = "Harici Trafik Bilgisi URI'si"
"externalTrafficInformURIDesc" = "Trafik güncellemeleri bu URI'ye gönderildi."
"fragment" = "Parçalama"
"fragmentDesc" = "TLS merhaba paketinin parçalanmasını etkinleştir."
"fragmentSett" = "Parçalama Ayarları"
"noisesDesc" = "Noises'i Etkinleştir."
"noisesSett" = "Noises Ayarları"
"mux" = "Mux"
"muxDesc" = "Kurulmuş bir veri akışında birden çok bağımsız veri akışını iletir."
"muxSett" = "Mux Ayarları"
"direct" = "Doğrudan Bağlantı"
"directDesc" = "Belirli bir ülkenin alan adları veya IP aralıkları ile doğrudan bağlantı kurar."
"notifications" = "Bildirimler"
"certs" = "Sertifikalar"
"externalTraffic" = "Harici Trafik"
"dateAndTime" = "Tarih ve Saat"
"proxyAndServer" = "Proxy ve Sunucu"
"intervals" = "Aralıklar"
"information" = "Bilgi"
"language" = "Dil"
"telegramBotLanguage" = "Telegram Bot Dili"
[pages.xray]
"title" = "Xray Yapılandırmaları"
"save" = "Kaydet"
"restart" = "Xray'i Yeniden Başlat"
"restartSuccess" = "Xray başarıyla yeniden başlatıldı"
"stopSuccess" = "Xray başarıyla durduruldu"
"restartError" = "Xray yeniden başlatılırken bir hata oluştu."
"stopError" = "Xray durdurulurken bir hata oluştu."
"basicTemplate" = "Temeller"
"advancedTemplate" = "Gelişmiş"
"generalConfigs" = "Genel"
"generalConfigsDesc" = "Bu seçenekler genel ayarlamaları belirler."
"logConfigs" = "Günlük"
"logConfigsDesc" = "Günlükler sunucunuzun verimliliğini etkileyebilir. Yalnızca ihtiyaç durumunda akıllıca etkinleştirmeniz önerilir"
"blockConfigsDesc" = "Bu seçenekler belirli istek protokolleri ve web siteleri temelinde trafiği engeller."
"basicRouting" = "Temel Yönlendirme"
"blockConnectionsConfigsDesc" = "Bu seçenekler belirli bir istenen ülkeye göre trafiği engelleyecektir."
"directConnectionsConfigsDesc" = "Doğrudan bağlantı, belirli bir trafiğin başka bir sunucu üzerinden yönlendirilmediğini sağlar."
"blockips" = "IP'leri Engelle"
"blockdomains" = "Alan Adlarını Engelle"
"directips" = "Doğrudan IP'ler"
"directdomains" = "Doğrudan Alan Adları"
"ipv4Routing" = "IPv4 Yönlendirme"
"ipv4RoutingDesc" = "Bu seçenekler belirli bir varış yerine IPv4 üzerinden trafiği yönlendirir."
"warpRouting" = "WARP Yönlendirme"
"warpRoutingDesc" = "Bu seçenekler belirli bir varış yerine WARP üzerinden trafiği yönlendirir."
"Template" = "Gelişmiş Xray Yapılandırma Şablonu"
"TemplateDesc" = "Nihai Xray yapılandırma dosyası bu şablona göre oluşturulacaktır."
"FreedomStrategy" = "Freedom Protokol Stratejisi"
"FreedomStrategyDesc" = "Freedom Protokolünde ağın çıkış stratejisini ayarlayın."
"RoutingStrategy" = "Genel Yönlendirme Stratejisi"
"RoutingStrategyDesc" = "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın."
"outboundTestUrl" = "Outbound test URL"
"outboundTestUrlDesc" = "Outbound bağlantı testinde kullanılan URL"
"Torrent" = "BitTorrent Protokolünü Engelle"
"Inbounds" = "Gelenler"
"InboundsDesc" = "Belirli müşterileri kabul eder."
"Outbounds" = "Gidenler"
"Balancers" = "Dengeler"
"OutboundsDesc" = "Giden trafiğin yolunu ayarlayın."
"Routings" = "Yönlendirme Kuralları"
"RoutingsDesc" = "Her kuralın önceliği önemlidir!"
"completeTemplate" = "Tümü"
"logLevel" = "Günlük Seviyesi"
"logLevelDesc" = "Hata günlükleri için günlük seviyesi, kaydedilmesi gereken bilgileri belirtir."
"accessLog" = "Erişim Günlüğü"
"accessLogDesc" = "Erişim günlüğü için dosya yolu. 'none' özel değeri erişim günlüklerini devre dışı bırakır"
"errorLog" = "Hata Günlüğü"
"errorLogDesc" = "Hata günlüğü için dosya yolu. 'none' özel değeri hata günlüklerini devre dışı bırakır"
"dnsLog" = "DNS Günlüğü"
"dnsLogDesc" = "DNS sorgu günlüklerini etkinleştirin"
"maskAddress" = "Adres Maskesi"
"maskAddressDesc" = "IP adresi maskesi, etkinleştirildiğinde, günlükte görünen IP adresini otomatik olarak değiştirecektir."
"statistics" = "İstatistikler"
"statsInboundUplink" = "Gelen Yükleme İstatistikleri"
"statsInboundUplinkDesc" = "Tüm gelen proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir."
"statsInboundDownlink" = "Gelen İndirme İstatistikleri"
"statsInboundDownlinkDesc" = "Tüm gelen proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir."
"statsOutboundUplink" = "Giden Yükleme İstatistikleri"
"statsOutboundUplinkDesc" = "Tüm giden proxy'lerin yükleme trafiği için istatistik toplamayı etkinleştirir."
"statsOutboundDownlink" = "Giden İndirme İstatistikleri"
"statsOutboundDownlinkDesc" = "Tüm giden proxy'lerin indirme trafiği için istatistik toplamayı etkinleştirir."
[pages.xray.rules]
"first" = "İlk"
"last" = "Son"
"up" = "Yukarı"
"down" = "Aşağı"
"source" = "Kaynak"
"dest" = "Hedef"
"inbound" = "Gelen"
"outbound" = "Giden"
"balancer" = "Dengeler"
"info" = "Bilgi"
"add" = "Kural Ekle"
"edit" = "Kuralı Düzenle"
"useComma" = "Virgülle ayrılmış öğeler"
[pages.xray.outbound]
"addOutbound" = "Giden Ekle"
"addReverse" = "Ters Ekle"
"editOutbound" = "Gideni Düzenle"
"editReverse" = "Tersi Düzenle"
"tag" = "Etiket"
"tagDesc" = "Benzersiz Etiket"
"address" = "Adres"
"reverse" = "Ters"
"domain" = "Alan Adı"
"type" = "Tür"
"bridge" = "Köprü"
"portal" = "Portal"
"link" = "Bağlantı"
"intercon" = "Bağlantı"
"settings" = "Ayarlar"
"accountInfo" = "Hesap Bilgileri"
"outboundStatus" = "Giden Durumu"
"sendThrough" = "Üzerinden Gönder"
"test" = "Test"
"testResult" = "Test Sonucu"
"testing" = "Bağlantı test ediliyor..."
"testSuccess" = "Test başarılı"
"testFailed" = "Test başarısız"
"testError" = "Giden test edilemedi"
[pages.xray.balancer]
"addBalancer" = "Dengeleyici Ekle"
"editBalancer" = "Dengeleyiciyi Düzenle"
"balancerStrategy" = "Strateji"
"balancerSelectors" = "Seçiciler"
"tag" = "Etiket"
"tagDesc" = "Benzersiz Etiket"
"balancerDesc" = "Dengeleyici Etiketi ve Giden Etiketi aynı anda kullanılamaz. Aynı anda kullanıldığında yalnızca giden etiketi çalışır."
[pages.xray.wireguard]
"secretKey" = "Gizli Anahtar"
"publicKey" = "Genel Anahtar"
"allowedIPs" = "İzin Verilen IP'ler"
"endpoint" = "Uç Nokta"
"psk" = "Ön Paylaşılan Anahtar"
"domainStrategy" = "Alan Adı Stratejisi"
[pages.xray.tun]
"nameDesc" = "TUN arabiriminin adı. Varsayılan değer 'xray0'dir"
"mtuDesc" = "Maksimum İletim Birimi. Veri paketlerinin maksimum boyutu. Varsayılan değer 1500'dür"
"userLevel" = "Kullanıcı Seviyesi"
"userLevelDesc" = "Bu giriş yoluyla yapılan tüm bağlantılar bu kullanıcı seviyesini kullanacaktır. Varsayılan değer 0'dır"
[pages.xray.dns]
"enable" = "DNS'yi Etkinleştir"
"enableDesc" = "Dahili DNS sunucusunu etkinleştir"
"tag" = "DNS Gelen Etiketi"
"tagDesc" = "Bu etiket, yönlendirme kurallarında Gelen etiketi olarak kullanılabilir."
"clientIp" = "İstemci IP"
"clientIpDesc" = "DNS sorguları sırasında belirtilen IP konumunu sunucuya bildirmek için kullanılır"
"disableCache" = "Önbelleği devre dışı bırak"
"disableCacheDesc" = "DNS önbelleğini devre dışı bırakır"
"disableFallback" = "Yedeklemeyi devre dışı bırak"
"disableFallbackDesc" = "Yedek DNS sorgularını devre dışı bırakır"
"disableFallbackIfMatch" = "Eşleşirse Yedeklemeyi Devre Dışı Bırak"
"disableFallbackIfMatchDesc" = "DNS sunucusunun eşleşen alan adı listesi vurulduğunda yedek DNS sorgularını devre dışı bırakır"
"enableParallelQuery" = "Paralel Sorguyu Etkinleştir"
"enableParallelQueryDesc" = "Daha hızlı çözümleme için birden fazla sunucuya paralel DNS sorgularını etkinleştir"
"strategy" = "Sorgu Stratejisi"
"strategyDesc" = "Alan adlarını çözmek için genel strateji"
"add" = "Sunucu Ekle"
"edit" = "Sunucuyu Düzenle"
"domains" = "Alan Adları"
"expectIPs" = "Beklenen IP'ler"
"unexpectIPs" = "Beklenmeyen IP'ler"
"useSystemHosts" = "Sistem Hosts'larını Kullan"
"useSystemHostsDesc" = "Yüklü bir sistemden hosts dosyasını kullan"
"usePreset" = "Şablon kullan"
"dnsPresetTitle" = "DNS Şablonları"
"dnsPresetFamily" = "Aile"
[pages.xray.fakedns]
"add" = "Sahte DNS Ekle"
"edit" = "Sahte DNS'i Düzenle"
"ipPool" = "IP Havuzu Alt Ağı"
"poolSize" = "Havuz Boyutu"
[pages.settings.security]
"admin" = "Yönetici kimlik bilgileri"
"twoFactor" = "İki adımlı doğrulama"
"twoFactorEnable" = "2FA'yı Etkinleştir"
"twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler."
"twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir"
"twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak"
"twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:"
"twoFactorModalFirstStep" = "1. Bu QR kodunu doğrulama uygulamasında tarayın veya QR kodunun yanındaki token'ı kopyalayıp uygulamaya yapıştırın"
"twoFactorModalSecondStep" = "2. Uygulamadaki kodu girin"
"twoFactorModalRemoveStep" = "İki adımlı doğrulamayı kaldırmak için uygulamadaki kodu girin."
"twoFactorModalChangeCredentialsTitle" = "Kimlik bilgilerini değiştir"
"twoFactorModalChangeCredentialsStep" = "Yönetici kimlik bilgilerini değiştirmek için uygulamadaki kodu girin."
"twoFactorModalSetSuccess" = "İki faktörlü kimlik doğrulama başarıyla kuruldu"
"twoFactorModalDeleteSuccess" = "İki faktörlü kimlik doğrulama başarıyla silindi"
"twoFactorModalError" = "Yanlış kod"
[pages.settings.toasts]
"modifySettings" = "Parametreler değiştirildi."
"getSettings" = "Parametreler alınırken bir hata oluştu."
"modifyUserError" = "Yönetici kimlik bilgileri değiştirilirken bir hata oluştu."
"modifyUser" = "Yönetici kimlik bilgilerini başarıyla değiştirdiniz."
"originalUserPassIncorrect" = "Mevcut kullanıcı adı veya şifre geçersiz"
"userPassMustBeNotEmpty" = "Yeni kullanıcı adı ve şifre boş olamaz"
"getOutboundTrafficError" = "Giden trafik alınırken hata"
"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
[tgbot]
"keyboardClosed" = "❌ Klavye kapatıldı!"
"noResult" = "❗ Sonuç yok!"
"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
"wentWrong" = "❌ Bir şeyler yanlış gitti!"
"noIpRecord" = "❗ IP Kaydı Yok!"
"noInbounds" = "❗ Gelen bağlantı bulunamadı!"
"unlimited" = "♾ Sınırsız (Sıfırla)"
"add" = "Ekle"
"month" = "Ay"
"months" = "Aylar"
"day" = "Gün"
"days" = "Günler"
"hours" = "Saatler"
"minutes" = "Dakika"
"unknown" = "Bilinmeyen"
"inbounds" = "Gelenler"
"clients" = "İstemciler"
"offline" = "🔴 Çevrimdışı"
"online" = "🟢 Çevrimiçi"
[tgbot.commands]
"unknown" = "❗ Bilinmeyen komut."
"pleaseChoose" = "👇 Lütfen seçin:\r\n"
"help" = "🤖 Bu bota hoş geldiniz! Web panelinden belirli verileri sunmak ve gerektiğinde değişiklik yapmanıza olanak tanımak için tasarlanmıştır.\r\n\r\n"
"start" = "👋 Merhaba <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 <b>{{ .Hostname }}</b> yönetim botuna hoş geldiniz.\r\n"
"status" = "✅ Bot çalışıyor!"
"usage" = "❗ Lütfen aramak için bir metin sağlayın!"
"getID" = "🆔 Kimliğiniz: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Xray Core'u yeniden başlatmak için:\r\n<code>/restart</code>\r\n\r\nBir müşteri e-postasını aramak için:\r\n<code>/usage [E-posta]</code>\r\n\r\nGelenleri aramak için (müşteri istatistikleri ile):\r\n<code>/inbound [Açıklama]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>"
"helpClientCommands" = "İstatistikleri aramak için şu komutu kullanın:\r\n\r\n<code>/usage [E-posta]</code>\r\n\r\nTelegram Sohbet Kimliği:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ İşlem başarılı!"
"restartFailed" = "❗ İşlem hatası.\r\n\r\n<code>Hata: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core çalışmıyor."
"startDesc" = "Ana menüyü göster"
"helpDesc" = "Bot yardımı"
"statusDesc" = "Bot durumunu kontrol et"
"idDesc" = "Telegram ID'nizi göster"
[tgbot.messages]
"cpuThreshold" = "🔴 CPU Yükü {{ .Percent }}% eşiği {{ .Threshold }}%'yi aşıyor"
"selectUserFailed" = "❌ Kullanıcı seçiminde hata!"
"userSaved" = "✅ Telegram Kullanıcısı kaydedildi."
"loginSuccess" = "✅ Panele başarıyla giriş yapıldı.\r\n"
"loginFailed" = "❗Panele giriş denemesi başarısız oldu.\r\n"
"2faFailed" = "2FA Hatası"
"report" = "🕰 Planlanmış Raporlar: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tarih&Zaman: {{ .DateTime }}\r\n"
"hostname" = "💻 Sunucu: {{ .Hostname }}\r\n"
"version" = "🚀 3X-UI Sürümü: {{ .Version }}\r\n"
"xrayVersion" = "📡 Xray Sürümü: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP'ler:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Çalışma Süresi: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Sistem Yükü: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Trafik: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Durum: {{ .State }}\r\n"
"username" = "👤 Kullanıcı Adı: {{ .Username }}\r\n"
"password" = "👤 Şifre: {{ .Password }}\r\n"
"time" = "⏰ Zaman: {{ .Time }}\r\n"
"inbound" = "📍 Gelen: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Son Kullanma Tarihi: {{ .Time }}\r\n"
"expireIn" = "📅 Sona Erecek: {{ .Time }}\r\n"
"active" = "💡 Aktif: {{ .Enable }}\r\n"
"enabled" = "🚨 Etkin: {{ .Enable }}\r\n"
"online" = "🌐 Bağlantı durumu: {{ .Status }}\r\n"
"lastOnline" = "🔙 Son çevrimiçi: {{ .Time }}\r\n"
"email" = "📧 E-posta: {{ .Email }}\r\n"
"upload" = "🔼 Yükleme: ↑{{ .Upload }}\r\n"
"download" = "🔽 İndirme: ↓{{ .Download }}\r\n"
"total" = "📊 Toplam: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Telegram Kullanıcısı: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Tükenmiş {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Tükenmiş {{ .Type }} sayısı:\r\n"
"onlinesCount" = "🌐 Çevrimiçi Müşteriler: {{ .Count }}\r\n"
"disabled" = "🛑 Devre Dışı: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Yakında Tükenecek: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Yedekleme Zamanı: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Yenilendi: {{ .Time }}\r\n\r\n"
"yes" = "✅ Evet"
"no" = "❌ Hayır"
"received_id" = "🔑📥 Kimlik güncellendi."
"received_password" = "🔑📥 Şifre güncellendi."
"received_email" = "📧📥 E-posta güncellendi."
"received_comment" = "💬📥 Yorum güncellendi."
"id_prompt" = "🔑 Varsayılan Kimlik: {{ .ClientId }}\n\nKimliğinizi girin."
"pass_prompt" = "🔑 Varsayılan Şifre: {{ .ClientPassword }}\n\nŞifrenizi girin."
"email_prompt" = "📧 Varsayılan E-posta: {{ .ClientEmail }}\n\nE-postanızı girin."
"comment_prompt" = "💬 Varsayılan Yorum: {{ .ClientComment }}\n\nYorumunuzu girin."
"inbound_client_data_id" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!"
"inbound_client_data_pass" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!"
"cancel" = "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄"
"error_add_client" = "⚠️ Hata:\n\n {{ .error }}"
"using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊"
"incorrect_input" = "Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫"
"AreYouSure" = "Emin misin? 🤔"
"SuccessResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı"
"FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠 Hata: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Tüm müşteriler için trafik sıfırlama işlemi tamamlandı."
[tgbot.buttons]
"closeKeyboard" = "❌ Klavyeyi Kapat"
"cancel" = "❌ İptal"
"cancelReset" = "❌ Sıfırlamayı İptal Et"
"cancelIpLimit" = "❌ IP Limitini İptal Et"
"confirmResetTraffic" = "✅ Trafiği Sıfırlamayı Onayla?"
"confirmClearIps" = "✅ IP'leri Temizlemeyi Onayla?"
"confirmRemoveTGUser" = "✅ Telegram Kullanıcısını Kaldırmayı Onayla?"
"confirmToggle" = "✅ Kullanıcıyı Etkinleştirme/Devre Dışı Bırakmayı Onayla?"
"dbBackup" = "Veritabanı Yedeği Al"
"serverUsage" = "Sunucu Kullanımı"
"getInbounds" = "Gelenleri Al"
"depleteSoon" = "Yakında Tükenecek"
"clientUsage" = "Kullanımı Al"
"onlines" = "Çevrimiçi Müşteriler"
"commands" = "Komutlar"
"refresh" = "🔄 Yenile"
"clearIPs" = "❌ IP'leri Temizle"
"removeTGUser" = "❌ Telegram Kullanıcısını Kaldır"
"selectTGUser" = "👤 Telegram Kullanıcısını Seç"
"selectOneTGUser" = "👤 Bir Telegram Kullanıcısını Seçin:"
"resetTraffic" = "📈 Trafiği Sıfırla"
"resetExpire" = "📅 Son Kullanma Tarihini Değiştir"
"ipLog" = "🔢 IP Günlüğü"
"ipLimit" = "🔢 IP Limiti"
"setTGUser" = "👤 Telegram Kullanıcısını Ayarla"
"toggle" = "🔘 Etkinleştir / Devre Dışı Bırak"
"custom" = "🔢 Özel"
"confirmNumber" = "✅ Onayla: {{ .Num }}"
"confirmNumberAdd" = "✅ Ekleme onayı: {{ .Num }}"
"limitTraffic" = "🚧 Trafik Sınırı"
"getBanLogs" = "Yasak Günlüklerini Al"
"allClients" = "Tüm Müşteriler"
"addClient" = "Müşteri Ekle"
"submitDisable" = "Devre Dışı Olarak Gönder ☑️"
"submitEnable" = "Etkin Olarak Gönder ✅"
"use_default" = "🏷️ Varsayılanı Kullan"
"change_id" = "⚙️🔑 Kimlik"
"change_password" = "⚙️🔑 Şifre"
"change_email" = "⚙️📧 E-posta"
"change_comment" = "⚙️💬 Yorum"
"ResetAllTraffics" = "Tüm Trafikleri Sıfırla"
"SortedTrafficUsageReport" = "Sıralı Trafik Kullanım Raporu"
[tgbot.answers]
"successfulOperation" = "✅ İşlem başarılı!"
"errorOperation" = "❗ İşlemde hata."
"getInboundsFailed" = "❌ Gelenler alınamadı."
"getClientsFailed" = "❌ Müşteriler alınamadı."
"canceled" = "❌ {{ .Email }}: İşlem iptal edildi."
"clientRefreshSuccess" = "✅ {{ .Email }}: Müşteri başarıyla yenilendi."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP'ler başarıyla yenilendi."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Müşterinin Telegram Kullanıcısı başarıyla yenilendi."
"resetTrafficSuccess" = "✅ {{ .Email }}: Trafik başarıyla sıfırlandı."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Trafik limiti başarıyla kaydedildi."
"expireResetSuccess" = "✅ {{ .Email }}: Son kullanma günleri başarıyla sıfırlandı."
"resetIpSuccess" = "✅ {{ .Email }}: IP limiti {{ .Count }} başarıyla kaydedildi."
"clearIpSuccess" = "✅ {{ .Email }}: IP'ler başarıyla temizlendi."
"getIpLog" = "✅ {{ .Email }}: IP Günlüğü alındı."
"getUserInfo" = "✅ {{ .Email }}: Telegram Kullanıcı Bilgisi alındı."
"removedTGUserSuccess" = "✅ {{ .Email }}: Telegram Kullanıcısı başarıyla kaldırıldı."
"enableSuccess" = "✅ {{ .Email }}: Başarıyla etkinleştirildi."
"disableSuccess" = "✅ {{ .Email }}: Başarıyla devre dışı bırakıldı."
"askToAddUserId" = "Yapılandırmanız bulunamadı!\r\nLütfen yöneticinizden yapılandırmalarınıza Telegram ChatID'nizi eklemesini isteyin.\r\n\r\nKullanıcı ChatID'niz: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Gelen {{ .Inbound }} için bir Müşteri Seçin"
"chooseInbound" = "Bir Gelen Seçin"

View file

@ -1,795 +0,0 @@
"username" = "Ім'я користувача"
"password" = "Пароль"
"login" = "Увійти"
"confirm" = "Підтвердити"
"cancel" = "Скасувати"
"close" = "Закрити"
"create" = "Створити"
"update" = "Оновити"
"copy" = "Копіювати"
"copied" = "Скопійовано"
"download" = "Завантажити"
"remark" = "Примітка"
"enable" = "Увімкнути"
"protocol" = "Протокол"
"search" = "Пошук"
"filter" = "Фільтр"
"loading" = "Завантаження..."
"second" = "Секунда"
"minute" = "Хвилина"
"hour" = "Година"
"day" = "День"
"check" = "Перевірка"
"indefinite" = "Безстроково"
"unlimited" = "Безлімітний"
"none" = "Немає"
"qrCode" = "QR-Код"
"info" = "Більше інформації"
"edit" = "Редагувати"
"delete" = "Видалити"
"reset" = "Скидання"
"noData" = "Немає даних."
"copySuccess" = "Скопійовано успішно"
"sure" = "Звичайно"
"encryption" = "Шифрування"
"useIPv4ForHost" = "Використовувати IPv4 для хоста"
"transmission" = "Протокол передачи"
"host" = "Хост"
"path" = "Шлях"
"camouflage" = "Маскування"
"status" = "Статус"
"enabled" = "Увімкнено"
"disabled" = "Вимкнено"
"depleted" = "Вичерпано"
"depletingSoon" = "Вичерпується"
"offline" = "Офлайн"
"online" = "Онлайн"
"domainName" = "Доменне ім`я"
"monitor" = "Слухати IP"
"certificate" = "Цифровий сертифікат"
"fail" = "Помилка"
"comment" = "Коментар"
"success" = "Успішно"
"lastOnline" = "Був(ла) онлайн"
"getVersion" = "Отримати версію"
"install" = "Встановити"
"clients" = "Клієнти"
"usage" = "Використання"
"twoFactorCode" = "Код"
"remained" = "Залишилося"
"security" = "Беспека"
"secAlertTitle" = "Попередження системи безпеки"
"secAlertSsl" = "Це з'єднання не є безпечним. Будь ласка, уникайте введення конфіденційної інформації, поки TLS не буде активовано для захисту даних."
"secAlertConf" = "Деякі налаштування вразливі до атак. Рекомендується посилити протоколи безпеки, щоб запобігти можливим порушенням."
"secAlertSSL" = "Панель не має безпечного з'єднання. Будь ласка, встановіть сертифікат TLS для захисту даних."
"secAlertPanelPort" = "Стандартний порт панелі вразливий. Будь ласка, сконфігуруйте випадковий або конкретний порт."
"secAlertPanelURI" = "Стандартний URI-шлях панелі небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
"secAlertSubURI" = "Стандартний URI-шлях підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
"secAlertSubJsonURI" = "Стандартний URI-шлях JSON підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
"emptyDnsDesc" = "Немає доданих DNS-серверів."
"emptyFakeDnsDesc" = "Немає доданих Fake DNS-серверів."
"emptyBalancersDesc" = "Немає доданих балансувальників."
"emptyReverseDesc" = "Немає доданих зворотних проксі."
"somethingWentWrong" = "Щось пішло не так"
[subscription]
"title" = "Інформація про підписку"
"subId" = "ID підписки"
"status" = "Статус"
"downloaded" = "Завантажено"
"uploaded" = "Відвантажено"
"expiry" = "Термін дії"
"totalQuota" = "Загальна квота"
"individualLinks" = "Окремі посилання"
"active" = "Активна"
"inactive" = "Неактивна"
"unlimited" = "Безліміт"
"noExpiry" = "Без строку"
[menu]
"theme" = "Тема"
"dark" = "Темна"
"ultraDark" = "Ультра темна"
"dashboard" = "Огляд"
"inbounds" = "Вхідні"
"settings" = "Параметри панелі"
"xray" = "Конфігурації Xray"
"logout" = "Вийти"
"link" = "Керувати"
[pages.login]
"hello" = "Привіт"
"title" = "Привітання!"
"loginAgain" = "Ваш сеанс закінчився, увійдіть знову"
[pages.login.toasts]
"invalidFormData" = "Формат вхідних даних недійсний."
"emptyUsername" = "Потрібне ім'я користувача"
"emptyPassword" = "Потрібен пароль"
"wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації."
"successLogin" = "Ви успішно увійшли до свого облікового запису."
"successRegister" = "Реєстрація пройшла успішно, будь ласка, увійдіть."
"userExists" = "Ім'я користувача вже існує"
"errorRegister" = "Помилка реєстрації"
[pages.index]
"title" = "Огляд"
"cpu" = "ЦП"
"logicalProcessors" = "Логічні процесори"
"frequency" = "Частота"
"swap" = "Своп"
"storage" = "Сховище"
"memory" = "ОЗП"
"threads" = "Потоки"
"xrayStatus" = "Xray"
"stopXray" = "Зупинити"
"restartXray" = "Перезапустити"
"xraySwitch" = "Версія"
"xraySwitchClick" = "Виберіть версію, на яку ви хочете перейти."
"xraySwitchClickDesk" = "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями."
"xrayStatusUnknown" = "Невідомо"
"xrayStatusRunning" = "Запущено"
"xrayStatusStop" = "Зупинено"
"xrayStatusError" = "Помилка"
"xrayErrorPopoverTitle" = "Під час роботи Xray сталася помилка"
"operationHours" = "Час роботи"
"systemLoad" = "Завантаження системи"
"systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин"
"connectionCount" = "Статистика з'єднання"
"ipAddresses" = "IP-адреси"
"toggleIpVisibility" = "Перемкнути видимість IP"
"overallSpeed" = "Загальна швидкість"
"upload" = "Відправка"
"download" = "Завантаження"
"totalData" = "Загальний обсяг даних"
"sent" = "Відправлено"
"received" = "Отримано"
"documentation" = "Документація"
"xraySwitchVersionDialog" = "Ви дійсно хочете змінити версію Xray?"
"xraySwitchVersionDialogDesc" = "Це змінить версію Xray на #version#."
"xraySwitchVersionPopover" = "Xray успішно оновлено"
"geofileUpdateDialog" = "Ви дійсно хочете оновити геофайл?"
"geofileUpdateDialogDesc" = "Це оновить файл #filename#."
"geofilesUpdateDialogDesc" = "Це оновить усі геофайли."
"geofilesUpdateAll" = "Оновити все"
"geofileUpdatePopover" = "Геофайл успішно оновлено"
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
"logs" = "Журнали"
"config" = "Конфігурація"
"backup" = "Резервна копія"
"backupTitle" = "Резервне копіювання та відновлення бази даних"
"exportDatabase" = "Резервна копія"
"exportDatabaseDesc" = "Натисніть, щоб завантажити файл .db, що містить резервну копію вашої поточної бази даних на ваш пристрій."
"importDatabase" = "Відновити"
"importDatabaseDesc" = "Натисніть, щоб вибрати та завантажити файл .db з вашого пристрою для відновлення бази даних з резервної копії."
"importDatabaseSuccess" = "Базу даних успішно імпортовано"
"importDatabaseError" = "Виникла помилка під час імпорту бази даних"
"readDatabaseError" = "Виникла помилка під час читання бази даних"
"getDatabaseError" = "Виникла помилка під час отримання бази даних"
"getConfigError" = "Виникла помилка під час отримання файлу конфігурації"
[pages.inbounds]
"allTimeTraffic" = "Загальний трафік"
"allTimeTrafficUsage" = "Загальне використання за весь час"
"title" = "Вхідні"
"totalDownUp" = "Всього надісланих/отриманих"
"totalUsage" = "Всього використанно"
"inboundCount" = "Загальна кількість вхідних"
"operate" = "Меню"
"enable" = "Увімкнено"
"remark" = "Примітка"
"protocol" = "Протокол"
"port" = "Порт"
"portMap" = "Порт-перехід"
"traffic" = "Трафік"
"details" = "Деталі"
"transportConfig" = "Транспорт"
"expireDate" = "Тривалість"
"createdAt" = "Створено"
"updatedAt" = "Оновлено"
"resetTraffic" = "Скинути трафік"
"addInbound" = "Додати вхідний"
"generalActions" = "Загальні дії"
"autoRefresh" = "Автооновлення"
"autoRefreshInterval" = "Інтервал"
"modifyInbound" = "Змінити вхідний"
"deleteInbound" = "Видалити вхідні"
"deleteInboundContent" = "Ви впевнені, що хочете видалити вхідні?"
"deleteClient" = "Видалити клієнта"
"deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?"
"resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?"
"copyLink" = "Копіювати URL"
"address" = "Адреса"
"network" = "Мережа"
"destinationPort" = "Порт призначення"
"targetAddress" = "Цільова адреса"
"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси"
"meansNoLimit" = "= Необмежено. (одиниця: ГБ)"
"totalFlow" = "Загальна витрата"
"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався"
"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням"
"certificatePath" = "Шлях до файлу"
"certificateContent" = "Вміст файлу"
"publicKey" = "Публічний ключ"
"privatekey" = "Закритий ключ"
"clickOnQRcode" = "Натисніть QR-код, щоб скопіювати"
"client" = "Клієнт"
"export" = "Експортувати всі URL-адреси"
"clone" = "Клон"
"cloneInbound" = "Клонувати"
"cloneInboundContent" = "Усі налаштування цього вхідного потоку, крім порту, IP-адреси прослуховування та клієнтів, будуть застосовані до клону."
"cloneInboundOk" = "Клонувати"
"resetAllTraffic" = "Скинути весь вхідний трафік"
"resetAllTrafficTitle" = "Скинути весь вхідний трафік"
"resetAllTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх вхідних?"
"resetInboundClientTraffics" = "Скинути трафік клієнтів"
"resetInboundClientTrafficTitle" = "Скинути трафік клієнтів"
"resetInboundClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік клієнтів цього вхідного потоку?"
"resetAllClientTraffics" = "Скинути весь трафік клієнтів"
"resetAllClientTrafficTitle" = "Скинути весь трафік клієнтів"
"resetAllClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх клієнтів?"
"delDepletedClients" = "Видалити вичерпані клієнти"
"delDepletedClientsTitle" = "Видалити вичерпані клієнти"
"delDepletedClientsContent" = "Ви впевнені, що хочете видалити всі вичерпані клієнти?"
"email" = "Електронна пошта"
"emailDesc" = "Будь ласка, надайте унікальну адресу електронної пошти."
"IPLimit" = "Обмеження IP"
"IPLimitDesc" = "Вимикає вхідний, якщо кількість перевищує встановлене значення. (0 = вимкнено)"
"IPLimitlog" = "Журнал IP"
"IPLimitlogDesc" = "Журнал історії IP-адрес. (щоб увімкнути вхідну після вимкнення, очистіть журнал)"
"IPLimitlogclear" = "Очистити журнал"
"setDefaultCert" = "Установити сертифікат з панелі"
"telegramDesc" = "Будь ласка, вкажіть ID чату Telegram. (використовуйте команду '/id' у боті) або (@userinfobot)"
"subscriptionDesc" = "Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів."
"info" = "Інформація"
"same" = "Те саме"
"inboundData" = "Вхідні дані"
"exportInbound" = "Експортувати вхідні"
"import" = "Імпорт"
"importInbound" = "Імпортувати вхідний"
"periodicTrafficResetTitle" = "Скидання трафіку"
"periodicTrafficResetDesc" = "Автоматично скидати лічильник трафіку через певні проміжки часу"
"lastReset" = "Останнє скидання"
[pages.client]
"add" = "Додати клієнта"
"edit" = "Редагувати клієнта"
"submitAdd" = "Додати клієнта"
"submitEdit" = "Зберегти зміни"
"clientCount" = "Кількість клієнтів"
"bulk" = "Додати групу"
"method" = "Метод"
"first" = "Перший"
"last" = "Останній"
"prefix" = "Префікс"
"postfix" = "Постфікс"
"delayedStart" = "Початок використання"
"expireDays" = "Тривалість"
"days" = "Дні(в)"
"renew" = "Автоматичне оновлення"
"renewDesc" = "Автоматичне поновлення після закінчення терміну дії. (0 = вимкнено)(одиниця: день)"
[pages.inbounds.periodicTrafficReset]
"never" = "Ніколи"
"daily" = "Щодня"
"weekly" = "Щотижня"
"monthly" = "Щомісяця"
[pages.inbounds.toasts]
"obtain" = "Отримати"
"updateSuccess" = "Оновлення пройшло успішно"
"logCleanSuccess" = "Журнал очищено"
"inboundsUpdateSuccess" = "Вхідні підключення успішно оновлено"
"inboundUpdateSuccess" = "Вхідне підключення успішно оновлено"
"inboundCreateSuccess" = "Вхідне підключення успішно створено"
"inboundDeleteSuccess" = "Вхідне підключення успішно видалено"
"inboundClientAddSuccess" = "Клієнт(и) вхідного підключення додано"
"inboundClientDeleteSuccess" = "Клієнта вхідного підключення видалено"
"inboundClientUpdateSuccess" = "Клієнта вхідного підключення оновлено"
"delDepletedClientsSuccess" = "Усі вичерпані клієнти видалені"
"resetAllClientTrafficSuccess" = "Весь трафік клієнта скинуто"
"resetAllTrafficSuccess" = "Весь трафік скинуто"
"resetInboundClientTrafficSuccess" = "Трафік скинуто"
"trafficGetError" = "Помилка отримання даних про трафік"
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
"getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc."
[pages.inbounds.stream.general]
"request" = "Запит"
"response" = "Відповідь"
"name" = "Ім'я"
"value" = "Значення"
[pages.inbounds.stream.tcp]
"version" = "Версія"
"method" = "Метод"
"path" = "Шлях"
"status" = "Статус"
"statusDescription" = "Опис стану"
"requestHeader" = "Заголовок запиту"
"responseHeader" = "Заголовок відповіді"
[pages.settings]
"title" = "Параметри панелі"
"save" = "Зберегти"
"infoDesc" = "Кожна внесена тут зміна повинна бути збережена. Перезапустіть панель, щоб застосувати зміни."
"restartPanel" = "Перезапустити панель"
"restartPanelDesc" = "Ви впевнені, що бажаєте перезапустити панель? Якщо ви не можете отримати доступ до панелі після перезапуску, будь ласка, перегляньте інформацію журналу панелі на сервері."
"restartPanelSuccess" = "Панель успішно перезапущено"
"actions" = "Дії"
"resetDefaultConfig" = "Відновити значення за замовчуванням"
"panelSettings" = "Загальні"
"securitySettings" = "Автентифікація"
"TGBotSettings" = "Telegram Бот"
"panelListeningIP" = "Слухати IP"
"panelListeningIPDesc" = "IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)"
"panelListeningDomain" = "Домен прослуховування"
"panelListeningDomainDesc" = "Доменне ім'я для веб-панелі. (залиште порожнім, щоб слухати всі домени та IP-адреси)"
"panelPort" = "Порт прослуховування"
"panelPortDesc" = "Номер порту для веб-панелі. (має бути невикористаний порт)"
"publicKeyPath" = "Шлях відкритого ключа"
"publicKeyPathDesc" = "Шлях до файлу відкритого ключа для веб-панелі. (починається з /)"
"privateKeyPath" = "Шлях приватного ключа"
"privateKeyPathDesc" = "Шлях до файлу приватного ключа для веб-панелі. (починається з /)"
"panelUrlPath" = "Шлях URL"
"panelUrlPathDesc" = "Шлях URL для веб-панелі. (починається з / і закінчується /)"
"pageSize" = "Розмір сторінки"
"pageSizeDesc" = "Визначити розмір сторінки для вхідної таблиці. (0 = вимкнено)"
"remarkModel" = "Модель зауваження та роздільний символ"
"datepicker" = "Тип календаря"
"datepickerPlaceholder" = "Виберіть дату"
"datepickerDescription" = "Заплановані завдання виконуватимуться на основі цього календаря."
"sampleRemark" = "Зразок зауваження"
"oldUsername" = "Поточне ім'я користувача"
"currentPassword" = "Поточний пароль"
"newUsername" = "Нове ім'я користувача"
"newPassword" = "Новий пароль"
"telegramBotEnable" = "Увімкнути Telegram Bot"
"telegramBotEnableDesc" = "Вмикає бота Telegram."
"telegramToken" = "Telegram Токен"
"telegramTokenDesc" = "Токен бота Telegram, отриманий від '@BotFather'."
"telegramProxy" = "SOCKS Проксі"
"telegramProxyDesc" = "Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)"
"telegramAPIServer" = "Сервер Telegram API"
"telegramAPIServerDesc" = "Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням."
"telegramChatId" = "Ідентифікатор чату адміністратора"
"telegramChatIdDesc" = "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут @userinfobot) або (використовуйте команду '/id' у боті)"
"telegramNotifyTime" = "Час сповіщення"
"telegramNotifyTimeDesc" = "Час повідомлення бота Telegram, встановлений для періодичних звітів. (використовуйте формат часу crontab)"
"tgNotifyBackup" = "Резервне копіювання бази даних"
"tgNotifyBackupDesc" = "Надіслати файл резервної копії бази даних зі звітом."
"tgNotifyLogin" = "Сповіщення про вхід"
"tgNotifyLoginDesc" = "Отримувати сповіщення про ім'я користувача, IP-адресу та час щоразу, коли хтось намагається увійти у вашу веб-панель."
"sessionMaxAge" = "Тривалість сеансу"
"sessionMaxAgeDesc" = "Тривалість, протягом якої ви можете залишатися в системі. (одиниця: хвилина)"
"expireTimeDiff" = "Повідомлення про дату закінчення"
"expireTimeDiffDesc" = "Отримувати сповіщення про термін дії при досягненні цього порогу. (одиниця: день)"
"trafficDiff" = "Повідомлення про обмеження трафіку"
"trafficDiffDesc" = "Отримувати сповіщення про обмеження трафіку при досягненні цього порогу. (одиниця: ГБ)"
"tgNotifyCpu" = "Сповіщення про завантаження ЦП"
"tgNotifyCpuDesc" = "Отримувати сповіщення, якщо навантаження ЦП перевищує це порогове значення. (одиниця: %)"
"timeZone" = "Часовий пояс"
"timeZoneDesc" = "Заплановані завдання виконуватимуться на основі цього часового поясу."
"subSettings" = "Підписка"
"subEnable" = "Увімкнути службу підписки"
"subEnableDesc" = "Вмикає службу підписки."
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
"subTitle" = "Назва Підписки"
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
"subSupportUrl" = "URL підтримки"
"subSupportUrlDesc" = "Посилання на технічну підтримку, що відображається у VPN-клієнті"
"subProfileUrl" = "URL профілю"
"subProfileUrlDesc" = "Посилання на ваш вебсайт, що відображається у VPN-клієнті"
"subAnnounce" = "Оголошення"
"subAnnounceDesc" = "Текст оголошення, що відображається у VPN-клієнті"
"subEnableRouting" = "Увімкнути маршрутизацію"
"subEnableRoutingDesc" = "Глобальне налаштування для увімкнення маршрутизації у VPN-клієнті. (Тільки для Happ)"
"subRoutingRules" = "Правила маршрутизації"
"subRoutingRulesDesc" = "Глобальні правила маршрутизації для VPN-клієнта. (Тільки для Happ)"
"subListen" = "Слухати IP"
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
"subPort" = "Слухати порт"
"subPortDesc" = "Номер порту для служби підписки. (має бути невикористаний порт)"
"subCertPath" = "Шлях відкритого ключа"
"subCertPathDesc" = "Шлях до файлу відкритого ключа для служби підписки. (починається з /)"
"subKeyPath" = "Шлях приватного ключа"
"subKeyPathDesc" = "Шлях до файлу приватного ключа для служби підписки. (починається з /)"
"subPath" = "Шлях URI"
"subPathDesc" = "Шлях URI для служби підписки. (починається з / і закінчується /)"
"subDomain" = "Домен прослуховування"
"subDomainDesc" = "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)"
"subUpdates" = "Інтервали оновлення"
"subUpdatesDesc" = "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)"
"subEncrypt" = "Закодувати"
"subEncryptDesc" = "Повернений вміст послуги підписки матиме кодування Base64."
"subShowInfo" = "Показати інформацію про використання"
"subShowInfoDesc" = "Залишок трафіку та дата відображатимуться в клієнтських програмах."
"subURI" = "URI зворотного проксі"
"subURIDesc" = "URI до URL-адреси підписки для використання за проксі."
"externalTrafficInformEnable" = "Інформація про зовнішній трафік"
"externalTrafficInformEnableDesc" = "Інформувати зовнішній API про кожне оновлення трафіку."
"externalTrafficInformURI" = "Інформаційний URI зовнішнього трафіку"
"externalTrafficInformURIDesc" = "Оновлення трафіку надсилаються на цей URI."
"fragment" = "Фрагментація"
"fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS"
"fragmentSett" = "Параметри фрагментації"
"noisesDesc" = "Увімкнути Noises."
"noisesSett" = "Налаштування Noises"
"mux" = "Mux"
"muxDesc" = "Передавати кілька незалежних потоків даних у межах встановленого потоку даних."
"muxSett" = "Налаштування Mux"
"direct" = "Пряме підключення"
"directDesc" = "Безпосередньо встановлює з’єднання з доменами або діапазонами IP певної країни."
"notifications" = "Сповіщення"
"certs" = "Сертифікати"
"externalTraffic" = "Зовнішній трафік"
"dateAndTime" = "Дата та час"
"proxyAndServer" = "Проксі та сервер"
"intervals" = "Інтервали"
"information" = "Інформація"
"language" = "Мова"
"telegramBotLanguage" = "Мова Telegram-бота"
[pages.xray]
"title" = "Xray конфігурації"
"save" = "Зберегти"
"restart" = "Перезапустити Xray"
"restartSuccess" = "Xray успішно перезапущено"
"stopSuccess" = "Xray успішно зупинено"
"restartError" = "Виникла помилка під час перезапуску Xray."
"stopError" = "Виникла помилка під час зупинки Xray."
"basicTemplate" = "Базовий шаблон"
"advancedTemplate" = "Додатково"
"generalConfigs" = "Загальні конфігурації"
"generalConfigsDesc" = "Ці параметри визначатимуть загальні налаштування."
"logConfigs" = "Журнал"
"logConfigsDesc" = "Журнали можуть вплинути на ефективність вашого сервера. Рекомендується вмикати його з розумом лише у випадку ваших потреб"
"blockConfigsDesc" = "Ці параметри блокуватимуть трафік на основі конкретних запитуваних протоколів і веб-сайтів."
"basicRouting" = "Основна Маршрутизація"
"blockConnectionsConfigsDesc" = "Ці параметри блокуватимуть трафік на основі запитаних країн."
"directConnectionsConfigsDesc" = "Пряме з'єднання гарантує, що певний трафік не буде маршрутизовано через інший сервер."
"blockips" = "Блокувати IP"
"blockdomains" = "Блокувати домени"
"directips" = "Прямі IP"
"directdomains" = "Прямі домени"
"ipv4Routing" = "Маршрутизація IPv4"
"ipv4RoutingDesc" = "Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4."
"warpRouting" = "WARP Маршрутизація"
"warpRoutingDesc" = "Ці параметри маршрутизуватимуть трафік на основі певного пункту призначення через WARP."
"Template" = "Шаблон розширеної конфігурації Xray"
"TemplateDesc" = "Остаточний конфігураційний файл Xray буде створено на основі цього шаблону."
"FreedomStrategy" = "Стратегія протоколу свободи"
"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи."
"RoutingStrategy" = "Загальна стратегія маршрутизації"
"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів."
"outboundTestUrl" = "URL тесту outbound"
"outboundTestUrlDesc" = "URL для перевірки з'єднання outbound"
"Torrent" = "Блокувати протокол BitTorrent"
"Inbounds" = "Вхідні"
"InboundsDesc" = "Прийняття певних клієнтів."
"Outbounds" = "Вихід"
"Balancers" = "Балансери"
"OutboundsDesc" = "Встановити шлях вихідного трафіку."
"Routings" = "Правила маршрутизації"
"RoutingsDesc" = "Пріоритет кожного правила важливий!"
"completeTemplate" = "Усі"
"logLevel" = "Рівень журналу"
"logLevelDesc" = "Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати."
"accessLog" = "Журнал доступу"
"accessLogDesc" = "Шлях до файлу журналу доступу. Спеціальне значення 'none' вимикає журнали доступу"
"errorLog" = "Журнал помилок"
"errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок"
"dnsLog" = "Журнал DNS"
"dnsLogDesc" = "Чи включити журнали запитів DNS"
"maskAddress" = "Маскувати Адресу"
"maskAddressDesc" = "Маска IP-адреси, при активації автоматично замінює IP-адресу, яка з'являється у журналі."
"statistics" = "Статистика"
"statsInboundUplink" = "Статистика вхідного аплінку"
"statsInboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вхідних проксі."
"statsInboundDownlink" = "Статистика вхідного даунлінку"
"statsInboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вхідних проксі."
"statsOutboundUplink" = "Статистика вихідного аплінку"
"statsOutboundUplinkDesc" = "Увімкнення збору статистики для вхідного трафіку всіх вихідних проксі."
"statsOutboundDownlink" = "Статистика вихідного даунлінку"
"statsOutboundDownlinkDesc" = "Увімкнення збору статистики для вихідного трафіку всіх вихідних проксі."
[pages.xray.rules]
"first" = "Перший"
"last" = "Останній"
"up" = "Вгору"
"down" = "Вниз"
"source" = "Джерело"
"dest" = "Пункт призначення"
"inbound" = "Вхідний"
"outbound" = "Вихідний"
"balancer" = "Балансувальник"
"info" = "Інформація"
"add" = "Додати правило"
"edit" = "Редагувати правило"
"useComma" = "Елементи, розділені комами"
[pages.xray.outbound]
"addOutbound" = "Додати вихідний"
"addReverse" = "Додати реверс"
"editOutbound" = "Редагувати вихідні"
"editReverse" = "Редагувати реверс"
"tag" = "Тег"
"tagDesc" = "Унікальний тег"
"address" = "Адреса"
"reverse" = "Зворотний"
"domain" = "Домен"
"type" = "Тип"
"bridge" = "Міст"
"portal" = "Портал"
"link" = "Посилання"
"intercon" = "Взаємозв'язок"
"settings" = "Налаштування"
"accountInfo" = "Інформація про обліковий запис"
"outboundStatus" = "Статус виходу"
"sendThrough" = "Надіслати через"
"test" = "Тест"
"testResult" = "Результат тесту"
"testing" = "Тестування з'єднання..."
"testSuccess" = "Тест успішний"
"testFailed" = "Тест не пройдено"
"testError" = "Не вдалося протестувати вихідне з'єднання"
[pages.xray.balancer]
"addBalancer" = "Додати балансир"
"editBalancer" = "Редагувати балансир"
"balancerStrategy" = "Стратегія"
"balancerSelectors" = "Селектори"
"tag" = "Тег"
"tagDesc" = "Унікальний тег"
"balancerDesc" = "Неможливо використовувати balancerTag і outboundTag одночасно. Якщо використовувати одночасно, працюватиме лише outboundTag."
[pages.xray.wireguard]
"secretKey" = "Приватний ключ"
"publicKey" = "Публічний ключ"
"allowedIPs" = "Дозволені IP-адреси"
"endpoint" = "Кінцева точка"
"psk" = "Спільний ключ"
"domainStrategy" = "Стратегія домену"
[pages.xray.tun]
"nameDesc" = "Назва інтерфейсу TUN. Значення за замовчуванням - 'xray0'"
"mtuDesc" = "Максимальна одиниця передачі. Максимальний розмір пакетів даних. Значення за замовчуванням - 1500"
"userLevel" = "Рівень користувача"
"userLevelDesc" = "Всі з'єднання, встановлені через цей вхід, використовуватимуть цей рівень користувача. Значення за замовчуванням - 0"
[pages.xray.dns]
"enable" = "Увімкнути DNS"
"enableDesc" = "Увімкнути вбудований DNS-сервер"
"tag" = "Мітка вхідного DNS"
"tagDesc" = "Ця мітка буде доступна як вхідна мітка в правилах маршрутизації."
"clientIp" = "IP клієнта"
"clientIpDesc" = "Використовується для повідомлення серверу про вказане місцезнаходження IP під час DNS-запитів"
"disableCache" = "Вимкнути кеш"
"disableCacheDesc" = "Вимкнути кешування DNS"
"disableFallback" = "Вимкнути резервний DNS"
"disableFallbackDesc" = "Вимкнути резервні DNS-запити"
"disableFallbackIfMatch" = "Вимкнути резервний DNS при збігу"
"disableFallbackIfMatchDesc" = "Вимкнути резервні DNS-запити при збігу списку доменів DNS-сервера"
"enableParallelQuery" = "Увімкнути паралельні запити"
"enableParallelQueryDesc" = "Увімкнути паралельні DNS-запити до кількох серверів для швидшого вирішення"
"strategy" = "Стратегія запиту"
"strategyDesc" = "Загальна стратегія вирішення доменних імен"
"add" = "Додати сервер"
"edit" = "Редагувати сервер"
"domains" = "Домени"
"expectIPs" = "Очікувані IP"
"unexpectIPs" = "Неочікувані IP"
"useSystemHosts" = "Використовувати системні Hosts"
"useSystemHostsDesc" = "Використовувати файл hosts з встановленої системи"
"usePreset" = "Використати шаблон"
"dnsPresetTitle" = "Шаблони DNS"
"dnsPresetFamily" = "Сімейний"
[pages.xray.fakedns]
"add" = "Додати підроблений DNS"
"edit" = "Редагувати підроблений DNS"
"ipPool" = "Підмережа IP-пулу"
"poolSize" = "Розмір пулу"
[pages.settings.security]
"admin" = "Облікові дані адміністратора"
"twoFactor" = "Двофакторна аутентифікація"
"twoFactorEnable" = "Увімкнути 2FA"
"twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки."
"twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію"
"twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію"
"twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:"
"twoFactorModalFirstStep" = "1. Відскануйте цей QR-код у програмі для аутентифікації або скопіюйте токен біля QR-коду та вставте його в програму"
"twoFactorModalSecondStep" = "2. Введіть код з програми"
"twoFactorModalRemoveStep" = "Введіть код з програми, щоб вимкнути двофакторну аутентифікацію."
"twoFactorModalChangeCredentialsTitle" = "Змінити облікові дані"
"twoFactorModalChangeCredentialsStep" = "Введіть код з додатку, щоб змінити облікові дані адміністратора."
"twoFactorModalSetSuccess" = "Двофакторна аутентифікація була успішно встановлена"
"twoFactorModalDeleteSuccess" = "Двофакторна аутентифікація була успішно видалена"
"twoFactorModalError" = "Невірний код"
[pages.settings.toasts]
"modifySettings" = "Параметри було змінено."
"getSettings" = "Виникла помилка під час отримання параметрів."
"modifyUserError" = "Виникла помилка під час зміни облікових даних адміністратора."
"modifyUser" = "Ви успішно змінили облікові дані адміністратора."
"originalUserPassIncorrect" = "Поточне ім'я користувача або пароль недійсні"
"userPassMustBeNotEmpty" = "Нове ім'я користувача та пароль порожні"
"getOutboundTrafficError" = "Помилка отримання вихідного трафіку"
"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
[tgbot]
"keyboardClosed" = "❌ Клавіатуру закрито!"
"noResult" = "❗ Немає результату!"
"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
"wentWrong" = "❌ Щось пішло не так!"
"noIpRecord" = "❗ Немає запису IP!"
"noInbounds" = "❗ Вхідні не знайдені!"
"unlimited" = "♾ Необмежено (Скинути)"
"add" = "Додати"
"month" = "Місяць"
"months" = "Місяці"
"day" = "День"
"days" = "Дні"
"hours" = "Години"
"minutes" = "Хвилини"
"unknown" = "Невідомо"
"inbounds" = "Вхідні"
"clients" = "Клієнти"
"offline" = "🔴 Офлайн"
"online" = "🟢 Онлайн"
[tgbot.commands]
"unknown" = "❗ Невідома команда."
"pleaseChoose" = "👇 Будь ласка, виберіть:\r\n"
"help" = "🤖 Ласкаво просимо до цього бота! Він розроблений, щоб надавати певні дані з веб-панелі та дозволяє вносити зміни за потреби.\r\n\r\n"
"start" = "👋 Привіт <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Ласкаво просимо до <b>{{ .Hostname }}</b> бота керування.\r\n"
"status" = "✅ Бот в порядку!"
"usage" = "❗ Введіть текст для пошуку!"
"getID" = "🆔 Ваш ідентифікатор: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Для перезапуску Xray Core:\r\n<code>/restart</code>\r\n\r\nДля пошуку електронної пошти клієнта:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nДля пошуку вхідних (зі статистикою клієнта):\r\n<code>/inbound [Примітка]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Для пошуку статистики використовуйте наступну команду:\r\n<code>/usage [Електронна пошта]</code>\r\n\r\nID чату Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Операція успішна!"
"restartFailed" = "❗ Помилка в операції.\r\n\r\n<code>Помилка: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core не запущений."
"startDesc" = "Показати головне меню"
"helpDesc" = "Довідка по боту"
"statusDesc" = "Перевірити статус бота"
"idDesc" = "Показати ваш Telegram ID"
[tgbot.messages]
"cpuThreshold" = "🔴 Навантаження ЦП {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%"
"selectUserFailed" = "❌ Помилка під час вибору користувача!"
"userSaved" = "✅ Користувача Telegram збережено."
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
"loginFailed" = "❗️ Помилка входу в панель.\r\n"
"2faFailed" = "Помилка 2FA"
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
"hostname" = "💻 Хост: {{ .Hostname }}\r\n"
"version" = "🚀 3X-UI Версія: {{ .Version }}\r\n"
"xrayVersion" = "📡 Xray Версія: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP-адреси:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Трафік: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Статус: {{ .State }}\r\n"
"username" = "👤 Ім'я користувача: {{ .Username }}\r\n"
"password" = "👤 Пароль: {{ .Password }}\r\n"
"time" = "⏰ Час: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата закінчення: {{ .Time }}\r\n"
"expireIn" = "📅 Термін дії: {{ .Time }}\r\n"
"active" = "💡 Активний: {{ .Enable }}\r\n"
"enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n"
"online" = "🌐 Стан підключення: {{ .Status }}\r\n"
"lastOnline" = "🔙 Був(ла) онлайн: {{ .Time }}\r\n"
"email" = "📧 Електронна пошта: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
"total" = "📊 Всього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Користувач Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Вичерпано {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Вичерпано кількість {{ .Type }} count:\r\n"
"onlinesCount" = "🌐 Онлайн-клієнти: {{ .Count }}\r\n"
"disabled" = "🛑 Вимкнено: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Скоро вичерпається: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Час резервного копіювання: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n"
"yes" = "✅ Так"
"no" = "❌ Ні"
"received_id" = "🔑📥 ID оновлено."
"received_password" = "🔑📥 Пароль оновлено."
"received_email" = "📧📥 Електронна пошта оновлена."
"received_comment" = "💬📥 Коментар оновлено."
"id_prompt" = "🔑 Стандартний ID: {{ .ClientId }}\n\nВведіть ваш ID."
"pass_prompt" = "🔑 Стандартний пароль: {{ .ClientPassword }}\n\nВведіть ваш пароль."
"email_prompt" = "📧 Стандартний email: {{ .ClientEmail }}\n\nВведіть ваш email."
"comment_prompt" = "💬 Стандартний коментар: {{ .ClientComment }}\n\nВведіть ваш коментар."
"inbound_client_data_id" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!"
"inbound_client_data_pass" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!"
"cancel" = "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄"
"error_add_client" = "⚠️ Помилка:\n\n {{ .error }}"
"using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊"
"incorrect_input" = "Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫"
"AreYouSure" = "Ви впевнені? 🤔"
"SuccessResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно"
"FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠 Помилка: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Процес скидання трафіку завершено для всіх клієнтів."
[tgbot.buttons]
"closeKeyboard" = "❌ Закрити клавіатуру"
"cancel" = "❌ Скасувати"
"cancelReset" = "❌ Скасувати скидання"
"cancelIpLimit" = "❌ Скасувати обмеження IP"
"confirmResetTraffic" = "✅ Підтвердити скидання трафіку?"
"confirmClearIps" = "✅ Підтвердити очищення IP-адрес?"
"confirmRemoveTGUser" = "✅ Підтвердити видалення користувача Telegram?"
"confirmToggle" = "✅ Підтвердити ввімкнути/вимкнути користувача?"
"dbBackup" = "Отримати резервну копію БД"
"serverUsage" = "Використання сервера"
"getInbounds" = "Отримати вхідні"
"depleteSoon" = "Скоро вичерпати"
"clientUsage" = "Отримати використання"
"onlines" = "Онлайн-клієнти"
"commands" = "Команди"
"refresh" = "🔄 Оновити"
"clearIPs" = "❌ Очистити IP-адреси"
"removeTGUser" = "❌ Видалити користувача Telegram"
"selectTGUser" = "👤 Виберіть користувача Telegram"
"selectOneTGUser" = "👤 Виберіть користувача Telegram:"
"resetTraffic" = "📈 Скинути трафік"
"resetExpire" = "📅 Змінити термін дії"
"ipLog" = "🔢 IP журнал"
"ipLimit" = "🔢 IP Ліміт"
"setTGUser" = "👤 Встановити користувача Telegram"
"toggle" = "🔘 Увімкнути / Вимкнути"
"custom" = "🔢 Custom"
"confirmNumber" = "✅ Підтвердити: {{ .Num }}"
"confirmNumberAdd" = "✅ Підтвердити додавання: {{ .Num }}"
"limitTraffic" = "🚧 Ліміт трафіку"
"getBanLogs" = "Отримати журнали заборон"
"allClients" = "Всі Клієнти"
"addClient" = "Додати клієнта"
"submitDisable" = "Надіслати як вимкнено ☑️"
"submitEnable" = "Надіслати як увімкнено ✅"
"use_default" = "🏷️ Використати типове"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Пароль"
"change_email" = "⚙️📧 Електронна пошта"
"change_comment" = "⚙️💬 Коментар"
"ResetAllTraffics" = "Скинути весь трафік"
"SortedTrafficUsageReport" = "Відсортований звіт про використання трафіку"
[tgbot.answers]
"successfulOperation" = "✅ Операція успішна!"
"errorOperation" = "❗ Помилка в роботі."
"getInboundsFailed" = "❌ Не вдалося отримати вхідні повідомлення."
"getClientsFailed" = "❌ Не вдалося отримати клієнтів."
"canceled" = "❌ {{ .Email }}: Операцію скасовано."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клієнт успішно оновлено."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреси успішно оновлено."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Користувач Telegram клієнта успішно оновлено."
"resetTrafficSuccess" = "✅ {{ .Email }}: Трафік скинуто успішно."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Ліміт трафіку успішно збережено."
"expireResetSuccess" = "✅ {{ .Email }}: Успішно скинуто дні закінчення терміну дії."
"resetIpSuccess" = "✅ {{ .Email }}: IP обмеження {{ .Count }} успішно збережено."
"clearIpSuccess" = "✅ {{ .Email }}: IP успішно очищено."
"getIpLog" = "✅ {{ .Email }}: Отримати IP-журнал."
"getUserInfo" = "✅ {{ .Email }}: Отримати інформацію про користувача Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Користувача Telegram видалено успішно."
"enableSuccess" = "✅ {{ .Email }}: Увімкнути успішно."
"disableSuccess" = "✅ {{ .Email }}: Успішно вимкнено."
"askToAddUserId" = "Вашу конфігурацію не знайдено!\r\nБудь ласка, попросіть свого адміністратора використовувати ваш ідентифікатор Telegram у вашій конфігурації.\r\n\r\nВаш ідентифікатор користувача: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Виберіть клієнта для Вхідного {{ .Inbound }}"
"chooseInbound" = "Виберіть Вхідний"

View file

@ -1,795 +0,0 @@
"username" = "Tên người dùng"
"password" = "Mật khẩu"
"login" = "Đăng nhập"
"confirm" = "Xác nhận"
"cancel" = "Hủy bỏ"
"close" = "Đóng"
"create" = "Tạo"
"update" = "Cập nhật"
"copy" = "Sao chép"
"copied" = "Đã sao chép"
"download" = "Tải xuống"
"remark" = "Ghi chú"
"enable" = "Kích hoạt"
"protocol" = "Giao thức"
"search" = "Tìm kiếm"
"filter" = "Bộ lọc"
"loading" = "Đang tải"
"second" = "Giây"
"minute" = "Phút"
"hour" = "Giờ"
"day" = "Ngày"
"check" = "Kiểm tra"
"indefinite" = "Không xác định"
"unlimited" = "Không giới hạn"
"none" = "None"
"qrCode" = "Mã QR"
"info" = "Thông tin thêm"
"edit" = "Chỉnh sửa"
"delete" = "Xóa"
"reset" = "Đặt lại"
"noData" = "Không có dữ liệu."
"copySuccess" = "Đã sao chép thành công"
"sure" = "Chắc chắn"
"encryption" = "Mã hóa"
"useIPv4ForHost" = "Sử dụng IPv4 cho máy chủ"
"transmission" = "Truyền tải"
"host" = "Máy chủ"
"path" = "Đường dẫn"
"camouflage" = "Ngụy trang"
"status" = "Trạng thái"
"enabled" = "Đã kích hoạt"
"disabled" = "Đã tắt"
"depleted" = "Depleted"
"depletingSoon" = "Depleting..."
"offline" = "Ngoại tuyến"
"online" = "Trực tuyến"
"domainName" = "Tên miền"
"monitor" = "Listening IP"
"certificate" = "Chứng chỉ số"
"fail" = "Thất bại"
"comment" = "Bình luận"
"success" = "Thành công"
"lastOnline" = "Lần online gần nhất"
"getVersion" = "Lấy phiên bản"
"install" = "Cài đặt"
"clients" = "Các khách hàng"
"usage" = "Sử dụng"
"twoFactorCode" = "Mã"
"remained" = "Còn lại"
"security" = "Bảo vệ"
"secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
"secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
"secAlertConf" = "Một số cài đặt có thể dễ bị tấn công. Đề xuất tăng cường các giao thức bảo mật để ngăn chặn các vi phạm tiềm ẩn."
"secAlertSSL" = "Bảng điều khiển thiếu kết nối an toàn. Vui lòng cài đặt chứng chỉ TLS để bảo vệ dữ liệu."
"secAlertPanelPort" = "Cổng mặc định của bảng điều khiển có thể dễ bị tấn công. Vui lòng cấu hình một cổng ngẫu nhiên hoặc cụ thể."
"secAlertPanelURI" = "Đường dẫn URI mặc định của bảng điều khiển không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
"secAlertSubURI" = "Đường dẫn URI mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
"secAlertSubJsonURI" = "Đường dẫn URI JSON mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
"emptyDnsDesc" = "Không có máy chủ DNS nào được thêm."
"emptyFakeDnsDesc" = "Không có máy chủ Fake DNS nào được thêm."
"emptyBalancersDesc" = "Không có bộ cân bằng tải nào được thêm."
"emptyReverseDesc" = "Không có proxy ngược nào được thêm."
"somethingWentWrong" = "Đã xảy ra lỗi"
[subscription]
"title" = "Thông tin đăng ký"
"subId" = "ID đăng ký"
"status" = "Trạng thái"
"downloaded" = "Đã tải xuống"
"uploaded" = "Đã tải lên"
"expiry" = "Hết hạn"
"totalQuota" = "Tổng hạn mức"
"individualLinks" = "Liên kết riêng lẻ"
"active" = "Hoạt động"
"inactive" = "Không hoạt động"
"unlimited" = "Không giới hạn"
"noExpiry" = "Không hết hạn"
[menu]
"theme" = "Chủ đề"
"dark" = "Tối"
"ultraDark" = "Siêu tối"
"dashboard" = "Trạng thái hệ thống"
"inbounds" = "Đầu vào khách hàng"
"settings" = "Cài đặt bảng điều khiển"
"logout" = "Đăng xuất"
"xray" = "Cài đặt Xray"
"link" = "Quản lý"
[pages.login]
"hello" = "Xin chào"
"title" = "Chào mừng"
"loginAgain" = "Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại."
[pages.login.toasts]
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
"emptyUsername" = "Vui lòng nhập tên người dùng."
"emptyPassword" = "Vui lòng nhập mật khẩu."
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
"successRegister" = "Đăng ký thành công, vui lòng đăng nhập."
"userExists" = "Tên người dùng đã tồn tại"
"errorRegister" = "Đăng ký thất bại"
[pages.index]
"title" = "Trạng thái hệ thống"
"cpu" = "CPU"
"logicalProcessors" = "Bộ xử lý logic"
"frequency" = "Tần số"
"swap" = "Swap"
"storage" = "Lưu trữ"
"memory" = "RAM"
"threads" = "Luồng"
"xrayStatus" = "Xray"
"stopXray" = "Dừng lại"
"restartXray" = "Khởi động lại"
"xraySwitch" = "Phiên bản"
"xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang."
"xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại."
"xrayStatusUnknown" = "Không xác định"
"xrayStatusRunning" = "Đang chạy"
"xrayStatusStop" = "Dừng"
"xrayStatusError" = "Lỗi"
"xrayErrorPopoverTitle" = "Đã xảy ra lỗi khi chạy Xray"
"operationHours" = "Thời gian hoạt động"
"systemLoad" = "Tải hệ thống"
"systemLoadDesc" = "trung bình tải hệ thống trong 1, 5 và 15 phút qua"
"connectionCount" = "Số lượng kết nối"
"ipAddresses" = "Địa chỉ IP"
"toggleIpVisibility" = "Chuyển đổi hiển thị IP"
"overallSpeed" = "Tốc độ tổng thể"
"upload" = "Tải lên"
"download" = "Tải xuống"
"totalData" = "Tổng dữ liệu"
"sent" = "Đã gửi"
"received" = "Đã nhận"
"documentation" = "Tài liệu"
"xraySwitchVersionDialog" = "Bạn có chắc chắn muốn thay đổi phiên bản Xray không?"
"xraySwitchVersionDialogDesc" = "Hành động này sẽ thay đổi phiên bản Xray thành #version#."
"xraySwitchVersionPopover" = "Xray đã được cập nhật thành công"
"geofileUpdateDialog" = "Bạn có chắc chắn muốn cập nhật geofile không?"
"geofileUpdateDialogDesc" = "Hành động này sẽ cập nhật tệp #filename#."
"geofilesUpdateDialogDesc" = "Thao tác này sẽ cập nhật tất cả các tập tin."
"geofilesUpdateAll" = "Cập nhật tất cả"
"geofileUpdatePopover" = "Geofile đã được cập nhật thành công"
"dontRefresh" = "Đang tiến hành cài đặt, vui lòng không làm mới trang này."
"logs" = "Nhật ký"
"config" = "Cấu hình"
"backup" = "Sao lưu"
"backupTitle" = "Sao lưu & Khôi phục Cơ sở dữ liệu"
"exportDatabase" = "Sao lưu"
"exportDatabaseDesc" = "Nhấp để tải xuống tệp .db chứa bản sao lưu cơ sở dữ liệu hiện tại của bạn vào thiết bị."
"importDatabase" = "Khôi phục"
"importDatabaseDesc" = "Nhấp để chọn và tải lên tệp .db từ thiết bị của bạn để khôi phục cơ sở dữ liệu từ bản sao lưu."
"importDatabaseSuccess" = "Đã nhập cơ sở dữ liệu thành công"
"importDatabaseError" = "Lỗi xảy ra khi nhập cơ sở dữ liệu"
"readDatabaseError" = "Lỗi xảy ra khi đọc cơ sở dữ liệu"
"getDatabaseError" = "Lỗi xảy ra khi truy xuất cơ sở dữ liệu"
"getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình"
[pages.inbounds]
"allTimeTraffic" = "Tổng Lưu Lượng"
"allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc"
"title" = "Điểm vào (Inbounds)"
"totalDownUp" = "Tổng tải lên/tải xuống"
"totalUsage" = "Tổng sử dụng"
"inboundCount" = "Số lượng điểm vào"
"operate" = "Thao tác"
"enable" = "Kích hoạt"
"remark" = "Chú thích"
"protocol" = "Giao thức"
"port" = "Cổng"
"portMap" = "Cổng tạo"
"traffic" = "Lưu lượng"
"details" = "Chi tiết"
"transportConfig" = "Giao vận"
"expireDate" = "Ngày hết hạn"
"createdAt" = "Tạo lúc"
"updatedAt" = "Cập nhật"
"resetTraffic" = "Đặt lại lưu lượng"
"addInbound" = "Thêm điểm vào"
"generalActions" = "Hành động chung"
"autoRefresh" = "Tự động làm mới"
"autoRefreshInterval" = "Khoảng thời gian"
"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)"
"deleteInbound" = "Xóa điểm vào (Inbound)"
"deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)"
"deleteClient" = "Xóa người dùng"
"deleteClientContent" = "Bạn có chắc chắn muốn xóa người dùng không?"
"resetTrafficContent" = "Xác nhận đặt lại lưu lượng?"
"copyLink" = "Sao chép liên kết"
"address" = "Địa chỉ"
"network" = "Mạng"
"destinationPort" = "Cổng đích"
"targetAddress" = "Địa chỉ mục tiêu"
"monitorDesc" = "Mặc định để trống"
"meansNoLimit" = "= Không giới hạn (đơn vị: GB)"
"totalFlow" = "Tổng lưu lượng"
"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn"
"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định"
"certificatePath" = "Đường dẫn tập"
"certificateContent" = "Nội dung tập"
"publicKey" = "Khóa công khai"
"privatekey" = "Khóa cá nhân"
"clickOnQRcode" = "Nhấn vào Mã QR để sao chép"
"client" = "Người dùng"
"export" = "Xuất liên kết"
"clone" = "Sao chép"
"cloneInbound" = "Sao chép điểm vào (Inbound)"
"cloneInboundContent" = "Tất cả cài đặt của điểm vào này, trừ Cổng, IP nghe và máy khách, sẽ được áp dụng cho bản sao."
"cloneInboundOk" = "Sao chép"
"resetAllTraffic" = "Đặt lại lưu lượng cho tất cả điểm vào"
"resetAllTrafficTitle" = "Đặt lại lưu lượng cho tất cả điểm vào"
"resetAllTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng cho tất cả điểm vào không?"
"resetInboundClientTraffics" = "Đặt lại lưu lượng toàn bộ người dùng của điểm vào"
"resetInboundClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng của điểm vào"
"resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các người dùng của điểm vào này không?"
"resetAllClientTraffics" = "Đặt lại lưu lượng cho toàn bộ người dùng"
"resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho toàn bộ người dùng"
"resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho toàn bộ người dùng không?"
"delDepletedClients" = "Xóa các người dùng đã cạn kiệt"
"delDepletedClientsTitle" = "Xóa các người dùng đã cạn kiệt"
"delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa toàn bộ người dùng đã cạn kiệt không?"
"email" = "Email"
"emailDesc" = "Vui lòng cung cấp một địa chỉ email duy nhất."
"IPLimit" = "Giới hạn IP"
"IPLimitDesc" = "Vô hiệu hóa điểm vào nếu số lượng vượt quá giá trị đã nhập (nhập 0 để vô hiệu hóa giới hạn IP)."
"IPLimitlog" = "Lịch sử IP"
"IPLimitlogDesc" = "Lịch sử đăng nhập IP (trước khi kích hoạt điểm vào sau khi bị vô hiệu hóa bởi giới hạn IP, bạn nên xóa lịch sử)."
"IPLimitlogclear" = "Xóa Lịch sử"
"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
"telegramDesc" = "Vui lòng cung cấp ID Trò chuyện Telegram. (sử dụng lệnh '/id' trong bot) hoặc (@userinfobot)"
"subscriptionDesc" = "Bạn có thể tìm liên kết gói đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau"
"info" = "Thông tin"
"same" = "Giống nhau"
"inboundData" = "Dữ liệu gửi đến"
"exportInbound" = "Xuất nhập khẩu"
"import" = "Nhập"
"importInbound" = "Nhập inbound"
"periodicTrafficResetTitle" = "Đặt lại lưu lượng"
"periodicTrafficResetDesc" = "Tự động đặt lại bộ đếm lưu lượng theo khoảng thời gian xác định"
"lastReset" = "Đặt lại lần cuối"
[pages.client]
"add" = "Thêm người dùng"
"edit" = "Chỉnh sửa người dùng"
"submitAdd" = "Thêm"
"submitEdit" = "Lưu thay đổi"
"clientCount" = "Số lượng người dùng"
"bulk" = "Thêm hàng loạt"
"method" = "Phương pháp"
"first" = "Đầu tiên"
"last" = "Cuối cùng"
"prefix" = "Tiền tố"
"postfix" = "Hậu tố"
"delayedStart" = "Bắt đầu ở Lần Đầu"
"expireDays" = "Khoảng thời gian"
"days" = "ngày"
"renew" = "Tự động gia hạn"
"renewDesc" = "Tự động gia hạn sau khi hết hạn. (0 = tắt)(đơn vị: ngày)"
[pages.inbounds.periodicTrafficReset]
"never" = "Không bao giờ"
"daily" = "Hàng ngày"
"weekly" = "Hàng tuần"
"monthly" = "Hàng tháng"
[pages.inbounds.toasts]
"obtain" = "Nhận"
"updateSuccess" = "Cập nhật thành công"
"logCleanSuccess" = "Đã xóa nhật ký"
"inboundsUpdateSuccess" = "Đã cập nhật thành công các kết nối inbound"
"inboundUpdateSuccess" = "Đã cập nhật thành công kết nối inbound"
"inboundCreateSuccess" = "Đã tạo thành công kết nối inbound"
"inboundDeleteSuccess" = "Đã xóa thành công kết nối inbound"
"inboundClientAddSuccess" = "Đã thêm client inbound"
"inboundClientDeleteSuccess" = "Đã xóa client inbound"
"inboundClientUpdateSuccess" = "Đã cập nhật client inbound"
"delDepletedClientsSuccess" = "Đã xóa tất cả client hết hạn"
"resetAllClientTrafficSuccess" = "Đã đặt lại toàn bộ lưu lượng client"
"resetAllTrafficSuccess" = "Đã đặt lại toàn bộ lưu lượng"
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
"getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65."
"getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc."
[pages.inbounds.stream.general]
"request" = "Lời yêu cầu"
"response" = "Phản ứng"
"name" = "Tên"
"value" = "Giá trị"
[pages.inbounds.stream.tcp]
"version" = "Phiên bản"
"method" = "Phương pháp"
"path" = "Đường dẫn"
"status" = "Trạng thái"
"statusDescription" = "Tình trạng Mô tả"
"requestHeader" = "Header yêu cầu"
"responseHeader" = "Header phản hồi"
[pages.settings]
"title" = "Cài đặt"
"save" = "Lưu"
"infoDesc" = "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi."
"restartPanel" = "Khởi động lại bảng điều khiển"
"restartPanelDesc" = "Bạn có chắc chắn muốn khởi động lại bảng điều khiển? Nhấn OK để khởi động lại sau 3 giây. Nếu bạn không thể truy cập bảng điều khiển sau khi khởi động lại, vui lòng xem thông tin nhật ký của bảng điều khiển trên máy chủ."
"restartPanelSuccess" = "Đã khởi động lại bảng điều khiển thành công"
"actions" = "Hành động"
"resetDefaultConfig" = "Đặt lại cấu hình mặc định"
"panelSettings" = "Bảng điều khiển"
"securitySettings" = "Bảo mật"
"TGBotSettings" = "Bot Telegram"
"panelListeningIP" = "IP Nghe của bảng điều khiển"
"panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP."
"panelListeningDomain" = "Tên miền của nghe bảng điều khiển"
"panelListeningDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
"panelPort" = "Cổng bảng điều khiển"
"panelPortDesc" = "Cổng được sử dụng để kết nối với bảng điều khiển này"
"publicKeyPath" = "Đường dẫn file chứng chỉ bảng điều khiển"
"publicKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')"
"privateKeyPath" = "Đường dẫn file khóa của chứng chỉ bảng điều khiển"
"privateKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu từ '/')"
"panelUrlPath" = "Đường dẫn gốc URL bảng điều khiển"
"panelUrlPathDesc" = "Phải bắt đầu và kết thúc bằng '/'"
"pageSize" = "Kích thước phân trang"
"pageSizeDesc" = "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt"
"remarkModel" = "Ghi chú mô hình và ký tự phân tách"
"datepicker" = "Kiểu lịch"
"datepickerPlaceholder" = "Chọn ngày"
"datepickerDescription" = "Tác vụ chạy theo lịch trình sẽ chạy theo kiểu lịch này."
"sampleRemark" = "Nhận xét mẫu"
"oldUsername" = "Tên người dùng hiện tại"
"currentPassword" = "Mật khẩu hiện tại"
"newUsername" = "Tên người dùng mới"
"newPassword" = "Mật khẩu mới"
"telegramBotEnable" = "Bật Bot Telegram"
"telegramBotEnableDesc" = "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram"
"telegramToken" = "Token Telegram"
"telegramTokenDesc" = "Bạn phải nhận token từ quản lý bot Telegram @botfather"
"telegramProxy" = "Socks5 Proxy"
"telegramProxyDesc" = "Nếu bạn cần socks5 proxy để kết nối với Telegram. Điều chỉnh cài đặt của nó theo hướng dẫn."
"telegramAPIServer" = "Telegram API Server"
"telegramAPIServerDesc" = "Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định."
"telegramChatId" = "Chat ID Telegram của quản trị viên"
"telegramChatIdDesc" = "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng @userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn."
"telegramNotifyTime" = "Thời gian thông báo của bot Telegram"
"telegramNotifyTimeDesc" = "Sử dụng định dạng thời gian Crontab."
"tgNotifyBackup" = "Sao lưu Cơ sở dữ liệu"
"tgNotifyBackupDesc" = "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo."
"tgNotifyLogin" = "Thông báo Đăng nhập"
"tgNotifyLoginDesc" = "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn."
"sessionMaxAge" = "Thời gian tối đa của phiên"
"sessionMaxAgeDesc" = "Thời gian của phiên đăng nhập (đơn vị: phút)"
"expireTimeDiff" = "Ngưỡng hết hạn cho thông báo"
"expireTimeDiffDesc" = "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)"
"trafficDiff" = "Ngưỡng lưu lượng cho thông báo"
"trafficDiffDesc" = "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)"
"tgNotifyCpu" = "Ngưỡng cảnh báo tỷ lệ CPU"
"tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)"
"timeZone" = "Múi giờ"
"timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này."
"subSettings" = "Gói đăng ký"
"subEnable" = "Bật dịch vụ"
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
"subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập."
"subTitle" = "Tiêu đề Đăng ký"
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
"subSupportUrl" = "URL Hỗ trợ"
"subSupportUrlDesc" = "Liên kết hỗ trợ kỹ thuật hiển thị trong ứng dụng VPN"
"subProfileUrl" = "URL Hồ sơ"
"subProfileUrlDesc" = "Liên kết đến trang web của bạn hiển thị trong ứng dụng VPN"
"subAnnounce" = "Thông báo"
"subAnnounceDesc" = "Văn bản thông báo hiển thị trong ứng dụng VPN"
"subEnableRouting" = "Bật định tuyến"
"subEnableRoutingDesc" = "Cài đặt toàn cục để bật định tuyến trong ứng dụng khách VPN. (Chỉ dành cho Happ)"
"subRoutingRules" = "Quy tắc định tuyến"
"subRoutingRulesDesc" = "Quy tắc định tuyến toàn cầu cho client VPN. (Chỉ dành cho Happ)"
"subListen" = "Listening IP"
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
"subPort" = "Cổng gói đăng ký"
"subPortDesc" = "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ"
"subCertPath" = "Đường dẫn file chứng chỉ gói đăng ký"
"subCertPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')"
"subKeyPath" = "Đường dẫn file khóa của chứng chỉ gói đăng ký"
"subKeyPathDesc" = "Điền vào đường dẫn đầy đủ (bắt đầu với '/')"
"subPath" = "Đường dẫn gốc URL gói đăng ký"
"subPathDesc" = "Phải bắt đầu và kết thúc bằng '/'"
"subDomain" = "Tên miền con"
"subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
"subUpdates" = "Khoảng thời gian cập nhật gói đăng ký"
"subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách"
"subEncrypt" = "Mã hóa cấu hình"
"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong gói đăng ký"
"subShowInfo" = "Hiển thị thông tin sử dụng"
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
"subURI" = "URI proxy trung gian"
"subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
"externalTrafficInformEnable" = "Thông báo giao thông bên ngoài"
"externalTrafficInformEnableDesc" = "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập."
"externalTrafficInformURI" = "URI thông báo lưu lượng truy cập bên ngoài"
"externalTrafficInformURIDesc" = "Cập nhật lưu lượng truy cập được gửi tới URI này."
"fragment" = "Sự phân mảnh"
"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
"fragmentSett" = "Cài đặt phân mảnh"
"noisesDesc" = "Bật Noises."
"noisesSett" = "Cài đặt Noises"
"mux" = "Mux"
"muxDesc" = "Truyền nhiều luồng dữ liệu độc lập trong luồng dữ liệu đã thiết lập."
"muxSett" = "Mux Cài đặt"
"direct" = "Kết nối trực tiếp"
"directDesc" = "Trực tiếp thiết lập kết nối với tên miền hoặc dải IP của một quốc gia cụ thể."
"notifications" = "Thông báo"
"certs" = "Chứng chỉ"
"externalTraffic" = "Lưu lượng bên ngoài"
"dateAndTime" = "Ngày và giờ"
"proxyAndServer" = "Proxy và máy chủ"
"intervals" = "Khoảng thời gian"
"information" = "Thông tin"
"language" = "Ngôn ngữ"
"telegramBotLanguage" = "Ngôn ngữ của Bot Telegram"
[pages.xray]
"title" = "Cài đặt Xray"
"save" = "Lưu cài đặt"
"restart" = "Khởi động lại Xray"
"restartSuccess" = "Đã khởi động lại Xray thành công"
"stopSuccess" = "Xray đã được dừng thành công"
"restartError" = "Đã xảy ra lỗi khi khởi động lại Xray."
"stopError" = "Đã xảy ra lỗi khi dừng Xray."
"basicTemplate" = "Mẫu Cơ bản"
"advancedTemplate" = "Mẫu Nâng cao"
"generalConfigs" = "Cấu hình Chung"
"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát."
"logConfigs" = "Nhật ký"
"logConfigsDesc" = "Nhật ký có thể ảnh hưởng đến hiệu suất máy chủ của bạn. Bạn chỉ nên kích hoạt nó một cách khôn ngoan trong trường hợp bạn cần"
"blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể."
"basicRouting" = "Định tuyến Cơ bản"
"blockConnectionsConfigsDesc" = "Các tùy chọn này sẽ chặn lưu lượng truy cập dựa trên quốc gia được yêu cầu cụ thể."
"directConnectionsConfigsDesc" = "Kết nối trực tiếp đảm bảo rằng lưu lượng truy cập cụ thể không được định tuyến qua máy chủ khác."
"blockips" = "Chặn IP"
"blockdomains" = "Chặn Tên Miền"
"directips" = "IP Trực Tiếp"
"directdomains" = "Tên Miền Trực Tiếp"
"ipv4Routing" = "Định tuyến IPv4"
"ipv4RoutingDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4."
"warpRouting" = "Định tuyến WARP"
"warpRoutingDesc" = "Cảnh báo: Trước khi sử dụng những tùy chọn này, hãy cài đặt WARP ở chế độ proxy socks5 trên máy chủ của bạn bằng cách làm theo các bước trên GitHub của bảng điều khiển. WARP sẽ định tuyến lưu lượng đến các trang web qua máy chủ Cloudflare."
"Template" = "Mẫu Cấu hình Xray"
"TemplateDesc" = "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này."
"FreedomStrategy" = "Cấu hình Chiến lược cho Giao thức Freedom"
"FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Freedom."
"RoutingStrategy" = "Cấu hình Chiến lược Định tuyến Tên miền"
"RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể cho việc giải quyết DNS."
"outboundTestUrl" = "URL kiểm tra outbound"
"outboundTestUrlDesc" = "URL dùng khi kiểm tra kết nối outbound"
"Torrent" = "Cấu hình sử dụng BitTorrent"
"Inbounds" = "Đầu vào"
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
"Outbounds" = "Đầu ra"
"Balancers" = "Cân bằng"
"OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
"Routings" = "Quy tắc định tuyến"
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
"completeTemplate" = "All"
"logLevel" = "Mức đăng nhập"
"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại."
"accessLog" = "Nhật ký truy cập"
"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'"
"errorLog" = "Nhật ký lỗi"
"errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'"
"dnsLog" = "Nhật ký DNS"
"dnsLogDesc" = "Có bật nhật ký truy vấn DNS không"
"maskAddress" = "Ẩn Địa Chỉ"
"maskAddressDesc" = "Mặt nạ địa chỉ IP, khi được bật, sẽ tự động thay thế địa chỉ IP xuất hiện trong nhật ký."
"statistics" = "Thống kê"
"statsInboundUplink" = "Thống kê tải lên đầu vào"
"statsInboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu vào."
"statsInboundDownlink" = "Thống kê tải xuống đầu vào"
"statsInboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu vào."
"statsOutboundUplink" = "Thống kê tải lên đầu ra"
"statsOutboundUplinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải lên của tất cả các proxy đầu ra."
"statsOutboundDownlink" = "Thống kê tải xuống đầu ra"
"statsOutboundDownlinkDesc" = "Kích hoạt thu thập thống kê cho lưu lượng tải xuống của tất cả các proxy đầu ra."
[pages.xray.rules]
"first" = "Đầu tiên"
"last" = "Cuối cùng"
"up" = "Lên"
"down" = "Xuống"
"source" = "Nguồn"
"dest" = "Đích"
"inbound" = "Vào"
"outbound" = "Ra"
"balancer" = "Cân bằng"
"info" = "Thông tin"
"add" = "Thêm quy tắc"
"edit" = "Chỉnh sửa quy tắc"
"useComma" = "Các mục được phân tách bằng dấu phẩy"
[pages.xray.outbound]
"addOutbound" = "Thêm thư đi"
"addReverse" = "Thêm đảo ngược"
"editOutbound" = "Chỉnh sửa gửi đi"
"editReverse" = "Chỉnh sửa ngược lại"
"tag" = "Thẻ"
"tagDesc" = "thẻ duy nhất"
"address" = "Địa chỉ"
"reverse" = "Đảo ngược"
"domain" = "Miền"
"type" = "Loại"
"bridge" = "Cầu"
"portal" = "Cổng thông tin"
"link" = "Liên kết"
"intercon" = "Kết nối"
"settings" = "cài đặt"
"accountInfo" = "Thông tin tài khoản"
"outboundStatus" = "Trạng thái đầu ra"
"sendThrough" = "Gửi qua"
"test" = "Kiểm tra"
"testResult" = "Kết quả kiểm tra"
"testing" = "Đang kiểm tra kết nối..."
"testSuccess" = "Kiểm tra thành công"
"testFailed" = "Kiểm tra thất bại"
"testError" = "Không thể kiểm tra đầu ra"
[pages.xray.balancer]
"addBalancer" = "Thêm cân bằng"
"editBalancer" = "Chỉnh sửa cân bằng"
"balancerStrategy" = "Chiến lược"
"balancerSelectors" = "Bộ chọn"
"tag" = "Thẻ"
"tagDesc" = "thẻ duy nhất"
"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động."
[pages.xray.wireguard]
"secretKey" = "Khoá bí mật"
"publicKey" = "Khóa công khai"
"allowedIPs" = "IP được phép"
"endpoint" = "Điểm cuối"
"psk" = "Khóa chia sẻ"
"domainStrategy" = "Chiến lược tên miền"
[pages.xray.tun]
"nameDesc" = "Tên của giao diện TUN. Giá trị mặc định là 'xray0'"
"mtuDesc" = "Đơn vị Truyền Tối đa. Kích thước tối đa của các gói dữ liệu. Giá trị mặc định là 1500"
"userLevel" = "Mức Người Dùng"
"userLevelDesc" = "Tất cả các kết nối được thực hiện thông qua inbound này sẽ sử dụng mức người dùng này. Giá trị mặc định là 0"
[pages.xray.dns]
"enable" = "Kích hoạt DNS"
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"
"tag" = "Thẻ gửi đến DNS"
"tagDesc" = "Thẻ này sẽ có sẵn dưới dạng thẻ Gửi đến trong quy tắc định tuyến."
"clientIp" = "IP khách hàng"
"clientIpDesc" = "Được sử dụng để thông báo cho máy chủ về vị trí IP được chỉ định trong các truy vấn DNS"
"disableCache" = "Tắt bộ nhớ đệm"
"disableCacheDesc" = "Tắt bộ nhớ đệm DNS"
"disableFallback" = "Tắt Fallback"
"disableFallbackDesc" = "Tắt các truy vấn DNS Fallback"
"disableFallbackIfMatch" = "Tắt Fallback Nếu Khớp"
"disableFallbackIfMatchDesc" = "Tắt các truy vấn DNS Fallback khi danh sách tên miền khớp của máy chủ DNS được kích hoạt"
"enableParallelQuery" = "Bật Truy vấn Song song"
"enableParallelQueryDesc" = "Bật truy vấn DNS song song đến nhiều máy chủ để phân giải nhanh hơn"
"strategy" = "Chiến lược truy vấn"
"strategyDesc" = "Chiến lược tổng thể để phân giải tên miền"
"add" = "Thêm máy chủ"
"edit" = "Chỉnh sửa máy chủ"
"domains" = "Tên miền"
"expectIPs" = "Các IP Dự Kiến"
"unexpectIPs" = "IP không mong muốn"
"useSystemHosts" = "Sử dụng Hosts hệ thống"
"useSystemHostsDesc" = "Sử dụng file hosts từ hệ thống đã cài đặt"
"usePreset" = "Dùng mẫu"
"dnsPresetTitle" = "Mẫu DNS"
"dnsPresetFamily" = "Gia đình"
[pages.xray.fakedns]
"add" = "Thêm DNS giả"
"edit" = "Chỉnh sửa DNS giả"
"ipPool" = "Mạng con nhóm IP"
"poolSize" = "Kích thước bể bơi"
[pages.settings.security]
"admin" = "Thông tin đăng nhập quản trị viên"
"twoFactor" = "Xác thực hai yếu tố"
"twoFactorEnable" = "Bật 2FA"
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
"twoFactorModalFirstStep" = "1. Quét mã QR này trong ứng dụng xác thực hoặc sao chép mã token gần mã QR và dán vào ứng dụng"
"twoFactorModalSecondStep" = "2. Nhập mã từ ứng dụng"
"twoFactorModalRemoveStep" = "Nhập mã từ ứng dụng để xóa xác thực hai yếu tố."
"twoFactorModalChangeCredentialsTitle" = "Thay đổi thông tin xác thực"
"twoFactorModalChangeCredentialsStep" = "Nhập mã từ ứng dụng để thay đổi thông tin xác thực quản trị viên."
"twoFactorModalSetSuccess" = "Xác thực hai yếu tố đã được thiết lập thành công"
"twoFactorModalDeleteSuccess" = "Xác thực hai yếu tố đã được xóa thành công"
"twoFactorModalError" = "Mã sai"
[pages.settings.toasts]
"modifySettings" = "Các tham số đã được thay đổi."
"getSettings" = "Lỗi xảy ra khi truy xuất tham số."
"modifyUserError" = "Đã xảy ra lỗi khi thay đổi thông tin đăng nhập quản trị viên."
"modifyUser" = "Bạn đã thay đổi thông tin đăng nhập quản trị viên thành công."
"originalUserPassIncorrect" = "Tên người dùng hoặc mật khẩu gốc không đúng"
"userPassMustBeNotEmpty" = "Tên người dùng mới và mật khẩu mới không thể để trống"
"getOutboundTrafficError" = "Lỗi khi lấy lưu lượng truy cập đi"
"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
[tgbot]
"keyboardClosed" = "❌ Bàn phím đã đóng!"
"noResult" = "❗ Không có kết quả!"
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
"wentWrong" = "❌ Đã xảy ra lỗi!"
"noIpRecord" = "❗ Không có bản ghi IP!"
"noInbounds" = "❗ Không tìm thấy inbound!"
"unlimited" = "♾ Không giới hạn (Đặt lại)"
"add" = "Thêm"
"month" = "Tháng"
"months" = "Tháng"
"day" = "Ngày"
"days" = "Ngày"
"hours" = "Giờ"
"minutes" = "Phút"
"unknown" = "Không xác định"
"inbounds" = "Inbound"
"clients" = "Client"
"offline" = "🔴 Ngoại tuyến"
"online" = "🟢 Trực tuyến"
[tgbot.commands]
"unknown" = "❗ Lệnh không rõ"
"pleaseChoose" = "👇 Vui lòng chọn:\r\n"
"help" = "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n"
"start" = "👋 Xin chào <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Chào mừng đến với bot quản lý của <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Bot hoạt động bình thường!"
"usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!"
"getID" = "🆔 ID của bạn: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Để khởi động lại Xray Core:\r\n<code>/restart</code>\r\n\r\nĐể tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n\r\nĐể tìm kiếm các nhập (với số liệu thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>"
"helpClientCommands" = "Để tìm kiếm thống kê, sử dụng lệnh sau:\r\n<code>/usage [Email]</code>\r\n\r\nID Trò chuyện Telegram:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Hoạt động thành công!"
"restartFailed" = "❗ Lỗi trong quá trình hoạt động.\r\n\r\n<code>Lỗi: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core không chạy."
"startDesc" = "Hiển thị menu chính"
"helpDesc" = "Trợ giúp bot"
"statusDesc" = "Kiểm tra trạng thái bot"
"idDesc" = "Hiển thị ID Telegram của bạn"
[tgbot.messages]
"cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
"selectUserFailed" = "❌ Lỗi khi chọn người dùng!"
"userSaved" = "✅ Người dùng Telegram đã được lưu."
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
"loginFailed" = "❗️ Đăng nhập vào bảng điều khiển thất bại.\r\n"
"2faFailed" = "Lỗi 2FA"
"report" = "🕰 Báo cáo định kỳ: {{ .RunTime }}\r\n"
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
"version" = "🚀 Phiên bản X-UI: {{ .Version }}\r\n"
"xrayVersion" = "📡 Phiên bản Xray: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 Các IP:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 Số lượng kết nối TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 Số lượng kết nối UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Lưu lượng: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Trạng thái Xray: {{ .State }}\r\n"
"username" = "👤 Tên người dùng: {{ .Username }}\r\n"
"password" = "👤 Mật khẩu: {{ .Password }}\r\n"
"time" = "⏰ Thời gian: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Cổng: {{ .Port }}\r\n"
"expire" = "📅 Ngày hết hạn: {{ .Time }}\r\n"
"expireIn" = "📅 Hết hạn sau: {{ .Time }}\r\n"
"active" = "💡 Đang hoạt động: {{ .Enable }}\r\n"
"enabled" = "🚨 Đã bật: {{ .Enable }}\r\n"
"online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n"
"lastOnline" = "🔙 Lần online gần nhất: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n"
"download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n"
"total" = "📊 Tổng cộng: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Người dùng Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Sự cạn kiệt {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Số lần cạn kiệt {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Khách hàng trực tuyến: {{ .Count }}\r\n"
"disabled" = "🛑 Vô hiệu hóa: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Sắp cạn kiệt: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Đã cập nhật lần cuối vào: {{ .Time }}\r\n\r\n"
"yes" = "✅ Có"
"no" = "❌ Không"
"received_id" = "🔑📥 ID đã được cập nhật."
"received_password" = "🔑📥 Mật khẩu đã được cập nhật."
"received_email" = "📧📥 Email đã được cập nhật."
"received_comment" = "💬📥 Bình luận đã được cập nhật."
"id_prompt" = "🔑 ID mặc định: {{ .ClientId }}\n\nVui lòng nhập ID của bạn."
"pass_prompt" = "🔑 Mật khẩu mặc định: {{ .ClientPassword }}\n\nVui lòng nhập mật khẩu của bạn."
"email_prompt" = "📧 Email mặc định: {{ .ClientEmail }}\n\nVui lòng nhập email của bạn."
"comment_prompt" = "💬 Bình luận mặc định: {{ .ClientComment }}\n\nVui lòng nhập bình luận của bạn."
"inbound_client_data_id" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!"
"inbound_client_data_pass" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 Mật khẩu: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!"
"cancel" = "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄"
"error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}"
"using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊"
"incorrect_input" = "Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫"
"AreYouSure" = "Bạn có chắc không? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠 Lỗi: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 Quá trình đặt lại lưu lượng đã hoàn tất cho tất cả khách hàng."
[tgbot.buttons]
"closeKeyboard" = "❌ Đóng Bàn Phím"
"cancel" = "❌ Hủy"
"cancelReset" = "❌ Hủy Đặt Lại"
"cancelIpLimit" = "❌ Hủy Giới Hạn IP"
"confirmResetTraffic" = "✅ Xác Nhận Đặt Lại Lưu Lượng?"
"confirmClearIps" = "✅ Xác Nhận Xóa Các IP?"
"confirmRemoveTGUser" = "✅ Xác Nhận Xóa Người Dùng Telegram?"
"confirmToggle" = "✅ Xác nhận Bật/Tắt người dùng?"
"dbBackup" = "Tải bản sao lưu cơ sở dữ liệu"
"serverUsage" = "Sử Dụng Máy Chủ"
"getInbounds" = "Lấy cổng vào"
"depleteSoon" = "Depleted Soon"
"clientUsage" = "Lấy Sử Dụng"
"onlines" = "Khách hàng trực tuyến"
"commands" = "Lệnh"
"refresh" = "🔄 Cập Nhật"
"clearIPs" = "❌ Xóa IP"
"removeTGUser" = "❌ Xóa Người Dùng Telegram"
"selectTGUser" = "👤 Chọn Người Dùng Telegram"
"selectOneTGUser" = "👤 Chọn một người dùng telegram:"
"resetTraffic" = "📈 Đặt Lại Lưu Lượng"
"resetExpire" = "📅 Thay đổi ngày hết hạn"
"ipLog" = "🔢 Nhật ký địa chỉ IP"
"ipLimit" = "🔢 Giới Hạn địa chỉ IP"
"setTGUser" = "👤 Đặt Người Dùng Telegram"
"toggle" = "🔘 Bật / Tắt"
"custom" = "🔢 Tùy chỉnh"
"confirmNumber" = "✅ Xác nhận: {{ .Num }}"
"confirmNumberAdd" = "✅ Xác nhận thêm: {{ .Num }}"
"limitTraffic" = "🚧 Giới hạn lưu lượng"
"getBanLogs" = "Cấm nhật ký"
"allClients" = "Tất cả Khách hàng"
"addClient" = "Thêm Khách Hàng"
"submitDisable" = "Gửi Dưới Dạng Vô Hiệu ☑️"
"submitEnable" = "Gửi Dưới Dạng Kích Hoạt ✅"
"use_default" = "🏷️ Sử Dụng Mặc Định"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Mật Khẩu"
"change_email" = "⚙️📧 Email"
"change_comment" = "⚙️💬 Bình Luận"
"ResetAllTraffics" = "Đặt lại tất cả lưu lượng"
"SortedTrafficUsageReport" = "Báo cáo sử dụng lưu lượng đã sắp xếp"
[tgbot.answers]
"successfulOperation" = "✅ Thành công!"
"errorOperation" = "❗ Lỗi Trong Quá Trình Thực Hiện."
"getInboundsFailed" = "❌ Không Thể Lấy Được Inbounds"
"getClientsFailed" = "❌ Không thể lấy khách hàng."
"canceled" = "❌ {{ .Email }} : Thao Tác Đã Bị Hủy."
"clientRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Khách Hàng."
"IpRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho IPs."
"TGIdRefreshSuccess" = "✅ {{ .Email }} : Cập Nhật Thành Công Cho Người Dùng Telegram."
"resetTrafficSuccess" = "✅ {{ .Email }} : Đặt Lại Lưu Lượng Thành Công."
"setTrafficLimitSuccess" = "✅ {{ .Email }} : Đã lưu thành công giới hạn lưu lượng."
"expireResetSuccess" = "✅ {{ .Email }} : Đặt Lại Ngày Hết Hạn Thành Công."
"resetIpSuccess" = "✅ {{ .Email }} : Giới Hạn IP {{ .Count }} Đã Được Lưu Thành Công."
"clearIpSuccess" = "✅ {{ .Email }} : IP Đã Được Xóa Thành Công."
"getIpLog" = "✅ {{ .Email }} : Lấy nhật ký IP Thành Công."
"getUserInfo" = "✅ {{ .Email }} : Lấy Thông Tin Người Dùng Telegram Thành Công."
"removedTGUserSuccess" = "✅ {{ .Email }} : Người Dùng Telegram Đã Được Xóa Thành Công."
"enableSuccess" = "✅ {{ .Email }} : Đã Bật Thành Công."
"disableSuccess" = "✅ {{ .Email }} : Đã Tắt Thành Công."
"askToAddUserId" = "Cấu hình của bạn không được tìm thấy!\r\nVui lòng yêu cầu Quản trị viên sử dụng ID người dùng telegram của bạn trong cấu hình của bạn.\r\n\r\nID người dùng của bạn: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Chọn một Khách hàng cho Inbound {{ .Inbound }}"
"chooseInbound" = "Chọn một Inbound"

View file

@ -116,6 +116,8 @@
"successRegister" = "注册成功,请登录。"
"userExists" = "用户名已存在"
"errorRegister" = "注册失败"
"invalidUsername" = "用户名长度必须为3-64个字符"
"invalidPassword" = "密码长度必须为8-128个字符"
[pages.index]
"title" = "系统状态"
@ -315,6 +317,17 @@
"requestHeader" = "请求头"
"responseHeader" = "响应头"
[pages.user]
"title" = "用户面板"
"username" = "用户名"
"upload" = "上传"
"download" = "下载"
"totalTraffic" = "总流量"
"expiryTime" = "到期时间"
"lastOnline" = "上次在线"
"remained" = "剩余流量"
"status" = "状态"
[pages.settings]
"title" = "面板设置"
"save" = "保存"

View file

@ -1,795 +0,0 @@
"username" = "使用者名稱"
"password" = "密碼"
"login" = "登入"
"confirm" = "確定"
"cancel" = "取消"
"close" = "關閉"
"create" = "建立"
"update" = "更新"
"copy" = "複製"
"copied" = "已複製"
"download" = "下載"
"remark" = "備註"
"enable" = "啟用"
"protocol" = "協議"
"search" = "搜尋"
"filter" = "篩選"
"loading" = "載入中..."
"second" = "秒"
"minute" = "分鐘"
"hour" = "小時"
"day" = "天"
"check" = "檢視"
"indefinite" = "無限期"
"unlimited" = "無限制"
"none" = "無"
"qrCode" = "二維碼"
"info" = "更多資訊"
"edit" = "編輯"
"delete" = "刪除"
"reset" = "重置"
"noData" = "無數據。"
"copySuccess" = "複製成功"
"sure" = "確定"
"encryption" = "加密"
"useIPv4ForHost" = "使用 IPv4 連接主機"
"transmission" = "傳輸"
"host" = "主機"
"path" = "路徑"
"camouflage" = "偽裝"
"status" = "狀態"
"enabled" = "開啟"
"disabled" = "關閉"
"depleted" = "耗盡"
"depletingSoon" = "即將耗盡"
"offline" = "離線"
"online" = "線上"
"domainName" = "域名"
"monitor" = "監聽"
"certificate" = "憑證"
"fail" = "失敗"
"comment" = "評論"
"success" = "成功"
"lastOnline" = "上次上線"
"getVersion" = "獲取版本"
"install" = "安裝"
"clients" = "客戶端"
"usage" = "使用情況"
"twoFactorCode" = "代碼"
"remained" = "剩餘"
"security" = "安全"
"secAlertTitle" = "安全警報"
"secAlertSsl" = "此連線不安全。在啟用 TLS 進行資料保護之前,請勿輸入敏感資訊。"
"secAlertConf" = "某些設定易受攻擊。建議加強安全協議以防止潛在漏洞。"
"secAlertSSL" = "面板缺少安全連線。請安裝 TLS 證書以保護資料安全。"
"secAlertPanelPort" = "面板預設埠存在安全風險。請配置隨機埠或特定埠。"
"secAlertPanelURI" = "面板預設 URI 路徑不安全。請配置複雜的 URI 路徑。"
"secAlertSubURI" = "訂閱預設 URI 路徑不安全。請配置複雜的 URI 路徑。"
"secAlertSubJsonURI" = "訂閱 JSON 預設 URI 路徑不安全。請配置複雜的 URI 路徑。"
"emptyDnsDesc" = "未添加DNS伺服器。"
"emptyFakeDnsDesc" = "未添加Fake DNS伺服器。"
"emptyBalancersDesc" = "未添加負載平衡器。"
"emptyReverseDesc" = "未添加反向代理。"
"somethingWentWrong" = "發生錯誤"
[subscription]
"title" = "訂閱資訊"
"subId" = "訂閱 ID"
"status" = "狀態"
"downloaded" = "已下載"
"uploaded" = "已上傳"
"expiry" = "到期"
"totalQuota" = "總配額"
"individualLinks" = "個別連結"
"active" = "啟用"
"inactive" = "停用"
"unlimited" = "無限制"
"noExpiry" = "無到期"
[menu]
"theme" = "主題"
"dark" = "深色"
"ultraDark" = "超深色"
"dashboard" = "系統狀態"
"inbounds" = "入站列表"
"settings" = "面板設定"
"xray" = "Xray 設定"
"logout" = "退出登入"
"link" = "管理"
[pages.login]
"hello" = "你好"
"title" = "歡迎"
"loginAgain" = "登入時效已過,請重新登入"
[pages.login.toasts]
"invalidFormData" = "資料格式錯誤"
"emptyUsername" = "請輸入使用者名稱"
"emptyPassword" = "請輸入密碼"
"wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。"
"successLogin" = "您已成功登入您的帳戶。"
"successRegister" = "註冊成功,請登入。"
"userExists" = "使用者名稱已存在"
"errorRegister" = "註冊失敗"
[pages.index]
"title" = "系統狀態"
"cpu" = "CPU"
"logicalProcessors" = "邏輯處理器"
"frequency" = "頻率"
"swap" = "交換空間"
"storage" = "儲存"
"memory" = "記憶體"
"threads" = "執行緒"
"xrayStatus" = "Xray"
"stopXray" = "停止"
"restartXray" = "重啟"
"xraySwitch" = "版本"
"xraySwitchClick" = "選擇你要切換到的版本"
"xraySwitchClickDesk" = "請謹慎選擇,因為較舊版本可能與當前配置不相容"
"xrayStatusUnknown" = "未知"
"xrayStatusRunning" = "運行中"
"xrayStatusStop" = "停止"
"xrayStatusError" = "錯誤"
"xrayErrorPopoverTitle" = "執行Xray時發生錯誤"
"operationHours" = "系統正常執行時間"
"systemLoad" = "系統負載"
"systemLoadDesc" = "過去 1、5 和 15 分鐘的系統平均負載"
"connectionCount" = "連線數"
"ipAddresses" = "IP地址"
"toggleIpVisibility" = "切換IP可見性"
"overallSpeed" = "整體速度"
"upload" = "上傳"
"download" = "下載"
"totalData" = "總數據"
"sent" = "已發送"
"received" = "已接收"
"documentation" = "文件"
"xraySwitchVersionDialog" = "您確定要變更Xray版本嗎"
"xraySwitchVersionDialogDesc" = "這將會把Xray版本變更為#version#。"
"xraySwitchVersionPopover" = "Xray 更新成功"
"geofileUpdateDialog" = "您確定要更新地理檔案嗎?"
"geofileUpdateDialogDesc" = "這將更新 #filename# 檔案。"
"geofilesUpdateDialogDesc" = "這將更新所有文件。"
"geofilesUpdateAll" = "全部更新"
"geofileUpdatePopover" = "地理檔案更新成功"
"dontRefresh" = "安裝中,請勿重新整理此頁面"
"logs" = "日誌"
"config" = "配置"
"backup" = "備份和恢復"
"backupTitle" = "備份和恢復資料庫"
"exportDatabase" = "備份"
"exportDatabaseDesc" = "點擊下載包含當前資料庫備份的 .db 文件到您的設備。"
"importDatabase" = "恢復"
"importDatabaseDesc" = "點擊選擇並上傳設備中的 .db 文件以從備份恢復資料庫。"
"importDatabaseSuccess" = "資料庫匯入成功"
"importDatabaseError" = "匯入資料庫時發生錯誤"
"readDatabaseError" = "讀取資料庫時發生錯誤"
"getDatabaseError" = "檢索資料庫時發生錯誤"
"getConfigError" = "檢索設定檔時發生錯誤"
[pages.inbounds]
"allTimeTraffic" = "累計總流量"
"allTimeTrafficUsage" = "所有时间总使用量"
"title" = "入站列表"
"totalDownUp" = "總上傳 / 下載"
"totalUsage" = "總用量"
"inboundCount" = "入站數量"
"operate" = "選單"
"enable" = "啟用"
"remark" = "備註"
"protocol" = "協議"
"port" = "埠"
"portMap" = "埠映射"
"traffic" = "流量"
"details" = "詳細資訊"
"transportConfig" = "傳輸配置"
"expireDate" = "到期時間"
"createdAt" = "建立時間"
"updatedAt" = "更新時間"
"resetTraffic" = "重置流量"
"addInbound" = "新增入站"
"generalActions" = "通用操作"
"autoRefresh" = "自動刷新"
"autoRefreshInterval" = "間隔"
"modifyInbound" = "修改入站"
"deleteInbound" = "刪除入站"
"deleteInboundContent" = "確定要刪除入站嗎?"
"deleteClient" = "刪除客戶端"
"deleteClientContent" = "確定要刪除客戶端嗎?"
"resetTrafficContent" = "確定要重置流量嗎?"
"copyLink" = "複製連結"
"address" = "地址"
"network" = "網路"
"destinationPort" = "目標埠"
"targetAddress" = "目標地址"
"monitorDesc" = "留空表示監聽所有 IP"
"meansNoLimit" = "= 無限制單位GB)"
"totalFlow" = "總流量"
"leaveBlankToNeverExpire" = "留空表示永不過期"
"noRecommendKeepDefault" = "建議保留預設值"
"certificatePath" = "檔案路徑"
"certificateContent" = "檔案內容"
"publicKey" = "公鑰"
"privatekey" = "私鑰"
"clickOnQRcode" = "點選二維碼複製"
"client" = "客戶"
"export" = "匯出連結"
"clone" = "複製"
"cloneInbound" = "複製"
"cloneInboundContent" = "此入站規則除埠Port、監聽 IPListening IP和客戶端Clients以外的所有配置都將應用於克隆"
"cloneInboundOk" = "建立克隆"
"resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "確定要重置所有入站流量嗎?"
"resetInboundClientTraffics" = "重置客戶端流量"
"resetInboundClientTrafficTitle" = "重置所有客戶端流量"
"resetInboundClientTrafficContent" = "確定要重置此入站客戶端的所有流量嗎?"
"resetAllClientTraffics" = "重置所有客戶端流量"
"resetAllClientTrafficTitle" = "重置所有客戶端流量"
"resetAllClientTrafficContent" = "確定要重置所有客戶端的所有流量嗎?"
"delDepletedClients" = "刪除流量耗盡的客戶端"
"delDepletedClientsTitle" = "刪除流量耗盡的客戶端"
"delDepletedClientsContent" = "確定要刪除所有流量耗盡的客戶端嗎?"
"email" = "電子郵件"
"emailDesc" = "電子郵件必須完全唯一"
"IPLimit" = "IP 限制"
"IPLimitDesc" = "如果數量超過設定值則禁用入站流量。0 = 禁用)"
"IPLimitlog" = "IP 日誌"
"IPLimitlogDesc" = "IP 歷史日誌(要啟用被禁用的入站流量,請清除日誌)"
"IPLimitlogclear" = "清除日誌"
"setDefaultCert" = "從面板設定證書"
"telegramDesc" = "請提供Telegram聊天ID。在機器人中使用'/id'命令)或(@userinfobot"
"subscriptionDesc" = "要找到你的訂閱 URL請導航到“詳細資訊”。此外你可以為多個客戶端使用相同的名稱。"
"info" = "資訊"
"same" = "相同"
"inboundData" = "入站資料"
"exportInbound" = "匯出入站規則"
"import" = "匯入"
"importInbound" = "匯入入站規則"
"periodicTrafficResetTitle" = "流量重置"
"periodicTrafficResetDesc" = "按指定間隔自動重置流量計數器"
"lastReset" = "上次重置"
[pages.client]
"add" = "新增客戶端"
"edit" = "編輯客戶端"
"submitAdd" = "新增客戶端"
"submitEdit" = "儲存修改"
"clientCount" = "客戶端數量"
"bulk" = "批量建立"
"method" = "方法"
"first" = "置頂"
"last" = "置底"
"prefix" = "字首"
"postfix" = "字尾"
"delayedStart" = "首次使用後開始"
"expireDays" = "期間"
"days" = "天"
"renew" = "自動續訂"
"renewDesc" = "到期後自動續訂。(0 = 禁用)(單位: 天)"
[pages.inbounds.periodicTrafficReset]
"never" = "從不"
"daily" = "每日"
"weekly" = "每週"
"monthly" = "每月"
[pages.inbounds.toasts]
"obtain" = "獲取"
"updateSuccess" = "更新成功"
"logCleanSuccess" = "日誌已清除"
"inboundsUpdateSuccess" = "入站連接已成功更新"
"inboundUpdateSuccess" = "入站連接已成功更新"
"inboundCreateSuccess" = "入站連接已成功建立"
"inboundDeleteSuccess" = "入站連接已成功刪除"
"inboundClientAddSuccess" = "已新增入站客戶端"
"inboundClientDeleteSuccess" = "入站客戶端已刪除"
"inboundClientUpdateSuccess" = "入站客戶端已更新"
"delDepletedClientsSuccess" = "所有耗盡客戶端已刪除"
"resetAllClientTrafficSuccess" = "客戶端所有流量已重置"
"resetAllTrafficSuccess" = "所有流量已重置"
"resetInboundClientTrafficSuccess" = "流量已重置"
"trafficGetError" = "取得流量資料時發生錯誤"
"getNewX25519CertError" = "取得X25519憑證時發生錯誤。"
"getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。"
"getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。"
[pages.inbounds.stream.general]
"request" = "請求"
"response" = "響應"
"name" = "名稱"
"value" = "值"
[pages.inbounds.stream.tcp]
"version" = "版本"
"method" = "方法"
"path" = "路徑"
"status" = "狀態"
"statusDescription" = "狀態說明"
"requestHeader" = "請求頭"
"responseHeader" = "響應頭"
[pages.settings]
"title" = "面板設定"
"save" = "儲存"
"infoDesc" = "此處的所有更改都需要儲存並重啟面板才能生效"
"restartPanel" = "重啟面板"
"restartPanelDesc" = "確定要重啟面板嗎?若重啟後無法訪問面板,請前往伺服器檢視面板日誌資訊"
"restartPanelSuccess" = "面板已成功重新啟動"
"actions" = "操作"
"resetDefaultConfig" = "重置為預設配置"
"panelSettings" = "常規"
"securitySettings" = "安全設定"
"TGBotSettings" = "Telegram 機器人配置"
"panelListeningIP" = "面板監聽 IP"
"panelListeningIPDesc" = "預設留空監聽所有 IP"
"panelListeningDomain" = "面板監聽域名"
"panelListeningDomainDesc" = "預設情況下留空以監視所有域名和 IP 地址"
"panelPort" = "面板監聽埠"
"panelPortDesc" = "重啟面板生效"
"publicKeyPath" = "面板證書公鑰檔案路徑"
"publicKeyPathDesc" = "填寫一個 '/' 開頭的絕對路徑"
"privateKeyPath" = "面板證書金鑰檔案路徑"
"privateKeyPathDesc" = "填寫一個 '/' 開頭的絕對路徑"
"panelUrlPath" = "面板 url 根路徑"
"panelUrlPathDesc" = "必須以 '/' 開頭,以 '/' 結尾"
"pageSize" = "分頁大小"
"pageSizeDesc" = "定義入站表的頁面大小。設定 0 表示禁用"
"remarkModel" = "備註模型和分隔符"
"datepicker" = "日期選擇器"
"datepickerPlaceholder" = "選擇日期"
"datepickerDescription" = "選擇器日曆類型指定到期日期"
"sampleRemark" = "備註示例"
"oldUsername" = "原使用者名稱"
"currentPassword" = "原密碼"
"newUsername" = "新使用者名稱"
"newPassword" = "新密碼"
"telegramBotEnable" = "啟用 Telegram 機器人"
"telegramBotEnableDesc" = "啟用 Telegram 機器人功能"
"telegramToken" = "Telegram 機器人令牌token"
"telegramTokenDesc" = "從 '@BotFather' 獲取的 Telegram 機器人令牌"
"telegramProxy" = "SOCKS5 Proxy"
"telegramProxyDesc" = "啟用 SOCKS5 代理連線到 Telegram根據指南調整設定"
"telegramAPIServer" = "Telegram API Server"
"telegramAPIServerDesc" = "要使用的 Telegram API 伺服器。留空以使用預設伺服器。"
"telegramChatId" = "管理員聊天 ID"
"telegramChatIdDesc" = "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 @userinfobot 獲取,或在機器人中使用 '/id' 命令獲取)"
"telegramNotifyTime" = "通知時間"
"telegramNotifyTimeDesc" = "設定週期性的 Telegram 機器人通知時間(使用 crontab 時間格式)"
"tgNotifyBackup" = "資料庫備份"
"tgNotifyBackupDesc" = "傳送帶有報告的資料庫備份檔案"
"tgNotifyLogin" = "登入通知"
"tgNotifyLoginDesc" = "當有人試圖登入你的面板時顯示使用者名稱、IP 地址和時間"
"sessionMaxAge" = "會話時長"
"sessionMaxAgeDesc" = "保持登入狀態的時長(單位:分鐘)"
"expireTimeDiff" = "到期通知閾值"
"expireTimeDiffDesc" = "達到此閾值時,將收到有關到期時間的通知(單位:天)"
"trafficDiff" = "流量耗盡閾值"
"trafficDiffDesc" = "達到此閾值時將收到有關流量耗盡的通知單位GB"
"tgNotifyCpu" = "CPU 負載通知閾值"
"tgNotifyCpuDesc" = "CPU 負載超過此閾值時,將收到通知(單位:%"
"timeZone" = "時區"
"timeZoneDesc" = "定時任務將按照該時區的時間執行"
"subSettings" = "訂閱設定"
"subEnable" = "啟用訂閱服務"
"subEnableDesc" = "啟用訂閱服務功能"
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
"subTitle" = "訂閱標題"
"subTitleDesc" = "在VPN客戶端中顯示的標題"
"subSupportUrl" = "支援連結"
"subSupportUrlDesc" = "VPN 用戶端中顯示的技術支援連結"
"subProfileUrl" = "個人資料連結"
"subProfileUrlDesc" = "VPN 用戶端中顯示的網站連結"
"subAnnounce" = "公告"
"subAnnounceDesc" = "VPN 用戶端中顯示的公告文字"
"subEnableRouting" = "啟用路由"
"subEnableRoutingDesc" = "在 VPN 用戶端中啟用路由的全域設定。(僅限 Happ"
"subRoutingRules" = "路由規則"
"subRoutingRulesDesc" = "VPN 用戶端的全域路由規則。(僅限 Happ"
"subListen" = "監聽 IP"
"subListenDesc" = "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP"
"subPort" = "監聽埠"
"subPortDesc" = "訂閱服務監聽的埠號(必須是未使用的埠)"
"subCertPath" = "公鑰路徑"
"subCertPathDesc" = "訂閱服務使用的公鑰檔案路徑(以 '/' 開頭)"
"subKeyPath" = "私鑰路徑"
"subKeyPathDesc" = "訂閱服務使用的私鑰檔案路徑(以 '/' 開頭)"
"subPath" = "URI 路徑"
"subPathDesc" = "訂閱服務使用的 URI 路徑(以 '/' 開頭,以 '/' 結尾)"
"subDomain" = "監聽域名"
"subDomainDesc" = "訂閱服務監聽的域名(留空表示監聽所有域名和 IP"
"subUpdates" = "更新間隔"
"subUpdatesDesc" = "客戶端應用中訂閱 URL 的更新間隔(單位:小時)"
"subEncrypt" = "編碼"
"subEncryptDesc" = "訂閱服務返回的內容將採用 Base64 編碼"
"subShowInfo" = "顯示使用資訊"
"subShowInfoDesc" = "客戶端應用中將顯示剩餘流量和日期資訊"
"subURI" = "反向代理 URI"
"subURIDesc" = "用於代理後面的訂閱 URL 的 URI 路徑"
"externalTrafficInformEnable" = "外部交通通知"
"externalTrafficInformEnableDesc" = "每次流量更新時通知外部 API"
"externalTrafficInformURI" = "外部流量通知 URI"
"externalTrafficInformURIDesc" = "流量更新將會傳送到此 URI"
"fragment" = "分片"
"fragmentDesc" = "啟用 TLS hello 資料包分片"
"fragmentSett" = "設定"
"noisesDesc" = "啟用 Noises."
"noisesSett" = "Noises 設定"
"mux" = "多路複用器"
"muxDesc" = "在已建立的資料流內傳輸多個獨立的資料流"
"muxSett" = "複用器設定"
"direct" = "直接連線"
"directDesc" = "直接與特定國家的域或IP範圍建立連線"
"notifications" = "通知"
"certs" = "證書"
"externalTraffic" = "外部流量"
"dateAndTime" = "日期和時間"
"proxyAndServer" = "代理和伺服器"
"intervals" = "間隔"
"information" = "資訊"
"language" = "語言"
"telegramBotLanguage" = "Telegram 機器人語言"
[pages.xray]
"title" = "Xray 配置"
"save" = "儲存"
"restart" = "重新啟動 Xray"
"restartSuccess" = "Xray 已成功重新啟動"
"stopSuccess" = "Xray 已成功停止"
"restartError" = "重新啟動Xray時發生錯誤。"
"stopError" = "停止Xray時發生錯誤。"
"basicTemplate" = "基礎配置"
"advancedTemplate" = "高階配置"
"generalConfigs" = "常規配置"
"generalConfigsDesc" = "這些選項將決定常規配置"
"logConfigs" = "日誌"
"logConfigsDesc" = "日誌可能會影響伺服器的效能,建議僅在需要時啟用"
"blockConfigsDesc" = "這些選項將阻止使用者連線到特定協議和網站"
"basicRouting" = "基本路由"
"blockConnectionsConfigsDesc" = "這些選項將根據特定的請求國家阻止流量。"
"directConnectionsConfigsDesc" = "直接連線確保特定的流量不會通過其他伺服器路由。"
"blockips" = "阻止IP"
"blockdomains" = "阻止域名"
"directips" = "直接IP"
"directdomains" = "直接域名"
"ipv4Routing" = "IPv4 路由"
"ipv4RoutingDesc" = "此選項將僅通過 IPv4 路由到目標域"
"warpRouting" = "WARP 路由"
"warpRoutingDesc" = "注意:在使用這些選項之前,請按照面板 GitHub 上的步驟在你的伺服器上以 socks5 代理模式安裝 WARP。WARP 將通過 Cloudflare 伺服器將流量路由到網站。"
"Template" = "高階 Xray 配置模板"
"TemplateDesc" = "最終的 Xray 配置檔案將基於此模板生成"
"FreedomStrategy" = "Freedom 協議策略"
"FreedomStrategyDesc" = "設定 Freedom 協議中網路的輸出策略"
"RoutingStrategy" = "配置路由域策略"
"RoutingStrategyDesc" = "設定 DNS 解析的整體路由策略"
"outboundTestUrl" = "出站測試 URL"
"outboundTestUrlDesc" = "測試出站連線時使用的 URL"
"Torrent" = "遮蔽 BitTorrent 協議"
"Inbounds" = "入站規則"
"InboundsDesc" = "接受來自特定客戶端的流量"
"Outbounds" = "出站規則"
"Balancers" = "負載均衡"
"OutboundsDesc" = "設定出站流量傳出方式"
"Routings" = "路由規則"
"RoutingsDesc" = "每條規則的優先順序都很重要"
"completeTemplate" = "全部"
"logLevel" = "日誌級別"
"logLevelDesc" = "錯誤日誌的日誌級別,用於指示需要記錄的資訊"
"accessLog" = "訪問日誌"
"accessLogDesc" = "訪問日誌的檔案路徑。特殊值 'none' 禁用訪問日誌"
"errorLog" = "錯誤日誌"
"errorLogDesc" = "錯誤日誌的檔案路徑。特殊值 'none' 禁用錯誤日誌"
"dnsLog" = "DNS 日誌"
"dnsLogDesc" = "是否啟用 DNS 查詢日誌"
"maskAddress" = "隱藏地址"
"maskAddressDesc" = "IP 地址掩碼,啟用時會自動替換日誌中出現的 IP 地址。"
"statistics" = "統計"
"statsInboundUplink" = "入站上傳統計"
"statsInboundUplinkDesc" = "啟用所有入站代理的上行流量統計收集。"
"statsInboundDownlink" = "入站下載統計"
"statsInboundDownlinkDesc" = "啟用所有入站代理的下行流量統計收集。"
"statsOutboundUplink" = "出站上傳統計"
"statsOutboundUplinkDesc" = "啟用所有出站代理的上行流量統計收集。"
"statsOutboundDownlink" = "出站下載統計"
"statsOutboundDownlinkDesc" = "啟用所有出站代理的下行流量統計收集。"
[pages.xray.rules]
"first" = "置頂"
"last" = "置底"
"up" = "向上"
"down" = "向下"
"source" = "來源"
"dest" = "目的地址"
"inbound" = "入站"
"outbound" = "出站"
"balancer" = "負載均衡"
"info" = "資訊"
"add" = "新增規則"
"edit" = "編輯規則"
"useComma" = "逗號分隔的項目"
[pages.xray.outbound]
"addOutbound" = "新增出站"
"addReverse" = "新增反向"
"editOutbound" = "編輯出站"
"editReverse" = "編輯反向"
"tag" = "標籤"
"tagDesc" = "唯一標籤"
"address" = "地址"
"reverse" = "反向"
"domain" = "域名"
"type" = "類型"
"bridge" = "Bridge"
"portal" = "Portal"
"link" = "連結"
"intercon" = "互連"
"settings" = "設定"
"accountInfo" = "帳戶資訊"
"outboundStatus" = "出站狀態"
"sendThrough" = "傳送通過"
"test" = "測試"
"testResult" = "測試結果"
"testing" = "正在測試連接..."
"testSuccess" = "測試成功"
"testFailed" = "測試失敗"
"testError" = "測試出站失敗"
[pages.xray.balancer]
"addBalancer" = "新增負載均衡"
"editBalancer" = "編輯負載均衡"
"balancerStrategy" = "策略"
"balancerSelectors" = "選擇器"
"tag" = "標籤"
"tagDesc" = "唯一標籤"
"balancerDesc" = "無法同時使用 balancerTag 和 outboundTag。如果同時使用則只有 outboundTag 會生效。"
[pages.xray.wireguard]
"secretKey" = "金鑰"
"publicKey" = "公鑰"
"allowedIPs" = "允許的 IP"
"endpoint" = "端點"
"psk" = "共享金鑰"
"domainStrategy" = "域策略"
[pages.xray.tun]
"nameDesc" = "TUN 介面的名稱。預設值為 'xray0'"
"mtuDesc" = "最大傳輸單元。資料包的最大大小。預設值為 1500"
"userLevel" = "用戶級別"
"userLevelDesc" = "通過此入站的所有連接都將使用此用戶級別。預設值為 0"
[pages.xray.dns]
"enable" = "啟用 DNS"
"enableDesc" = "啟用內建 DNS 伺服器"
"tag" = "DNS 入站標籤"
"tagDesc" = "此標籤將在路由規則中可用作入站標籤"
"clientIp" = "客戶端IP"
"clientIpDesc" = "用於在DNS查詢期間通知伺服器指定的IP位置"
"disableCache" = "禁用快取"
"disableCacheDesc" = "禁用DNS快取"
"disableFallback" = "禁用回退"
"disableFallbackDesc" = "禁用回退DNS查詢"
"disableFallbackIfMatch" = "匹配時禁用回退"
"disableFallbackIfMatchDesc" = "當DNS伺服器的匹配域名列表命中時禁用回退DNS查詢"
"enableParallelQuery" = "啟用並行查詢"
"enableParallelQueryDesc" = "啟用並行DNS查詢到多個伺服器以實現更快的解析"
"strategy" = "查詢策略"
"strategyDesc" = "解析域名的總體策略"
"add" = "新增伺服器"
"edit" = "編輯伺服器"
"domains" = "域"
"expectIPs" = "預期 IP"
"unexpectIPs" = "意外IP"
"useSystemHosts" = "使用系統Hosts"
"useSystemHostsDesc" = "使用已安裝系統的hosts檔案"
"usePreset" = "使用範本"
"dnsPresetTitle" = "DNS範本"
"dnsPresetFamily" = "家庭"
[pages.xray.fakedns]
"add" = "新增假 DNS"
"edit" = "編輯假 DNS"
"ipPool" = "IP 池子網"
"poolSize" = "池大小"
[pages.settings.security]
"admin" = "管理員憑證"
"twoFactor" = "雙重驗證"
"twoFactorEnable" = "啟用2FA"
"twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。"
"twoFactorModalSetTitle" = "啟用雙重認證"
"twoFactorModalDeleteTitle" = "停用雙重認證"
"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
"twoFactorModalFirstStep" = "1. 在認證應用程式中掃描此QR碼或複製QR碼附近的令牌並貼到應用程式中"
"twoFactorModalSecondStep" = "2. 輸入應用程式中的驗證碼"
"twoFactorModalRemoveStep" = "輸入應用程式中的驗證碼以移除雙重認證。"
"twoFactorModalChangeCredentialsTitle" = "更改憑證"
"twoFactorModalChangeCredentialsStep" = "輸入應用程式中的代碼以更改管理員憑證。"
"twoFactorModalSetSuccess" = "雙重身份驗證已成功建立"
"twoFactorModalDeleteSuccess" = "雙重身份驗證已成功刪除"
"twoFactorModalError" = "驗證碼錯誤"
[pages.settings.toasts]
"modifySettings" = "參數已更改。"
"getSettings" = "取得參數時發生錯誤"
"modifyUserError" = "變更管理員憑證時發生錯誤。"
"modifyUser" = "您已成功變更管理員憑證。"
"originalUserPassIncorrect" = "原使用者名稱或原密碼錯誤"
"userPassMustBeNotEmpty" = "新使用者名稱和新密碼不能為空"
"getOutboundTrafficError" = "取得出站流量錯誤"
"resetOutboundTrafficError" = "重設出站流量錯誤"
[tgbot]
"keyboardClosed" = "❌ 自定義鍵盤已關閉!"
"noResult" = "❗ 沒有結果!"
"noQuery" = "❌ 未找到查詢!請再次使用該命令!"
"wentWrong" = "❌ 出了點問題!"
"noIpRecord" = "❗ 沒有IP記錄"
"noInbounds" = "❗ 未找到入站!"
"unlimited" = "♾ 無限(重置)"
"add" = "添加"
"month" = "月"
"months" = "月"
"day" = "天"
"days" = "天"
"hours" = "小時"
"minutes" = "分鐘"
"unknown" = "未知"
"inbounds" = "入站"
"clients" = "客戶端"
"offline" = "🔴 離線"
"online" = "🟢 在線"
[tgbot.commands]
"unknown" = "❗ 未知命令"
"pleaseChoose" = "👇 請選擇:\r\n"
"help" = "🤖 歡迎使用本機器人!它旨在為您提供來自伺服器的特定資料,並允許您進行必要的修改。\r\n\r\n"
"start" = "👋 你好,<i>{{ .Firstname }}</i>。\r\n"
"welcome" = "🤖 歡迎來到 <b>{{ .Hostname }}</b> 管理機器人。\r\n"
"status" = "✅ 機器人正常執行!"
"usage" = "❗ 請輸入要搜尋的文字!"
"getID" = "🆔 您的 ID 為:<code>{{ .ID }}</code>"
"helpAdminCommands" = "要重新啟動 Xray Core\r\n<code>/restart</code>\r\n\r\n要搜尋客戶電子郵件\r\n<code>/usage [電子郵件]</code>\r\n\r\n要搜尋入站帶有客戶統計資料\r\n<code>/inbound [備註]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"helpClientCommands" = "要搜尋統計資料,請使用以下命令:\r\n<code>/usage [電子郵件]</code>\r\n\r\nTelegram聊天ID\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ 操作成功!"
"restartFailed" = "❗ 操作錯誤。\r\n\r\n<code>錯誤: {{ .Error }}</code>."
"xrayNotRunning" = "❗ Xray Core 未運行。"
"startDesc" = "顯示主選單"
"helpDesc" = "機器人幫助"
"statusDesc" = "檢查機器人狀態"
"idDesc" = "顯示您的 Telegram ID"
[tgbot.messages]
"cpuThreshold" = "🔴 CPU 使用率為 {{ .Percent }}%,超過閾值 {{ .Threshold }}%"
"selectUserFailed" = "❌ 使用者選擇錯誤!"
"userSaved" = "✅ 電報使用者已儲存。"
"loginSuccess" = "✅ 成功登入到面板。\r\n"
"loginFailed" = "❗️ 面板登入失敗。\r\n"
"2faFailed" = "2FA 失敗"
"report" = "🕰 定時報告:{{ .RunTime }}\r\n"
"datetime" = "⏰ 日期時間:{{ .DateTime }}\r\n"
"hostname" = "💻 主機名:{{ .Hostname }}\r\n"
"version" = "🚀 X-UI 版本:{{ .Version }}\r\n"
"xrayVersion" = "📡 Xray 版本: {{ .XrayVersion }}\r\n"
"ipv6" = "🌐 IPv6{{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4{{ .IPv4 }}\r\n"
"ip" = "🌐 IP{{ .IP }}\r\n"
"ips" = "🔢 IP 地址:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ 伺服器執行時間:{{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 伺服器負載:{{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 伺服器記憶體:{{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP 連線數:{{ .Count }}\r\n"
"udpCount" = "🔸 UDP 連線數:{{ .Count }}\r\n"
"traffic" = "🚦 流量:{{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Xray 狀態:{{ .State }}\r\n"
"username" = "👤 使用者名稱:{{ .Username }}\r\n"
"password" = "👤 密碼: {{ .Password }}\r\n"
"time" = "⏰ 時間:{{ .Time }}\r\n"
"inbound" = "📍 入站:{{ .Remark }}\r\n"
"port" = "🔌 埠:{{ .Port }}\r\n"
"expire" = "📅 過期日期:{{ .Time }}\r\n"
"expireIn" = "📅 剩餘時間:{{ .Time }}\r\n"
"active" = "💡 啟用:{{ .Enable }}\r\n"
"enabled" = "🚨 已啟用:{{ .Enable }}\r\n"
"online" = "🌐 連線狀態:{{ .Status }}\r\n"
"lastOnline" = "🔙 上次上線: {{ .Time }}\r\n"
"email" = "📧 郵箱:{{ .Email }}\r\n"
"upload" = "🔼 上傳↑:{{ .Upload }}\r\n"
"download" = "🔽 下載↓:{{ .Download }}\r\n"
"total" = "📊 總計:{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 電報使用者:{{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 耗盡的 {{ .Type }}\r\n"
"exhaustedCount" = "🚨 耗盡的 {{ .Type }} 數量:\r\n"
"onlinesCount" = "🌐 線上客戶:{{ .Count }}\r\n"
"disabled" = "🛑 禁用:{{ .Disabled }}\r\n"
"depleteSoon" = "🔜 即將耗盡:{{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 備份時間:{{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 重新整理時間:{{ .Time }}\r\n\r\n"
"yes" = "✅ 是的"
"no" = "❌ 沒有"
"received_id" = "🔑📥 ID 已更新。"
"received_password" = "🔑📥 密碼已更新。"
"received_email" = "📧📥 電子郵件已更新。"
"received_comment" = "💬📥 評論已更新。"
"id_prompt" = "🔑 預設 ID: {{ .ClientId }}\n\n請輸入您的 ID。"
"pass_prompt" = "🔑 預設密碼: {{ .ClientPassword }}\n\n請輸入您的密碼。"
"email_prompt" = "📧 預設電子郵件: {{ .ClientEmail }}\n\n請輸入您的電子郵件。"
"comment_prompt" = "💬 預設評論: {{ .ClientComment }}\n\n請輸入您的評論。"
"inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了"
"inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密碼: {{ .ClientPass }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了"
"cancel" = "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄"
"error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}"
"using_default_value" = "好的,我會使用預設值。 😊"
"incorrect_input" = "您的輸入無效。\n短語應連續輸入不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫"
"AreYouSure" = "你確定嗎?🤔"
"SuccessResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功"
"FailedResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 錯誤: [ {{ .ErrorMessage }} ]"
"FinishProcess" = "🔚 所有客戶的流量重置已完成。"
[tgbot.buttons]
"closeKeyboard" = "❌ 關閉鍵盤"
"cancel" = "❌ 取消"
"cancelReset" = "❌ 取消重置"
"cancelIpLimit" = "❌ 取消 IP 限制"
"confirmResetTraffic" = "✅ 確認重置流量?"
"confirmClearIps" = "✅ 確認清除 IP"
"confirmRemoveTGUser" = "✅ 確認移除 Telegram 使用者?"
"confirmToggle" = "✅ 確認啟用/禁用使用者?"
"dbBackup" = "獲取資料庫備份"
"serverUsage" = "伺服器使用情況"
"getInbounds" = "獲取入站資訊"
"depleteSoon" = "即將耗盡"
"clientUsage" = "獲取使用情況"
"onlines" = "線上客戶端"
"commands" = "命令"
"refresh" = "🔄 重新整理"
"clearIPs" = "❌ 清除 IP"
"removeTGUser" = "❌ 移除 Telegram 使用者"
"selectTGUser" = "👤 選擇 Telegram 使用者"
"selectOneTGUser" = "👤 選擇一個 Telegram 使用者:"
"resetTraffic" = "📈 重置流量"
"resetExpire" = "📅 更改到期日期"
"ipLog" = "🔢 IP 日誌"
"ipLimit" = "🔢 IP 限制"
"setTGUser" = "👤 設定 Telegram 使用者"
"toggle" = "🔘 啟用/禁用"
"custom" = "🔢 風俗"
"confirmNumber" = "✅ 確認: {{ .Num }}"
"confirmNumberAdd" = "✅ 確認新增:{{ .Num }}"
"limitTraffic" = "🚧 流量限制"
"getBanLogs" = "禁止日誌"
"allClients" = "所有客戶"
"addClient" = "新增客戶"
"submitDisable" = "以停用方式送出 ☑️"
"submitEnable" = "以啟用方式送出 ✅"
"use_default" = "🏷️ 使用預設值"
"change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 密碼"
"change_email" = "⚙️📧 電子郵件"
"change_comment" = "⚙️💬 評論"
"ResetAllTraffics" = "重設所有流量"
"SortedTrafficUsageReport" = "排序過的流量使用報告"
[tgbot.answers]
"successfulOperation" = "✅ 成功!"
"errorOperation" = "❗ 操作錯誤。"
"getInboundsFailed" = "❌ 獲取入站資訊失敗。"
"getClientsFailed" = "❌ 獲取客戶失敗。"
"canceled" = "❌ {{ .Email }}:操作已取消。"
"clientRefreshSuccess" = "✅ {{ .Email }}:客戶端重新整理成功。"
"IpRefreshSuccess" = "✅ {{ .Email }}IP 重新整理成功。"
"TGIdRefreshSuccess" = "✅ {{ .Email }}:客戶端的 Telegram 使用者重新整理成功。"
"resetTrafficSuccess" = "✅ {{ .Email }}:流量已重置成功。"
"setTrafficLimitSuccess" = "✅ {{ .Email }}: 流量限制儲存成功。"
"expireResetSuccess" = "✅ {{ .Email }}:過期天數已重置成功。"
"resetIpSuccess" = "✅ {{ .Email }}:成功儲存 IP 限制數量為 {{ .Count }}。"
"clearIpSuccess" = "✅ {{ .Email }}IP 已成功清除。"
"getIpLog" = "✅ {{ .Email }}:獲取 IP 日誌。"
"getUserInfo" = "✅ {{ .Email }}:獲取 Telegram 使用者資訊。"
"removedTGUserSuccess" = "✅ {{ .Email }}Telegram 使用者已成功移除。"
"enableSuccess" = "✅ {{ .Email }}:已成功啟用。"
"disableSuccess" = "✅ {{ .Email }}:已成功禁用。"
"askToAddUserId" = "未找到您的配置!\r\n請向管理員詢問在您的配置中使用您的 Telegram 使用者 ChatID。\r\n\r\n您的使用者 ChatID<code>{{ .TgUserID }}</code>"
"chooseClient" = "為入站 {{ .Inbound }} 選擇一個客戶"
"chooseInbound" = "選擇一個入站"

149
x-ui.sh
View file

@ -2202,6 +2202,146 @@ show_usage() {
└────────────────────────────────────────────────────────────────┘"
}
# Read dbType from /etc/x-ui/x-ui.json using the Go binary
read_json_dbtype() {
local db_type
db_type=$(${xui_folder}/x-ui setting -showDbType 2>/dev/null)
if [ -z "$db_type" ]; then
echo "sqlite"
else
echo "$db_type"
fi
}
# Show current database status
db_show_status() {
local current_type=$(read_json_dbtype)
echo -e "${green}当前数据库类型: ${current_type}${plain}"
if [ "$current_type" = "mariadb" ]; then
local json_path="/etc/x-ui/x-ui.json"
if command -v jq >/dev/null 2>&1; then
local host=$(jq -r '.other.dbHost // "127.0.0.1"' "$json_path" 2>/dev/null)
local port=$(jq -r '.other.dbPort // "3306"' "$json_path" 2>/dev/null)
local dbname=$(jq -r '.other.dbName // "3xui"' "$json_path" 2>/dev/null)
else
local host=$(grep -o '"dbHost"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_path" 2>/dev/null | tail -1 | sed 's/.*"\([^"]*\)"$/\1/')
local port=$(grep -o '"dbPort"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_path" 2>/dev/null | tail -1 | sed 's/.*"\([^"]*\)"$/\1/')
local dbname=$(grep -o '"dbName"[[:space:]]*:[[:space:]]*"[^"]*"' "$json_path" 2>/dev/null | tail -1 | sed 's/.*"\([^"]*\)"$/\1/')
fi
echo -e "${green}MariaDB 主机: ${host:-127.0.0.1}:${port:-3306}${plain}"
echo -e "${green}数据库名: ${dbname:-3xui}${plain}"
fi
}
# Switch to MariaDB
db_switch_to_mariadb() {
local current_type=$(read_json_dbtype)
if [ "$current_type" = "mariadb" ]; then
echo -e "${yellow}当前已经是 MariaDB${plain}"
db_menu
return
fi
echo -e "${green}请输入 MariaDB 连接信息(直接回车使用默认值):${plain}"
read -rp "MariaDB IP默认 127.0.0.1: " db_host
db_host=${db_host:-127.0.0.1}
read -rp "MariaDB 端口(默认 3306: " db_port
db_port=${db_port:-3306}
read -rp "MariaDB 用户名: " db_user
if [ -z "$db_user" ]; then
echo -e "${red}用户名不能为空${plain}"
db_menu
return
fi
read -rsp "MariaDB 密码: " db_pass
echo
if [ -z "$db_pass" ]; then
echo -e "${red}密码不能为空${plain}"
db_menu
return
fi
read -rp "数据库名(默认 3xui: " db_name
db_name=${db_name:-3xui}
echo -e "${green}正在配置 MariaDB 连接...${plain}"
XUI_DB_PASSWORD="$db_pass" ${xui_folder}/x-ui setting -dbHost "$db_host" -dbPort "$db_port" -dbUser "$db_user" -dbName "$db_name" >/dev/null 2>&1
echo -e "${green}正在迁移数据从 SQLite 到 MariaDB...${plain}"
${xui_folder}/x-ui migrate-db -direction sqlite-to-mariadb
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
${xui_folder}/x-ui setting -dbType mariadb >/dev/null 2>&1
restart
else
echo -e "${red}数据迁移失败,保持 SQLite 不变${plain}"
restart
fi
}
# Switch to SQLite
db_switch_to_sqlite() {
local current_type=$(read_json_dbtype)
if [ "$current_type" = "sqlite" ]; then
echo -e "${yellow}当前已经是 SQLite${plain}"
db_menu
return
fi
echo -e "${green}正在迁移数据从 MariaDB 到 SQLite...${plain}"
${xui_folder}/x-ui migrate-db -direction mariadb-to-sqlite
if [ $? -eq 0 ]; then
echo -e "${green}数据库切换成功,正在重启面板...${plain}"
${xui_folder}/x-ui setting -dbType sqlite >/dev/null 2>&1
restart
else
echo -e "${red}数据迁移失败,保持 MariaDB 不变${plain}"
db_menu
fi
}
# Database management menu
db_menu() {
local current_type=$(read_json_dbtype)
echo -e "
╔────────────────────────────────────────────────╗
${green}数据库管理${plain}
│────────────────────────────────────────────────│
${green}0.${plain} 返回主菜单 │
${green}1.${plain} 查看当前数据库类型(当前: ${current_type}
${green}2.${plain} 切换到 MariaDB │
${green}3.${plain} 切换到 SQLite │
╚════════════════════════════════════════════════╝
"
read -rp "请输入选择 [0-3]" num
case "${num}" in
0)
show_menu
;;
1)
db_show_status
db_menu
;;
2)
db_switch_to_mariadb
;;
3)
db_switch_to_sqlite
;;
*)
echo -e "${red}无效选项,请选择有效数字。${plain}\n"
db_menu
;;
esac
}
show_menu() {
echo -e "
╔────────────────────────────────────────────────╗
@ -2239,10 +2379,12 @@ show_menu() {
${green}24.${plain} BBR 管理 │
${green}25.${plain} 更新 Geo 文件 │
${green}26.${plain} 网速测试 (Speedtest)
│────────────────────────────────────────────────│
${green}27.${plain} 数据库管理 │
╚────────────────────────────────────────────────╝
"
show_status
echo && read -rp "请输入选择 [0-26]" num
echo && read -rp "请输入选择 [0-27]" num
case "${num}" in
0)
@ -2326,8 +2468,11 @@ show_menu() {
26)
run_speedtest
;;
27)
check_install && db_menu
;;
*)
LOGE "请输入正确的数字 [0-26]"
LOGE "请输入正确的数字 [0-27]"
;;
esac
}

172
xray/api_test.go Normal file
View file

@ -0,0 +1,172 @@
package xray
import (
"testing"
)
func TestProcessTraffic_Inbound(t *testing.T) {
matches := []string{
"inbound>>>vmess-in>>>traffic>>>uplink",
"inbound",
"vmess-in",
"uplink",
}
trafficMap := make(map[string]*Traffic)
processTraffic(matches, 1024, trafficMap)
tr, ok := trafficMap["vmess-in"]
if !ok {
t.Fatal("should have vmess-in entry")
}
if !tr.IsInbound {
t.Error("should be inbound")
}
if tr.IsOutbound {
t.Error("should not be outbound")
}
if tr.Tag != "vmess-in" {
t.Errorf("tag should be vmess-in, got %q", tr.Tag)
}
if tr.Up != 1024 {
t.Errorf("up should be 1024, got %d", tr.Up)
}
if tr.Down != 0 {
t.Errorf("down should be 0, got %d", tr.Down)
}
}
func TestProcessTraffic_Outbound(t *testing.T) {
matches := []string{
"outbound>>>direct>>>traffic>>>downlink",
"outbound",
"direct",
"downlink",
}
trafficMap := make(map[string]*Traffic)
processTraffic(matches, 2048, trafficMap)
tr, ok := trafficMap["direct"]
if !ok {
t.Fatal("should have direct entry")
}
if tr.IsOutbound != true {
t.Error("should be outbound")
}
if tr.IsInbound != false {
t.Error("should not be inbound")
}
if tr.Down != 2048 {
t.Errorf("down should be 2048, got %d", tr.Down)
}
}
func TestProcessTraffic_ApiTagSkipped(t *testing.T) {
matches := []string{
"inbound>>>api>>>traffic>>>uplink",
"inbound",
"api",
"uplink",
}
trafficMap := make(map[string]*Traffic)
processTraffic(matches, 1024, trafficMap)
if _, ok := trafficMap["api"]; ok {
t.Error("api tag should be skipped")
}
}
func TestProcessTraffic_Aggregates(t *testing.T) {
trafficMap := make(map[string]*Traffic)
// First: uplink
processTraffic([]string{"", "inbound", "test-tag", "uplink"}, 100, trafficMap)
// Second: downlink on same tag
processTraffic([]string{"", "inbound", "test-tag", "downlink"}, 200, trafficMap)
tr := trafficMap["test-tag"]
if tr.Up != 100 {
t.Errorf("expected up=100, got %d", tr.Up)
}
if tr.Down != 200 {
t.Errorf("expected down=200, got %d", tr.Down)
}
}
func TestProcessClientTraffic(t *testing.T) {
clientMap := make(map[string]*ClientTraffic)
processClientTraffic([]string{"", "user@example.com", "uplink"}, 500, clientMap)
processClientTraffic([]string{"", "user@example.com", "downlink"}, 1500, clientMap)
ct, ok := clientMap["user@example.com"]
if !ok {
t.Fatal("should have client entry")
}
if ct.Email != "user@example.com" {
t.Errorf("email should be user@example.com, got %q", ct.Email)
}
if ct.Up != 500 {
t.Errorf("up should be 500, got %d", ct.Up)
}
if ct.Down != 1500 {
t.Errorf("down should be 1500, got %d", ct.Down)
}
}
func TestProcessClientTraffic_MultipleClients(t *testing.T) {
clientMap := make(map[string]*ClientTraffic)
processClientTraffic([]string{"", "user1@test.com", "uplink"}, 100, clientMap)
processClientTraffic([]string{"", "user2@test.com", "uplink"}, 200, clientMap)
if len(clientMap) != 2 {
t.Errorf("expected 2 clients, got %d", len(clientMap))
}
if clientMap["user1@test.com"].Up != 100 {
t.Error("user1 up mismatch")
}
if clientMap["user2@test.com"].Up != 200 {
t.Error("user2 up mismatch")
}
}
func TestMapToSlice_Empty(t *testing.T) {
m := make(map[string]*Traffic)
result := mapToSlice(m)
if len(result) != 0 {
t.Errorf("expected empty slice, got length %d", len(result))
}
}
func TestMapToSlice_Nil(t *testing.T) {
var m map[string]*Traffic
result := mapToSlice(m)
if len(result) != 0 {
t.Errorf("expected empty slice for nil map, got length %d", len(result))
}
}
func TestMapToSlice_Multiple(t *testing.T) {
m := map[string]*Traffic{
"a": {Tag: "a", Up: 1},
"b": {Tag: "b", Up: 2},
"c": {Tag: "c", Up: 3},
}
result := mapToSlice(m)
if len(result) != 3 {
t.Errorf("expected 3 elements, got %d", len(result))
}
}
func TestXrayAPI_Init_InvalidPort(t *testing.T) {
api := &XrayAPI{}
if err := api.Init(0); err == nil {
t.Error("Init with port 0 should return error")
}
if err := api.Init(-1); err == nil {
t.Error("Init with negative port should return error")
}
if err := api.Init(70000); err == nil {
t.Error("Init with port > 65535 should return error")
}
}

View file

@ -6,7 +6,7 @@ type ClientTraffic struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
InboundId int `json:"inboundId" form:"inboundId"`
Enable bool `json:"enable" form:"enable"`
Email string `json:"email" form:"email" gorm:"unique"`
Email string `json:"email" form:"email"`
UUID string `json:"uuid" form:"uuid" gorm:"-"`
SubId string `json:"subId" form:"subId" gorm:"-"`
Up int64 `json:"up" form:"up"`

125
xray/config_test.go Normal file
View file

@ -0,0 +1,125 @@
package xray
import (
"testing"
)
func TestInboundConfig_Equals_Equal(t *testing.T) {
a := &InboundConfig{
Listen: []byte(`"0.0.0.0"`),
Port: 443,
Protocol: "vless",
Settings: []byte(`{}`),
StreamSettings: []byte(`{"network":"tcp"}`),
Tag: "inbound-443",
Sniffing: []byte(`{"enabled":true}`),
}
b := &InboundConfig{
Listen: []byte(`"0.0.0.0"`),
Port: 443,
Protocol: "vless",
Settings: []byte(`{}`),
StreamSettings: []byte(`{"network":"tcp"}`),
Tag: "inbound-443",
Sniffing: []byte(`{"enabled":true}`),
}
if !a.Equals(b) {
t.Error("identical InboundConfigs should be equal")
}
}
func TestInboundConfig_Equals_DifferentPort(t *testing.T) {
a := &InboundConfig{Port: 443, Protocol: "vless"}
b := &InboundConfig{Port: 8443, Protocol: "vless"}
if a.Equals(b) {
t.Error("InboundConfigs with different ports should not be equal")
}
}
func TestInboundConfig_Equals_DifferentProtocol(t *testing.T) {
a := &InboundConfig{Port: 443, Protocol: "vless"}
b := &InboundConfig{Port: 443, Protocol: "trojan"}
if a.Equals(b) {
t.Error("InboundConfigs with different protocols should not be equal")
}
}
func TestInboundConfig_Equals_DifferentTag(t *testing.T) {
a := &InboundConfig{Port: 443, Protocol: "vless", Tag: "tag-a"}
b := &InboundConfig{Port: 443, Protocol: "vless", Tag: "tag-b"}
if a.Equals(b) {
t.Error("InboundConfigs with different tags should not be equal")
}
}
func TestInboundConfig_Equals_NilRawMessages(t *testing.T) {
a := &InboundConfig{Port: 443, Protocol: "vless", Listen: nil, Settings: nil}
b := &InboundConfig{Port: 443, Protocol: "vless", Listen: nil, Settings: nil}
if !a.Equals(b) {
t.Error("InboundConfigs with nil RawMessages should be equal")
}
}
func TestInboundConfig_Equals_DifferentListen(t *testing.T) {
a := &InboundConfig{Listen: []byte(`"0.0.0.0"`), Port: 443}
b := &InboundConfig{Listen: []byte(`"127.0.0.1"`), Port: 443}
if a.Equals(b) {
t.Error("InboundConfigs with different Listen should not be equal")
}
}
func TestConfig_Equals_Equal(t *testing.T) {
a := &Config{
LogConfig: []byte(`{"loglevel":"info"}`),
RouterConfig: []byte(`{}`),
InboundConfigs: []InboundConfig{
{Port: 443, Protocol: "vless"},
},
}
b := &Config{
LogConfig: []byte(`{"loglevel":"info"}`),
RouterConfig: []byte(`{}`),
InboundConfigs: []InboundConfig{
{Port: 443, Protocol: "vless"},
},
}
if !a.Equals(b) {
t.Error("identical Configs should be equal")
}
}
func TestConfig_Equals_DifferentInboundCount(t *testing.T) {
a := &Config{
InboundConfigs: []InboundConfig{{Port: 443}},
}
b := &Config{
InboundConfigs: []InboundConfig{},
}
if a.Equals(b) {
t.Error("Configs with different inbound counts should not be equal")
}
}
func TestConfig_Equals_DifferentLogConfig(t *testing.T) {
a := &Config{LogConfig: []byte(`{"loglevel":"info"}`)}
b := &Config{LogConfig: []byte(`{"loglevel":"debug"}`)}
if a.Equals(b) {
t.Error("Configs with different LogConfig should not be equal")
}
}
func TestConfig_Equals_EmptyConfigs(t *testing.T) {
a := &Config{}
b := &Config{}
if !a.Equals(b) {
t.Error("two empty Configs should be equal")
}
}
func TestConfig_Equals_NilVsEmpty(t *testing.T) {
a := &Config{}
b := &Config{InboundConfigs: []InboundConfig{}}
if !a.Equals(b) {
t.Error("nil and empty slice InboundConfigs should be equal")
}
}

91
xray/log_writer_test.go Normal file
View file

@ -0,0 +1,91 @@
package xray
import (
"testing"
"github.com/mhsanaei/3x-ui/v2/logger"
"github.com/op/go-logging"
)
func init() {
// Initialize logger for tests that use LogWriter (which calls logger.Debug/Error/etc.)
logger.InitLogger(logging.DEBUG)
}
func TestNewLogWriter(t *testing.T) {
lw := NewLogWriter()
if lw == nil {
t.Fatal("NewLogWriter should not return nil")
}
}
func TestLogWriter_Write_CrashDetection(t *testing.T) {
lw := NewLogWriter()
msg := []byte("panic: runtime error: index out of range")
n, err := lw.Write(msg)
if err != nil {
t.Fatalf("Write should not return error: %v", err)
}
if n != len(msg) {
t.Errorf("Write returned %d, expected %d", n, len(msg))
}
}
func TestLogWriter_Write_FatalError(t *testing.T) {
lw := NewLogWriter()
msg := []byte("fatal error: concurrent map writes")
n, err := lw.Write(msg)
if err != nil {
t.Fatalf("Write should not return error: %v", err)
}
if n != len(msg) {
t.Errorf("Write returned %d, expected %d", n, len(msg))
}
}
func TestLogWriter_Write_Exception(t *testing.T) {
lw := NewLogWriter()
msg := []byte("unhandled exception occurred")
n, err := lw.Write(msg)
if err != nil {
t.Fatalf("Write should not return error: %v", err)
}
if n != len(msg) {
t.Errorf("Write returned %d, expected %d", n, len(msg))
}
}
func TestLogWriter_Write_EmptyMessage(t *testing.T) {
lw := NewLogWriter()
n, err := lw.Write([]byte(""))
if err != nil {
t.Fatalf("Write should not error: %v", err)
}
if n != 0 {
t.Errorf("Write returned %d, expected 0", n)
}
}
func TestLogWriter_Write_TLSErrorSuppressed(t *testing.T) {
lw := NewLogWriter()
msg := []byte("some tls handshake error occurred")
n, err := lw.Write(msg)
if err != nil {
t.Fatalf("Write should not return error: %v", err)
}
if n != len(msg) {
t.Errorf("Write returned %d, expected %d", n, len(msg))
}
}
func TestLogWriter_Write_FailedKeyword(t *testing.T) {
lw := NewLogWriter()
msg := []byte("connection failed to remote")
n, err := lw.Write(msg)
if err != nil {
t.Fatalf("Write should not return error: %v", err)
}
if n != len(msg) {
t.Errorf("Write returned %d, expected %d", n, len(msg))
}
}