feat(editor): F4.5e a router's connections in the device sheet

SheetRun now clicks through Java's JConnectionsDialog with a router, a
computer and a switch cabled in the GUIContainer: a click marks a card,
the same click unmarks it, another card swaps the cables or moves the
one; "+" adds 192.168.0.10/24 up to eight cards, "-" removes the last
card with its cable down to none, and a router without a card keeps no
gateway.

The document adds, removes and swaps a router's cards (the same cables,
the same devices), Java's click logic is one function for the test and
the page, and the sheet names the address at a cable's far end. The
page "Verbindungen verwalten" shows Java's RJ45 buttons in Java's
colours with each card's far end below. Not adopted: the last card
stays. A card tab or a mark whose card is undone away falls back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 13:12:27 +00:00
co-authored by Claude Opus 5.5
parent 4e062bb347
commit 6e97e3565d
21 changed files with 2780 additions and 41 deletions
+96
View File
@@ -400,6 +400,102 @@ test("sets up a router's firewall on its own page: the switches, the default, ru
await expect(sheet(page).getByRole('heading')).toHaveText('Vermittlungsrechner');
});
test("manages a router's connections on its own page: cables swapped with two taps, cards added and removed, the last kept", async ({ page }) => {
await openEditor(page, server.base, 'zwei_rechnernetze_komplett.fls');
const id = (await drawing(page)).nodes.find((node) => node.kind === 'router')!.id;
await choose(page, id);
const joined = async () => ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.device ?? '');
const values = (await sheetOf(page, id)) as RouterSheet;
const [one, two] = await joined();
const cables = (await drawing(page)).links.length;
await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click();
await expect(sheet(page).getByRole('heading', { level: 2 })).toHaveText('Verbindungen verwalten');
const items = sheet(page).locator('ol.connections li');
const port = (index: number) => items.nth(index).getByRole('button');
await expect(items.locator('.connection-local')).toHaveText(values.interfaces.map((card, index) => `NIC ${index + 1}: ${card.ipAddress}`));
await expect(items.locator('.connection-remote')).toHaveText([one!, two!]);
// Java's colours: green for a plugged card, yellow for the marked one, red for a free one.
await expect(port(0)).toHaveCSS('background-color', 'rgb(0, 255, 0)');
await port(0).click();
await expect(port(0)).toHaveAttribute('aria-pressed', 'true');
await expect(port(0)).toHaveCSS('background-color', 'rgb(255, 255, 0)');
await port(0).click();
await expect(port(0)).toHaveAttribute('aria-pressed', 'false');
await port(0).click();
await port(1).click();
await expect.poll(joined).toEqual([two, one]);
await expect(items.nth(0)).toHaveAttribute('data-state', 'plugged');
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
await expect(items).toHaveCount(3);
await expect(items.nth(2).locator('.connection-local')).toHaveText('NIC 3: 192.168.0.10');
await expect(port(2)).toHaveCSS('background-color', 'rgb(255, 0, 0)');
await expect(port(2)).toHaveAccessibleName('NIC 3: 192.168.0.10, Keine Verbindung');
// A free card takes the marked card's cable.
await port(0).click();
await port(2).click();
await expect.poll(joined).toEqual(['', one, two]);
await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
await expect.poll(joined).toEqual(['', one]);
expect((await drawing(page)).links).toHaveLength(cables - 1);
for (let count = 2; count < 8; count++) await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
await expect(items).toHaveCount(8);
await expect(sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' })).toBeDisabled();
await expect(sheet(page).getByText('Maximal 8 Schnittstellen erlaubt!')).toBeVisible();
for (let count = 8; count > 1; count--) await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
await expect(items).toHaveCount(1);
await expect(sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' })).toBeDisabled();
await expect(sheet(page).getByText('Eine Schnittstelle bleibt immer.')).toBeVisible();
await sheet(page).getByRole('button', { name: 'Zurück' }).click();
const tabs = sheet(page).getByRole('tab');
await expect(tabs).toHaveText(['Allgemein', values.interfaces[0]!.ipAddress, 'Weiterleitungstabelle']);
// Undone, the second card comes back with its cable; a card's tab whose card goes again shows the general tab.
await page.keyboard.press('Control+z');
await expect(tabs).toHaveCount(4);
await tabs.nth(2).click();
await expect(sheet(page).locator('.sheet-connected')).toHaveText(`Verbunden mit ${one}`);
await page.keyboard.press('Control+Shift+z');
await expect(tabs).toHaveCount(3);
await expect(tabs.first()).toHaveAttribute('aria-selected', 'true');
await expect(field(page, 'name')).toHaveValue(values.name);
});
test("names a router's computer by its address, and lets go of a marked card when cards change", async ({ page }) => {
await openEditor(page, server.base, 'internetwork_komplett.fls');
const routers = (await drawing(page)).nodes.filter((node) => node.kind === 'router');
let id = '';
for (const each of routers) if (((await sheetOf(page, each.id)) as RouterSheet).interfaces.some((card) => card.connected?.address)) id = each.id;
await choose(page, id);
const values = (await sheetOf(page, id)) as RouterSheet;
await sheet(page).getByRole('button', { name: 'Verbindungen verwalten' }).click();
const items = sheet(page).locator('ol.connections li');
const port = (index: number) => items.nth(index).getByRole('button');
// Java: a computer's or a router's name with the address of its card, a switch's name alone.
await expect(items.locator('.connection-remote')).toHaveText(
values.interfaces.flatMap((card) => (card.connected === null ? [] : [card.connected.address === null ? card.connected.device : `${card.connected.device} (${card.connected.address})`])),
);
expect(values.interfaces.some((card) => card.connected?.address !== null && card.connected?.address !== undefined)).toBe(true);
const pressed = () => sheet(page).locator('.connection-port[aria-pressed="true"]');
await port(0).click();
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
await expect(pressed()).toHaveCount(0);
await port(0).click();
await sheet(page).getByRole('button', { name: 'Schnittstelle entfernen' }).click();
await expect(pressed()).toHaveCount(0);
// A marked card undone away is marked no more: the next tap marks, it swaps nothing.
await sheet(page).getByRole('button', { name: 'Schnittstelle hinzufügen' }).click();
const count = values.interfaces.length + 1;
await port(count - 1).click();
await page.keyboard.press('Control+z');
await expect(items).toHaveCount(count - 1);
const before = ((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null);
await port(0).click();
await expect(port(0)).toHaveAttribute('aria-pressed', 'true');
expect(((await sheetOf(page, id)) as RouterSheet).interfaces.map((card) => card.connected?.link ?? null)).toEqual(before);
});
test("opens a home router's firewall from its general tab", async ({ page }) => {
await openEditor(page, server.base, 'new');
await page.locator('[data-palette="gateway"]').click();
Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

+4
View File
@@ -7,7 +7,11 @@ import laptop from '../assets/hardware/laptop.png';
import modem from '../assets/hardware/modem.png';
import router from '../assets/hardware/router.png';
import server from '../assets/hardware/server.png';
import rj45 from '../assets/hardware/rj45.png';
import networkSwitch from '../assets/hardware/switch.png';
import type { IconName } from './layout.js';
export const ICON_URLS: Readonly<Record<IconName, string>> = { cloud, gateway, laptop, modem, router, server, switch: networkSwitch };
/** A card's socket, on the buttons of a router's connections (JConnectionsDialog). */
export const RJ45_URL = rj45;
+8
View File
@@ -11,6 +11,13 @@
"cable.wanOccupied": "WAN-Anschluss (extern) bereits belegt!",
"cable.wirelessNotPossible": "Drahtlose Verbindung nicht möglich",
"canvas.label": "Netzwerk",
"connections.add": "Schnittstelle hinzufügen",
"connections.fewest": "Eine Schnittstelle bleibt immer.",
"connections.hint": "Hinweis: Zum Umstecken von Verbindungen:\n1. Verbindung auswählen durch Klick auf Schaltfläche links neben der Schnittstellenbezeichnung\n2. Auswählen der neuen Schnittstelle durch Klick auf die entsprechende Schaltfläche\nSollte die Zielschnittstelle bereits belegt sein, so werden die Verbindungen vertauscht.",
"connections.local": "Lokale Schnittstellen",
"connections.most": "Maximal 8 Schnittstellen erlaubt!",
"connections.nic": "NIC {number}: {address}",
"connections.remove": "Schnittstelle entfernen",
"design.actions": "Aktionen",
"design.palette": "Komponenten",
"design.redo": "Wiederholen",
@@ -88,6 +95,7 @@
"sheet.cloud": "als Wolke darstellen",
"sheet.connectedTo": "Verbunden mit {device}",
"sheet.connection": "Anschluss",
"sheet.connections": "Verbindungen verwalten",
"sheet.defaultGateway": "Gateway",
"sheet.deleteEntry": "Eintrag löschen",
"sheet.dhcpClient": "DHCP zur Konfiguration verwenden",
+8
View File
@@ -11,6 +11,13 @@
"cable.wanOccupied": "WAN port (external) already occupied!",
"cable.wirelessNotPossible": "Wireless connection not possible",
"canvas.label": "Network",
"connections.add": "Add interface",
"connections.fewest": "One interface always stays.",
"connections.hint": "Note: For reorganising cable connections:\n1. Choose cable connection by clicking the button next to the interface name\n2. Choose new interface by clicking according button\nIf the target interface is already occupied by another connection, then those cables will be swapped.",
"connections.local": "Local interfaces",
"connections.most": "At most 8 interfaces supported!",
"connections.nic": "NIC {number}: {address}",
"connections.remove": "Remove interface",
"design.actions": "Actions",
"design.palette": "Components",
"design.redo": "Redo",
@@ -88,6 +95,7 @@
"sheet.cloud": "Display as Cloud",
"sheet.connectedTo": "Connected to {device}",
"sheet.connection": "Connection",
"sheet.connections": "Manage connections",
"sheet.defaultGateway": "Gateway",
"sheet.deleteEntry": "Delete entry",
"sheet.dhcpClient": "Use DHCP for configuration",
@@ -0,0 +1,92 @@
// A router's connections as a page of the device sheet, after Java's dialog
// (JConnectionsDialog): each card with its RJ45 button left of its name and,
// below, what its cable joins; a card's button marks it, another card's
// button then swaps the two cards' cables (or moves the one), as Java's
// buttons do; "+" adds a card after the last, "−" removes the last with its
// cable — down to one card, not to none as in Java. Java's colours on the
// buttons: green plugged, red free, yellow marked. Java draws its cables
// between two columns; a narrow sheet has room for one.
import { useState } from 'react';
import { addRouterInterface, connectionsClick, removeRouterInterface, ROUTER_INTERFACES, swapRouterCables, type RouterSheet } from '@mdg-filius/doc';
import { RJ45_URL } from '../canvas/icons.js';
import { useTranslate } from '../i18n/i18n.js';
import type { Change } from './NameField.js';
import { SheetNote } from './SheetField.js';
export function ConnectionsPanel({ sheet, change }: { readonly sheet: RouterSheet; readonly change: Change }) {
const t = useTranslate();
const [chosen, setChosen] = useState<number | null>(null);
const cards = sheet.interfaces;
// A card another person removed is no longer marked.
const marked = chosen !== null && chosen < cards.length ? chosen : null;
const click = (index: number) => {
const next = connectionsClick(marked, index);
if (next.swap !== undefined) {
const [first, second] = next.swap;
change.run(() => swapRouterCables(change.doc, change.id, cards[first]!.portId, cards[second]!.portId, change.origin));
}
setChosen(next.marked);
};
return (
<>
<h3 className="sheet-subtitle">{t('connections.local')}</h3>
<ol className="connections">
{cards.map((card, index) => {
const name = t('connections.nic', { number: index + 1, address: card.ipAddress });
const joined = card.connected;
return (
<li key={card.portId} data-state={index === marked ? 'marked' : joined === null ? 'free' : 'plugged'}>
<button
type="button"
className="connection-port"
aria-pressed={index === marked}
aria-label={joined === null ? `${name}, ${t('sheet.notConnected')}` : name}
onClick={() => click(index)}
>
<img src={RJ45_URL} alt="" />
</button>
<span className="connection-local">{name}</span>
{joined === null ? null : <span className="connection-remote">{joined.address === null ? joined.device : `${joined.device} (${joined.address})`}</span>}
</li>
);
})}
</ol>
<div className="sheet-buttons">
<button
type="button"
name="addInterface"
aria-label={t('connections.add')}
title={t('connections.add')}
disabled={cards.length >= ROUTER_INTERFACES.maximum}
onClick={() => {
change.run(() => addRouterInterface(change.doc, change.id, crypto.randomUUID(), change.origin));
setChosen(null);
}}
>
+
</button>
<button
type="button"
name="removeInterface"
aria-label={t('connections.remove')}
title={t('connections.remove')}
disabled={cards.length <= ROUTER_INTERFACES.fewest}
onClick={() => {
change.run(() => removeRouterInterface(change.doc, change.id, change.origin));
setChosen(null);
}}
>
−
</button>
</div>
{cards.length >= ROUTER_INTERFACES.maximum ? (
<p className="sheet-note">{t('connections.most')}</p>
) : cards.length <= ROUTER_INTERFACES.fewest ? (
<p className="sheet-note">{t('connections.fewest')}</p>
) : null}
{/* Below the cards, as in Java's dialog. */}
<SheetNote text={t('connections.hint')} />
</>
);
}
+9 -2
View File
@@ -30,6 +30,7 @@ import { ICON_URLS } from '../canvas/icons.js';
import { useTranslate } from '../i18n/i18n.js';
import { NameField, type Change } from './NameField.js';
import { HomeRouterFields, RouterFields } from './RouterSheets.js';
import { ConnectionsPanel } from './ConnectionsPanel.js';
import { FirewallPanel } from './FirewallPanel.js';
import { DhcpServerPanel, PortForwardingPanel } from './ServicePanels.js';
import { SheetCheck, SheetField } from './SheetField.js';
@@ -62,7 +63,9 @@ export interface DeviceSheetProps {
}
/** A page of the sheet that Java opens as a dialog. */
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall';
export type SheetPage = 'dhcp' | 'forwarding' | 'firewall' | 'connections';
const PAGE_TITLES: Readonly<Record<SheetPage, string>> = { dhcp: 'sheet.dhcpServer', forwarding: 'sheet.portForwarding', firewall: 'sheet.firewall', connections: 'sheet.connections' };
export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onClose, onHighlight }: DeviceSheetProps) {
const t = useTranslate();
@@ -92,7 +95,7 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
‹
</button>
)}
<h2>{open === 'dhcp' ? t('sheet.dhcpServer') : open === 'forwarding' ? t('sheet.portForwarding') : open === 'firewall' ? t('sheet.firewall') : title}</h2>
<h2>{open === null ? title : t(PAGE_TITLES[open])}</h2>
<button type="button" className="sheet-close" onClick={onClose} aria-label={t('sheet.close')} title={t('sheet.close')}>
×
</button>
@@ -121,6 +124,10 @@ export function DeviceSheet({ doc, id, label, origin, onChanged, onMessage, onCl
<div key={`${id}-firewall`}>
<FirewallPanel firewall={sheet.firewall} change={change} />
</div>
) : open === 'connections' && sheet.kind === 'router' ? (
<div key={`${id}-connections`}>
<ConnectionsPanel sheet={sheet} change={change} />
</div>
) : open === 'forwarding' && sheet.kind === 'gateway' ? (
<div key={`${id}-forwarding`}>
<PortForwardingPanel sheet={sheet} change={change} />
+5 -18
View File
@@ -22,7 +22,7 @@ import {
import type { FirewallAction, FirewallProtocol, FirewallRule } from '@mdg-filius/core';
import { useTranslate } from '../i18n/i18n.js';
import type { Change } from './NameField.js';
import { SheetCheck, SheetField } from './SheetField.js';
import { SheetCheck, SheetField, SheetNote } from './SheetField.js';
import { SheetTabs } from './SheetTabs.js';
/** Java's protocol names in the rule table (ruleAsVector): "*" for all. */
@@ -32,19 +32,6 @@ const OFFERED: readonly FirewallProtocol[] = ['all', 'tcp', 'udp'];
const cellText = (rule: FirewallRule, column: (typeof FIREWALL_RULE_COLUMNS)[number]) => (column === 'port' ? (rule.port < 0 ? '' : String(rule.port)) : rule[column]);
/** A text with Java's line breaks as paragraphs. */
function Note({ text }: { readonly text: string }) {
return (
<>
{text.split('\n').map((line, index) => (
<p key={index} className="sheet-note">
{line}
</p>
))}
</>
);
}
export function FirewallPanel({ firewall, change }: { readonly firewall: FirewallSheet; readonly change: Change }) {
const t = useTranslate();
const [tab, setTab] = useState('general');
@@ -68,15 +55,15 @@ export function FirewallPanel({ firewall, change }: { readonly firewall: Firewal
{tab === 'general' ? (
<>
<SheetCheck name="firewallActive" label={t('firewall.active')} checked={firewall.isActive} onChange={flag('isActive')} />
<Note text={t('firewall.activeHint')} />
<SheetNote text={t('firewall.activeHint')} />
<SheetCheck name="dropIcmp" label={t('firewall.dropIcmp')} checked={firewall.dropIcmp} onChange={flag('dropIcmp')} />
<Note text={t('firewall.dropIcmpHint')} />
<SheetNote text={t('firewall.dropIcmpHint')} />
<SheetCheck name="filterSyn" label={t('firewall.filterSyn')} checked={firewall.filterSynSegmentsOnly} onChange={flag('filterSynSegmentsOnly')} />
<Note text={t('firewall.filterSynHint')} />
<SheetNote text={t('firewall.filterSynHint')} />
</>
) : (
<>
<Note text={t('firewall.intro')} />
<SheetNote text={t('firewall.intro')} />
<label className="sheet-field">
<span>{t('firewall.defaultPolicy')}</span>
<select
+10 -5
View File
@@ -3,8 +3,9 @@
// general settings, each card and — on a router — the forwarding table
// (JWeiterleitungsTabelle), greyed out while automatic routing (RIP) runs.
// The card whose tab is open has its cable drawn active, as Java draws it.
// The firewall, the port forwarding, the DHCP server and the change of the
// cards follow with their dialogs (F4.5c).
// The firewall, the port forwarding, the DHCP server and the router's
// connections open as pages of the sheet (ServicePanels, FirewallPanel,
// ConnectionsPanel).
import { useEffect, useState } from 'react';
import {
@@ -74,12 +75,13 @@ export function RouterFields({
readonly sheet: RouterSheet;
readonly change: Change;
readonly onHighlight: (link: string | null) => void;
readonly onOpen: (page: 'firewall') => void;
readonly onOpen: (page: 'firewall' | 'connections') => void;
}) {
const t = useTranslate();
const [tab, setTab] = useState('general');
// Java greys the table out while RIP runs; an open table goes back to the general tab.
const shown = tab === 'table' && sheet.rip ? 'general' : tab;
// Java greys the table out while RIP runs; an open table goes back to the general tab, as does the tab of a card removed.
const gone = tab.startsWith('card-') && Number(tab.slice(5)) >= sheet.interfaces.length;
const shown = (tab === 'table' && sheet.rip) || gone ? 'general' : tab;
const card = shown.startsWith('card-') ? sheet.interfaces[Number(shown.slice(5))] : undefined;
useHighlight(card?.connected?.link ?? null, onHighlight);
const tabs: Tab[] = [
@@ -103,6 +105,9 @@ export function RouterFields({
<button type="button" onClick={() => onOpen('firewall')}>
{t('sheet.firewall')}
</button>
<button type="button" onClick={() => onOpen('connections')}>
{t('sheet.connections')}
</button>
</div>
</>
) : card !== undefined ? (
+13
View File
@@ -110,3 +110,16 @@ export function SheetCheck({
</label>
);
}
/** A text with Java's line breaks as paragraphs. */
export function SheetNote({ text }: { readonly text: string }) {
return (
<>
{text.split('\n').map((line, index) => (
<p key={index} className="sheet-note">
{line}
</p>
))}
</>
);
}
+64
View File
@@ -252,3 +252,67 @@
font-size: 13px;
color: var(--danger);
}
.sheet-subtitle {
margin: 12px 0 4px;
font-size: 14px;
font-weight: 600;
}
/* A router's cards: Java's RJ45 button left of the card's name, what its
* cable joins below, joined by a line like Java's cable. */
.connections {
margin: 0;
padding: 0;
list-style: none;
}
.connections li {
display: grid;
grid-template-columns: 52px 1fr;
column-gap: 10px;
align-items: center;
padding: 4px 0;
}
.connection-port {
grid-row: span 2;
width: 52px;
height: 44px;
padding: 0;
border: 1px solid var(--border-strong);
border-radius: 8px;
cursor: pointer;
}
.connections li[data-state='plugged'] .connection-port {
background: var(--connection-plugged);
}
.connections li[data-state='free'] .connection-port {
background: var(--connection-free);
}
.connections li[data-state='marked'] .connection-port {
background: var(--connection-marked);
outline: 2px solid var(--border-focus);
outline-offset: 1px;
}
.connection-port img {
display: block;
margin: auto;
}
.connection-local {
font-size: 14px;
font-variant-numeric: tabular-nums;
}
.connection-remote {
padding-left: 10px;
border-left: 2px solid var(--canvas-cable);
font-size: 13px;
color: var(--text-secondary);
}
+7
View File
@@ -33,6 +33,10 @@
/* Java's active cable (JCablePanel.farbeBlinken): a frame on its way, or the card whose tab is open. */
--canvas-cable-active: rgb(0 255 64);
--canvas-label: #000000;
/* Java's buttons of a router's connections (JConnectionsDialog): a card plugged, free, marked. */
--connection-plugged: rgb(0 255 0);
--connection-free: rgb(255 0 0);
--connection-marked: rgb(255 255 0);
}
:root[data-theme='dark'] {
@@ -64,6 +68,9 @@
--canvas-cable-wireless: rgb(150 150 150);
--canvas-cable-active: rgb(0 200 50);
--canvas-label: #000000;
--connection-plugged: rgb(0 190 0);
--connection-free: rgb(210 40 40);
--connection-marked: rgb(235 215 0);
}
html,
+16 -2
View File
@@ -9,9 +9,9 @@ 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 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)));
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);
@@ -105,6 +105,10 @@ const FROM_JAVA: Readonly<Record<string, string>> = {
'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',
};
/** Java's words, mended: the prompt says "Vermittlungsrechnerns" (guicontainer_msg1). */
@@ -124,6 +128,16 @@ const FROM_JAVA_MENDED: Readonly<Record<string, readonly [string, (java: string)
// 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 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'),
],
};
describe('the catalogs', () => {