feat(domains): Registrieren zeigt Nameserver aus AutoDNS nur zur Kontrolle (h3t)

- Nameserver schreibgeschuetzt in AutoDNS-Reihenfolge, ohne Eingabe
- Hinweis mit Neu-lesen-Knopf, Zusammenfassung gesperrt ohne Nameserver
- createOrder sendet keine Nameserver mehr

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 12:34:32 +02:00
parent 473738db9e
commit 1b20b84e2f
5 changed files with 172 additions and 83 deletions
@@ -27,6 +27,7 @@ const mockCreate = vi.fn();
const mockSubmit = vi.fn(); const mockSubmit = vi.fn();
const mockCancel = vi.fn(); const mockCancel = vi.fn();
const mockListContacts = vi.fn(); const mockListContacts = vi.fn();
const mockNameServers = vi.fn();
vi.mock('@/lib/domains-api', async (importOriginal) => { vi.mock('@/lib/domains-api', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/domains-api')>(); const actual = await importOriginal<typeof import('@/lib/domains-api')>();
@@ -37,6 +38,7 @@ vi.mock('@/lib/domains-api', async (importOriginal) => {
submitOrder: (...a: unknown[]) => mockSubmit(...a), submitOrder: (...a: unknown[]) => mockSubmit(...a),
cancelOrder: (...a: unknown[]) => mockCancel(...a), cancelOrder: (...a: unknown[]) => mockCancel(...a),
listContacts: (...a: unknown[]) => mockListContacts(...a), listContacts: (...a: unknown[]) => mockListContacts(...a),
getNameServers: (...a: unknown[]) => mockNameServers(...a),
}; };
}); });
@@ -46,7 +48,7 @@ function status(over: Partial<DomainsStatus> = {}): DomainsStatus {
configured: true, configured: true,
demoConfigured: true, demoConfigured: true,
liveConfigured: true, liveConfigured: true,
defaultNameServers: ['ns1.example.com', 'ns2.example.com'], defaultNameServers: [],
...over, ...over,
}; };
} }
@@ -122,7 +124,7 @@ function summary(over: Partial<OrderSummary> = {}): OrderSummary {
tech: { id: 22, displayName: 'Unsere Firma GmbH' }, tech: { id: 22, displayName: 'Unsere Firma GmbH' },
zone: { id: 22, displayName: 'Unsere Firma GmbH' }, zone: { id: 22, displayName: 'Unsere Firma GmbH' },
}, },
nameServers: ['ns1.example.com', 'ns2.example.com'], nameServers: ['z.example.net', 'b.example.org', 'a.example.org'],
periodYears: 1, periodYears: 1,
version: '2026-10-08T10:00:00.000Z', version: '2026-10-08T10:00:00.000Z',
...over, ...over,
@@ -156,6 +158,10 @@ beforeEach(() => {
mockCreate.mockReset().mockResolvedValue(summary()); mockCreate.mockReset().mockResolvedValue(summary());
mockSubmit.mockReset().mockResolvedValue(order({ status: 'SUBMITTED', jobStatus: 'RUNNING' })); mockSubmit.mockReset().mockResolvedValue(order({ status: 'SUBMITTED', jobStatus: 'RUNNING' }));
mockCancel.mockReset().mockResolvedValue(order({ status: 'CANCELED' })); mockCancel.mockReset().mockResolvedValue(order({ status: 'CANCELED' }));
mockNameServers.mockReset().mockResolvedValue({
environment: 'DEMO',
nameServers: ['z.example.net', 'b.example.org', 'a.example.org'],
});
mockListContacts.mockReset().mockResolvedValue({ mockListContacts.mockReset().mockResolvedValue({
environment: 'DEMO', environment: 'DEMO',
contacts: CONTACTS, contacts: CONTACTS,
@@ -207,7 +213,7 @@ describe('RegisterTab — Verfuegbarkeit', () => {
}); });
describe('RegisterTab — Kontakte und Nameserver', () => { describe('RegisterTab — Kontakte und Nameserver', () => {
it('Voreinstellung: Admin = Inhaber, Technik/Zone = Kontakt der eigenen Firma; Nameserver aus den Einstellungen', async () => { it('Voreinstellung: Admin = Inhaber, Technik/Zone = Kontakt der eigenen Firma', async () => {
renderTab(); renderTab();
await checkFree(); await checkFree();
const owner = (await screen.findByLabelText('Inhaber')) as HTMLSelectElement; const owner = (await screen.findByLabelText('Inhaber')) as HTMLSelectElement;
@@ -217,12 +223,6 @@ describe('RegisterTab — Kontakte und Nameserver', () => {
expect((screen.getByLabelText('Admin-Kontakt') as HTMLSelectElement).value).toBe('33'); expect((screen.getByLabelText('Admin-Kontakt') as HTMLSelectElement).value).toBe('33');
expect((screen.getByLabelText('Technischer Kontakt') as HTMLSelectElement).value).toBe('22'); expect((screen.getByLabelText('Technischer Kontakt') as HTMLSelectElement).value).toBe('22');
expect((screen.getByLabelText('Zonenkontakt') as HTMLSelectElement).value).toBe('22'); expect((screen.getByLabelText('Zonenkontakt') as HTMLSelectElement).value).toBe('22');
expect((screen.getByLabelText('Nameserver 1') as HTMLInputElement).value).toBe(
'ns1.example.com',
);
expect((screen.getByLabelText('Nameserver 2') as HTMLInputElement).value).toBe(
'ns2.example.com',
);
}); });
it('ohne Kontakt der eigenen Firma folgen Technik und Zone dem Inhaber', async () => { it('ohne Kontakt der eigenen Firma folgen Technik und Zone dem Inhaber', async () => {
@@ -245,18 +245,67 @@ describe('RegisterTab — Kontakte und Nameserver', () => {
).toBe(true); ).toBe(true);
}); });
it('Nameserver lassen sich zwischen 2 und 6 hinzufuegen und entfernen', async () => { it('zeigt die AutoDNS-Nameserver schreibgeschuetzt in AutoDNS-Reihenfolge', async () => {
renderTab({ defaultNameServers: [] }); renderTab();
await checkFree(); await checkFree();
await screen.findByLabelText('Inhaber'); await screen.findByLabelText('Inhaber');
expect(screen.queryByRole('button', { name: 'Entfernen' })).toBeNull(); const items = await screen.findAllByRole('listitem');
for (let i = 0; i < 4; i += 1) { expect(items.map((li) => li.textContent)).toEqual([
fireEvent.click(screen.getByRole('button', { name: 'Nameserver hinzufügen' })); 'Nameserver 1z.example.net',
} 'Nameserver 2b.example.org',
'Nameserver 3a.example.org',
]);
expect(screen.queryByLabelText('Nameserver 1')).toBeNull();
expect(screen.queryByRole('textbox', { name: /Nameserver/ })).toBeNull();
expect(screen.queryByRole('button', { name: 'Nameserver hinzufügen' })).toBeNull(); expect(screen.queryByRole('button', { name: 'Nameserver hinzufügen' })).toBeNull();
expect(screen.getByLabelText('Nameserver 6')).toBeTruthy(); });
fireEvent.click(screen.getAllByRole('button', { name: 'Entfernen' })[0]);
expect(screen.queryByLabelText('Nameserver 6')).toBeNull(); it('keine Nameserver in AutoDNS: Hinweis, Zusammenfassung gesperrt, kein Entwurf', async () => {
mockNameServers.mockRejectedValue(
new DomainsRequestError(409, 'noDefaultNameServers', 'Serverseitiger Text.'),
);
renderTab();
expect((await screen.findByRole('alert')).textContent).toContain(
'In AutoDNS sind keine Standard-Nameserver hinterlegt',
);
await checkFree();
const owner = await screen.findByLabelText('Inhaber');
await waitFor(() => expect(mockListContacts).toHaveBeenCalled());
fireEvent.change(owner, { target: { value: '11' } });
const button = screen.getByRole('button', {
name: 'Zusammenfassung anzeigen',
}) as HTMLButtonElement;
expect(button.disabled).toBe(true);
fireEvent.click(button);
expect(mockCreate).not.toHaveBeenCalled();
});
it('AutoDNS nicht lesbar: Hinweis mit Servertext; "Erneut aus AutoDNS lesen" schaltet frei', async () => {
mockNameServers.mockRejectedValueOnce(
new DomainsRequestError(502, 'autodnsAuth', 'Anmeldung bei AutoDNS fehlgeschlagen.'),
);
renderTab();
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain(
'Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen',
);
expect(alert.textContent).toContain('Anmeldung bei AutoDNS fehlgeschlagen.');
await checkFree();
const owner = await screen.findByLabelText('Inhaber');
await waitFor(() => expect(mockListContacts).toHaveBeenCalled());
fireEvent.change(owner, { target: { value: '11' } });
expect(
(screen.getByRole('button', { name: 'Zusammenfassung anzeigen' }) as HTMLButtonElement)
.disabled,
).toBe(true);
fireEvent.click(screen.getByRole('button', { name: 'Erneut aus AutoDNS lesen' }));
await waitFor(() => expect(mockNameServers).toHaveBeenCalledTimes(2));
await screen.findByText('z.example.net');
expect(screen.queryByRole('alert')).toBeNull();
expect(
(screen.getByRole('button', { name: 'Zusammenfassung anzeigen' }) as HTMLButtonElement)
.disabled,
).toBe(false);
}); });
}); });
@@ -270,14 +319,13 @@ describe('RegisterTab — Zusammenfassung und verbindliche Bestaetigung', () =>
adminContactId: 11, adminContactId: 11,
techContactId: 22, techContactId: 22,
zoneContactId: 22, zoneContactId: 22,
nameServers: ['ns1.example.com', 'ns2.example.com'],
}); });
expect(screen.getByTestId('domains-environment-badge').textContent).toBe( expect(screen.getByTestId('domains-environment-badge').textContent).toBe(
'Demo-System (Testbetrieb)', 'Demo-System (Testbetrieb)',
); );
expect(screen.getByText('1 Jahr')).toBeTruthy(); expect(screen.getByText('1 Jahr')).toBeTruthy();
expect(screen.getByText(/4,90/)).toBeTruthy(); expect(screen.getByText(/4,90/)).toBeTruthy();
expect(screen.getByText('ns1.example.com, ns2.example.com')).toBeTruthy(); expect(screen.getByText('z.example.net, b.example.org, a.example.org')).toBeTruthy();
expect( expect(
screen.getByText('Ich bestätige die Registrierung im Demo-System von AutoDNS (Testbetrieb).'), screen.getByText('Ich bestätige die Registrierung im Demo-System von AutoDNS (Testbetrieb).'),
).toBeTruthy(); ).toBeTruthy();
@@ -16,6 +16,7 @@ import {
type DomainsOrder, type DomainsOrder,
DomainsRequestError, DomainsRequestError,
type DomainsStatus, type DomainsStatus,
getNameServers,
listContacts, listContacts,
type OrderPrice, type OrderPrice,
type OrderSummary, type OrderSummary,
@@ -24,7 +25,13 @@ import {
import { EnvironmentBadge } from './EnvironmentBadge'; import { EnvironmentBadge } from './EnvironmentBadge';
const MIN_NAMESERVERS = 2; const MIN_NAMESERVERS = 2;
const MAX_NAMESERVERS = 6;
/** Standard-Nameserver aus AutoDNS: wird gelesen, ist da, fehlt dort oder liess sich nicht lesen. */
type NameServerState =
| { kind: 'loading' }
| { kind: 'ok'; list: string[] }
| { kind: 'missing' }
| { kind: 'unreadable'; detail: string };
type Role = 'owner' | 'admin' | 'tech' | 'zone'; type Role = 'owner' | 'admin' | 'tech' | 'zone';
const ROLES: Role[] = ['owner', 'admin', 'tech', 'zone']; const ROLES: Role[] = ['owner', 'admin', 'tech', 'zone'];
@@ -39,15 +46,12 @@ export function formatPrice(price: OrderPrice, locale: string): string {
} }
} }
function initialNameServers(defaults: string[]): string[] {
const list = defaults.slice(0, MAX_NAMESERVERS);
while (list.length < MIN_NAMESERVERS) list.push('');
return list;
}
/** /**
* Reiter "Registrieren" (L-04): Verfuegbarkeit -> Kontakte und Nameserver -> * Reiter "Registrieren" (L-04): Verfuegbarkeit -> Kontakte (Nameserver nur zur
* Zusammenfassung -> verbindliche Bestaetigung. Der Knopf "Jetzt verbindlich * Kontrolle) -> Zusammenfassung -> verbindliche Bestaetigung. Die Nameserver
* stammen aus dem AutoDNS-Benutzerprofil, werden schreibgeschuetzt gezeigt und
* vom Server beim Entwurf selbst gelesen; fehlen sie, ist die Registrierung
* gesperrt (h3t). Der Knopf "Jetzt verbindlich
* registrieren" ist nur Komfort-Schutz gegen Doppelklick (`useRef` sperrt noch * registrieren" ist nur Komfort-Schutz gegen Doppelklick (`useRef` sperrt noch
* vor dem naechsten Zeichnen); die eigentliche Sperre ist der atomare Anspruch * vor dem naechsten Zeichnen); die eigentliche Sperre ist der atomare Anspruch
* auf dem Server. Nach einem Fehler beim Abschicken bleibt der Knopf gesperrt, * auf dem Server. Nach einem Fehler beim Abschicken bleibt der Knopf gesperrt,
@@ -75,9 +79,8 @@ export function RegisterTab({
const [contacts, setContacts] = useState<DomainsContact[] | null>(null); const [contacts, setContacts] = useState<DomainsContact[] | null>(null);
const [contactsLoading, setContactsLoading] = useState(false); const [contactsLoading, setContactsLoading] = useState(false);
const [chosen, setChosen] = useState<Partial<Record<Role, number>>>({}); const [chosen, setChosen] = useState<Partial<Record<Role, number>>>({});
const [nameServers, setNameServers] = useState<string[]>(() => const [nameServerState, setNameServerState] = useState<NameServerState>({ kind: 'loading' });
initialNameServers(status.defaultNameServers), const [nameServerReload, setNameServerReload] = useState(0);
);
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [summary, setSummary] = useState<OrderSummary | null>(null); const [summary, setSummary] = useState<OrderSummary | null>(null);
@@ -110,6 +113,31 @@ export function RegisterTab({
}; };
}, [available, contacts, tc]); }, [available, contacts, tc]);
// Standard-Nameserver aus AutoDNS lesen: beim Oeffnen, beim Systemwechsel und auf Wunsch.
// biome-ignore lint/correctness/useExhaustiveDependencies: Systemwechsel und Neuladen loesen das Lesen bewusst aus
useEffect(() => {
let alive = true;
setNameServerState({ kind: 'loading' });
getNameServers()
.then((res) => {
if (alive) setNameServerState({ kind: 'ok', list: res.nameServers });
})
.catch((e) => {
if (!alive) return;
if (e instanceof DomainsRequestError && e.code === 'noDefaultNameServers') {
setNameServerState({ kind: 'missing' });
} else {
setNameServerState({
kind: 'unreadable',
detail: e instanceof DomainsRequestError ? e.message : tc('requestFailed'),
});
}
});
return () => {
alive = false;
};
}, [status.environment, nameServerReload, tc]);
const ownCompanyId = customers.find((c) => c.isOwnCompany)?.id ?? null; const ownCompanyId = customers.find((c) => c.isOwnCompany)?.id ?? null;
const ownCompanyContact = useMemo( const ownCompanyContact = useMemo(
() => (ownCompanyId ? contacts?.find((c) => c.customerId === ownCompanyId) : undefined), () => (ownCompanyId ? contacts?.find((c) => c.customerId === ownCompanyId) : undefined),
@@ -127,12 +155,12 @@ export function RegisterTab({
const groups = useMemo(() => groupByCustomer(contacts ?? [], customers), [contacts, customers]); const groups = useMemo(() => groupByCustomer(contacts ?? [], customers), [contacts, customers]);
const filledNameServers = nameServers.map((n) => n.trim()).filter((n) => n.length > 0);
const contactIds = ROLES.map(effective); const contactIds = ROLES.map(effective);
const canSummarize = const canSummarize =
available && available &&
contactIds.every((id) => id !== null) && contactIds.every((id) => id !== null) &&
filledNameServers.length >= MIN_NAMESERVERS && nameServerState.kind === 'ok' &&
nameServerState.list.length >= MIN_NAMESERVERS &&
!creating; !creating;
function resetAll() { function resetAll() {
@@ -141,7 +169,6 @@ export function RegisterTab({
setAvailability(null); setAvailability(null);
setError(null); setError(null);
setChosen({}); setChosen({});
setNameServers(initialNameServers(status.defaultNameServers));
setSummary(null); setSummary(null);
setConfirmed(false); setConfirmed(false);
setSubmitting(false); setSubmitting(false);
@@ -176,7 +203,6 @@ export function RegisterTab({
adminContactId, adminContactId,
techContactId, techContactId,
zoneContactId, zoneContactId,
nameServers: filledNameServers,
}), }),
); );
setConfirmed(false); setConfirmed(false);
@@ -348,6 +374,25 @@ export function RegisterTab({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{(nameServerState.kind === 'missing' || nameServerState.kind === 'unreadable') && (
<div role="alert" className="rounded-lg border border-destructive/40 p-4 text-sm">
<p className="text-foreground">
{nameServerState.kind === 'missing'
? t('nameServersMissing')
: t('nameServersUnreadable')}
</p>
{nameServerState.kind === 'unreadable' && (
<p className="mt-1 text-muted-foreground">{nameServerState.detail}</p>
)}
<button
type="button"
className={`${SECONDARY_BUTTON} mt-3`}
onClick={() => setNameServerReload((n) => n + 1)}
>
{t('nameServersRetry')}
</button>
</div>
)}
<SettingsSection title={t('domainLabel')} description={t('intro')}> <SettingsSection title={t('domainLabel')} description={t('intro')}>
<div className="flex flex-wrap items-end gap-3"> <div className="flex flex-wrap items-end gap-3">
<div className="min-w-56 flex-1"> <div className="min-w-56 flex-1">
@@ -472,45 +517,24 @@ export function RegisterTab({
</button> </button>
} }
> >
<div className="space-y-2"> {nameServerState.kind === 'loading' && (
{nameServers.map((value, index) => { <p className="text-sm text-muted-foreground">{t('nameServersLoading')}</p>
const label = t('nameServer', { number: index + 1 });
return (
// biome-ignore lint/suspicious/noArrayIndexKey: feste, nummerierte Eingabefelder
<div key={index} className="flex items-center gap-2">
<input
type="text"
aria-label={label}
className={INPUT_CLASS}
value={value}
onChange={(e) =>
setNameServers((prev) =>
prev.map((n, i) => (i === index ? e.target.value : n)),
)
}
/>
{nameServers.length > MIN_NAMESERVERS && (
<button
type="button"
className={SECONDARY_BUTTON}
onClick={() => setNameServers((prev) => prev.filter((_, i) => i !== index))}
>
{t('removeNameServer')}
</button>
)} )}
</div> {nameServerState.kind === 'ok' && (
); <ol className="space-y-1 text-sm">
})} {nameServerState.list.map((host, index) => (
{nameServers.length < MAX_NAMESERVERS && ( <li key={host} className="flex gap-3">
<button <span className="w-28 text-muted-foreground">
type="button" {t('nameServer', { number: index + 1 })}
className={SECONDARY_BUTTON} </span>
onClick={() => setNameServers((prev) => [...prev, ''])} <span className="text-foreground">{host}</span>
> </li>
{t('addNameServer')} ))}
</button> </ol>
)}
{(nameServerState.kind === 'missing' || nameServerState.kind === 'unreadable') && (
<p className="text-sm text-muted-foreground">{t('nameServersBlocked')}</p>
)} )}
</div>
</SettingsSection> </SettingsSection>
</> </>
)} )}
+11
View File
@@ -314,9 +314,20 @@ export interface CreateOrderInput {
adminContactId: number; adminContactId: number;
techContactId: number; techContactId: number;
zoneContactId: number; zoneContactId: number;
// Die Nameserver liest der Server selbst aus dem AutoDNS-Profil (h3t).
}
/** Standard-Nameserver aus dem AutoDNS-Benutzerprofil, in der Reihenfolge von AutoDNS. */
export interface DomainsNameServers {
environment: DomainsEnvironment;
nameServers: string[]; nameServers: string[];
} }
/** Liest die Standard-Nameserver aus AutoDNS (nur zur Anzeige; der Server liest sie beim Entwurf neu). */
export function getNameServers(): Promise<DomainsNameServers> {
return request<DomainsNameServers>('/name-servers');
}
export function checkAvailability(domain: string): Promise<AvailabilityResult> { export function checkAvailability(domain: string): Promise<AvailabilityResult> {
return request<AvailabilityResult>('/availability', { method: 'POST', json: { domain } }); return request<AvailabilityResult>('/availability', { method: 'POST', json: { domain } });
} }
+7 -4
View File
@@ -2209,7 +2209,7 @@
"confirmDeleteYes": "Endgültig löschen" "confirmDeleteYes": "Endgültig löschen"
}, },
"register": { "register": {
"intro": "Prüfen Sie, ob eine Domain frei ist, wählen Sie die Kontakte und Nameserver und registrieren Sie die Domain verbindlich bei AutoDNS.", "intro": "Prüfen Sie, ob eine Domain frei ist, wählen Sie die Kontakte und registrieren Sie die Domain verbindlich bei AutoDNS. Die Nameserver übernimmt Tessera aus AutoDNS.",
"domainLabel": "Domain", "domainLabel": "Domain",
"domainPlaceholder": "beispiel.de", "domainPlaceholder": "beispiel.de",
"check": "Verfügbarkeit prüfen", "check": "Verfügbarkeit prüfen",
@@ -2231,10 +2231,13 @@
"zone": "Zonenkontakt" "zone": "Zonenkontakt"
}, },
"nameServersTitle": "Nameserver", "nameServersTitle": "Nameserver",
"nameServersDescription": "Mindestens zwei, höchstens sechs. Die Nameserver müssen bei Ihrem Anbieter bereits eingerichtet sein, sonst scheitert die Registrierung.", "nameServersDescription": "Diese Standard-Nameserver sind in AutoDNS hinterlegt. Tessera verwendet sie unverändert und in dieser Reihenfolge; ändern lassen sie sich nur in AutoDNS.",
"nameServer": "Nameserver {number}", "nameServer": "Nameserver {number}",
"addNameServer": "Nameserver hinzufügen", "nameServersLoading": "Nameserver werden aus AutoDNS gelesen …",
"removeNameServer": "Entfernen", "nameServersMissing": "In AutoDNS sind keine Standard-Nameserver hinterlegt. Bitte hinterlegen Sie mindestens zwei Nameserver als Standard in AutoDNS. Bis dahin ist keine Registrierung möglich.",
"nameServersUnreadable": "Tessera konnte die Standard-Nameserver nicht aus AutoDNS lesen. Bis das gelingt, ist keine Registrierung möglich.",
"nameServersRetry": "Erneut aus AutoDNS lesen",
"nameServersBlocked": "Ohne Standard-Nameserver aus AutoDNS ist keine Registrierung möglich – siehe Hinweis oben.",
"showSummary": "Zusammenfassung anzeigen", "showSummary": "Zusammenfassung anzeigen",
"creating": "Wird vorbereitet …", "creating": "Wird vorbereitet …",
"summary": { "summary": {
+7 -4
View File
@@ -2209,7 +2209,7 @@
"confirmDeleteYes": "Delete permanently" "confirmDeleteYes": "Delete permanently"
}, },
"register": { "register": {
"intro": "Check whether a domain is available, choose the contacts and name servers, and register the domain with AutoDNS as a binding order.", "intro": "Check whether a domain is available, choose the contacts, and register the domain with AutoDNS as a binding order. Tessera takes the name servers from AutoDNS.",
"domainLabel": "Domain", "domainLabel": "Domain",
"domainPlaceholder": "example.com", "domainPlaceholder": "example.com",
"check": "Check availability", "check": "Check availability",
@@ -2231,10 +2231,13 @@
"zone": "Zone contact" "zone": "Zone contact"
}, },
"nameServersTitle": "Name servers", "nameServersTitle": "Name servers",
"nameServersDescription": "At least two, at most six. The name servers must already be set up with your provider, otherwise the registration fails.", "nameServersDescription": "These default name servers are stored in AutoDNS. Tessera uses them unchanged and in this order; they can only be changed in AutoDNS.",
"nameServer": "Name server {number}", "nameServer": "Name server {number}",
"addNameServer": "Add name server", "nameServersLoading": "Reading name servers from AutoDNS …",
"removeNameServer": "Remove", "nameServersMissing": "No default name servers are stored in AutoDNS. Please store at least two name servers as the default in AutoDNS. Until then, registration is not possible.",
"nameServersUnreadable": "Tessera could not read the default name servers from AutoDNS. Until that works, registration is not possible.",
"nameServersRetry": "Read from AutoDNS again",
"nameServersBlocked": "Without default name servers from AutoDNS, registration is not possible – see the notice above.",
"showSummary": "Show summary", "showSummary": "Show summary",
"creating": "Preparing …", "creating": "Preparing …",
"summary": { "summary": {