mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 08:16:22 +00:00
feat: keep the formatting toolbar in one row, with the rest behind More formatting
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import React from 'react';
|
||||
|
||||
import { RichTextEditor, formattingFoldCount } from '../rich-text-editor';
|
||||
|
||||
// The formatting toolbar keeps one row: in a narrow composer (585 px at
|
||||
// 1280x720) it wrapped to two and took the body's room. What doesn't fit goes
|
||||
// behind "More formatting", the least used first.
|
||||
|
||||
let toolbarWidth = 1200;
|
||||
const observers = new Set<ResizeObserverCallback>();
|
||||
class ReportingResizeObserver {
|
||||
constructor(private cb: ResizeObserverCallback) {}
|
||||
observe() { observers.add(this.cb); }
|
||||
unobserve() {}
|
||||
disconnect() { observers.delete(this.cb); }
|
||||
}
|
||||
vi.stubGlobal('ResizeObserver', ReportingResizeObserver);
|
||||
|
||||
const realClientWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'clientWidth')!;
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get(this: HTMLElement) {
|
||||
return this.getAttribute('role') === 'toolbar' ? toolbarWidth : realClientWidth.get!.call(this);
|
||||
},
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
toolbarWidth = 1200;
|
||||
});
|
||||
|
||||
async function setup() {
|
||||
let ready = false;
|
||||
render(<RichTextEditor content="<p>Hello</p>" onChange={() => {}} onEditorReady={() => { ready = true; }} />);
|
||||
await waitFor(() => expect(ready).toBe(true));
|
||||
}
|
||||
|
||||
async function resize(width: number) {
|
||||
toolbarWidth = width;
|
||||
await act(async () => {
|
||||
for (const cb of observers) cb([], {} as ResizeObserver);
|
||||
});
|
||||
}
|
||||
|
||||
const toolbar = () => screen.getByRole('toolbar', { name: 'formatting' });
|
||||
const rowItem = (name: string) => toolbar().querySelector(`[data-toolbar-item="${name}"]`);
|
||||
|
||||
describe('formatting toolbar in a narrow composer', () => {
|
||||
it('shows every button in one row when there is room, without a More button', async () => {
|
||||
await setup();
|
||||
for (const name of ['heading-1', 'quote', 'align-left', 'table', 'undo']) expect(rowItem(name)).not.toBeNull();
|
||||
expect(rowItem('more-formatting')).toBeNull();
|
||||
});
|
||||
|
||||
it('moves what does not fit behind "More formatting", and shows it there', async () => {
|
||||
await setup();
|
||||
await resize(561);
|
||||
|
||||
// The least used go first: undo, redo, clear formatting, then alignment.
|
||||
for (const name of ['undo', 'redo', 'clear-formatting', 'align-left', 'align-right']) expect(rowItem(name)).toBeNull();
|
||||
for (const name of ['bold', 'bullet-list', 'link', 'heading-1', 'table']) expect(rowItem(name)).not.toBeNull();
|
||||
|
||||
const more = rowItem('more-formatting') as HTMLButtonElement;
|
||||
expect(more).toHaveAccessibleName('more_formatting');
|
||||
expect(more).toHaveAttribute('aria-expanded', 'false');
|
||||
fireEvent.click(more);
|
||||
const panel = screen.getByRole('dialog', { name: 'more_formatting' });
|
||||
expect(more).toHaveAttribute('aria-expanded', 'true');
|
||||
for (const name of ['align-left', 'align-center', 'align-right', 'clear-formatting', 'undo', 'redo']) {
|
||||
expect(within(panel).getByRole('button', { name: new RegExp(`^${name.replace('-', '_')}`) })).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps the arrows in the row: the panel buttons are no Tab stops of the toolbar', async () => {
|
||||
await setup();
|
||||
await resize(561);
|
||||
fireEvent.click(rowItem('more-formatting')!);
|
||||
const panel = screen.getByRole('dialog', { name: 'more_formatting' });
|
||||
const rowStops = Array.from(toolbar().querySelectorAll<HTMLElement>('[data-toolbar-item]'))
|
||||
.filter((el) => !panel.contains(el) && el.tabIndex === 0);
|
||||
expect(rowStops).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('brings everything back into the row when the composer gets wider', async () => {
|
||||
await setup();
|
||||
await resize(400);
|
||||
expect(rowItem('heading-1')).toBeNull();
|
||||
await resize(1000);
|
||||
expect(rowItem('heading-1')).not.toBeNull();
|
||||
expect(rowItem('undo')).not.toBeNull();
|
||||
expect(rowItem('more-formatting')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('formattingFoldCount', () => {
|
||||
const sizes = { button: 28, gap: 2, separator: 5 };
|
||||
|
||||
it('folds nothing for a toolbar that is not laid out', () => {
|
||||
expect(formattingFoldCount(0, sizes)).toBe(0);
|
||||
});
|
||||
|
||||
it('folds more groups the narrower the row', () => {
|
||||
const counts = [900, 700, 561, 480, 400, 300].map((width) => formattingFoldCount(width, sizes));
|
||||
expect(counts[0]).toBe(0);
|
||||
for (let i = 1; i < counts.length; i++) expect(counts[i]).toBeGreaterThanOrEqual(counts[i - 1]);
|
||||
expect(counts[counts.length - 1]).toBe(5);
|
||||
});
|
||||
|
||||
it('makes room for the text direction button', () => {
|
||||
const width = 700;
|
||||
expect(formattingFoldCount(width, { ...sizes, directionButton: true })).toBeGreaterThanOrEqual(formattingFoldCount(width, sizes));
|
||||
});
|
||||
});
|
||||
@@ -70,8 +70,56 @@ import {
|
||||
Rows3,
|
||||
Columns3,
|
||||
ImageIcon,
|
||||
Ellipsis,
|
||||
} from "@/components/icons";
|
||||
|
||||
/**
|
||||
* Groups of the formatting toolbar that move behind "More formatting" when one
|
||||
* row is too narrow for all of them, the least used first. Each with its
|
||||
* buttons and the separators that go with it.
|
||||
*/
|
||||
const FOLDABLE_GROUPS = [
|
||||
{ id: "history", buttons: 3, separators: 2 },
|
||||
{ id: "align", buttons: 3, separators: 1 },
|
||||
{ id: "headings", buttons: 2, separators: 1 },
|
||||
{ id: "table", buttons: 1, separators: 0 },
|
||||
{ id: "blocks", buttons: 2, separators: 0 },
|
||||
] as const;
|
||||
type FoldableGroup = (typeof FOLDABLE_GROUPS)[number]["id"];
|
||||
/**
|
||||
* Always in the row: font size, bold, italic, underline, strikethrough, the
|
||||
* two colours, the lists, link, an image's description and More (12), with
|
||||
* their 3 separators.
|
||||
*/
|
||||
const FIXED_TOOLBAR = { buttons: 12, separators: 3 };
|
||||
|
||||
/**
|
||||
* How many of FOLDABLE_GROUPS go behind "More formatting" for one row of
|
||||
* `width` px. 0 for a toolbar that isn't laid out (hidden, or not measured).
|
||||
*/
|
||||
export function formattingFoldCount(width: number, sizes: { button: number; gap: number; separator: number; directionButton?: boolean }): number {
|
||||
if (!(width > 0)) return 0;
|
||||
const span = (buttons: number, separators: number) => buttons * (sizes.button + sizes.gap) + separators * (sizes.separator + sizes.gap);
|
||||
const groups = FOLDABLE_GROUPS.map((group) => span(group.buttons + (group.id === "align" && sizes.directionButton ? 1 : 0), group.separators));
|
||||
let needed = span(FIXED_TOOLBAR.buttons, FIXED_TOOLBAR.separators) + groups.reduce((sum, group) => sum + group, 0);
|
||||
// All of it fits without the More button.
|
||||
if (needed - (sizes.button + sizes.gap) <= width + 1) return 0;
|
||||
for (let count = 1; count <= groups.length; count++) {
|
||||
needed -= groups[count - 1];
|
||||
if (needed <= width + 1) return count;
|
||||
}
|
||||
return groups.length;
|
||||
}
|
||||
|
||||
/** The toolbar's inner width and the sizes of what is in it, in px. */
|
||||
function measureFormattingToolbar(toolbar: HTMLElement, directionButton: boolean): number {
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
||||
const style = getComputedStyle(toolbar);
|
||||
const width = toolbar.clientWidth - (parseFloat(style.paddingLeft) || 0) - (parseFloat(style.paddingRight) || 0);
|
||||
const button = toolbar.querySelector<HTMLElement>(":scope > [data-toolbar-item], :scope > div > [data-toolbar-item]")?.offsetWidth || 1.75 * rem;
|
||||
return formattingFoldCount(width, { button, gap: 0.125 * rem, separator: 1 + 0.25 * rem, directionButton });
|
||||
}
|
||||
|
||||
export interface InlineImageUpload {
|
||||
src: string;
|
||||
cid?: string;
|
||||
@@ -699,6 +747,7 @@ export function RichTextEditor({
|
||||
}, [editor, linkDialog]);
|
||||
|
||||
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);
|
||||
@@ -717,11 +766,13 @@ export function RichTextEditor({
|
||||
// focus last keeps it, the arrows, Home and End move inside.
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
const rovingItemRef = useRef<string | null>(null);
|
||||
// Buttons in the "More formatting" panel are the panel's, with its own keys.
|
||||
const inRow = (el: HTMLElement) => !el.closest("[data-toolbar-more-panel]");
|
||||
const toolbarItems = () =>
|
||||
Array.from(toolbarRef.current?.querySelectorAll<HTMLButtonElement>("[data-toolbar-item]") ?? [])
|
||||
.filter((el) => !el.disabled);
|
||||
.filter((el) => !el.disabled && inRow(el));
|
||||
const syncToolbarTabStops = (focus?: HTMLButtonElement) => {
|
||||
const all = Array.from(toolbarRef.current?.querySelectorAll<HTMLButtonElement>("[data-toolbar-item]") ?? []);
|
||||
const all = Array.from(toolbarRef.current?.querySelectorAll<HTMLButtonElement>("[data-toolbar-item]") ?? []).filter(inRow);
|
||||
const enabled = all.filter((el) => !el.disabled);
|
||||
const current = focus ?? enabled.find((el) => el.dataset.toolbarItem === rovingItemRef.current) ?? enabled[0];
|
||||
if (current) rovingItemRef.current = current.dataset.toolbarItem ?? null;
|
||||
@@ -740,6 +791,36 @@ export function RichTextEditor({
|
||||
return !!stop && document.activeElement === stop;
|
||||
};
|
||||
|
||||
// One row: what doesn't fit goes behind "More formatting" (a wrapped
|
||||
// second row took the body's room in a narrow composer).
|
||||
const [foldCount, setFoldCount] = useState(0);
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const moreWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const hasEditor = !!editor;
|
||||
useLayoutEffect(() => {
|
||||
const toolbar = toolbarRef.current;
|
||||
if (!toolbar) return;
|
||||
const measure = () => setFoldCount(measureFormattingToolbar(toolbar, !!rtlEditingSupport));
|
||||
measure();
|
||||
if (typeof ResizeObserver === "undefined") return;
|
||||
const observer = new ResizeObserver(() => {
|
||||
measure();
|
||||
setMoreOpen(false);
|
||||
});
|
||||
observer.observe(toolbar);
|
||||
return () => observer.disconnect();
|
||||
}, [hasEditor, rtlEditingSupport]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!moreOpen) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (moreWrapperRef.current && !moreWrapperRef.current.contains(e.target as Node)) setMoreOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
return () => document.removeEventListener("mousedown", handler);
|
||||
}, [moreOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fontSizeMenuOpen) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
@@ -799,6 +880,15 @@ export function RichTextEditor({
|
||||
const altTarget = findAltTarget(editor, altRequest);
|
||||
const imageSelected = editor.isActive("image");
|
||||
|
||||
// A group that doesn't fit renders in the "More formatting" panel instead, in row order.
|
||||
const folded = new Set<FoldableGroup>(FOLDABLE_GROUPS.slice(0, foldCount).map((group) => group.id));
|
||||
const overflow: React.ReactNode[] = [];
|
||||
const place = (group: FoldableGroup, node: React.ReactNode) => {
|
||||
if (!folded.has(group)) return node;
|
||||
overflow.push(<React.Fragment key={group}>{node}</React.Fragment>);
|
||||
return null;
|
||||
};
|
||||
|
||||
const onToolbarKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
// Keys inside a popover are the popover's.
|
||||
@@ -819,7 +909,7 @@ export function RichTextEditor({
|
||||
};
|
||||
const onToolbarFocus = (e: React.FocusEvent<HTMLDivElement>) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target instanceof HTMLButtonElement && target.hasAttribute("data-toolbar-item")) syncToolbarTabStops(target);
|
||||
if (target instanceof HTMLButtonElement && target.hasAttribute("data-toolbar-item") && inRow(target)) syncToolbarTabStops(target);
|
||||
};
|
||||
|
||||
const renderPalette = (current: string | null, apply: (color: string) => void, removeLabel: string, remove: () => void) => (
|
||||
@@ -1084,6 +1174,7 @@ export function RichTextEditor({
|
||||
|
||||
<ToolbarSeparator />
|
||||
|
||||
{place("headings", <>
|
||||
<ToolbarButton
|
||||
item="heading-1"
|
||||
pressed={editor.isActive("heading", { level: 1 })}
|
||||
@@ -1102,6 +1193,7 @@ export function RichTextEditor({
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarSeparator />
|
||||
</>)}
|
||||
|
||||
<ToolbarButton
|
||||
item="bullet-list"
|
||||
@@ -1119,6 +1211,7 @@ export function RichTextEditor({
|
||||
>
|
||||
<ListOrdered className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
{place("blocks", <>
|
||||
<ToolbarButton
|
||||
item="quote"
|
||||
pressed={editor.isActive("blockquote")}
|
||||
@@ -1135,9 +1228,11 @@ export function RichTextEditor({
|
||||
>
|
||||
<Code className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
</>)}
|
||||
|
||||
<ToolbarSeparator />
|
||||
|
||||
{place("align", <>
|
||||
<ToolbarButton
|
||||
item="align-left"
|
||||
pressed={editor.isActive({ textAlign: "left" })}
|
||||
@@ -1181,6 +1276,7 @@ export function RichTextEditor({
|
||||
)}
|
||||
|
||||
<ToolbarSeparator />
|
||||
</>)}
|
||||
|
||||
<ToolbarButton
|
||||
item="link"
|
||||
@@ -1205,7 +1301,7 @@ export function RichTextEditor({
|
||||
</ToolbarButton>
|
||||
)}
|
||||
|
||||
<div ref={tableWrapperRef} className="relative">
|
||||
{place("table", <div ref={tableWrapperRef} className="relative">
|
||||
<ToolbarButton
|
||||
ref={tableButtonRef}
|
||||
item="table"
|
||||
@@ -1257,8 +1353,9 @@ export function RichTextEditor({
|
||||
</ToolbarPopover>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{place("history", <>
|
||||
<ToolbarSeparator />
|
||||
|
||||
<ToolbarButton
|
||||
@@ -1287,6 +1384,35 @@ export function RichTextEditor({
|
||||
>
|
||||
<Redo className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
</>)}
|
||||
|
||||
{overflow.length > 0 && (
|
||||
<div ref={moreWrapperRef} className="relative">
|
||||
<ToolbarButton
|
||||
ref={moreButtonRef}
|
||||
item="more-formatting"
|
||||
popup="dialog"
|
||||
expanded={moreOpen}
|
||||
onClick={() => setMoreOpen((v) => !v)}
|
||||
title={tWindows("more_formatting")}
|
||||
>
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</ToolbarButton>
|
||||
{moreOpen && (
|
||||
<ToolbarPopover
|
||||
label={tWindows("more_formatting")}
|
||||
role="dialog"
|
||||
triggerRef={moreButtonRef}
|
||||
onClose={() => setMoreOpen(false)}
|
||||
className="p-1 flex w-max max-w-[16rem] flex-wrap items-center gap-0.5"
|
||||
>
|
||||
<div data-toolbar-more-panel="" className="contents">
|
||||
{overflow}
|
||||
</div>
|
||||
</ToolbarPopover>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "تكتب هذه المفاتيح حرفًا على لوحة مفاتيحك، وسيتوقف عن العمل في كل التطبيقات. جرّب مفاتيح أخرى.",
|
||||
"reserved": "يستخدم النظام أو معظم التطبيقات هذه المفاتيح. جرّب مفاتيح أخرى."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "مزيد من التنسيق"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "تذكير",
|
||||
"snooze": "تأجيل {count} د",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Aquestes tecles escriuen un caràcter al teu teclat, i deixaria de funcionar a totes les aplicacions. Prova altres tecles.",
|
||||
"reserved": "El sistema o la majoria d'aplicacions fan servir aquestes tecles. Prova altres tecles."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Més format"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Recordatori",
|
||||
"snooze": "Posposa {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Tyto klávesy na vaší klávesnici píší znak, který by pak nefungoval v žádné aplikaci. Zkuste jiné klávesy.",
|
||||
"reserved": "Tyto klávesy používá systém nebo většina aplikací. Zkuste jiné klávesy."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Další formátování"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Připomenutí",
|
||||
"snooze": "Odložit o {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Disse taster skriver et tegn på dit tastatur, og det ville holde op med at virke i alle apps. Prøv andre taster.",
|
||||
"reserved": "Dit system eller de fleste apps bruger disse taster. Prøv andre taster."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Mere formatering"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Påmindelse",
|
||||
"snooze": "Udsæt {count} min.",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Diese Tasten erzeugen auf Ihrer Tastatur ein Zeichen, das dann in keiner App mehr funktionieren würde. Versuchen Sie andere Tasten.",
|
||||
"reserved": "Ihr System oder die meisten Apps verwenden diese Tasten. Versuchen Sie andere Tasten."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Weitere Formatierung"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Erinnerung",
|
||||
"snooze": "{count} Min. schlummern",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "These keys type a character on your keyboard, and it would stop working in every app. Try other keys.",
|
||||
"reserved": "Your system or most apps use these keys. Try other keys."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "More formatting"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Reminder",
|
||||
"snooze": "Snooze {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Estas teclas escriben un carácter en tu teclado, y dejaría de funcionar en todas las aplicaciones. Prueba otras teclas.",
|
||||
"reserved": "Tu sistema o la mayoría de las aplicaciones usan estas teclas. Prueba otras teclas."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Más formato"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Recordatorio",
|
||||
"snooze": "Posponer {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Need klahvid kirjutavad teie klaviatuuril märgi, mis lakkaks kõigis rakendustes töötamast. Proovige teisi klahve.",
|
||||
"reserved": "Neid klahve kasutab süsteem või enamik rakendusi. Proovige teisi klahve."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Rohkem vormindust"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Meeldetuletus",
|
||||
"snooze": "Lükka {count} min edasi",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "این کلیدها روی صفحهکلید شما نویسهای مینویسند و آن نویسه دیگر در هیچ برنامهای کار نمیکند. کلیدهای دیگری را امتحان کنید.",
|
||||
"reserved": "سیستم یا بیشتر برنامهها از این کلیدها استفاده میکنند. کلیدهای دیگری را امتحان کنید."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "قالببندی بیشتر"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "یادآوری",
|
||||
"snooze": "تعویق {count} دقیقه",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Ces touches produisent un caractère sur votre clavier, et il ne fonctionnerait plus dans aucune application. Essayez d'autres touches.",
|
||||
"reserved": "Votre système ou la plupart des applications utilisent ces touches. Essayez d'autres touches."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Plus de mise en forme"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Rappel",
|
||||
"snooze": "Reporter de {count} min",
|
||||
|
||||
@@ -7067,6 +7067,9 @@
|
||||
"types_character": "המקשים האלה מקלידים תו במקלדת שלכם, והוא יפסיק לעבוד בכל האפליקציות. נסו מקשים אחרים.",
|
||||
"reserved": "המערכת או רוב האפליקציות משתמשות במקשים האלה. נסו מקשים אחרים."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "עוד עיצוב"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "תזכורת",
|
||||
"snooze": "דחייה ב-{count} דק'",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Ezek a billentyűk karaktert írnak az Ön billentyűzetén, amely többé egyik alkalmazásban sem működne. Próbáljon más billentyűket.",
|
||||
"reserved": "Ezeket a billentyűket a rendszer vagy a legtöbb alkalmazás használja. Próbáljon más billentyűket."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "További formázás"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Emlékeztető",
|
||||
"snooze": "Halasztás {count} perccel",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Questi tasti scrivono un carattere sulla tua tastiera, che smetterebbe di funzionare in tutte le app. Prova altri tasti.",
|
||||
"reserved": "Il sistema o la maggior parte delle app usano questi tasti. Prova altri tasti."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Altre opzioni di formattazione"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Promemoria",
|
||||
"snooze": "Rimanda di {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "このキーはお使いのキーボードで文字を入力します。割り当てると、すべてのアプリでその文字を入力できなくなります。別のキーをお試しください。",
|
||||
"reserved": "このキーはシステムまたは多くのアプリが使用しています。別のキーをお試しください。"
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "その他の書式"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "リマインダー",
|
||||
"snooze": "{count} 分後に再通知",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "이 키는 사용 중인 키보드에서 문자를 입력하며, 지정하면 모든 앱에서 그 문자를 입력할 수 없게 됩니다. 다른 키를 사용해 보세요.",
|
||||
"reserved": "시스템 또는 대부분의 앱이 이 키를 사용합니다. 다른 키를 사용해 보세요."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "추가 서식"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "알림",
|
||||
"snooze": "{count}분 후 다시 알림",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Šie taustiņi jūsu tastatūrā raksta rakstzīmi, kas pārstātu darboties visās lietotnēs. Izmēģiniet citus taustiņus.",
|
||||
"reserved": "Šos taustiņus izmanto sistēma vai lielākā daļa lietotņu. Izmēģiniet citus taustiņus."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Vairāk formatēšanas"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Atgādinājums",
|
||||
"snooze": "Atlikt par {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Эдгээр товч таны гар дээр тэмдэгт бичдэг тул тэр тэмдэгт бүх аппад ажиллахаа болино. Өөр товч туршина уу.",
|
||||
"reserved": "Систем эсвэл ихэнх апп эдгээр товчийг ашигладаг. Өөр товч туршина уу."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Бусад хэлбэржүүлэлт"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Сануулга",
|
||||
"snooze": "{count} минут хойшлуулах",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Disse tastene skriver et tegn på tastaturet ditt, og det ville slutte å virke i alle apper. Prøv andre taster.",
|
||||
"reserved": "Systemet ditt eller de fleste apper bruker disse tastene. Prøv andre taster."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Mer formatering"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Påminnelse",
|
||||
"snooze": "Utsett {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Deze toetsen typen een teken op je toetsenbord, en dat zou in geen enkele app meer werken. Probeer andere toetsen.",
|
||||
"reserved": "Je systeem of de meeste apps gebruiken deze toetsen. Probeer andere toetsen."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Meer opmaak"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Herinnering",
|
||||
"snooze": "{count} min uitstellen",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Te klawisze wpisują znak na Twojej klawiaturze i przestałby on działać we wszystkich aplikacjach. Wypróbuj inne klawisze.",
|
||||
"reserved": "Te klawisze są używane przez system lub większość aplikacji. Wypróbuj inne klawisze."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Więcej formatowania"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Przypomnienie",
|
||||
"snooze": "Odłóż o {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Estas teclas escrevem um carácter no seu teclado, e ele deixaria de funcionar em todas as aplicações. Experimente outras teclas.",
|
||||
"reserved": "O seu sistema ou a maioria das aplicações usam estas teclas. Experimente outras teclas."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Mais formatação"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Lembrete",
|
||||
"snooze": "Adiar {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Aceste taste scriu un caracter pe tastatura ta, iar el n-ar mai funcționa în nicio aplicație. Încearcă alte taste.",
|
||||
"reserved": "Sistemul sau majoritatea aplicațiilor folosesc aceste taste. Încearcă alte taste."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Mai multe opțiuni de formatare"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Memento",
|
||||
"snooze": "Amână {count} min",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "На вашей клавиатуре эти клавиши вводят символ, и он перестал бы работать во всех приложениях. Попробуйте другие клавиши.",
|
||||
"reserved": "Эти клавиши использует система или большинство приложений. Попробуйте другие клавиши."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Дополнительное форматирование"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Напоминание",
|
||||
"snooze": "Отложить на {count} мин",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Tieto klávesy na vašej klávesnici píšu znak, ktorý by potom nefungoval v žiadnej aplikácii. Skúste iné klávesy.",
|
||||
"reserved": "Tieto klávesy používa systém alebo väčšina aplikácií. Skúste iné klávesy."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Ďalšie formátovanie"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Pripomienka",
|
||||
"snooze": "Odložiť o {count} min",
|
||||
|
||||
@@ -7074,6 +7074,9 @@
|
||||
"types_character": "ปุ่มเหล่านี้พิมพ์อักขระบนแป้นพิมพ์ของคุณ และอักขระนั้นจะใช้ไม่ได้ในทุกแอป ลองใช้ปุ่มอื่น",
|
||||
"reserved": "ระบบหรือแอปส่วนใหญ่ใช้ปุ่มเหล่านี้ ลองใช้ปุ่มอื่น"
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "การจัดรูปแบบเพิ่มเติม"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "การแจ้งเตือน",
|
||||
"snooze": "เลื่อน {count} นาที",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "Bu tuşlar klavyenizde bir karakter yazar ve bu karakter hiçbir uygulamada çalışmaz olur. Başka tuşlar deneyin.",
|
||||
"reserved": "Bu tuşları sisteminiz veya çoğu uygulama kullanıyor. Başka tuşlar deneyin."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Daha fazla biçimlendirme"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Anımsatıcı",
|
||||
"snooze": "{count} dk ertele",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "На вашій клавіатурі ці клавіші вводять символ, і він перестав би працювати в усіх застосунках. Спробуйте інші клавіші.",
|
||||
"reserved": "Ці клавіші використовує система або більшість застосунків. Спробуйте інші клавіші."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Додаткове форматування"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Нагадування",
|
||||
"snooze": "Відкласти на {count} хв",
|
||||
|
||||
@@ -7078,6 +7078,9 @@
|
||||
"types_character": "Các phím này gõ một ký tự trên bàn phím của bạn, và ký tự đó sẽ không dùng được trong mọi ứng dụng. Hãy thử phím khác.",
|
||||
"reserved": "Hệ thống hoặc hầu hết ứng dụng dùng các phím này. Hãy thử phím khác."
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "Thêm định dạng"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "Lời nhắc",
|
||||
"snooze": "Báo lại sau {count} phút",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "這些鍵在您的鍵盤上會輸入一個字元,占用後該字元在所有應用程式中都無法輸入。請試試其他按鍵。",
|
||||
"reserved": "系統或大多數應用程式正在使用這些鍵。請試試其他按鍵。"
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "更多格式"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "提醒",
|
||||
"snooze": "{count} 分鐘後再提醒",
|
||||
|
||||
@@ -7066,6 +7066,9 @@
|
||||
"types_character": "这些键在您的键盘上会输入一个字符,占用后该字符在所有应用中都无法输入。请试试其他按键。",
|
||||
"reserved": "系统或大多数应用在使用这些键。请试试其他按键。"
|
||||
},
|
||||
"compose_windows": {
|
||||
"more_formatting": "更多格式"
|
||||
},
|
||||
"desktopReminders": {
|
||||
"generic_title": "提醒",
|
||||
"snooze": "{count} 分钟后再提醒",
|
||||
|
||||
Reference in New Issue
Block a user