The oracle BrowserWindowRun uses Java's browser and web server windows as a person does; the room keeps a browser window per computer and sends a program window's view, the start names the person's language. The editor shows the page in a sandboxed frame, links and forms go to the room, the web server has its button, virtual hosts and lines. Better than Java: relative images and links count from the page's folder, a mark stays in the page, the error page from Java's template, the virtual hosts' file with complete rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
234 lines
12 KiB
TypeScript
234 lines
12 KiB
TypeScript
// The catalogs: the same keys in both languages, placeholders filled, and —
|
|
// where Java FILIUS names a thing — Java's word in its bundle (M11).
|
|
|
|
import { existsSync, readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { CATALOGS, translate } from '../src/i18n/i18n.js';
|
|
|
|
const bundles = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/filius/messages');
|
|
|
|
/** A Java .properties file: ISO-8859-1, \uXXXX escapes, `key = value`, a backslash at a line's end going on in the next without its indent. */
|
|
function properties(file: string): Map<string, string> {
|
|
const text = new TextDecoder('latin1').decode(readFileSync(join(bundles, file))).replace(/\\\r?\n[ \t]*/g, '');
|
|
const entries = new Map<string, string>();
|
|
for (const line of text.split(/\r?\n/)) {
|
|
const match = /^([\w.]+)\s*=\s*(.*)$/.exec(line);
|
|
if (match !== null) entries.set(match[1]!, match[2]!.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) => String.fromCharCode(Number.parseInt(hex, 16))).trim());
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
/** The editor's keys whose words are Java's, and the key of each in Java's bundle. */
|
|
const FROM_JAVA: Readonly<Record<string, string>> = {
|
|
'device.pc': 'hw_rechner_msg1',
|
|
'device.notebook': 'hw_notebook_msg1',
|
|
'device.networkSwitch': 'hw_switch_msg1',
|
|
'device.router': 'hw_vermittlungsrechner_msg1',
|
|
'device.gateway': 'hw_gateway_msg1',
|
|
'device.cable': 'hw_kabel_msg1',
|
|
'menu.removeCable': 'guievents_msg5',
|
|
'menu.removeAllCables': 'guievents_msg6',
|
|
'menu.delete': 'guievents_msg7',
|
|
'cable.noFreePort': 'guievents_msg1',
|
|
'cable.alreadyConnected': 'guievents_msg12',
|
|
'cable.wanOccupied': 'guievents_msg22',
|
|
'cable.lanOccupied': 'guievents_msg23',
|
|
'cable.wan': 'guievents_msg24',
|
|
'cable.lan': 'guievents_msg25',
|
|
'cable.wirelessNotPossible': 'guievents_msg26',
|
|
'router.title': 'guicontainer_msg2',
|
|
'sheet.name': 'jhostkonfiguration_msg1',
|
|
'sheet.ipAddress': 'jhostkonfiguration_msg3',
|
|
'sheet.subnetMask': 'jhostkonfiguration_msg4',
|
|
'sheet.defaultGateway': 'jhostkonfiguration_msg5',
|
|
'sheet.dnsServer': 'jhostkonfiguration_msg6',
|
|
'sheet.dhcpClient': 'jhostkonfiguration_msg7',
|
|
'sheet.macAddress': 'jhostkonfiguration_msg9',
|
|
'sheet.ipAsName': 'jhostkonfiguration_msg10',
|
|
'sheet.ipForwarding': 'jhostkonfiguration_msg11',
|
|
'sheet.macAsName': 'jhostkonfiguration_msg12',
|
|
'sheet.wireless': 'jhostkonfiguration_msg13',
|
|
'sheet.cable': 'jhostkonfiguration_msg14',
|
|
'sheet.chooseNetwork': 'jhostkonfiguration_msg16',
|
|
'sheet.cloud': 'jswitchkonfiguration_msg3',
|
|
'sheet.switchSsid': 'jswitchkonfiguration_msg4',
|
|
'sheet.retention': 'jswitchkonfiguration_msg5',
|
|
'sheet.port': 'jmodemkonfiguration_msg4',
|
|
'sheet.general': 'jvermittlungsrechnerkonfiguration_msg17',
|
|
'sheet.notConnected': 'jvermittlungsrechnerkonfiguration_msg16',
|
|
'sheet.rip': 'jvermittlungsrechnerkonfiguration_msg26',
|
|
'sheet.forwardingTable': 'jvermittlungsrechnerkonfiguration_msg15',
|
|
'sheet.showAllEntries': 'jvermittlungsrechnerkonfiguration_msg11',
|
|
'sheet.newEntry': 'jvermittlungsrechnerkonfiguration_msg12',
|
|
'route.destinationNetwork': 'jweiterleitungstabelle_msg3',
|
|
'route.subnetMask': 'jweiterleitungstabelle_msg4',
|
|
'route.gateway': 'jweiterleitungstabelle_msg5',
|
|
'route.interfaceIPAddress': 'jweiterleitungstabelle_msg6',
|
|
'sheet.dhcpServer': 'jhostkonfiguration_msg8',
|
|
'sheet.portForwarding': 'jgatewayconfiguration_msg19',
|
|
'dhcp.lowerBound': 'jdhcpkonfiguration_msg1',
|
|
'dhcp.upperBound': 'jdhcpkonfiguration_msg2',
|
|
'dhcp.subnetMask': 'jdhcpkonfiguration_msg3',
|
|
'dhcp.gateway': 'jdhcpkonfiguration_msg4',
|
|
'dhcp.dnsServer': 'jdhcpkonfiguration_msg5',
|
|
'dhcp.active': 'jdhcpkonfiguration_msg6',
|
|
'dhcp.ownSettings': 'jdhcpkonfiguration_msg8',
|
|
'dhcp.basic': 'jdhcpkonfiguration_msg10',
|
|
'dhcp.static': 'jdhcpkonfiguration_msg11',
|
|
'dhcp.macAddress': 'jdhcpkonfiguration_msg12',
|
|
'dhcp.ipAddress': 'jdhcpkonfiguration_msg13',
|
|
'dhcp.add': 'jdhcpkonfiguration_msg14',
|
|
'dhcp.remove': 'jdhcpkonfiguration_msg15',
|
|
'forwarding.protocolValue': 'jportforwarding_msg4',
|
|
'forwarding.publicPortValue': 'jportforwarding_msg5',
|
|
'forwarding.lanIPAddress': 'jportforwarding_msg6',
|
|
'forwarding.lanPortValue': 'jportforwarding_msg7',
|
|
'sheet.firewall': 'jvermittlungsrechnerkonfiguration_msg4',
|
|
'firewall.rules': 'jfirewalldialog_msg21',
|
|
'firewall.up': 'jfirewalldialog_msg22',
|
|
'firewall.down': 'jfirewalldialog_msg23',
|
|
'firewall.add': 'jfirewalldialog_msg24',
|
|
'firewall.delete': 'jfirewalldialog_msg25',
|
|
'firewall.number': 'jfirewalldialog_msg26',
|
|
'firewall.sourceIPAddress': 'jfirewalldialog_msg27',
|
|
'firewall.sourceSubnetMask': 'jfirewalldialog_msg28',
|
|
'firewall.destinationIPAddress': 'jfirewalldialog_msg29',
|
|
'firewall.destinationSubnetMask': 'jfirewalldialog_msg30',
|
|
'firewall.protocol': 'jfirewalldialog_msg31',
|
|
'firewall.accept': 'jfirewalldialog_msg33',
|
|
'firewall.drop': 'jfirewalldialog_msg34',
|
|
'firewall.port': 'jfirewalldialog_msg35',
|
|
'firewall.active': 'jfirewalldialog_msg38',
|
|
'firewall.activeHint': 'jfirewalldialog_msg39',
|
|
'firewall.dropIcmp': 'jfirewalldialog_msg40',
|
|
'firewall.dropIcmpHint': 'jfirewalldialog_msg41',
|
|
'firewall.filterSyn': 'jfirewalldialog_msg42',
|
|
'sheet.connections': 'jvermittlungsrechnerkonfiguration_msg23',
|
|
'connections.local': 'jvermittlungsrechnerkonfiguration_msg22',
|
|
'connections.add': 'jvermittlungsrechnerkonfiguration_msg24',
|
|
'connections.most': 'jvermittlungsrechnerkonfiguration_msg25',
|
|
'mode.speed': 'guimainmemu_msg16',
|
|
'installation.installed': 'installationsdialog_msg3',
|
|
'installation.available': 'installationsdialog_msg4',
|
|
'installation.apply': 'installationsdialog_msg2',
|
|
'mode.currentSpeed': 'guimainmemu_msg15',
|
|
'menu.showDesktop': 'guievents_msg3',
|
|
'desktop.home': 'desktoppanel_application',
|
|
'desktop.installation': 'desktoppanel_msg1',
|
|
'network.ipAddress': 'network_msg1',
|
|
'network.subnetMask': 'network_msg4',
|
|
'network.dnsServer': 'network_msg2',
|
|
'network.gateway': 'network_msg3',
|
|
'network.macAddress': 'network_msg9',
|
|
'browser.go': 'webbrowser_msg2',
|
|
'server.start': 'webserver_msg1',
|
|
'server.stop': 'webserver_msg2',
|
|
'webServer.virtualHosts': 'webserver_msg3',
|
|
'webServer.hostName': 'webserver_msg4',
|
|
'webServer.folder': 'webserver_msg5',
|
|
'webServer.useVirtualHosts': 'webserver_msg6',
|
|
'server.acceptingStopped': 'sw_serveranwendung_msg1',
|
|
'server.acceptingStarted': 'sw_serveranwendung_msg2',
|
|
'server.serverSocketClosed': 'sw_serveranwendung_msg3',
|
|
'server.serverSocketFailed': 'sw_serveranwendung_msg6',
|
|
'server.sendFailed.notConnected': 'sw_tcpsocket_msg6',
|
|
'server.sendFailed.connectionLost': 'sw_tcpsocket_msg7',
|
|
'server.sendFailed.timedOut': 'sw_tcpsocket_msg8',
|
|
};
|
|
|
|
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
|
|
const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string) => string]>> = {
|
|
'router.prompt': ['guicontainer_msg1', (java) => java.replace(' \\n ', ' ').replace('Vermittlungsrechnerns', 'Vermittlungsrechners')],
|
|
// A label above its list, without the colon of Java's label beside it.
|
|
'sheet.network': ['jhostkonfiguration_msg15', (java) => java.replace(/:$/, '')],
|
|
// Java writes "Eintrag Löschen".
|
|
'sheet.deleteEntry': ['jvermittlungsrechnerkonfiguration_msg13', (java) => java.replace('Löschen', 'löschen')],
|
|
// Java's word, then what it joins or which card.
|
|
'sheet.connectedTo': ['jvermittlungsrechnerkonfiguration_msg6', (java) => `${java} {device}`],
|
|
'sheet.nic': ['jvermittlungsrechnerkonfiguration_msg10', (java) => `${java} {number}`],
|
|
// A column's title without Java's question mark ("Aktion?").
|
|
'firewall.action': ['jfirewalldialog_msg32', (java) => java.replace(/\?$/, '')],
|
|
// A label above its list, without the colon.
|
|
'firewall.defaultPolicy': ['jfirewalldialog_msg36', (java) => java.replace(/:$/, '')],
|
|
// Java's "\n" as line breaks; the last sentence of the German starts with a small letter there.
|
|
'firewall.intro': ['jfirewalldialog_msg37', (java) => java.replaceAll('\\n', '\n').replace('\nüber', '\nÜber')],
|
|
'firewall.filterSynHint': ['jfirewalldialog_msg43', (java) => java.replaceAll('\\n', '\n')],
|
|
// Without Java's key (Strg+D, Strg+R): in a browser they bookmark and reload the page.
|
|
'mode.design': ['guimainmemu_msg3', (java) => java.replace(/ \((Strg|Ctrl)\+D\)$/, '')],
|
|
'mode.simulation': ['guimainmemu_msg4', (java) => java.replace(/ \((Strg|Ctrl)\+R\)$/, '')],
|
|
// Java's lines without their leading space and the empty one; a step starts with a capital letter.
|
|
'connections.hint': [
|
|
'jvermittlungsrechnerkonfiguration_msg19',
|
|
(java) =>
|
|
java
|
|
.split('\\n')
|
|
.map((line) => line.trim().replace(/^(\d\. )(\p{Ll})/u, (_, step: string, letter: string) => step + letter.toUpperCase()))
|
|
.filter((line) => line !== '')
|
|
.join('\n'),
|
|
],
|
|
};
|
|
|
|
/** Java's words around a value, as its programs join them („Verbindung zu“ + address + „hergestellt“). */
|
|
const FROM_JAVA_JOINED: Readonly<Record<string, readonly [string, string, string]>> = {
|
|
'server.connectionAccepted': ['sw_serveranwendung_msg4', '{address}', 'sw_serveranwendung_msg5'],
|
|
'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'],
|
|
};
|
|
|
|
/** Java's page for a server it cannot reach (sw_webbrowser_msg1): its title and its heading. */
|
|
const FROM_JAVA_PAGE: Readonly<Record<string, readonly [string, RegExp]>> = {
|
|
'browser.unreachableTitle': ['sw_webbrowser_msg1', /<title>(.*)<\/title>/],
|
|
'browser.unreachable': ['sw_webbrowser_msg1', /<h1>(.*)<\/h1>/],
|
|
};
|
|
|
|
const templates = join(dirname(fileURLToPath(import.meta.url)), '../../../tools/java-oracle/.upstream/src/main/resources/tmpl');
|
|
|
|
describe('the catalogs', () => {
|
|
it('have the same keys in German and English, each with a text', () => {
|
|
expect(Object.keys(CATALOGS.en).sort()).toEqual(Object.keys(CATALOGS.de).sort());
|
|
for (const catalog of Object.values(CATALOGS)) for (const text of Object.values(catalog)) expect(text.trim()).not.toBe('');
|
|
});
|
|
|
|
it('fill placeholders, and show a key they lack', () => {
|
|
expect(translate('de', 'zoom.level', { percent: 75 })).toBe('75 %');
|
|
expect(translate('en', 'zoom.level')).toBe('{percent} %');
|
|
expect(translate('de', 'no.such.key')).toBe('no.such.key');
|
|
});
|
|
|
|
it('name every kind of device', () => {
|
|
for (const kind of ['pc', 'notebook', 'networkSwitch', 'router', 'gateway', 'remoteLink', 'unsupported']) expect(CATALOGS.de[`device.${kind}`]).toBeDefined();
|
|
// Java's Modem.TYPE, the same in every language.
|
|
expect([CATALOGS.de['device.remoteLink'], CATALOGS.en['device.remoteLink']]).toEqual(['Modem', 'Modem']);
|
|
});
|
|
|
|
it.skipIf(!existsSync(bundles))("use Java's words where Java has them (M11)", () => {
|
|
const [de, en] = [properties('MessagesBundle_de_DE.properties'), properties('MessagesBundle_en_GB.properties')];
|
|
for (const [key, javaKey] of Object.entries(FROM_JAVA)) {
|
|
expect(CATALOGS.de[key], key).toBe(de.get(javaKey));
|
|
expect(CATALOGS.en[key], key).toBe(en.get(javaKey));
|
|
}
|
|
for (const [key, [javaKey, mend]] of Object.entries(FROM_JAVA_MENDED)) {
|
|
expect(CATALOGS.de[key], key).toBe(mend(de.get(javaKey)!));
|
|
expect(CATALOGS.en[key], key).toBe(mend(en.get(javaKey)!));
|
|
}
|
|
for (const [key, [before, value, after]] of Object.entries(FROM_JAVA_JOINED)) {
|
|
expect(CATALOGS.de[key], key).toBe(`${de.get(before)} ${value} ${de.get(after)}`);
|
|
expect(CATALOGS.en[key], key).toBe(`${en.get(before)} ${value} ${en.get(after)}`);
|
|
}
|
|
for (const [key, [javaKey, part]] of Object.entries(FROM_JAVA_PAGE)) {
|
|
expect(CATALOGS.de[key], key).toBe(part.exec(de.get(javaKey)!)![1]);
|
|
expect(CATALOGS.en[key], key).toBe(part.exec(en.get(javaKey)!)![1]);
|
|
}
|
|
});
|
|
|
|
it.skipIf(!existsSync(templates))("write the browser's error page as Java's template does (tmpl/http_fehler)", () => {
|
|
for (const [language, file] of [['de', 'http_fehler_de_DE.txt'], ['en', 'http_fehler_en_GB.txt']] as const) {
|
|
const template = readFileSync(join(templates, file), 'utf8').replaceAll(':meldung:', '{message}').replaceAll(':code:', '{code}');
|
|
expect(CATALOGS[language]['browser.errorTitle']).toBe(/<title>(.*)<\/title>/.exec(template)![1]);
|
|
expect(CATALOGS[language]['browser.errorText']).toBe(/<p>(.*)<\/p>/.exec(template)![1]!.trim());
|
|
expect(/<h1>(.*)<\/h1>/.exec(template)![1]).toBe('{message}');
|
|
}
|
|
});
|
|
});
|