fix(web): align workspace switcher states and semantics (#41713)

This commit is contained in:
Jingyi 2026-09-03 07:18:49 +00:00 committed by GitHub
parent 8f7cca3ae2
commit 642ad2fffe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 314 additions and 86 deletions

View File

@ -194,6 +194,27 @@ describe('WorkspaceCard', () => {
expect(screen.getByRole('button', { name: workspaceMenuAccessibleName })).toBeInTheDocument()
})
it('keeps full workspace names on the final interactive title owner', async () => {
const user = userEvent.setup()
renderWorkspaceCard()
const trigger = screen.getByRole('button', { name: workspaceMenuAccessibleName })
expect(trigger).toHaveAttribute('title', 'Solar Studio')
expect(within(trigger).getByText('Solar Studio')).not.toHaveAttribute('title')
await user.click(trigger)
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
const workspaceItem = within(panel).getByRole('button', {
name: 'Evan Workspace',
})
expect(workspaceItem).toHaveAttribute('title', 'Evan Workspace')
expect(within(workspaceItem).getByText('Evan Workspace')).not.toHaveAttribute('title')
expect(
within(panel).getByRole('button', { name: 'common.mainNav.workspace.settings' }),
).toHaveAttribute('title', 'common.mainNav.workspace.settings')
})
it('includes the visible workspace plan in the menu trigger accessible name', () => {
renderWorkspaceCard({ systemFeatures: { deployment_edition: 'CLOUD' } })
@ -308,8 +329,12 @@ describe('WorkspaceCard', () => {
expect(
within(panel).getByRole('button', { name: 'common.mainNav.workspace.sort.openMenu' }),
).toBeDisabled()
expect(within(panel).getByRole('button', { name: 'common.operation.search' })).toBeDisabled()
expect(within(panel).getByRole('button', { name: 'common.operation.search' })).toHaveAttribute(
'aria-disabled',
'true',
)
expect(panel.querySelector('[aria-busy="true"]')).toBeInTheDocument()
expect(within(panel).getByRole('status', { name: 'common.loading' })).toBeInTheDocument()
expect(within(panel).queryByRole('button', { name: 'Evan Workspace' })).not.toBeInTheDocument()
})
@ -390,32 +415,79 @@ describe('WorkspaceCard', () => {
).toBeInTheDocument()
const workspaceItem = within(panel).getByRole('button', { name: 'Evan Workspace' })
expect(workspaceItem).toBeInTheDocument()
expect(workspaceItem.parentElement).toHaveClass('max-h-[240px]', 'overflow-y-auto')
const workspaceList = within(panel).getByRole('list', {
name: 'common.userProfile.workspace',
})
expect(within(workspaceList).getAllByRole('listitem')).toHaveLength(2)
expect(workspaceList.parentElement).toHaveClass('max-h-[240px]', 'overflow-y-auto')
})
it('filters workspace switcher options from the search action', async () => {
const user = userEvent.setup()
renderWorkspaceCard()
fireEvent.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.search' }))
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
const searchTrigger = await screen.findByRole('button', { name: 'common.operation.search' })
expect(searchTrigger).toHaveAttribute('aria-expanded', 'false')
await user.click(searchTrigger)
expect(screen.getByText('common.userProfile.workspace')).toBeInTheDocument()
expect(
screen.getByRole('button', { name: 'common.mainNav.workspace.sort.openMenu' }),
).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.operation.search' })).toHaveClass(
'bg-state-base-hover',
)
expect(searchTrigger).toHaveAttribute('aria-expanded', 'true')
const controlledPanelId = searchTrigger.getAttribute('aria-controls')
expect(controlledPanelId).toBeTruthy()
fireEvent.change(screen.getByPlaceholderText('common.mainNav.workspace.searchPlaceholder'), {
target: { value: 'evan' },
})
const searchInput = screen.getByPlaceholderText('common.mainNav.workspace.searchPlaceholder')
expect(document.getElementById(controlledPanelId!)).toContainElement(searchInput)
expect(searchInput).toHaveFocus()
await user.type(searchInput, 'evan')
const panel = screen.getByRole('dialog', { name: 'Solar Studio' })
expect(within(panel).getByRole('button', { name: 'Evan Workspace' })).toBeInTheDocument()
expect(within(panel).queryByRole('button', { name: 'Solar Studio' })).not.toBeInTheDocument()
})
it('announces an empty workspace search result', async () => {
const user = userEvent.setup()
renderWorkspaceCard()
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
await user.click(await screen.findByRole('button', { name: 'common.operation.search' }))
await user.type(
screen.getByPlaceholderText('common.mainNav.workspace.searchPlaceholder'),
'missing',
)
const panel = screen.getByRole('dialog', { name: 'Solar Studio' })
expect(within(panel).getByRole('status')).toHaveTextContent(
'common.mainNav.workspace.noResults',
)
expect(
within(panel).queryByRole('list', { name: 'common.userProfile.workspace' }),
).not.toBeInTheDocument()
})
it('exposes only the current workspace as current', async () => {
const user = userEvent.setup()
renderWorkspaceCard()
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
const workspaceList = within(panel).getByRole('list', {
name: 'common.userProfile.workspace',
})
expect(
within(workspaceList).getByRole('button', { name: 'Solar Studio', current: true }),
).toBeInTheDocument()
expect(
within(workspaceList).getByRole('button', { name: 'Evan Workspace' }),
).not.toHaveAttribute('aria-current')
})
it('sorts workspaces by last opened and can sort by created time', async () => {
mockWorkspaces = [
{
@ -451,7 +523,10 @@ describe('WorkspaceCard', () => {
fireEvent.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
const defaultWorkspaceOptions = within(panel)
const workspaceList = within(panel).getByRole('list', {
name: 'common.userProfile.workspace',
})
const defaultWorkspaceOptions = within(workspaceList)
.getAllByRole('button')
.map((item) => item.getAttribute('title'))
.filter(Boolean)
@ -476,7 +551,7 @@ describe('WorkspaceCard', () => {
screen.getByRole('menuitemradio', { name: 'common.mainNav.workspace.sort.createdTime' }),
)
const createdTimeWorkspaceOptions = within(panel)
const createdTimeWorkspaceOptions = within(workspaceList)
.getAllByRole('button')
.map((item) => item.getAttribute('title'))
.filter(Boolean)
@ -488,6 +563,39 @@ describe('WorkspaceCard', () => {
])
})
it('closes the nested sort menu before the workspace popover on Escape', async () => {
const user = userEvent.setup()
renderWorkspaceCard()
const workspaceTrigger = screen.getByRole('button', { name: workspaceMenuAccessibleName })
await user.click(workspaceTrigger)
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
const sortTrigger = within(panel).getByRole('button', {
name: 'common.mainNav.workspace.sort.openMenu',
})
await user.click(sortTrigger)
expect(
await screen.findByRole('menuitemradio', {
name: 'common.mainNav.workspace.sort.lastOpened',
}),
).toBeInTheDocument()
await user.keyboard('{Escape}')
expect(
screen.queryByRole('menuitemradio', {
name: 'common.mainNav.workspace.sort.lastOpened',
}),
).not.toBeInTheDocument()
expect(panel).toBeInTheDocument()
expect(sortTrigger).toHaveFocus()
await user.keyboard('{Escape}')
await waitFor(() => expect(panel).not.toBeInTheDocument())
expect(workspaceTrigger).toHaveFocus()
})
it('opens account settings from workspace menu actions', async () => {
renderWorkspaceCard()
@ -527,6 +635,20 @@ describe('WorkspaceCard', () => {
)
})
it('closes the popover without switching when the current workspace is selected', async () => {
const user = userEvent.setup()
renderWorkspaceCard()
await user.click(screen.getByRole('button', { name: workspaceMenuAccessibleName }))
const panel = await screen.findByRole('dialog', { name: 'Solar Studio' })
await user.click(within(panel).getByRole('button', { name: 'Solar Studio', current: true }))
await waitFor(() =>
expect(screen.queryByRole('dialog', { name: 'Solar Studio' })).not.toBeInTheDocument(),
)
expect(mockSwitchWorkspace).not.toHaveBeenCalled()
})
it('keeps workspace settings visible for dataset operators without member management permission', async () => {
mockCurrentWorkspaceQuery({
...currentWorkspaceValue,

View File

@ -0,0 +1,62 @@
import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import { page, userEvent } from 'vite-plus/test/browser'
import { render } from 'vitest-browser-react'
import { WorkspaceSwitcher } from '../workspace-switcher'
const workspaces: TenantListItemResponse[] = [
{
id: 'workspace-current',
name: 'Solar Studio',
plan: 'sandbox',
status: 'normal',
created_at: 2,
current: true,
},
{
id: 'workspace-other',
name: 'Solar Studio',
plan: 'team',
status: 'normal',
created_at: 1,
current: false,
},
]
describe('WorkspaceSwitcher visual states', () => {
it('keeps current, hover, and focus-visible as independent states', async () => {
// Chromium owns the rendered hover/focus-visible output that happy-dom cannot reproduce.
const screen = await render(
<WorkspaceSwitcher workspaces={workspaces} isPending={false} onSwitchWorkspace={vi.fn()} />,
)
const workspaceList = screen.getByRole('list', { name: 'common.userProfile.workspace' })
const workspaceItems = workspaceList.getByRole('listitem')
const current = workspaceItems.nth(0).getByRole('button')
const other = workspaceItems.nth(1).getByRole('button')
const captureList = () => page.screenshot({ element: workspaceList, save: false })
const currentResting = await page.screenshot({ element: current, save: false })
const otherResting = await page.screenshot({ element: other, save: false })
expect(currentResting).not.toBe(otherResting)
const resting = await captureList()
await other.hover()
const hover = await captureList()
expect(hover).not.toBe(resting)
await userEvent.unhover(other)
await current.hover()
const currentHover = await captureList()
expect(currentHover).not.toBe(resting)
expect(currentHover).not.toBe(hover)
await userEvent.unhover(current)
await userEvent.tab()
await userEvent.tab()
await userEvent.tab()
expect(current.element()).toHaveFocus()
const focusVisible = await captureList()
expect(focusVisible).not.toBe(resting)
expect(focusVisible).not.toBe(currentHover)
})
})

View File

@ -126,10 +126,7 @@ function WorkspaceCardTrigger({
</span>
<div className="min-w-0 grow">
<div className="flex min-w-0 items-center gap-1 pr-0.5">
<span
className="max-w-30 min-w-0 shrink truncate system-sm-medium text-text-primary"
title={name}
>
<span className="max-w-30 min-w-0 shrink truncate system-sm-medium text-text-primary">
{name}
</span>
{showStatus && <span className="flex shrink-0 items-center">{status}</span>}
@ -183,8 +180,8 @@ function WorkspaceMenuHeader({
name: string
status: ReactNode
showInviteMembers: boolean
settingsLabel: ReactNode
inviteMembersLabel: ReactNode
settingsLabel: string
inviteMembersLabel: string
onOpenSettings: () => void
onInviteMembers: () => void
}) {
@ -205,6 +202,7 @@ function WorkspaceMenuHeader({
</div>
<button
type="button"
title={settingsLabel}
className="flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
onClick={onOpenSettings}
>
@ -218,6 +216,7 @@ function WorkspaceMenuHeader({
{showInviteMembers && (
<button
type="button"
title={inviteMembersLabel}
className="flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
onClick={onInviteMembers}
>

View File

@ -1,15 +1,17 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
export function WorkspaceMenuItemContent({
icon,
label,
trailing,
trailingClassName,
}: {
icon: ReactNode
label: ReactNode
trailing?: ReactNode
trailingClassName?: string
}) {
const labelTitle = typeof label === 'string' ? label : undefined
const showTrailing = trailing !== undefined && trailing !== null
return (
@ -20,14 +22,15 @@ export function WorkspaceMenuItemContent({
>
{icon}
</span>
<span
className="min-w-0 flex-1 truncate text-left system-md-regular text-text-secondary"
title={labelTitle}
>
<span className="min-w-0 flex-1 truncate text-left system-md-regular text-text-secondary">
{label}
</span>
{showTrailing && (
<span className="flex h-4 w-4 shrink-0 items-center justify-center">{trailing}</span>
<span
className={cn('flex h-4 w-4 shrink-0 items-center justify-center', trailingClassName)}
>
{trailing}
</span>
)}
</>
)

View File

@ -2,6 +2,7 @@
import type { TenantListItemResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import { cn } from '@langgenius/dify-ui/cn'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import {
DropdownMenu,
DropdownMenuContent,
@ -10,14 +11,13 @@ import {
DropdownMenuRadioItemIndicator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { useMemo, useState } from 'react'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useId, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SearchInput } from '@/app/components/base/search-input'
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
import { WorkspaceMenuItemContent } from './workspace-menu-content'
const workspaceSwitchActionButtonClassName =
'flex shrink-0 items-center justify-center rounded-md p-0.5 text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled'
const workspaceSwitchActionIconWrapClassName = 'flex size-5 shrink-0 items-center justify-center'
const workspaceSwitchActionIconClassName = 'size-3.5 shrink-0'
const workspaceSwitchListClassName = 'max-h-[240px] overflow-y-auto overscroll-contain scroll-py-1'
@ -31,19 +31,20 @@ const getWorkspaceLastOpenedAt = (workspace: TenantListItemResponse) =>
function WorkspaceSwitchControls({
disabled,
labelId,
searchText,
sort,
onSearchTextChange,
onSortChange,
}: {
disabled: boolean
labelId: string
searchText: string
sort: WorkspaceSort
onSearchTextChange: (value: string) => void
onSortChange: (value: WorkspaceSort) => void
}) {
const { t } = useTranslation()
const [searchVisible, setSearchVisible] = useState(false)
const [sortMenuOpen, setSortMenuOpen] = useState(false)
const sortMenuLabel = t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.sort.openMenu')], {
ns: 'common',
@ -64,26 +65,28 @@ function WorkspaceSwitchControls({
]
return (
<>
<Collapsible>
<div className="flex items-start gap-0.5 py-1 pr-2 pl-3">
<div className="flex min-w-0 flex-1 items-center justify-center py-1">
<span className="min-w-0 flex-1 truncate system-xs-medium-uppercase text-text-tertiary">
<span
id={labelId}
className="min-w-0 flex-1 truncate system-xs-medium-uppercase text-text-tertiary"
>
{t(($) => $['userProfile.workspace'], { ns: 'common' })}
</span>
</div>
<DropdownMenu open={sortMenuOpen} onOpenChange={setSortMenuOpen}>
<DropdownMenuTrigger
aria-label={sortMenuLabel}
disabled={disabled}
className={cn(
workspaceSwitchActionButtonClassName,
'data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary',
)}
>
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
<span className={cn('i-ri-sort-desc', workspaceSwitchActionIconClassName)} />
</span>
</DropdownMenuTrigger>
className="data-popup-open:bg-state-base-hover data-popup-open:text-text-secondary"
render={
<IconButton aria-label={sortMenuLabel}>
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
<span className={cn('i-ri-sort-desc', workspaceSwitchActionIconClassName)} />
</span>
</IconButton>
}
/>
<DropdownMenuContent
placement="bottom-end"
sideOffset={4}
@ -113,22 +116,19 @@ function WorkspaceSwitchControls({
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<button
type="button"
aria-label={t(($) => $['operation.search'], { ns: 'common' })}
<CollapsibleTrigger
disabled={disabled}
className={cn(
workspaceSwitchActionButtonClassName,
searchVisible && 'bg-state-base-hover text-text-secondary',
)}
onClick={() => setSearchVisible((visible) => !visible)}
>
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
<span className={cn('i-ri-search-line', workspaceSwitchActionIconClassName)} />
</span>
</button>
className="size-6 min-h-0 w-6 justify-center gap-0 rounded-md p-0.5 data-panel-open:bg-state-base-hover data-panel-open:text-text-secondary"
render={
<IconButton aria-label={t(($) => $['operation.search'], { ns: 'common' })}>
<span aria-hidden className={workspaceSwitchActionIconWrapClassName}>
<span className={cn('i-ri-search-line', workspaceSwitchActionIconClassName)} />
</span>
</IconButton>
}
/>
</div>
{searchVisible && (
<CollapsiblePanel>
<div className="px-2 pb-2">
<SearchInput
value={searchText}
@ -140,8 +140,8 @@ function WorkspaceSwitchControls({
autoFocus
/>
</div>
)}
</>
</CollapsiblePanel>
</Collapsible>
)
}
@ -156,6 +156,8 @@ export function WorkspaceSwitcher({
isPending,
onSwitchWorkspace,
}: WorkspaceSwitcherProps) {
const { t } = useTranslation()
const workspaceListLabelId = useId()
const [workspaceSearchText, setWorkspaceSearchText] = useState('')
const [workspaceSort, setWorkspaceSort] = useState<WorkspaceSort>('lastOpened')
const displayedWorkspaces = useMemo(() => {
@ -183,6 +185,7 @@ export function WorkspaceSwitcher({
<div className="p-1 pb-2">
<WorkspaceSwitchControls
disabled={isPending}
labelId={workspaceListLabelId}
searchText={workspaceSearchText}
sort={workspaceSort}
onSearchTextChange={setWorkspaceSearchText}
@ -190,39 +193,54 @@ export function WorkspaceSwitcher({
/>
<div aria-busy={isPending} className={workspaceSwitchListClassName}>
{isPending ? (
<div aria-hidden className="flex h-8 items-center justify-center">
<span className="i-ri-loader-2-line size-4 animate-spin text-text-tertiary motion-reduce:animate-none" />
<div
role="status"
aria-label={t(($) => $.loading, { ns: 'common' })}
className="flex h-8 items-center justify-center"
>
<span
aria-hidden
className="i-ri-loader-2-line size-4 animate-spin text-text-tertiary motion-reduce:animate-none"
/>
</div>
) : displayedWorkspaces.length === 0 ? (
<div
role="status"
className="flex h-8 items-center px-3 system-xs-regular text-text-tertiary"
>
{t(($) => $[workspaceSwitchI18nKey('mainNav.workspace.noResults')], {
ns: 'common',
})}
</div>
) : (
displayedWorkspaces.map((workspace) => {
const workspaceName = getWorkspaceName(workspace)
<ul aria-labelledby={workspaceListLabelId}>
{displayedWorkspaces.map((workspace) => {
const workspaceName = getWorkspaceName(workspace)
return (
<button
type="button"
key={workspace.id}
aria-current={workspace.current ? 'true' : undefined}
title={workspaceName}
className={cn(
'flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid',
workspace.current && 'bg-state-base-hover',
)}
onClick={() => {
onSwitchWorkspace(workspace.id)
}}
>
<WorkspaceMenuItemContent
icon={<WorkspaceAvatar name={workspaceName} size="xs" />}
label={workspaceName}
trailing={
workspace.current ? (
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
) : undefined
}
/>
</button>
)
})
return (
<li key={workspace.id}>
<button
type="button"
aria-current={workspace.current ? 'true' : undefined}
title={workspaceName}
className="group flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg px-3 py-1 text-left outline-hidden hover:bg-state-base-hover focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
onClick={() => {
onSwitchWorkspace(workspace.id)
}}
>
<WorkspaceMenuItemContent
icon={<WorkspaceAvatar name={workspaceName} size="xs" />}
label={workspaceName}
trailingClassName="hidden group-aria-current:flex"
trailing={
<span aria-hidden className="i-ri-check-line h-4 w-4 text-text-accent" />
}
/>
</button>
</li>
)
})}
</ul>
)}
</div>
</div>

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} رصيد",
"mainNav.workspace.creditsUnit": "أرصدة",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "لم يتم العثور على مساحات عمل",
"mainNav.workspace.openMenu": "فتح قائمة مساحة العمل",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} Credits",
"mainNav.workspace.creditsUnit": "Credits",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Keine Arbeitsbereiche gefunden",
"mainNav.workspace.openMenu": "Workspace-Menü öffnen",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} credits",
"mainNav.workspace.creditsUnit": "credits",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "No workspaces found",
"mainNav.workspace.openMenu": "Open workspace menu",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} créditos",
"mainNav.workspace.creditsUnit": "créditos",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "No se encontraron espacios de trabajo",
"mainNav.workspace.openMenu": "Abrir menú del espacio de trabajo",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} اعتبار",
"mainNav.workspace.creditsUnit": "اعتبار",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "هیچ فضای کاری یافت نشد",
"mainNav.workspace.openMenu": "باز کردن منوی فضای کاری",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} crédits",
"mainNav.workspace.creditsUnit": "crédits",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Aucun espace de travail trouvé",
"mainNav.workspace.openMenu": "Ouvrir le menu de lespace de travail",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} क्रेडिट",
"mainNav.workspace.creditsUnit": "क्रेडिट",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "कोई वर्कस्पेस नहीं मिला",
"mainNav.workspace.openMenu": "वर्कस्पेस मेनू खोलें",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} kredit",
"mainNav.workspace.creditsUnit": "kredit",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Tidak ada workspace yang ditemukan",
"mainNav.workspace.openMenu": "Buka menu workspace",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} crediti",
"mainNav.workspace.creditsUnit": "crediti",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Nessun workspace trovato",
"mainNav.workspace.openMenu": "Apri menu workspace",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} クレジット",
"mainNav.workspace.creditsUnit": "クレジット",
"mainNav.workspace.inviteMembers": "メンバーを招待",
"mainNav.workspace.noResults": "ワークスペースが見つかりません",
"mainNav.workspace.openMenu": "ワークスペースメニューを開く",
"mainNav.workspace.searchPlaceholder": "ワークスペースを検索",
"mainNav.workspace.settings": "ワークスペース設定",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} 크레딧",
"mainNav.workspace.creditsUnit": "크레딧",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "워크스페이스를 찾을 수 없습니다",
"mainNav.workspace.openMenu": "워크스페이스 메뉴 열기",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} ເຄຣດິດ",
"mainNav.workspace.creditsUnit": "ເຄຣດິດ",
"mainNav.workspace.inviteMembers": "ເຊີນສະມາຊິກ",
"mainNav.workspace.noResults": "ບໍ່ພົບພື້ນທີ່ເຮັດວຽກ",
"mainNav.workspace.openMenu": "ເປີດເມນູພື້ນທີ່ເຮັດວຽກ",
"mainNav.workspace.searchPlaceholder": "ຄົ້ນຫາພື້ນທີ່ເຮັດວຽກ",
"mainNav.workspace.settings": "ການຕັ້ງຄ່າພື້ນທີ່ເຮັດວຽກ",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} credits",
"mainNav.workspace.creditsUnit": "credits",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Geen werkruimtes gevonden",
"mainNav.workspace.openMenu": "Werkruimtemenu openen",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} kredytów",
"mainNav.workspace.creditsUnit": "kredyty",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Nie znaleziono obszarów roboczych",
"mainNav.workspace.openMenu": "Otwórz menu obszaru roboczego",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} créditos",
"mainNav.workspace.creditsUnit": "créditos",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Nenhum workspace encontrado",
"mainNav.workspace.openMenu": "Abrir menu do workspace",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} credite",
"mainNav.workspace.creditsUnit": "credite",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Nu s-au găsit spații de lucru",
"mainNav.workspace.openMenu": "Deschide meniul spațiului de lucru",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} кредитов",
"mainNav.workspace.creditsUnit": "кредиты",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Рабочие пространства не найдены",
"mainNav.workspace.openMenu": "Открыть меню рабочей области",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} kreditov",
"mainNav.workspace.creditsUnit": "krediti",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Ni najdenih delovnih prostorov",
"mainNav.workspace.openMenu": "Odpri meni delovnega prostora",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} เครดิต",
"mainNav.workspace.creditsUnit": "เครดิต",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "ไม่พบพื้นที่ทำงาน",
"mainNav.workspace.openMenu": "เปิดเมนูพื้นที่ทำงาน",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} kredi",
"mainNav.workspace.creditsUnit": "kredi",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Çalışma alanı bulunamadı",
"mainNav.workspace.openMenu": "Çalışma alanı menüsünü aç",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} кредитів",
"mainNav.workspace.creditsUnit": "кредити",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Робочі простори не знайдено",
"mainNav.workspace.openMenu": "Відкрити меню робочого простору",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} tín dụng",
"mainNav.workspace.creditsUnit": "tín dụng",
"mainNav.workspace.inviteMembers": "Invite members",
"mainNav.workspace.noResults": "Không tìm thấy không gian làm việc",
"mainNav.workspace.openMenu": "Mở menu workspace",
"mainNav.workspace.searchPlaceholder": "Search workspace",
"mainNav.workspace.settings": "Workspace settings",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} 消息额度",
"mainNav.workspace.creditsUnit": "消息额度",
"mainNav.workspace.inviteMembers": "邀请成员",
"mainNav.workspace.noResults": "未找到工作空间",
"mainNav.workspace.openMenu": "打开工作空间菜单",
"mainNav.workspace.searchPlaceholder": "搜索工作空间",
"mainNav.workspace.settings": "工作空间设置",

View File

@ -211,6 +211,7 @@
"mainNav.workspace.credits": "{{count}} 點額度",
"mainNav.workspace.creditsUnit": "點額度",
"mainNav.workspace.inviteMembers": "邀請成員",
"mainNav.workspace.noResults": "找不到工作區",
"mainNav.workspace.openMenu": "開啟工作區選單",
"mainNav.workspace.searchPlaceholder": "搜尋工作區",
"mainNav.workspace.settings": "工作區設定",