Remove the dedicated completion toast and failure action after user review while leaving all other update and queue improvements intact.
256 lines
15 KiB
JavaScript
256 lines
15 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const espree = require('espree');
|
|
|
|
const { normalizeLanguage, translateText } = require('../renderer/i18n');
|
|
|
|
test('translates permanent source deletion controls to English', () => {
|
|
assert.equal(translateText('Quelldatei nach vollständigem Upload dauerhaft löschen', 'en'), 'Permanently delete source file after complete upload');
|
|
assert.equal(translateText('Dauerhaftes Löschen aktivieren', 'en'), 'Enable permanent deletion');
|
|
});
|
|
|
|
test('translates the remaining upload size label', () => {
|
|
assert.equal(translateText('Verbleibende Größe', 'en'), 'Remaining size');
|
|
assert.equal(translateText('Remaining size', 'de'), 'Verbleibende Größe');
|
|
});
|
|
|
|
test('English is the fallback language and German remains selectable', () => {
|
|
assert.equal(normalizeLanguage(), 'en');
|
|
assert.equal(normalizeLanguage('fr'), 'en');
|
|
assert.equal(normalizeLanguage('en'), 'en');
|
|
assert.equal(normalizeLanguage('de'), 'de');
|
|
});
|
|
|
|
test('translations cover static labels and interpolated status text in both languages', () => {
|
|
assert.equal(translateText('Einstellungen', 'en'), 'Settings');
|
|
assert.equal(translateText('Update v2.1.0 verfügbar', 'en'), 'Update v2.1.0 available');
|
|
assert.equal(translateText('Settings', 'de'), 'Einstellungen');
|
|
assert.equal(translateText('Update v2.1.0 available', 'de'), 'Update v2.1.0 verfügbar');
|
|
assert.equal(translateText('Alle Status', 'en'), 'Any status');
|
|
assert.equal(translateText('Any status', 'de'), 'Alle Status');
|
|
assert.equal(translateText('Filter', 'en'), 'Filters');
|
|
});
|
|
|
|
test('sidebar hierarchy uses distinct English and German kicker labels', () => {
|
|
assert.equal(translateText('Arbeitsbereich', 'en'), 'Workspace');
|
|
assert.equal(translateText('Accounts verwalten', 'en'), 'Manage accounts');
|
|
assert.equal(translateText('Archiv', 'en'), 'Archive');
|
|
assert.equal(translateText('Workspace', 'de'), 'Arbeitsbereich');
|
|
assert.equal(translateText('Manage accounts', 'de'), 'Accounts verwalten');
|
|
assert.equal(translateText('Archive', 'de'), 'Archiv');
|
|
});
|
|
|
|
test('runtime queue, account, toast, and shutdown copy translates completely', () => {
|
|
const cases = [
|
|
['Wartet', 'Waiting'],
|
|
['Abgebrochen', 'Canceled'],
|
|
['Fehlgeschlagen: Verbindung verloren', 'Failed: Connection lost'],
|
|
['Retry 2/3 · Primär nicht verfügbar', 'Retry 2/3 · Primary unavailable'],
|
|
['Link kopiert', 'Link copied'],
|
|
['Kopiert!', 'Copied!'],
|
|
['Sende…', 'Sending…'],
|
|
['Lade…', 'Loading…'],
|
|
['Upload-Statistiken', 'Upload statistics'],
|
|
['Ruhezustand in 30s...', 'Sleep in 30s...'],
|
|
['Herunterfahren in 30s...', 'Shut down in 30s...']
|
|
];
|
|
|
|
for (const [german, english] of cases) {
|
|
assert.equal(translateText(german, 'en'), english);
|
|
assert.equal(translateText(english, 'de'), german);
|
|
}
|
|
});
|
|
|
|
test('English copy uses complete actions and correct singular plurals', () => {
|
|
assert.equal(translateText('Fehlgeschlagene erneut', 'en'), 'Retry failed uploads');
|
|
assert.equal(translateText('Suche Aktualisierungen', 'en'), 'Check for updates');
|
|
assert.equal(translateText('1 Job zum erneuten Upload zurückgesetzt', 'en'), '1 job reset for upload');
|
|
assert.equal(translateText('2 Jobs zum erneuten Upload zurückgesetzt', 'en'), '2 jobs reset for upload');
|
|
assert.equal(translateText('1 Verlaufseintrag wird dauerhaft entfernt.', 'en'), '1 history entry will be permanently removed.');
|
|
assert.equal(translateText('2 Verlaufseinträge werden dauerhaft entfernt.', 'en'), '2 history entries will be permanently removed.');
|
|
assert.equal(translateText('Wiederholungen', 'en'), 'Retries');
|
|
assert.equal(translateText('Maximale Geschwindigkeit (MB/s)', 'en'), 'Maximum speed (MB/s)');
|
|
assert.equal(translateText('Neustart unter (kB/s)', 'en'), 'Restart below (kB/s)');
|
|
assert.equal(translateText('Maximale Größe (MB)', 'en'), 'Maximum size (MB)');
|
|
assert.equal(translateText('Aktiv auf Port 9100 — 1 Client verbunden', 'en'), 'Active on port 9100 — 1 client connected');
|
|
assert.equal(translateText('Aktiv auf Port 9100 — 2 Clients verbunden', 'en'), 'Active on port 9100 — 2 clients connected');
|
|
});
|
|
|
|
test('translates duplicate desktop drop feedback to English', () => {
|
|
assert.equal(translateText('Auswahl ist bereits in den Upload-Aufträgen.', 'en'), 'The selection is already in the upload jobs.');
|
|
});
|
|
|
|
test('rare account, backup, update, and confirmation states translate in both directions', () => {
|
|
const cases = [
|
|
['Einstellungen konnten vor dem Update nicht gespeichert werden', 'Settings could not be saved before the update'],
|
|
['Das Update wurde nicht gestartet, weil die Einstellungen vor dem Beenden nicht gespeichert werden konnten', 'The update was not started because the settings could not be saved before quitting'],
|
|
['Login ok, Upload-Seite bereit', 'Login successful, upload page ready'],
|
|
['Login oder API Key fehlt', 'Login or API key is missing'],
|
|
['Account-Check lieferte kein gültiges JSON', 'Account check did not return valid JSON'],
|
|
['Account-Check fehlgeschlagen', 'Account check failed'],
|
|
['Upload-Server-Check lieferte kein gültiges JSON', 'Upload server check did not return valid JSON'],
|
|
['API Key gültig, Upload-Server verfügbar', 'API key is valid, upload server available'],
|
|
['API Key gültig, aktuell kein Server von API (Uploader nutzt Fallback)', 'API key is valid, but the API currently provides no server (the uploader uses a fallback)'],
|
|
['API Key gültig, Upload-Server aktuell nicht geliefert', 'API key is valid, but the API currently provides no upload server'],
|
|
['Username oder Passwort fehlt', 'Username or password is missing'],
|
|
['Upload-URL wurde nicht erkannt', 'Upload URL was not recognized'],
|
|
['API Key gültig, aktuell kein Server verfügbar', 'API key is valid, but no server is currently available'],
|
|
['API Key ungültig oder Server nicht erreichbar', 'API key is invalid or the server is unreachable'],
|
|
['Login ok, aber Upload-Seite liefert kein CSRF-Token', 'Login successful, but the upload page did not provide a CSRF token'],
|
|
['API Key fehlt', 'API key is missing'],
|
|
['API lieferte kein gültiges JSON', 'API did not return valid JSON'],
|
|
['API Key gültig', 'API key is valid'],
|
|
['Clouddrop Auth fehlgeschlagen', 'Clouddrop authentication failed'],
|
|
['Account-ID fehlt im Check-Payload', 'Account ID is missing from the check payload'],
|
|
['Kein Health-Check für diesen Hoster', 'No health check is available for this host'],
|
|
['Health-Check fehlgeschlagen', 'Health check failed'],
|
|
['Hoster fehlt', 'Host is missing'],
|
|
['Validierung fehlgeschlagen', 'Validation failed'],
|
|
['Ein Upload wird bereits ausgeführt oder abgeschlossen', 'An upload is already running or completed'],
|
|
['Kein gültiger Account für diesen Hoster', 'No valid account is available for this host'],
|
|
['Keine gültigen Zugangsdaten für die gewählten Hoster.', 'No valid credentials are available for the selected hosts.'],
|
|
['Unbekannter Fehler', 'Unknown error'],
|
|
['Kein Log-Pfad gefunden', 'No log path was found'],
|
|
['Ungültige URL (muss mit http(s):// beginnen)', 'Invalid URL (must start with http(s)://)'],
|
|
['Backup-Datei ist zu groß oder ungültig', 'The backup file is too large or invalid'],
|
|
['Online-Sicherungsschlüssel ist ungültig', 'The online backup key is invalid'],
|
|
['Ein Update wird bereits vorbereitet', 'An update is already being prepared'],
|
|
['Die Anwendung ist noch nicht bereit, das Update sicher zu installieren', 'The application is not yet ready to install the update safely'],
|
|
['Update abgebrochen', 'Update canceled'],
|
|
['Filter zurücksetzen', 'Reset filters'],
|
|
['Download abbrechen', 'Cancel download'],
|
|
['Abbrechen…', 'Cancelling…'],
|
|
['Download abgebrochen', 'Download canceled'],
|
|
['Wiederholen', 'Retry'],
|
|
['Update konnte nicht abgebrochen werden', 'The update could not be canceled'],
|
|
['Ausgewählte Einträge entfernen?', 'Remove selected entries?'],
|
|
['Export fehlgeschlagen', 'Export failed'],
|
|
['Backup exportiert', 'Backup exported'],
|
|
['Online-Backup importiert', 'Online backup imported'],
|
|
['Passwort', 'Password'],
|
|
['Backup importiert', 'Backup imported'],
|
|
['Import fehlgeschlagen', 'Import failed'],
|
|
['Upload-Start fehlgeschlagen', 'Failed to start upload'],
|
|
['erneut versuchbar', 'retryable'],
|
|
['manuell', 'manual'],
|
|
['Abgebrochen.', 'Canceled.'],
|
|
['API-Token neu erzeugen?', 'Generate a new API token?'],
|
|
['Der bisherige Token wird sofort ungültig. Verbundene Clients müssen den neuen Token verwenden.', 'The current token will become invalid immediately. Connected clients must use the new token.'],
|
|
['Neu erzeugen', 'Generate new'],
|
|
['Verbindungs-Code neu erzeugen?', 'Generate a new connection code?'],
|
|
['Der bisherige Diagnose-Code wird sofort ungültig.', 'The current diagnostics code will become invalid immediately.'],
|
|
['Aktivieren', 'Enable'],
|
|
['Bitte den OTP-Code eingeben.', 'Enter the OTP code.'],
|
|
['Login fehlgeschlagen', 'Login failed'],
|
|
['Aufbewahrung ändern?', 'Change retention?'],
|
|
['Update fehlgeschlagen', 'Update failed']
|
|
];
|
|
|
|
for (const [german, english] of cases) {
|
|
assert.equal(translateText(german, 'en'), english, german);
|
|
assert.equal(translateText(english, 'de'), german, english);
|
|
}
|
|
});
|
|
|
|
test('interpolated rare errors translate without leaking German copy', () => {
|
|
const cases = [
|
|
['Login ok, Upload-Form bereit (Dateifeld: file)', 'Login successful, upload form ready (file field: file)'],
|
|
['Klartext-Backup ist kein gültiges JSON: Unexpected token', 'Plain JSON backup is not valid JSON: Unexpected token'],
|
|
['Export fehlgeschlagen: Zugriff verweigert', 'Export failed: Zugriff verweigert'],
|
|
['Import fehlgeschlagen: Datei beschädigt', 'Import failed: Datei beschädigt'],
|
|
['Initialisierung fehlgeschlagen: Konfiguration fehlt', 'Initialization failed: Konfiguration fehlt']
|
|
];
|
|
|
|
for (const [german, english] of cases) assert.equal(translateText(german, 'en'), english, german);
|
|
});
|
|
|
|
test('main-process user-facing copy contains no mojibake', () => {
|
|
const source = fs.readFileSync(path.join(__dirname, '..', 'main.js'), 'utf8');
|
|
assert.doesNotMatch(source, /Ã|Â|â€/u);
|
|
});
|
|
|
|
test('library and updater errors exposed in the UI translate to English', () => {
|
|
const cases = [
|
|
['Ungültiges Backup-Format', 'Invalid backup format'],
|
|
['Keine gültige .mhu Backup-Datei', 'Not a valid .mhu backup file'],
|
|
['Falsches Passwort oder beschädigte Datei', 'Incorrect password or damaged file'],
|
|
['Online-Sicherungsdienst ist nicht erreichbar', 'The online backup service is unavailable'],
|
|
['Online-Sicherung wurde nicht gefunden', 'Online backup was not found'],
|
|
['Online-Sicherung konnte nicht gelöscht werden', 'Online backup could not be deleted'],
|
|
['Kein Ordnerpfad angegeben', 'No folder path was provided'],
|
|
['Kein Update verfügbar', 'No update available'],
|
|
['Update-Asset unvollständig (URL oder Name fehlt)', 'The update asset is incomplete (URL or name is missing)'],
|
|
['Heruntergeladene Datei ist keine gültige EXE', 'The downloaded file is not a valid EXE'],
|
|
['SHA-512 Prüfung fehlgeschlagen', 'SHA-512 verification failed'],
|
|
['Datei nicht gefunden', 'File not found'],
|
|
['Netzwerkfehler', 'Network error']
|
|
];
|
|
|
|
for (const [german, english] of cases) {
|
|
assert.equal(translateText(german, 'en'), english, german);
|
|
assert.equal(translateText(english, 'de'), german, english);
|
|
}
|
|
});
|
|
|
|
test('interpolated host and updater errors translate their user-facing copy', () => {
|
|
const cases = [
|
|
['Clouddrop: Datei nicht lesbar: video.mkv', 'Clouddrop: File cannot be read: video.mkv'],
|
|
['Doodstream: konnte Upload-Server nicht ermitteln (Endpoint geändert?). Details', 'Doodstream: Could not determine the upload server (endpoint changed?). Details'],
|
|
['Doodstream Upload fehlgeschlagen: rejected', 'Doodstream upload failed: rejected'],
|
|
['Upload zu VOE wurde vom Server abgelehnt.', 'The upload to VOE was rejected by the server.'],
|
|
['Vidmoly Upload abgelehnt: rejected', 'Vidmoly upload rejected: rejected'],
|
|
['VOE Upload-Fehler: rejected', 'VOE upload error: rejected'],
|
|
['Download fehlgeschlagen: HTTP 503', 'Download failed: HTTP 503']
|
|
];
|
|
|
|
for (const [german, english] of cases) assert.equal(translateText(german, 'en'), english, german);
|
|
});
|
|
|
|
test('German runtime error copy in the application source has an English translation', () => {
|
|
const files = [
|
|
path.join(__dirname, '..', 'main.js'),
|
|
path.join(__dirname, '..', 'renderer', 'app.js'),
|
|
...fs.readdirSync(path.join(__dirname, '..', 'lib'))
|
|
.filter(file => file.endsWith('.js'))
|
|
.map(file => path.join(__dirname, '..', 'lib', file))
|
|
];
|
|
const germanCopy = /[äöüß]|\b(?:der|die|das|ein|eine|kein|keine|nicht|fehlgeschlagen|ungültig|beschädigt|wurde|werden|konnte|können|verfügbar|gefunden|erhalten|abgelehnt|Antwort|Datei|Einstellungen|Upload-Server|Prüfung|Passwort|Session|Verlauf|Sicherung|Fehler)\b/i;
|
|
const candidates = [];
|
|
|
|
const addCandidate = (file, node) => {
|
|
if (node?.type === 'Literal' && typeof node.value === 'string') candidates.push([file, node.loc.start.line, node.value]);
|
|
if (node?.type === 'TemplateLiteral') {
|
|
if (node.expressions.some(expression => expression.type === 'ConditionalExpression')) return;
|
|
let sample = '';
|
|
node.quasis.forEach((part, index) => {
|
|
sample += part.value.cooked;
|
|
if (index < node.expressions.length) sample += 'X';
|
|
});
|
|
candidates.push([file, node.loc.start.line, sample]);
|
|
}
|
|
};
|
|
|
|
const walk = (file, node) => {
|
|
if (!node || typeof node !== 'object') return;
|
|
if (node.type === 'NewExpression' && ['Error', 'AggregateError'].includes(node.callee?.name)) addCandidate(file, node.arguments?.[0]);
|
|
if (node.type === 'Property' && ['error', 'message', 'reason'].includes(node.key?.name || node.key?.value)) addCandidate(file, node.value);
|
|
for (const value of Object.values(node)) {
|
|
if (Array.isArray(value)) value.forEach(child => walk(file, child));
|
|
else if (value && typeof value === 'object' && value.type) walk(file, value);
|
|
}
|
|
};
|
|
|
|
for (const file of files) {
|
|
const source = fs.readFileSync(file, 'utf8');
|
|
const tree = espree.parse(source, { ecmaVersion: 'latest', sourceType: 'script', loc: true });
|
|
walk(path.relative(path.join(__dirname, '..'), file), tree);
|
|
}
|
|
|
|
const untranslated = candidates
|
|
.filter(([, , value]) => germanCopy.test(value) && translateText(value, 'en') === value)
|
|
.map(([file, line, value]) => `${file}:${line} ${value}`);
|
|
assert.deepEqual(untranslated, []);
|
|
});
|