From 3d1bdf0943b493009e65f6299fcd2e9b00dbaba7 Mon Sep 17 00:00:00 2001 From: dealerweb Date: Tue, 18 Aug 2026 17:31:28 +0200 Subject: [PATCH] fix: let a double-click unlock the embedded signature for editing Since the signature became a verbatim, non-editable atom (#476), there was no way to edit it in the composer at all (#822) - the styling guarantee silently cost the editability #329 had established. Keep the atom as the default so branded signatures still survive 1:1, and let an explicit gesture opt out: double-clicking the block dissolves it into regular editable content at the same position. Double-clicks that land on a link are ignored (the click is working the link), and Enter on the selected block keeps ProseMirror's default create-paragraph-after behaviour. Faithfulness of the dissolved signature: - Leaf
lines become paragraphs; only vertical margins the line does not declare are reset to 0 (how a div actually renders), so explicit spacing and indents survive. Divs carrying hr/pre and other non-line blocks are left to the schema parse. - Inherited styles (font, color, ...) on wrapper divs - which have no schema node and would be dropped - are hoisted onto the generated line paragraphs, the line's own declarations winning. - The content is parsed as a CLOSED document via the schema DOMParser and spliced in with one transaction; insertContentAt fits an open slice, which can split text runs at hard breaks and turned
-lined signatures into one spaced paragraph per line. The bracketing data-signature-block markers live outside the atom and stay put, so the identity-switch splice and the send path's already-embedded detection keep working on unlocked content, and switching identities re-embeds a fresh locked block. Undo restores the atom. A translated tooltip on the block makes the gesture discoverable. --- .../email/__tests__/signature-block.test.ts | 210 ++++++++++++++++++ components/email/rich-text-editor.tsx | 34 +-- components/email/signature-block.ts | 133 ++++++++++- components/email/styled-block-attributes.ts | 28 +++ locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh/common.json | 1 + 29 files changed, 399 insertions(+), 31 deletions(-) create mode 100644 components/email/styled-block-attributes.ts diff --git a/components/email/__tests__/signature-block.test.ts b/components/email/__tests__/signature-block.test.ts index b4949fb60..bee9c3747 100644 --- a/components/email/__tests__/signature-block.test.ts +++ b/components/email/__tests__/signature-block.test.ts @@ -1,15 +1,52 @@ import { describe, expect, it } from 'vitest'; import { Editor } from '@tiptap/core'; import StarterKit from '@tiptap/starter-kit'; +import Paragraph from '@tiptap/extension-paragraph'; import { SignatureBlock, buildSignatureBlock, containsEmbeddedSignature, + unlockSignatureBlock, SIGNATURE_BLOCK_MARKER, SIGNATURE_RANGE_MARKER, } from '../signature-block'; import { serializeEditorContent } from '../quoted-html'; +import { sanitizeSignatureHtml } from '@/lib/email-sanitization'; +import { styledBlockAttributes } from '../styled-block-attributes'; + +// The exact paragraph the composer runs (rich-text-editor.tsx builds +// StyledParagraph from the same styledBlockAttributes), so marker-survival +// and line-spacing assertions test the schema users actually get. +const ComposerParagraph = Paragraph.extend({ + addAttributes() { + return { + ...this.parent?.(), + ...styledBlockAttributes, + }; + }, +}); + +const bracketed = (inner: string) => + `

Hi

--

` + + `${buildSignatureBlock(inner)}

`; + +function makeEditor(content: string) { + return new Editor({ + element: document.createElement('div'), + extensions: [StarterKit.configure({ paragraph: false }), ComposerParagraph, SignatureBlock], + content, + }); +} + +function findSignaturePos(editor: Editor): number { + let found = -1; + editor.state.doc.descendants((node, pos) => { + if (node.type.name === 'signatureBlock') { found = pos; return false; } + return true; + }); + return found; +} describe('signature-block', () => { it('buildSignatureBlock wraps html in the marker div', () => { @@ -81,3 +118,176 @@ describe('signature-block', () => { } }); }); + +describe('unlockSignatureBlock (#822)', () => { + it('dissolves the atom into regular editable content between the markers', () => { + const editor = makeEditor(bracketed('Alice

CEO

')); + try { + const pos = findSignaturePos(editor); + expect(pos).toBeGreaterThanOrEqual(0); + expect(unlockSignatureBlock(editor, pos)).toBe(true); + + const out = serializeEditorContent(editor); + // The atom is gone, the signature text is ordinary schema content now. + expect(out).not.toContain(SIGNATURE_BLOCK_MARKER); + expect(out).toContain('Alice'); + expect(out).toContain('CEO'); + // The bracketing markers survive, so the identity-switch splice and the + // send path's already-embedded detection keep working. + expect(out).toContain(`${SIGNATURE_RANGE_MARKER}="separator"`); + expect(out).toContain(`${SIGNATURE_RANGE_MARKER}="end"`); + expect(containsEmbeddedSignature(out)).toBe(true); + } finally { + editor.destroy(); + } + }); + + it('unlocked content is genuinely editable', () => { + const editor = makeEditor(bracketed('

Old signature line

')); + try { + unlockSignatureBlock(editor, findSignaturePos(editor)); + // Type INSIDE the former signature text - not merely anywhere in the doc. + let textPos = -1; + editor.state.doc.descendants((node, pos) => { + if (node.isText && node.text?.includes('Old signature line')) { textPos = pos; return false; } + return true; + }); + expect(textPos).toBeGreaterThanOrEqual(0); + editor.commands.insertContentAt(textPos + 4, 'EDITED '); + expect(serializeEditorContent(editor)).toContain('Old EDITED signature line'); + } finally { + editor.destroy(); + } + }); + + it('returns false and leaves the doc alone for a non-signature position', () => { + const editor = makeEditor(bracketed('Alice')); + try { + const before = serializeEditorContent(editor); + expect(unlockSignatureBlock(editor, 0)).toBe(false); + expect(serializeEditorContent(editor)).toBe(before); + } finally { + editor.destroy(); + } + }); + + it('undo restores the locked atom', () => { + const editor = makeEditor(bracketed('Alice')); + try { + unlockSignatureBlock(editor, findSignaturePos(editor)); + expect(serializeEditorContent(editor)).not.toContain(SIGNATURE_BLOCK_MARKER); + editor.commands.undo(); + expect(serializeEditorContent(editor)).toContain(SIGNATURE_BLOCK_MARKER); + } finally { + editor.destroy(); + } + }); + + it('an empty signature unlocks to an empty paragraph instead of vanishing weirdly', () => { + const editor = makeEditor(bracketed('')); + try { + expect(unlockSignatureBlock(editor, findSignaturePos(editor))).toBe(true); + expect(serializeEditorContent(editor)).not.toContain(SIGNATURE_BLOCK_MARKER); + } finally { + editor.destroy(); + } + }); + + // The classic signature shape: one
per line. Divs render margin-less + // in mail clients but have no schema node, so a naive parse re-wraps every + // line into a default-margin paragraph and the signature falls apart into + // spaced paragraphs (second #822 report, live test). + it('keeps tight line spacing for div-per-line signatures', () => { + const editor = makeEditor(bracketed('
Jane Doe
IT-Management
')); + try { + unlockSignatureBlock(editor, findSignaturePos(editor)); + const out = serializeEditorContent(editor); + // Both lines carry the vertical margin reset, none is a bare default paragraph. + expect(out.match(/margin-top: 0px; margin-bottom: 0px/g)?.length).toBe(2); + } finally { + editor.destroy(); + } + }); + + // Longhand margins must not defeat the reset: only the missing verticals + // are zeroed, explicit spacing and indents survive. + it('keeps indents while resetting missing vertical margins', () => { + const editor = makeEditor(bracketed('
Indented line
')); + try { + unlockSignatureBlock(editor, findSignaturePos(editor)); + const out = serializeEditorContent(editor); + expect(out).toContain('margin-left: 12px'); + expect(out).toContain('margin-top: 0px'); + expect(out).toContain('margin-bottom: 0px'); + } finally { + editor.destroy(); + } + }); + + // Wrapper divs have no schema node and are dropped by the parse; their + // inherited styles (the font/color the whole signature lives in) must move + // onto the generated line paragraphs, with the line's own styles winning. + it('hoists inherited wrapper styles onto the line paragraphs', () => { + const editor = makeEditor(bracketed( + '
' + + '
Name
Title
' + )); + try { + unlockSignatureBlock(editor, findSignaturePos(editor)); + const out = serializeEditorContent(editor); + expect(out.match(/font-family: Arial/g)?.length).toBe(2); + expect(out).toContain('color: rgb(51, 51, 51)'); + expect(out).toContain('color: rgb(255, 0, 0)'); + } finally { + editor.destroy(); + } + }); + + it('keeps
line runs together and respects explicit margins', () => { + const editor = makeEditor(bracketed( + '
Tel: 1 • Fax: 2
Mobil: 3

Legal

' + )); + try { + unlockSignatureBlock(editor, findSignaturePos(editor)); + const out = serializeEditorContent(editor); + // The br stays a line break inside one paragraph, not a paragraph split. + expect(out).toMatch(/Fax: 2
Mobil: 3/); + // An explicit margin is not overridden by the margin-0 default. + expect(out).toContain('margin: 4px 0'); + } finally { + editor.destroy(); + } + }); +}); + +describe('unlockSignatureBlock keeps the classic Outlook-style signature intact', () => { + // One

with
line runs, font-size on the block, colored bold spans - + // the shape that shipped broken twice: schema parse must keep it a single + // paragraph with hard breaks, through the real pipeline (sanitize -> embed + // -> initial parse -> unlock). + const SIG = `

Mit freundlichem Gruß

+Jane Doe
+IT-Management

+Tel: 111 • Fax: 222

+Der Inhalt dieser E-Mail ist vertraulich. +

`; + + it('stays one paragraph with all hard breaks after the unlock', () => { + const sanitized = sanitizeSignatureHtml(SIG); + const body = `

--

` + + `${buildSignatureBlock(sanitized)}

`; + const editor = makeEditor(body); + try { + const pos = findSignaturePos(editor); + expect(unlockSignatureBlock(editor, pos)).toBe(true); + const out = serializeEditorContent(editor); + // Leading paragraph + separator + ONE signature paragraph + end marker. + expect((out.match(/

el.getAttribute("style"), - renderHTML: (attrs: Record) => - attrs.style ? { style: attrs.style } : {}, - }, - class: { - default: null as string | null, - parseHTML: (el: HTMLElement) => el.getAttribute("class"), - renderHTML: (attrs: Record) => - attrs.class ? { class: attrs.class } : {}, - }, - "data-signature-block": { - default: null as string | null, - parseHTML: (el: HTMLElement) => el.getAttribute("data-signature-block"), - renderHTML: (attrs: Record) => - attrs["data-signature-block"] - ? { "data-signature-block": attrs["data-signature-block"] } - : {}, - }, -}; - const StyledParagraph = Paragraph.extend({ addAttributes() { return { @@ -198,6 +172,7 @@ export function RichTextEditor({ onEditorReady, }: RichTextEditorProps) { const rtlEditingSupport = useSettingsStore((st) => st.rtlEditingSupport); + const tComposer = useTranslations("email_composer"); const onImageUploadRef = React.useRef(onImageUpload); onImageUploadRef.current = onImageUpload; const onEditorReadyRef = React.useRef(onEditorReady); @@ -253,8 +228,9 @@ export function RichTextEditor({ QuotedHtml, // Identity signature - held verbatim as a non-editable atomic node so // rich/branded signatures keep their inline styling in the editor and - // in the sent mail (see signature-block.ts). - SignatureBlock, + // in the sent mail (see signature-block.ts). Double-click unlocks it + // into editable content (#822). + SignatureBlock.configure({ editHint: tComposer('signature_edit_hint') }), TextDirection, ], content, diff --git a/components/email/signature-block.ts b/components/email/signature-block.ts index 507775d2f..0918ae1fe 100644 --- a/components/email/signature-block.ts +++ b/components/email/signature-block.ts @@ -1,6 +1,9 @@ "use client"; -import { Node as TiptapNode, mergeAttributes } from "@tiptap/core"; +import { Node as TiptapNode, mergeAttributes, type Editor } from "@tiptap/core"; +import { TextSelection } from "@tiptap/pm/state"; +import { DOMParser as ProseMirrorDOMParser } from "@tiptap/pm/model"; +import { parseHtmlSafely } from "@/lib/email-sanitization"; // Marker attribute that identifies the signature wrapper in serialized HTML, // so parseHTML can recognise it on the way back in (initial content, drafts). @@ -43,6 +46,14 @@ function forceLinksToNewTab(root: HTMLElement): void { * Mirrors QuotedHtml, but the inner region is read-only: a signature is meant * to be inserted/removed as a unit, not edited inline. Select the node and * press Backspace/Delete to drop the whole signature. + * + * Read-only is the default, not a wall (#822): double-click the block to + * dissolve it into regular editable content via + * {@link unlockSignatureBlock}. That trades the verbatim-styling guarantee + * for editability - the explicit user gesture is what makes the trade + * acceptable, unlike the silent flattening #476 fixed. (Enter on the selected + * block deliberately keeps ProseMirror's default create-paragraph-after + * behaviour - it is a low-intent everyday key.) */ export const SignatureBlock = TiptapNode.create({ name: "signatureBlock", @@ -53,6 +64,15 @@ export const SignatureBlock = TiptapNode.create({ // Isolating keeps selection/gapcursor behaviour sane at the boundary. isolating: true, + addOptions() { + return { + // Translated tooltip shown on the block ("Double-click to edit the + // signature"). Set via .configure() where translations are available; + // empty means no tooltip. + editHint: "", + }; + }, + addAttributes() { return { html: { @@ -79,10 +99,25 @@ export const SignatureBlock = TiptapNode.create({ }, addNodeView() { - return ({ node }) => { + return ({ node, editor, getPos }) => { const dom = document.createElement("div"); dom.setAttribute(SIGNATURE_BLOCK_MARKER, ""); dom.className = "signature-block-island"; + if (this.options.editHint) { + dom.title = this.options.editHint; + } + // Double-click dissolves the atom into regular editable content (#822). + // The listener sits on the wrapper; shadow-root events are composed, so + // it fires for the whole island. + dom.addEventListener("dblclick", (event) => { + // A double-click landing on a link is the user working the link (the + // first click already opened it) - not a request to start editing. + if (event.composedPath().some((t) => (t as Element).tagName === "A")) return; + const pos = typeof getPos === "function" ? getPos() : undefined; + if (pos == null) return; + event.preventDefault(); + unlockSignatureBlock(editor, pos); + }); // CRITICAL: render the signature inside a Shadow Root. The app's global @@ -125,6 +160,100 @@ export const SignatureBlock = TiptapNode.create({ }, }); +// Block elements that must not end up inside a

: a div containing any of +// these is a container, not a text line, and is left for the schema parse. +const NON_LINE_BLOCKS = + "div, p, table, ul, ol, blockquote, h1, h2, h3, h4, h5, h6, hr, pre, address, figure, dl"; + +// Inherited style properties hoisted from wrapper divs onto the generated +// line paragraphs. Wrappers themselves have no schema node and are dropped by +// the parse, so without hoisting the font/color the whole signature inherits +// from its container would silently vanish on unlock. Non-inherited +// properties (background, border, padding) are deliberately NOT hoisted - +// per-line copies would render differently than the container did. +const INHERITED_STYLE_PROPS = [ + "font", "font-family", "font-size", "font-style", "font-weight", + "color", "line-height", "letter-spacing", "text-transform", "direction", +]; + +/** + * Line-level

s render margin-less in mail clients, but the editor schema + * has no div node: parsing would re-wrap their content into default-margin + * paragraphs and the signature's tight line structure blows apart into spaced + * paragraphs. Rewrite leaf divs into explicit margin-0 paragraphs (keeping + * their attributes, inheriting wrapper styles) so the unlocked signature + * keeps the locked rendering's line spacing - the composer's paragraph + * preserves inline styles. Vertical margins are reset only where the line + * does not set them itself; horizontal margins (indented lines) are left + * alone.
runs and

blocks are untouched (the schema already handles + * those faithfully). Returns the parsed body element, ready for the + * ProseMirror DOM parser. + */ +function normalizeSignatureHtmlForEditing(html: string): HTMLElement { + const doc = parseHtmlSafely(html); + const body = doc.body; + body.querySelectorAll("div").forEach((div) => { + if (div.querySelector(NON_LINE_BLOCKS)) return; + const p = doc.createElement("p"); + for (const attr of Array.from(div.attributes)) p.setAttribute(attr.name, attr.value); + // Hoist inherited styles from wrapper divs, outermost first so closer + // wrappers win; the line's own declarations always take precedence. + const wrappers: HTMLElement[] = []; + for (let ancestor = div.parentElement; ancestor && ancestor !== body; ancestor = ancestor.parentElement) { + if (ancestor.tagName === "DIV") wrappers.unshift(ancestor); + } + for (const wrapper of wrappers) { + for (const prop of INHERITED_STYLE_PROPS) { + const value = wrapper.style.getPropertyValue(prop); + if (value && !div.style.getPropertyValue(prop)) p.style.setProperty(prop, value); + } + } + // A div renders with zero margins; reset only the verticals the line + // does not declare itself, keeping explicit spacing and indents intact. + if (!p.style.margin) { + if (!p.style.marginTop) p.style.marginTop = "0"; + if (!p.style.marginBottom) p.style.marginBottom = "0"; + } + while (div.firstChild) p.appendChild(div.firstChild); + div.replaceWith(p); + }); + // An empty signature still needs a block to put the caret into. + if (!body.firstElementChild) body.appendChild(doc.createElement("p")); + return body; +} + +/** + * Dissolve the signature atom at `pos` into regular, editable editor content + * (#822). The verbatim HTML is parsed into the schema at the atom's position - + * the pre-#476 representation, with the known consequence that heavily styled + * markup may be normalized by the schema. Leaf

lines are rewritten to + * margin-0 paragraphs first so the line spacing survives the parse (see + * normalizeSignatureHtmlForEditing). The bracketing `data-signature-block` + * marker paragraphs live OUTSIDE the atom and are left untouched, so the + * identity-switch splice and the send path's already-embedded detection keep + * working on the unlocked content, and switching identities re-embeds a fresh + * locked block. Undo restores the atom. + */ +export function unlockSignatureBlock(editor: Editor, pos: number): boolean { + if (!editor.isEditable) return false; + const node = editor.state.doc.nodeAt(pos); + if (!node || node.type.name !== "signatureBlock") return false; + const body = normalizeSignatureHtmlForEditing(String(node.attrs.html || "")); + // Parse as a CLOSED document (complete blocks) and splice it in with one + // plain transaction. insertContentAt parses an OPEN slice instead, and + // fitting an open slice may split text runs at hard breaks - which turned a + //
-lined signature into one spaced paragraph per line in the browser. + // A closed fragment of complete blocks cannot be refitted that way. + const parsed = ProseMirrorDOMParser.fromSchema(editor.state.schema).parse(body, { + preserveWhitespace: false, + }); + const tr = editor.state.tr.replaceWith(pos, pos + node.nodeSize, parsed.content); + tr.setSelection(TextSelection.near(tr.doc.resolve(pos))).scrollIntoView(); + editor.view.dispatch(tr); + editor.view.focus(); + return true; +} + /** * Build the editor-content wrapper that embeds the signature as a single * SignatureBlock node. The inner HTML must be pre-sanitized diff --git a/components/email/styled-block-attributes.ts b/components/email/styled-block-attributes.ts new file mode 100644 index 000000000..9cee9056f --- /dev/null +++ b/components/email/styled-block-attributes.ts @@ -0,0 +1,28 @@ +// Pasted email content (signatures, replies, quoted text) commonly carries +// inline styles on block elements. StarterKit's default Paragraph/Heading +// drop unknown attributes; the composer extends them with these so `style`, +// `class` and the signature range marker round-trip the editor. Kept in its +// own module so tests can build the exact composer paragraph instead of +// hand-copying (and silently drifting from) this config. +export const styledBlockAttributes = { + style: { + default: null as string | null, + parseHTML: (el: HTMLElement) => el.getAttribute("style"), + renderHTML: (attrs: Record) => + attrs.style ? { style: attrs.style } : {}, + }, + class: { + default: null as string | null, + parseHTML: (el: HTMLElement) => el.getAttribute("class"), + renderHTML: (attrs: Record) => + attrs.class ? { class: attrs.class } : {}, + }, + "data-signature-block": { + default: null as string | null, + parseHTML: (el: HTMLElement) => el.getAttribute("data-signature-block"), + renderHTML: (attrs: Record) => + attrs["data-signature-block"] + ? { "data-signature-block": attrs["data-signature-block"] } + : {}, + }, +}; diff --git a/locales/ar/common.json b/locales/ar/common.json index b39180bb8..fded78cd5 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "إيصال القراءة مطلوب (انقر للتعطيل)", "read_receipt_off": "طلب إيصال قراءة", "new_message": "رسالة جديدة", + "signature_edit_hint": "انقر نقرًا مزدوجًا لتحرير التوقيع", "reply": "رد", "reply_all": "الرد على الجميع", "forward": "إعادة توجيه", diff --git a/locales/ca/common.json b/locales/ca/common.json index 22f503834..825892a61 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Confirmació de lectura sol·licitada (feu clic per desactivar-la)", "read_receipt_off": "Sol·licita una confirmació de lectura", "new_message": "Missatge nou", + "signature_edit_hint": "Feu doble clic per editar la signatura", "reply": "Respon", "reply_all": "Respon a tots", "forward": "Reenvia", diff --git a/locales/cs/common.json b/locales/cs/common.json index 2570b45f2..0557708b1 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Vyžádáno potvrzení o přečtení (kliknutím vypnete)", "read_receipt_off": "Vyžádat potvrzení o přečtení", "new_message": "Nová zpráva", + "signature_edit_hint": "Dvojklikem upravíte podpis", "reply": "Odpovědět", "reply_all": "Odpovědět všem", "forward": "Přeposlat", diff --git a/locales/da/common.json b/locales/da/common.json index 3cb27e398..f86bf048a 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Læsekvittering anmodet (klik for at deaktivere)", "read_receipt_off": "Anmod om læsekvittering", "new_message": "Ny besked", + "signature_edit_hint": "Dobbeltklik for at redigere signaturen", "reply": "Svar", "reply_all": "Svar alle", "forward": "Videresend", diff --git a/locales/de/common.json b/locales/de/common.json index d4d7045a6..bdbdcb58a 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Lesebestätigung angefordert (klicken zum Deaktivieren)", "read_receipt_off": "Lesebestätigung anfordern", "new_message": "Neue Nachricht", + "signature_edit_hint": "Doppelklick, um die Signatur zu bearbeiten", "reply": "Antworten", "reply_all": "Allen antworten", "forward": "Weiterleiten", diff --git a/locales/en/common.json b/locales/en/common.json index 9280dc65f..df4fee084 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Read receipt requested (click to disable)", "read_receipt_off": "Request a read receipt", "new_message": "New Message", + "signature_edit_hint": "Double-click to edit the signature", "reply": "Reply", "reply_all": "Reply All", "forward": "Forward", diff --git a/locales/es/common.json b/locales/es/common.json index 25e20438e..5f55898ae 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Confirmación de lectura solicitada (haz clic para desactivar)", "read_receipt_off": "Solicitar confirmación de lectura", "new_message": "Nuevo Mensaje", + "signature_edit_hint": "Haz doble clic para editar la firma", "reply": "Responder", "reply_all": "Responder a Todos", "forward": "Reenviar", diff --git a/locales/fa/common.json b/locales/fa/common.json index 629d514fc..f4d3811ad 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "درخواست تأیید خواندن فعال (کلیک برای غیرفعال کردن)", "read_receipt_off": "درخواست تأیید خواندن", "new_message": "پیام جدید", + "signature_edit_hint": "برای ویرایش امضا دوبار کلیک کنید", "reply": "پاسخ", "reply_all": "پاسخ به همه", "forward": "ارسال مجدد", diff --git a/locales/fr/common.json b/locales/fr/common.json index 366374a3e..31f1c9a70 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Accusé de lecture demandé (cliquez pour désactiver)", "read_receipt_off": "Demander un accusé de lecture", "new_message": "Nouveau message", + "signature_edit_hint": "Double-cliquez pour modifier la signature", "reply": "Répondre", "reply_all": "Répondre à tous", "forward": "Transférer", diff --git a/locales/he/common.json b/locales/he/common.json index bc6c7f5d1..c10b8f43d 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -537,6 +537,7 @@ }, "email_composer": { "new_message": "הודעה חדשה", + "signature_edit_hint": "לחץ לחיצה כפולה לעריכת החתימה", "reply": "השב", "reply_all": "השב לכולם", "forward": "קדימה", diff --git a/locales/hu/common.json b/locales/hu/common.json index c1d7170f8..8f61011ca 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Olvasási visszaigazolás kérve (kattints a letiltáshoz)", "read_receipt_off": "Olvasási visszaigazolás kérése", "new_message": "Új üzenet", + "signature_edit_hint": "Kattintson duplán az aláírás szerkesztéséhez", "reply": "Válasz", "reply_all": "Válasz mindenkinek", "forward": "Továbbítás", diff --git a/locales/it/common.json b/locales/it/common.json index 45a2bc615..8c03fb27c 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Conferma di lettura richiesta (clicca per disattivare)", "read_receipt_off": "Richiedi una conferma di lettura", "new_message": "Nuovo messaggio", + "signature_edit_hint": "Doppio clic per modificare la firma", "reply": "Rispondi", "reply_all": "Rispondi a tutti", "forward": "Inoltra", diff --git a/locales/ja/common.json b/locales/ja/common.json index d4b7d4df4..3876f6505 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "開封確認を要求中(クリックで無効化)", "read_receipt_off": "開封確認を要求する", "new_message": "新規メッセージ", + "signature_edit_hint": "ダブルクリックで署名を編集", "reply": "返信", "reply_all": "全員に返信", "forward": "転送", diff --git a/locales/ko/common.json b/locales/ko/common.json index 56ed12e9d..c3723ee4c 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "읽음 확인 요청됨 (클릭하여 해제)", "read_receipt_off": "읽음 확인 요청", "new_message": "새 메시지", + "signature_edit_hint": "서명을 편집하려면 두 번 클릭하세요", "reply": "답장", "reply_all": "전체 답장", "forward": "전달", diff --git a/locales/lv/common.json b/locales/lv/common.json index 0c4f15193..1d2113128 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Pieprasīts lasīšanas apstiprinājums (noklikšķiniet, lai atspējotu)", "read_receipt_off": "Pieprasīt lasīšanas apstiprinājumu", "new_message": "Jauns ziņojums", + "signature_edit_hint": "Veiciet dubultklikšķi, lai rediģētu parakstu", "reply": "Atbildēt", "reply_all": "Atbildēt visiem", "forward": "Pārsūtīt", diff --git a/locales/mn/common.json b/locales/mn/common.json index 59424b3d0..4fae3ccc6 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Уншсан баримт хүссэн (идэвхгүй болгохын тулд дарна уу)", "read_receipt_off": "Уншсан баримт хүсэх", "new_message": "Шинэ мессеж", + "signature_edit_hint": "Гарын үсгийг засахын тулд давхар товшино уу", "reply": "Хариулах", "reply_all": "Бүгдэд нь хариулах", "forward": "Урагшаа", diff --git a/locales/nl/common.json b/locales/nl/common.json index c8e460521..83a666c7e 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Leesbevestiging aangevraagd (klik om uit te schakelen)", "read_receipt_off": "Leesbevestiging aanvragen", "new_message": "Nieuw bericht", + "signature_edit_hint": "Dubbelklik om de handtekening te bewerken", "reply": "Beantwoorden", "reply_all": "Allen beantwoorden", "forward": "Doorsturen", diff --git a/locales/pl/common.json b/locales/pl/common.json index f4d1fa706..4f449c468 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Zażądano potwierdzenia przeczytania (kliknij, aby wyłączyć)", "read_receipt_off": "Zażądaj potwierdzenia przeczytania", "new_message": "Nowa wiadomość", + "signature_edit_hint": "Kliknij dwukrotnie, aby edytować podpis", "reply": "Odpowiedz", "reply_all": "Odpowiedz wszystkim", "forward": "Przekaż dalej", diff --git a/locales/pt/common.json b/locales/pt/common.json index d234128a5..dc06101f6 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Confirmação de leitura solicitada (clique para desativar)", "read_receipt_off": "Solicitar confirmação de leitura", "new_message": "Nova Mensagem", + "signature_edit_hint": "Clique duas vezes para editar a assinatura", "reply": "Responder", "reply_all": "Responder a Todos", "forward": "Encaminhar", diff --git a/locales/ro/common.json b/locales/ro/common.json index 1a0d06a7d..82938f547 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Solicitare confirmare de citire (faceți clic pentru a dezactiva)", "read_receipt_off": "Solicitați o confirmare de citire", "new_message": "Mesaj nou", + "signature_edit_hint": "Dublu clic pentru a edita semnătura", "reply": "Răspunde", "reply_all": "Răspunde tuturor", "forward": "Redirecționează", diff --git a/locales/ru/common.json b/locales/ru/common.json index 4725a400c..051ded7a8 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Запрошено уведомление о прочтении (нажмите, чтобы отключить)", "read_receipt_off": "Запросить уведомление о прочтении", "new_message": "Новое письмо", + "signature_edit_hint": "Дважды щёлкните, чтобы изменить подпись", "reply": "Ответить", "reply_all": "Ответить всем", "forward": "Переслать", diff --git a/locales/sk/common.json b/locales/sk/common.json index 6cded9d01..60e88ad20 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Potvrdenie o prečítaní požiadané (kliknutím vypnete)", "read_receipt_off": "Požiadať o potvrdenie o prečítaní", "new_message": "Nová správa", + "signature_edit_hint": "Dvojklikom upravíte podpis", "reply": "Odpovedať", "reply_all": "Odpovedať všetkým", "forward": "Preposlať", diff --git a/locales/tr/common.json b/locales/tr/common.json index 65d2da05a..c4b241e82 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Okundu bilgisi istendi (devre dışı bırakmak için tıklayın)", "read_receipt_off": "Okundu bilgisi iste", "new_message": "Yeni İleti", + "signature_edit_hint": "İmzayı düzenlemek için çift tıklayın", "reply": "Yanıtla", "reply_all": "Tümünü Yanıtla", "forward": "İlet", diff --git a/locales/uk/common.json b/locales/uk/common.json index f21e1ebb3..2a5e08a73 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "Запитано сповіщення про прочитання (натисніть, щоб вимкнути)", "read_receipt_off": "Запитати сповіщення про прочитання", "new_message": "Нове повідомлення", + "signature_edit_hint": "Двічі клацніть, щоб редагувати підпис", "reply": "Відповісти", "reply_all": "Відповісти всім", "forward": "вперед", diff --git a/locales/zh/common.json b/locales/zh/common.json index 7bd5deab1..a339e7ee7 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -574,6 +574,7 @@ "read_receipt_on": "已请求已读回执(点击以关闭)", "read_receipt_off": "请求已读回执", "new_message": "新邮件", + "signature_edit_hint": "双击以编辑签名", "reply": "回复", "reply_all": "全部回复", "forward": "转发",