mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
Merge pull request #513 from Vaso73/i18n/slovak-monitor-cli-messages
i18n: complete Slovak Monitor and CLI messages
This commit is contained in:
@@ -13,6 +13,8 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
PYTHON_FILES = (
|
PYTHON_FILES = (
|
||||||
|
"tests/test_post_install_i18n.py",
|
||||||
|
"tests/test_remaining_cli_i18n.py",
|
||||||
"AppImage/scripts/tests/test_pbs_i18n.py",
|
"AppImage/scripts/tests/test_pbs_i18n.py",
|
||||||
"AppImage/scripts/tests/test_monitor_links_i18n.py",
|
"AppImage/scripts/tests/test_monitor_links_i18n.py",
|
||||||
"AppImage/scripts/tests/test_monitor_links.py",
|
"AppImage/scripts/tests/test_monitor_links.py",
|
||||||
@@ -31,6 +33,9 @@ PYTHON_FILES = (
|
|||||||
"AppImage/scripts/tests/test_health_persistence_recurrence.py",
|
"AppImage/scripts/tests/test_health_persistence_recurrence.py",
|
||||||
)
|
)
|
||||||
NODE_FILES = (
|
NODE_FILES = (
|
||||||
|
"tests/test_api_import_boundaries.cjs",
|
||||||
|
"tests/test_api_errors.cjs",
|
||||||
|
"tests/test_optimization_labels.cjs",
|
||||||
"tests/test_optimization_source.cjs",
|
"tests/test_optimization_source.cjs",
|
||||||
"tests/lxc_updates/test_app_editor_errors.cjs",
|
"tests/lxc_updates/test_app_editor_errors.cjs",
|
||||||
"tests/test_oci_update_cancellation.cjs",
|
"tests/test_oci_update_cancellation.cjs",
|
||||||
|
|||||||
@@ -69,13 +69,22 @@ class TerminalCatalogsTests(unittest.TestCase):
|
|||||||
def test_shipped_active_catalogs_keep_literal_contracts(self):
|
def test_shipped_active_catalogs_keep_literal_contracts(self):
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'),
|
[sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'),
|
||||||
'--root', str(ROOT), '--require-complete'],
|
'--root', str(ROOT)],
|
||||||
capture_output=True, text=True, timeout=60,
|
capture_output=True, text=True, timeout=60,
|
||||||
)
|
)
|
||||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
for locale in MODULE.DEFAULT_LANGUAGES:
|
for locale in MODULE.DEFAULT_LANGUAGES:
|
||||||
self.assertIn(f'[{locale}] active=', result.stdout)
|
self.assertIn(f'[{locale}] active=', result.stdout)
|
||||||
self.assertNotIn('missing=1', result.stdout)
|
# New hand-reviewed Slovak keys ship with their source changes;
|
||||||
|
# other locales may use the runtime English fallback until translated.
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, '-B', str(SCRIPTS / 'check_terminal_catalogs.py'),
|
||||||
|
'--root', str(ROOT), '--languages', 'sk', '--require-complete'],
|
||||||
|
capture_output=True, text=True, timeout=60,
|
||||||
|
)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
self.assertIn('missing=0', result.stdout)
|
||||||
|
|
||||||
|
|
||||||
def test_optional_completeness_still_rejects_invalid_present_translations(self):
|
def test_optional_completeness_still_rejects_invalid_present_translations(self):
|
||||||
with tempfile.TemporaryDirectory() as temporary:
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
|||||||
@@ -1530,7 +1530,7 @@ export function NotificationSettings() {
|
|||||||
<Input
|
<Input
|
||||||
type={showSecrets["tg_token"] ? "text" : "password"}
|
type={showSecrets["tg_token"] ? "text" : "password"}
|
||||||
className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`}
|
className={`h-7 text-xs font-mono ${!editMode ? "opacity-50" : ""}`}
|
||||||
placeholder="7595377878:AAGE6Fb2cy... (with or without 'bot' prefix)"
|
placeholder={t("settings.notifications.ui.botTokenPlaceholder")}
|
||||||
value={secretValue("tg_token", config.channels.telegram?.bot_token || "")}
|
value={secretValue("tg_token", config.channels.telegram?.bot_token || "")}
|
||||||
onChange={e => updateChannel("telegram", "bot_token", e.target.value)}
|
onChange={e => updateChannel("telegram", "bot_token", e.target.value)}
|
||||||
disabled={!editMode}
|
disabled={!editMode}
|
||||||
@@ -1542,6 +1542,7 @@ export function NotificationSettings() {
|
|||||||
{showSecrets["tg_token"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
{showSecrets["tg_token"] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="text-[10px] text-muted-foreground">{t("settings.notifications.ui.botTokenHelp")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.chatId")}</Label>
|
<Label className="text-[11px] text-muted-foreground">{t("settings.notifications.ui.chatId")}</Label>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { NotificationSettings } from "./notification-settings"
|
|||||||
import { HealthThresholds } from "./health-thresholds"
|
import { HealthThresholds } from "./health-thresholds"
|
||||||
import { LxcUpdateDetection } from "./lxc-update-detection"
|
import { LxcUpdateDetection } from "./lxc-update-detection"
|
||||||
import { DiskExclusions } from "./disk-exclusions"
|
import { DiskExclusions } from "./disk-exclusions"
|
||||||
|
import { optimizationDescription, optimizationToolName } from "../lib/optimization-descriptions"
|
||||||
import { ScriptTerminalModal } from "./script-terminal-modal"
|
import { ScriptTerminalModal } from "./script-terminal-modal"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
|
||||||
import { Switch } from "./ui/switch"
|
import { Switch } from "./ui/switch"
|
||||||
@@ -597,7 +598,7 @@ export function Settings() {
|
|||||||
if (entries.length === 0) return
|
if (entries.length === 0) return
|
||||||
const batch = entries.map(e => `${e.source}:${e.function}:${e.key}`).join("\n")
|
const batch = entries.map(e => `${e.source}:${e.function}:${e.key}`).join("\n")
|
||||||
const title = entries.length === 1
|
const title = entries.length === 1
|
||||||
? t("settings.optimizations.updateOneTitle", { name: entries[0].name })
|
? t("settings.optimizations.updateOneTitle", { name: optimizationToolName(entries[0].key, entries[0].name, t) })
|
||||||
: t("settings.optimizations.updateManyTitle", { count: entries.length })
|
: t("settings.optimizations.updateManyTitle", { count: entries.length })
|
||||||
const description = entries.length === 1
|
const description = entries.length === 1
|
||||||
? t("settings.optimizations.updateOneDescription", { functionName: entries[0].function, source: entries[0].source })
|
? t("settings.optimizations.updateOneDescription", { functionName: entries[0].function, source: entries[0].source })
|
||||||
@@ -2142,7 +2143,7 @@ export function Settings() {
|
|||||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
||||||
hasUpdate ? 'bg-purple-400' : (isDeprecated ? 'bg-amber-500' : 'bg-green-500')
|
hasUpdate ? 'bg-purple-400' : (isDeprecated ? 'bg-amber-500' : 'bg-green-500')
|
||||||
}`} />
|
}`} />
|
||||||
<span className="text-sm font-medium truncate">{tool.name}</span>
|
<span className="text-sm font-medium truncate">{optimizationToolName(tool.key, tool.name, t)}</span>
|
||||||
{isDeprecated && (
|
{isDeprecated && (
|
||||||
<span className="text-[9px] uppercase tracking-wider text-amber-500 bg-amber-500/10 border border-amber-500/30 px-1.5 py-0.5 rounded flex-shrink-0">
|
<span className="text-[9px] uppercase tracking-wider text-amber-500 bg-amber-500/10 border border-amber-500/30 px-1.5 py-0.5 rounded flex-shrink-0">
|
||||||
{t("settings.optimizations.legacy")}
|
{t("settings.optimizations.legacy")}
|
||||||
@@ -2158,7 +2159,7 @@ export function Settings() {
|
|||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleSingleToolUpdate(tool) }}
|
onClick={(e) => { e.stopPropagation(); handleSingleToolUpdate(tool) }}
|
||||||
className="text-purple-300 hover:text-purple-200 transition-colors"
|
className="text-purple-300 hover:text-purple-200 transition-colors"
|
||||||
title={t("settings.optimizations.updateToolTitle", { name: tool.name, version: tool.available_version || "?" })}
|
title={t("settings.optimizations.updateToolTitle", { name: optimizationToolName(tool.key, tool.name, t), version: tool.available_version || "?" })}
|
||||||
>
|
>
|
||||||
<ArrowUpCircle className="h-4 w-4" />
|
<ArrowUpCircle className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@@ -2318,13 +2319,13 @@ export function Settings() {
|
|||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<span className="text-sm font-medium">{tool.name}</span>
|
<span className="text-sm font-medium">{optimizationToolName(tool.key, tool.name, t)}</span>
|
||||||
<span className="text-[10px] text-purple-300 bg-purple-500/15 border border-purple-500/30 px-1.5 py-0.5 rounded font-mono">
|
<span className="text-[10px] text-purple-300 bg-purple-500/15 border border-purple-500/30 px-1.5 py-0.5 rounded font-mono">
|
||||||
v{tool.version || '1.0'} → v{tool.available_version || '?'}
|
v{tool.version || '1.0'} → v{tool.available_version || '?'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{tool.description && (
|
{tool.description && (
|
||||||
<p className="text-xs text-muted-foreground mt-1 leading-snug">{tool.description}</p>
|
<p className="text-xs text-muted-foreground mt-1 leading-snug">{optimizationDescription(tool.description, t)}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
* API Configuration for ProxMenux Monitor
|
* API Configuration for ProxMenux Monitor
|
||||||
* Handles API URL generation with automatic proxy detection
|
* Handles API URL generation with automatic proxy detection
|
||||||
*/
|
*/
|
||||||
|
// Resolve wording only for a generic transport failure. Normal responses and
|
||||||
|
// backend error payloads keep the transport free of locale-loading work.
|
||||||
|
async function apiFailure(...args: Parameters<typeof import("./api-errors").apiFailure>): Promise<string> {
|
||||||
|
const messages = await import("./api-errors")
|
||||||
|
return messages.apiFailure(...args)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Server Port Configuration
|
* API Server Port Configuration
|
||||||
@@ -114,7 +120,7 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
hadToken = true
|
hadToken = true
|
||||||
}
|
}
|
||||||
if (!hadToken) {
|
if (!hadToken) {
|
||||||
throw new Error(`Unauthorized: ${endpoint}`)
|
throw new Error(await apiFailure("unauthorized", { endpoint }))
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem("proxmenux-auth-token")
|
localStorage.removeItem("proxmenux-auth-token")
|
||||||
@@ -131,7 +137,7 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
window.location.reload()
|
window.location.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`Unauthorized: ${endpoint}`)
|
throw new Error(await apiFailure("unauthorized", { endpoint }))
|
||||||
}
|
}
|
||||||
// Try to surface the backend's JSON error payload instead of a
|
// Try to surface the backend's JSON error payload instead of a
|
||||||
// bare `500 INTERNAL SERVER ERROR`. The Flask routes consistently
|
// bare `500 INTERNAL SERVER ERROR`. The Flask routes consistently
|
||||||
@@ -168,7 +174,7 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
throw parseErr
|
throw parseErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`)
|
throw new Error(await apiFailure("http", { status: response.status, statusText: response.statusText }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check content type to ensure we're getting JSON
|
// Check content type to ensure we're getting JSON
|
||||||
@@ -176,13 +182,13 @@ export async function fetchApi<T>(endpoint: string, options?: RequestInit): Prom
|
|||||||
if (!contentType || !contentType.includes("application/json")) {
|
if (!contentType || !contentType.includes("application/json")) {
|
||||||
const text = await response.text()
|
const text = await response.text()
|
||||||
console.error("fetchApi: Expected JSON but got:", contentType, "- Body preview:", text.substring(0, 200))
|
console.error("fetchApi: Expected JSON but got:", contentType, "- Body preview:", text.substring(0, 200))
|
||||||
throw new Error(`Expected JSON response but got ${contentType || "unknown content type"}`)
|
throw new Error(await apiFailure("unexpectedContentType", { contentType: contentType || await apiFailure("unknownContentType") }))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await response.json()
|
return await response.json()
|
||||||
} catch (jsonError) {
|
} catch (jsonError) {
|
||||||
console.error("fetchApi: JSON parse error for", endpoint, "-", jsonError)
|
console.error("fetchApi: JSON parse error for", endpoint, "-", jsonError)
|
||||||
throw new Error(`Invalid JSON response from ${endpoint}`)
|
throw new Error(await apiFailure("invalidJson", { endpoint }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MESSAGE_CATALOG } from "./i18n/catalogs"
|
||||||
|
import { detectBrowserLanguage, isSupportedLanguage, LANGUAGE_STORAGE_KEY } from "./i18n/languages"
|
||||||
|
|
||||||
|
type Failure = keyof (typeof import("../messages/en/common.json"))["apiErrors"]
|
||||||
|
|
||||||
|
// fetchApi also runs outside React hooks. Read the provider's current document
|
||||||
|
// language, with its stored/browser preference as a fallback.
|
||||||
|
export function apiFailure(key: Failure, params: Record<string, string | number> = {}): string {
|
||||||
|
let preference = typeof document === "undefined" ? "" : document.documentElement?.lang
|
||||||
|
if (!isSupportedLanguage(preference) && typeof window !== "undefined") {
|
||||||
|
try { preference = window.localStorage.getItem(LANGUAGE_STORAGE_KEY) || "" } catch { /* private browsing */ }
|
||||||
|
}
|
||||||
|
const language = isSupportedLanguage(preference) ? preference : detectBrowserLanguage()
|
||||||
|
const messages = MESSAGE_CATALOG[language].apiErrors
|
||||||
|
const localized = messages && typeof messages === "object" ? (messages as Record<string, unknown>)[key] : undefined
|
||||||
|
const fallback = (MESSAGE_CATALOG.en.apiErrors as Record<Failure, string>)[key]
|
||||||
|
const template = typeof localized === "string" ? localized : fallback
|
||||||
|
return template.replace(/\{(\w+)\}/g, (match, name) => params[name] === undefined ? match : String(params[name]))
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import enMessages from "../../messages/en/common.json"
|
||||||
|
import deMessages from "../../messages/de/common.json"
|
||||||
|
import esMessages from "../../messages/es/common.json"
|
||||||
|
import frMessages from "../../messages/fr/common.json"
|
||||||
|
import itMessages from "../../messages/it/common.json"
|
||||||
|
import ptMessages from "../../messages/pt/common.json"
|
||||||
|
import skMessages from "../../messages/sk/common.json"
|
||||||
|
import svMessages from "../../messages/sv/common.json"
|
||||||
|
import type { LanguageCode } from "./languages"
|
||||||
|
|
||||||
|
type MessageTree = Record<string, unknown>
|
||||||
|
|
||||||
|
// Shared by React translations and error handling outside React hooks.
|
||||||
|
export const MESSAGE_CATALOG: Record<LanguageCode, MessageTree> = {
|
||||||
|
en: enMessages as MessageTree,
|
||||||
|
de: deMessages as MessageTree,
|
||||||
|
es: esMessages as MessageTree,
|
||||||
|
fr: frMessages as MessageTree,
|
||||||
|
it: itMessages as MessageTree,
|
||||||
|
pt: ptMessages as MessageTree,
|
||||||
|
sk: skMessages as MessageTree,
|
||||||
|
sv: svMessages as MessageTree,
|
||||||
|
}
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
|
||||||
import enMessages from "../../messages/en/common.json"
|
import { MESSAGE_CATALOG } from "./catalogs"
|
||||||
import deMessages from "../../messages/de/common.json"
|
|
||||||
import esMessages from "../../messages/es/common.json"
|
|
||||||
import frMessages from "../../messages/fr/common.json"
|
|
||||||
import itMessages from "../../messages/it/common.json"
|
|
||||||
import ptMessages from "../../messages/pt/common.json"
|
|
||||||
import skMessages from "../../messages/sk/common.json"
|
|
||||||
import svMessages from "../../messages/sv/common.json"
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE,
|
||||||
LANGUAGE_STORAGE_KEY,
|
LANGUAGE_STORAGE_KEY,
|
||||||
@@ -21,17 +14,6 @@ import {
|
|||||||
type MessageTree = Record<string, unknown>
|
type MessageTree = Record<string, unknown>
|
||||||
type TranslationParams = Record<string, string | number>
|
type TranslationParams = Record<string, string | number>
|
||||||
|
|
||||||
const MESSAGE_CATALOG: Record<LanguageCode, MessageTree> = {
|
|
||||||
en: enMessages as MessageTree,
|
|
||||||
de: deMessages as MessageTree,
|
|
||||||
es: esMessages as MessageTree,
|
|
||||||
fr: frMessages as MessageTree,
|
|
||||||
it: itMessages as MessageTree,
|
|
||||||
pt: ptMessages as MessageTree,
|
|
||||||
sk: skMessages as MessageTree,
|
|
||||||
sv: svMessages as MessageTree,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface I18nContextValue {
|
interface I18nContextValue {
|
||||||
language: LanguageCode
|
language: LanguageCode
|
||||||
setLanguage: (language: LanguageCode) => void
|
setLanguage: (language: LanguageCode) => void
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import enMessages from "../messages/en/common.json"
|
||||||
|
|
||||||
|
type Translate = (key: string, params?: Record<string, string | number>) => string
|
||||||
|
|
||||||
|
// Descriptions are live server metadata. Translate only exact known wording;
|
||||||
|
// a newer script description must remain visible rather than become stale text.
|
||||||
|
export function optimizationDescription(description: string, t: Translate): string {
|
||||||
|
const entries = Object.entries(enMessages.settings.optimizations.toolDescriptions)
|
||||||
|
const match = entries.find(([, english]) => english === description)
|
||||||
|
return match ? t(`settings.optimizations.toolDescriptions.${match[0]}`) : description
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown tools retain the server's name until their key is translated.
|
||||||
|
export function optimizationToolName(tool: string, fallback: string, t: Translate): string {
|
||||||
|
return Object.prototype.hasOwnProperty.call(enMessages.settings.optimizations.toolNames, tool)
|
||||||
|
? t(`settings.optimizations.toolNames.${tool}`)
|
||||||
|
: fallback
|
||||||
|
}
|
||||||
@@ -2407,7 +2407,9 @@
|
|||||||
"inactiveFooter": "Enable notifications to receive system, health and security alerts through the configured channels.",
|
"inactiveFooter": "Enable notifications to receive system, health and security alerts through the configured channels.",
|
||||||
"notificationLanguage": "Notification language",
|
"notificationLanguage": "Notification language",
|
||||||
"selectNotificationLanguage": "Select notification language",
|
"selectNotificationLanguage": "Select notification language",
|
||||||
"notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled."
|
"notificationLanguageHint": "Used for deterministic runtime notifications even when AI is disabled.",
|
||||||
|
"botTokenPlaceholder": "7595377878:AAGE6Fb2cy...",
|
||||||
|
"botTokenHelp": "The token can include the 'bot' prefix or be entered without it."
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"title": "AI enhancement",
|
"title": "AI enhancement",
|
||||||
@@ -2585,7 +2587,73 @@
|
|||||||
"availableUpdates": "Available updates",
|
"availableUpdates": "Available updates",
|
||||||
"availableUpdatesDescription": "{count} optimizations can be updated to a newer version.",
|
"availableUpdatesDescription": "{count} optimizations can be updated to a newer version.",
|
||||||
"selectedCount": "{selected} of {total} selected",
|
"selectedCount": "{selected} of {total} selected",
|
||||||
"updateSelected": "Update selected"
|
"updateSelected": "Update selected",
|
||||||
|
"toolDescriptions": {
|
||||||
|
"subscriptionAuto": "Patch the Proxmox web UI to suppress the subscription dialog and register a successful patch.",
|
||||||
|
"timeSync": "Detect timezone from public IP and enable systemd time sync (NTP).",
|
||||||
|
"aptLanguages": "Stop APT from downloading translation files to speed up updates.",
|
||||||
|
"journald": "Cap journald size, raise rate limit and force info-level logging so the log viewer and Fail2Ban work.",
|
||||||
|
"logrotate": "Replace logrotate.conf with a Log2RAM-friendly profile (daily rotation, copytruncate).",
|
||||||
|
"systemLimits": "Raise inotify watches, file descriptors, process keys and PID limits to enterprise levels.",
|
||||||
|
"memory": "Tune swappiness, dirty page ratios and compaction proactiveness for VM hosts without overriding the kernel's memory-overcommit policy.",
|
||||||
|
"kernelPanic": "Auto-reboot on kernel panic / oops / hardlockup; write crash dumps to /var/crash.",
|
||||||
|
"aptIpv4": "Force APT to use IPv4 to avoid stalls on hosts with broken IPv6 connectivity.",
|
||||||
|
"network": "Tune TCP buffers, somaxconn, IPv4 hardening and disable rp_filter on fw bridges (PVE 9 compatible).",
|
||||||
|
"bashrc": "Install and safely migrate the managed ProxMenux Bash prompt and aliases while preserving or selecting the short/full working-directory style.",
|
||||||
|
"log2ramAuto": "Install Log2RAM with size auto-tuned to host RAM (128M/256M/512M); SSD/M.2 detection skips on rotational disks.",
|
||||||
|
"zfsAutotrim": "Enable ZFS autotrim on detected pools and record only pools changed by ProxMenux.",
|
||||||
|
"persistentNetwork": "Pin NIC names to MAC addresses via systemd .link files so kernel updates don't shuffle interface names.",
|
||||||
|
"kexec": "Install kexec-tools and add a Ctrl+Alt+K hotkey + systemd unit for fast reboots that skip BIOS/POST.",
|
||||||
|
"amd": "Detect AMD EPYC/Ryzen CPUs and apply microcode + IOMMU + KVM-specific kernel boot params.",
|
||||||
|
"openVSwitch": "Install OpenVSwitch for software-defined networking inside VMs and containers.",
|
||||||
|
"tcpBbr": "Enable TCP Fast Open (clients + server) and BBR congestion control for better latency under load.",
|
||||||
|
"ceph": "Install Ceph (client + server packages) for distributed RBD/CephFS storage; PVE 8/9 aware repo selection.",
|
||||||
|
"zfsArc": "Cap ZFS ARC max using Proxmox VE's 10%-of-RAM policy (16 GiB ceiling), safely reconcile conflicting module settings and report the pool-size guideline before applying it.",
|
||||||
|
"zfsSnapshots": "Install zfs-auto-snapshot with cron schedules for hourly/daily/weekly/monthly snapshots.",
|
||||||
|
"rpc": "Disable rpcbind service/socket while preserving their exact previous systemd state for rollback.",
|
||||||
|
"pigz": "Replace gzip with pigz (parallel implementation) for faster vzdump backup compression.",
|
||||||
|
"guestAgent": "Detect the host's hypervisor (qemu/vmware/hyperv/virtualbox) and install the matching guest agent.",
|
||||||
|
"vfio": "Enable IOMMU and load VFIO modules to allow GPU/PCI passthrough into VMs.",
|
||||||
|
"motd": "Add the ProxMenux MOTD banner while preserving the original file contents or absence for rollback.",
|
||||||
|
"subscriptionCustom": "Patch the Proxmox web UI to suppress the \"no valid subscription\" dialog (PVE 8 + 9 variants supported).",
|
||||||
|
"zram": "Enable an opt-in ZRAM swap profile sized to one tenth of RAM.",
|
||||||
|
"backupLimits": "Lift vzdump bandwidth/IO limits so backups run at the storage's real throughput.",
|
||||||
|
"ovh": "Detect OVH-rented hardware via whois lookup and install OVH Real-Time Monitoring (no-op on non-OVH).",
|
||||||
|
"ha": "Enable the Proxmox HA stack (pve-ha-lrm, pve-ha-crm, corosync) for cluster failover.",
|
||||||
|
"fastfetch": "Install Fastfetch system summary tool with the ProxMenux logo + status block as the SSH login banner.",
|
||||||
|
"figurine": "Install Figurine (ASCII-art hostname banner) and wire it into the SSH login flow.",
|
||||||
|
"log2ramCustom": "Install Log2RAM with user-chosen RAM size; prompts for size and SSD/M.2 awareness before applying.",
|
||||||
|
"systemUtilities": "Install selected system utilities and track only packages that were newly added by ProxMenux."
|
||||||
|
},
|
||||||
|
"toolNames": {
|
||||||
|
"subscription_banner": "Subscription Banner Removal",
|
||||||
|
"time_sync": "Time Synchronization",
|
||||||
|
"apt_languages": "APT Language Skip",
|
||||||
|
"journald": "Journald Optimization",
|
||||||
|
"logrotate": "Logrotate Optimization",
|
||||||
|
"system_limits": "System Limits Increase",
|
||||||
|
"memory_settings": "Memory Settings Optimization",
|
||||||
|
"zram_swap": "ZRAM Swap Profile",
|
||||||
|
"kernel_panic": "Kernel Panic Configuration",
|
||||||
|
"apt_ipv4": "APT IPv4 Force",
|
||||||
|
"kexec": "kexec for quick reboots",
|
||||||
|
"rpc": "RPC / rpcbind Disable",
|
||||||
|
"motd": "Custom MOTD Banner",
|
||||||
|
"system_utils": "System Utilities",
|
||||||
|
"network_optimization": "Network Optimizations",
|
||||||
|
"bashrc_custom": "Bashrc Customization",
|
||||||
|
"figurine": "Figurine",
|
||||||
|
"fastfetch": "Fastfetch",
|
||||||
|
"log2ram": "Log2ram (SSD Protection)",
|
||||||
|
"zfs_autotrim": "ZFS Autotrim",
|
||||||
|
"amd_fixes": "AMD CPU (Ryzen/EPYC) fixes",
|
||||||
|
"persistent_network": "Setting persistent network interfaces",
|
||||||
|
"vfio_iommu": "VFIO/IOMMU Passthrough",
|
||||||
|
"lvm_repair": "LVM PV Headers Repair",
|
||||||
|
"repo_cleanup": "Repository Cleanup",
|
||||||
|
"proxmox_repos": "Proxmox APT Repositories",
|
||||||
|
"entropy": "Entropy Generation (haveged)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"healthThresholds": {
|
"healthThresholds": {
|
||||||
"title": "Health Monitor Thresholds",
|
"title": "Health Monitor Thresholds",
|
||||||
@@ -6959,5 +7027,12 @@
|
|||||||
"notFound": "The node was not found. Reload the list and try again.",
|
"notFound": "The node was not found. Reload the list and try again.",
|
||||||
"detectedRemove": "Only nodes added by hand can be removed. Detected nodes can be hidden."
|
"detectedRemove": "Only nodes added by hand can be removed. Detected nodes can be hidden."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"apiErrors": {
|
||||||
|
"http": "API request failed: {status} {statusText}",
|
||||||
|
"unexpectedContentType": "Expected JSON response but got {contentType}",
|
||||||
|
"unknownContentType": "unknown content type",
|
||||||
|
"invalidJson": "Invalid JSON response from {endpoint}",
|
||||||
|
"unauthorized": "Unauthorized: {endpoint}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -820,7 +820,7 @@
|
|||||||
"idleTitle": "Nečítaj, kým ho nič nepoužíva, aby sa to mohlo roztočiť. Jeho teplota sa znova zobrazí, akonáhle sa disk používa.",
|
"idleTitle": "Nečítaj, kým ho nič nepoužíva, aby sa to mohlo roztočiť. Jeho teplota sa znova zobrazí, akonáhle sa disk používa.",
|
||||||
"diskExcluded": "Vylúčené",
|
"diskExcluded": "Vylúčené",
|
||||||
"diskExcludedTitle": "Vylúčené z periodických čítania v Nastaveniach.",
|
"diskExcludedTitle": "Vylúčené z periodických čítania v Nastaveniach.",
|
||||||
"temperatureNoDisks": "No disks",
|
"temperatureNoDisks": "Žiadne disky",
|
||||||
"temperatureNormal": "Teploty normálne",
|
"temperatureNormal": "Teploty normálne",
|
||||||
"temperatureHighCount": "Vysoké teploty: {count}",
|
"temperatureHighCount": "Vysoké teploty: {count}",
|
||||||
"temperatureCriticalCount": "Kritické teploty: {count}",
|
"temperatureCriticalCount": "Kritické teploty: {count}",
|
||||||
@@ -2279,7 +2279,9 @@
|
|||||||
"appriseSlackDocs": "Formáty URL a nastavenie Slack",
|
"appriseSlackDocs": "Formáty URL a nastavenie Slack",
|
||||||
"appriseMatrixSteps": "Použite účet pre oznámenia, ktorý môže odosielať správy do miestnosti. Nahraďte vzorového používateľa, heslo, domovský server a úplné ID miestnosti; špeciálne znaky v prihlasovacích údajoch zakódujte pre URL. Schéma matrixs používa HTTPS. Podporu tokenov, SSO a šifrovaných miestností v nainštalovanej verzii Apprise nájdete v dokumentácii.",
|
"appriseMatrixSteps": "Použite účet pre oznámenia, ktorý môže odosielať správy do miestnosti. Nahraďte vzorového používateľa, heslo, domovský server a úplné ID miestnosti; špeciálne znaky v prihlasovacích údajoch zakódujte pre URL. Schéma matrixs používa HTTPS. Podporu tokenov, SSO a šifrovaných miestností v nainštalovanej verzii Apprise nájdete v dokumentácii.",
|
||||||
"appriseMatrixDocs": "Formáty URL a nastavenie Matrix",
|
"appriseMatrixDocs": "Formáty URL a nastavenie Matrix",
|
||||||
"appriseServiceFinish": "Povoľte Apprise, zadajte svoju URL, vyberte kategórie oznámení alebo jednotlivé udalosti, uložte a potom použite Odoslať test. Tieto príklady nemenia uloženú URL. Tento kanál Apprise má jeden cieľ; pre viac služieb naraz použite API Apprise."
|
"appriseServiceFinish": "Povoľte Apprise, zadajte svoju URL, vyberte kategórie oznámení alebo jednotlivé udalosti, uložte a potom použite Odoslať test. Tieto príklady nemenia uloženú URL. Tento kanál Apprise má jeden cieľ; pre viac služieb naraz použite API Apprise.",
|
||||||
|
"botTokenPlaceholder": "7595377878:AAGE6Fb2cy...",
|
||||||
|
"botTokenHelp": "Token môže byť s prefixom „bot“ alebo bez neho."
|
||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"title": "AI vylepšenie",
|
"title": "AI vylepšenie",
|
||||||
@@ -2407,13 +2409,13 @@
|
|||||||
"emptyTitle": "Zatiaľ nie sú nainštalované žiadne systémové optimalizácie",
|
"emptyTitle": "Zatiaľ nie sú nainštalované žiadne systémové optimalizácie",
|
||||||
"emptyDescription": "Spustite ProxMenux a nastavte systémové optimalizácie",
|
"emptyDescription": "Spustite ProxMenux a nastavte systémové optimalizácie",
|
||||||
"installedTools": "Nainštalované nástroje",
|
"installedTools": "Nainštalované nástroje",
|
||||||
"activeCount": "{count} aktívne",
|
"activeCount": "Aktívne: {count}",
|
||||||
"viewUpdates": "Zobraziť dostupné aktualizácie",
|
"viewUpdates": "Zobraziť dostupné aktualizácie",
|
||||||
"updateCount": "{count} aktualizácií",
|
"updateCount": "Aktualizácie: {count}",
|
||||||
"updateOneTitle": "Aktualizovať: {name}",
|
"updateOneTitle": "Aktualizovať: {name}",
|
||||||
"updateManyTitle": "Aktualizovať {count} optimalizácií",
|
"updateManyTitle": "Aktualizácia optimalizácií ({count})",
|
||||||
"updateOneDescription": "Znova sa spustí {functionName} z toku {source}.",
|
"updateOneDescription": "Znova sa spustí {functionName} z toku {source}.",
|
||||||
"updateManyDescription": "Postupne sa spustí {count} post-install funkcií.",
|
"updateManyDescription": "Počet funkcií, ktoré sa postupne spustia: {count}.",
|
||||||
"legacySourceTitle": "Staršia optimalizácia - kliknutím zobrazíte zdroj",
|
"legacySourceTitle": "Staršia optimalizácia - kliknutím zobrazíte zdroj",
|
||||||
"sourceTitle": "Kliknutím zobrazíte zdrojový kód",
|
"sourceTitle": "Kliknutím zobrazíte zdrojový kód",
|
||||||
"sourceDialogTitle": "Zdrojový kód: {name}",
|
"sourceDialogTitle": "Zdrojový kód: {name}",
|
||||||
@@ -2455,9 +2457,75 @@
|
|||||||
"legacy": "staršie",
|
"legacy": "staršie",
|
||||||
"updateToolTitle": "Aktualizovať {name} na v{version}",
|
"updateToolTitle": "Aktualizovať {name} na v{version}",
|
||||||
"availableUpdates": "Dostupné aktualizácie",
|
"availableUpdates": "Dostupné aktualizácie",
|
||||||
"availableUpdatesDescription": "{count} optimalizácií sa dá aktualizovať na novšiu verziu.",
|
"availableUpdatesDescription": "Nástroje s dostupnou novšou verziou: {count}.",
|
||||||
"selectedCount": "Vybrané {selected} z {total}",
|
"selectedCount": "Vybrané {selected} z {total}",
|
||||||
"updateSelected": "Aktualizovať vybrané"
|
"updateSelected": "Aktualizovať vybrané",
|
||||||
|
"toolDescriptions": {
|
||||||
|
"subscriptionAuto": "Upraví web Proxmoxu tak, aby nezobrazoval upozornenie na predplatné, a zaznamená úspešnú úpravu.",
|
||||||
|
"timeSync": "Zistí časové pásmo podľa verejnej IP adresy a zapne synchronizáciu času cez systemd (NTP).",
|
||||||
|
"aptLanguages": "Vypne sťahovanie prekladových súborov APT a zrýchli aktualizácie.",
|
||||||
|
"journald": "Obmedzí veľkosť journald, zvýši limit správ a zapne úroveň info, aby fungoval prehliadač záznamov aj Fail2Ban.",
|
||||||
|
"logrotate": "Nahradí logrotate.conf profilom vhodným pre Log2RAM (denná rotácia, copytruncate).",
|
||||||
|
"systemLimits": "Zvýši limity sledovaní inotify, otvorených súborov, kľúčov procesov a PID pre náročnú prevádzku.",
|
||||||
|
"memory": "Upraví swappiness, podiel neuložených stránok a aktívne zhusťovanie pamäte pre VM servery bez zmeny pravidiel jadra pre nadmerné prideľovanie pamäte.",
|
||||||
|
"kernelPanic": "Zapne automatický reštart pri kernel panic, oops alebo hardlockup a uloží výpisy pádu do /var/crash.",
|
||||||
|
"aptIpv4": "Vynúti IPv4 pre APT, aby sa predišlo čakaniu pri nefunkčnom pripojení IPv6.",
|
||||||
|
"network": "Upraví TCP vyrovnávacie pamäte, somaxconn a zabezpečenie IPv4; vypne rp_filter na firewallových mostoch (kompatibilné s PVE 9).",
|
||||||
|
"bashrc": "Nainštaluje a bezpečne prenesie spravovaný Bash prompt a aliasy ProxMenuxu so zachovaním alebo výberom krátkej či úplnej cesty pracovného priečinka.",
|
||||||
|
"log2ramAuto": "Nainštaluje Log2RAM s veľkosťou podľa RAM servera (128M/256M/512M); pri rotačných diskoch vynechá detekciu SSD/M.2.",
|
||||||
|
"zfsAutotrim": "Zapne ZFS autotrim na nájdených pooloch a zaznamená iba pooly zmenené ProxMenuxom.",
|
||||||
|
"persistentNetwork": "Priradí názvy sieťových rozhraní k MAC adresám cez súbory systemd .link, aby ich aktualizácie jadra nemenili.",
|
||||||
|
"kexec": "Nainštaluje kexec-tools a pridá skratku Ctrl+Alt+K aj službu systemd na rýchly reštart s vynechaním BIOS/POST.",
|
||||||
|
"amd": "Rozpozná procesory AMD EPYC/Ryzen a nastaví mikrokód aj parametre jadra pre IOMMU a KVM.",
|
||||||
|
"openVSwitch": "Nainštaluje OpenVSwitch na softvérovo riadené siete vo VM a kontajneroch.",
|
||||||
|
"tcpBbr": "Zapne TCP Fast Open pre klienta aj server a riadenie zahltenia BBR na zníženie oneskorenia pri záťaži.",
|
||||||
|
"ceph": "Nainštaluje klientské aj serverové balíky Ceph pre úložisko RBD/CephFS; repozitár vyberie podľa PVE 8/9.",
|
||||||
|
"zfsArc": "Obmedzí ZFS ARC podľa pravidla Proxmox VE na 10 % RAM (najviac 16 GiB), bezpečne zosúladí konfliktné nastavenia modulu a pred použitím zobrazí odporúčanie podľa veľkosti poolu.",
|
||||||
|
"zfsSnapshots": "Nainštaluje zfs-auto-snapshot s hodinovými, dennými, týždennými a mesačnými plánmi v crone.",
|
||||||
|
"rpc": "Vypne službu aj socket rpcbind a zachová ich presný pôvodný stav systemd na prípadnú obnovu.",
|
||||||
|
"pigz": "Nahradí gzip paralelným pigz na rýchlejšiu kompresiu záloh vzdump.",
|
||||||
|
"guestAgent": "Rozpozná hypervízor servera (qemu/vmware/hyperv/virtualbox) a nainštaluje príslušného hosťovského agenta.",
|
||||||
|
"vfio": "Zapne IOMMU a načíta moduly VFIO na priame priradenie GPU/PCI do VM.",
|
||||||
|
"motd": "Pridá uvítaciu správu MOTD ProxMenuxu a na obnovu zachová pôvodný obsah súboru alebo jeho neprítomnosť.",
|
||||||
|
"subscriptionCustom": "Upraví web Proxmoxu tak, aby nezobrazoval upozornenie na neplatné predplatné (podporuje PVE 8 aj 9).",
|
||||||
|
"zram": "Zapne voliteľný profil swapu ZRAM s veľkosťou jednej desatiny RAM.",
|
||||||
|
"backupLimits": "Odstráni limity prenosu a I/O vzdump, aby zálohy využili skutočnú priepustnosť úložiska.",
|
||||||
|
"ovh": "Zistí hardvér prenajatý od OVH cez whois a nainštaluje OVH Real-Time Monitoring (mimo OVH nič nemení).",
|
||||||
|
"ha": "Zapne služby Proxmox HA (pve-ha-lrm, pve-ha-crm, corosync) na prevzatie prevádzky v klastri pri výpadku.",
|
||||||
|
"fastfetch": "Nainštaluje systémový prehľad Fastfetch s logom a stavom ProxMenuxu ako uvítanie pri prihlásení cez SSH.",
|
||||||
|
"figurine": "Nainštaluje Figurine na zobrazenie názvu servera pomocou ASCII grafiky pri prihlásení cez SSH.",
|
||||||
|
"log2ramCustom": "Nainštaluje Log2RAM s veľkosťou zvolenou používateľom; pred použitím sa opýta na veľkosť a zohľadní SSD/M.2.",
|
||||||
|
"systemUtilities": "Nainštaluje vybrané systémové nástroje a zaznamená iba balíky novo pridané ProxMenuxom."
|
||||||
|
},
|
||||||
|
"toolNames": {
|
||||||
|
"subscription_banner": "Odstránenie upozornenia na predplatné",
|
||||||
|
"time_sync": "Synchronizácia času",
|
||||||
|
"apt_languages": "Vynechanie jazykových balíkov APT",
|
||||||
|
"journald": "Optimalizácia journald",
|
||||||
|
"logrotate": "Optimalizácia logrotate",
|
||||||
|
"system_limits": "Zvýšenie systémových limitov",
|
||||||
|
"memory_settings": "Optimalizácia nastavení pamäte",
|
||||||
|
"zram_swap": "Profil swapu ZRAM",
|
||||||
|
"kernel_panic": "Nastavenie správania pri páde jadra",
|
||||||
|
"apt_ipv4": "Vynútenie IPv4 pre APT",
|
||||||
|
"kexec": "Rýchly reštart cez kexec",
|
||||||
|
"rpc": "Vypnutie RPC / rpcbind",
|
||||||
|
"motd": "Vlastná uvítacia správa MOTD",
|
||||||
|
"system_utils": "Systémové nástroje",
|
||||||
|
"network_optimization": "Optimalizácia siete",
|
||||||
|
"bashrc_custom": "Prispôsobenie bashrc",
|
||||||
|
"figurine": "Figurine",
|
||||||
|
"fastfetch": "Fastfetch",
|
||||||
|
"log2ram": "Log2ram (ochrana SSD)",
|
||||||
|
"zfs_autotrim": "ZFS Autotrim",
|
||||||
|
"amd_fixes": "Opravy pre procesory AMD (Ryzen/EPYC)",
|
||||||
|
"persistent_network": "Trvalé názvy sieťových rozhraní",
|
||||||
|
"vfio_iommu": "Priame priradenie zariadení (VFIO/IOMMU)",
|
||||||
|
"lvm_repair": "Oprava hlavičiek fyzických zväzkov LVM",
|
||||||
|
"repo_cleanup": "Vyčistenie repozitárov",
|
||||||
|
"proxmox_repos": "Repozitáre APT pre Proxmox",
|
||||||
|
"entropy": "Generovanie entropie (haveged)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"healthThresholds": {
|
"healthThresholds": {
|
||||||
"title": "Hranice kontroly stavu",
|
"title": "Hranice kontroly stavu",
|
||||||
@@ -7750,5 +7818,12 @@
|
|||||||
"notFound": "Uzol sa nenašiel. Obnov zoznam a skús to znova.",
|
"notFound": "Uzol sa nenašiel. Obnov zoznam a skús to znova.",
|
||||||
"detectedRemove": "Odstrániť môžeš iba ručne pridané uzly. Automaticky zistené uzly môžeš skryť."
|
"detectedRemove": "Odstrániť môžeš iba ručne pridané uzly. Automaticky zistené uzly môžeš skryť."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"apiErrors": {
|
||||||
|
"http": "Požiadavka na API zlyhala: {status} {statusText}",
|
||||||
|
"unexpectedContentType": "Očakávala sa odpoveď JSON, ale prišiel typ {contentType}",
|
||||||
|
"unknownContentType": "neznámy typ obsahu",
|
||||||
|
"invalidJson": "Odpoveď z {endpoint} obsahuje neplatný JSON",
|
||||||
|
"unauthorized": "Chýba platné prihlásenie: {endpoint}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-4
@@ -669,7 +669,7 @@
|
|||||||
"Bookstack is a free and open source Wiki designed for creating beautiful documentation. Featuring a simple, but powerful WYSIWYG editor it allows for teams to create detailed and useful documentation with ease.": "Bookstack je bezplatný a open source Wiki navrhnutý pre vytvorenie krásnej dokumentácie. Vďaka jednoduchému, ale výkonnému WYSIWYG editoru umožňuje tímom jednoducho vytvárať podrobnú a užitočnú dokumentáciu.",
|
"Bookstack is a free and open source Wiki designed for creating beautiful documentation. Featuring a simple, but powerful WYSIWYG editor it allows for teams to create detailed and useful documentation with ease.": "Bookstack je bezplatný a open source Wiki navrhnutý pre vytvorenie krásnej dokumentácie. Vďaka jednoduchému, ale výkonnému WYSIWYG editoru umožňuje tímom jednoducho vytvárať podrobnú a užitočnú dokumentáciu.",
|
||||||
"Boot Disk": "Bootovací disk",
|
"Boot Disk": "Bootovací disk",
|
||||||
"Boot artifacts regenerated — reboot the host to activate the merged config.": "Boot súbory boli znovu vytvorené - reštartujte host, aby sa zlúčené nastavenie aktivovalo.",
|
"Boot artifacts regenerated — reboot the host to activate the merged config.": "Boot súbory boli znovu vytvorené - reštartujte host, aby sa zlúčené nastavenie aktivovalo.",
|
||||||
"Boot disk:": "Boot disk:",
|
"Boot disk:": "Zavádzací disk:",
|
||||||
"Boot order set to": "Poradie bootovania nastavené na",
|
"Boot order set to": "Poradie bootovania nastavené na",
|
||||||
"Boot partition UUIDs from backup not found on this host:": "UUID boot oddielov zo zálohy sa na tomto hostovi nenašli:",
|
"Boot partition UUIDs from backup not found on this host:": "UUID boot oddielov zo zálohy sa na tomto hostovi nenašli:",
|
||||||
"Boot type (grub/zfs):": "Typ bootovania (grub/zfs):",
|
"Boot type (grub/zfs):": "Typ bootovania (grub/zfs):",
|
||||||
@@ -3826,7 +3826,7 @@
|
|||||||
"Mount Point": "Bod pripojenia",
|
"Mount Point": "Bod pripojenia",
|
||||||
"Mount Point ID": "ID mount pointu",
|
"Mount Point ID": "ID mount pointu",
|
||||||
"Mount Point Removal Summary:": "Zhrnutie odstránenia mount pointu:",
|
"Mount Point Removal Summary:": "Zhrnutie odstránenia mount pointu:",
|
||||||
"Mount Point:": "Mount point:",
|
"Mount Point:": "Bod pripojenia:",
|
||||||
"Mount Samba Share": "Pripojiť Samba zdieľanie",
|
"Mount Samba Share": "Pripojiť Samba zdieľanie",
|
||||||
"Mount Samba Share on Host": "Pripojiť Samba zdieľanie na hostovi",
|
"Mount Samba Share on Host": "Pripojiť Samba zdieľanie na hostovi",
|
||||||
"Mount USB disk?": "Pripojiť USB disk?",
|
"Mount USB disk?": "Pripojiť USB disk?",
|
||||||
@@ -3852,7 +3852,7 @@
|
|||||||
"Mount point not visible inside the container yet": "Mount point zatiaľ nie je vo vnútri kontajnera viditeľný",
|
"Mount point not visible inside the container yet": "Mount point zatiaľ nie je vo vnútri kontajnera viditeľný",
|
||||||
"Mount point ready:": "Mount point je pripravený:",
|
"Mount point ready:": "Mount point je pripravený:",
|
||||||
"Mount point removed successfully": "Mount point bol úspešne odstránený",
|
"Mount point removed successfully": "Mount point bol úspešne odstránený",
|
||||||
"Mount point:": "Mount point:",
|
"Mount point:": "Bod pripojenia:",
|
||||||
"Mount points added:": "Pridanie bodov pripojenia:",
|
"Mount points added:": "Pridanie bodov pripojenia:",
|
||||||
"Mount read-only": "Pripojiť iba na čítanie",
|
"Mount read-only": "Pripojiť iba na čítanie",
|
||||||
"Mount shares on HOST first": "Najprv pripojiť zdieľania na HOSTE",
|
"Mount shares on HOST first": "Najprv pripojiť zdieľania na HOSTE",
|
||||||
@@ -8365,5 +8365,117 @@
|
|||||||
"⚠ Disk data will NOT be erased.": "⚠ Dáta na disku sa NEVYMAŽÚ.",
|
"⚠ Disk data will NOT be erased.": "⚠ Dáta na disku sa NEVYMAŽÚ.",
|
||||||
"⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Disk sa odpojí a odstráni z /etc/fstab.",
|
"⚠ Disk will be unmounted and removed from /etc/fstab.": "⚠ Disk sa odpojí a odstráni z /etc/fstab.",
|
||||||
"⚠ The /etc/fstab entry will be removed.": "⚠ Záznam v /etc/fstab bude odstránený.",
|
"⚠ The /etc/fstab entry will be removed.": "⚠ Záznam v /etc/fstab bude odstránený.",
|
||||||
"⚠ The disk will be unmounted.": "⚠ Disk bude odpojený."
|
"⚠ The disk will be unmounted.": "⚠ Disk bude odpojený.",
|
||||||
|
"Allocated Cores:": "Pridelené jadrá:",
|
||||||
|
"Allocated RAM:": "Pridelená RAM:",
|
||||||
|
"Allocating virtual disk for %s...": "Prideľuje sa virtuálny disk pre %s...",
|
||||||
|
"BRIDGE": "SIEŤOVÝ MOST (BRIDGE)",
|
||||||
|
"CORE COUNT": "POČET JADIER",
|
||||||
|
"Choose": "Vyber",
|
||||||
|
"Choose BIOS Type": "Vyber typ BIOS",
|
||||||
|
"Choose Type": "Vyber typ",
|
||||||
|
"Code": "Kód",
|
||||||
|
"Configured VM description": "Popis VM je nastavený",
|
||||||
|
"Configured controller/NVMe as hostpci%s: %s": "Radič/NVMe je nastavený ako hostpci%s: %s",
|
||||||
|
"Configured import disk %s as %s": "Importovaný disk %s je nastavený ako %s",
|
||||||
|
"Configured loader disk as ide0": "Disk zavádzača je nastavený ako ide0",
|
||||||
|
"Configured virtual disk as %s, %sGB on %s": "Virtuálny disk je nastavený ako %s, %sGB na %s",
|
||||||
|
"Configuring Serial Port:": "Nastavenie sériového portu:",
|
||||||
|
"Controller/NVMe already present in VM config (%s)": "Radič/NVMe už je v nastaveniach VM (%s)",
|
||||||
|
"Create a %s": "Vytvorený %s",
|
||||||
|
"Created with ProxMenuX": "Vytvorené pomocou ProxMenuX",
|
||||||
|
"Creating a %s using the above advanced settings": "Vytvára sa %s s uvedenými pokročilými nastaveniami",
|
||||||
|
"Creating a %s using the above default settings": "Vytvára sa %s s uvedenými predvolenými nastaveniami",
|
||||||
|
"Creating images directory": "Vytvára sa priečinok s obrazmi",
|
||||||
|
"Creating virtual disk (format=raw) for %s...": "Vytvára sa virtuálny disk (formát raw) pre %s...",
|
||||||
|
"Custom Loader Image (from %s)": "Vlastný obraz zavádzača (z %s)",
|
||||||
|
"Default": "Predvolené",
|
||||||
|
"Do-Over": "Začať znova",
|
||||||
|
"Docs": "Dokumentácia",
|
||||||
|
"Downloaded %s to %s": "Stiahnuté %s do %s",
|
||||||
|
"EFI Disk Storage": "Úložisko EFI disku",
|
||||||
|
"EFI disk created (raw) and configured on %s": "EFI disk (raw) bol vytvorený a nastavený na %s",
|
||||||
|
"EFI disk created and configured on %s": "EFI disk bol vytvorený a nastavený na %s",
|
||||||
|
"EFI storage selection failed or was cancelled.": "Výber úložiska pre EFI disk zlyhal alebo bol zrušený.",
|
||||||
|
"Edition:": "Edícia:",
|
||||||
|
"Enter temporary folder path (default: %s):": "Zadaj cestu dočasného priečinka (predvolené: %s):",
|
||||||
|
"Exit-Script": "Ukončiť skript",
|
||||||
|
"Failed to allocate virtual disk %s": "Pridelenie virtuálneho disku %s zlyhalo",
|
||||||
|
"Failed to assign disk %s (%s) on %s": "Priradenie disku %s (%s) na %s zlyhalo",
|
||||||
|
"Failed to assign loader disk": "Priradenie disku zavádzača zlyhalo",
|
||||||
|
"Failed to assign loader disk to ide0": "Priradenie disku zavádzača k ide0 zlyhalo",
|
||||||
|
"Failed to configure controller/NVMe: %s": "Nastavenie radiča/NVMe zlyhalo: %s",
|
||||||
|
"Failed to configure import disk %s as %s": "Nastavenie importovaného disku %s ako %s zlyhalo",
|
||||||
|
"Failed to configure virtual disk as %s": "Nastavenie virtuálneho disku ako %s zlyhalo",
|
||||||
|
"Failed to install unzip": "Inštalácia unzip zlyhala",
|
||||||
|
"Failed to install:": "Inštalácia zlyhala:",
|
||||||
|
"HOSTNAME": "NÁZOV SERVERA",
|
||||||
|
"ID %s is already in use": "ID %s sa už používa",
|
||||||
|
"Images directory created: %s": "Priečinok s obrazmi bol vytvorený: %s",
|
||||||
|
"SeaBIOS (Legacy)": "SeaBIOS (starší režim BIOS)",
|
||||||
|
"Installed unzip successfully.": "unzip bol úspešne nainštalovaný.",
|
||||||
|
"Installing dependencies:": "Inštalujú sa závislosti:",
|
||||||
|
"Installing unzip...": "Inštaluje sa unzip...",
|
||||||
|
"Language:": "Jazyk:",
|
||||||
|
"Loader": "Zavádzač",
|
||||||
|
"Loader configured as boot device.": "Zavádzač je nastavený ako zariadenie na zavedenie systému.",
|
||||||
|
"Loader import failed. No disk detected in config.": "Import zavádzača zlyhal. V nastaveniach sa nenašiel disk.",
|
||||||
|
"Loader import failed. No disk detected.": "Import zavádzača zlyhal. Nenašiel sa disk.",
|
||||||
|
"Loader imported successfully to %s": "Zavádzač bol úspešne importovaný do %s",
|
||||||
|
"Loader storage selection failed or was cancelled.": "Výber úložiska pre zavádzač zlyhal alebo bol zrušený.",
|
||||||
|
"Loader:": "Zavádzač:",
|
||||||
|
"MAC ADDRESS": "MAC ADRESA",
|
||||||
|
"MTU SIZE": "VEĽKOSŤ MTU",
|
||||||
|
"Machine i440fx": "Stroj i440fx",
|
||||||
|
"Machine q35": "Stroj q35",
|
||||||
|
"No compatible images found in %s\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again.": "V %s sa nenašli podporované obrazy.\n\nPodporované formáty: .img, .iso, .qcow2, .vmdk\n\nPridaj obrazy a skús to znova.",
|
||||||
|
"No custom image selected": "Nebol vybraný vlastný obraz",
|
||||||
|
"No disks/controllers configured.": "Nie sú nastavené žiadne disky ani radiče.",
|
||||||
|
"Ready to create a %s?": "Chceš vytvoriť %s?",
|
||||||
|
"Retrieving the URL for the %s loader": "Zisťuje sa URL zavádzača %s",
|
||||||
|
"SELECT CUSTOM IMAGE": "VÝBER VLASTNÉHO OBRAZU",
|
||||||
|
"SELECT LOADER": "VÝBER ZAVÁDZAČA",
|
||||||
|
"SETTINGS": "NASTAVENIA",
|
||||||
|
"STORAGE PLAN": "PLÁN ÚLOŽISKA",
|
||||||
|
"Selected Loader:": "Vybraný zavádzač:",
|
||||||
|
"Start VM when completed:": "Spustiť VM po dokončení:",
|
||||||
|
"Starting ISO conversion...": "Spúšťa sa vytváranie ISO...",
|
||||||
|
"Synology VM Creator": "Vytvorenie VM Synology",
|
||||||
|
"This will create a New %s. Proceed?": "Vytvorí sa nový %s. Pokračovať?",
|
||||||
|
"Unable to detect a valid storage location for EFI disk.": "Nenašlo sa použiteľné úložisko pre EFI disk.",
|
||||||
|
"Unable to detect a valid storage location.": "Nenašlo sa použiteľné úložisko.",
|
||||||
|
"Using Advanced Settings": "Použijú sa pokročilé nastavenia",
|
||||||
|
"Using BIOS Type:": "Typ BIOS:",
|
||||||
|
"Using Bridge:": "Sieťový most (bridge):",
|
||||||
|
"Using CPU Model:": "Model CPU:",
|
||||||
|
"Using Custom Image:": "Vlastný obraz:",
|
||||||
|
"Using Default Settings": "Použijú sa predvolené nastavenia",
|
||||||
|
"Using Hostname:": "Názov servera:",
|
||||||
|
"Using Interface MTU Size:": "Veľkosť MTU rozhrania:",
|
||||||
|
"Using MAC Address:": "MAC adresa:",
|
||||||
|
"Using Machine Type:": "Typ stroja:",
|
||||||
|
"Using VLAN:": "VLAN:",
|
||||||
|
"Using Virtual Machine ID:": "ID virtuálneho stroja:",
|
||||||
|
"Using Vlan:": "VLAN:",
|
||||||
|
"VIRTUAL DISK": "VIRTUÁLNY DISK",
|
||||||
|
"VIRTUAL MACHINE ID": "ID VIRTUÁLNEHO STROJA",
|
||||||
|
"VM created with errors. Check configuration.": "VM bola vytvorená s chybami. Skontroluj jej nastavenia.",
|
||||||
|
"Virtual Machine ID:": "ID virtuálneho stroja:",
|
||||||
|
"Command not found.": "Príkaz sa nenašiel.",
|
||||||
|
"Custom Image": "Vlastný obraz",
|
||||||
|
"Error generating UUP download list.": "Vytvorenie zoznamu súborov UUP na stiahnutie zlyhalo.",
|
||||||
|
"Importing loader disk:": "Importuje sa disk zavádzača:",
|
||||||
|
"genisoimage/mkisofs not found.": "Nenašiel sa genisoimage ani mkisofs.",
|
||||||
|
"%s of %s updates failed — see the messages above.": "Zlyhalo %s z %s aktualizácií — pozri hlášky vyššie.",
|
||||||
|
"%s of %s updates failed.": "Zlyhalo %s z %s aktualizácií.",
|
||||||
|
"All %s update(s) completed successfully.": "Všetky aktualizácie (%s) sa úspešne dokončili.",
|
||||||
|
"Function '%s' is not defined in the %s flow.": "Funkcia '%s' nie je definovaná v postupe %s.",
|
||||||
|
"Press Enter to close this terminal...": "Stlač Enter na zatvorenie terminálu...",
|
||||||
|
"Update for %s completed.": "Aktualizácia %s je dokončená.",
|
||||||
|
"Update for %s exited with status %s.": "Aktualizácia %s skončila s návratovým kódom %s.",
|
||||||
|
"Updating %s (running %s from %s flow)...": "Aktualizuje sa %s (spúšťa sa %s z postupu %s)...",
|
||||||
|
"function '%s' is not defined in the %s flow.": "funkcia '%s' nie je definovaná v postupe %s.",
|
||||||
|
"invalid source '%s' (must be 'auto', 'custom', or 'detected')": "neplatný zdroj '%s' (musí byť 'auto', 'custom' alebo 'detected')",
|
||||||
|
"provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode).": "zadaj SOURCE_TYPE+FUNCTION_NAME pre jednu funkciu alebo FUNCTIONS_BATCH pre dávku.",
|
||||||
|
"update for %s exited with status %s.": "aktualizácia %s skončila s návratovým kódom %s."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ if command -v initialize_cache >/dev/null 2>&1; then
|
|||||||
initialize_cache
|
initialize_cache
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Keep standalone invocations usable when the shared utilities are unavailable.
|
||||||
|
if ! command -v translate >/dev/null 2>&1; then
|
||||||
|
translate() { printf '%s\n' "$1"; }
|
||||||
|
fi
|
||||||
|
|
||||||
if command -v show_proxmenux_logo >/dev/null 2>&1; then
|
if command -v show_proxmenux_logo >/dev/null 2>&1; then
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
fi
|
fi
|
||||||
@@ -60,7 +65,7 @@ if [[ -n "$FUNCTIONS_BATCH" ]]; then
|
|||||||
elif [[ -n "$SOURCE_TYPE" && -n "$FUNCTION_NAME" ]]; then
|
elif [[ -n "$SOURCE_TYPE" && -n "$FUNCTION_NAME" ]]; then
|
||||||
BATCH+=("${SOURCE_TYPE}:${FUNCTION_NAME}:${TOOL_KEY}")
|
BATCH+=("${SOURCE_TYPE}:${FUNCTION_NAME}:${TOOL_KEY}")
|
||||||
else
|
else
|
||||||
echo "ERROR: provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode)."
|
echo "ERROR: $(translate "provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode).")"
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -99,7 +104,7 @@ ensure_flow_loaded() {
|
|||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "ERROR: invalid source '$source_type' (must be 'auto', 'custom', or 'detected')"
|
echo "ERROR: $(printf "$(translate "invalid source '%s' (must be 'auto', 'custom', or 'detected')")" "$source_type")"
|
||||||
return 2
|
return 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -135,9 +140,9 @@ for entry in "${BATCH[@]}"; do
|
|||||||
[[ -z "$tkey" ]] && tkey="$fn"
|
[[ -z "$tkey" ]] && tkey="$fn"
|
||||||
|
|
||||||
if command -v msg_info2 >/dev/null 2>&1; then
|
if command -v msg_info2 >/dev/null 2>&1; then
|
||||||
msg_info2 "[$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..."
|
msg_info2 "[$INDEX/$TOTAL] $(printf "$(translate "Updating %s (running %s from %s flow)...")" "$tkey" "$fn" "$src")"
|
||||||
else
|
else
|
||||||
echo "[ProxMenux] [$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..."
|
echo "[ProxMenux] [$INDEX/$TOTAL] $(printf "$(translate "Updating %s (running %s from %s flow)...")" "$tkey" "$fn" "$src")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! ensure_flow_loaded "$src"; then
|
if ! ensure_flow_loaded "$src"; then
|
||||||
@@ -147,9 +152,9 @@ for entry in "${BATCH[@]}"; do
|
|||||||
|
|
||||||
if ! declare -F "$fn" >/dev/null 2>&1; then
|
if ! declare -F "$fn" >/dev/null 2>&1; then
|
||||||
if command -v msg_error >/dev/null 2>&1; then
|
if command -v msg_error >/dev/null 2>&1; then
|
||||||
msg_error "Function '$fn' is not defined in the ${src} flow."
|
msg_error "$(printf "$(translate "Function '%s' is not defined in the %s flow.")" "$fn" "$src")"
|
||||||
else
|
else
|
||||||
echo "ERROR: function '$fn' is not defined in the ${src} flow."
|
echo "ERROR: $(printf "$(translate "function '%s' is not defined in the %s flow.")" "$fn" "$src")"
|
||||||
fi
|
fi
|
||||||
FAILED=$((FAILED + 1))
|
FAILED=$((FAILED + 1))
|
||||||
continue
|
continue
|
||||||
@@ -162,15 +167,15 @@ for entry in "${BATCH[@]}"; do
|
|||||||
|
|
||||||
if [[ $RC -eq 0 ]]; then
|
if [[ $RC -eq 0 ]]; then
|
||||||
if command -v msg_ok >/dev/null 2>&1; then
|
if command -v msg_ok >/dev/null 2>&1; then
|
||||||
msg_ok "Update for ${tkey} completed."
|
msg_ok "$(printf "$(translate "Update for %s completed.")" "$tkey")"
|
||||||
else
|
else
|
||||||
echo "[ProxMenux] Update for ${tkey} completed."
|
echo "[ProxMenux] $(printf "$(translate "Update for %s completed.")" "$tkey")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if command -v msg_error >/dev/null 2>&1; then
|
if command -v msg_error >/dev/null 2>&1; then
|
||||||
msg_error "Update for ${tkey} exited with status $RC."
|
msg_error "$(printf "$(translate "Update for %s exited with status %s.")" "$tkey" "$RC")"
|
||||||
else
|
else
|
||||||
echo "ERROR: update for ${tkey} exited with status $RC."
|
echo "ERROR: $(printf "$(translate "update for %s exited with status %s.")" "$tkey" "$RC")"
|
||||||
fi
|
fi
|
||||||
FAILED=$((FAILED + 1))
|
FAILED=$((FAILED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -178,22 +183,22 @@ done
|
|||||||
|
|
||||||
if [[ $FAILED -eq 0 ]]; then
|
if [[ $FAILED -eq 0 ]]; then
|
||||||
if command -v msg_success >/dev/null 2>&1; then
|
if command -v msg_success >/dev/null 2>&1; then
|
||||||
msg_success "All $TOTAL update(s) completed successfully."
|
msg_success "$(printf "$(translate "All %s update(s) completed successfully.")" "$TOTAL")"
|
||||||
else
|
else
|
||||||
echo "[ProxMenux] All $TOTAL update(s) completed successfully."
|
echo "[ProxMenux] $(printf "$(translate "All %s update(s) completed successfully.")" "$TOTAL")"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if command -v msg_warn >/dev/null 2>&1; then
|
if command -v msg_warn >/dev/null 2>&1; then
|
||||||
msg_warn "$FAILED of $TOTAL updates failed — see the messages above."
|
msg_warn "$(printf "$(translate "%s of %s updates failed — see the messages above.")" "$FAILED" "$TOTAL")"
|
||||||
else
|
else
|
||||||
echo "WARNING: $FAILED of $TOTAL updates failed."
|
echo "WARNING: $(printf "$(translate "%s of %s updates failed.")" "$FAILED" "$TOTAL")"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v msg_success >/dev/null 2>&1; then
|
if command -v msg_success >/dev/null 2>&1; then
|
||||||
msg_success "Press Enter to close this terminal..."
|
msg_success "$(translate "Press Enter to close this terminal...")"
|
||||||
else
|
else
|
||||||
echo "Press Enter to close this terminal..."
|
echo "$(translate "Press Enter to close this terminal...")"
|
||||||
fi
|
fi
|
||||||
read -r 2>/dev/null || true
|
read -r 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
@@ -125,18 +125,18 @@ function run_uupdump_creator() {
|
|||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
echo -e
|
echo -e
|
||||||
msg_info "$(translate "Installing dependencies: ${MISSING[*]}")"
|
msg_info "$(translate "Installing dependencies:") ${MISSING[*]}"
|
||||||
apt-get update -qq >/dev/null 2>&1
|
apt-get update -qq >/dev/null 2>&1
|
||||||
msg_ok "$(translate "All dependencies installed and verified.")"
|
msg_ok "$(translate "All dependencies installed and verified.")"
|
||||||
# Build dependencies land on the host, so they are recorded;
|
# Build dependencies land on the host, so they are recorded;
|
||||||
# building the ISO is an operation and is not.
|
# building the ISO is an operation and is not.
|
||||||
if declare -F pmx_install_pkg >/dev/null 2>&1; then
|
if declare -F pmx_install_pkg >/dev/null 2>&1; then
|
||||||
if ! PMX_JOURNAL_SOURCE="uup_dump_iso_creator.sh" pmx_install_pkg "${MISSING[@]}"; then
|
if ! PMX_JOURNAL_SOURCE="uup_dump_iso_creator.sh" pmx_install_pkg "${MISSING[@]}"; then
|
||||||
msg_error "$(translate "Failed to install: ${MISSING[*]}")"
|
msg_error "$(translate "Failed to install:") ${MISSING[*]}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
elif ! apt-get install -y "${MISSING[@]}" >/dev/null 2>&1; then
|
elif ! apt-get install -y "${MISSING[@]}" >/dev/null 2>&1; then
|
||||||
msg_error "$(translate "Failed to install: ${MISSING[*]}")"
|
msg_error "$(translate "Failed to install:") ${MISSING[*]}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -151,7 +151,7 @@ function run_uupdump_creator() {
|
|||||||
#if [[ ${#FAILED[@]} -eq 0 ]]; then
|
#if [[ ${#FAILED[@]} -eq 0 ]]; then
|
||||||
# msg_ok "$(translate "All dependencies installed and verified.")"
|
# msg_ok "$(translate "All dependencies installed and verified.")"
|
||||||
#else
|
#else
|
||||||
# msg_error "$(translate "Missing commands after installation: ${FAILED[*]}")"
|
# msg_error "$(translate "Missing commands after installation:") ${FAILED[*]}"
|
||||||
# exit 1
|
# exit 1
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ function run_uupdump_creator() {
|
|||||||
######################################
|
######################################
|
||||||
|
|
||||||
DEFAULT_TMP="/root/uup-temp"
|
DEFAULT_TMP="/root/uup-temp"
|
||||||
USER_INPUT=$(dialog --backtitle "ProxMenux" --inputbox "Enter temporary folder path (default: $DEFAULT_TMP):" 10 60 "$DEFAULT_TMP" 3>&1 1>&2 2>&3)
|
USER_INPUT=$(dialog --backtitle "ProxMenux" --inputbox "$(printf "$(translate "Enter temporary folder path (default: %s):")" "$DEFAULT_TMP")" 10 60 "$DEFAULT_TMP" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
@@ -237,10 +237,10 @@ function run_uupdump_creator() {
|
|||||||
echo -e
|
echo -e
|
||||||
echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}"
|
echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}"
|
||||||
echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}"
|
echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}"
|
||||||
echo -e " ${BGN}🌐 Language:${CL} ${DGN}$LANG${CL}"
|
echo -e " ${BGN}🌐 $(translate "Language:")${CL} ${DGN}$LANG${CL}"
|
||||||
echo -e " ${BGN}💿 Edition:${CL} ${DGN}$EDITION${CL}"
|
echo -e " ${BGN}💿 $(translate "Edition:")${CL} ${DGN}$EDITION${CL}"
|
||||||
echo -e " ${BGN}🖥️ Architecture:${CL} ${DGN}$ARCH${CL}"
|
echo -e " ${BGN}🖥️ $(translate "Architecture:")${CL} ${DGN}$ARCH${CL}"
|
||||||
echo -e " ${BGN}📁 Destination:${CL} ${DGN}$OUT_DIR${CL}"
|
echo -e " ${BGN}📁 $(translate "Destination:")${CL} ${DGN}$OUT_DIR${CL}"
|
||||||
echo -e "${BGN}===============================================${CL}\n"
|
echo -e "${BGN}===============================================${CL}\n"
|
||||||
|
|
||||||
|
|
||||||
@@ -262,10 +262,10 @@ mkdir -p files
|
|||||||
echo "https://git.uupdump.net/uup-dump/converter/archive/refs/heads/master.tar.gz" > files/converter_multi
|
echo "https://git.uupdump.net/uup-dump/converter/archive/refs/heads/master.tar.gz" > files/converter_multi
|
||||||
|
|
||||||
for prog in aria2c cabextract wimlib-imagex chntpw; do
|
for prog in aria2c cabextract wimlib-imagex chntpw; do
|
||||||
which \$prog &>/dev/null || { echo "\$prog not found."; exit 1; }
|
which \$prog &>/dev/null || { printf '%s\n' "\$prog: $(translate "Command not found.")"; exit 1; }
|
||||||
done
|
done
|
||||||
|
|
||||||
which genisoimage &>/dev/null || which mkisofs &>/dev/null || { echo "genisoimage/mkisofs not found."; exit 1; }
|
which genisoimage &>/dev/null || which mkisofs &>/dev/null || { echo "$(translate "genisoimage/mkisofs not found.")"; exit 1; }
|
||||||
|
|
||||||
destDir="UUPs"
|
destDir="UUPs"
|
||||||
tempScript="aria2_script.\$RANDOM.txt"
|
tempScript="aria2_script.\$RANDOM.txt"
|
||||||
@@ -277,7 +277,7 @@ aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download
|
|||||||
-o"\$tempScript" --allow-overwrite=true --auto-file-renaming=false \\
|
-o"\$tempScript" --allow-overwrite=true --auto-file-renaming=false \\
|
||||||
"https://uupdump.net/get.php?id=$BUILD_ID&pack=$LANG&edition=$EDITION&aria2=2" || exit 1
|
"https://uupdump.net/get.php?id=$BUILD_ID&pack=$LANG&edition=$EDITION&aria2=2" || exit 1
|
||||||
|
|
||||||
grep '#UUPDUMP_ERROR:' "\$tempScript" && { echo "❌ Error generating UUP download list."; exit 1; }
|
grep '#UUPDUMP_ERROR:' "\$tempScript" && { echo "❌ $(translate "Error generating UUP download list.")"; exit 1; }
|
||||||
|
|
||||||
aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download.log" \\
|
aria2c --no-conf --console-log-level=warn --log-level=info --log="aria2_download.log" \\
|
||||||
-x16 -s16 -j5 -c -R -d"\$destDir" -i"\$tempScript" || exit 1
|
-x16 -s16 -j5 -c -R -d"\$destDir" -i"\$tempScript" || exit 1
|
||||||
@@ -293,7 +293,7 @@ EOF
|
|||||||
[[ -z "$UUP_FOLDER" ]] && msg_error "$(translate "No UUP folder found.")" && exit 1
|
[[ -z "$UUP_FOLDER" ]] && msg_error "$(translate "No UUP folder found.")" && exit 1
|
||||||
|
|
||||||
echo -e "\n${GN}=======================================${CL}"
|
echo -e "\n${GN}=======================================${CL}"
|
||||||
echo -e " 💿 ${GN}Starting ISO conversion...${CL}"
|
echo -e " 💿 ${GN}$(translate "Starting ISO conversion...")${CL}"
|
||||||
echo -e "${GN}=======================================${CL}\n"
|
echo -e "${GN}=======================================${CL}\n"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+128
-128
@@ -90,7 +90,7 @@ VM_STORAGE_IOMMU_PENDING_REBOOT=0
|
|||||||
|
|
||||||
function exit_script() {
|
function exit_script() {
|
||||||
clear
|
clear
|
||||||
if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then
|
if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(printf "$(translate "This will create a New %s. Proceed?")" "$NAME")" 10 58; then
|
||||||
start_script
|
start_script
|
||||||
else
|
else
|
||||||
clear
|
clear
|
||||||
@@ -104,7 +104,7 @@ function exit_script() {
|
|||||||
function header_info() {
|
function header_info() {
|
||||||
clear
|
clear
|
||||||
show_proxmenux_logo
|
show_proxmenux_logo
|
||||||
msg_title "Synology VM Creator"
|
msg_title "$(translate "Synology VM Creator")"
|
||||||
}
|
}
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
|
|
||||||
@@ -211,13 +211,13 @@ function run_gpu_passthrough_wizard() {
|
|||||||
# start Script
|
# start Script
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
function start_script() {
|
function start_script() {
|
||||||
if (whiptail --backtitle "ProxMenuX" --title "SETTINGS" --yesno "$(translate "Use Default Settings?")" --no-button Advanced 10 58); then
|
if (whiptail --backtitle "ProxMenuX" --title "$(translate "SETTINGS")" --yesno "$(translate "Use Default Settings?")" --no-button "$(translate "Advanced")" 10 58); then
|
||||||
header_info
|
header_info
|
||||||
echo -e "${DEF}Using Default Settings${CL}"
|
echo -e "${DEF}$(translate "Using Default Settings")${CL}"
|
||||||
default_settings || return 1
|
default_settings || return 1
|
||||||
else
|
else
|
||||||
header_info
|
header_info
|
||||||
echo -e "${CUS}Using Advanced Settings${CL}"
|
echo -e "${CUS}$(translate "Using Advanced Settings")${CL}"
|
||||||
advanced_settings || return 1
|
advanced_settings || return 1
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
@@ -247,21 +247,21 @@ function default_settings() {
|
|||||||
SERIAL_PORT="socket"
|
SERIAL_PORT="socket"
|
||||||
START_VM="no"
|
START_VM="no"
|
||||||
|
|
||||||
echo -e " ${TAB}${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using Virtual Machine ID:") ${BGN}${VMID}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using Machine Type: ${BGN}q35${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using Machine Type:") ${BGN}q35${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using BIOS Type:") ${BGN}OVMF (UEFI)${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using Hostname: ${BGN}${HN}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using Hostname:") ${BGN}${HN}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using CPU Model: ${BGN}Host${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using CPU Model:") ${BGN}Host${CL}"
|
||||||
echo -e " ${TAB}${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Allocated Cores:") ${BGN}${CORE_COUNT}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Allocated RAM:") ${BGN}${RAM_SIZE}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using Bridge: ${BGN}${BRG}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using Bridge:") ${BGN}${BRG}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using MAC Address:") ${BGN}${MAC}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using VLAN: ${BGN}Default${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using VLAN:") ${BGN}$(translate "Default")${CL}"
|
||||||
echo -e " ${TAB}${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Using Interface MTU Size:") ${BGN}$(translate "Default")${CL}"
|
||||||
echo -e " ${TAB}${DGN}Configuring Serial Port: ${BGN}${SERIAL_PORT}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Configuring Serial Port:") ${BGN}${SERIAL_PORT}${CL}"
|
||||||
echo -e " ${TAB}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
|
echo -e " ${TAB}${DGN}$(translate "Start VM when completed:") ${BGN}${START_VM}${CL}"
|
||||||
echo -e
|
echo -e
|
||||||
echo -e "${DEF}Creating a $NAME using the above default settings${CL}"
|
echo -e "${DEF}$(printf "$(translate "Creating a %s using the above default settings")" "$NAME")${CL}"
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
select_disk_type || return 1
|
select_disk_type || return 1
|
||||||
@@ -278,16 +278,16 @@ function default_settings() {
|
|||||||
function advanced_settings() {
|
function advanced_settings() {
|
||||||
# VM ID Selection
|
# VM ID Selection
|
||||||
while true; do
|
while true; do
|
||||||
if VMID=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Virtual Machine ID")" 8 58 $NEXTID --title "VIRTUAL MACHINE ID" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if VMID=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Virtual Machine ID")" 8 58 $NEXTID --title "$(translate "VIRTUAL MACHINE ID")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z "$VMID" ]; then
|
if [ -z "$VMID" ]; then
|
||||||
VMID="$NEXTID"
|
VMID="$NEXTID"
|
||||||
fi
|
fi
|
||||||
if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
|
if pct status "$VMID" &>/dev/null || qm status "$VMID" &>/dev/null; then
|
||||||
echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
|
echo -e "${CROSS}${RD} $(printf "$(translate "ID %s is already in use")" "$VMID")${CL}"
|
||||||
sleep 1
|
sleep 1
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
|
echo -e "${DGN}$(translate "Virtual Machine ID:") ${BGN}$VMID${CL}"
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
@@ -295,16 +295,16 @@ function advanced_settings() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
# Machine Type Selection
|
# Machine Type Selection
|
||||||
if MACH=$(whiptail --backtitle "ProxMenuX" --title "$(translate "MACHINE TYPE")" --radiolist --cancel-button Exit-Script "Choose Type" 10 58 2 \
|
if MACH=$(whiptail --backtitle "ProxMenuX" --title "$(translate "MACHINE TYPE")" --radiolist --cancel-button "$(translate "Exit-Script")" "$(translate "Choose Type")" 10 58 2 \
|
||||||
"q35" "Machine q35" ON \
|
"q35" "$(translate "Machine q35")" ON \
|
||||||
"i440fx" "Machine i440fx" OFF \
|
"i440fx" "$(translate "Machine i440fx")" OFF \
|
||||||
3>&1 1>&2 2>&3); then
|
3>&1 1>&2 2>&3); then
|
||||||
if [ $MACH = q35 ]; then
|
if [ $MACH = q35 ]; then
|
||||||
echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
|
echo -e "${DGN}$(translate "Using Machine Type:") ${BGN}$MACH${CL}"
|
||||||
FORMAT=""
|
FORMAT=""
|
||||||
MACHINE=" -machine q35"
|
MACHINE=" -machine q35"
|
||||||
else
|
else
|
||||||
echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
|
echo -e "${DGN}$(translate "Using Machine Type:") ${BGN}$MACH${CL}"
|
||||||
FORMAT=",efitype=4m"
|
FORMAT=",efitype=4m"
|
||||||
MACHINE=""
|
MACHINE=""
|
||||||
fi
|
fi
|
||||||
@@ -313,15 +313,15 @@ function advanced_settings() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# BIOS Type Selection
|
# BIOS Type Selection
|
||||||
if BIOS=$(whiptail --backtitle "ProxMenuX" --title "$(translate "BIOS TYPE")" --radiolist --cancel-button Exit-Script "Choose BIOS Type" 10 58 2 \
|
if BIOS=$(whiptail --backtitle "ProxMenuX" --title "$(translate "BIOS TYPE")" --radiolist --cancel-button "$(translate "Exit-Script")" "$(translate "Choose BIOS Type")" 10 58 2 \
|
||||||
"ovmf" "UEFI (OVMF)" ON \
|
"ovmf" "UEFI (OVMF)" ON \
|
||||||
"seabios" "SeaBIOS (Legacy)" OFF \
|
"seabios" "$(translate "SeaBIOS (Legacy)")" OFF \
|
||||||
3>&1 1>&2 2>&3); then
|
3>&1 1>&2 2>&3); then
|
||||||
if [ "$BIOS" = "seabios" ]; then
|
if [ "$BIOS" = "seabios" ]; then
|
||||||
echo -e "${DGN}Using BIOS Type: ${BGN}SeaBIOS${CL}"
|
echo -e "${DGN}$(translate "Using BIOS Type:") ${BGN}SeaBIOS${CL}"
|
||||||
BIOS_TYPE=" -bios seabios"
|
BIOS_TYPE=" -bios seabios"
|
||||||
else
|
else
|
||||||
echo -e "${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}"
|
echo -e "${DGN}$(translate "Using BIOS Type:") ${BGN}OVMF (UEFI)${CL}"
|
||||||
BIOS_TYPE=" -bios ovmf"
|
BIOS_TYPE=" -bios ovmf"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -329,28 +329,28 @@ function advanced_settings() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Hostname Selection
|
# Hostname Selection
|
||||||
if VM_NAME=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Hostname")" 8 58 Synology-DSM --title "HOSTNAME" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if VM_NAME=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Hostname")" 8 58 Synology-DSM --title "$(translate "HOSTNAME")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $VM_NAME ]; then
|
if [ -z $VM_NAME ]; then
|
||||||
HN="Synology-DSM"
|
HN="Synology-DSM"
|
||||||
echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
|
echo -e "${DGN}$(translate "Using Hostname:") ${BGN}$HN${CL}"
|
||||||
else
|
else
|
||||||
HN=$(echo ${VM_NAME,,} | tr -d ' ')
|
HN=$(echo ${VM_NAME,,} | tr -d ' ')
|
||||||
echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
|
echo -e "${DGN}$(translate "Using Hostname:") ${BGN}$HN${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# CPU Type Selection
|
# CPU Type Selection
|
||||||
if CPU_TYPE1=$(whiptail --backtitle "ProxMenuX" --title "$(translate "CPU MODEL")" --radiolist "Choose" --cancel-button Exit-Script 10 58 2 \
|
if CPU_TYPE1=$(whiptail --backtitle "ProxMenuX" --title "$(translate "CPU MODEL")" --radiolist "$(translate "Choose")" --cancel-button "$(translate "Exit-Script")" 10 58 2 \
|
||||||
"1" "Host" ON \
|
"1" "Host" ON \
|
||||||
"0" "KVM64" OFF \
|
"0" "KVM64" OFF \
|
||||||
3>&1 1>&2 2>&3); then
|
3>&1 1>&2 2>&3); then
|
||||||
if [ $CPU_TYPE1 = "1" ]; then
|
if [ $CPU_TYPE1 = "1" ]; then
|
||||||
echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
|
echo -e "${DGN}$(translate "Using CPU Model:") ${BGN}Host${CL}"
|
||||||
CPU_TYPE=" -cpu host"
|
CPU_TYPE=" -cpu host"
|
||||||
else
|
else
|
||||||
echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
|
echo -e "${DGN}$(translate "Using CPU Model:") ${BGN}KVM64${CL}"
|
||||||
CPU_TYPE=""
|
CPU_TYPE=""
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -358,77 +358,77 @@ function advanced_settings() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Core Count Selection
|
# Core Count Selection
|
||||||
if CORE_COUNT=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate CPU Cores")" 8 58 2 --title "CORE COUNT" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if CORE_COUNT=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate CPU Cores")" 8 58 2 --title "$(translate "CORE COUNT")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $CORE_COUNT ]; then
|
if [ -z $CORE_COUNT ]; then
|
||||||
CORE_COUNT="2"
|
CORE_COUNT="2"
|
||||||
echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
|
echo -e "${DGN}$(translate "Allocated Cores:") ${BGN}$CORE_COUNT${CL}"
|
||||||
else
|
else
|
||||||
echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
|
echo -e "${DGN}$(translate "Allocated Cores:") ${BGN}$CORE_COUNT${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# RAM Size Selection
|
# RAM Size Selection
|
||||||
if RAM_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate RAM in MiB")" 8 58 4096 --title "RAM" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if RAM_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Allocate RAM in MiB")" 8 58 4096 --title "RAM" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $RAM_SIZE ]; then
|
if [ -z $RAM_SIZE ]; then
|
||||||
RAM_SIZE="4096"
|
RAM_SIZE="4096"
|
||||||
echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
|
echo -e "${DGN}$(translate "Allocated RAM:") ${BGN}$RAM_SIZE${CL}"
|
||||||
else
|
else
|
||||||
echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
|
echo -e "${DGN}$(translate "Allocated RAM:") ${BGN}$RAM_SIZE${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Bridge Selection
|
# Bridge Selection
|
||||||
if BRG=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Bridge")" 8 58 vmbr0 --title "BRIDGE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if BRG=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Bridge")" 8 58 vmbr0 --title "$(translate "BRIDGE")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $BRG ]; then
|
if [ -z $BRG ]; then
|
||||||
BRG="vmbr0"
|
BRG="vmbr0"
|
||||||
echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
|
echo -e "${DGN}$(translate "Using Bridge:") ${BGN}$BRG${CL}"
|
||||||
else
|
else
|
||||||
echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
|
echo -e "${DGN}$(translate "Using Bridge:") ${BGN}$BRG${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# MAC Address Selection
|
# MAC Address Selection
|
||||||
if MAC1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a MAC Address")" 8 58 $GEN_MAC --title "MAC ADDRESS" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if MAC1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a MAC Address")" 8 58 $GEN_MAC --title "$(translate "MAC ADDRESS")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $MAC1 ]; then
|
if [ -z $MAC1 ]; then
|
||||||
MAC="$GEN_MAC"
|
MAC="$GEN_MAC"
|
||||||
echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
|
echo -e "${DGN}$(translate "Using MAC Address:") ${BGN}$MAC${CL}"
|
||||||
else
|
else
|
||||||
MAC="$MAC1"
|
MAC="$MAC1"
|
||||||
echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
|
echo -e "${DGN}$(translate "Using MAC Address:") ${BGN}$MAC1${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# VLAN Selection
|
# VLAN Selection
|
||||||
if VLAN1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Vlan(leave blank for default)")" 8 58 --title "VLAN" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if VLAN1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set a Vlan(leave blank for default)")" 8 58 --title "VLAN" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $VLAN1 ]; then
|
if [ -z $VLAN1 ]; then
|
||||||
VLAN1="Default"
|
VLAN1="$(translate "Default")"
|
||||||
VLAN=""
|
VLAN=""
|
||||||
echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
|
echo -e "${DGN}$(translate "Using Vlan:") ${BGN}$VLAN1${CL}"
|
||||||
else
|
else
|
||||||
VLAN=",tag=$VLAN1"
|
VLAN=",tag=$VLAN1"
|
||||||
echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
|
echo -e "${DGN}$(translate "Using Vlan:") ${BGN}$VLAN1${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# MTU Selection
|
# MTU Selection
|
||||||
if MTU1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Interface MTU Size (leave blank for default)")" 8 58 --title "MTU SIZE" --cancel-button Exit-Script 3>&1 1>&2 2>&3); then
|
if MTU1=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "Set Interface MTU Size (leave blank for default)")" 8 58 --title "$(translate "MTU SIZE")" --cancel-button "$(translate "Exit-Script")" 3>&1 1>&2 2>&3); then
|
||||||
if [ -z $MTU1 ]; then
|
if [ -z $MTU1 ]; then
|
||||||
MTU1="Default"
|
MTU1="$(translate "Default")"
|
||||||
MTU=""
|
MTU=""
|
||||||
echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
|
echo -e "${DGN}$(translate "Using Interface MTU Size:") ${BGN}$MTU1${CL}"
|
||||||
else
|
else
|
||||||
MTU=",mtu=$MTU1"
|
MTU=",mtu=$MTU1"
|
||||||
echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
|
echo -e "${DGN}$(translate "Using Interface MTU Size:") ${BGN}$MTU1${CL}"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit_script
|
exit_script
|
||||||
@@ -437,15 +437,15 @@ function advanced_settings() {
|
|||||||
|
|
||||||
|
|
||||||
# Confirmation
|
# Confirmation
|
||||||
if (whiptail --backtitle "ProxMenuX" --title "$(translate "ADVANCED SETTINGS COMPLETE")" --yesno "Ready to create a $NAME?" --no-button Do-Over 10 58); then
|
if (whiptail --backtitle "ProxMenuX" --title "$(translate "ADVANCED SETTINGS COMPLETE")" --yesno "$(printf "$(translate "Ready to create a %s?")" "$NAME")" --no-button "$(translate "Do-Over")" 10 58); then
|
||||||
echo -e
|
echo -e
|
||||||
echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}"
|
echo -e "${CUS}$(printf "$(translate "Creating a %s using the above advanced settings")" "$NAME")${CL}"
|
||||||
sleep 1
|
sleep 1
|
||||||
select_disk_type || return 1
|
select_disk_type || return 1
|
||||||
else
|
else
|
||||||
header_info
|
header_info
|
||||||
sleep 1
|
sleep 1
|
||||||
echo -e "${CUS}Using Advanced Settings${CL}"
|
echo -e "${CUS}$(translate "Using Advanced Settings")${CL}"
|
||||||
advanced_settings
|
advanced_settings
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -482,13 +482,13 @@ function select_disk_type() {
|
|||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(whiptail --backtitle "ProxMenuX" --title "STORAGE PLAN" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
choice=$(whiptail --backtitle "ProxMenuX" --title "$(translate "STORAGE PLAN")" --menu "$(_build_storage_plan_summary)" 18 78 5 \
|
||||||
"a" "$(translate "Add virtual disk")" \
|
"a" "$(translate "Add virtual disk")" \
|
||||||
"b" "$(translate "Add import disk")" \
|
"b" "$(translate "Add import disk")" \
|
||||||
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
"c" "$(translate "Add Controller or NVMe (PCI passthrough)")" \
|
||||||
"r" "$(translate "Reset current storage selection")" \
|
"r" "$(translate "Reset current storage selection")" \
|
||||||
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
"d" "$(translate "──── [ Finish and continue ] ────")" \
|
||||||
--ok-button "Select" --cancel-button "Cancel" 3>&1 1>&2 2>&3) || {
|
--ok-button "$(translate "Select")" --cancel-button "$(translate "Cancel")" 3>&1 1>&2 2>&3) || {
|
||||||
msg_warn "$(translate "Storage plan selection cancelled.")"
|
msg_warn "$(translate "Storage plan selection cancelled.")"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -560,7 +560,7 @@ function select_virtual_disk() {
|
|||||||
local VALID
|
local VALID
|
||||||
VALID=$(pvesm status -content images | awk 'NR>1')
|
VALID=$(pvesm status -content images | awk 'NR>1')
|
||||||
if [[ -z "$VALID" ]]; then
|
if [[ -z "$VALID" ]]; then
|
||||||
msg_error "Unable to detect a valid storage location."
|
msg_error "$(translate "Unable to detect a valid storage location.")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -577,7 +577,7 @@ function select_virtual_disk() {
|
|||||||
|
|
||||||
local DISK_SIZE
|
local DISK_SIZE
|
||||||
stop_spinner
|
stop_spinner
|
||||||
DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "VIRTUAL DISK" --cancel-button Cancel 3>&1 1>&2 2>&3) || return 0
|
DISK_SIZE=$(whiptail --backtitle "ProxMenuX" --inputbox "$(translate "System Disk Size (GB)")" 8 58 32 --title "$(translate "VIRTUAL DISK")" --cancel-button "$(translate "Cancel")" 3>&1 1>&2 2>&3) || return 0
|
||||||
[[ -z "$DISK_SIZE" ]] && DISK_SIZE="32"
|
[[ -z "$DISK_SIZE" ]] && DISK_SIZE="32"
|
||||||
|
|
||||||
VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}")
|
VIRTUAL_DISKS+=("${STORAGE}:${DISK_SIZE}")
|
||||||
@@ -592,7 +592,7 @@ function select_controller_nvme() {
|
|||||||
fi
|
fi
|
||||||
elif declare -F _pci_is_iommu_active >/dev/null 2>&1; then
|
elif declare -F _pci_is_iommu_active >/dev/null 2>&1; then
|
||||||
if ! _pci_is_iommu_active; then
|
if ! _pci_is_iommu_active; then
|
||||||
whiptail --title "Controller + NVMe" --msgbox \
|
whiptail --title "$(translate "Controller + NVMe")" --msgbox \
|
||||||
"$(translate "IOMMU is not active on this host.")\n\n$(translate "Controller/NVMe passthrough requires IOMMU enabled in BIOS/UEFI and kernel.")\n\n$(translate "Enable IOMMU, reboot the host, and try again.")" \
|
"$(translate "IOMMU is not active on this host.")\n\n$(translate "Controller/NVMe passthrough requires IOMMU enabled in BIOS/UEFI and kernel.")\n\n$(translate "Enable IOMMU, reboot the host, and try again.")" \
|
||||||
14 90
|
14 90
|
||||||
return 1
|
return 1
|
||||||
@@ -675,12 +675,12 @@ function select_controller_nvme() {
|
|||||||
msg+="$(translate "Hidden for safety"):\n${blocked_report}"
|
msg+="$(translate "Hidden for safety"):\n${blocked_report}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
whiptail --title "Controller + NVMe" --msgbox "$msg" 18 84
|
whiptail --title "$(translate "Controller + NVMe")" --msgbox "$msg" 18 84
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local selected
|
local selected
|
||||||
selected=$(whiptail --title "Controller + NVMe" --checklist \
|
selected=$(whiptail --title "$(translate "Controller + NVMe")" --checklist \
|
||||||
"$(translate "Select available Controllers/NVMe to add:")" 20 96 10 \
|
"$(translate "Select available Controllers/NVMe to add:")" 20 96 10 \
|
||||||
"${menu_items[@]}" 3>&1 1>&2 2>&3) || return 1
|
"${menu_items[@]}" 3>&1 1>&2 2>&3) || return 1
|
||||||
|
|
||||||
@@ -692,7 +692,7 @@ function select_controller_nvme() {
|
|||||||
|
|
||||||
if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]] && declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>&1; then
|
if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]] && declare -F _vm_storage_confirm_controller_passthrough_risk >/dev/null 2>&1; then
|
||||||
local vm_name_for_notice="${HN:-$NAME}"
|
local vm_name_for_notice="${HN:-$NAME}"
|
||||||
if ! _vm_storage_confirm_controller_passthrough_risk "${VMID:-}" "$vm_name_for_notice" "Controller + NVMe"; then
|
if ! _vm_storage_confirm_controller_passthrough_risk "${VMID:-}" "$vm_name_for_notice" "$(translate "Controller + NVMe")"; then
|
||||||
CONTROLLER_NVME_PCIS=()
|
CONTROLLER_NVME_PCIS=()
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
@@ -745,18 +745,18 @@ function prompt_controller_conflict_policy() {
|
|||||||
function select_loader() {
|
function select_loader() {
|
||||||
# Ensure the images directory exists
|
# Ensure the images directory exists
|
||||||
if [ ! -d "$IMAGES_DIR" ]; then
|
if [ ! -d "$IMAGES_DIR" ]; then
|
||||||
msg_info "Creating images directory"
|
msg_info "$(translate "Creating images directory")"
|
||||||
mkdir -p "$IMAGES_DIR"
|
mkdir -p "$IMAGES_DIR"
|
||||||
chmod 755 "$IMAGES_DIR"
|
chmod 755 "$IMAGES_DIR"
|
||||||
msg_ok "Images directory created: $IMAGES_DIR"
|
msg_ok "$(printf "$(translate "Images directory created: %s")" "$IMAGES_DIR")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create the loader selection menu
|
# Create the loader selection menu
|
||||||
LOADER_OPTION=$(whiptail --backtitle "ProxMenuX" --title "SELECT LOADER" --menu "$(translate "Choose a loader for Synology DSM:")" 15 70 4 \
|
LOADER_OPTION=$(whiptail --backtitle "ProxMenuX" --title "$(translate "SELECT LOADER")" --menu "$(translate "Choose a loader for Synology DSM:")" 15 70 4 \
|
||||||
"1" "AuxXxilium Arc Loader" \
|
"1" "AuxXxilium Arc Loader" \
|
||||||
"2" "RedPill Loader (RROrg - RR)" \
|
"2" "RedPill Loader (RROrg - RR)" \
|
||||||
"3" "Alpine RedPill Loader (PeterSuh-Q3 M-shell)" \
|
"3" "Alpine RedPill Loader (PeterSuh-Q3 M-shell)" \
|
||||||
"4" "Custom Loader Image (from $IMAGES_DIR)" \
|
"4" "$(printf "$(translate "Custom Loader Image (from %s)")" "$IMAGES_DIR")" \
|
||||||
3>&1 1>&2 2>&3)
|
3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
if [ -z "$LOADER_OPTION" ]; then
|
if [ -z "$LOADER_OPTION" ]; then
|
||||||
@@ -768,28 +768,28 @@ function select_loader() {
|
|||||||
LOADER_TYPE="arc"
|
LOADER_TYPE="arc"
|
||||||
LOADER_NAME="AuxXxilium Arc"
|
LOADER_NAME="AuxXxilium Arc"
|
||||||
LOADER_URL="https://github.com/AuxXxilium/arc/"
|
LOADER_URL="https://github.com/AuxXxilium/arc/"
|
||||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}"
|
||||||
download_loader
|
download_loader
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
LOADER_TYPE="redpill"
|
LOADER_TYPE="redpill"
|
||||||
LOADER_NAME="RedPill RR"
|
LOADER_NAME="RedPill RR"
|
||||||
LOADER_URL="https://github.com/RROrg/rr/"
|
LOADER_URL="https://github.com/RROrg/rr/"
|
||||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}"
|
||||||
download_loader
|
download_loader
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
LOADER_TYPE="tinycore"
|
LOADER_TYPE="tinycore"
|
||||||
LOADER_NAME="Alpine RedPill M-shell"
|
LOADER_NAME="Alpine RedPill M-shell"
|
||||||
LOADER_URL="https://github.com/PeterSuh-Q3/tinycore-redpill/"
|
LOADER_URL="https://github.com/PeterSuh-Q3/tinycore-redpill/"
|
||||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}"
|
||||||
download_loader
|
download_loader
|
||||||
;;
|
;;
|
||||||
4)
|
4)
|
||||||
LOADER_TYPE="custom"
|
LOADER_TYPE="custom"
|
||||||
LOADER_NAME="Custom Image"
|
LOADER_NAME="$(translate "Custom Image")"
|
||||||
LOADER_URL="https://xpenology.com/forum/"
|
LOADER_URL="https://xpenology.com/forum/"
|
||||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
echo -e "${DGN}${TAB}$(translate "Selected Loader:") ${BGN}$LOADER_NAME${CL}"
|
||||||
select_custom_image
|
select_custom_image
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -800,7 +800,7 @@ function select_custom_image() {
|
|||||||
IMAGES=$(find "$IMAGES_DIR" -type f -name "*.img" -o -name "*.iso" -o -name "*.qcow2" -o -name "*.vmdk" | sort)
|
IMAGES=$(find "$IMAGES_DIR" -type f -name "*.img" -o -name "*.iso" -o -name "*.qcow2" -o -name "*.vmdk" | sort)
|
||||||
|
|
||||||
if [ -z "$IMAGES" ]; then
|
if [ -z "$IMAGES" ]; then
|
||||||
whiptail --title "$(translate "No Images Found")" --msgbox "No compatible images found in $IMAGES_DIR\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again." 15 70
|
whiptail --title "$(translate "No Images Found")" --msgbox "$(printf "$(translate "No compatible images found in %s\n\nSupported formats: .img, .iso, .qcow2, .vmdk\n\nPlease add some images and try again.")" "$IMAGES_DIR")" 15 70
|
||||||
select_loader
|
select_loader
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -814,14 +814,14 @@ function select_custom_image() {
|
|||||||
done <<< "$IMAGES"
|
done <<< "$IMAGES"
|
||||||
|
|
||||||
# Let the user select an image
|
# Let the user select an image
|
||||||
LOADER_FILE=$(whiptail --backtitle "ProxMenuX" --title "SELECT CUSTOM IMAGE" --menu "$(translate "Choose a custom image:")" 20 70 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
LOADER_FILE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "SELECT CUSTOM IMAGE")" --menu "$(translate "Choose a custom image:")" 20 70 10 "${IMAGE_OPTIONS[@]}" 3>&1 1>&2 2>&3)
|
||||||
|
|
||||||
if [ -z "$LOADER_FILE" ]; then
|
if [ -z "$LOADER_FILE" ]; then
|
||||||
msg_error "No custom image selected"
|
msg_error "$(translate "No custom image selected")"
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${DGN}${TAB}Using Custom Image: ${BGN}$(basename "$LOADER_FILE")${CL}"
|
echo -e "${DGN}${TAB}$(translate "Using Custom Image:") ${BGN}$(basename "$LOADER_FILE")${CL}"
|
||||||
FILE=$(basename "$LOADER_FILE")
|
FILE=$(basename "$LOADER_FILE")
|
||||||
}
|
}
|
||||||
# ==========================================================
|
# ==========================================================
|
||||||
@@ -840,17 +840,17 @@ function download_loader() {
|
|||||||
FILE=""
|
FILE=""
|
||||||
LOADER_FILE=""
|
LOADER_FILE=""
|
||||||
|
|
||||||
echo -e "${DGN}${TAB}Retrieving the URL for the ${BGN}$LOADER_NAME loader${CL}"
|
echo -e "${DGN}${TAB}$(printf "$(translate "Retrieving the URL for the %s loader")" "${BGN}$LOADER_NAME")${CL}"
|
||||||
|
|
||||||
if [[ "$LOADER_TYPE" == "arc" || "$LOADER_TYPE" == "redpill" ]] && ! command -v unzip &> /dev/null; then
|
if [[ "$LOADER_TYPE" == "arc" || "$LOADER_TYPE" == "redpill" ]] && ! command -v unzip &> /dev/null; then
|
||||||
msg_info "Installing unzip..."
|
msg_info "$(translate "Installing unzip...")"
|
||||||
apt-get update -qq && apt-get install -y unzip -qq >/dev/null 2>&1
|
apt-get update -qq && apt-get install -y unzip -qq >/dev/null 2>&1
|
||||||
if ! command -v unzip &> /dev/null; then
|
if ! command -v unzip &> /dev/null; then
|
||||||
msg_error "Failed to install unzip"
|
msg_error "$(translate "Failed to install unzip")"
|
||||||
sleep 2
|
sleep 2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
msg_ok "Installed unzip successfully."
|
msg_ok "$(translate "Installed unzip successfully.")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
asset_pattern='^[^/]+[.]img[.]zip$'
|
asset_pattern='^[^/]+[.]img[.]zip$'
|
||||||
@@ -927,7 +927,7 @@ function download_loader() {
|
|||||||
rm -rf "$stage"
|
rm -rf "$stage"
|
||||||
FILE="$image_name"
|
FILE="$image_name"
|
||||||
LOADER_FILE="$IMAGES_DIR/$FILE"
|
LOADER_FILE="$IMAGES_DIR/$FILE"
|
||||||
msg_ok "Downloaded ${CL}${BL}${FILE}${CL} to ${IMAGES_DIR}"
|
msg_ok "$(printf "$(translate "Downloaded %s to %s")" "${CL}${BL}${FILE}${CL}" "${IMAGES_DIR}")"
|
||||||
}
|
}
|
||||||
# =======================================================
|
# =======================================================
|
||||||
|
|
||||||
@@ -960,7 +960,7 @@ function select_efi_storage() {
|
|||||||
|
|
||||||
VALID=$(pvesm status -content images | awk 'NR>1')
|
VALID=$(pvesm status -content images | awk 'NR>1')
|
||||||
if [ -z "$VALID" ]; then
|
if [ -z "$VALID" ]; then
|
||||||
msg_error "Unable to detect a valid storage location for EFI disk." >&2
|
msg_error "$(translate "Unable to detect a valid storage location for EFI disk.")" >&2
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
|
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
|
||||||
@@ -969,7 +969,7 @@ function select_efi_storage() {
|
|||||||
else
|
else
|
||||||
[[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" > /dev/null 2>&1
|
[[ -n "${SPINNER_PID:-}" ]] && kill "$SPINNER_PID" > /dev/null 2>&1
|
||||||
while [ -z "${STORAGE:+x}" ]; do
|
while [ -z "${STORAGE:+x}" ]; do
|
||||||
STORAGE=$(whiptail --backtitle "ProxMenuX" --title "EFI Disk Storage" --radiolist \
|
STORAGE=$(whiptail --backtitle "ProxMenuX" --title "$(translate "EFI Disk Storage")" --radiolist \
|
||||||
"$(translate "Choose the storage for the EFI disk (4MB):")"$'\n\n'"$(translate "Use Spacebar to select.")" \
|
"$(translate "Choose the storage for the EFI disk (4MB):")"$'\n\n'"$(translate "Use Spacebar to select.")" \
|
||||||
16 $(($MSG_MAX_LENGTH + 23)) 6 \
|
16 $(($MSG_MAX_LENGTH + 23)) 6 \
|
||||||
"${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || {
|
"${STORAGE_MENU[@]}" 3>&1 1>&2 2>&3) || {
|
||||||
@@ -1015,7 +1015,7 @@ function select_storage_volume() {
|
|||||||
|
|
||||||
VALID=$(pvesm status -content images | awk 'NR>1')
|
VALID=$(pvesm status -content images | awk 'NR>1')
|
||||||
if [ -z "$VALID" ]; then
|
if [ -z "$VALID" ]; then
|
||||||
msg_error "Unable to detect a valid storage location." >&2
|
msg_error "$(translate "Unable to detect a valid storage location.")" >&2
|
||||||
return 1
|
return 1
|
||||||
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
|
elif [ $((${#STORAGE_MENU[@]} / 3)) -eq 1 ]; then
|
||||||
STORAGE=${STORAGE_MENU[0]}
|
STORAGE=${STORAGE_MENU[0]}
|
||||||
@@ -1048,19 +1048,19 @@ function create_vm() {
|
|||||||
if ! qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
|
if ! qm create $VMID -agent 1${MACHINE} -tablet 0 -localtime 1${BIOS_TYPE}${CPU_TYPE} -cores $CORE_COUNT -memory $RAM_SIZE \
|
||||||
-name $HN -tags proxmenux,nas -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \
|
-name $HN -tags proxmenux,nas -net0 virtio,bridge=$BRG,macaddr=$MAC$VLAN$MTU -onboot 1 -ostype l26 -scsihw virtio-scsi-pci \
|
||||||
-serial0 socket; then
|
-serial0 socket; then
|
||||||
msg_error "Failed to create base VM. Check VM ID and host configuration."
|
msg_error "$(translate "Failed to create base VM. Check VM ID and host configuration.")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
msg_ok "Create a $NAME"
|
msg_ok "$(printf "$(translate "Create a %s")" "$NAME")"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Check if UEFI (OVMF) is being used ===================
|
# Check if UEFI (OVMF) is being used ===================
|
||||||
if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then
|
if [[ "$BIOS_TYPE" == *"ovmf"* ]]; then
|
||||||
|
|
||||||
msg_info "Configuring EFI disk"
|
msg_info "$(translate "Configuring EFI disk")"
|
||||||
if ! EFI_STORAGE=$(select_efi_storage $VMID); then
|
if ! EFI_STORAGE=$(select_efi_storage $VMID); then
|
||||||
msg_error "EFI storage selection failed or was cancelled."
|
msg_error "$(translate "EFI storage selection failed or was cancelled.")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
EFI_DISK_NAME="vm-${VMID}-disk-efivars"
|
EFI_DISK_NAME="vm-${VMID}-disk-efivars"
|
||||||
@@ -1084,9 +1084,9 @@ function create_vm() {
|
|||||||
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
|
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
|
||||||
|
|
||||||
if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:4,efitype=4m,format=raw,pre-enrolled-keys=0" >/dev/null 2>&1; then
|
if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:4,efitype=4m,format=raw,pre-enrolled-keys=0" >/dev/null 2>&1; then
|
||||||
msg_ok "EFI disk created (raw) and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}"
|
msg_ok "$(printf "$(translate "EFI disk created (raw) and configured on %s")" "${CL}${BL}$EFI_STORAGE${GN}${CL}")"
|
||||||
else
|
else
|
||||||
msg_error "Failed to configure EFI disk"
|
msg_error "$(translate "Failed to configure EFI disk")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -1097,13 +1097,13 @@ function create_vm() {
|
|||||||
|
|
||||||
if pvesm alloc "$EFI_STORAGE" "$VMID" "$EFI_DISK_NAME" 4M >/dev/null 2>&1; then
|
if pvesm alloc "$EFI_STORAGE" "$VMID" "$EFI_DISK_NAME" 4M >/dev/null 2>&1; then
|
||||||
if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:${EFI_DISK_NAME},pre-enrolled-keys=0" >/dev/null 2>&1; then
|
if qm set "$VMID" -$EFI_DISK_ID "$EFI_STORAGE:${EFI_DISK_NAME},pre-enrolled-keys=0" >/dev/null 2>&1; then
|
||||||
msg_ok "EFI disk created and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}"
|
msg_ok "$(printf "$(translate "EFI disk created and configured on %s")" "${CL}${BL}$EFI_STORAGE${GN}${CL}")"
|
||||||
else
|
else
|
||||||
msg_error "Failed to configure EFI disk"
|
msg_error "$(translate "Failed to configure EFI disk")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_error "Failed to create EFI disk"
|
msg_error "$(translate "Failed to create EFI disk")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1116,7 +1116,7 @@ function create_vm() {
|
|||||||
# Select storage volume for loader =======================
|
# Select storage volume for loader =======================
|
||||||
|
|
||||||
if ! LOADER_STORAGE=$(select_storage_volume $VMID "loader disk"); then
|
if ! LOADER_STORAGE=$(select_storage_volume $VMID "loader disk"); then
|
||||||
msg_error "Loader storage selection failed or was cancelled."
|
msg_error "$(translate "Loader storage selection failed or was cancelled.")"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1126,7 +1126,7 @@ function create_vm() {
|
|||||||
import_pid=$!
|
import_pid=$!
|
||||||
|
|
||||||
# Show a simple progress indicator
|
# Show a simple progress indicator
|
||||||
echo -n "Importing loader disk: "
|
echo -n "$(translate "Importing loader disk:") "
|
||||||
while kill -0 $import_pid 2>/dev/null; do
|
while kill -0 $import_pid 2>/dev/null; do
|
||||||
echo -n "."
|
echo -n "."
|
||||||
sleep 2.5
|
sleep 2.5
|
||||||
@@ -1139,10 +1139,10 @@ function create_vm() {
|
|||||||
|
|
||||||
# If the disk was not imported correctly, show an error message but continue
|
# If the disk was not imported correctly, show an error message but continue
|
||||||
if [ -z "$IMPORTED_DISK" ]; then
|
if [ -z "$IMPORTED_DISK" ]; then
|
||||||
msg_error "Loader import failed. No disk detected."
|
msg_error "$(translate "Loader import failed. No disk detected.")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
else
|
else
|
||||||
msg_ok "Loader imported successfully to ${CL}${BL}$LOADER_STORAGE${GN}${CL}"
|
msg_ok "$(printf "$(translate "Loader imported successfully to %s")" "${CL}${BL}$LOADER_STORAGE${GN}${CL}")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
@@ -1159,23 +1159,23 @@ function create_vm() {
|
|||||||
|
|
||||||
if [[ -n "$IMPORTED_REF" && -n "$IMPORTED_ID" ]]; then
|
if [[ -n "$IMPORTED_REF" && -n "$IMPORTED_ID" ]]; then
|
||||||
if qm set "$VMID" -ide0 "$IMPORTED_REF" >/dev/null 2>&1; then
|
if qm set "$VMID" -ide0 "$IMPORTED_REF" >/dev/null 2>&1; then
|
||||||
msg_ok "Configured loader disk as ide0"
|
msg_ok "$(translate "Configured loader disk as ide0")"
|
||||||
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
|
qm set "$VMID" -delete "$IMPORTED_ID" >/dev/null 2>&1
|
||||||
else
|
else
|
||||||
msg_error "Failed to assign loader disk to ide0"
|
msg_error "$(translate "Failed to assign loader disk to ide0")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_error "Loader import failed. No disk detected in config."
|
msg_error "$(translate "Loader import failed. No disk detected in config.")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
|
||||||
DISK_NAME="vm-${VMID}-disk-0"
|
DISK_NAME="vm-${VMID}-disk-0"
|
||||||
if qm set "$VMID" -ide0 "$LOADER_STORAGE:${DISK_NAME}" >/dev/null 2>&1; then
|
if qm set "$VMID" -ide0 "$LOADER_STORAGE:${DISK_NAME}" >/dev/null 2>&1; then
|
||||||
msg_ok "Configured loader disk as ide0"
|
msg_ok "$(translate "Configured loader disk as ide0")"
|
||||||
else
|
else
|
||||||
msg_error "Failed to assign loader disk"
|
msg_error "$(translate "Failed to assign loader disk")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1185,7 +1185,7 @@ function create_vm() {
|
|||||||
|
|
||||||
result=$(qm set "$VMID" -boot order=ide0 2>&1)
|
result=$(qm set "$VMID" -boot order=ide0 2>&1)
|
||||||
if [[ $? -eq 0 ]]; then
|
if [[ $? -eq 0 ]]; then
|
||||||
msg_ok "Loader configured as boot device."
|
msg_ok "$(translate "Loader configured as boot device.")"
|
||||||
else
|
else
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
@@ -1217,27 +1217,27 @@ if [[ ${#VIRTUAL_DISKS[@]} -gt 0 ]]; then
|
|||||||
SATA_ID="sata${DISK_SLOT_INDEX}"
|
SATA_ID="sata${DISK_SLOT_INDEX}"
|
||||||
|
|
||||||
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
|
if [[ "$STORAGE_TYPE" == "btrfs" || "$STORAGE_TYPE" == "dir" || "$STORAGE_TYPE" == "nfs" ]]; then
|
||||||
msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..."
|
msg_info "$(printf "$(translate "Creating virtual disk (format=raw) for %s...")" "$STORAGE_TYPE")"
|
||||||
if ! qm set "$VMID" -$SATA_ID "$STORAGE:$SIZE,format=raw" >/dev/null 2>&1; then
|
if ! qm set "$VMID" -$SATA_ID "$STORAGE:$SIZE,format=raw" >/dev/null 2>&1; then
|
||||||
msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE"
|
msg_error "$(printf "$(translate "Failed to assign disk %s (%s) on %s")" "$DISK_NUM" "$SATA_ID" "$STORAGE")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
msg_info "Allocating virtual disk for $STORAGE_TYPE..."
|
msg_info "$(printf "$(translate "Allocating virtual disk for %s...")" "$STORAGE_TYPE")"
|
||||||
if ! pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME" "$SIZE"G >/dev/null 2>&1; then
|
if ! pvesm alloc "$STORAGE" "$VMID" "$DISK_NAME" "$SIZE"G >/dev/null 2>&1; then
|
||||||
msg_error "Failed to allocate virtual disk $DISK_NUM"
|
msg_error "$(printf "$(translate "Failed to allocate virtual disk %s")" "$DISK_NUM")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
if ! qm set "$VMID" -$SATA_ID "$STORAGE:${DISK_REF}$DISK_NAME" >/dev/null 2>&1; then
|
if ! qm set "$VMID" -$SATA_ID "$STORAGE:${DISK_REF}$DISK_NAME" >/dev/null 2>&1; then
|
||||||
msg_error "Failed to configure virtual disk as $SATA_ID"
|
msg_error "$(printf "$(translate "Failed to configure virtual disk as %s")" "$SATA_ID")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_ok "Configured virtual disk as $SATA_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}"
|
msg_ok "$(printf "$(translate "Configured virtual disk as %s, %sGB on %s")" "$SATA_ID" "${SIZE}" "${CL}${BL}$STORAGE${CL} ${GN}")"
|
||||||
DISK_INFO="${DISK_INFO}<p>Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE}</p>"
|
DISK_INFO="${DISK_INFO}<p>Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE}</p>"
|
||||||
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} ($SATA_ID)\n"
|
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Virtual Disk $DISK_NUM: ${SIZE}GB on ${STORAGE} ($SATA_ID)\n"
|
||||||
DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1))
|
DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1))
|
||||||
@@ -1259,12 +1259,12 @@ if [[ ${#EFFECTIVE_IMPORT_DISKS[@]} -gt 0 ]]; then
|
|||||||
DISK_ID="sata${DISK_SLOT_INDEX}"
|
DISK_ID="sata${DISK_SLOT_INDEX}"
|
||||||
|
|
||||||
if qm set "$VMID" -${DISK_ID} ${DISK} >/dev/null 2>&1; then
|
if qm set "$VMID" -${DISK_ID} ${DISK} >/dev/null 2>&1; then
|
||||||
msg_ok "Configured import disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID"
|
msg_ok "$(printf "$(translate "Configured import disk %s as %s")" "${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN}" "$DISK_ID")"
|
||||||
DISK_INFO="${DISK_INFO}<p>Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE)</p>"
|
DISK_INFO="${DISK_INFO}<p>Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE)</p>"
|
||||||
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE) (${DISK_ID})\n"
|
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Import Disk ${DISK_NUM}: $DISK ($DISK_MODEL $DISK_SIZE) (${DISK_ID})\n"
|
||||||
DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1))
|
DISK_SLOT_INDEX=$((DISK_SLOT_INDEX + 1))
|
||||||
else
|
else
|
||||||
msg_error "Failed to configure import disk $DISK as $DISK_ID"
|
msg_error "$(printf "$(translate "Failed to configure import disk %s as %s")" "$DISK" "$DISK_ID")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -1343,14 +1343,14 @@ if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then
|
|||||||
for PCI_DEV in "${CONTROLLER_NVME_PCIS[@]}"; do
|
for PCI_DEV in "${CONTROLLER_NVME_PCIS[@]}"; do
|
||||||
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
|
if declare -F _pci_function_assigned_to_vm >/dev/null 2>&1; then
|
||||||
if _pci_function_assigned_to_vm "$PCI_DEV" "$VMID"; then
|
if _pci_function_assigned_to_vm "$PCI_DEV" "$VMID"; then
|
||||||
msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})"
|
msg_warn "$(printf "$(translate "Controller/NVMe already present in VM config (%s)")" "${PCI_DEV}")"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
local PCI_BDF
|
local PCI_BDF
|
||||||
PCI_BDF="${PCI_DEV#0000:}"
|
PCI_BDF="${PCI_DEV#0000:}"
|
||||||
if qm config "$VMID" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*(0000:)?${PCI_BDF}([,[:space:]]|$)"; then
|
if qm config "$VMID" 2>/dev/null | grep -qE "^hostpci[0-9]+:.*(0000:)?${PCI_BDF}([,[:space:]]|$)"; then
|
||||||
msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})"
|
msg_warn "$(printf "$(translate "Controller/NVMe already present in VM config (%s)")" "${PCI_DEV}")"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -1397,12 +1397,12 @@ if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if qm set "$VMID" --hostpci${HOSTPCI_INDEX} "${PCI_DEV},pcie=1" >/dev/null 2>&1; then
|
if qm set "$VMID" --hostpci${HOSTPCI_INDEX} "${PCI_DEV},pcie=1" >/dev/null 2>&1; then
|
||||||
msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}"
|
msg_ok "$(printf "$(translate "Configured controller/NVMe as hostpci%s: %s")" "${HOSTPCI_INDEX}" "${PCI_DEV}")"
|
||||||
DISK_INFO="${DISK_INFO}<p>Controller/NVMe: ${PCI_DEV}</p>"
|
DISK_INFO="${DISK_INFO}<p>Controller/NVMe: ${PCI_DEV}</p>"
|
||||||
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n"
|
CONSOLE_DISK_INFO="${CONSOLE_DISK_INFO}- Controller/NVMe: ${PCI_DEV} (hostpci${HOSTPCI_INDEX})\n"
|
||||||
HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1))
|
HOSTPCI_INDEX=$((HOSTPCI_INDEX + 1))
|
||||||
else
|
else
|
||||||
msg_error "Failed to configure controller/NVMe: ${PCI_DEV}"
|
msg_error "$(printf "$(translate "Failed to configure controller/NVMe: %s")" "${PCI_DEV}")"
|
||||||
ERROR_FLAG=true
|
ERROR_FLAG=true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -1411,7 +1411,7 @@ if [[ ${#CONTROLLER_NVME_PCIS[@]} -gt 0 ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#EFFECTIVE_IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then
|
if [[ ${#VIRTUAL_DISKS[@]} -eq 0 && ${#EFFECTIVE_IMPORT_DISKS[@]} -eq 0 && ${#CONTROLLER_NVME_PCIS[@]} -eq 0 ]]; then
|
||||||
msg_error "No disks/controllers configured."
|
msg_error "$(translate "No disks/controllers configured.")"
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -1424,8 +1424,8 @@ HTML_DESC="<div align='center'>
|
|||||||
</td>
|
</td>
|
||||||
<td style='vertical-align: middle;'>
|
<td style='vertical-align: middle;'>
|
||||||
<h1 style='margin: 0;'>Synology DSM VM</h1>
|
<h1 style='margin: 0;'>Synology DSM VM</h1>
|
||||||
<p style='margin: 0;'>Created with ProxMenuX</p>
|
<p style='margin: 0;'>$(translate "Created with ProxMenuX")</p>
|
||||||
<p style='margin: 0;'>Loader: $LOADER_NAME</p>
|
<p style='margin: 0;'>$(translate "Loader:") $LOADER_NAME</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -1442,16 +1442,16 @@ ${DISK_INFO}
|
|||||||
</div>
|
</div>
|
||||||
</div>"
|
</div>"
|
||||||
|
|
||||||
msg_info "Setting VM description"
|
msg_info "$(translate "Setting VM description")"
|
||||||
if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then
|
if ! qm set "$VMID" -description "$HTML_DESC" >/dev/null 2>&1; then
|
||||||
msg_error "Failed to set VM description"
|
msg_error "$(translate "Failed to set VM description")"
|
||||||
exit_script
|
exit_script
|
||||||
fi
|
fi
|
||||||
msg_ok "Configured VM description"
|
msg_ok "$(translate "Configured VM description")"
|
||||||
|
|
||||||
|
|
||||||
if [ "$ERROR_FLAG" = true ]; then
|
if [ "$ERROR_FLAG" = true ]; then
|
||||||
msg_error "VM created with errors. Check configuration."
|
msg_error "$(translate "VM created with errors. Check configuration.")"
|
||||||
else
|
else
|
||||||
if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then
|
if [[ "${WIZARD_ADD_GPU:-no}" == "yes" ]]; then
|
||||||
WIZARD_GPU_RESULT="cancelled"
|
WIZARD_GPU_RESULT="cancelled"
|
||||||
@@ -1540,7 +1540,7 @@ header_info
|
|||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
# Start script
|
# Start script
|
||||||
if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(translate "This will create a New $NAME. Proceed?")" 10 58; then
|
if whiptail --backtitle "ProxMenuX" --title "$NAME" --yesno "$(printf "$(translate "This will create a New %s. Proceed?")" "$NAME")" 10 58; then
|
||||||
start_vm_wizard_capture
|
start_vm_wizard_capture
|
||||||
if ! start_script; then
|
if ! start_script; then
|
||||||
stop_vm_wizard_capture
|
stop_vm_wizard_capture
|
||||||
|
|||||||
@@ -29,10 +29,11 @@ const lookupCode = pure.map(node => printer.printNode(ts.EmitHint.Unspecified, n
|
|||||||
const catalogs = Object.fromEntries(fs.readdirSync(path.join(app, 'messages')).filter(locale =>
|
const catalogs = Object.fromEntries(fs.readdirSync(path.join(app, 'messages')).filter(locale =>
|
||||||
fs.existsSync(path.join(app, 'messages', locale, 'common.json'))).map(locale =>
|
fs.existsSync(path.join(app, 'messages', locale, 'common.json'))).map(locale =>
|
||||||
[locale, JSON.parse(read(`messages/${locale}/common.json`))]));
|
[locale, JSON.parse(read(`messages/${locale}/common.json`))]));
|
||||||
const registered = provider.statements.flatMap(statement => ts.isVariableStatement(statement) ?
|
const catalogSource = ts.createSourceFile('catalogs.ts', read('lib/i18n/catalogs.ts'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||||
statement.declarationList.declarations.filter(declaration => declaration.name.getText(provider) === 'MESSAGE_CATALOG') : []);
|
const registered = catalogSource.statements.flatMap(statement => ts.isVariableStatement(statement) ?
|
||||||
assert.equal(registered.length, 1, 'real provider catalog declaration');
|
statement.declarationList.declarations.filter(declaration => declaration.name.getText(catalogSource) === 'MESSAGE_CATALOG') : []);
|
||||||
assert.deepEqual(registered[0].initializer.properties.map(property => property.name.getText(provider)).sort(),
|
assert.equal(registered.length, 1, 'real shared catalog declaration');
|
||||||
|
assert.deepEqual(registered[0].initializer.properties.map(property => property.name.getText(catalogSource)).sort(),
|
||||||
Object.keys(catalogs).sort(), 'test each runtime-registered shipped locale');
|
Object.keys(catalogs).sort(), 'test each runtime-registered shipped locale');
|
||||||
assert.ok(catalogs.en, 'English fallback catalog exists');
|
assert.ok(catalogs.en, 'English fallback catalog exists');
|
||||||
const translator = (language, messages = catalogs) =>
|
const translator = (language, messages = catalogs) =>
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Actual Settings handlers and real fetchApi; all responses are inert fixtures.
|
||||||
|
const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm')
|
||||||
|
const {execFileSync}=require('node:child_process')
|
||||||
|
const root=path.resolve(__dirname,'..'),app=path.join(root,'AppImage')
|
||||||
|
const ts=require(path.join(app,'node_modules/typescript'))
|
||||||
|
const catalogs=Object.fromEntries(['en','sk','de'].map(locale=>[locale,JSON.parse(fs.readFileSync(path.join(app,`messages/${locale}/common.json`)))]))
|
||||||
|
const translate=locale=>(key,params={})=>{
|
||||||
|
const find=data=>key.split('.').reduce((item,name)=>item?.[name],data)
|
||||||
|
return (find(catalogs[locale])??find(catalogs.en)??key).replace(/\{(\w+)\}/g,(all,key)=>params[key]??all)
|
||||||
|
}
|
||||||
|
const flatten=node=>!node||typeof node!=='object'?[]:[node,...[node.props?.children].flat(Infinity).flatMap(flatten)]
|
||||||
|
const text=node=>node==null||node===false?'':typeof node!=='object'?String(node):[node.props?.children].flat(Infinity).map(text).join(' ')
|
||||||
|
const compile=source=>ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020,jsx:ts.JsxEmit.ReactJSX}}).outputText
|
||||||
|
const source=fs.readFileSync(path.join(app,'components/settings.tsx'),'utf8')
|
||||||
|
const descriptions={exports:{},require:name=>({default:require(path.resolve(app,'lib',name))})};vm.runInNewContext(compile(fs.readFileSync(path.join(app,'lib/optimization-descriptions.ts'),'utf8')),descriptions)
|
||||||
|
const tools=['one','two'].map(key=>({key,name:'Demo '+key,enabled:true,has_source:true,version:'1.0',available_version:'1.1'}))
|
||||||
|
const sourceBody={success:true,function:'demo',script:'demo.sh',source:'demo() {\n echo "<tag>& $HOME"\n}\n',deprecated:false}
|
||||||
|
function fixture(locale='sk',catalog=tools){
|
||||||
|
let cursor=0,states=[],refs=[],effects=[],memo=[],tree,language=locale,reply=()=>sourceBody
|
||||||
|
const calls=[],events={},timers=[],copies=[];let fallbackCopies=0
|
||||||
|
const t=(key,params)=>translate(language)(key,params)
|
||||||
|
const react={
|
||||||
|
useState(value){const i=cursor++;if(!(i in states))states[i]=typeof value==='function'?value():value;return[states[i],v=>{states[i]=typeof v==='function'?v(states[i]):v}]},
|
||||||
|
useRef(value){const i=cursor++;return refs[i]??=( {current:value})},
|
||||||
|
useEffect(fn,deps){const i=cursor++,old=memo[i];if(!old||deps?.some((v,n)=>v!==old.deps?.[n]))effects.push(()=>{old?.cleanup?.();memo[i]={deps,cleanup:fn()}})},
|
||||||
|
}
|
||||||
|
const response=(body,status=200)=>({ok:status<400,status,statusText:'fixture',headers:{get:()=> 'application/json'},json:async()=>body})
|
||||||
|
const fetch=async(url,options={})=>{
|
||||||
|
const endpoint=String(url).replace(/^https?:\/\/[^/]+/,'');calls.push({endpoint,method:options.method||'GET'})
|
||||||
|
assert.equal(options.method||'GET','GET','Source viewer may never mutate')
|
||||||
|
if(endpoint.includes('/tool-source/')){const result=await reply(endpoint);if(result instanceof Error)throw result;return result?.http?response(result.body,result.http):response(result)}
|
||||||
|
if(endpoint.endsWith('/installed-tools'))return response({success:true,installed_tools:catalog,updates_available_count:catalog.filter(tool=>tool.has_update).length})
|
||||||
|
return response({categories:[],storages:[],interfaces:[],dismissed:[],success:true,candidates:[],configured:false})
|
||||||
|
}
|
||||||
|
const api={exports:{},fetch,process:{env:{}},console:{error(){}},require:()=>require('./ts-fixture.cjs').loadTypeScript('lib/api-errors.ts')};vm.runInNewContext(compile(fs.readFileSync(path.join(app,'lib/api-config.ts'),'utf8')),api)
|
||||||
|
const add=(name,handler)=>{(events[name]??=new Set()).add(handler)},remove=(name,handler)=>events[name]?.delete(handler)
|
||||||
|
const win={addEventListener:add,removeEventListener:remove,setTimeout:fn=>{timers.push(fn);return timers.length},isSecureContext:true}
|
||||||
|
const doc={addEventListener:add,removeEventListener:remove,body:{appendChild(){},removeChild(){}},createElement:()=>({style:{},focus(){},select(){}}),execCommand:()=>{fallbackCopies++;return true}}
|
||||||
|
const clipboard={writeText:async value=>copies.push(value)}
|
||||||
|
const context={exports:{},window:win,document:doc,navigator:{clipboard},console:{error(){}},setTimeout:win.setTimeout,
|
||||||
|
require:name=>{
|
||||||
|
if(name==='react')return react
|
||||||
|
if(name==='react/jsx-runtime')return{jsx:(type,props)=>({type,props}),jsxs:(type,props)=>({type,props})}
|
||||||
|
if(name.includes('optimization-descriptions'))return descriptions.exports
|
||||||
|
if(name.includes('api-config'))return api.exports
|
||||||
|
if(name.includes('i18n/provider'))return{SUPPORTED_LANGUAGES:[],useI18n:()=>({t,language,setLanguage:value=>language=value})}
|
||||||
|
if(name.includes('use-platform'))return{useIsPbs:()=>false}
|
||||||
|
if(name.includes('format-network'))return{getNetworkUnit:()=> 'Bytes'}
|
||||||
|
return new Proxy({},{get:(_,key)=>key})
|
||||||
|
}}
|
||||||
|
vm.runInNewContext(compile(source),context)
|
||||||
|
const render=()=>{cursor=0;effects=[];tree=context.exports.Settings();for(const effect of effects)effect();return tree}
|
||||||
|
const settle=async()=>{for(let i=0;i<24;i++)await Promise.resolve();render()}
|
||||||
|
const tool=name=>flatten(tree).find(node=>node.props?.title===t('settings.optimizations.sourceTitle')&&text(node).includes(descriptions.exports.optimizationToolName(name,catalog.find(tool=>tool.key===name)?.name||'Demo '+name,t)))
|
||||||
|
const dialog=()=>flatten(tree).find(node=>node.props?.role==='dialog')
|
||||||
|
const close=()=>flatten(tree).find(node=>node.props?.['aria-label']===t('actions.close')).props.onClick()
|
||||||
|
return{render,settle,tool,dialog,close,calls,copies,timers,tree:()=>tree,setReply:value=>reply=value,t,
|
||||||
|
language:value=>{language=value;render()},escape:()=>events.keydown?.forEach(fn=>fn({key:'Escape'})),
|
||||||
|
unmount:()=>memo.forEach(effect=>effect?.cleanup?.()),clipboard,win,fallbackCopies:()=>fallbackCopies}
|
||||||
|
}
|
||||||
|
module.exports={fixture,flatten,text,translate,catalogs,descriptions}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
const assert=require('node:assert/strict')
|
||||||
|
const fs=require('node:fs'),path=require('node:path')
|
||||||
|
const {loadTypeScript}=require('./ts-fixture.cjs')
|
||||||
|
async function withApiCatalog(language,values,run){
|
||||||
|
const original=fs.readFileSync,target=path.resolve(__dirname,'../AppImage/messages',language,'common.json')
|
||||||
|
fs.readFileSync=function(file,...args){
|
||||||
|
const content=original.call(this,file,...args)
|
||||||
|
if(typeof file==='string'&&path.resolve(file)===target){
|
||||||
|
const catalog=JSON.parse(content);catalog.apiErrors=values;return JSON.stringify(catalog)
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
try{return await run()}finally{fs.readFileSync=original}
|
||||||
|
}
|
||||||
|
function fixture(language='sk',token='fixture-token'){
|
||||||
|
const local=new Map(token?[['proxmenux-auth-token',token]]:[]),session=new Map(),calls=[];let reloads=0,reply=()=>new Response('{}',{headers:{'Content-Type':'application/json'}})
|
||||||
|
const storage=map=>({getItem:key=>map.get(key)||null,setItem:(key,v)=>map.set(key,v),removeItem:key=>map.delete(key)})
|
||||||
|
const win={localStorage:storage(local),location:{protocol:'https:',hostname:'fixture.invalid',port:'',reload:()=>reloads++}}
|
||||||
|
const globals={window:win,document:{documentElement:{lang:language}},localStorage:win.localStorage,sessionStorage:storage(session),console:{error(){}},fetch:async(url,options)=>{calls.push({url,options});return reply()}}
|
||||||
|
const {fetchApi}=loadTypeScript('lib/api-config.ts',globals)
|
||||||
|
return{fetchApi,calls,reply:value=>reply=value,globals,local,reloads:()=>reloads}
|
||||||
|
}
|
||||||
|
async function main(){
|
||||||
|
// Future translations must work without another language-specific code branch.
|
||||||
|
for(const language of ['de','es','fr','it','pt','sv']){
|
||||||
|
await withApiCatalog(language,{http:language+' HTTP {status} {statusText}',unknownContentType:language+' TYPE'},async()=>{
|
||||||
|
const f=fixture(language)
|
||||||
|
f.reply(()=>new Response('html',{status:502,statusText:'Bad Gateway'}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===language+' HTTP 502 Bad Gateway')
|
||||||
|
// A partial catalog falls back per key, preserving the original endpoint.
|
||||||
|
f.reply(()=>new Response('{',{headers:{'Content-Type':'application/json'}}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message==='Invalid JSON response from /api/demo')
|
||||||
|
assert.equal(f.calls.length,2,'Localization never retries or makes its own request')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await withApiCatalog('fr',{http:{invalid:'not a message'},invalidJson:'FR {endpoint}'},async()=>{
|
||||||
|
const f=fixture('fr');f.reply(()=>new Response('',{status:503,statusText:'Unavailable'}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message==='API request failed: 503 Unavailable')
|
||||||
|
const browser=loadTypeScript('lib/api-errors.ts',{navigator:{language:'fr-FR',languages:['sk']}})
|
||||||
|
assert.equal(browser.apiFailure('invalidJson',{endpoint:'/literal?x=<a>&y=1'}),'FR /literal?x=<a>&y=1')
|
||||||
|
const document={documentElement:{lang:'sk'}};let storedReads=0
|
||||||
|
const current=loadTypeScript('lib/api-errors.ts',{document,window:{localStorage:{getItem:()=>{storedReads++;return 'sk'}}}})
|
||||||
|
assert.equal(current.apiFailure('invalidJson',{endpoint:'/api/demo'}),'Odpoveď z /api/demo obsahuje neplatný JSON')
|
||||||
|
document.documentElement.lang='fr'
|
||||||
|
assert.equal(current.apiFailure('invalidJson',{endpoint:'/api/demo'}),'FR /api/demo','The next error follows a language switch without reloading')
|
||||||
|
assert.equal(storedReads,0,'The provider document language takes precedence')
|
||||||
|
document.documentElement.lang='unsupported'
|
||||||
|
assert.equal(current.apiFailure('unknownContentType'),'neznámy typ obsahu')
|
||||||
|
assert.equal(storedReads,1,'An invalid document language uses a supported stored preference')
|
||||||
|
const denied=loadTypeScript('lib/api-errors.ts',{window:{get localStorage(){throw new Error('denied')}},navigator:{language:'fr-FR',languages:[]}})
|
||||||
|
assert.equal(denied.apiFailure('invalidJson',{endpoint:'/api/demo'}),'FR /api/demo')
|
||||||
|
})
|
||||||
|
for(const lang of ['sk','en','de']){
|
||||||
|
const f=fixture(lang),sk=lang==='sk'
|
||||||
|
f.reply(()=>new Response('html',{status:502,statusText:'Bad Gateway',headers:{'Content-Type':'text/html'}}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===(sk?'Požiadavka na API zlyhala: 502 Bad Gateway':'API request failed: 502 Bad Gateway'))
|
||||||
|
f.reply(()=>new Response('<html>',{headers:{'Content-Type':'text/html'}}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===(sk?'Očakávala sa odpoveď JSON, ale prišiel typ text/html':'Expected JSON response but got text/html'))
|
||||||
|
f.reply(()=>new Response('{',{headers:{'Content-Type':'application/json'}}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message===(sk?'Odpoveď z /api/demo obsahuje neplatný JSON':'Invalid JSON response from /api/demo'))
|
||||||
|
f.reply(()=>({ok:true,headers:{get:()=>null},text:async()=>''}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message.includes(sk?'neznámy typ obsahu':'unknown content type'))
|
||||||
|
const detail={error:'Original technical detail <raw>&',details:{keep:1},suggestion:'Keep diagnostics'}
|
||||||
|
f.reply(()=>new Response(JSON.stringify(detail),{status:400,headers:{'Content-Type':'application/json'}}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo',{method:'POST',body:'fixture'}),e=>{assert.equal(e.message,detail.error);assert.equal(e.status,400);assert.deepEqual(e.body,detail);return true})
|
||||||
|
f.reply(()=>new Response('{',{status:400,statusText:'Bad Request',headers:{'Content-Type':'application/json'}}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),e=>e.message.includes('400 Bad Request'))
|
||||||
|
const result={ok:true,items:[1,2],empty:null};f.reply(()=>new Response(JSON.stringify(result),{headers:{'Content-Type':'application/json'}}))
|
||||||
|
assert.deepEqual(await f.fetchApi('/api/demo',{method:'PUT',body:'unchanged',headers:{'X-Fixture':'keep'}}),result)
|
||||||
|
const call=f.calls.at(-1);assert.equal(call.url,'/api/demo');assert.equal(call.options.method,'PUT');assert.equal(call.options.body,'unchanged');assert.equal(call.options.headers.Authorization,'Bearer fixture-token');assert.equal(call.options.headers['X-Fixture'],'keep');assert.equal(call.options.cache,'no-store')
|
||||||
|
assert.equal(f.calls.length,7,'Each request calls fetch exactly once, no retries or side calls')
|
||||||
|
const network=new Error('Original network diagnostic');f.reply(()=>{throw network});await assert.rejects(f.fetchApi('/api/demo'),e=>e===network)
|
||||||
|
assert.equal(f.calls.length,8);assert.equal(f.reloads(),0)
|
||||||
|
}
|
||||||
|
for(const token of ['', 'fixture-token']){
|
||||||
|
const f=fixture('sk',token);f.reply(()=>new Response('',{status:401}))
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),/Chýba platné prihlásenie/)
|
||||||
|
assert.equal(f.local.has('proxmenux-auth-token'),false);assert.equal(f.reloads(),token?1:0)
|
||||||
|
await assert.rejects(f.fetchApi('/api/demo'),/Chýba platné prihlásenie/);assert.equal(f.reloads(),token?1:0)
|
||||||
|
assert.equal(f.calls.length,2)
|
||||||
|
}
|
||||||
|
const privateMode=loadTypeScript('lib/api-errors.ts',{document:{documentElement:{lang:'sk'}},window:{get localStorage(){throw new Error('denied')}}})
|
||||||
|
assert.equal(privateMode.apiFailure('invalidJson',{endpoint:'/api/demo'}),'Odpoveď z /api/demo obsahuje neplatný JSON')
|
||||||
|
const stored=loadTypeScript('lib/api-errors.ts',{window:{localStorage:{getItem:()=> 'sk'}}})
|
||||||
|
assert.equal(stored.apiFailure('unknownContentType'),'neznámy typ obsahu')
|
||||||
|
// Existing consumer paths, with their actual imports bound to real fetchApi.
|
||||||
|
const helper=fixture();Object.assign(helper.globals,{AbortSignal,setTimeout:fn=>{fn();return 1}})
|
||||||
|
const control=loadTypeScript('lib/vm-control.ts',helper.globals)
|
||||||
|
helper.reply(()=>new Response(JSON.stringify({status:'stopped',exitstatus:'OK',guest_status:'running'}),{headers:{'Content-Type':'application/json'}}))
|
||||||
|
await control.waitForVmControlTask(999,'reboot','UPID:fixture');assert.ok(helper.calls.at(-1).url.includes('/control-task/UPID%3Afixture'))
|
||||||
|
const archive=loadTypeScript('lib/guest-backup-archives.ts',helper.globals),backup={volid:'store:fixture',storage:'store'}
|
||||||
|
helper.reply(()=>new Response(JSON.stringify({results:[{volid:backup.volid,status:'deleted'}]}),{headers:{'Content-Type':'application/json'}}))
|
||||||
|
assert.equal((await archive.deleteGuestBackups(999,[backup]))[0].status,'deleted')
|
||||||
|
const deletion=helper.calls.at(-1);assert.equal(deletion.options.method,'POST');assert.equal(JSON.parse(deletion.options.body).confirm,true)
|
||||||
|
helper.reply(()=>new Response('fixture',{status:500,statusText:'Fixture'}));const count=helper.calls.length
|
||||||
|
await assert.rejects(archive.deleteGuestBackups(999,[backup]),/Požiadavka na API zlyhala/);assert.equal(helper.calls.length,count+1,'No retry of delete after a lost response')
|
||||||
|
const updates=loadTypeScript('lib/lxc-update-observation.ts',helper.globals)
|
||||||
|
helper.reply(()=>new Response(JSON.stringify({count:2,available:true}),{headers:{'Content-Type':'application/json'}}))
|
||||||
|
assert.equal((await updates.checkLxcUpdates(999)).count,2);assert.equal(helper.calls.at(-1).url,'/api/vms/999/updates/check')
|
||||||
|
helper.reply(()=>new Response(JSON.stringify(helper.calls.at(-1).options.method==='POST'?{job_id:'fixture'}:{state:'complete',update_check:{count:0}}),{headers:{'Content-Type':'application/json'}}))
|
||||||
|
assert.equal((await updates.refreshCheckLxcUpdates(999)).count,0)
|
||||||
|
const terminal=loadTypeScript('lib/terminal-ws.ts',helper.globals)
|
||||||
|
helper.reply(()=>new Response('fixture',{status:500}));assert.equal(await terminal.getTicketedWsUrl('wss://fixture.invalid/ws'),'wss://fixture.invalid/ws')
|
||||||
|
assert.equal(helper.calls.at(-1).url,'/api/terminal/ticket')
|
||||||
|
console.log('PASS actual fetchApi: EN/SK/fallback, HTTP/type/JSON/401, backend detail/body/status, original requests and success; no extra fetch/retry/mutation')
|
||||||
|
}
|
||||||
|
main().catch(error=>{console.error(error);process.exitCode=1})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm')
|
||||||
|
const root=path.resolve(__dirname,'..'),ts=require(root+'/AppImage/node_modules/typescript')
|
||||||
|
const code=ts.transpileModule(fs.readFileSync(root+'/AppImage/lib/api-config.ts','utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText
|
||||||
|
async function main(){
|
||||||
|
let calls=0,reply=()=>new Response(JSON.stringify({ok:true}),{headers:{'Content-Type':'application/json'}})
|
||||||
|
// Existing consumers compile the actual transport in VM contexts without imports.
|
||||||
|
const context={exports:{},process:{env:{}},console:{error(){}},fetch:async()=>{calls++;return reply()}}
|
||||||
|
vm.runInNewContext(code,context)
|
||||||
|
assert.deepEqual(await context.exports.fetchApi('/api/fixture'),{ok:true})
|
||||||
|
const body={error:'Original backend detail',code:'fixture',details:{keep:true}}
|
||||||
|
reply=()=>new Response(JSON.stringify(body),{status:400,headers:{'Content-Type':'application/json'}})
|
||||||
|
await assert.rejects(context.exports.fetchApi('/api/fixture',{method:'POST',body:'unchanged'}),error=>{assert.equal(error.message,body.error);assert.equal(error.status,400);assert.deepEqual(error.body,body);return true})
|
||||||
|
const network=new Error('Original network detail');reply=()=>{throw network}
|
||||||
|
await assert.rejects(context.exports.fetchApi('/api/fixture'),error=>error===network)
|
||||||
|
assert.equal(calls,3,'No locale import, retry or extra request on these paths')
|
||||||
|
console.log('PASS legacy transport imports: success, backend payload/status and network identity without a locale loader; exactly three calls')
|
||||||
|
}
|
||||||
|
main().catch(error=>{console.error(error);process.exitCode=1})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Actual Settings callbacks; terminal is a JSX stub, never a real updater.
|
||||||
|
const assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path')
|
||||||
|
const {fixture,flatten,text,translate,catalogs,descriptions}=require('./optimization-settings-fixture.cjs')
|
||||||
|
async function main(){
|
||||||
|
const known={key:'time_sync',name:'Time Synchronization',has_source:true,has_update:true,version:'1.0',available_version:'1.1',function:'configure_time_sync',function_auto:'configure_time_sync',source:'auto',description:catalogs.en.settings.optimizations.toolDescriptions.timeSync}
|
||||||
|
const unknown={...known,key:'future',name:'Future Tool',function:'future_fn',function_auto:'future_fn',description:'New server wording <raw>&'}
|
||||||
|
for(const locale of ['sk','en','de']){
|
||||||
|
const f=fixture(locale,[known,unknown]);f.render();await f.settle()
|
||||||
|
const expected=translate(locale)('settings.optimizations.toolNames.time_sync')
|
||||||
|
assert.ok(text(f.tree()).includes(expected));assert.ok(text(f.tree()).includes('Future Tool'))
|
||||||
|
const open=flatten(f.tree()).find(n=>n.props?.title===f.t('settings.optimizations.viewUpdates'));assert.ok(open)
|
||||||
|
open.props.onClick();f.render()
|
||||||
|
assert.ok(text(f.tree()).includes(descriptions.exports.optimizationDescription(known.description,translate(locale))))
|
||||||
|
assert.ok(text(f.tree()).includes(unknown.description))
|
||||||
|
const count=f.calls.length
|
||||||
|
flatten(f.tree()).find(n=>n.type==='button'&&text(n)===f.t('actions.cancel')).props.onClick();f.render()
|
||||||
|
assert.equal(f.calls.length,count,'Cancel has no scan/update effects')
|
||||||
|
open.props.onClick();f.render()
|
||||||
|
flatten(f.tree()).find(n=>n.type==='button'&&text(n).trim()===f.t('settings.optimizations.updateSelected')).props.onClick();f.render()
|
||||||
|
const terminal=flatten(f.tree()).find(n=>n.props?.scriptName==='update_post_install_function')
|
||||||
|
assert.equal(terminal.props.params.FUNCTIONS_BATCH,'auto:configure_time_sync:time_sync\nauto:future_fn:future')
|
||||||
|
assert.equal(terminal.props.params.EXECUTION_MODE,'web');assert.equal(f.calls.length,count,'Only an inert terminal node was created')
|
||||||
|
const single=fixture(locale,[known]);single.render();await single.settle()
|
||||||
|
const button=flatten(single.tree()).find(n=>n.props?.title===single.t('settings.optimizations.updateToolTitle',{name:expected,version:'1.1'}));assert.ok(button)
|
||||||
|
button.props.onClick({stopPropagation(){}});single.render()
|
||||||
|
const one=flatten(single.tree()).find(n=>n.props?.scriptName==='update_post_install_function')
|
||||||
|
assert.equal(one.props.title,single.t('settings.optimizations.updateOneTitle',{name:expected}))
|
||||||
|
assert.equal(one.props.params.FUNCTIONS_BATCH,'auto:configure_time_sync:time_sync')
|
||||||
|
assert.equal([...f.calls,...single.calls].filter(c=>c.method!=='GET'||/scan|script-runner/.test(c.endpoint)).length,0)
|
||||||
|
}
|
||||||
|
for(const locale of ['sk','en','de']){
|
||||||
|
const t=translate(locale)
|
||||||
|
for(const [key,name] of Object.entries(catalogs.en.settings.optimizations.toolNames)){
|
||||||
|
assert.equal(descriptions.exports.optimizationToolName(key,name,t),locale==='sk'?catalogs.sk.settings.optimizations.toolNames[key]:name)
|
||||||
|
}
|
||||||
|
assert.equal(descriptions.exports.optimizationToolName('future','Future <raw>&',t),'Future <raw>&')
|
||||||
|
}
|
||||||
|
const source=path.join(__dirname,'../scripts/post_install')
|
||||||
|
const actual=new Set(['auto_post_install.sh','customizable_post_install.sh'].flatMap(file=>[...fs.readFileSync(path.join(source,file),'utf8').matchAll(/^\s*# description: (.+)$/gm)].map(m=>m[1].trim())))
|
||||||
|
const english=catalogs.en.settings.optimizations.toolDescriptions
|
||||||
|
assert.deepEqual(new Set(Object.values(english)),actual)
|
||||||
|
for(const [key,description]of Object.entries(english))assert.equal(descriptions.exports.optimizationDescription(description,translate('sk')),catalogs.sk.settings.optimizations.toolDescriptions[key])
|
||||||
|
assert.equal(descriptions.exports.optimizationDescription('Changed version description',translate('sk')),'Changed version description')
|
||||||
|
console.log('PASS optimization names/descriptions: actual Settings cards/single/batch/cancel, unchanged payload, EN/SK/fallback, exact live-description matching; host calls 0')
|
||||||
|
}
|
||||||
|
main().catch(error=>{console.error(error);process.exitCode=1})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Run the real wrapper only against inert flow scripts and temporary markers."""
|
||||||
|
from pathlib import Path
|
||||||
|
import os,re,shlex,subprocess,tempfile,unittest
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
FILE='scripts/post_install/update_post_install_function.sh'
|
||||||
|
TRANSLATE=re.search(r'(?ms)^translate\(\) \{.*?^\}',(ROOT/'scripts/utils.sh').read_text()).group()
|
||||||
|
|
||||||
|
def run(source,params,language='en',utilities=True):
|
||||||
|
with tempfile.TemporaryDirectory(prefix='postinstall-i18n-') as temp:
|
||||||
|
tmp=Path(temp);base=tmp/'base';flow=base/'scripts/post_install';flow.mkdir(parents=True)
|
||||||
|
trace=tmp/'trace';trace.write_text('');blocked=tmp/'blocked';blocked.write_text('')
|
||||||
|
for name in ('auto_post_install.sh','customizable_post_install.sh'):
|
||||||
|
(flow/name).write_text('fixture_ok() { echo ok >> "$FIXTURE_TRACE"; return 0; }\nfixture_fail() { echo fail >> "$FIXTURE_TRACE"; return 7; }\n')
|
||||||
|
if utilities:
|
||||||
|
(base/'utils.sh').write_text('load_language(){ LANGUAGE="$FIXTURE_LANG"; }; initialize_cache(){ :; }; show_proxmenux_logo(){ :; }; LANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\n'+'\n'.join(name+'(){ printf "%s\\n" "$1"; }' for name in ('msg_info2','msg_error','msg_ok','msg_warn','msg_success'))+'\n')
|
||||||
|
binary=tmp/'bin';binary.mkdir()
|
||||||
|
for name in ('qm','pct','pvesh','pvesm','apt-get','curl','wget','mount','systemctl','reboot'):
|
||||||
|
p=binary/name;p.write_text('#!/bin/sh\necho '+shlex.quote(name)+' >> "$FIXTURE_BLOCKED"\nexit 97\n');p.chmod(0o755)
|
||||||
|
rewritten=source.replace('/usr/local/share/proxmenux',str(base)).replace('/var/run/proxmenux-update-',str(tmp/'proxmenux-update-'))
|
||||||
|
script=tmp/'wrapper.sh';script.write_text(rewritten)
|
||||||
|
env={**os.environ,'PATH':str(binary)+':'+os.environ['PATH'],'FIXTURE_TRACE':str(trace),'FIXTURE_BLOCKED':str(blocked),'FIXTURE_LANG':language,'SOURCE_TYPE':'','FUNCTION_NAME':'','TOOL_KEY':'','FUNCTIONS_BATCH':'',**params}
|
||||||
|
result=subprocess.run(['bash',str(script)],input='\n',text=True,capture_output=True,env=env,timeout=10)
|
||||||
|
assert blocked.read_text()=='','Forbidden host command'
|
||||||
|
assert not (tmp/'proxmenux-update-in-progress').exists(),'Marker cleanup'
|
||||||
|
return result.returncode,result.stdout+result.stderr,trace.read_text()
|
||||||
|
|
||||||
|
class PostInstallI18n(unittest.TestCase):
|
||||||
|
def test_single_batch_exit_codes_and_fallback_without_host_calls(self):
|
||||||
|
source=(ROOT/FILE).read_text()
|
||||||
|
cases=[({},2,''),
|
||||||
|
({'SOURCE_TYPE':'auto','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'},0,'ok\n'),
|
||||||
|
({'SOURCE_TYPE':'detected','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'},0,'ok\n'),
|
||||||
|
({'SOURCE_TYPE':'custom','FUNCTION_NAME':'missing_fn','TOOL_KEY':'fixture'},1,''),
|
||||||
|
({'SOURCE_TYPE':'wrong','FUNCTION_NAME':'fixture_ok'},1,''),
|
||||||
|
({'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three\ndetected:fixture_ok:four'},1,'ok\nfail\nok\n')]
|
||||||
|
for utilities in (False,True):
|
||||||
|
for params,expected_code,expected_trace in cases:
|
||||||
|
with self.subTest(utilities=utilities,params=params):
|
||||||
|
code,text,trace=run(source,params,utilities=utilities)
|
||||||
|
self.assertEqual(code,expected_code)
|
||||||
|
self.assertEqual(trace,expected_trace)
|
||||||
|
self.assertNotIn('command not found',text)
|
||||||
|
def test_slovak_missing_failure_summary_and_no_real_host_calls(self):
|
||||||
|
source=(ROOT/FILE).read_text()
|
||||||
|
code,text,trace=run(source,{'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three'},language='sk')
|
||||||
|
self.assertEqual(code,1);self.assertEqual(trace,'ok\nfail\n')
|
||||||
|
self.assertIn('Aktualizácia one je dokončená.',text)
|
||||||
|
self.assertIn('Aktualizácia two skončila s návratovým kódom 7.',text)
|
||||||
|
self.assertIn("Funkcia 'missing_fn' nie je definovaná v postupe auto.",text)
|
||||||
|
self.assertIn('Zlyhalo 2 z 3 aktualizácií',text)
|
||||||
|
self.assertIn('Stlač Enter na zatvorenie terminálu',text)
|
||||||
|
def test_syntax(self):subprocess.run(['bash','-n',str(ROOT/FILE)],check=True)
|
||||||
|
|
||||||
|
if __name__=='__main__':unittest.main()
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"""Real translate and display-only shell calls; never source a VM creator."""
|
||||||
|
from pathlib import Path
|
||||||
|
import importlib.util,json,os,re,shlex,subprocess,tempfile,unittest
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
ROOT=Path(__file__).resolve().parents[1]
|
||||||
|
SPEC=importlib.util.spec_from_file_location('official_i18n',ROOT/'.github/scripts/build_translation_cache.py')
|
||||||
|
OFFICIAL=importlib.util.module_from_spec(SPEC);SPEC.loader.exec_module(OFFICIAL)
|
||||||
|
TRANSLATE=re.search(r'(?ms)^translate\(\) \{.*?^\}',(ROOT/'scripts/utils.sh').read_text()).group()
|
||||||
|
FILES=('scripts/vm/synology.sh','scripts/utilities/uup_dump_iso_creator.sh')
|
||||||
|
DISPLAY=re.compile(r'^\s*(?:msg_(?:info2?|ok|warn|error|success|title)|echo -[en])\s+".*"\s*(?:>&2)?\s*$')
|
||||||
|
|
||||||
|
def display_calls(source):
|
||||||
|
return [line for line in source.splitlines() if DISPLAY.fullmatch(line) and not re.search(r'\$\((?!(?:translate|printf|basename)\b)',line)]
|
||||||
|
|
||||||
|
def render(calls,language):
|
||||||
|
with tempfile.TemporaryDirectory(prefix='remaining-sk-display-') as tmp:
|
||||||
|
blocked=Path(tmp)/'blocked';blocked.write_text('')
|
||||||
|
definitions='\n'.join(name+'() { printf "%s\\n" "$1"; }' for name in ('msg_info','msg_info2','msg_ok','msg_warn','msg_error','msg_success','msg_title'))
|
||||||
|
forbidden='\n'.join(name+'() { echo '+shlex.quote(name)+' >> '+shlex.quote(str(blocked))+'; return 97; }' for name in ('qm','pct','pvesh','apt-get','curl','wget','mount','reboot','systemctl','rm','touch','mkdir','chmod'))
|
||||||
|
values={'NAME':'Synology VM','VMID':'999','IMAGES_DIR':'/fixture/images','EFI_STORAGE':'efi-store','LOADER_STORAGE':'loader-store','STORAGE':'data-store','STORAGE_TYPE':'zfspool','LOADER_NAME':'Arc','LOADER_FILE':'/fixture/loader.img','FILE':'loader.img','DISK_NUM':'2','SATA_ID':'sata2','SIZE':'32','DISK_MODEL':'Fixture SSD','DISK_SIZE':'32G','DISK_ID':'sata3','DISK':'/dev/fixture','PCI_DEV':'0000:01:00.0','HOSTPCI_INDEX':'0','HN':'fixture-vm','CORE_COUNT':'2','RAM_SIZE':'4096','BRG':'vmbr0','MAC':'02:00:00:00:00:01','VLAN1':'Default','MTU1':'Default','SERIAL_PORT':'socket','START_VM':'no','MACH':'q35','OUT_DIR':'/fixture/iso','BUILD_ID':'fixture','EDITION':'professional','ARCH':'amd64','DEFAULT_TMP':'/fixture/tmp','TMP_DIR':'/fixture/tmp','ISO_FILE':'fixture.iso','user_path':'/fixture'}
|
||||||
|
catalog={} if language=='en' else json.loads((ROOT/'lang'/f'{language}.json').read_text())
|
||||||
|
values.update({key:catalog.get('Default','Default') for key in ('VLAN1','MTU1')})
|
||||||
|
assignments='\n'.join(key+'='+shlex.quote(value) for key,value in values.items())
|
||||||
|
script='LANGUAGE='+shlex.quote(language)+'\nLANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\n'+definitions+'\n'+forbidden+'\n'+assignments+'\n'+'\n'.join(calls)+'\n'
|
||||||
|
result=subprocess.run(['bash','-c',script],text=True,capture_output=True,timeout=15,check=True)
|
||||||
|
assert blocked.read_text()==''
|
||||||
|
return result.stdout+result.stderr
|
||||||
|
|
||||||
|
class RemainingCliI18n(unittest.TestCase):
|
||||||
|
def test_badges_remain_valid_with_hostile_translations_without_host_calls(self):
|
||||||
|
source=(ROOT/FILES[0]).read_text()
|
||||||
|
html=re.search(r'(?ms)^HTML_DESC=".*?^</div>"',source).group()
|
||||||
|
class Images(HTMLParser):
|
||||||
|
def __init__(self):super().__init__();self.images=[]
|
||||||
|
def handle_starttag(self,tag,attrs):
|
||||||
|
if tag=='img':self.images.append(dict(attrs))
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
blocked=Path(tmp)/'blocked';blocked.write_text('')
|
||||||
|
forbidden='\n'.join(name+'(){ echo '+shlex.quote(name)+' >> '+shlex.quote(str(blocked))+'; return 97; }' for name in ('qm','pct','pvesh','curl','wget','mount','reboot','systemctl'))
|
||||||
|
script=forbidden+'\ntranslate(){ printf "%s" "O\'Brien & <label> / ? #"; }; LOADER_URL=https://fixture.invalid/loader; LOADER_NAME=fixture; DISK_INFO=;\n'+html+'\nprintf "%s" "$HTML_DESC"'
|
||||||
|
result=subprocess.run(['bash','-c',script],text=True,capture_output=True,timeout=10,check=True)
|
||||||
|
self.assertEqual(blocked.read_text(),'')
|
||||||
|
parser=Images();parser.feed(result.stdout)
|
||||||
|
badges=[image for image in parser.images if image.get('src','').startswith('https://img.shields.io/')]
|
||||||
|
self.assertEqual([badge.get('alt') for badge in badges],['Docs','Code','Loader','Ko-fi'])
|
||||||
|
for badge in badges:
|
||||||
|
self.assertNotIn('Brien',badge['src'])
|
||||||
|
self.assertNotIn('<label>',badge['src'])
|
||||||
|
def test_real_default_and_advanced_wizard_cancel_before_host_calls(self):
|
||||||
|
source=(ROOT/'scripts/vm/synology.sh').read_text()
|
||||||
|
functions='\n'.join(re.search(r'(?ms)^function '+name+r'\(\) \{.*?^\}',source).group() for name in ('default_settings','select_disk_type','_build_storage_plan_summary','advanced_settings','exit_script'))
|
||||||
|
for action in ('default_settings','advanced_settings'):
|
||||||
|
with tempfile.TemporaryDirectory(prefix='remaining-sk-cancel-') as tmp:
|
||||||
|
blocked=Path(tmp)/'blocked';blocked.write_text('');dialog_log=Path(tmp)/'dialog';dialog_log.write_text('')
|
||||||
|
forbidden='\n'.join(name+'() { echo '+shlex.quote(name)+' >> '+shlex.quote(str(blocked))+'; return 97; }' for name in ('qm','pct','pvesh','pvesm','curl','wget','apt-get','mount','systemctl','reboot','select_loader'))
|
||||||
|
script='LANGUAGE=sk\nLANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\n'+forbidden+'\n'+functions+'\nclear(){ :; }; sleep(){ :; }; header_info(){ :; }; msg_warn(){ echo "$1"; }; whiptail(){ printf "%s\\n" "$*" >> '+shlex.quote(str(dialog_log))+'; return 1; }; NAME="Synology VM"; NEXTID=999; GEN_MAC="02:00:00:00:00:01"; '+action
|
||||||
|
result=subprocess.run(['bash','-c',script],text=True,capture_output=True,timeout=10)
|
||||||
|
self.assertEqual(blocked.read_text(),'')
|
||||||
|
self.assertIn('Zrušiť' if action=='default_settings' else 'Ukončiť skript',dialog_log.read_text())
|
||||||
|
self.assertNotIn('command not found',result.stderr)
|
||||||
|
def test_generated_uup_helper_is_localized_and_valid_without_execution(self):
|
||||||
|
source=(ROOT/FILES[1]).read_text();body=source.split('cat > uup_download_linux.sh <<EOF\n',1)[1].split('\nEOF',1)[0]
|
||||||
|
script='LANGUAGE=sk\nLANG_DIR='+shlex.quote(str(ROOT/'lang'))+'\n'+TRANSLATE+'\ncat <<EOF\n'+body+'\nEOF\n'
|
||||||
|
generated=subprocess.check_output(['bash','-c',script],text=True,timeout=10)
|
||||||
|
self.assertIn('Príkaz sa nenašiel.',generated);self.assertIn('Vytvorenie zoznamu súborov UUP',generated)
|
||||||
|
subprocess.run(['bash','-n'],input=generated,text=True,check=True)
|
||||||
|
def test_slovak_display_errors_without_host_calls(self):
|
||||||
|
for file in FILES:
|
||||||
|
translated=render(display_calls((ROOT/file).read_text()),'sk')
|
||||||
|
if file.endswith('synology.sh'):
|
||||||
|
self.assertIn('Priradenie disku 2 (sata2) na data-store zlyhalo',translated)
|
||||||
|
self.assertIn('Nenašlo sa použiteľné úložisko.',translated)
|
||||||
|
self.assertNotIn('Failed to configure EFI disk',translated)
|
||||||
|
self.assertIn('/fixture/images',translated)
|
||||||
|
self.assertIn('Veľkosť MTU rozhrania: Predvolené',translated)
|
||||||
|
self.assertNotIn('Default',translated)
|
||||||
|
def test_official_extractor_and_placeholders(self):
|
||||||
|
catalog=json.loads((ROOT/'lang/sk.json').read_text())
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
keys=OFFICIAL.extract_translate_texts(Path(tmp),[ROOT/file for file in FILES])
|
||||||
|
self.assertGreater(len(keys),90)
|
||||||
|
self.assertEqual([key for key in keys if not catalog.get(key)],[])
|
||||||
|
for key in keys:
|
||||||
|
self.assertEqual(re.findall(r'%(?:[0-9]+\$)?[sd]',key),re.findall(r'%(?:[0-9]+\$)?[sd]',catalog[key]),key)
|
||||||
|
self.assertEqual(catalog['Boot disk:'],'Zavádzací disk:')
|
||||||
|
self.assertEqual(catalog['Mount point:'],'Bod pripojenia:')
|
||||||
|
def test_shell_syntax(self):
|
||||||
|
for file in FILES:subprocess.run(['bash','-n',str(ROOT/file)],check=True)
|
||||||
|
|
||||||
|
if __name__=='__main__':unittest.main()
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Load real TS modules with injected host boundaries; no bundler or network.
|
||||||
|
const fs=require('node:fs'),path=require('node:path'),vm=require('node:vm')
|
||||||
|
const app=path.resolve(__dirname,'../AppImage'),ts=require(path.join(app,'node_modules/typescript'))
|
||||||
|
function loadTypeScript(file,globals={}){
|
||||||
|
const resolved=path.isAbsolute(file)?file:path.join(app,file)
|
||||||
|
if(resolved.endsWith('.json'))return JSON.parse(fs.readFileSync(resolved,'utf8'))
|
||||||
|
const exports={}
|
||||||
|
const context={process:{env:{}},console,Error,SyntaxError,...globals,exports,module:{exports},require:name=>{
|
||||||
|
if(name.startsWith('@/'))return loadTypeScript(path.join(app,name.slice(2))+'.ts',globals)
|
||||||
|
if(!name.startsWith('.'))return require(name)
|
||||||
|
let target=path.resolve(path.dirname(resolved),name)
|
||||||
|
if(!fs.existsSync(target))target+='.ts'
|
||||||
|
return loadTypeScript(target,globals)
|
||||||
|
}}
|
||||||
|
const compiled=ts.transpileModule(fs.readFileSync(resolved,'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020,esModuleInterop:true}}).outputText
|
||||||
|
vm.runInNewContext(compiled,context,{filename:resolved})
|
||||||
|
return context.module.exports
|
||||||
|
}
|
||||||
|
module.exports={loadTypeScript}
|
||||||
Reference in New Issue
Block a user