chore: update/make more clear connect flow
This commit is contained in:
@@ -98,6 +98,11 @@ export function upsertInstance(url: string, name?: string): Instance {
|
|||||||
return inst;
|
return inst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Clear a backend's stored session without forgetting the instance itself. */
|
||||||
|
export function clearInstanceAuth(id: string): void {
|
||||||
|
localStorage.removeItem(scopedKey('auth', id));
|
||||||
|
}
|
||||||
|
|
||||||
/** Remove a backend and wipe every scoped key it owns. */
|
/** Remove a backend and wipe every scoped key it owns. */
|
||||||
export function removeInstance(id: string): void {
|
export function removeInstance(id: string): void {
|
||||||
writeRegistry(readRegistry().filter((i) => i.id !== id));
|
writeRegistry(readRegistry().filter((i) => i.id !== id));
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query';
|
||||||
import { Card, TextField, Button, Callout, Badge } from '@olly/modern-sk';
|
import { Card, TextField, Button, Callout, Badge, Dialog } from '@olly/modern-sk';
|
||||||
import { Icon } from '../../components/common/Icon';
|
import { Icon } from '../../components/common/Icon';
|
||||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
||||||
|
import { useConnectionStatus } from '../../hooks/useConnectionStatus';
|
||||||
import { setTokens, setUser } from '../../store/slices/auth';
|
import { setTokens, setUser } from '../../store/slices/auth';
|
||||||
import { setApiBaseUrl } from '../../config/runtime-config';
|
|
||||||
import {
|
import {
|
||||||
useLoginMutation,
|
useLoginMutation,
|
||||||
useRegisterMutation,
|
useRegisterMutation,
|
||||||
@@ -17,10 +17,27 @@ import {
|
|||||||
getActiveInstanceId,
|
getActiveInstanceId,
|
||||||
setActiveInstanceId,
|
setActiveInstanceId,
|
||||||
removeInstance,
|
removeInstance,
|
||||||
|
clearInstanceAuth,
|
||||||
|
upsertInstance,
|
||||||
|
type Instance,
|
||||||
} from '../../config/instances';
|
} from '../../config/instances';
|
||||||
|
|
||||||
type Mode = 'login' | 'register';
|
type Mode = 'login' | 'register';
|
||||||
|
|
||||||
|
const HEALTH_VARIANTS = {
|
||||||
|
connected: 'lime',
|
||||||
|
connecting: 'neutral',
|
||||||
|
disconnected: 'ember',
|
||||||
|
error: 'ember',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const HEALTH_KEY = {
|
||||||
|
connected: 'conn.connected',
|
||||||
|
connecting: 'conn.connecting',
|
||||||
|
disconnected: 'conn.disconnected',
|
||||||
|
error: 'conn.error',
|
||||||
|
} as const;
|
||||||
|
|
||||||
/** Map an RTKQ login failure to a user-facing i18n key. */
|
/** Map an RTKQ login failure to a user-facing i18n key. */
|
||||||
function resolveLoginError(err: unknown): string {
|
function resolveLoginError(err: unknown): string {
|
||||||
const e = err as FetchBaseQueryError | undefined;
|
const e = err as FetchBaseQueryError | undefined;
|
||||||
@@ -43,6 +60,121 @@ function resolveRegisterError(err: unknown): string {
|
|||||||
return 'connect.errors.registerFailed';
|
return 'connect.errors.registerFailed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function InstanceRow({
|
||||||
|
inst,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
onLogout,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
inst: Instance;
|
||||||
|
selected: boolean;
|
||||||
|
onSelect: () => void;
|
||||||
|
onLogout: () => void;
|
||||||
|
onRemove: () => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const status = useConnectionStatus(inst.baseUrl);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.625rem',
|
||||||
|
padding: '0.375rem 0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Badge variant={HEALTH_VARIANTS[status]} dot>
|
||||||
|
{t(HEALTH_KEY[status])}
|
||||||
|
</Badge>
|
||||||
|
<div style={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
color: 'var(--color-text-1)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inst.name}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: 'var(--color-text-3)',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{inst.baseUrl}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selected ? (
|
||||||
|
<Badge variant="outline">{t('connect.domains.selected')}</Badge>
|
||||||
|
) : (
|
||||||
|
<Button variant="ghost" size="sm" onClick={onSelect}>
|
||||||
|
{t('connect.domains.use')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Dialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
title={t('connect.removeDialog.title')}
|
||||||
|
description={t('connect.removeDialog.description', {
|
||||||
|
name: inst.name,
|
||||||
|
})}
|
||||||
|
trigger={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="iconbtn sm"
|
||||||
|
title={t('connect.domains.forgetTitle')}
|
||||||
|
>
|
||||||
|
<Icon name="trash" />
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div
|
||||||
|
style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem' }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setDialogOpen(false)}
|
||||||
|
>
|
||||||
|
{t('connect.removeDialog.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
onLogout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('connect.removeDialog.logout')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ember"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
onRemove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('connect.removeDialog.removeAndLogout')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ConnectPage() {
|
export function ConnectPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -50,10 +182,15 @@ export function ConnectPage() {
|
|||||||
|
|
||||||
const [rev, setRev] = useState(0);
|
const [rev, setRev] = useState(0);
|
||||||
const instances = listInstances();
|
const instances = listInstances();
|
||||||
const activeId = getActiveInstanceId();
|
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(() =>
|
||||||
|
getActiveInstanceId(),
|
||||||
|
);
|
||||||
|
const selectedInstance = instances.find((i) => i.id === selectedId) ?? null;
|
||||||
|
|
||||||
|
const [addUrl, setAddUrl] = useState('https://');
|
||||||
|
|
||||||
const [mode, setMode] = useState<Mode>('login');
|
const [mode, setMode] = useState<Mode>('login');
|
||||||
const [apiUrl, setApiUrl] = useState('https://');
|
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -67,23 +204,40 @@ export function ConnectPage() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const switchTo = (id: string) => {
|
// Switching the active instance and reloading lets the app pick the saved
|
||||||
|
// session for that instance back up (if any); if it has none, ProtectedRoute
|
||||||
|
// bounces back here and `selectedId` defaults to it, surfacing the login card.
|
||||||
|
const selectInstance = (id: string) => {
|
||||||
setActiveInstanceId(id);
|
setActiveInstanceId(id);
|
||||||
window.location.assign('/');
|
window.location.assign('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
const forget = (id: string) => {
|
const handleAdd = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const url = addUrl.trim();
|
||||||
|
if (!url || url === 'https://') return;
|
||||||
|
const inst = upsertInstance(url);
|
||||||
|
setActiveInstanceId(inst.id);
|
||||||
|
setAddUrl('https://');
|
||||||
|
setSelectedId(inst.id);
|
||||||
|
setRev((r) => r + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = (id: string) => {
|
||||||
|
clearInstanceAuth(id);
|
||||||
|
setRev((r) => r + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (id: string) => {
|
||||||
removeInstance(id);
|
removeInstance(id);
|
||||||
|
if (selectedId === id) setSelectedId(getActiveInstanceId());
|
||||||
setRev((r) => r + 1);
|
setRev((r) => r + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!selectedInstance) return;
|
||||||
setError(null);
|
setError(null);
|
||||||
// Point the API layer at this backend *before* logging in — baseQuery reads
|
|
||||||
// the active instance's URL at request time. Auth tokens then persist under
|
|
||||||
// that instance's namespace, never bleeding across servers.
|
|
||||||
setApiBaseUrl(apiUrl);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const action =
|
const action =
|
||||||
@@ -145,7 +299,6 @@ export function ConnectPage() {
|
|||||||
<Icon name="vinyl-record" fill /> MCMA
|
<Icon name="vinyl-record" fill /> MCMA
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{instances.length > 0 && (
|
|
||||||
<Card>
|
<Card>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -155,82 +308,45 @@ export function ConnectPage() {
|
|||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{instances.length > 0 && (
|
||||||
|
<>
|
||||||
<span className="msk-label" style={{ marginBottom: '0.25rem' }}>
|
<span className="msk-label" style={{ marginBottom: '0.25rem' }}>
|
||||||
{t('connect.savedInstances')}
|
{t('connect.domains.title')}
|
||||||
</span>
|
</span>
|
||||||
{instances.map((inst) => (
|
{instances.map((inst) => (
|
||||||
<div
|
<InstanceRow
|
||||||
key={inst.id}
|
key={inst.id}
|
||||||
|
inst={inst}
|
||||||
|
selected={inst.id === selectedId}
|
||||||
|
onSelect={() => selectInstance(inst.id)}
|
||||||
|
onLogout={() => handleLogout(inst.id)}
|
||||||
|
onRemove={() => handleRemove(inst.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={handleAdd}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
gap: '0.5rem',
|
||||||
gap: '0.625rem',
|
marginTop: instances.length > 0 ? '0.5rem' : 0,
|
||||||
padding: '0.375rem 0',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<TextField
|
||||||
className={`led ${inst.id === activeId ? 'online' : 'offline'}`}
|
value={addUrl}
|
||||||
style={{
|
onChange={(e) => setAddUrl(e.target.value)}
|
||||||
width: 8,
|
placeholder={t('connect.domains.addPlaceholder')}
|
||||||
height: 8,
|
style={{ flex: 1 }}
|
||||||
borderRadius: '50%',
|
|
||||||
background:
|
|
||||||
inst.id === activeId ? 'var(--lime)' : 'var(--fg-3)',
|
|
||||||
boxShadow:
|
|
||||||
inst.id === activeId ? '0 0 6px var(--lime)' : 'none',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div style={{ minWidth: 0, flex: 1 }}>
|
<Button type="submit" variant="primary">
|
||||||
<div
|
<Icon name="plus" /> {t('connect.domains.addButton')}
|
||||||
style={{
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: 'var(--color-text-1)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{inst.name}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: 'var(--color-text-3)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{inst.baseUrl}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{inst.id === activeId ? (
|
|
||||||
<Badge variant="lime">{t('connect.active')}</Badge>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => switchTo(inst.id)}
|
|
||||||
>
|
|
||||||
{t('connect.use')}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</form>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="iconbtn sm"
|
|
||||||
onClick={() => forget(inst.id)}
|
|
||||||
title={t('connect.forgetTitle')}
|
|
||||||
>
|
|
||||||
<Icon name="trash" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
|
||||||
|
|
||||||
|
{selectedInstance && (
|
||||||
<Card>
|
<Card>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
@@ -243,20 +359,13 @@ export function ConnectPage() {
|
|||||||
>
|
>
|
||||||
<span className="msk-label">
|
<span className="msk-label">
|
||||||
{mode === 'register'
|
{mode === 'register'
|
||||||
? t('connect.form.registerTitle')
|
? t('connect.login.registerTitle', {
|
||||||
: t('connect.form.title')}
|
name: selectedInstance.name,
|
||||||
|
})
|
||||||
|
: t('connect.login.title', { name: selectedInstance.name })}
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelStyle}>{t('connect.form.serverUrl')}</label>
|
<label style={labelStyle}>{t('connect.login.username')}</label>
|
||||||
<TextField
|
|
||||||
value={apiUrl}
|
|
||||||
onChange={(e) => setApiUrl(e.target.value)}
|
|
||||||
placeholder="https://your-server.example.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style={labelStyle}>{t('connect.form.username')}</label>
|
|
||||||
<TextField
|
<TextField
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
@@ -266,7 +375,7 @@ export function ConnectPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style={labelStyle}>{t('connect.form.password')}</label>
|
<label style={labelStyle}>{t('connect.login.password')}</label>
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
@@ -286,7 +395,7 @@ export function ConnectPage() {
|
|||||||
marginTop: '0.375rem',
|
marginTop: '0.375rem',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('connect.form.passwordHint')}
|
{t('connect.login.passwordHint')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -299,11 +408,11 @@ export function ConnectPage() {
|
|||||||
>
|
>
|
||||||
{isLoading
|
{isLoading
|
||||||
? mode === 'register'
|
? mode === 'register'
|
||||||
? t('connect.form.registering')
|
? t('connect.login.registering')
|
||||||
: t('connect.form.submitting')
|
: t('connect.login.submitting')
|
||||||
: mode === 'register'
|
: mode === 'register'
|
||||||
? t('connect.form.registerSubmit')
|
? t('connect.login.registerSubmit')
|
||||||
: t('connect.form.submit')}
|
: t('connect.login.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{REGISTRATION_ENABLED && (
|
{REGISTRATION_ENABLED && (
|
||||||
@@ -316,26 +425,26 @@ export function ConnectPage() {
|
|||||||
>
|
>
|
||||||
{mode === 'register' ? (
|
{mode === 'register' ? (
|
||||||
<>
|
<>
|
||||||
{t('connect.form.haveAccount')}{' '}
|
{t('connect.login.haveAccount')}{' '}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => switchMode('login')}
|
onClick={() => switchMode('login')}
|
||||||
>
|
>
|
||||||
{t('connect.form.signInLink')}
|
{t('connect.login.signInLink')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{t('connect.form.noAccount')}{' '}
|
{t('connect.login.noAccount')}{' '}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => switchMode('register')}
|
onClick={() => switchMode('register')}
|
||||||
>
|
>
|
||||||
{t('connect.form.registerLink')}
|
{t('connect.login.registerLink')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -343,6 +452,7 @@ export function ConnectPage() {
|
|||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { getApiBaseUrl } from '../config/runtime-config';
|
|||||||
|
|
||||||
type ConnectionStatus = 'connected' | 'connecting' | 'disconnected' | 'error';
|
type ConnectionStatus = 'connected' | 'connecting' | 'disconnected' | 'error';
|
||||||
|
|
||||||
export function useConnectionStatus() {
|
/** Pings `${baseUrl}/health` (defaults to the active instance's base URL). */
|
||||||
|
export function useConnectionStatus(baseUrl?: string) {
|
||||||
|
const url = baseUrl ?? getApiBaseUrl();
|
||||||
const [status, setStatus] = useState<ConnectionStatus>('connecting');
|
const [status, setStatus] = useState<ConnectionStatus>('connecting');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -13,7 +15,7 @@ export function useConnectionStatus() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setStatus('connecting');
|
setStatus('connecting');
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${getApiBaseUrl()}/health`, {
|
const res = await fetch(`${url}/health`, {
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
if (!cancelled) setStatus(res.ok ? 'connected' : 'error');
|
if (!cancelled) setStatus(res.ok ? 'connected' : 'error');
|
||||||
@@ -30,7 +32,7 @@ export function useConnectionStatus() {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [url]);
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-12
@@ -25,25 +25,36 @@ const en = {
|
|||||||
signOut: 'Sign out',
|
signOut: 'Sign out',
|
||||||
},
|
},
|
||||||
connect: {
|
connect: {
|
||||||
savedInstances: 'Saved instances',
|
domains: {
|
||||||
active: 'active',
|
title: 'Saved instances',
|
||||||
|
addPlaceholder: 'https://your-server.example.com',
|
||||||
|
addButton: 'Add instance',
|
||||||
|
selected: 'Selected',
|
||||||
use: 'Use',
|
use: 'Use',
|
||||||
forgetTitle: 'Forget this instance',
|
forgetTitle: 'Remove this instance',
|
||||||
form: {
|
},
|
||||||
title: 'Connect to a backend',
|
removeDialog: {
|
||||||
registerTitle: 'Create an account',
|
title: 'Remove cached data?',
|
||||||
serverUrl: 'Server URL',
|
description:
|
||||||
|
'This removes "{{name}}" from your saved instances and clears its cached data on this device.',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
logout: 'Just log out',
|
||||||
|
removeAndLogout: 'Remove data & log out',
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Log in to {{name}}',
|
||||||
|
registerTitle: 'Sign up for {{name}}',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
passwordHint: 'At least 8 characters.',
|
passwordHint: 'At least 8 characters.',
|
||||||
submit: 'Connect',
|
submit: 'Log in',
|
||||||
submitting: 'Connecting…',
|
submitting: 'Logging in…',
|
||||||
registerSubmit: 'Create account',
|
registerSubmit: 'Sign up',
|
||||||
registering: 'Creating account…',
|
registering: 'Signing up…',
|
||||||
noAccount: "Don't have an account?",
|
noAccount: "Don't have an account?",
|
||||||
registerLink: 'Sign up',
|
registerLink: 'Sign up',
|
||||||
haveAccount: 'Already have an account?',
|
haveAccount: 'Already have an account?',
|
||||||
signInLink: 'Sign in',
|
signInLink: 'Log in',
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
unreachable:
|
unreachable:
|
||||||
|
|||||||
+22
-11
@@ -27,21 +27,32 @@ const ru: Translations = {
|
|||||||
signOut: 'Выйти',
|
signOut: 'Выйти',
|
||||||
},
|
},
|
||||||
connect: {
|
connect: {
|
||||||
savedInstances: 'Сохранённые серверы',
|
domains: {
|
||||||
active: 'активный',
|
title: 'Сохранённые серверы',
|
||||||
|
addPlaceholder: 'https://your-server.example.com',
|
||||||
|
addButton: 'Добавить сервер',
|
||||||
|
selected: 'Выбран',
|
||||||
use: 'Выбрать',
|
use: 'Выбрать',
|
||||||
forgetTitle: 'Забыть этот сервер',
|
forgetTitle: 'Удалить этот сервер',
|
||||||
form: {
|
},
|
||||||
title: 'Подключиться к серверу',
|
removeDialog: {
|
||||||
registerTitle: 'Создать аккаунт',
|
title: 'Удалить локальные данные?',
|
||||||
serverUrl: 'URL сервера',
|
description:
|
||||||
|
'Сервер «{{name}}» будет удалён из сохранённых, а его данные на этом устройстве — очищены.',
|
||||||
|
cancel: 'Отмена',
|
||||||
|
logout: 'Просто выйти',
|
||||||
|
removeAndLogout: 'Удалить данные и выйти',
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Вход в {{name}}',
|
||||||
|
registerTitle: 'Регистрация на {{name}}',
|
||||||
username: 'Имя пользователя',
|
username: 'Имя пользователя',
|
||||||
password: 'Пароль',
|
password: 'Пароль',
|
||||||
passwordHint: 'Не менее 8 символов.',
|
passwordHint: 'Не менее 8 символов.',
|
||||||
submit: 'Подключиться',
|
submit: 'Войти',
|
||||||
submitting: 'Подключение…',
|
submitting: 'Вход…',
|
||||||
registerSubmit: 'Создать аккаунт',
|
registerSubmit: 'Зарегистрироваться',
|
||||||
registering: 'Создание аккаунта…',
|
registering: 'Регистрация…',
|
||||||
noAccount: 'Нет аккаунта?',
|
noAccount: 'Нет аккаунта?',
|
||||||
registerLink: 'Зарегистрироваться',
|
registerLink: 'Зарегистрироваться',
|
||||||
haveAccount: 'Уже есть аккаунт?',
|
haveAccount: 'Уже есть аккаунт?',
|
||||||
|
|||||||
Reference in New Issue
Block a user