release: Multi-Hoster-Upload 2.1.7
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
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('English is the fallback language and German remains selectable', () => {
|
||||
assert.equal(normalizeLanguage(), 'en');
|
||||
assert.equal(normalizeLanguage('fr'), 'en');
|
||||
@@ -61,3 +69,169 @@ test('English copy uses complete actions and correct singular plurals', () => {
|
||||
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('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'],
|
||||
['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, []);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user