mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 22:16:26 +00:00
feat(planner): ask before a stray click throws away an edited place or booking (#2253)
This commit is contained in:
@@ -995,6 +995,8 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
|
||||
labelledBy={titleId}
|
||||
width={width}
|
||||
align="top"
|
||||
// A stray click beside the editor asks before the typing is lost (#2253).
|
||||
discardGuard={{ form, files: S.pendingFiles.length }}
|
||||
onPaste={S.handlePaste}
|
||||
header={header}
|
||||
footer={footer}
|
||||
|
||||
@@ -439,6 +439,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
|
||||
labelledBy={titleId}
|
||||
width="editor"
|
||||
align="top"
|
||||
// A stray click beside the editor asks before the typing is lost (#2253).
|
||||
discardGuard={{ form, files: pendingFiles.length, travelers: [...travelerIds] }}
|
||||
onSubmit={handleSubmit}
|
||||
header={header}
|
||||
footer={footer}
|
||||
|
||||
@@ -1055,6 +1055,8 @@ export function TransportModal({ isOpen, onClose, onSave, reservation, days, sel
|
||||
width="editor"
|
||||
align="top"
|
||||
blocked={showDeleteConfirm}
|
||||
// A stray click beside the editor asks before the typing is lost (#2253).
|
||||
discardGuard={{ form, waypoints, trainWaypoints, carStops, files: pendingFiles.length, travelers: [...travelerIds] }}
|
||||
onSubmit={automated ? undefined : handleSubmit}
|
||||
header={header}
|
||||
footer={footer}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// FE-COMP-DIALOGSHELL-001 to FE-COMP-DIALOGSHELL-013
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useState } from 'react';
|
||||
import { render, screen, fireEvent } from '../../../tests/helpers/render';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { resetBodyScrollLock } from '../../utils/bodyScrollLock';
|
||||
@@ -59,6 +60,58 @@ describe('DialogShell', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogShell unsaved-changes question (#2253)', () => {
|
||||
function Editor() {
|
||||
const [name, setName] = useState('');
|
||||
return (
|
||||
<DialogShell onClose={onClose} labelledBy="e-title" discardGuard={{ name }}
|
||||
header={<DialogHeader tile={null} tint={NEUTRAL_TINT} labelId="e-title" onClose={onClose} title="Place" />}>
|
||||
<input aria-label="Name" value={name} onChange={e => setName(e.target.value)} />
|
||||
</DialogShell>
|
||||
);
|
||||
}
|
||||
const backdrop = () => document.querySelector('.trek-modal-backdrop') as HTMLElement;
|
||||
const clickBackdrop = () => { fireEvent.mouseDown(backdrop()); fireEvent.click(backdrop()); };
|
||||
|
||||
it('FE-COMP-DIALOGSHELL-2253-1: an untouched editor closes on the backdrop straight away', () => {
|
||||
render(<Editor />);
|
||||
clickBackdrop();
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('FE-COMP-DIALOGSHELL-2253-2: after typing, the backdrop asks; keep editing stays, discard closes', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Editor />);
|
||||
await user.type(screen.getByLabelText('Name'), 'Cafe');
|
||||
clickBackdrop();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('alertdialog')).toHaveTextContent('Discard your changes?');
|
||||
await user.click(screen.getByRole('button', { name: 'Keep editing' }));
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Name')).toHaveValue('Cafe');
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
expect(screen.getByRole('alertdialog')).toBeInTheDocument();
|
||||
// Escape on the question only takes the question away.
|
||||
await user.keyboard('{Escape}');
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
|
||||
clickBackdrop();
|
||||
await user.click(screen.getByRole('button', { name: 'Discard' }));
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('FE-COMP-DIALOGSHELL-2253-3: typing back to where it started closes without asking', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<Editor />);
|
||||
await user.type(screen.getByLabelText('Name'), 'a');
|
||||
await user.clear(screen.getByLabelText('Name'));
|
||||
clickBackdrop();
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DialogHeader', () => {
|
||||
it('FE-COMP-DIALOGSHELL-004: a plain title is the heading the dialog is named by', () => {
|
||||
render(<DialogHeader tile={<DialogTile><span data-testid="tile-icon" /></DialogTile>} tint={NEUTRAL_TINT} labelId="t1" onClose={onClose} title="Export" />);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, type ButtonHTMLAttributes, type ClipboardEvent, type CSSProperties, type FormEvent, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState, type ButtonHTMLAttributes, type ClipboardEvent, type CSSProperties, type FormEvent, type KeyboardEvent as ReactKeyboardEvent, type ReactNode } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Trash2, X } from 'lucide-react'
|
||||
import { useTranslation } from '../../i18n'
|
||||
@@ -39,6 +39,20 @@ export interface DialogShellProps {
|
||||
bodyClassName?: string
|
||||
/** A paste anywhere in the panel, head band included (a note takes pasted images from its title too). */
|
||||
onPaste?: (e: ClipboardEvent<HTMLDivElement>) => void
|
||||
/**
|
||||
* The editor's state, for the unsaved-changes question (#2253). Snapshotted at
|
||||
* the first key or pointer press inside the panel, so whatever the dialog
|
||||
* loaded on open counts as the starting point. A click on the backdrop or
|
||||
* Escape then asks before throwing away a state that differs from it. Must be
|
||||
* JSON-serialisable; pass counts for files and arrays for sets.
|
||||
*/
|
||||
discardGuard?: unknown
|
||||
}
|
||||
|
||||
const DISCARD_BUTTON = 'inline-flex items-center gap-1.5 rounded-[10px] bg-danger px-4 py-2 font-medium text-white hover:opacity-90' // theme-lint-disable: white on the danger fill, as ConfirmDialog draws it
|
||||
|
||||
const snapshot = (value: unknown): string | null => {
|
||||
try { return JSON.stringify(value) ?? null } catch { return null }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,18 +63,40 @@ export function DialogShell({ open = true, ...frame }: DialogShellProps) {
|
||||
return open ? <DialogFrame {...frame} /> : null
|
||||
}
|
||||
|
||||
function DialogFrame({ onClose, labelledBy, width = 'detail', align = 'center', blocked = false, header, footer, onSubmit, children, bodyClassName, onPaste }: Omit<DialogShellProps, 'open'>) {
|
||||
function DialogFrame({ onClose, labelledBy, width = 'detail', align = 'center', blocked = false, header, footer, onSubmit, children, bodyClassName, onPaste, discardGuard }: Omit<DialogShellProps, 'open'>) {
|
||||
const { t } = useTranslation()
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const pressedOn = useRef<EventTarget | null>(null)
|
||||
// Read while rendering, before a field inside can take the focus with autoFocus.
|
||||
const [focusedBefore] = useState(() => document.activeElement)
|
||||
// The unsaved-changes question (#2253): the state at the first touch, and
|
||||
// whether the question is on screen.
|
||||
const baseline = useRef<string | null>(null)
|
||||
const guarded = discardGuard !== undefined
|
||||
const guardRef = useRef(discardGuard)
|
||||
useEffect(() => { guardRef.current = discardGuard })
|
||||
const [asking, setAsking] = useState(false)
|
||||
const markStart = () => { if (guarded && baseline.current === null) baseline.current = snapshot(discardGuard) }
|
||||
// The backdrop and Escape: the two ways out a hand can take by accident.
|
||||
const requestClose = useCallback(() => {
|
||||
if (guardRef.current !== undefined && baseline.current !== null && snapshot(guardRef.current) !== baseline.current) setAsking(true)
|
||||
else onClose()
|
||||
}, [onClose])
|
||||
const held = blocked || asking
|
||||
|
||||
useEffect(() => {
|
||||
if (blocked) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !e.defaultPrevented) onClose() }
|
||||
if (held) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !e.defaultPrevented) requestClose() }
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [blocked, onClose])
|
||||
}, [held, requestClose])
|
||||
|
||||
useEffect(() => {
|
||||
if (!asking) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); setAsking(false) } }
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [asking])
|
||||
|
||||
// The page stays put behind the dialog, the focus moves into it (the first text
|
||||
// field on a desktop, #1302), and whatever opened it gets the focus back.
|
||||
@@ -90,7 +126,7 @@ function DialogFrame({ onClose, labelledBy, width = 'detail', align = 'center',
|
||||
// dragged out of a field must not throw the dialog away.
|
||||
onMouseDown={e => { pressedOn.current = e.target }}
|
||||
onClick={e => {
|
||||
if (!blocked && e.target === e.currentTarget && pressedOn.current === e.currentTarget) onClose()
|
||||
if (!held && e.target === e.currentTarget && pressedOn.current === e.currentTarget) requestClose()
|
||||
pressedOn.current = null
|
||||
}}
|
||||
>
|
||||
@@ -102,12 +138,28 @@ function DialogFrame({ onClose, labelledBy, width = 'detail', align = 'center',
|
||||
aria-labelledby={labelledBy}
|
||||
tabIndex={-1}
|
||||
onPaste={onPaste}
|
||||
onKeyDownCapture={markStart}
|
||||
onPointerDownCapture={markStart}
|
||||
onKeyDown={e => { if (panelRef.current) trapTab(e, panelRef.current) }}
|
||||
className={`trek-modal-enter flex max-h-full w-full ${PANEL_WIDTH[width]} flex-col overflow-hidden rounded-[22px] bg-surface-card shadow-2xl outline-none`}
|
||||
className={`trek-modal-enter relative flex max-h-full w-full ${PANEL_WIDTH[width]} flex-col overflow-hidden rounded-[22px] bg-surface-card shadow-2xl outline-none`}
|
||||
>
|
||||
{header}
|
||||
{body}
|
||||
{footer}
|
||||
{asking && (
|
||||
<div className="trek-backdrop-enter absolute inset-0 z-10 grid place-items-center p-6 backdrop-blur-[2px]" style={{ background: 'color-mix(in srgb, var(--bg-card) 72%, transparent)' }}>
|
||||
<div role="alertdialog" aria-labelledby={`${labelledBy}-discard`} className="trek-modal-enter w-full max-w-[340px] rounded-[18px] border border-edge-faint bg-surface-card p-5 shadow-xl">
|
||||
<div id={`${labelledBy}-discard`} className="font-semibold text-content" style={fs(15, 'subtitle')}>{t('common.unsavedTitle')}</div>
|
||||
<p className="m-0 mt-1.5 text-content-muted" style={fs(12.5, 'body')}>{t('common.unsavedMessage')}</p>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<DialogButton autoFocus onClick={() => setAsking(false)}>{t('common.keepEditing')}</DialogButton>
|
||||
<button type="button" onClick={() => { setAsking(false); onClose() }} className={DISCARD_BUTTON} style={fs(13, 'body')}>
|
||||
{t('common.discard')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'إعادة تسمية',
|
||||
'common.discardChanges': 'تجاهل التغييرات',
|
||||
'common.discard': 'تجاهل',
|
||||
'common.unsavedTitle': 'تجاهل تغييراتك؟',
|
||||
'common.unsavedMessage': 'ما أدخلته هنا لم يُحفظ بعد.',
|
||||
'common.keepEditing': 'متابعة التعديل',
|
||||
'common.name': 'الاسم',
|
||||
'common.email': 'البريد الإلكتروني',
|
||||
'common.password': 'كلمة المرور',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Adını dəyiş',
|
||||
'common.discardChanges': 'Dəyişiklikləri ləğv et',
|
||||
'common.discard': 'İmtina et',
|
||||
'common.unsavedTitle': 'Dəyişikliklər ləğv edilsin?',
|
||||
'common.unsavedMessage': 'Burada daxil etdikləriniz hələ yadda saxlanmayıb.',
|
||||
'common.keepEditing': 'Redaktəyə davam et',
|
||||
'common.name': 'Ad',
|
||||
'common.email': 'E-poçt',
|
||||
'common.password': 'Parol',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Renomear',
|
||||
'common.discardChanges': 'Descartar alterações',
|
||||
'common.discard': 'Descartar',
|
||||
'common.unsavedTitle': 'Descartar suas alterações?',
|
||||
'common.unsavedMessage': 'O que você inseriu aqui ainda não foi salvo.',
|
||||
'common.keepEditing': 'Continuar editando',
|
||||
'common.name': 'Nome',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Senha',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Canviar nom',
|
||||
'common.discardChanges': 'Descartar canvis',
|
||||
'common.discard': 'Descartar',
|
||||
'common.unsavedTitle': 'Vols descartar els canvis?',
|
||||
'common.unsavedMessage': "El que has introduït aquí encara no s'ha desat.",
|
||||
'common.keepEditing': 'Continua editant',
|
||||
'common.name': 'Nom',
|
||||
'common.email': 'Correu',
|
||||
'common.password': 'Contrasenya',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Přejmenovat',
|
||||
'common.discardChanges': 'Zahodit změny',
|
||||
'common.discard': 'Zahodit',
|
||||
'common.unsavedTitle': 'Zahodit změny?',
|
||||
'common.unsavedMessage': 'To, co jste zde zadali, ještě není uložené.',
|
||||
'common.keepEditing': 'Pokračovat v úpravách',
|
||||
'common.name': 'Jméno',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Heslo',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Umbenennen',
|
||||
'common.discardChanges': 'Änderungen verwerfen',
|
||||
'common.discard': 'Verwerfen',
|
||||
'common.unsavedTitle': 'Änderungen verwerfen?',
|
||||
'common.unsavedMessage': 'Was du hier eingegeben hast, ist noch nicht gespeichert.',
|
||||
'common.keepEditing': 'Weiter bearbeiten',
|
||||
'common.name': 'Name',
|
||||
'common.email': 'E-Mail',
|
||||
'common.password': 'Passwort',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Rename',
|
||||
'common.discardChanges': 'Discard Changes',
|
||||
'common.discard': 'Discard',
|
||||
'common.unsavedTitle': 'Discard your changes?',
|
||||
'common.unsavedMessage': 'What you entered here has not been saved yet.',
|
||||
'common.keepEditing': 'Keep editing',
|
||||
'common.name': 'Name',
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Password',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Renombrar',
|
||||
'common.discardChanges': 'Descartar cambios',
|
||||
'common.discard': 'Descartar',
|
||||
'common.unsavedTitle': '¿Descartar los cambios?',
|
||||
'common.unsavedMessage': 'Lo que has introducido aquí aún no se ha guardado.',
|
||||
'common.keepEditing': 'Seguir editando',
|
||||
'common.name': 'Nombre',
|
||||
'common.email': 'Correo',
|
||||
'common.password': 'Contraseña',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Nimeta ümber',
|
||||
'common.discardChanges': 'Loobu muudatustest',
|
||||
'common.discard': 'Loobu',
|
||||
'common.unsavedTitle': 'Kas loobuda muudatustest?',
|
||||
'common.unsavedMessage': 'Siia sisestatu pole veel salvestatud.',
|
||||
'common.keepEditing': 'Jätka muutmist',
|
||||
'common.name': 'Nimi',
|
||||
'common.email': 'E-post',
|
||||
'common.password': 'Parool',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Renommer',
|
||||
'common.discardChanges': 'Ignorer les modifications',
|
||||
'common.discard': 'Ignorer',
|
||||
'common.unsavedTitle': 'Abandonner vos modifications ?',
|
||||
'common.unsavedMessage': "Ce que vous avez saisi ici n'est pas encore enregistré.",
|
||||
'common.keepEditing': 'Continuer la modification',
|
||||
'common.name': 'Nom',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Mot de passe',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Μετονομασία',
|
||||
'common.discardChanges': 'Απόρριψη αλλαγών',
|
||||
'common.discard': 'Απόρριψη',
|
||||
'common.unsavedTitle': 'Απόρριψη των αλλαγών;',
|
||||
'common.unsavedMessage': 'Όσα καταχωρίσατε εδώ δεν έχουν αποθηκευτεί ακόμη.',
|
||||
'common.keepEditing': 'Συνέχεια επεξεργασίας',
|
||||
'common.name': 'Όνομα',
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Κωδικός',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Átnevezés',
|
||||
'common.discardChanges': 'Változtatások elvetése',
|
||||
'common.discard': 'Elveti',
|
||||
'common.unsavedTitle': 'Elveted a módosításokat?',
|
||||
'common.unsavedMessage': 'Amit itt megadtál, még nincs elmentve.',
|
||||
'common.keepEditing': 'Szerkesztés folytatása',
|
||||
'common.name': 'Név',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Jelszó',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Ganti nama',
|
||||
'common.discardChanges': 'Buang perubahan',
|
||||
'common.discard': 'Buang',
|
||||
'common.unsavedTitle': 'Buang perubahan?',
|
||||
'common.unsavedMessage': 'Yang kamu masukkan di sini belum disimpan.',
|
||||
'common.keepEditing': 'Lanjut mengedit',
|
||||
'common.name': 'Nama',
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Kata sandi',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Rinomina',
|
||||
'common.discardChanges': 'Scarta modifiche',
|
||||
'common.discard': 'Scarta',
|
||||
'common.unsavedTitle': 'Scartare le modifiche?',
|
||||
'common.unsavedMessage': 'Quello che hai inserito qui non è ancora stato salvato.',
|
||||
'common.keepEditing': 'Continua a modificare',
|
||||
'common.name': 'Nome',
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Password',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': '名前を変更',
|
||||
'common.discardChanges': '変更をキャンセル',
|
||||
'common.discard': 'キャンセル',
|
||||
'common.unsavedTitle': '変更を破棄しますか?',
|
||||
'common.unsavedMessage': 'ここで入力した内容はまだ保存されていません。',
|
||||
'common.keepEditing': '編集を続ける',
|
||||
'common.name': '名前',
|
||||
'common.email': 'メールアドレス',
|
||||
'common.password': 'パスワード',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': '이름 변경',
|
||||
'common.discardChanges': '변경 사항 취소',
|
||||
'common.discard': '취소',
|
||||
'common.unsavedTitle': '변경 사항을 버릴까요?',
|
||||
'common.unsavedMessage': '여기에 입력한 내용은 아직 저장되지 않았습니다.',
|
||||
'common.keepEditing': '계속 편집',
|
||||
'common.name': '이름',
|
||||
'common.email': '이메일',
|
||||
'common.password': '비밀번호',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Hernoemen',
|
||||
'common.discardChanges': 'Wijzigingen verwerpen',
|
||||
'common.discard': 'Verwerpen',
|
||||
'common.unsavedTitle': 'Wijzigingen weggooien?',
|
||||
'common.unsavedMessage': 'Wat je hier hebt ingevuld, is nog niet opgeslagen.',
|
||||
'common.keepEditing': 'Verder bewerken',
|
||||
'common.name': 'Naam',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Wachtwoord',
|
||||
|
||||
@@ -31,6 +31,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Zmień nazwę',
|
||||
'common.discardChanges': 'Odrzuć zmiany',
|
||||
'common.discard': 'Odrzuć',
|
||||
'common.unsavedTitle': 'Odrzucić zmiany?',
|
||||
'common.unsavedMessage': 'To, co tu wpisano, nie zostało jeszcze zapisane.',
|
||||
'common.keepEditing': 'Kontynuuj edycję',
|
||||
'common.name': 'Nazwa',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Hasło',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Переименовать',
|
||||
'common.discardChanges': 'Отменить изменения',
|
||||
'common.discard': 'Отменить',
|
||||
'common.unsavedTitle': 'Отменить изменения?',
|
||||
'common.unsavedMessage': 'Введённое здесь ещё не сохранено.',
|
||||
'common.keepEditing': 'Продолжить',
|
||||
'common.name': 'Имя',
|
||||
'common.email': 'Эл. почта',
|
||||
'common.password': 'Пароль',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Premenovať',
|
||||
'common.discardChanges': 'Zahodiť zmeny',
|
||||
'common.discard': 'Zahodiť',
|
||||
'common.unsavedTitle': 'Zahodiť zmeny?',
|
||||
'common.unsavedMessage': 'To, čo ste tu zadali, ešte nie je uložené.',
|
||||
'common.keepEditing': 'Pokračovať v úpravách',
|
||||
'common.name': 'Meno',
|
||||
'common.email': 'E-mail',
|
||||
'common.password': 'Heslo',
|
||||
|
||||
@@ -47,6 +47,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Döp om',
|
||||
'common.discardChanges': 'Ångra ändringar',
|
||||
'common.discard': 'Ångra',
|
||||
'common.unsavedTitle': 'Slänga ändringarna?',
|
||||
'common.unsavedMessage': 'Det du har fyllt i här är inte sparat än.',
|
||||
'common.keepEditing': 'Fortsätt redigera',
|
||||
'common.name': 'Namn',
|
||||
'common.email': 'E-post',
|
||||
'common.password': 'Lösenord',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': "เปลี่ยนชื่อ",
|
||||
'common.discardChanges': "ยกเลิกการเปลี่ยนแปลง",
|
||||
'common.discard': "ทิ้ง",
|
||||
'common.unsavedTitle': 'ทิ้งการเปลี่ยนแปลงไหม',
|
||||
'common.unsavedMessage': 'สิ่งที่คุณกรอกไว้ที่นี่ยังไม่ได้บันทึก',
|
||||
'common.keepEditing': 'แก้ไขต่อ',
|
||||
'common.name': "ชื่อ",
|
||||
'common.email': "อีเมล",
|
||||
'common.password': "รหัสผ่าน",
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Yeniden adlandır',
|
||||
'common.discardChanges': 'Değişiklikleri iptal et',
|
||||
'common.discard': 'Vazgeç',
|
||||
'common.unsavedTitle': 'Değişiklikler atılsın mı?',
|
||||
'common.unsavedMessage': 'Buraya girdikleriniz henüz kaydedilmedi.',
|
||||
'common.keepEditing': 'Düzenlemeye devam et',
|
||||
'common.name': 'Ad',
|
||||
'common.email': 'E-posta',
|
||||
'common.password': 'Şifre',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Перейменувати',
|
||||
'common.discardChanges': 'Скасувати зміни',
|
||||
'common.discard': 'Скасувати',
|
||||
'common.unsavedTitle': 'Скасувати зміни?',
|
||||
'common.unsavedMessage': 'Введене тут ще не збережено.',
|
||||
'common.keepEditing': 'Продовжити редагування',
|
||||
'common.name': "Ім'я",
|
||||
'common.email': 'Ел. пошта',
|
||||
'common.password': 'Пароль',
|
||||
|
||||
@@ -47,6 +47,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': 'Đổi tên',
|
||||
'common.discardChanges': 'Hủy thay đổi',
|
||||
'common.discard': 'Bỏ',
|
||||
'common.unsavedTitle': 'Bỏ các thay đổi?',
|
||||
'common.unsavedMessage': 'Những gì bạn nhập ở đây chưa được lưu.',
|
||||
'common.keepEditing': 'Tiếp tục chỉnh sửa',
|
||||
'common.name': 'Tên',
|
||||
'common.email': 'Email',
|
||||
'common.password': 'Mật khẩu',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': '重新命名',
|
||||
'common.discardChanges': '捨棄變更',
|
||||
'common.discard': '捨棄',
|
||||
'common.unsavedTitle': '放棄變更?',
|
||||
'common.unsavedMessage': '你在這裡輸入的內容尚未儲存。',
|
||||
'common.keepEditing': '繼續編輯',
|
||||
'common.name': '名稱',
|
||||
'common.email': '郵箱',
|
||||
'common.password': '密碼',
|
||||
|
||||
@@ -35,6 +35,9 @@ const common: TranslationStrings = {
|
||||
'common.rename': '重命名',
|
||||
'common.discardChanges': '放弃更改',
|
||||
'common.discard': '放弃',
|
||||
'common.unsavedTitle': '放弃更改?',
|
||||
'common.unsavedMessage': '你在这里输入的内容尚未保存。',
|
||||
'common.keepEditing': '继续编辑',
|
||||
'common.name': '名称',
|
||||
'common.email': '邮箱',
|
||||
'common.password': '密码',
|
||||
|
||||
Reference in New Issue
Block a user