// 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 { const text = new TextDecoder('latin1').decode(readFileSync(join(bundles, file))).replace(/\\\r?\n[ \t]*/g, ''); const entries = new Map(); 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> = { 'device.pc': 'hw_rechner_msg1', 'explorer.newFolder': 'fileexplorer_msg3', 'explorer.delete': 'fileexplorer_msg4', 'explorer.cut': 'fileexplorer_msg5', 'explorer.copy': 'fileexplorer_msg6', 'explorer.paste': 'fileexplorer_msg7', 'explorer.rename': 'fileexplorer_msg8', 'explorer.newName': 'fileexplorer_msg9', 'explorer.file': 'fileexplorer_msg10', 'explorer.folder': 'fileexplorer_msg11', 'explorer.import': 'fileexplorer_msg2', 'explorer.importTitle': 'fileexplorer_msg12', 'explorer.localFile': 'fileexplorer_msg13', 'explorer.chooseFile': 'fileexplorer_msg14', 'explorer.importButton': 'fileexplorer_msg15', 'explorer.importMissing': 'fileexplorer_msg16', 'explorer.sure': 'fileexplorer_msg18', 'explorer.invalidTarget': 'fileexplorer_msg19', 'explorer.close': 'sw_fileimporter_msg3', 'explorer.imported': 'sw_fileexplorer_msg2', 'explorer.notImported': 'sw_fileexplorer_msg1', 'explorer.tooLarge': 'sw_fileexplorer_msg3', 'viewer.open': 'imageviewer_msg2', 'echoServer.port': 'serverbaustein_msg1', 'echoServer.stop': 'serverbaustein_msg2', 'echoServer.start': 'serverbaustein_msg3', 'simpleClient.address': 'clientbaustein_msg1', 'simpleClient.port': 'clientbaustein_msg2', 'simpleClient.connect': 'clientbaustein_msg3', 'simpleClient.message': 'clientbaustein_msg4', 'simpleClient.send': 'clientbaustein_msg5', 'simpleClient.disconnect': 'clientbaustein_msg6', 'simpleClient.connected': 'sw_clientbaustein_msg2', 'simpleClient.disconnected': 'sw_clientbaustein_msg3', 'simpleClient.notConnected': 'sw_clientbaustein_msg4', 'simpleClient.receiveFailed': 'sw_tcpsocket_msg9', 'simpleClient.failure.unresolved': 'sw_socket_msg1', 'simpleClient.failure.timedOut': 'sw_tcpsocket_msg2', 'simpleClient.failure.failed': 'sw_tcpsocket_msg3', 'dnsServer.start': 'dnsserver_msg1', 'dnsServer.stop': 'dnsserver_msg14', 'dnsServer.tabA': 'dnsserver_msg2', 'dnsServer.tabMX': 'dnsserver_msg3', 'dnsServer.tabNS': 'dnsserver_msg15', 'dnsServer.aName': 'dnsserver_msg4', 'dnsServer.aAddress': 'dnsserver_msg5', 'dnsServer.add': 'dnsserver_msg6', 'dnsServer.remove': 'dnsserver_msg7', 'dnsServer.aNameColumn': 'dnsserver_msg8', 'dnsServer.aAddressColumn': 'dnsserver_msg9', 'dnsServer.mxDomain': 'dnsserver_msg10', 'dnsServer.mxServer': 'dnsserver_msg11', 'dnsServer.mxDomainColumn': 'dnsserver_msg12', 'dnsServer.mxServerColumn': 'dnsserver_msg13', 'dnsServer.nsDomain': 'dnsserver_msg16', 'dnsServer.nsServer': 'dnsserver_msg17', 'dnsServer.nsDomainColumn': 'dnsserver_msg18', 'dnsServer.nsServerColumn': 'dnsserver_msg19', 'dnsServer.recursive': 'dnsserver_msg20', 'textEditor.new': 'texteditor_msg3', 'textEditor.open': 'texteditor_msg4', 'textEditor.save': 'texteditor_msg5', 'textEditor.saveAs': 'texteditor_msg6', 'fileDialog.up': 'dmtnfilechooser_msg1', 'fileDialog.name': 'dmtnfilechooser_msg2', 'fileDialog.cancel': 'dmtnfilechooser_msg4', 'fileDialog.saveTitle': 'dmtnfilechooser_msg5', 'fileDialog.save': 'dmtnfilechooser_msg6', 'fileDialog.openTitle': 'dmtnfilechooser_msg7', 'fileDialog.open': 'dmtnfilechooser_msg8', 'fileDialog.folder': 'dmtnfilechooser_msg11', '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', 'emailClient.sender': 'emailanwendung_msg1', 'emailClient.subject': 'emailanwendung_msg2', 'emailClient.recipient': 'emailanwendung_msg3', 'emailClient.inbox': 'emailanwendung_msg5', 'emailClient.sent': 'emailanwendung_msg6', 'emailClient.fetch': 'emailanwendung_msg7', 'emailClient.compose': 'emailanwendung_msg8', 'emailClient.answer': 'emailanwendung_msg9', 'emailClient.deleteHint': 'emailanwendung_msg10', 'emailClient.accountHint': 'emailanwendung_msg11', 'emailClient.composeTitle': 'emailanwendung_msg12', 'emailClient.send': 'emailanwendung_msg13', 'emailClient.toLabel': 'emailanwendung_msg14', 'emailClient.ccLabel': 'emailanwendung_msg15', 'emailClient.bccLabel': 'emailanwendung_msg16', 'emailClient.subjectLabel': 'emailanwendung_msg17', 'emailClient.re': 'emailanwendung_msg18', 'emailClient.wrote': 'emailanwendung_msg19', 'emailClient.invalidAddress': 'emailanwendung_msg20', 'emailClient.noRecipient': 'emailanwendung_msg21', 'emailClient.accountTitle': 'emailanwendung_msg25', 'emailClient.email': 'emailanwendung_msg26', 'emailClient.pop3Host': 'emailanwendung_msg27', 'emailClient.pop3Port': 'emailanwendung_msg28', 'emailClient.smtpHost': 'emailanwendung_msg29', 'emailClient.smtpPort': 'emailanwendung_msg30', 'emailClient.username': 'emailanwendung_msg31', 'emailClient.password': 'emailanwendung_msg32', 'emailClient.save': 'emailanwendung_msg33', 'emailClient.cancel': 'emailanwendung_msg37', 'emailClient.delete': 'emailanwendung_msg43', 'emailClient.account': 'emailanwendung_msg44', 'emailClient.name': 'emailanwendung_msg45', 'emailClient.accountInvalid': 'emailanwendung_msg46', 'emailClient.noAccount': 'emailanwendung_msg47', 'emailServer.start': 'emailserver_msg1', 'emailServer.started': 'emailserver_msg2', 'emailServer.stopped': 'emailserver_msg3', 'emailServer.domain': 'emailserver_msg4', 'emailServer.deleteAccount': 'emailserver_msg8', 'emailServer.yes': 'emailserver_msg9', 'emailServer.cancel': 'emailserver_msg10', 'emailServer.confirmTitle': 'emailserver_msg13', 'emailServer.address': 'emailserver_msg14', 'emailServer.mails': 'emailserver_msg15', 'emailServer.username': 'emailserver_msg16', 'emailServer.name': 'emailserver_msg17', 'emailServer.password': 'emailserver_msg18', 'emailServer.create': 'emailserver_msg19', 'emailServer.lastNamePlaceholder': 'emailserver_msg20', 'emailServer.firstNamePlaceholder': 'emailserver_msg21', 'emailServer.whiteSpace': 'emailserver_msg24', 'emailServer.missing': 'emailserver_msg25', 'emailServer.newAccount': 'emailserver_msg26', 'emailServer.accounts': 'emailserver_msg27', 'emailServer.log': 'emailserver_msg28', 'emailServer.stop': 'emailserver_msg29', 'mail.failure.unresolved': 'sw_socket_msg1', 'mail.failure.timedOut': 'sw_tcpsocket_msg2', 'mail.failure.failed': 'sw_tcpsocket_msg3', 'mail.failure.receiveFailed': 'sw_tcpsocket_msg6', }; /** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */ const FROM_JAVA_MENDED: Readonly 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's message dialog breaks its lines with \n. 'dnsServer.invalidA': ['dnsserver_msg21', (java) => java.replaceAll('\\n', '\n')], 'dnsServer.invalidNS': ['dnsserver_msg22', (java) => java.replaceAll('\\n', '\n')], 'dnsServer.invalidMX': ['dnsserver_msg23', (java) => java.replaceAll('\\n', '\n')], // 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')], // Java's welcome without its bold tags (the panel writes it as text, Java's "!" after it). 'emailClient.welcome': ['emailanwendung_msg4', (java) => java.replace(/<\/?b>/g, '')], // 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> = { 'server.connectionAccepted': ['sw_serveranwendung_msg4', '{address}', 'sw_serveranwendung_msg5'], 'simpleClient.connectionClosed': ['sw_clientbaustein_msg5', '{address}', 'sw_clientbaustein_msg6'], 'server.connectionClosed': ['sw_servermitarbeiter_msg1', '{address}', 'sw_servermitarbeiter_msg2'], 'emailServer.created': ['emailserver_msg22', '{name}', 'emailserver_msg23'], 'mail.stored': ['sw_smtpmitarbeiter_msg12', '{name}', 'sw_smtpmitarbeiter_msg13'], 'mail.connected': ['sw_smtpclient_msg1', '{address}', 'sw_smtpclient_msg2'], 'mail.disconnected': ['sw_smtpclient_msg1', '{address}', 'sw_smtpclient_msg3'], }; /** Java's words before a value, joined as its programs join them (SMTPMitarbeiter, SMTPClient, the server window's question). */ const FROM_JAVA_BEFORE: Readonly> = { 'mail.sendFailed': ['sw_smtpmitarbeiter_msg14', ' {address}'], 'mail.mailSent': ['sw_smtpclient_msg4', ' {recipients}'], // "'" + name + "'", the question's two halves written against it. 'emailServer.confirmDelete': ['emailserver_msg11', '{name}', 'emailserver_msg12'], }; /** Java's page for a server it cannot reach (sw_webbrowser_msg1): its title and its heading. */ const FROM_JAVA_PAGE: Readonly> = { 'browser.unreachableTitle': ['sw_webbrowser_msg1', /(.*)<\/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, [before, value, after]] of Object.entries(FROM_JAVA_BEFORE)) { expect(CATALOGS.de[key], key).toBe(`${de.get(before)}${value}${after === undefined ? '' : de.get(after)}`); expect(CATALOGS.en[key], key).toBe(`${en.get(before)}${value}${after === undefined ? '' : 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}'); } }); });