mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-01 10:22:08 +00:00
fix(i18n): address Monitor and CLI review feedback
This commit is contained in:
@@ -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 =>
|
||||
fs.existsSync(path.join(app, 'messages', locale, 'common.json'))).map(locale =>
|
||||
[locale, JSON.parse(read(`messages/${locale}/common.json`))]));
|
||||
const registered = provider.statements.flatMap(statement => ts.isVariableStatement(statement) ?
|
||||
statement.declarationList.declarations.filter(declaration => declaration.name.getText(provider) === 'MESSAGE_CATALOG') : []);
|
||||
assert.equal(registered.length, 1, 'real provider catalog declaration');
|
||||
assert.deepEqual(registered[0].initializer.properties.map(property => property.name.getText(provider)).sort(),
|
||||
const catalogSource = ts.createSourceFile('catalogs.ts', read('lib/i18n/catalogs.ts'), ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
const registered = catalogSource.statements.flatMap(statement => ts.isVariableStatement(statement) ?
|
||||
statement.declarationList.declarations.filter(declaration => declaration.name.getText(catalogSource) === 'MESSAGE_CATALOG') : []);
|
||||
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');
|
||||
assert.ok(catalogs.en, 'English fallback catalog exists');
|
||||
const translator = (language, messages = catalogs) =>
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
msg_title "Synology VM Creator"
|
||||
msg_warn "$(translate "No compatible GPU detected for VM passthrough. Skipping GPU wizard option.")"
|
||||
msg_warn "$gpu_notice"
|
||||
msg_info2 "$(translate "Launching GPU passthrough assistant for VM") ${VMID}..."
|
||||
echo -e "${DEF}Using Default Settings${CL}"
|
||||
echo -e "${CUS}Using Advanced Settings${CL}"
|
||||
echo -e " ${TAB}${DGN}Using Virtual Machine ID: ${BGN}${VMID}${CL}"
|
||||
echo -e " ${TAB}${DGN}Using Machine Type: ${BGN}q35${CL}"
|
||||
echo -e " ${TAB}${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}"
|
||||
echo -e " ${TAB}${DGN}Using Hostname: ${BGN}${HN}${CL}"
|
||||
echo -e " ${TAB}${DGN}Using CPU Model: ${BGN}Host${CL}"
|
||||
echo -e " ${TAB}${DGN}Allocated Cores: ${BGN}${CORE_COUNT}${CL}"
|
||||
echo -e " ${TAB}${DGN}Allocated RAM: ${BGN}${RAM_SIZE}${CL}"
|
||||
echo -e " ${TAB}${DGN}Using Bridge: ${BGN}${BRG}${CL}"
|
||||
echo -e " ${TAB}${DGN}Using MAC Address: ${BGN}${MAC}${CL}"
|
||||
echo -e " ${TAB}${DGN}Using VLAN: ${BGN}Default${CL}"
|
||||
echo -e " ${TAB}${DGN}Using Interface MTU Size: ${BGN}Default${CL}"
|
||||
echo -e " ${TAB}${DGN}Configuring Serial Port: ${BGN}${SERIAL_PORT}${CL}"
|
||||
echo -e " ${TAB}${DGN}Start VM when completed: ${BGN}${START_VM}${CL}"
|
||||
echo -e "${DEF}Creating a $NAME using the above default settings${CL}"
|
||||
echo -e "${CROSS}${RD} ID $VMID is already in use${CL}"
|
||||
echo -e "${DGN}Virtual Machine ID: ${BGN}$VMID${CL}"
|
||||
echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
|
||||
echo -e "${DGN}Using Machine Type: ${BGN}$MACH${CL}"
|
||||
echo -e "${DGN}Using BIOS Type: ${BGN}SeaBIOS${CL}"
|
||||
echo -e "${DGN}Using BIOS Type: ${BGN}OVMF (UEFI)${CL}"
|
||||
echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
|
||||
echo -e "${DGN}Using Hostname: ${BGN}$HN${CL}"
|
||||
echo -e "${DGN}Using CPU Model: ${BGN}Host${CL}"
|
||||
echo -e "${DGN}Using CPU Model: ${BGN}KVM64${CL}"
|
||||
echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
|
||||
echo -e "${DGN}Allocated Cores: ${BGN}$CORE_COUNT${CL}"
|
||||
echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
|
||||
echo -e "${DGN}Allocated RAM: ${BGN}$RAM_SIZE${CL}"
|
||||
echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
|
||||
echo -e "${DGN}Using Bridge: ${BGN}$BRG${CL}"
|
||||
echo -e "${DGN}Using MAC Address: ${BGN}$MAC${CL}"
|
||||
echo -e "${DGN}Using MAC Address: ${BGN}$MAC1${CL}"
|
||||
echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
|
||||
echo -e "${DGN}Using Vlan: ${BGN}$VLAN1${CL}"
|
||||
echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
|
||||
echo -e "${DGN}Using Interface MTU Size: ${BGN}$MTU1${CL}"
|
||||
echo -e "${CUS}Creating a $NAME using the above advanced settings${CL}"
|
||||
echo -e "${CUS}Using Advanced Settings${CL}"
|
||||
echo -e "$summary"
|
||||
msg_warn "$(translate "Storage plan selection cancelled.")"
|
||||
msg_ok "$(translate "Virtual Disks Created:")"
|
||||
msg_ok "$(translate "Import Disks Selected:")"
|
||||
msg_ok "$(translate "Controllers + NVMe Selected:")"
|
||||
msg_info "$(translate "Detecting storage for disk images...")"
|
||||
msg_error "Unable to detect a valid storage location."
|
||||
msg_info "$(translate "Detecting PCI storage controllers and NVMe devices...")"
|
||||
msg_info "Creating images directory"
|
||||
msg_ok "Images directory created: $IMAGES_DIR"
|
||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
||||
echo -e "${DGN}${TAB}Selected Loader: ${BGN}$LOADER_NAME${CL}"
|
||||
msg_error "No custom image selected"
|
||||
echo -e "${DGN}${TAB}Using Custom Image: ${BGN}$(basename "$LOADER_FILE")${CL}"
|
||||
echo -e "${DGN}${TAB}Retrieving the URL for the ${BGN}$LOADER_NAME loader${CL}"
|
||||
msg_info "Installing unzip..."
|
||||
msg_error "Failed to install unzip"
|
||||
msg_ok "Installed unzip successfully."
|
||||
msg_error "$(translate "Failed to retrieve loader release metadata.")"
|
||||
msg_error "$(translate "No unambiguous loader download found in release metadata.")"
|
||||
msg_error "$(translate "Failed to prepare loader download directory.")"
|
||||
msg_error "$(translate "Failed to download loader image.")"
|
||||
msg_error "$(translate "Failed to extract a nonempty loader image.")"
|
||||
msg_error "$(translate "Failed to save loader image.")"
|
||||
msg_ok "Downloaded ${CL}${BL}${FILE}${CL} to ${IMAGES_DIR}"
|
||||
msg_error "Unable to detect a valid storage location for EFI disk." >&2
|
||||
msg_warn "$(translate "EFI storage selection cancelled.")" >&2
|
||||
msg_error "Unable to detect a valid storage location." >&2
|
||||
msg_warn "$(translate "Storage selection cancelled for $purpose.")" >&2
|
||||
msg_error "Failed to create base VM. Check VM ID and host configuration."
|
||||
msg_ok "Create a $NAME"
|
||||
msg_info "Configuring EFI disk"
|
||||
msg_error "EFI storage selection failed or was cancelled."
|
||||
msg_ok "EFI disk created (raw) and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}"
|
||||
msg_error "Failed to configure EFI disk"
|
||||
msg_ok "EFI disk created and configured on ${CL}${BL}$EFI_STORAGE${GN}${CL}"
|
||||
msg_error "Failed to configure EFI disk"
|
||||
msg_error "Failed to create EFI disk"
|
||||
msg_error "Loader storage selection failed or was cancelled."
|
||||
echo -n "Importing loader disk: "
|
||||
echo -n "."
|
||||
msg_error "Loader import failed. No disk detected."
|
||||
msg_ok "Loader imported successfully to ${CL}${BL}$LOADER_STORAGE${GN}${CL}"
|
||||
msg_ok "Configured loader disk as ide0"
|
||||
msg_error "Failed to assign loader disk to ide0"
|
||||
msg_error "Loader import failed. No disk detected in config."
|
||||
msg_ok "Configured loader disk as ide0"
|
||||
msg_error "Failed to assign loader disk"
|
||||
msg_ok "Loader configured as boot device."
|
||||
msg_info "Creating virtual disk (format=raw) for $STORAGE_TYPE..."
|
||||
msg_error "Failed to assign disk $DISK_NUM ($SATA_ID) on $STORAGE"
|
||||
msg_info "Allocating virtual disk for $STORAGE_TYPE..."
|
||||
msg_error "Failed to allocate virtual disk $DISK_NUM"
|
||||
msg_error "Failed to configure virtual disk as $SATA_ID"
|
||||
msg_ok "Configured virtual disk as $SATA_ID, ${SIZE}GB on ${CL}${BL}$STORAGE${CL} ${GN}"
|
||||
msg_ok "Configured import disk ${CL}${BL}($DISK_MODEL $DISK_SIZE)${CL}${GN} as $DISK_ID"
|
||||
msg_error "Failed to configure import disk $DISK as $DISK_ID"
|
||||
msg_warn "$(translate "Skipping SR-IOV device"): ${SRIOV_SKIPPED}"
|
||||
msg_warn "$(translate "No eligible Controller/NVMe devices remain after SR-IOV filtering. Skipping.")"
|
||||
msg_warn "$(translate "IOMMU was configured during this wizard and a reboot is pending.")"
|
||||
msg_warn "$(translate "Controller + NVMe assignment will be written now and become active after host reboot.")"
|
||||
msg_error "$(translate "IOMMU is not active. Skipping Controller + NVMe assignment.")"
|
||||
msg_error "$(translate "Controller + NVMe passthrough requires machine type q35. Skipping controller assignment.")"
|
||||
msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})"
|
||||
msg_warn "Controller/NVMe already present in VM config (${PCI_DEV})"
|
||||
msg_warn "$(translate "Controller/NVMe is in use by running VM") ${SRC_VMID} ($(translate "stop source VM first"))"
|
||||
msg_warn "$(translate "Start on boot disabled for VM") ${SRC_VMID}"
|
||||
msg_ok "$(translate "Controller/NVMe removed from source VM") ${SRC_VMID} (${PCI_DEV})"
|
||||
msg_info2 "$(translate "Skipped device"): ${PCI_DEV}"
|
||||
msg_ok "Configured controller/NVMe as hostpci${HOSTPCI_INDEX}: ${PCI_DEV}"
|
||||
msg_error "Failed to configure controller/NVMe: ${PCI_DEV}"
|
||||
msg_error "No disks/controllers configured."
|
||||
msg_info "Setting VM description"
|
||||
msg_error "Failed to set VM description"
|
||||
msg_ok "Configured VM description"
|
||||
msg_error "VM created with errors. Check configuration."
|
||||
msg_success "$(translate "Completed Successfully with GPU passthrough configured!")"
|
||||
msg_warn "$(translate "Host VFIO configuration changed (initramfs updated). Reboot required before starting the VM.")"
|
||||
msg_success "$(translate "Completed Successfully!")"
|
||||
msg_warn "$(translate "GPU passthrough was skipped (no compatible GPU detected).")"
|
||||
msg_warn "$(translate "GPU passthrough was not applied.")"
|
||||
echo -e "${TAB}${GN}$(translate "Next Steps:")${CL}"
|
||||
echo -e "${TAB}1. $(translate "Start the VM")"
|
||||
echo -e "${TAB}2. $(translate "Open the VM console and wait for the loader to boot")"
|
||||
echo -e "${TAB}3. $(translate "In the loader interface, follow the instructions to select your Synology model")"
|
||||
echo -e "${TAB}4. $(translate "Complete the DSM installation wizard")"
|
||||
echo -e "${TAB}5. $(translate "Find your device using https://finds.synology.com")"
|
||||
echo -e "${TAB}- $(translate "If you want to use a physical monitor on the passthrough GPU:")"
|
||||
echo -e "${TAB}• $(translate "First complete DSM setup and verify Web UI/SSH access.")"
|
||||
echo -e "${TAB}• $(translate "Then change the VM display to none (vga: none) when the system is stable.")"
|
||||
msg_ok "$(translate "IOMMU has been enabled — a system reboot is required")"
|
||||
msg_warn "$(translate "Rebooting the system...")"
|
||||
msg_info2 "$(translate "To use the VM without issues, the host must be restarted before starting it.")"
|
||||
msg_info2 "$(translate "Do not start the VM until the system has been rebooted.")"
|
||||
msg_warn "$(translate "VM creation cancelled before disk planning.")"
|
||||
@@ -1,200 +0,0 @@
|
||||
#!/bin/bash
|
||||
# ==========================================================
|
||||
# ProxMenux - Sprint 12B: re-run a single post-install function
|
||||
# ==========================================================
|
||||
# Invoked by the Monitor's "Update" buttons (Settings → ProxMenux
|
||||
# Optimizations) and by the bash menu's update flow. Sources the
|
||||
# appropriate post_install script and invokes one specific function so
|
||||
# the user can update a single tool without re-running the whole flow.
|
||||
#
|
||||
# Two invocation modes:
|
||||
#
|
||||
# SINGLE — set SOURCE_TYPE + FUNCTION_NAME (+ optionally TOOL_KEY):
|
||||
# SOURCE_TYPE=auto FUNCTION_NAME=install_log2ram_auto
|
||||
#
|
||||
# BATCH — set FUNCTIONS_BATCH to a newline-separated list of
|
||||
# "source:function:tool_key" triples (tool_key optional):
|
||||
# FUNCTIONS_BATCH="auto:install_log2ram_auto:log2ram
|
||||
# custom:install_ceph:ceph"
|
||||
#
|
||||
# In batch mode the wrapper iterates the list, sourcing each flow once
|
||||
# and re-using its function definitions. Tools coming from the same
|
||||
# flow share a single source step so the terminal output stays clean.
|
||||
# ==========================================================
|
||||
|
||||
set -e
|
||||
|
||||
LOCAL_SCRIPTS="/usr/local/share/proxmenux/scripts"
|
||||
BASE_DIR="/usr/local/share/proxmenux"
|
||||
UTILS_FILE="$BASE_DIR/utils.sh"
|
||||
|
||||
if [[ -f "$UTILS_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$UTILS_FILE"
|
||||
fi
|
||||
|
||||
if command -v load_language >/dev/null 2>&1; then
|
||||
load_language
|
||||
fi
|
||||
if command -v initialize_cache >/dev/null 2>&1; then
|
||||
initialize_cache
|
||||
fi
|
||||
|
||||
if command -v show_proxmenux_logo >/dev/null 2>&1; then
|
||||
show_proxmenux_logo
|
||||
fi
|
||||
|
||||
SOURCE_TYPE="${SOURCE_TYPE:-}"
|
||||
FUNCTION_NAME="${FUNCTION_NAME:-}"
|
||||
TOOL_KEY="${TOOL_KEY:-${FUNCTION_NAME}}"
|
||||
FUNCTIONS_BATCH="${FUNCTIONS_BATCH:-}"
|
||||
|
||||
# Build the list of (source, function, tool_key) tuples from either the
|
||||
# single-mode env vars or the batch payload.
|
||||
declare -a BATCH=()
|
||||
if [[ -n "$FUNCTIONS_BATCH" ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
BATCH+=("$line")
|
||||
done <<< "$FUNCTIONS_BATCH"
|
||||
elif [[ -n "$SOURCE_TYPE" && -n "$FUNCTION_NAME" ]]; then
|
||||
BATCH+=("${SOURCE_TYPE}:${FUNCTION_NAME}:${TOOL_KEY}")
|
||||
else
|
||||
echo "ERROR: provide either SOURCE_TYPE+FUNCTION_NAME (single mode) or FUNCTIONS_BATCH (batch mode)."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Source the flow scripts on first use. Both auto and customizable
|
||||
# guard their entry point with `if [[ "${BASH_SOURCE[0]}" == "${0}" ]]`,
|
||||
# so sourcing loads function definitions without triggering the
|
||||
# interactive menu or the extremeshok dialog.
|
||||
# ----------------------------------------------------------------------
|
||||
SOURCED_AUTO=0
|
||||
SOURCED_CUSTOM=0
|
||||
ensure_flow_loaded() {
|
||||
local source_type="$1"
|
||||
case "$source_type" in
|
||||
# `detected` is the synthetic source the registry assigns to a
|
||||
# tool when a legacy detector finds it on the host but the
|
||||
# entry was never written to installed_tools.json (e.g.
|
||||
# proxmox_repos on a pre-1.2.2 install). Treat it as `auto`
|
||||
# for the source-loading step — the legacy-tool's wrapper
|
||||
# function lives in auto_post_install.sh, and once the update
|
||||
# runs successfully `register_tool` overwrites the synthetic
|
||||
# entry with source="auto" so subsequent runs don't hit this
|
||||
# branch again.
|
||||
auto|detected)
|
||||
if [[ $SOURCED_AUTO -eq 0 ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$LOCAL_SCRIPTS/post_install/auto_post_install.sh"
|
||||
SOURCED_AUTO=1
|
||||
fi
|
||||
;;
|
||||
custom)
|
||||
if [[ $SOURCED_CUSTOM -eq 0 ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$LOCAL_SCRIPTS/post_install/customizable_post_install.sh"
|
||||
SOURCED_CUSTOM=1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: invalid source '$source_type' (must be 'auto', 'custom', or 'detected')"
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Suppress the Monitor's `service_fail` notifications while this
|
||||
# wrapper runs. Some post-install functions legitimately restart their
|
||||
# services, and systemd can expose a transient "failed" state before the
|
||||
# fresh unit starts — same class of noise as PVE services restarting
|
||||
# during an apt full-upgrade. Shared markers with
|
||||
# scripts/utilities/proxmox_update.sh; consumed by
|
||||
# AppImage/scripts/notification_events.py::is_apt_active_on_host.
|
||||
_PROXMENUX_UPDATE_MARKER="/var/run/proxmenux-update-in-progress"
|
||||
_PROXMENUX_UPDATE_FINISHED_MARKER="/var/run/proxmenux-update-just-finished"
|
||||
_proxmenux_postinstall_cleanup() {
|
||||
rm -f "$_PROXMENUX_UPDATE_MARKER"
|
||||
touch "$_PROXMENUX_UPDATE_FINISHED_MARKER"
|
||||
}
|
||||
trap _proxmenux_postinstall_cleanup EXIT
|
||||
touch "$_PROXMENUX_UPDATE_MARKER"
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Run each tool. We don't bail on the first failure — the user marked a
|
||||
# multi-select, they expect every chosen tool to be attempted. RCs are
|
||||
# collected and the wrapper exits non-zero if any failed.
|
||||
# ----------------------------------------------------------------------
|
||||
TOTAL=${#BATCH[@]}
|
||||
FAILED=0
|
||||
INDEX=0
|
||||
for entry in "${BATCH[@]}"; do
|
||||
INDEX=$((INDEX + 1))
|
||||
IFS=':' read -r src fn tkey <<< "$entry"
|
||||
[[ -z "$tkey" ]] && tkey="$fn"
|
||||
|
||||
if command -v msg_info2 >/dev/null 2>&1; then
|
||||
msg_info2 "[$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..."
|
||||
else
|
||||
echo "[ProxMenux] [$INDEX/$TOTAL] Updating ${tkey} (running ${fn} from ${src} flow)..."
|
||||
fi
|
||||
|
||||
if ! ensure_flow_loaded "$src"; then
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! declare -F "$fn" >/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."
|
||||
else
|
||||
echo "ERROR: function '$fn' is not defined in the ${src} flow."
|
||||
fi
|
||||
FAILED=$((FAILED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
set +e
|
||||
"$fn"
|
||||
RC=$?
|
||||
set -e
|
||||
|
||||
if [[ $RC -eq 0 ]]; then
|
||||
if command -v msg_ok >/dev/null 2>&1; then
|
||||
msg_ok "Update for ${tkey} completed."
|
||||
else
|
||||
echo "[ProxMenux] Update for ${tkey} completed."
|
||||
fi
|
||||
else
|
||||
if command -v msg_error >/dev/null 2>&1; then
|
||||
msg_error "Update for ${tkey} exited with status $RC."
|
||||
else
|
||||
echo "ERROR: update for ${tkey} exited with status $RC."
|
||||
fi
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $FAILED -eq 0 ]]; then
|
||||
if command -v msg_success >/dev/null 2>&1; then
|
||||
msg_success "All $TOTAL update(s) completed successfully."
|
||||
else
|
||||
echo "[ProxMenux] All $TOTAL update(s) completed successfully."
|
||||
fi
|
||||
else
|
||||
if command -v msg_warn >/dev/null 2>&1; then
|
||||
msg_warn "$FAILED of $TOTAL updates failed — see the messages above."
|
||||
else
|
||||
echo "WARNING: $FAILED of $TOTAL updates failed."
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v msg_success >/dev/null 2>&1; then
|
||||
msg_success "Press Enter to close this terminal..."
|
||||
else
|
||||
echo "Press Enter to close this terminal..."
|
||||
fi
|
||||
read -r 2>/dev/null || true
|
||||
|
||||
[[ $FAILED -eq 0 ]] && exit 0 || exit 1
|
||||
@@ -1,23 +0,0 @@
|
||||
msg_info "$(translate "Installing dependencies: ${MISSING[*]}")"
|
||||
msg_ok "$(translate "All dependencies installed and verified.")"
|
||||
msg_error "$(translate "Failed to install: ${MISSING[*]}")"
|
||||
msg_error "$(translate "Failed to install: ${MISSING[*]}")"
|
||||
msg_error "$(translate "Could not determine a valid ISO storage directory.")"
|
||||
msg_error "$(translate "Failed to create temporary directory:") $TMP_DIR"
|
||||
msg_warn "$(translate "Cancelled by user or empty URL.")"
|
||||
msg_error "$(translate "The URL does not contain the required parameters (id, pack, edition).")"
|
||||
echo -e "\n${BGN}=============== UUP Dump Creator ===============${CL}"
|
||||
echo -e " ${BGN}🆔 ID:${CL} ${DGN}$BUILD_ID${CL}"
|
||||
echo -e " ${BGN}🌐 Language:${CL} ${DGN}$LANG${CL}"
|
||||
echo -e " ${BGN}💿 Edition:${CL} ${DGN}$EDITION${CL}"
|
||||
echo -e " ${BGN}🖥️ Architecture:${CL} ${DGN}$ARCH${CL}"
|
||||
echo -e " ${BGN}📁 Destination:${CL} ${DGN}$OUT_DIR${CL}"
|
||||
echo -e "${BGN}===============================================${CL}\n"
|
||||
echo -e "\n${GN}=======================================${CL}"
|
||||
echo -e " 💿 ${GN}Starting ISO conversion...${CL}"
|
||||
echo -e "${GN}=======================================${CL}\n"
|
||||
msg_ok "$(translate "ISO created successfully:") $OUT_DIR/$(basename "$ISO_FILE")"
|
||||
msg_ok "$(translate "Cleaning temporary files...")"
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
msg_warn "$(translate "No ISO was generated.")"
|
||||
msg_success "$(translate "Press Enter to return to menu...")"
|
||||
@@ -1,5 +1,17 @@
|
||||
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)})
|
||||
@@ -9,6 +21,35 @@ function fixture(language='sk',token='fixture-token'){
|
||||
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'}}))
|
||||
|
||||
@@ -26,12 +26,21 @@ def run(source,params,language='en',utilities=True):
|
||||
return result.returncode,result.stdout+result.stderr,trace.read_text()
|
||||
|
||||
class PostInstallI18n(unittest.TestCase):
|
||||
def test_existing_single_batch_failures_and_fallback_contract(self):
|
||||
before=(ROOT/'tests/fixtures/cli-before-i18n/update_post_install_function.sh').read_text();after=(ROOT/FILE).read_text()
|
||||
cases=[{}, {'SOURCE_TYPE':'auto','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'detected','FUNCTION_NAME':'fixture_ok','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'custom','FUNCTION_NAME':'missing_fn','TOOL_KEY':'fixture'}, {'SOURCE_TYPE':'wrong','FUNCTION_NAME':'fixture_ok'}, {'FUNCTIONS_BATCH':'auto:fixture_ok:one\ncustom:fixture_fail:two\nauto:missing_fn:three\ndetected:fixture_ok:four'}]
|
||||
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 in cases:
|
||||
with self.subTest(utilities=utilities,params=params):self.assertEqual(run(before,params,utilities=utilities),run(after,params,utilities=utilities))
|
||||
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')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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')
|
||||
@@ -27,6 +28,25 @@ def render(calls,language):
|
||||
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'))
|
||||
@@ -45,13 +65,9 @@ class RemainingCliI18n(unittest.TestCase):
|
||||
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_english_display_contract_and_slovak_errors(self):
|
||||
def test_slovak_display_errors_without_host_calls(self):
|
||||
for file in FILES:
|
||||
old=(ROOT/'tests/fixtures/cli-before-i18n'/(Path(file).stem+'-display.sh')).read_text()
|
||||
new=(ROOT/file).read_text();before=display_calls(old);after=display_calls(new)
|
||||
self.assertEqual(len(before),len(after),file)
|
||||
self.assertEqual(render(before,'en'),render(after,'en'),file)
|
||||
translated=render(after,'sk')
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user