fix(i18n): address Monitor and CLI review feedback

This commit is contained in:
VAIO73
2026-10-22 04:49:16 +00:00
parent 61168b9abd
commit dbcc4de6e1
11 changed files with 121 additions and 411 deletions
+5 -4
View File
@@ -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
View File
@@ -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...")"
+41
View File
@@ -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'}}))
+14 -5
View File
@@ -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')
+22 -6
View File
@@ -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)