mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: keep a plugin side panel from squeezing the message too narrow to read
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { render, screen, act, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import type { Email, Mailbox } from '@/lib/jmap/types';
|
||||
|
||||
// With a plugin panel open at 280 px next to a 300 px message (three panes
|
||||
// on a laptop), the subject broke after every few letters. The panel now
|
||||
// starts collapsed when it would leave the message under 480 px.
|
||||
|
||||
vi.mock('@/hooks/use-media-query', () => ({
|
||||
useDeviceDetection: () => ({ isMobile: false, isTablet: false, isDesktop: true }),
|
||||
useMediaQuery: () => false,
|
||||
}));
|
||||
vi.mock('@/components/tour/tour-provider', () => ({
|
||||
useTour: () => ({ activeTour: null, startTour: vi.fn(), endTour: vi.fn(), registerStep: vi.fn() }),
|
||||
}));
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({
|
||||
PluginSlot: ({ name }: { name: string }) => React.createElement('div', { 'data-testid': `slot-${name}` }),
|
||||
}));
|
||||
vi.mock('@/hooks/use-plugin-slot-offers', () => ({
|
||||
usePluginSlotOffers: (slot: string) => (slot === 'email-detail-sidebar' ? [{ pluginId: 'notes', order: 100 }] : []),
|
||||
}));
|
||||
|
||||
let reportWidth = 0;
|
||||
const observers: Array<(entries: Array<{ contentRect: { width: number } }>) => void> = [];
|
||||
class FakeResizeObserver {
|
||||
constructor(private cb: (entries: Array<{ contentRect: { width: number } }>) => void) { observers.push(cb); }
|
||||
observe() { this.cb([{ contentRect: { width: reportWidth } }]); }
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', FakeResizeObserver);
|
||||
|
||||
const { EmailViewer } = await import('../email-viewer');
|
||||
|
||||
const mailboxes = [
|
||||
{ id: 'inbox', name: 'Inbox', role: 'inbox', parentId: null, myRights: { mayAddItems: true } },
|
||||
] as unknown as Mailbox[];
|
||||
const email = {
|
||||
id: 'e1', blobId: 'b1', threadId: 't1', mailboxIds: { inbox: true }, keywords: { $seen: true },
|
||||
from: [{ name: 'Alice', email: 'alice@example.com' }], to: [], subject: 'Hello', receivedAt: '2026-01-01T00:00:00Z',
|
||||
preview: '', hasAttachment: false, size: 1, textBody: [], htmlBody: [], attachments: [], bodyValues: {},
|
||||
} as unknown as Email;
|
||||
|
||||
function renderViewer() {
|
||||
return render(<EmailViewer email={email} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" />);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
observers.length = 0;
|
||||
});
|
||||
|
||||
describe('plugin detail panel and narrow messages', () => {
|
||||
it('starts collapsed when it would leave the message too narrow', () => {
|
||||
reportWidth = 640;
|
||||
renderViewer();
|
||||
|
||||
expect(screen.queryByTestId('slot-email-detail-sidebar')).not.toBeInTheDocument();
|
||||
expect(screen.getByLabelText('plugin_panel_show')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens anyway when the user asks', () => {
|
||||
reportWidth = 640;
|
||||
renderViewer();
|
||||
|
||||
act(() => { fireEvent.click(screen.getByLabelText('plugin_panel_show')); });
|
||||
|
||||
expect(screen.getByTestId('slot-email-detail-sidebar')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('is open when there is room', () => {
|
||||
reportWidth = 1100;
|
||||
renderViewer();
|
||||
|
||||
expect(screen.getByTestId('slot-email-detail-sidebar')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -961,6 +961,9 @@ function DetailsCategorySlot({ email, category }: { email: EmailReadView | null;
|
||||
return <PluginSlot name="email-details-section" className="mt-2 empty:mt-0" extraProps={props} />;
|
||||
}
|
||||
|
||||
/** Narrowest the message may get next to a plugin panel before the panel collapses. */
|
||||
const MIN_READER_WIDTH = 480;
|
||||
|
||||
function SidebarSection({ icon: Icon, title, children }: { icon: React.ComponentType<{ className?: string }>; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
@@ -1422,6 +1425,25 @@ export function EmailViewer({
|
||||
} catch { return 280; }
|
||||
});
|
||||
const detailSidebarWidthRef = useRef(detailSidebarWidth);
|
||||
// The panel takes its width from the message. When that would leave the
|
||||
// message too narrow to read (three panes on a laptop), the panel starts
|
||||
// collapsed; opening it anyway is still the user's call.
|
||||
const viewerRootRef = useRef<HTMLDivElement>(null);
|
||||
const [viewerWidth, setViewerWidth] = useState(0);
|
||||
const [detailSidebarForcedOpen, setDetailSidebarForcedOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!hasDetailSidebar || typeof ResizeObserver === 'undefined') return;
|
||||
const el = viewerRootRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const width = Math.round(entries[0]?.contentRect.width ?? 0);
|
||||
setViewerWidth(width);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [hasDetailSidebar]);
|
||||
const readerTooNarrow = viewerWidth > 0 && viewerWidth - detailSidebarWidth < MIN_READER_WIDTH;
|
||||
const sidebarCollapsed = detailSidebarCollapsed || (readerTooNarrow && !detailSidebarForcedOpen);
|
||||
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem('emailDetailSidebarCollapsed', detailSidebarCollapsed ? '1' : '0'); } catch { /* ignore */ }
|
||||
@@ -3993,6 +4015,7 @@ export function EmailViewer({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={viewerRootRef}
|
||||
data-tour="email-viewer"
|
||||
className={cn("flex-1 flex flex-row h-full bg-background overflow-hidden relative", className)}
|
||||
>
|
||||
@@ -6073,20 +6096,23 @@ export function EmailViewer({
|
||||
{hasDetailSidebar && !isMobile && (
|
||||
<>
|
||||
{/* Collapse toggle when sidebar is collapsed */}
|
||||
{detailSidebarCollapsed && (
|
||||
{sidebarCollapsed && (
|
||||
<div className="flex flex-col items-center border-s border-border bg-background">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setDetailSidebarCollapsed(false)}
|
||||
onClick={() => {
|
||||
setDetailSidebarCollapsed(false);
|
||||
if (readerTooNarrow) setDetailSidebarForcedOpen(true);
|
||||
}}
|
||||
className="h-8 w-8 m-1"
|
||||
aria-label="Expand panel"
|
||||
aria-label={t('plugin_panel_show')}
|
||||
>
|
||||
<PanelRightOpen className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!detailSidebarCollapsed && (
|
||||
{!sidebarCollapsed && (
|
||||
<>
|
||||
<ResizeHandle
|
||||
onResizeStart={() => { detailSidebarWidthRef.current = detailSidebarWidth; }}
|
||||
@@ -6105,9 +6131,12 @@ export function EmailViewer({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => setDetailSidebarCollapsed(true)}
|
||||
onClick={() => {
|
||||
setDetailSidebarCollapsed(true);
|
||||
setDetailSidebarForcedOpen(false);
|
||||
}}
|
||||
className="h-7 w-7"
|
||||
aria-label="Collapse panel"
|
||||
aria-label={t('plugin_panel_hide')}
|
||||
>
|
||||
<PanelRightClose className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "تم نسخ الرمز",
|
||||
"copy_failed": "تعذّر نسخ الرمز"
|
||||
},
|
||||
"mv_position": "{index} من {total}"
|
||||
"mv_position": "{index} من {total}",
|
||||
"plugin_panel_show": "إظهار لوحة الإضافة",
|
||||
"plugin_panel_hide": "إخفاء لوحة الإضافة"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "تصغير",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Codi copiat",
|
||||
"copy_failed": "No s'ha pogut copiar el codi"
|
||||
},
|
||||
"mv_position": "{index} de {total}"
|
||||
"mv_position": "{index} de {total}",
|
||||
"plugin_panel_show": "Mostra el tauler del connector",
|
||||
"plugin_panel_hide": "Amaga el tauler del connector"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimitza",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kód zkopírován",
|
||||
"copy_failed": "Kód se nepodařilo zkopírovat"
|
||||
},
|
||||
"mv_position": "{index} z {total}"
|
||||
"mv_position": "{index} z {total}",
|
||||
"plugin_panel_show": "Zobrazit panel pluginu",
|
||||
"plugin_panel_hide": "Skrýt panel pluginu"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimalizovat",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kode kopieret",
|
||||
"copy_failed": "Koden kunne ikke kopieres"
|
||||
},
|
||||
"mv_position": "{index} af {total}"
|
||||
"mv_position": "{index} af {total}",
|
||||
"plugin_panel_show": "Vis pluginpanel",
|
||||
"plugin_panel_hide": "Skjul pluginpanel"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimer",
|
||||
|
||||
+12
-10
@@ -359,15 +359,15 @@
|
||||
"external_content_warning": "Bilder und externe Inhalte wurden blockiert",
|
||||
"load_external_content": "Bilder laden",
|
||||
"trust_sender": "Diesem Absender immer vertrauen",
|
||||
"sender_check": {
|
||||
"unverified_label": "Absender nicht bestätigt",
|
||||
"failed_label": "Absenderprüfung fehlgeschlagen",
|
||||
"unverified": "Diese Nachricht gibt vor, von {domain} zu kommen, aber nichts bestätigt das. Sie trägt keine gültige Signatur von {domain} und kam nicht von einem Server, den {domain} zulässt.",
|
||||
"unverified_sent_from": "Diese Nachricht gibt vor, von {domain} zu kommen, wurde aber von {host} verschickt und trägt keine gültige Signatur von {domain}.",
|
||||
"failed": "Diese Nachricht gibt vor, von {domain} zu kommen, hat aber die Absenderprüfung dieser Domain nicht bestanden. Sie ist möglicherweise gefälscht.",
|
||||
"failed_sent_from": "Diese Nachricht gibt vor, von {domain} zu kommen, wurde aber von {host} verschickt und hat die Absenderprüfung von {domain} nicht bestanden. Sie ist möglicherweise gefälscht.",
|
||||
"caution": "Seien Sie vorsichtig mit Links, Anhängen und Fragen nach Passwörtern oder Zahlungsdaten."
|
||||
},
|
||||
"sender_check": {
|
||||
"unverified_label": "Absender nicht bestätigt",
|
||||
"failed_label": "Absenderprüfung fehlgeschlagen",
|
||||
"unverified": "Diese Nachricht gibt vor, von {domain} zu kommen, aber nichts bestätigt das. Sie trägt keine gültige Signatur von {domain} und kam nicht von einem Server, den {domain} zulässt.",
|
||||
"unverified_sent_from": "Diese Nachricht gibt vor, von {domain} zu kommen, wurde aber von {host} verschickt und trägt keine gültige Signatur von {domain}.",
|
||||
"failed": "Diese Nachricht gibt vor, von {domain} zu kommen, hat aber die Absenderprüfung dieser Domain nicht bestanden. Sie ist möglicherweise gefälscht.",
|
||||
"failed_sent_from": "Diese Nachricht gibt vor, von {domain} zu kommen, wurde aber von {host} verschickt und hat die Absenderprüfung von {domain} nicht bestanden. Sie ist möglicherweise gefälscht.",
|
||||
"caution": "Seien Sie vorsichtig mit Links, Anhängen und Fragen nach Passwörtern oder Zahlungsdaten."
|
||||
},
|
||||
"back_to_list": "Zurück zur Liste",
|
||||
"view_contact": "Kontakt anzeigen",
|
||||
"message_details": "Nachrichtendetails",
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Code kopiert",
|
||||
"copy_failed": "Code konnte nicht kopiert werden"
|
||||
},
|
||||
"mv_position": "{index} von {total}"
|
||||
"mv_position": "{index} von {total}",
|
||||
"plugin_panel_show": "Plugin-Bereich einblenden",
|
||||
"plugin_panel_hide": "Plugin-Bereich ausblenden"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimieren",
|
||||
|
||||
+12
-10
@@ -359,15 +359,15 @@
|
||||
"external_content_warning": "Images and external content have been blocked",
|
||||
"load_external_content": "Load images",
|
||||
"trust_sender": "Always trust this sender",
|
||||
"sender_check": {
|
||||
"unverified_label": "Unverified sender",
|
||||
"failed_label": "Sender check failed",
|
||||
"unverified": "This message claims to be from {domain}, but nothing confirms that. It has no valid signature from {domain} and didn't come from a server {domain} authorizes.",
|
||||
"unverified_sent_from": "This message claims to be from {domain}, but it was sent from {host} and has no valid signature from {domain}.",
|
||||
"failed": "This message claims to be from {domain}, but it failed that domain's sender checks. It may be forged.",
|
||||
"failed_sent_from": "This message claims to be from {domain}, but it was sent from {host} and failed {domain}'s sender checks. It may be forged.",
|
||||
"caution": "Be careful with links, attachments and requests for passwords or payment details."
|
||||
},
|
||||
"sender_check": {
|
||||
"unverified_label": "Unverified sender",
|
||||
"failed_label": "Sender check failed",
|
||||
"unverified": "This message claims to be from {domain}, but nothing confirms that. It has no valid signature from {domain} and didn't come from a server {domain} authorizes.",
|
||||
"unverified_sent_from": "This message claims to be from {domain}, but it was sent from {host} and has no valid signature from {domain}.",
|
||||
"failed": "This message claims to be from {domain}, but it failed that domain's sender checks. It may be forged.",
|
||||
"failed_sent_from": "This message claims to be from {domain}, but it was sent from {host} and failed {domain}'s sender checks. It may be forged.",
|
||||
"caution": "Be careful with links, attachments and requests for passwords or payment details."
|
||||
},
|
||||
"back_to_list": "Back to list",
|
||||
"view_contact": "View contact",
|
||||
"message_details": "Message Details",
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Code copied",
|
||||
"copy_failed": "Could not copy the code"
|
||||
},
|
||||
"mv_position": "{index} of {total}"
|
||||
"mv_position": "{index} of {total}",
|
||||
"plugin_panel_show": "Show plugin panel",
|
||||
"plugin_panel_hide": "Hide plugin panel"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimize",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Código copiado",
|
||||
"copy_failed": "No se pudo copiar el código"
|
||||
},
|
||||
"mv_position": "{index} de {total}"
|
||||
"mv_position": "{index} de {total}",
|
||||
"plugin_panel_show": "Mostrar el panel del complemento",
|
||||
"plugin_panel_hide": "Ocultar el panel del complemento"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimizar",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "کد کپی شد",
|
||||
"copy_failed": "کپی کد ممکن نشد"
|
||||
},
|
||||
"mv_position": "{index} از {total}"
|
||||
"mv_position": "{index} از {total}",
|
||||
"plugin_panel_show": "نمایش پنل افزونه",
|
||||
"plugin_panel_hide": "پنهان کردن پنل افزونه"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "کوچک کردن",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Code copié",
|
||||
"copy_failed": "Impossible de copier le code"
|
||||
},
|
||||
"mv_position": "{index} sur {total}"
|
||||
"mv_position": "{index} sur {total}",
|
||||
"plugin_panel_show": "Afficher le panneau du plugin",
|
||||
"plugin_panel_hide": "Masquer le panneau du plugin"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Réduire",
|
||||
|
||||
@@ -585,7 +585,9 @@
|
||||
"copied": "הקוד הועתק",
|
||||
"copy_failed": "לא ניתן להעתיק את הקוד"
|
||||
},
|
||||
"mv_position": "{index} מתוך {total}"
|
||||
"mv_position": "{index} מתוך {total}",
|
||||
"plugin_panel_show": "הצגת חלונית התוסף",
|
||||
"plugin_panel_hide": "הסתרת חלונית התוסף"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "מזעור",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kód másolva",
|
||||
"copy_failed": "Nem sikerült másolni a kódot"
|
||||
},
|
||||
"mv_position": "{index} / {total}"
|
||||
"mv_position": "{index} / {total}",
|
||||
"plugin_panel_show": "Bővítménypanel megjelenítése",
|
||||
"plugin_panel_hide": "Bővítménypanel elrejtése"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Kis méret",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Codice copiato",
|
||||
"copy_failed": "Impossibile copiare il codice"
|
||||
},
|
||||
"mv_position": "{index} di {total}"
|
||||
"mv_position": "{index} di {total}",
|
||||
"plugin_panel_show": "Mostra il pannello del plugin",
|
||||
"plugin_panel_hide": "Nascondi il pannello del plugin"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Riduci a icona",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "コードをコピーしました",
|
||||
"copy_failed": "コードをコピーできませんでした"
|
||||
},
|
||||
"mv_position": "{index} / {total}"
|
||||
"mv_position": "{index} / {total}",
|
||||
"plugin_panel_show": "プラグインパネルを表示",
|
||||
"plugin_panel_hide": "プラグインパネルを隠す"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "最小化",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "코드를 복사했습니다",
|
||||
"copy_failed": "코드를 복사할 수 없습니다"
|
||||
},
|
||||
"mv_position": "{index} / {total}"
|
||||
"mv_position": "{index} / {total}",
|
||||
"plugin_panel_show": "플러그인 패널 표시",
|
||||
"plugin_panel_hide": "플러그인 패널 숨기기"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "최소화",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kods nokopēts",
|
||||
"copy_failed": "Neizdevās nokopēt kodu"
|
||||
},
|
||||
"mv_position": "{index} no {total}"
|
||||
"mv_position": "{index} no {total}",
|
||||
"plugin_panel_show": "Rādīt spraudņa paneli",
|
||||
"plugin_panel_hide": "Slēpt spraudņa paneli"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimizēt",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Код хуулагдлаа",
|
||||
"copy_failed": "Кодыг хуулж чадсангүй"
|
||||
},
|
||||
"mv_position": "{total}-с {index}"
|
||||
"mv_position": "{total}-с {index}",
|
||||
"plugin_panel_show": "Залгаасын самбарыг харуулах",
|
||||
"plugin_panel_hide": "Залгаасын самбарыг нуух"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Багасгах",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kode kopiert",
|
||||
"copy_failed": "Kunne ikke kopiere koden"
|
||||
},
|
||||
"mv_position": "{index} av {total}"
|
||||
"mv_position": "{index} av {total}",
|
||||
"plugin_panel_show": "Vis tilleggspanel",
|
||||
"plugin_panel_hide": "Skjul tilleggspanel"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimer",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Code gekopieerd",
|
||||
"copy_failed": "Kan de code niet kopiëren"
|
||||
},
|
||||
"mv_position": "{index} van {total}"
|
||||
"mv_position": "{index} van {total}",
|
||||
"plugin_panel_show": "Plug-inpaneel tonen",
|
||||
"plugin_panel_hide": "Plug-inpaneel verbergen"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimaliseren",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Skopiowano kod",
|
||||
"copy_failed": "Nie udało się skopiować kodu"
|
||||
},
|
||||
"mv_position": "{index} z {total}"
|
||||
"mv_position": "{index} z {total}",
|
||||
"plugin_panel_show": "Pokaż panel wtyczki",
|
||||
"plugin_panel_hide": "Ukryj panel wtyczki"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimalizuj",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Código copiado",
|
||||
"copy_failed": "Não foi possível copiar o código"
|
||||
},
|
||||
"mv_position": "{index} de {total}"
|
||||
"mv_position": "{index} de {total}",
|
||||
"plugin_panel_show": "Mostrar o painel do plugin",
|
||||
"plugin_panel_hide": "Ocultar o painel do plugin"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimizar",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Cod copiat",
|
||||
"copy_failed": "Codul nu a putut fi copiat"
|
||||
},
|
||||
"mv_position": "{index} din {total}"
|
||||
"mv_position": "{index} din {total}",
|
||||
"plugin_panel_show": "Afișează panoul pluginului",
|
||||
"plugin_panel_hide": "Ascunde panoul pluginului"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimizează",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Код скопирован",
|
||||
"copy_failed": "Не удалось скопировать код"
|
||||
},
|
||||
"mv_position": "{index} из {total}"
|
||||
"mv_position": "{index} из {total}",
|
||||
"plugin_panel_show": "Показать панель плагина",
|
||||
"plugin_panel_hide": "Скрыть панель плагина"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Свернуть",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kód skopírovaný",
|
||||
"copy_failed": "Kód sa nepodarilo skopírovať"
|
||||
},
|
||||
"mv_position": "{index} z {total}"
|
||||
"mv_position": "{index} z {total}",
|
||||
"plugin_panel_show": "Zobraziť panel doplnku",
|
||||
"plugin_panel_hide": "Skryť panel doplnku"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Minimalizovať",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Kod kopyalandı",
|
||||
"copy_failed": "Kod kopyalanamadı"
|
||||
},
|
||||
"mv_position": "{index} / {total}"
|
||||
"mv_position": "{index} / {total}",
|
||||
"plugin_panel_show": "Eklenti panelini göster",
|
||||
"plugin_panel_hide": "Eklenti panelini gizle"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Simge durumuna küçült",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "Код скопійовано",
|
||||
"copy_failed": "Не вдалося скопіювати код"
|
||||
},
|
||||
"mv_position": "{index} з {total}"
|
||||
"mv_position": "{index} з {total}",
|
||||
"plugin_panel_show": "Показати панель плагіна",
|
||||
"plugin_panel_hide": "Сховати панель плагіна"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "Згорнути",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "驗證碼已複製",
|
||||
"copy_failed": "無法複製驗證碼"
|
||||
},
|
||||
"mv_position": "第 {index} 封,共 {total} 封"
|
||||
"mv_position": "第 {index} 封,共 {total} 封",
|
||||
"plugin_panel_show": "顯示外掛程式面板",
|
||||
"plugin_panel_hide": "隱藏外掛程式面板"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "最小化",
|
||||
|
||||
@@ -620,7 +620,9 @@
|
||||
"copied": "验证码已复制",
|
||||
"copy_failed": "无法复制验证码"
|
||||
},
|
||||
"mv_position": "第 {index} 封,共 {total} 封"
|
||||
"mv_position": "第 {index} 封,共 {total} 封",
|
||||
"plugin_panel_show": "显示插件面板",
|
||||
"plugin_panel_hide": "隐藏插件面板"
|
||||
},
|
||||
"email_composer": {
|
||||
"minimize": "最小化",
|
||||
|
||||
Reference in New Issue
Block a user