diff --git a/components/command-palette/__tests__/command-palette.test.tsx b/components/command-palette/__tests__/command-palette.test.tsx index cc87ef4b1..7ccfc07b3 100644 --- a/components/command-palette/__tests__/command-palette.test.tsx +++ b/components/command-palette/__tests__/command-palette.test.tsx @@ -88,7 +88,8 @@ describe('command palette', () => { render(); pressCtrlK(); - const labels = screen.getAllByRole('option').map((o) => o.textContent); + // The label, without the key hint after it. + const labels = screen.getAllByRole('option').map((o) => o.querySelector('.flex-1')?.textContent); expect(labels).toContain('go_to(mail)'); expect(labels).toContain('go_to(settings)'); expect(labels).toContain('compose'); diff --git a/components/command-palette/__tests__/command-registry.test.tsx b/components/command-palette/__tests__/command-registry.test.tsx new file mode 100644 index 000000000..5a440c1e7 --- /dev/null +++ b/components/command-palette/__tests__/command-registry.test.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const nav = vi.hoisted(() => ({ pathname: '/', push: vi.fn() })); + +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string, params?: Record) => + params ? `${key}(${Object.values(params).join(',')})` : key, + useLocale: () => 'en', +})); +vi.mock('@/i18n/navigation', () => ({ + useRouter: () => ({ push: nav.push, back: vi.fn(), replace: vi.fn() }), + usePathname: () => nav.pathname, + Link: ({ children }: { children: React.ReactNode }) => children, +})); +vi.mock('@/components/keyboard-shortcuts-modal', () => ({ + KeyboardShortcutsModal: ({ isOpen }: { isOpen: boolean }) => (isOpen ?
shortcuts help
: null), +})); +vi.mock('@/components/global-search/global-search-palette', () => ({ + GlobalSearchPalette: () => null, +})); + +const { CommandPaletteHost } = await import('../command-palette'); +const { useRegisterCommands, commandMatches } = await import('@/lib/command-registry'); +const { useAuthStore } = await import('@/stores/auth-store'); +const { useAccountStore } = await import('@/stores/account-store'); +const { usePolicyStore } = await import('@/stores/policy-store'); +const { useGlobalSearchStore } = await import('@/stores/global-search-store'); +const { DEFAULT_POLICY } = await import('@/lib/admin/types'); +const { Plus } = await import('@/components/icons'); + +const pressCtrlK = () => act(() => { fireEvent.keyDown(document.body, { key: 'k', code: 'KeyK', ctrlKey: true }); }); +const optionIds = () => screen.getAllByRole('option').map((o) => o.getAttribute('data-command-id')); +const search = (text: string) => act(() => { fireEvent.change(screen.getByRole('combobox'), { target: { value: text } }); }); + +function CalendarApp({ newEvent = () => {}, todayVisible = true }: { newEvent?: () => void; todayVisible?: boolean }) { + useRegisterCommands('Calendar', [ + { id: 'new-event', label: 'New event', icon: Plus, keywords: ['appointment', 'meeting'], shortcut: 'c', run: newEvent }, + { id: 'today', label: 'Go to today', run: () => {}, when: () => todayVisible }, + ]); + return null; +} + +beforeEach(() => { + nav.pathname = '/calendar'; + nav.push.mockClear(); + useAuthStore.setState({ isAuthenticated: true, client: null, activeAccountId: 'a' } as never); + useAccountStore.setState({ accounts: [] } as never); + usePolicyStore.setState({ policy: { ...DEFAULT_POLICY } }); + useGlobalSearchStore.setState({ isOpen: false }); +}); + +describe('commands an app registers', () => { + it('are listed above the global ones while the app is mounted, and go when it unmounts', () => { + const { rerender } = render(<>); + pressCtrlK(); + const ids = optionIds(); + expect(ids.slice(0, 2)).toEqual(['app:Calendar:new-event', 'app:Calendar:today']); + expect(ids.indexOf('app:Calendar:today')).toBeLessThan(ids.indexOf('go-mail')); + expect(screen.getByText('Calendar')).toBeInTheDocument(); + act(() => { fireEvent.keyDown(screen.getByRole('combobox'), { key: 'Escape' }); }); + + rerender(); + pressCtrlK(); + expect(optionIds()).not.toContain('app:Calendar:new-event'); + expect(screen.queryByText('Calendar')).toBeNull(); + }); + + it('are found by label and keywords and run from the palette', () => { + const newEvent = vi.fn(); + render(<>); + pressCtrlK(); + search('appoint'); + expect(optionIds()).toEqual(['app:Calendar:new-event']); + search('new event'); + expect(optionIds()).toContain('app:Calendar:new-event'); + act(() => { fireEvent.keyDown(screen.getByRole('combobox'), { key: 'Enter' }); }); + expect(newEvent).toHaveBeenCalledTimes(1); + expect(screen.queryByRole('dialog')).toBeNull(); + }); + + it('leave out commands whose `when` is false', () => { + render(<>); + pressCtrlK(); + expect(optionIds()).not.toContain('app:Calendar:today'); + expect(optionIds()).toContain('app:Calendar:new-event'); + }); + + it('match every word in any order', () => { + expect(commandMatches('event new', 'New event')).toBe(true); + expect(commandMatches('meeting', 'New event', ['appointment', 'meeting'])).toBe(true); + expect(commandMatches('event task', 'New event')).toBe(false); + }); +}); + +describe('global commands', () => { + it('opens the global search from "Search everything"', () => { + render(); + pressCtrlK(); + act(() => { fireEvent.click(screen.getByRole('option', { name: 'title' })); }); + expect(useGlobalSearchStore.getState().isOpen).toBe(true); + }); + + it('switches to another account and signs out', () => { + const switchAccount = vi.fn().mockResolvedValue(undefined); + const logout = vi.fn().mockResolvedValue(undefined); + useAuthStore.setState({ switchAccount, logout } as never); + useAccountStore.setState({ + accounts: [ + { id: 'a', label: '', email: 'me@example.com', username: 'me', isConnected: true }, + { id: 'b', label: 'Work', email: 'me@work.example', username: 'me', isConnected: true }, + ], + } as never); + render(); + pressCtrlK(); + expect(screen.queryByRole('option', { name: /me@example\.com/ })).toBeNull(); + act(() => { fireEvent.click(screen.getByRole('option', { name: 'switch_to(Work)' })); }); + expect(switchAccount).toHaveBeenCalledWith('b'); + + pressCtrlK(); + act(() => { fireEvent.click(screen.getByRole('option', { name: 'sign_out' })); }); + expect(logout).toHaveBeenCalled(); + }); +}); diff --git a/components/command-palette/command-palette.tsx b/components/command-palette/command-palette.tsx index 0d2ffeabe..9bad0d600 100644 --- a/components/command-palette/command-palette.tsx +++ b/components/command-palette/command-palette.tsx @@ -7,25 +7,33 @@ import { Folder, Users, Keyboard, + LogOut, Mail, Moon, - PenLine, + PenSquare, Puzzle, Search, Settings, Sun, + UserCircle, iconForName, } from "@/components/icons"; import type { AppIcon } from "@/components/icons"; import { usePathname, useRouter } from "@/i18n/navigation"; import { useAuthStore } from "@/stores/auth-store"; +import { useAccountStore } from "@/stores/account-store"; +import { useGlobalSearchStore } from "@/stores/global-search-store"; import { useCalendarStore } from "@/stores/calendar-store"; import { usePolicyStore } from "@/stores/policy-store"; import { useThemeStore } from "@/stores/theme-store"; import { usePluginActions } from "@/components/plugins/plugin-actions"; import { KeyboardShortcutsModal } from "@/components/keyboard-shortcuts-modal"; +import { GlobalSearchPalette } from "@/components/global-search/global-search-palette"; import { runAction } from "@/lib/plugin-sandbox/actions"; import { COMMAND_PALETTE_OPEN_EVENT, parkAppCommand, requestAppCommand, type AppCommand } from "@/lib/app-commands"; +import { commandMatches, useRegisteredCommandGroups } from "@/lib/command-registry"; +import { shortcutAlternatives } from "@/lib/shortcut-keys"; +import { toUnicodeEmail } from "@/lib/idn"; import { isEditableEventTarget } from "@/lib/keyboard"; import { useFocusTrap } from "@/hooks/use-focus-trap"; import { cn } from "@/lib/utils"; @@ -36,7 +44,11 @@ interface PaletteCommand { icon: AppIcon; /** Shown after the label: the plugin that added the entry. */ source?: string; - group: "navigation" | "actions" | "plugins"; + /** The heading of the group the command is listed under. */ + group: string; + keywords?: readonly string[]; + /** Key hint at the end of the row (devices with a keyboard only). */ + shortcut?: string; run: () => void; } @@ -56,7 +68,8 @@ export function CommandPaletteHost() { const pathname = usePathname(); const [open, setOpen] = useState(false); const [showShortcuts, setShowShortcuts] = useState(false); - const enabled = isAuthenticated && !isProShell(pathname); + const proShell = isProShell(pathname); + const enabled = isAuthenticated && !proShell; useEffect(() => { if (!enabled) return; @@ -84,6 +97,8 @@ export function CommandPaletteHost() { return ( <> {open && setOpen(false)} onShowShortcuts={() => setShowShortcuts(true)} />} + {/* "Search everything" in the standard shell; the Pro shell mounts its own. */} + {enabled && } setShowShortcuts(false)} /> ); @@ -92,6 +107,7 @@ export function CommandPaletteHost() { function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onShowShortcuts: () => void }) { const t = useTranslations("command_palette"); const tSidebar = useTranslations("sidebar"); + const tGlobalSearch = useTranslations("global_search"); const router = useRouter(); const pathname = usePathname(); const listId = useId(); @@ -106,6 +122,9 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS const colorModeLocked = usePolicyStore((s) => s.getColorModePolicy().locked); const resolvedTheme = useThemeStore((s) => s.resolvedTheme); const pluginActions = usePluginActions(surface); + const registeredGroups = useRegisteredCommandGroups(); + const accounts = useAccountStore((s) => s.accounts); + const activeAccountId = useAuthStore((s) => s.activeAccountId); const dialogRef = useFocusTrap({ isActive: true, onEscape: onClose, restoreFocus: true }); @@ -117,38 +136,73 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS parkAppCommand(command); router.push("/"); }; - const list: PaletteCommand[] = [ - { id: "go-mail", group: "navigation", icon: Mail, label: t("go_to", { name: tSidebar("mail") }), run: go("/") }, - ]; + const navigation = t("group_navigation"); + const actions = t("group_actions"); + const account = t("group_account"); + // The current app's own commands come first (useRegisterCommands). + const list: PaletteCommand[] = registeredGroups.flatMap((group) => + group.commands.map((command) => ({ + id: `app:${group.label}:${command.id}`, + group: group.label, + icon: command.icon ?? Puzzle, + label: command.label, + keywords: command.keywords, + shortcut: command.shortcut, + run: command.run, + })), + ); + list.push({ id: "go-mail", group: navigation, icon: Mail, label: t("go_to", { name: tSidebar("mail") }), run: go("/") }); if (supportsCalendar && isFeatureEnabled("calendarEnabled")) { - list.push({ id: "go-calendar", group: "navigation", icon: CalendarEvent, label: t("go_to", { name: tSidebar("calendar") }), run: go("/calendar") }); + list.push({ id: "go-calendar", group: navigation, icon: CalendarEvent, label: t("go_to", { name: tSidebar("calendar") }), run: go("/calendar") }); } if ((client?.supportsContacts?.() ?? false) && isFeatureEnabled("contactsEnabled")) { - list.push({ id: "go-contacts", group: "navigation", icon: Users, label: t("go_to", { name: tSidebar("contacts") }), run: go("/contacts") }); + list.push({ id: "go-contacts", group: navigation, icon: Users, label: t("go_to", { name: tSidebar("contacts") }), run: go("/contacts") }); } if ((client?.supportsFiles?.() ?? false) && isFeatureEnabled("filesEnabled")) { - list.push({ id: "go-files", group: "navigation", icon: Folder, label: t("go_to", { name: tSidebar("files") }), run: go("/files") }); + list.push({ id: "go-files", group: navigation, icon: Folder, label: t("go_to", { name: tSidebar("files") }), run: go("/files") }); } list.push( - { id: "go-settings", group: "navigation", icon: Settings, label: t("go_to", { name: tSidebar("settings") }), run: go("/settings") }, - { id: "compose", group: "actions", icon: PenLine, label: t("compose"), run: onMail("compose") }, - { id: "search", group: "actions", icon: Search, label: t("search"), run: onMail("search") }, + { id: "go-settings", group: navigation, icon: Settings, label: t("go_to", { name: tSidebar("settings") }), run: go("/settings") }, + { id: "compose", group: actions, icon: PenSquare, label: t("compose"), shortcut: "c", run: onMail("compose") }, + { id: "search", group: actions, icon: Search, label: t("search"), shortcut: "/", run: onMail("search") }, + { + id: "search-everything", + group: actions, + icon: Search, + label: tGlobalSearch("title"), + keywords: [tSidebar("contacts"), tSidebar("calendar"), tSidebar("files")], + run: () => useGlobalSearchStore.getState().openPalette(), + }, ); // The theme policy can pin the color mode (#1280). if (!colorModeLocked) { list.push({ id: "theme", - group: "actions", + group: actions, icon: resolvedTheme === "dark" ? Sun : Moon, label: resolvedTheme === "dark" ? t("theme_light") : t("theme_dark"), run: () => useThemeStore.getState().setTheme(resolvedTheme === "dark" ? "light" : "dark"), }); } - list.push({ id: "shortcuts", group: "actions", icon: Keyboard, label: t("shortcuts"), run: onShowShortcuts }); + list.push({ id: "shortcuts", group: actions, icon: Keyboard, label: t("shortcuts"), shortcut: "?", run: onShowShortcuts }); + // Account: switch to each other signed-in account, sign out. + for (const entry of accounts) { + if (entry.id === activeAccountId) continue; + const name = entry.label || toUnicodeEmail(entry.email || entry.username); + list.push({ + id: `account:${entry.id}`, + group: account, + icon: UserCircle, + label: t("switch_to", { account: name }), + keywords: [tSidebar("switch_account"), entry.email], + run: () => { void useAuthStore.getState().switchAccount(entry.id); }, + }); + } + list.push({ id: "sign-out", group: account, icon: LogOut, label: tSidebar("sign_out"), run: () => { void useAuthStore.getState().logout(); } }); for (const action of pluginActions) { list.push({ id: `plugin:${action.pluginId}:${action.id}`, - group: "plugins", + group: t("group_plugins"), icon: iconForName(action.icon) ?? Puzzle, label: action.label, source: action.pluginName, @@ -156,11 +210,11 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS }); } return list; - }, [client, supportsCalendar, isFeatureEnabled, colorModeLocked, resolvedTheme, pluginActions, router, t, tSidebar, onShowShortcuts]); + }, [client, supportsCalendar, isFeatureEnabled, colorModeLocked, resolvedTheme, pluginActions, registeredGroups, accounts, activeAccountId, router, t, tSidebar, tGlobalSearch, onShowShortcuts]); - const needle = query.trim().toLowerCase(); + const needle = query.trim(); const visible = needle - ? commands.filter((c) => c.label.toLowerCase().includes(needle) || (c.source?.toLowerCase().includes(needle) ?? false)) + ? commands.filter((c) => commandMatches(needle, c.label, [...(c.keywords ?? []), ...(c.source ? [c.source] : [])])) : commands; const activeIndex = visible.length === 0 ? -1 : Math.min(active, visible.length - 1); @@ -203,9 +257,6 @@ function CommandPalette({ onClose, onShowShortcuts }: { onClose: () => void; onS } }; - const groupLabel = (group: PaletteCommand["group"]) => - group === "navigation" ? t("group_navigation") : group === "actions" ? t("group_actions") : t("group_plugins"); - return (
void; onS
  • {showGroup && ( )}
    void; onS {command.source && ( {command.source} )} + {command.shortcut && ( + // Key hints only where there is a keyboard (R8). + + )}
  • ); diff --git a/lib/command-registry.ts b/lib/command-registry.ts new file mode 100644 index 000000000..6a1b10a91 --- /dev/null +++ b/lib/command-registry.ts @@ -0,0 +1,101 @@ +'use client'; + +// Commands an app adds to the command palette (Ctrl/⌘+K) while it is +// mounted. The palette lists the registered groups above its own global +// commands and searches their labels and keywords. Registering happens in +// the app's component, so the commands come and go with the app. + +import { useEffect, useRef, useSyncExternalStore } from 'react'; +import type { AppIcon } from '@/components/icons'; + +export interface RegisteredCommand { + /** Unique within the group. */ + id: string; + /** Translated, sentence case: "New event", "Go to today". */ + label: string; + icon?: AppIcon; + /** More words the search matches, e.g. ["calendar", "appointment"]. Translated too. */ + keywords?: readonly string[]; + /** A key hint shown at the end of the row on devices with a keyboard, in the shortcut notation ("c", "Mod + Enter"). */ + shortcut?: string; + run: () => void; + /** Listed only while this returns true, checked each time the palette renders. */ + when?: () => boolean; +} + +export interface CommandGroup { + /** The group heading, translated ("Calendar"). Also identifies the group. */ + label: string; + commands: readonly RegisteredCommand[]; +} + +interface Registration { + token: object; + read: () => CommandGroup; +} + +let registrations: readonly Registration[] = []; +const listeners = new Set<() => void>(); + +function emit() { + for (const listener of listeners) listener(); +} + +function subscribe(listener: () => void) { + listeners.add(listener); + return () => listeners.delete(listener); +} + +const getSnapshot = () => registrations; +const getServerSnapshot = (): readonly Registration[] => []; + +/** Register a group; returns the function that removes it again. */ +export function registerCommands(read: () => CommandGroup): () => void { + const entry: Registration = { token: {}, read }; + registrations = [...registrations, entry]; + emit(); + return () => { + registrations = registrations.filter((r) => r !== entry); + emit(); + }; +} + +/** + * Add a group of commands to the palette while this component is mounted. + * The group and commands are read when the palette renders, so they can + * change every render without registering again. + * + * useRegisterCommands(t("sidebar.calendar"), [ + * { id: "new-event", label: t("calendar.new_event"), icon: Plus, keywords: [t("calendar.event")], shortcut: "c", run: openNewEvent }, + * { id: "today", label: t("calendar.today"), icon: CalendarEvent, run: goToToday, when: () => view !== "agenda" }, + * ]); + */ +export function useRegisterCommands(group: string, commands: readonly RegisteredCommand[], enabled = true): void { + const latest = useRef({ label: group, commands }); + useEffect(() => { + latest.current = { label: group, commands }; + }); + useEffect(() => { + if (!enabled) return; + return registerCommands(() => latest.current); + }, [enabled]); +} + +/** The groups registered right now, in the order they were registered, with `when` applied. */ +export function useRegisteredCommandGroups(): CommandGroup[] { + const regs = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); + return regs + .map((r) => { + const group = r.read(); + return { label: group.label, commands: group.commands.filter((c) => !c.when || c.when()) }; + }) + .filter((g) => g.commands.length > 0); +} + +/** Whether a command matches a search: every word of it in the label or the keywords. */ +export function commandMatches(needle: string, label: string, keywords: readonly string[] = []): boolean { + const words = needle.trim().toLowerCase().split(/\s+/).filter(Boolean); + if (words.length === 0) return true; + const haystack = [label, ...keywords].join(' ').toLowerCase(); + return words.every((w) => haystack.includes(w)); +} diff --git a/lib/shortcut-keys.ts b/lib/shortcut-keys.ts new file mode 100644 index 000000000..498365b86 --- /dev/null +++ b/lib/shortcut-keys.ts @@ -0,0 +1,34 @@ +// One notation for key combinations in the help modal and in hints: "Mod" +// (and the older "Ctrl/⌘", "Ctrl/Cmd" forms) is ⌘ on Apple devices and Ctrl +// everywhere else; Alt is ⌥ on Apple devices. Entries are written with +// "Mod + K", "Shift + Mod + Enter", alternatives separated by " / ". + +export function isApplePlatform(): boolean { + if (typeof navigator === 'undefined') return false; + const nav = navigator as Navigator & { userAgentData?: { platform?: string } }; + const platform = nav.userAgentData?.platform || nav.platform || nav.userAgent || ''; + return /mac|iphone|ipad|ipod/i.test(platform); +} + +const MOD = /^(mod|ctrl\/⌘|ctrl\/cmd|cmd\/ctrl|⌘\/ctrl)$/i; + +/** One key or combination ("Mod + Enter") in the notation of this platform. */ +export function formatShortcutCombo(combo: string, apple = isApplePlatform()): string { + // Keeps the spacing it was written with ("Ctrl+Shift+Y" from a plugin). + const separator = /\s\+\s/.test(combo) ? " + " : "+"; + return combo + .split(/\s*\+\s*/) + .map((part) => { + if (MOD.test(part)) return apple ? '⌘' : 'Ctrl'; + if (/^ctrl$/i.test(part)) return apple ? '⌘' : 'Ctrl'; + if (/^(cmd|⌘)$/i.test(part)) return apple ? '⌘' : 'Ctrl'; + if (/^alt$/i.test(part)) return apple ? '⌥' : 'Alt'; + return part; + }) + .join(separator); +} + +/** The alternatives of a shortcut ("j / ↓"), each in the notation of this platform. */ +export function shortcutAlternatives(key: string, apple = isApplePlatform()): string[] { + return key.split(' / ').map((combo) => formatShortcutCombo(combo, apple)); +} diff --git a/locales/ar/common.json b/locales/ar/common.json index 9d4014174..7a28d35b6 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -4567,14 +4567,16 @@ "placeholder": "اكتب أمرًا", "no_results": "لا توجد أوامر مطابقة", "go_to": "الانتقال إلى {name}", - "compose": "رسالة جديدة", + "compose": "إنشاء رسالة", "search": "البحث في البريد", "theme_dark": "التبديل إلى المظهر الداكن", "theme_light": "التبديل إلى المظهر الفاتح", "shortcuts": "عرض اختصارات لوحة المفاتيح", "group_navigation": "الانتقال إلى", "group_actions": "إجراءات", - "group_plugins": "الإضافات" + "group_plugins": "الإضافات", + "group_account": "الحساب", + "switch_to": "التبديل إلى {account}" }, "notification_center": { "title": "الإشعارات", diff --git a/locales/ca/common.json b/locales/ca/common.json index 22fa2483e..fb9de1d32 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Escriviu una ordre", "no_results": "Cap ordre coincident", "go_to": "Ves a {name}", - "compose": "Missatge nou", + "compose": "Redacta", "search": "Cerca al correu", "theme_dark": "Canvia al tema fosc", "theme_light": "Canvia al tema clar", "shortcuts": "Mostra les dreceres de teclat", "group_navigation": "Ves a", "group_actions": "Accions", - "group_plugins": "Connectors" + "group_plugins": "Connectors", + "group_account": "Compte", + "switch_to": "Canvia a {account}" }, "notification_center": { "title": "Notificacions", diff --git a/locales/cs/common.json b/locales/cs/common.json index 11c13ada8..463f624b4 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Zadejte příkaz", "no_results": "Žádné odpovídající příkazy", "go_to": "Přejít na {name}", - "compose": "Nová zpráva", + "compose": "Napsat", "search": "Hledat v poště", "theme_dark": "Přepnout na tmavý motiv", "theme_light": "Přepnout na světlý motiv", "shortcuts": "Zobrazit klávesové zkratky", "group_navigation": "Přejít na", "group_actions": "Akce", - "group_plugins": "Pluginy" + "group_plugins": "Pluginy", + "group_account": "Účet", + "switch_to": "Přepnout na {account}" }, "notification_center": { "title": "Oznámení", diff --git a/locales/da/common.json b/locales/da/common.json index cfc956144..a02bf02aa 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -4574,7 +4574,9 @@ "shortcuts": "Vis tastaturgenveje", "group_navigation": "Gå til", "group_actions": "Handlinger", - "group_plugins": "Plugins" + "group_plugins": "Plugins", + "group_account": "Konto", + "switch_to": "Skift til {account}" }, "notification_center": { "title": "Notifikationer", diff --git a/locales/de/common.json b/locales/de/common.json index 9be82dc88..bc6f7798e 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Befehl eingeben", "no_results": "Keine passenden Befehle", "go_to": "Zu {name} wechseln", - "compose": "Neue Nachricht", + "compose": "Verfassen", "search": "E-Mails durchsuchen", "theme_dark": "Dunkles Design verwenden", "theme_light": "Helles Design verwenden", "shortcuts": "Tastaturkürzel anzeigen", "group_navigation": "Wechseln zu", "group_actions": "Aktionen", - "group_plugins": "Plugins" + "group_plugins": "Plugins", + "group_account": "Konto", + "switch_to": "Zu {account} wechseln" }, "notification_center": { "title": "Benachrichtigungen", diff --git a/locales/en/common.json b/locales/en/common.json index ef96a3e74..9edc1a53e 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Type a command", "no_results": "No matching commands", "go_to": "Go to {name}", - "compose": "New message", + "compose": "Compose", "search": "Search mail", "theme_dark": "Switch to dark theme", "theme_light": "Switch to light theme", "shortcuts": "Show keyboard shortcuts", "group_navigation": "Go to", "group_actions": "Actions", - "group_plugins": "Plugins" + "group_plugins": "Plugins", + "group_account": "Account", + "switch_to": "Switch to {account}" }, "notification_center": { "title": "Notifications", diff --git a/locales/es/common.json b/locales/es/common.json index 0786a5e94..b3e7eb1e1 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Escribe un comando", "no_results": "No hay comandos que coincidan", "go_to": "Ir a {name}", - "compose": "Mensaje nuevo", + "compose": "Redactar", "search": "Buscar en el correo", "theme_dark": "Cambiar al tema oscuro", "theme_light": "Cambiar al tema claro", "shortcuts": "Mostrar atajos de teclado", "group_navigation": "Ir a", "group_actions": "Acciones", - "group_plugins": "Complementos" + "group_plugins": "Complementos", + "group_account": "Cuenta", + "switch_to": "Cambiar a {account}" }, "notification_center": { "title": "Notificaciones", diff --git a/locales/et/common.json b/locales/et/common.json index cd1ff559b..82292091f 100644 --- a/locales/et/common.json +++ b/locales/et/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Trüki käsk", "no_results": "Sobivaid käske pole", "go_to": "Mine: {name}", - "compose": "Uus kiri", + "compose": "Koosta", "search": "Otsi kirju", "theme_dark": "Lülitu tumedale teemale", "theme_light": "Lülitu heledale teemale", "shortcuts": "Näita kiirklahve", "group_navigation": "Mine", "group_actions": "Toimingud", - "group_plugins": "Pistikprogrammid" + "group_plugins": "Pistikprogrammid", + "group_account": "Konto", + "switch_to": "Lülitu kontole {account}" }, "notification_center": { "title": "Märguanded", diff --git a/locales/fa/common.json b/locales/fa/common.json index 2cf8f0db4..214f1c793 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -4567,14 +4567,16 @@ "placeholder": "یک فرمان بنویسید", "no_results": "فرمان منطبقی پیدا نشد", "go_to": "رفتن به {name}", - "compose": "پیام جدید", + "compose": "ایمیل جدید", "search": "جستجو در ایمیل", "theme_dark": "رفتن به پوسته تیره", "theme_light": "رفتن به پوسته روشن", "shortcuts": "نمایش میانبرهای صفحه کلید", "group_navigation": "رفتن به", "group_actions": "کارها", - "group_plugins": "افزونه‌ها" + "group_plugins": "افزونه‌ها", + "group_account": "حساب", + "switch_to": "رفتن به {account}" }, "notification_center": { "title": "اعلان‌ها", diff --git a/locales/fr/common.json b/locales/fr/common.json index 61f8abedf..10d6f2dfe 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Tapez une commande", "no_results": "Aucune commande correspondante", "go_to": "Aller à {name}", - "compose": "Nouveau message", + "compose": "Composer", "search": "Rechercher dans la messagerie", "theme_dark": "Passer au thème sombre", "theme_light": "Passer au thème clair", "shortcuts": "Afficher les raccourcis clavier", "group_navigation": "Aller à", "group_actions": "Actions", - "group_plugins": "Plugins" + "group_plugins": "Plugins", + "group_account": "Compte", + "switch_to": "Passer à {account}" }, "notification_center": { "title": "Notifications", diff --git a/locales/he/common.json b/locales/he/common.json index 983ddedd6..4ca1d3929 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -4568,14 +4568,16 @@ "placeholder": "הקלידו פקודה", "no_results": "אין פקודות תואמות", "go_to": "מעבר אל {name}", - "compose": "הודעה חדשה", + "compose": "כתיבה", "search": "חיפוש בדואר", "theme_dark": "מעבר לערכת נושא כהה", "theme_light": "מעבר לערכת נושא בהירה", "shortcuts": "הצגת קיצורי מקשים", "group_navigation": "מעבר אל", "group_actions": "פעולות", - "group_plugins": "תוספים" + "group_plugins": "תוספים", + "group_account": "חשבון", + "switch_to": "מעבר אל {account}" }, "notification_center": { "title": "התראות", diff --git a/locales/hu/common.json b/locales/hu/common.json index c61632ec8..b4530757b 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -4574,7 +4574,9 @@ "shortcuts": "Billentyűparancsok megjelenítése", "group_navigation": "Ugrás ide", "group_actions": "Műveletek", - "group_plugins": "Bővítmények" + "group_plugins": "Bővítmények", + "group_account": "Fiók", + "switch_to": "Váltás erre: {account}" }, "notification_center": { "title": "Értesítések", diff --git a/locales/it/common.json b/locales/it/common.json index e942c0d92..f09daade5 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Digita un comando", "no_results": "Nessun comando corrispondente", "go_to": "Vai a {name}", - "compose": "Nuovo messaggio", + "compose": "Scrivi", "search": "Cerca nella posta", "theme_dark": "Passa al tema scuro", "theme_light": "Passa al tema chiaro", "shortcuts": "Mostra le scorciatoie da tastiera", "group_navigation": "Vai a", "group_actions": "Azioni", - "group_plugins": "Plugin" + "group_plugins": "Plugin", + "group_account": "Account", + "switch_to": "Passa a {account}" }, "notification_center": { "title": "Notifiche", diff --git a/locales/ja/common.json b/locales/ja/common.json index 6685d7bd9..64865d541 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -4567,14 +4567,16 @@ "placeholder": "コマンドを入力", "no_results": "一致するコマンドはありません", "go_to": "{name}に移動", - "compose": "新規メッセージ", + "compose": "作成", "search": "メールを検索", "theme_dark": "ダークテーマに切り替え", "theme_light": "ライトテーマに切り替え", "shortcuts": "キーボードショートカットを表示", "group_navigation": "移動", "group_actions": "操作", - "group_plugins": "プラグイン" + "group_plugins": "プラグイン", + "group_account": "アカウント", + "switch_to": "{account} に切り替え" }, "notification_center": { "title": "通知", diff --git a/locales/ko/common.json b/locales/ko/common.json index 55ff3a530..5ac77dbec 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -4567,14 +4567,16 @@ "placeholder": "명령을 입력하세요", "no_results": "일치하는 명령이 없습니다", "go_to": "{name}(으)로 이동", - "compose": "새 메일", + "compose": "메일 쓰기", "search": "메일 검색", "theme_dark": "어두운 테마로 전환", "theme_light": "밝은 테마로 전환", "shortcuts": "단축키 보기", "group_navigation": "이동", "group_actions": "작업", - "group_plugins": "플러그인" + "group_plugins": "플러그인", + "group_account": "계정", + "switch_to": "{account}(으)로 전환" }, "notification_center": { "title": "알림", diff --git a/locales/lv/common.json b/locales/lv/common.json index 4b06f6852..de2e3b48b 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Ierakstiet komandu", "no_results": "Nav atbilstošu komandu", "go_to": "Pāriet uz: {name}", - "compose": "Jauna vēstule", + "compose": "Rakstīt", "search": "Meklēt pastā", "theme_dark": "Pārslēgt uz tumšo motīvu", "theme_light": "Pārslēgt uz gaišo motīvu", "shortcuts": "Rādīt tastatūras īsinājumtaustiņus", "group_navigation": "Pāriet uz", "group_actions": "Darbības", - "group_plugins": "Spraudņi" + "group_plugins": "Spraudņi", + "group_account": "Konts", + "switch_to": "Pārslēgties uz {account}" }, "notification_center": { "title": "Paziņojumi", diff --git a/locales/mn/common.json b/locales/mn/common.json index 8cfba34fc..5cb0e8ba9 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Команд бичнэ үү", "no_results": "Тохирох команд алга", "go_to": "{name} руу очих", - "compose": "Шинэ захидал", + "compose": "Бичилэх", "search": "Мэйлээс хайх", "theme_dark": "Бараан загвар руу шилжих", "theme_light": "Цайвар загвар руу шилжих", "shortcuts": "Гарын товчлолыг харуулах", "group_navigation": "Очих", "group_actions": "Үйлдлүүд", - "group_plugins": "Залгаасууд" + "group_plugins": "Залгаасууд", + "group_account": "Бүртгэл", + "switch_to": "{account} руу шилжих" }, "notification_center": { "title": "Мэдэгдэл", diff --git a/locales/nb/common.json b/locales/nb/common.json index ca25bfab4..e6abc3663 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Skriv en kommando", "no_results": "Ingen samsvarende kommandoer", "go_to": "Gå til {name}", - "compose": "Ny melding", + "compose": "Skriv", "search": "Søk i e-post", "theme_dark": "Bytt til mørkt tema", "theme_light": "Bytt til lyst tema", "shortcuts": "Vis tastatursnarveier", "group_navigation": "Gå til", "group_actions": "Handlinger", - "group_plugins": "Tillegg" + "group_plugins": "Tillegg", + "group_account": "Konto", + "switch_to": "Bytt til {account}" }, "notification_center": { "title": "Varsler", diff --git a/locales/nl/common.json b/locales/nl/common.json index f5ec92854..7307fc0e7 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -4574,7 +4574,9 @@ "shortcuts": "Sneltoetsen tonen", "group_navigation": "Naar", "group_actions": "Acties", - "group_plugins": "Plug-ins" + "group_plugins": "Plug-ins", + "group_account": "Account", + "switch_to": "Overschakelen naar {account}" }, "notification_center": { "title": "Meldingen", diff --git a/locales/pl/common.json b/locales/pl/common.json index 76e6ade5b..048e32f3b 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Wpisz polecenie", "no_results": "Brak pasujących poleceń", "go_to": "Przejdź do: {name}", - "compose": "Nowa wiadomość", + "compose": "Napisz", "search": "Szukaj w poczcie", "theme_dark": "Przełącz na ciemny motyw", "theme_light": "Przełącz na jasny motyw", "shortcuts": "Pokaż skróty klawiszowe", "group_navigation": "Przejdź do", "group_actions": "Działania", - "group_plugins": "Wtyczki" + "group_plugins": "Wtyczki", + "group_account": "Konto", + "switch_to": "Przełącz na {account}" }, "notification_center": { "title": "Powiadomienia", diff --git a/locales/pt/common.json b/locales/pt/common.json index 9aa227596..5079841a4 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Digite um comando", "no_results": "Nenhum comando correspondente", "go_to": "Ir para {name}", - "compose": "Nova mensagem", + "compose": "Escrever", "search": "Pesquisar no e-mail", "theme_dark": "Mudar para o tema escuro", "theme_light": "Mudar para o tema claro", "shortcuts": "Mostrar atalhos de teclado", "group_navigation": "Ir para", "group_actions": "Ações", - "group_plugins": "Plugins" + "group_plugins": "Plugins", + "group_account": "Conta", + "switch_to": "Mudar para {account}" }, "notification_center": { "title": "Notificações", diff --git a/locales/ro/common.json b/locales/ro/common.json index 3d87a124e..bad3efbe9 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Tastați o comandă", "no_results": "Nicio comandă potrivită", "go_to": "Mergi la {name}", - "compose": "Mesaj nou", + "compose": "Compune", "search": "Caută în e-mail", "theme_dark": "Comută la tema întunecată", "theme_light": "Comută la tema luminoasă", "shortcuts": "Afișează comenzile rapide de la tastatură", "group_navigation": "Mergi la", "group_actions": "Acțiuni", - "group_plugins": "Pluginuri" + "group_plugins": "Pluginuri", + "group_account": "Cont", + "switch_to": "Comută la {account}" }, "notification_center": { "title": "Notificări", diff --git a/locales/ru/common.json b/locales/ru/common.json index b41fa5cc2..5b20c7b0f 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Введите команду", "no_results": "Нет подходящих команд", "go_to": "Перейти: {name}", - "compose": "Новое письмо", + "compose": "Создать", "search": "Искать в почте", "theme_dark": "Включить тёмную тему", "theme_light": "Включить светлую тему", "shortcuts": "Показать сочетания клавиш", "group_navigation": "Перейти", "group_actions": "Действия", - "group_plugins": "Плагины" + "group_plugins": "Плагины", + "group_account": "Аккаунт", + "switch_to": "Переключиться на {account}" }, "notification_center": { "title": "Уведомления", diff --git a/locales/sk/common.json b/locales/sk/common.json index f15bfddd3..303cb32fd 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Zadajte príkaz", "no_results": "Žiadne zodpovedajúce príkazy", "go_to": "Prejsť na {name}", - "compose": "Nová správa", + "compose": "Napísať", "search": "Hľadať v pošte", "theme_dark": "Prepnúť na tmavý motív", "theme_light": "Prepnúť na svetlý motív", "shortcuts": "Zobraziť klávesové skratky", "group_navigation": "Prejsť na", "group_actions": "Akcie", - "group_plugins": "Doplnky" + "group_plugins": "Doplnky", + "group_account": "Účet", + "switch_to": "Prepnúť na {account}" }, "notification_center": { "title": "Oznámenia", diff --git a/locales/th/common.json b/locales/th/common.json index 88fb99ce2..c26810660 100644 --- a/locales/th/common.json +++ b/locales/th/common.json @@ -62,14 +62,16 @@ "placeholder": "พิมพ์คำสั่งหรือค้นหา...", "no_results": "ไม่พบคำสั่งที่ตรงกัน", "go_to": "ไปยัง {name}", - "compose": "เขียนจดหมายใหม่", + "compose": "เขียนจดหมาย", "search": "ค้นหาจดหมาย", "theme_dark": "เปลี่ยนเป็นธีมมืด", "theme_light": "เปลี่ยนเป็นธีมสว่าง", "shortcuts": "แสดงแป้นพิมพ์ลัด", "group_navigation": "ไปยัง", "group_actions": "การดำเนินการ", - "group_plugins": "ปลั๊กอิน" + "group_plugins": "ปลั๊กอิน", + "group_account": "บัญชี", + "switch_to": "สลับไปที่ {account}" }, "notification_center": { "title": "การแจ้งเตือน", diff --git a/locales/tr/common.json b/locales/tr/common.json index 8b497ec0e..d6adde4a7 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Bir komut yazın", "no_results": "Eşleşen komut yok", "go_to": "{name} bölümüne git", - "compose": "Yeni ileti", + "compose": "Yeni E-posta", "search": "Postada ara", "theme_dark": "Koyu temaya geç", "theme_light": "Açık temaya geç", "shortcuts": "Klavye kısayollarını göster", "group_navigation": "Git", "group_actions": "Eylemler", - "group_plugins": "Eklentiler" + "group_plugins": "Eklentiler", + "group_account": "Hesap", + "switch_to": "{account} hesabına geç" }, "notification_center": { "title": "Bildirimler", diff --git a/locales/uk/common.json b/locales/uk/common.json index 76268966c..182f17cbe 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -4567,14 +4567,16 @@ "placeholder": "Введіть команду", "no_results": "Немає відповідних команд", "go_to": "Перейти: {name}", - "compose": "Новий лист", + "compose": "Скласти", "search": "Шукати в пошті", "theme_dark": "Увімкнути темну тему", "theme_light": "Увімкнути світлу тему", "shortcuts": "Показати комбінації клавіш", "group_navigation": "Перейти", "group_actions": "Дії", - "group_plugins": "Плагіни" + "group_plugins": "Плагіни", + "group_account": "Обліковий запис", + "switch_to": "Перейти до {account}" }, "notification_center": { "title": "Сповіщення", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index e8662b6e2..4b59f807b 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -4567,14 +4567,16 @@ "placeholder": "輸入命令", "no_results": "沒有相符的命令", "go_to": "前往{name}", - "compose": "新郵件", + "compose": "撰寫郵件", "search": "搜尋郵件", "theme_dark": "切換為深色主題", "theme_light": "切換為淺色主題", "shortcuts": "顯示鍵盤快速鍵", "group_navigation": "前往", "group_actions": "動作", - "group_plugins": "外掛程式" + "group_plugins": "外掛程式", + "group_account": "帳戶", + "switch_to": "切換到 {account}" }, "notification_center": { "title": "通知", diff --git a/locales/zh/common.json b/locales/zh/common.json index 20709ebfd..552803ce4 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -4567,14 +4567,16 @@ "placeholder": "输入命令", "no_results": "没有匹配的命令", "go_to": "前往{name}", - "compose": "新邮件", + "compose": "撰写邮件", "search": "搜索邮件", "theme_dark": "切换到深色主题", "theme_light": "切换到浅色主题", "shortcuts": "显示键盘快捷键", "group_navigation": "前往", "group_actions": "操作", - "group_plugins": "插件" + "group_plugins": "插件", + "group_account": "账户", + "switch_to": "切换到 {account}" }, "notification_center": { "title": "通知",