Files
Multi-Hoster-Upload/tests/i18n.test.js
T

242 lines
14 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('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');
});
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'],
['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, []);
});