mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 00:14:18 +00:00
feat: ask for the link's text and address in Insert link, also with nothing selected
The first link dialog test now finds the address by its "Link" label and checks the linked text, since the dialog has two fields.
This commit is contained in:
@@ -29,11 +29,50 @@ describe('Insert link', () => {
|
||||
|
||||
fireEvent.click(screen.getByTitle('link'));
|
||||
expect(prompt).not.toHaveBeenCalled();
|
||||
const dialog = screen.getByRole('dialog', { name: 'insert_link' });
|
||||
fireEvent.change(dialog.querySelector('input')!, { target: { value: 'example.com/docs' } });
|
||||
screen.getByRole('dialog', { name: 'insert_link' });
|
||||
expect(screen.getByRole('textbox', { name: 'link_text' })).toHaveValue('docs');
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'link' }), { target: { value: 'example.com/docs' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'save' }));
|
||||
|
||||
expect(editor.getHTML()).toContain('href="https://example.com/docs"');
|
||||
expect(editor.getHTML()).toContain('<a target="_blank" rel="noopener noreferrer nofollow" href="https://example.com/docs">docs</a> here');
|
||||
});
|
||||
|
||||
// compose-p2-7: with nothing selected Save used to add nothing.
|
||||
it('inserts the text as a link when nothing is selected', async () => {
|
||||
const editor = await setup('<p>See it</p>');
|
||||
act(() => { editor.commands.setTextSelection(4); });
|
||||
|
||||
fireEvent.click(screen.getByTitle('link'));
|
||||
expect(screen.getByRole('textbox', { name: 'link_text' })).toHaveValue('');
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'link_text' }), { target: { value: 'our docs' } });
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'link' }), { target: { value: 'example.com/docs' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'save' }));
|
||||
|
||||
expect(editor.getHTML()).toMatch(/See<a [^>]*href="https:\/\/example\.com\/docs"[^>]*>our docs<\/a> it/);
|
||||
});
|
||||
|
||||
it('shows the address as the link when the text is left empty', async () => {
|
||||
const editor = await setup('<p>See it</p>');
|
||||
act(() => { editor.commands.setTextSelection(4); });
|
||||
|
||||
fireEvent.click(screen.getByTitle('link'));
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'link' }), { target: { value: 'https://example.com' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'save' }));
|
||||
|
||||
expect(editor.getHTML()).toMatch(/See<a [^>]*href="https:\/\/example\.com"[^>]*>https:\/\/example\.com<\/a> it/);
|
||||
});
|
||||
|
||||
it('edits the text of a link the caret is in', async () => {
|
||||
const editor = await setup('<p><a href="https://example.com">docs</a> here</p>');
|
||||
act(() => { editor.commands.setTextSelection(2); });
|
||||
|
||||
fireEvent.click(screen.getByTitle('link'));
|
||||
expect(screen.getByRole('textbox', { name: 'link_text' })).toHaveValue('docs');
|
||||
expect(screen.getByRole('textbox', { name: 'link' })).toHaveValue('https://example.com');
|
||||
fireEvent.change(screen.getByRole('textbox', { name: 'link_text' }), { target: { value: 'the docs' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'save' }));
|
||||
|
||||
expect(editor.getHTML()).toMatch(/<a [^>]*href="https:\/\/example\.com"[^>]*>the docs<\/a> here/);
|
||||
});
|
||||
|
||||
it('opens on Ctrl+K in the text, as in Gmail, Outlook and Word', async () => {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog } from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
export interface LinkDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
/** `text` is what the link shows; empty means the address itself. */
|
||||
onSubmit: (link: { text: string; href: string }) => void;
|
||||
/** The selected text, or the link's own text when editing one. */
|
||||
defaultText: string;
|
||||
defaultHref: string;
|
||||
/** Offers "Remove link" when editing a link. */
|
||||
onRemove?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert link (compose-p2-7): "Text" and "Link", as Gmail and Outlook ask. The
|
||||
* selection fills Text; with nothing selected the text (or the address) goes
|
||||
* in as a new link.
|
||||
*/
|
||||
export function LinkDialog({ isOpen, onClose, onSubmit, defaultText, defaultHref, onRemove }: LinkDialogProps) {
|
||||
const t = useTranslations("email_composer.toolbar");
|
||||
const tCommon = useTranslations("common");
|
||||
const id = useId();
|
||||
const [text, setText] = useState(defaultText);
|
||||
const [href, setHref] = useState(defaultHref);
|
||||
const textRef = useRef<HTMLInputElement>(null);
|
||||
const hrefRef = useRef<HTMLInputElement>(null);
|
||||
// With text already there, the address is what is left to give.
|
||||
const firstField = defaultText ? hrefRef : textRef;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
setText(defaultText);
|
||||
setHref(defaultHref);
|
||||
}, [isOpen, defaultText, defaultHref]);
|
||||
|
||||
const address = href.trim();
|
||||
const error = /\s/.test(address) ? t("link_invalid") : null;
|
||||
const canSubmit = address.length > 0 && !error;
|
||||
const errorId = `${id}-error`;
|
||||
|
||||
const submit = (event?: React.FormEvent) => {
|
||||
event?.preventDefault();
|
||||
if (!canSubmit) return;
|
||||
try {
|
||||
onSubmit({ text, href: address });
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={defaultHref ? t("edit_link") : t("insert_link")}
|
||||
showClose={false}
|
||||
initialFocus={firstField}
|
||||
onSubmit={submit}
|
||||
footer={
|
||||
<>
|
||||
{onRemove && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="me-auto"
|
||||
onClick={() => {
|
||||
try {
|
||||
onRemove();
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("remove_link")}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
{tCommon("cancel")}
|
||||
</Button>
|
||||
<Button type="submit" variant="default" disabled={!canSubmit}>
|
||||
{tCommon("save")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor={`${id}-text`} className="block text-sm font-medium text-foreground">{t("link_text")}</label>
|
||||
<Input id={`${id}-text`} ref={textRef} type="text" value={text} onChange={(e) => setText(e.target.value)} />
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor={`${id}-href`} className="block text-sm font-medium text-foreground">{t("link")}</label>
|
||||
<Input
|
||||
id={`${id}-href`}
|
||||
ref={hrefRef}
|
||||
type="text"
|
||||
value={href}
|
||||
onChange={(e) => setHref(e.target.value)}
|
||||
placeholder={t("link_address")}
|
||||
aria-invalid={error ? true : undefined}
|
||||
aria-describedby={error ? errorId : undefined}
|
||||
className={error ? "border-destructive focus-visible:border-destructive" : undefined}
|
||||
/>
|
||||
{error && (
|
||||
<p id={errorId} role="alert" className="text-xs text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,8 @@ import { RecipientMention, RecipientMentionList, type RecipientMentionListHandle
|
||||
import { MentionChipCard, setMentionRecipients } from "@/components/email/mention-chip";
|
||||
import { mentionSuggestions, type MentionCandidate, type MentionContact } from "@/lib/recipient-mentions";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { PromptDialog } from "@/components/ui/prompt-dialog";
|
||||
import { getMarkRange } from "@tiptap/core";
|
||||
import { LinkDialog } from "@/components/email/link-dialog";
|
||||
import { ImageAltDialog } from "@/components/email/image-alt-dialog";
|
||||
|
||||
/** A typed address without a scheme ("example.com") becomes a web link. */
|
||||
@@ -741,26 +742,43 @@ export function RichTextEditor({
|
||||
if (editor) setMentionRecipients(editor, mentionCandidates ? mentionCandidates.map((c) => c.email) : null);
|
||||
}, [editor, mentionCandidates]);
|
||||
|
||||
// Insert link asks for the address in the app's own dialog; on a link it
|
||||
// starts from the current address and offers "Remove link".
|
||||
// The selection is kept from when the dialog opened: the dialog takes the
|
||||
// focus, and with it the editor's selection.
|
||||
const [linkDialog, setLinkDialog] = useState<{ href: string; from: number; to: number } | null>(null);
|
||||
// Insert link asks for the text and the address in the app's own dialog
|
||||
// (compose-p2-7); on a link it starts from the link's text and address and
|
||||
// offers "Remove link". The range is kept from when the dialog opened: the
|
||||
// dialog takes the focus, and with it the editor's selection.
|
||||
const [linkDialog, setLinkDialog] = useState<{ href: string; text: string; from: number; to: number } | null>(null);
|
||||
const addLink = useCallback(() => {
|
||||
if (!editor) return;
|
||||
const previousUrl = editor.getAttributes("link").href as string | undefined;
|
||||
const { from, to } = editor.state.selection;
|
||||
setLinkDialog({ href: previousUrl ?? "", from, to });
|
||||
let { from, to } = editor.state.selection;
|
||||
// The caret in a link edits the whole link.
|
||||
const linkType = editor.schema.marks.link;
|
||||
if (previousUrl && from === to && linkType) {
|
||||
const range = getMarkRange(editor.state.doc.resolve(from), linkType);
|
||||
if (range) ({ from, to } = range);
|
||||
}
|
||||
const text = editor.state.doc.textBetween(from, to, " ");
|
||||
setLinkDialog({ href: previousUrl ?? "", text, from, to });
|
||||
}, [editor]);
|
||||
addLinkRef.current = addLink;
|
||||
const applyLink = useCallback((url: string) => {
|
||||
const applyLink = useCallback(({ text, href }: { text: string; href: string }) => {
|
||||
if (!editor || !linkDialog) return;
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setTextSelection({ from: linkDialog.from, to: linkDialog.to })
|
||||
const label = text.trim() ? text : href;
|
||||
const { from, to } = linkDialog;
|
||||
const chain = editor.chain().focus();
|
||||
if (from === to || label !== linkDialog.text) {
|
||||
// Nothing selected, or the text changed: the text goes in as the link.
|
||||
chain
|
||||
.insertContentAt({ from, to }, { type: "text", text: label, marks: [{ type: "link", attrs: { href } }] })
|
||||
.setTextSelection(from + label.length)
|
||||
.unsetMark("link")
|
||||
.run();
|
||||
return;
|
||||
}
|
||||
chain
|
||||
.setTextSelection({ from, to })
|
||||
.extendMarkRange("link")
|
||||
.setLink({ href: url })
|
||||
.setLink({ href })
|
||||
.run();
|
||||
}, [editor, linkDialog]);
|
||||
const removeLink = useCallback(() => {
|
||||
@@ -776,7 +794,6 @@ export function RichTextEditor({
|
||||
|
||||
const tToolbar = useTranslations("email_composer.toolbar");
|
||||
const tWindows = useTranslations("compose_windows");
|
||||
const tCommon = useTranslations("common");
|
||||
const [tableMenuOpen, setTableMenuOpen] = useState(false);
|
||||
const tableWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const [colorMenuOpen, setColorMenuOpen] = useState(false);
|
||||
@@ -1465,16 +1482,13 @@ export function RichTextEditor({
|
||||
document.body
|
||||
)}
|
||||
|
||||
<PromptDialog
|
||||
<LinkDialog
|
||||
isOpen={!!linkDialog}
|
||||
onClose={() => setLinkDialog(null)}
|
||||
title={linkDialog?.href ? tToolbar("edit_link") : tToolbar("insert_link")}
|
||||
placeholder={tToolbar("link_address")}
|
||||
defaultValue={linkDialog?.href ?? ""}
|
||||
confirmText={tCommon("save")}
|
||||
validate={(value) => (/\s/.test(value) ? tToolbar("link_invalid") : null)}
|
||||
onSubmit={(value) => applyLink(withLinkScheme(value))}
|
||||
extraAction={linkDialog?.href ? { label: tToolbar("remove_link"), onClick: removeLink } : undefined}
|
||||
defaultText={linkDialog?.text ?? ""}
|
||||
defaultHref={linkDialog?.href ?? ""}
|
||||
onSubmit={({ text, href }) => applyLink({ text, href: withLinkScheme(href) })}
|
||||
onRemove={linkDialog?.href ? removeLink : undefined}
|
||||
/>
|
||||
|
||||
<ImageAltDialog
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "تعديل الرابط",
|
||||
"remove_link": "إزالة الرابط",
|
||||
"link_address": "عنوان الرابط",
|
||||
"link_text": "النص",
|
||||
"link_invalid": "يجب ألا يحتوي العنوان على مسافات",
|
||||
"table": "جدول",
|
||||
"clear_formatting": "مسح التنسيق",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Edita l'enllaç",
|
||||
"remove_link": "Treu l'enllaç",
|
||||
"link_address": "Adreça de l'enllaç",
|
||||
"link_text": "Text",
|
||||
"link_invalid": "L'adreça no pot tenir espais",
|
||||
"table": "Taula",
|
||||
"clear_formatting": "Neteja el format",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Upravit odkaz",
|
||||
"remove_link": "Odebrat odkaz",
|
||||
"link_address": "Adresa odkazu",
|
||||
"link_text": "Text",
|
||||
"link_invalid": "Adresa nesmí obsahovat mezery",
|
||||
"table": "Tabulka",
|
||||
"clear_formatting": "Vymazat formátování",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Rediger link",
|
||||
"remove_link": "Fjern link",
|
||||
"link_address": "Linkadresse",
|
||||
"link_text": "Tekst",
|
||||
"link_invalid": "Adressen må ikke indeholde mellemrum",
|
||||
"table": "Tabel",
|
||||
"clear_formatting": "Ryd formatering",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Link bearbeiten",
|
||||
"remove_link": "Link entfernen",
|
||||
"link_address": "Linkadresse",
|
||||
"link_text": "Text",
|
||||
"link_invalid": "Die Adresse darf keine Leerzeichen enthalten",
|
||||
"table": "Tabelle",
|
||||
"clear_formatting": "Formatierung löschen",
|
||||
|
||||
@@ -1165,6 +1165,7 @@
|
||||
"edit_link": "Edit link",
|
||||
"remove_link": "Remove link",
|
||||
"link_address": "Link address",
|
||||
"link_text": "Text",
|
||||
"link_invalid": "The address can't contain spaces",
|
||||
"table": "Table",
|
||||
"clear_formatting": "Clear formatting",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Editar enlace",
|
||||
"remove_link": "Quitar enlace",
|
||||
"link_address": "Dirección del enlace",
|
||||
"link_text": "Texto",
|
||||
"link_invalid": "La dirección no puede contener espacios",
|
||||
"table": "Tabla",
|
||||
"clear_formatting": "Borrar formato",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Muuda linki",
|
||||
"remove_link": "Eemalda link",
|
||||
"link_address": "Lingi aadress",
|
||||
"link_text": "Tekst",
|
||||
"link_invalid": "Aadressis ei tohi olla tühikuid",
|
||||
"table": "Tabel",
|
||||
"clear_formatting": "Eemalda vormindus",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "ویرایش پیوند",
|
||||
"remove_link": "حذف پیوند",
|
||||
"link_address": "نشانی پیوند",
|
||||
"link_text": "متن",
|
||||
"link_invalid": "نشانی نباید فاصله داشته باشد",
|
||||
"table": "جدول",
|
||||
"clear_formatting": "پاک کردن قالببندی",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Modifier le lien",
|
||||
"remove_link": "Retirer le lien",
|
||||
"link_address": "Adresse du lien",
|
||||
"link_text": "Texte",
|
||||
"link_invalid": "L'adresse ne peut pas contenir d'espaces",
|
||||
"table": "Tableau",
|
||||
"clear_formatting": "Effacer la mise en forme",
|
||||
|
||||
@@ -1106,6 +1106,7 @@
|
||||
"edit_link": "עריכת קישור",
|
||||
"remove_link": "הסרת קישור",
|
||||
"link_address": "כתובת הקישור",
|
||||
"link_text": "טקסט",
|
||||
"link_invalid": "הכתובת לא יכולה להכיל רווחים",
|
||||
"table": "טבלה",
|
||||
"clear_formatting": "ניקוי עיצוב",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Hivatkozás szerkesztése",
|
||||
"remove_link": "Hivatkozás eltávolítása",
|
||||
"link_address": "Hivatkozás címe",
|
||||
"link_text": "Szöveg",
|
||||
"link_invalid": "A cím nem tartalmazhat szóközt",
|
||||
"table": "Táblázat",
|
||||
"clear_formatting": "Formázás törlése",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Modifica link",
|
||||
"remove_link": "Rimuovi link",
|
||||
"link_address": "Indirizzo del link",
|
||||
"link_text": "Testo",
|
||||
"link_invalid": "L'indirizzo non può contenere spazi",
|
||||
"table": "Tabella",
|
||||
"clear_formatting": "Cancella formattazione",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "リンクを編集",
|
||||
"remove_link": "リンクを解除",
|
||||
"link_address": "リンク先のアドレス",
|
||||
"link_text": "テキスト",
|
||||
"link_invalid": "アドレスに空白は使えません",
|
||||
"table": "表",
|
||||
"clear_formatting": "書式をクリア",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "링크 편집",
|
||||
"remove_link": "링크 제거",
|
||||
"link_address": "링크 주소",
|
||||
"link_text": "텍스트",
|
||||
"link_invalid": "주소에 공백을 넣을 수 없어요",
|
||||
"table": "표",
|
||||
"clear_formatting": "서식 지우기",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Rediģēt saiti",
|
||||
"remove_link": "Noņemt saiti",
|
||||
"link_address": "Saites adrese",
|
||||
"link_text": "Teksts",
|
||||
"link_invalid": "Adresē nedrīkst būt atstarpju",
|
||||
"table": "Tabula",
|
||||
"clear_formatting": "Notīrīt formatējumu",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Холбоос засах",
|
||||
"remove_link": "Холбоос хасах",
|
||||
"link_address": "Холбоосын хаяг",
|
||||
"link_text": "Текст",
|
||||
"link_invalid": "Хаяг хоосон зай агуулж болохгүй",
|
||||
"table": "Хүснэгт",
|
||||
"clear_formatting": "Форматыг арилгах",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Rediger lenke",
|
||||
"remove_link": "Fjern lenke",
|
||||
"link_address": "Lenkeadresse",
|
||||
"link_text": "Tekst",
|
||||
"link_invalid": "Adressen kan ikke inneholde mellomrom",
|
||||
"table": "Tabell",
|
||||
"clear_formatting": "Fjern formatering",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Link bewerken",
|
||||
"remove_link": "Link verwijderen",
|
||||
"link_address": "Linkadres",
|
||||
"link_text": "Tekst",
|
||||
"link_invalid": "Het adres mag geen spaties bevatten",
|
||||
"table": "Tabel",
|
||||
"clear_formatting": "Opmaak wissen",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Edytuj link",
|
||||
"remove_link": "Usuń link",
|
||||
"link_address": "Adres linku",
|
||||
"link_text": "Tekst",
|
||||
"link_invalid": "Adres nie może zawierać spacji",
|
||||
"table": "Tabela",
|
||||
"clear_formatting": "Wyczyść formatowanie",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Editar link",
|
||||
"remove_link": "Remover link",
|
||||
"link_address": "Endereço do link",
|
||||
"link_text": "Texto",
|
||||
"link_invalid": "O endereço não pode conter espaços",
|
||||
"table": "Tabela",
|
||||
"clear_formatting": "Limpar formatação",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Editează linkul",
|
||||
"remove_link": "Elimină linkul",
|
||||
"link_address": "Adresa linkului",
|
||||
"link_text": "Text",
|
||||
"link_invalid": "Adresa nu poate conține spații",
|
||||
"table": "Tabel",
|
||||
"clear_formatting": "Șterge formatarea",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Изменить ссылку",
|
||||
"remove_link": "Убрать ссылку",
|
||||
"link_address": "Адрес ссылки",
|
||||
"link_text": "Текст",
|
||||
"link_invalid": "В адресе не должно быть пробелов",
|
||||
"table": "Таблица",
|
||||
"clear_formatting": "Очистить форматирование",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Upraviť odkaz",
|
||||
"remove_link": "Odstrániť odkaz",
|
||||
"link_address": "Adresa odkazu",
|
||||
"link_text": "Text",
|
||||
"link_invalid": "Adresa nesmie obsahovať medzery",
|
||||
"table": "Tabuľka",
|
||||
"clear_formatting": "Vymazať formátovanie",
|
||||
|
||||
@@ -1552,6 +1552,7 @@
|
||||
"edit_link": "แก้ไขลิงก์",
|
||||
"remove_link": "นำลิงก์ออก",
|
||||
"link_address": "ที่อยู่ลิงก์",
|
||||
"link_text": "ข้อความ",
|
||||
"link_invalid": "ที่อยู่ต้องไม่มีช่องว่าง",
|
||||
"table": "ตาราง",
|
||||
"clear_formatting": "ล้างการจัดรูปแบบ",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Bağlantıyı düzenle",
|
||||
"remove_link": "Bağlantıyı kaldır",
|
||||
"link_address": "Bağlantı adresi",
|
||||
"link_text": "Metin",
|
||||
"link_invalid": "Adres boşluk içeremez",
|
||||
"table": "Tablo",
|
||||
"clear_formatting": "Biçimlendirmeyi temizle",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "Змінити посилання",
|
||||
"remove_link": "Прибрати посилання",
|
||||
"link_address": "Адреса посилання",
|
||||
"link_text": "Текст",
|
||||
"link_invalid": "Адреса не може містити пробілів",
|
||||
"table": "Таблиця",
|
||||
"clear_formatting": "Очистити форматування",
|
||||
|
||||
@@ -1177,6 +1177,7 @@
|
||||
"edit_link": "Chỉnh sửa liên kết",
|
||||
"remove_link": "Xóa liên kết",
|
||||
"link_address": "Địa chỉ liên kết",
|
||||
"link_text": "Văn bản",
|
||||
"link_invalid": "Địa chỉ không được chứa dấu cách",
|
||||
"table": "Bảng",
|
||||
"clear_formatting": "Xóa định dạng",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "編輯連結",
|
||||
"remove_link": "移除連結",
|
||||
"link_address": "連結網址",
|
||||
"link_text": "文字",
|
||||
"link_invalid": "網址中不能包含空格",
|
||||
"table": "表格",
|
||||
"clear_formatting": "清除格式",
|
||||
|
||||
@@ -1163,6 +1163,7 @@
|
||||
"edit_link": "编辑链接",
|
||||
"remove_link": "移除链接",
|
||||
"link_address": "链接地址",
|
||||
"link_text": "文本",
|
||||
"link_invalid": "地址中不能包含空格",
|
||||
"table": "表格",
|
||||
"clear_formatting": "清除格式",
|
||||
|
||||
Reference in New Issue
Block a user