Merge pull request #384 from f3rs3n/fix/tui-independent-wording-combined

fix(tui): correct network, storage, upgrade, and GPU guidance
This commit is contained in:
MacRimi
2026-09-25 21:39:33 +02:00
committed by GitHub
16 changed files with 355 additions and 20 deletions
@@ -0,0 +1,145 @@
"""Offline, bounded real Bash consumers: no administrative entrypoints are sourced."""
import importlib.util
import json
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[3]
SCRIPTS = ROOT / 'scripts'
NETWORK = {
'share/iscsi_host.sh': 1,
'share/nfs_client.sh': 1,
'share/nfs_host.sh': 2,
'share/samba_client.sh': 1,
'share/samba_host.sh': 2,
}
MOUNT = 'Mounting existing filesystem ({filesystem})...'
RAID = 'If you are sure you want to use it, remove the RAID metadata or format it manually using external tools.'
def function(text, name):
start = text.index(name + '() {')
return text[start:text.index('\n}', start) + 2] + '\n'
def network_blocks(path):
lines = path.read_text().splitlines()
blocks = []
for i, line in enumerate(lines):
if re.search(r'^\s*if (?:pct exec .* -- )?nc -z -w 2 ', line):
depth = len(line) - len(line.lstrip())
end = next(j for j in range(i + 1, len(lines))
if lines[j].strip() == 'fi' and len(lines[j]) - len(lines[j].lstrip()) == depth)
block = '\n'.join(lines[i:end + 1])
assert '$(translate "Open")' in block and '$(translate "Closed")' in block or '$(translate "Probe failed")' in block
blocks.append(block)
return blocks
def run(code, translations=None, statuses='1', variables='', dialog=False):
translations = translations or {}
with tempfile.TemporaryDirectory(prefix='tui-wording-') as directory:
root = Path(directory)
(root / 'xx.json').write_text(json.dumps(translations))
body = f'''LANGUAGE=xx LANG_DIR={root!s}
{function((SCRIPTS / 'utils.sh').read_text(), 'translate')}
GN='\\033[32m' RD='\\033[31m' CL='\\033[0m'
{variables}
statuses=({statuses})
nc() {{ local code=${{statuses[0]}}; statuses=("${{statuses[@]:1}}"); return "$code"; }}
pct() {{ [[ "$1" == exec && "$3" == -- && "$4" == nc ]] || return 98; shift 3; nc "$@"; }}
msg_info() {{ printf 'INFO:%s\\n' "$1"; }}
msg_error() {{ printf 'ERROR:%s\\n' "$1"; }}
msg_ok() {{ printf 'OK:%s\\n' "$1"; }}
msg_title() {{ :; }}
show_proxmenux_logo() {{ :; }}
pmx_journal_context() {{ :; }}
blkid() {{ [[ "$2" == TYPE ]] && printf 'ext4\\n' || printf 'UUID123\\n'; }}
mkdir() {{ :; }}
mount() {{ return 1; }}
dialog() {{ printf 'DIALOG:%s\\n' "$*"; }}
{code}
'''
result = subprocess.run(['/bin/bash', '--noprofile', '--norc', '-c', body],
env={'PATH': '/usr/bin:/bin', 'LC_ALL': 'C.UTF-8'},
capture_output=True, text=True, timeout=10)
if result.returncode or result.stderr:
raise AssertionError((result.returncode, result.stderr, result.stdout))
return result.stdout
class TuiNetworkStorageWording(unittest.TestCase):
def test_failed_tcp_probe_is_not_declared_closed_at_every_caller(self):
for path, count in NETWORK.items():
blocks = network_blocks(SCRIPTS / path)
self.assertEqual(len(blocks), count, path)
for index, block in enumerate(blocks):
cases = [('0 1', 'Open'), ('1 1', 'Probe failed'), ('124 124', 'Probe failed')]
if 'samba_' in path:
cases.append(('1 0', 'Open'))
for codes, expected in cases:
with self.subTest(path=path, index=index, codes=codes):
output = run(block, statuses=codes, translations={'Probe failed': 'SONDAGGIO FALLITO'})
self.assertIn('SONDAGGIO FALLITO' if expected == 'Probe failed' else 'Open', output)
self.assertNotIn('Closed', output)
if expected == 'Open':
self.assertNotIn('SONDAGGIO FALLITO', output)
else:
fallback = run(block, statuses=codes)
self.assertIn('Probe failed', fallback)
self.assertNotIn('Closed', fallback)
def test_mount_progress_is_whole_and_uses_real_lookup_with_fallback(self):
source = (SCRIPTS / 'share/disk_host.sh').read_text()
start = source.index('mount_existing_disk() {')
portion = source[start:source.index(' if ! mount "$disk"', start)] + ' return 0\n}\n'
for translation, expected in [({}, MOUNT.replace('{filesystem}', 'ext4')),
({MOUNT: 'SYNTHETIC {filesystem} in corso'}, 'SYNTHETIC ext4 in corso'),
({MOUNT: 'SYNTHETIC malformed'}, MOUNT.replace('{filesystem}', 'ext4'))]:
with self.subTest(translation=translation):
output = run(portion + '\nmount_existing_disk /dev/fixture /mnt/fixture', translations=translation)
self.assertIn('INFO:' + expected, output)
self.assertNotIn('{filesystem}', output)
def test_raid_warning_is_complete_message_with_disk_identifier_and_fallback(self):
source = (SCRIPTS / 'storage/disk-passthrough_ct.sh').read_text().splitlines()
invocation = '\n'.join(source[457:460]) # original dialog invocation, not the raid detection command
for translation, expected in [({}, RAID), ({RAID: 'SYNTHETIC RAID metadata remains'}, 'SYNTHETIC RAID metadata remains')]:
with self.subTest(translation=translation):
output = run(invocation, translations=translation,
variables="BACKTITLE=fixture DISK_INFO='/dev/fixture 2TiB' UI_RESULT_H=16 UI_RESULT_W=80")
self.assertIn(expected, output)
self.assertIn('/dev/fixture 2TiB', output)
self.assertNotIn('remove the RAID metadata or format it manually using external tools. RAID metadata', output)
def test_shipped_catalogs_use_english_fallback_for_new_keys(self):
catalogs = sorted((ROOT / 'lang').glob('*.json'))
self.assertTrue(catalogs)
block = network_blocks(SCRIPTS / 'share/iscsi_host.sh')[0]
source = (SCRIPTS / 'share/disk_host.sh').read_text()
start = source.index('mount_existing_disk() {')
mount = source[start:source.index(' if ! mount "$disk"', start)] + ' return 0\n}\nmount_existing_disk /dev/fixture /mnt/fixture'
raid = '\n'.join((SCRIPTS / 'storage/disk-passthrough_ct.sh').read_text().splitlines()[457:460])
for path in catalogs:
with self.subTest(locale=path.name):
catalog = json.loads(path.read_text())
self.assertIn(catalog.get('Probe failed') or 'Probe failed', run(block, statuses='1', translations=catalog))
self.assertIn((catalog.get(MOUNT) or MOUNT).replace('{filesystem}', 'ext4'), run(mount, translations=catalog))
self.assertIn(catalog.get(RAID) or RAID, run(raid, translations=catalog,
variables="BACKTITLE=fixture DISK_INFO='/dev/fixture' UI_RESULT_H=16 UI_RESULT_W=80"))
def test_extractor_discovers_whole_keys(self):
spec = importlib.util.spec_from_file_location('cache_builder', ROOT / '.github/scripts/build_translation_cache.py')
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
keys = module.extract_translate_texts(SCRIPTS)
for key in (MOUNT, RAID, 'Probe failed'):
self.assertTrue(key in keys, key)
if __name__ == '__main__':
unittest.main()
@@ -0,0 +1,23 @@
"""Run the inert Node TUI consumer fixture in the existing offline i18n lane."""
import os
from pathlib import Path
import subprocess
import unittest
ROOT = Path(__file__).resolve().parents[3]
class TuiUpgradeGpuWording(unittest.TestCase):
def test_source_extracted_node_consumer(self):
fixture = ROOT / 'tests/test_tui_upgrade_gpu_wording.cjs'
self.assertTrue(fixture.is_file(), 'Node fixture must be packaged with the candidate')
node = os.environ.get('NODE_BINARY') or 'node'
result = subprocess.run([node, str(fixture)], cwd=ROOT, capture_output=True,
text=True, timeout=60, env={**os.environ, 'PYTHONDONTWRITEBYTECODE': '1'})
self.assertEqual(result.returncode, 0, result.stdout + '\n' + result.stderr)
self.assertIn('PASS: extracted upgrade/GPU consumers', result.stdout)
if __name__ == '__main__':
unittest.main()
+2 -3
View File
@@ -1459,9 +1459,8 @@ check_switch_mode() {
msg="\n$(translate 'The selected GPU is already assigned to another VM that is currently running:')\n\n"
msg+=" VM ${vm_src_id} (${vm_src_name:-VM-${vm_src_id}})\n\n"
msg+="$(translate 'The same GPU cannot be used by two VMs at the same time.')\n\n"
msg+="$(translate 'Next step: stop that VM first, then run')\n"
msg+=" Hardware Graphics → Add GPU to VM\n"
msg+="$(translate 'to move the GPU safely.')"
msg+="$(translate 'Stop that VM, then choose:')\n"
msg+=" $(translate 'Hardware: GPUs and Coral-TPU') → $(translate 'Add GPU to VM (Intel | AMD | NVIDIA)')\n"
_pmx_msgbox "$(translate 'GPU Busy in Running VM')" "$msg" 16 78
exit 0
+7 -1
View File
@@ -646,7 +646,13 @@ mount_existing_disk() {
mkdir -p "$mount_path"
msg_ok "$(translate "Mount point created")"
msg_info "$(translate "Mounting existing") $existing_fs $(translate "filesystem...")"
local mount_message
mount_message="$(translate "Mounting existing filesystem ({filesystem})...")"
if [[ "$mount_message" != *'{filesystem}'* ]]; then
mount_message="Mounting existing filesystem ({filesystem})..."
fi
mount_message="${mount_message//\{filesystem\}/$existing_fs}"
msg_info "$mount_message"
if ! mount "$disk" "$mount_path" 2>/dev/null; then
msg_error "$(translate "Failed to mount disk")"
return 1
+1 -1
View File
@@ -471,7 +471,7 @@ test_iscsi_connectivity() {
if nc -z -w 2 "$portal_host" "$portal_port" 2>/dev/null; then
echo -e " | iSCSI port $portal_port: ${GN}$(translate "Open")${CL}"
else
echo -e " | iSCSI port $portal_port: ${RD}$(translate "Closed")${CL}"
echo -e " | iSCSI port $portal_port: ${RD}$(translate "Probe failed")${CL}"
fi
else
echo -e "${RD}$(translate "Unreachable")${CL}"
+1 -1
View File
@@ -678,7 +678,7 @@ test_nfs_connectivity() {
if pct exec "$CTID" -- nc -z -w 2 "$server" 2049 2>/dev/null; then
echo -e "\033[1;92m$(translate "Open")\033[0m"
else
echo -e "\033[1;91m$(translate "Closed")\033[0m"
echo -e "\033[1;91m$(translate "Probe failed")\033[0m"
fi
# Try to list exports
+2 -2
View File
@@ -839,7 +839,7 @@ test_nfs_connectivity() {
if nc -z -w 2 "$server" 2049 2>/dev/null; then
echo -e " | NFS port 2049: ${GN}$(translate "Open")${CL}"
else
echo -e " | NFS port 2049: ${RD}$(translate "Closed")${CL}"
echo -e " | NFS port 2049: ${RD}$(translate "Probe failed")${CL}"
fi
if showmount -e "$server" >/dev/null 2>&1; then
@@ -882,7 +882,7 @@ test_nfs_connectivity() {
if nc -z -w 2 "$server" 2049 2>/dev/null; then
echo -e " | NFS port 2049: ${GN}$(translate "Open")${CL}"
else
echo -e " | NFS port 2049: ${RD}$(translate "Closed")${CL}"
echo -e " | NFS port 2049: ${RD}$(translate "Probe failed")${CL}"
fi
else
echo -e "${RD}$(translate "Unreachable")${CL}"
+1 -1
View File
@@ -1044,7 +1044,7 @@ test_samba_connectivity() {
elif pct exec "$CTID" -- nc -z -w 2 "$server" 139 2>/dev/null; then
echo " $(translate "NetBIOS port 139:"): ${GN}$(translate "Open")${CL}"
else
echo " $(translate "SMB ports:"): ${RD}$(translate "Closed")${CL}"
echo " $(translate "SMB ports:"): ${RD}$(translate "Probe failed")${CL}"
fi
+2 -2
View File
@@ -918,7 +918,7 @@ test_samba_connectivity() {
elif nc -z -w 2 "$server" 139 2>/dev/null; then
echo -e " | NetBIOS 139: ${GN}$(translate "Open")${CL}"
else
echo -e " | SMB ports: ${RD}$(translate "Closed")${CL}"
echo -e " | SMB ports: ${RD}$(translate "Probe failed")${CL}"
fi
echo -n " $(translate "Guest access test:"): "
@@ -964,7 +964,7 @@ test_samba_connectivity() {
elif nc -z -w 2 "$server" 139 2>/dev/null; then
echo -e " | NetBIOS 139: ${GN}$(translate "Open")${CL}"
else
echo -e " | SMB ports: ${RD}$(translate "Closed")${CL}"
echo -e " | SMB ports: ${RD}$(translate "Probe failed")${CL}"
fi
else
echo -e "${RD}$(translate "Unreachable")${CL}"
+1 -1
View File
@@ -457,7 +457,7 @@ for DISK in $SELECTED; do
if lsblk "$DISK" | grep -q "raid" || grep -q "${DISK##*/}" /proc/mdstat; then
dialog --backtitle "$BACKTITLE" \
--title "$(translate "RAID Detected")" \
--msgbox "\n$(translate "The disk") $DISK_INFO $(translate "appears to be part of a") RAID. $(translate "For security reasons, the system cannot format it.")\\n\\n$(translate "If you are sure you want to use it, please remove the") RAID metadata $(translate "or format it manually using external tools.")\\n\\n$(translate "After that, run this script again to add it.")" $UI_RESULT_H $UI_RESULT_W
--msgbox "\n$(translate "The disk") $DISK_INFO $(translate "appears to be part of a") RAID. $(translate "For security reasons, the system cannot format it.")\\n\\n$(translate "If you are sure you want to use it, remove the RAID metadata or format it manually using external tools.")\\n\\n$(translate "After that, run this script again to add it.")" $UI_RESULT_H $UI_RESULT_W
continue
fi
+5 -1
View File
@@ -187,7 +187,11 @@ run_pve8to9_check2() {
echo -e
done
echo -e
msg_info2 "$(translate "Once finished, re-run the script 'PVE 8 to 9 check' to verify that all issues.")"
local check_help
check_help="$(translate "Once finished, repeat the check ({check}) to review any remaining issues.")"
[[ "$check_help" == *'{check}'* ]] || check_help="Once finished, repeat the check ({check}) to review any remaining issues."
check_help="${check_help//\{check\}/$(translate "Run PVE 8 to 9 check")}"
msg_info2 "$check_help"
echo -e
msg_success "$(translate "Press Enter to exit the script after reading instructions...")"
read -r
+12 -4
View File
@@ -135,7 +135,7 @@ ask_run_mode() {
--menu "$text" 20 70 8 \
1 "$(translate "Automatic/Unattended")" \
2 "$(translate "Interactive (guided, prompts visible)")" \
3 "$(translate "Run PVE 8 to 9") check" \
3 "$(translate "Run PVE 8 to 9 check")" \
4 "$(translate "Manual upgrade guide step by step")" \
3>&1 1>&2 2>&3
); status=$?
@@ -178,7 +178,7 @@ ask_run_mode() {
--menu "$(translate "Choose how to perform the upgrade:")" 20 70 8 \
"1" "$(translate "Automatic/Unattended")" \
"2" "$(translate "Interactive (guided, prompts visible)")" \
"3" "$(translate "Run PVE 8 to 9") check" \
"3" "$(translate "Run PVE 8 to 9 check")" \
"4" "$(translate "Manual upgrade guide step by step")" \
3>&1 1>&2 2>&3
); then
@@ -777,7 +777,11 @@ run_pve8to9_check() {
echo -e
done
echo -e
msg_info2 "$(translate "Once finished, re-run 'PVE 8 to 9 check' to verify that all issues are resolved \n before executing the PVE 8 → PVE 9 upgrade.")"
local check_help
check_help="$(translate "Once finished, repeat the check ({check}) to review any remaining issues before starting the PVE 8 → PVE 9 upgrade.")"
[[ "$check_help" == *'{check}'* ]] || check_help="Once finished, repeat the check ({check}) to review any remaining issues before starting the PVE 8 → PVE 9 upgrade."
check_help="${check_help//\{check\}/$(translate "Run PVE 8 to 9 check")}"
msg_info2 "$check_help"
echo -e
msg_success "$(translate "Press Enter to exit the script after reading instructions...")"
read -r
@@ -1143,7 +1147,11 @@ run_pve8to9_check2() {
echo -e
done
echo -e
msg_info2 "$(translate "Once finished, re-run the script 'PVE 8 to 9 check' to verify that all issues are resolved \n before rebooting.")"
local check_help
check_help="$(translate "Once finished, repeat the check ({check}) to review any remaining issues before rebooting.")"
[[ "$check_help" == *'{check}'* ]] || check_help="Once finished, repeat the check ({check}) to review any remaining issues before rebooting."
check_help="${check_help//\{check\}/$(translate "Run PVE 8 to 9 check")}"
msg_info2 "$check_help"
echo -e
msg_success "$(translate "Press Enter to exit the script after reading instructions...")"
read -r
+6 -1
View File
@@ -179,7 +179,12 @@ function run_gpu_passthrough_wizard() {
fi
if [[ ! -f "$gpu_script" ]]; then
msg_warn "$(translate "GPU passthrough assistant not found. You can run it later from Hardware Graphics.")"
local gpu_notice
gpu_notice="$(translate "GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again.")"
[[ "$gpu_notice" == *'{menu}'* && "$gpu_notice" == *'{action}'* ]] || gpu_notice="GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again."
gpu_notice="${gpu_notice//\{menu\}/$(translate "Hardware: GPUs and Coral-TPU")}"
gpu_notice="${gpu_notice//\{action\}/$(translate "Add GPU to VM (Intel | AMD | NVIDIA)")}"
msg_warn "$gpu_notice"
WIZARD_GPU_RESULT="cancelled"
return 0
fi
+6 -1
View File
@@ -234,7 +234,12 @@ function run_gpu_passthrough_wizard() {
fi
if [[ ! -f "$gpu_script" ]]; then
msg_warn "$(translate "GPU passthrough assistant not found. You can run it later from Hardware Graphics.")"
local gpu_notice
gpu_notice="$(translate "GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again.")"
[[ "$gpu_notice" == *'{menu}'* && "$gpu_notice" == *'{action}'* ]] || gpu_notice="GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again."
gpu_notice="${gpu_notice//\{menu\}/$(translate "Hardware: GPUs and Coral-TPU")}"
gpu_notice="${gpu_notice//\{action\}/$(translate "Add GPU to VM (Intel | AMD | NVIDIA)")}"
msg_warn "$gpu_notice"
return 0
fi
+6 -1
View File
@@ -171,7 +171,12 @@ function run_gpu_passthrough_wizard() {
fi
if [[ ! -f "$gpu_script" ]]; then
msg_warn "$(translate "GPU passthrough assistant not found. You can run it later from Hardware Graphics.")"
local gpu_notice
gpu_notice="$(translate "GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again.")"
[[ "$gpu_notice" == *'{menu}'* && "$gpu_notice" == *'{action}'* ]] || gpu_notice="GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again."
gpu_notice="${gpu_notice//\{menu\}/$(translate "Hardware: GPUs and Coral-TPU")}"
gpu_notice="${gpu_notice//\{action\}/$(translate "Add GPU to VM (Intel | AMD | NVIDIA)")}"
msg_warn "$gpu_notice"
WIZARD_GPU_RESULT="cancelled"
return 0
fi
+135
View File
@@ -0,0 +1,135 @@
// Source-extracted, inert UI/message seams; never source administrative scripts.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const cp = require('node:child_process');
const root = path.resolve(__dirname, '..');
const read = p => fs.readFileSync(path.join(root, p), 'utf8');
const bash = (code, env = {}) => {
const r = cp.spawnSync('bash', ['-c', code], {encoding: 'utf8', env: {...process.env, ...env}});
assert.equal(r.status, 0, `bash fixture failed: ${r.stderr}`);
return r.stdout.trim();
};
const func = (source, name) => {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const m = source.match(new RegExp(`(?:^|\\n)(?:function )?${escaped}\\(\\) \\{[\\s\\S]*?\\n\\}`, 'm'));
assert.ok(m, `function ${name} present`);
return m[0];
};
const lookup = func(read('scripts/utils.sh'), 'translate');
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'pmx-tui-wording-'));
const it = JSON.parse(read('lang/it.json'));
const newKeys = [
'Once finished, repeat the check ({check}) to review any remaining issues.',
'Once finished, repeat the check ({check}) to review any remaining issues before starting the PVE 8 → PVE 9 upgrade.',
'Once finished, repeat the check ({check}) to review any remaining issues before rebooting.',
'GPU passthrough assistant not found. Later, open {menu} → {action} from the main menu to try again.',
];
const marker = Object.fromEntries(newKeys.map((k, i) => [k, i === 3
? 'TRADOTTO_3 {action} {menu}' : `TRADOTTO_${i} {check} fine`]));
const setup = `LANG_DIR=${JSON.stringify(temp)}; LANGUAGE="${'${LANGUAGE:-en}'}"; ${lookup}\nmsg_warn() { printf '%s\\n' "$1"; }; msg_info2() { printf '%s\\n' "$1"; }; msg_title() { printf '%s\\n' "$1"; };`;
const store = (locale, entries) => fs.writeFileSync(path.join(temp, `${locale}.json`), JSON.stringify(entries));
store('it', it); store('synthetic', {
...marker,
'Run PVE 8 to 9 check': 'CONTROLLO_LOCALIZZATO',
'Hardware: GPUs and Coral-TPU': 'HARDWARE_LOCALIZZATO',
'Add GPU to VM (Intel | AMD | NVIDIA)': 'AZIONE_LOCALIZZATA',
'Stop that VM, then choose:': 'SCELTA_LOCALIZZATA:',
});
const menuSrc = read('scripts/utilities/upgrade_pve8_to_pve9.sh');
const menuLines = menuSrc.split('\n').filter(l => /(?:^|\s)(?:3|"3") "\$\(translate .*PVE 8 to 9/.test(l));
assert.equal(menuLines.length, 2, 'both actual menu consumers');
const renderMenu = locale => menuLines.map(l => bash(setup + '\nprintf \'%s\\n\' ' + l.trim().replace(/\\$/, '').replace(/^(?:3|"3") /, '') + '\n', {LANGUAGE: locale}));
assert.deepEqual(renderMenu('en'), ['Run PVE 8 to 9 check', 'Run PVE 8 to 9 check']);
assert.deepEqual(renderMenu('it'), [it['Run PVE 8 to 9 check'], it['Run PVE 8 to 9 check']]);
assert.deepEqual(renderMenu('synthetic'), ['CONTROLLO_LOCALIZZATO', 'CONTROLLO_LOCALIZZATO']);
const locales = ['es', 'fr', 'de', 'it', 'pt', 'sk', 'sv'];
for (const locale of locales) {
const catalog = JSON.parse(read(`lang/${locale}.json`));
store(locale, catalog);
const label = catalog['Run PVE 8 to 9 check'] || 'Run PVE 8 to 9 check';
assert.deepEqual(renderMenu(locale), [label, label], `${locale}: both menus`);
}
const checker = read('scripts/utilities/pve8to9_check.sh');
const title = checker.split('\n').find(l => /^msg_title "\$\(translate "Run PVE 8 to 9 check"\)"$/.test(l));
assert.ok(title);
const checkHelp = checker.match(/^\s*check_help="\$\(translate "Once finished,[^\n]+\n(?:[^\n]*\n){2}\s*msg_info2 "\$check_help"/m)?.[0];
const upgradeHelp = [...menuSrc.matchAll(/^\s*check_help="\$\(translate "Once finished,[^\n]+\n(?:[^\n]*\n){2}\s*msg_info2 "\$check_help"/gm)].map(m => m[0]);
assert.equal(upgradeHelp.length, 2);
const helpLines = [checkHelp, ...upgradeHelp];
const renderHelp = (locale, entries) => {
if (entries) store(locale, entries);
return helpLines.map(l => bash(setup + '\n' + l.trim() + '\n', {LANGUAGE: locale}));
};
const helpEn = renderHelp('en');
assert.equal(helpEn.length, 3);
for (const line of helpEn) {
assert.ok(line.includes('Run PVE 8 to 9 check'), line);
assert.ok(line.includes('repeat the check (Run PVE 8 to 9 check) to review any remaining issues'), line);
assert.ok(!line.includes('{check}') && !line.includes('all issues are resolved') && !line.includes('run Run'), line);
}
const helpIt = renderHelp('it'); // New keys absent from shipped Italian cache: complete English fallback.
for (const line of helpIt) assert.ok(line.includes(it['Run PVE 8 to 9 check']), line);
for (const locale of locales) {
const catalog = JSON.parse(read(`lang/${locale}.json`));
store(locale, catalog);
const label = catalog['Run PVE 8 to 9 check'] || 'Run PVE 8 to 9 check';
for (const line of renderHelp(locale)) assert.ok(line.includes(`(${label})`) && !line.includes('{check}'), `${locale}: ${line}`);
}
const helpSynthetic = renderHelp('synthetic');
for (const [i, line] of helpSynthetic.entries()) assert.equal(line, `TRADOTTO_${i} CONTROLLO_LOCALIZZATO fine`);
const brokenHelp = renderHelp('malformed', {...marker, 'Run PVE 8 to 9 check': 'CONTROLLO_LOCALIZZATO', [newKeys[0]]: 'NON_VALIDA {wrong}'});
assert.ok(brokenHelp[0].includes('Once finished, repeat the check (CONTROLLO_LOCALIZZATO)'), brokenHelp[0]);
const vmPaths = ['scripts/vm/synology.sh', 'scripts/vm/vm_creator.sh', 'scripts/vm/zimaos.sh'];
const vmFunctions = vmPaths.map(p => func(read(p), 'run_gpu_passthrough_wizard'));
const renderVm = locale => vmFunctions.map(fn => bash(setup + `\nLOCAL_SCRIPTS=${JSON.stringify(path.join(temp, 'absent'))}; WIZARD_ADD_GPU=yes; ` + fn + '\nrun_gpu_passthrough_wizard\n', {LANGUAGE: locale}));
for (const line of renderVm('en')) {
assert.ok(line.includes('Hardware: GPUs and Coral-TPU → Add GPU to VM (Intel | AMD | NVIDIA)'), line);
assert.ok(!line.includes('Hardware Graphics') && !line.includes('{menu}') && !line.includes('{action}'), line);
}
for (const line of renderVm('it')) {
assert.ok(line.includes(`${it['Hardware: GPUs and Coral-TPU']} → ${it['Add GPU to VM (Intel | AMD | NVIDIA)']}`), line);
assert.ok(!line.includes('Hardware Graphics'), line);
}
for (const locale of locales) {
const catalog = JSON.parse(read(`lang/${locale}.json`));
const menu = catalog['Hardware: GPUs and Coral-TPU'] || 'Hardware: GPUs and Coral-TPU';
const action = catalog['Add GPU to VM (Intel | AMD | NVIDIA)'] || 'Add GPU to VM (Intel | AMD | NVIDIA)';
for (const line of renderVm(locale)) assert.ok(line.includes(`${menu} → ${action}`) && !line.includes('{menu}') && !line.includes('{action}'), `${locale}: ${line}`);
}
for (const line of renderVm('synthetic')) assert.ok(line.includes('AZIONE_LOCALIZZATA HARDWARE_LOCALIZZATO'), line);
store('malformed', {...marker, 'Hardware: GPUs and Coral-TPU': 'MENU', 'Add GPU to VM (Intel | AMD | NVIDIA)': 'ACTION', [newKeys[3]]: 'BROKEN {menu} {wrong}'});
for (const line of renderVm('malformed')) assert.ok(line.startsWith('GPU passthrough assistant not found.') && line.includes('MENU → ACTION'), line);
store('malformed', {...marker, 'Hardware: GPUs and Coral-TPU': 'MENU', 'Add GPU to VM (Intel | AMD | NVIDIA)': 'ACTION', [newKeys[3]]: 'BROKEN {action} {wrong}'});
for (const line of renderVm('malformed')) assert.ok(line.startsWith('GPU passthrough assistant not found.') && line.includes('MENU → ACTION'), line);
store('malformed', {...marker, 'Hardware: GPUs and Coral-TPU': 'MENU', 'Add GPU to VM (Intel | AMD | NVIDIA)': 'ACTION', [newKeys[3]]: 'BROKEN {wrong}'});
for (const line of renderVm('malformed')) assert.ok(line.startsWith('GPU passthrough assistant not found.') && line.includes('MENU → ACTION'), line);
const additionalLead = read('scripts/gpu_tpu/add_gpu_vm.sh').split('\n').find(l => l.includes('Stop that VM, then choose:') && l.includes('msg+='));
assert.ok(additionalLead, 'busy-VM guidance has a standalone instruction before the rendered labels');
assert.equal(bash(setup + '\n' + additionalLead.trim() + '\nprintf \'%b\\n\' "$msg"', {LANGUAGE: 'synthetic'}), 'SCELTA_LOCALIZZATA:');
assert.ok(!read('scripts/gpu_tpu/add_gpu_vm.sh').includes("msg+=\"$(translate 'to move the GPU safely.')\""), 'no clipped tail follows the route');
const additionalNav = read('scripts/gpu_tpu/add_gpu_vm.sh').split('\n').find(l => l.includes('→') && l.includes('Add GPU to VM') && l.includes('msg+='));
assert.ok(additionalNav?.includes("$(translate 'Hardware: GPUs and Coral-TPU')"), 'busy-VM guidance uses actual menu label');
assert.ok(additionalNav?.includes("$(translate 'Add GPU to VM (Intel | AMD | NVIDIA)')"), 'busy-VM guidance uses actual action label');
for (const locale of ['en', 'it', 'synthetic']) {
const label = locale === 'en' ? 'Hardware: GPUs and Coral-TPU' : locale === 'it' ? it['Hardware: GPUs and Coral-TPU'] : 'HARDWARE_LOCALIZZATO';
const action = locale === 'en' ? 'Add GPU to VM (Intel | AMD | NVIDIA)' : locale === 'it' ? it['Add GPU to VM (Intel | AMD | NVIDIA)'] : 'AZIONE_LOCALIZZATA';
const rendered = bash(setup + `\nmsg=''; ${additionalNav.trim()}; printf '%b\\n' "$msg"`, {LANGUAGE: locale});
assert.equal(rendered, `${label} → ${action}`);
}
// Actual checker disables localization before both title and help; no behavior changed here.
const bypass = func(checker, 'disable_translation_post_upgrade');
assert.equal(bash(setup + `\n${bypass}\ndisable_translation_post_upgrade\n` + title), 'Run PVE 8 to 9 check');
assert.equal(bash(setup + `\n${bypass}\ndisable_translation_post_upgrade\n` + checkHelp), helpEn[0]);
const extraction = cp.spawnSync('python3', ['-c', `import importlib.util,pathlib
p=pathlib.Path('.github/scripts/build_translation_cache.py'); s=importlib.util.spec_from_file_location('cache',p); m=importlib.util.module_from_spec(s); s.loader.exec_module(m)
keys=set(m.extract_translate_texts(pathlib.Path('scripts')))
required=${JSON.stringify(['Run PVE 8 to 9 check', ...newKeys, 'Hardware: GPUs and Coral-TPU', 'Add GPU to VM (Intel | AMD | NVIDIA)'])}
assert set(required)<=keys, set(required)-keys
print('EXTRACTED',len(required))`], {cwd: root, encoding: 'utf8'});
assert.equal(extraction.status, 0, extraction.stderr);
// No source catalog edits; synthetic translation and malformed placeholders only live in scratch.
assert.ok(!Object.keys(it).some(k => newKeys.includes(k)));
fs.rmSync(temp, {recursive: true, force: true});
console.log('PASS: extracted upgrade/GPU consumers, seven caches/fallbacks, synthetic and malformed placeholders, real extractor');