feat: keep the formatting toolbar in one row, with the rest behind More formatting

This commit is contained in:
Linus Rath
2026-11-04 06:24:28 +01:00
parent 08dcff789f
commit c0300bcb7f
32 changed files with 335 additions and 5 deletions
@@ -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));
});
});
+131 -5
View File
@@ -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 */}
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "تكتب هذه المفاتيح حرفًا على لوحة مفاتيحك، وسيتوقف عن العمل في كل التطبيقات. جرّب مفاتيح أخرى.",
"reserved": "يستخدم النظام أو معظم التطبيقات هذه المفاتيح. جرّب مفاتيح أخرى."
},
"compose_windows": {
"more_formatting": "مزيد من التنسيق"
},
"desktopReminders": {
"generic_title": "تذكير",
"snooze": "تأجيل {count} د",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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.",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "این کلیدها روی صفحه‌کلید شما نویسه‌ای می‌نویسند و آن نویسه دیگر در هیچ برنامه‌ای کار نمی‌کند. کلیدهای دیگری را امتحان کنید.",
"reserved": "سیستم یا بیشتر برنامه‌ها از این کلیدها استفاده می‌کنند. کلیدهای دیگری را امتحان کنید."
},
"compose_windows": {
"more_formatting": "قالب‌بندی بیشتر"
},
"desktopReminders": {
"generic_title": "یادآوری",
"snooze": "تعویق {count} دقیقه",
+3
View File
@@ -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",
+3
View File
@@ -7067,6 +7067,9 @@
"types_character": "המקשים האלה מקלידים תו במקלדת שלכם, והוא יפסיק לעבוד בכל האפליקציות. נסו מקשים אחרים.",
"reserved": "המערכת או רוב האפליקציות משתמשות במקשים האלה. נסו מקשים אחרים."
},
"compose_windows": {
"more_formatting": "עוד עיצוב"
},
"desktopReminders": {
"generic_title": "תזכורת",
"snooze": "דחייה ב-{count} דק'",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "このキーはお使いのキーボードで文字を入力します。割り当てると、すべてのアプリでその文字を入力できなくなります。別のキーをお試しください。",
"reserved": "このキーはシステムまたは多くのアプリが使用しています。別のキーをお試しください。"
},
"compose_windows": {
"more_formatting": "その他の書式"
},
"desktopReminders": {
"generic_title": "リマインダー",
"snooze": "{count} 分後に再通知",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "이 키는 사용 중인 키보드에서 문자를 입력하며, 지정하면 모든 앱에서 그 문자를 입력할 수 없게 됩니다. 다른 키를 사용해 보세요.",
"reserved": "시스템 또는 대부분의 앱이 이 키를 사용합니다. 다른 키를 사용해 보세요."
},
"compose_windows": {
"more_formatting": "추가 서식"
},
"desktopReminders": {
"generic_title": "알림",
"snooze": "{count}분 후 다시 알림",
+3
View File
@@ -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",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "Эдгээр товч таны гар дээр тэмдэгт бичдэг тул тэр тэмдэгт бүх аппад ажиллахаа болино. Өөр товч туршина уу.",
"reserved": "Систем эсвэл ихэнх апп эдгээр товчийг ашигладаг. Өөр товч туршина уу."
},
"compose_windows": {
"more_formatting": "Бусад хэлбэржүүлэлт"
},
"desktopReminders": {
"generic_title": "Сануулга",
"snooze": "{count} минут хойшлуулах",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -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",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "На вашей клавиатуре эти клавиши вводят символ, и он перестал бы работать во всех приложениях. Попробуйте другие клавиши.",
"reserved": "Эти клавиши использует система или большинство приложений. Попробуйте другие клавиши."
},
"compose_windows": {
"more_formatting": "Дополнительное форматирование"
},
"desktopReminders": {
"generic_title": "Напоминание",
"snooze": "Отложить на {count} мин",
+3
View File
@@ -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",
+3
View File
@@ -7074,6 +7074,9 @@
"types_character": "ปุ่มเหล่านี้พิมพ์อักขระบนแป้นพิมพ์ของคุณ และอักขระนั้นจะใช้ไม่ได้ในทุกแอป ลองใช้ปุ่มอื่น",
"reserved": "ระบบหรือแอปส่วนใหญ่ใช้ปุ่มเหล่านี้ ลองใช้ปุ่มอื่น"
},
"compose_windows": {
"more_formatting": "การจัดรูปแบบเพิ่มเติม"
},
"desktopReminders": {
"generic_title": "การแจ้งเตือน",
"snooze": "เลื่อน {count} นาที",
+3
View File
@@ -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",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "На вашій клавіатурі ці клавіші вводять символ, і він перестав би працювати в усіх застосунках. Спробуйте інші клавіші.",
"reserved": "Ці клавіші використовує система або більшість застосунків. Спробуйте інші клавіші."
},
"compose_windows": {
"more_formatting": "Додаткове форматування"
},
"desktopReminders": {
"generic_title": "Нагадування",
"snooze": "Відкласти на {count} хв",
+3
View File
@@ -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",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "這些鍵在您的鍵盤上會輸入一個字元,占用後該字元在所有應用程式中都無法輸入。請試試其他按鍵。",
"reserved": "系統或大多數應用程式正在使用這些鍵。請試試其他按鍵。"
},
"compose_windows": {
"more_formatting": "更多格式"
},
"desktopReminders": {
"generic_title": "提醒",
"snooze": "{count} 分鐘後再提醒",
+3
View File
@@ -7066,6 +7066,9 @@
"types_character": "这些键在您的键盘上会输入一个字符,占用后该字符在所有应用中都无法输入。请试试其他按键。",
"reserved": "系统或大多数应用在使用这些键。请试试其他按键。"
},
"compose_windows": {
"more_formatting": "更多格式"
},
"desktopReminders": {
"generic_title": "提醒",
"snooze": "{count} 分钟后再提醒",