⚡️(a11y) improve keyboard access for language menu and action buttons
Enhances nav for language switch and makes DocsGridActions buttons accessible Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
@@ -15,6 +15,7 @@ and this project adheres to
|
|||||||
- #1235
|
- #1235
|
||||||
- #1255
|
- #1255
|
||||||
- #1262
|
- #1262
|
||||||
|
- #1244
|
||||||
- #1270
|
- #1270
|
||||||
|
|
||||||
## [3.5.0] - 2025-07-31
|
## [3.5.0] - 2025-07-31
|
||||||
@@ -35,7 +36,9 @@ and this project adheres to
|
|||||||
- 🔧(project) change env.d system by using local files #1200
|
- 🔧(project) change env.d system by using local files #1200
|
||||||
- ⚡️(frontend) improve tree stability #1207
|
- ⚡️(frontend) improve tree stability #1207
|
||||||
- ⚡️(frontend) improve accessibility #1232
|
- ⚡️(frontend) improve accessibility #1232
|
||||||
- 🛂(frontend) block drag n drop when not desktop #1239
|
- 🛂(frontend) block drag n drop when not desktop
|
||||||
|
#1239
|
||||||
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ test.describe('Home page', () => {
|
|||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
const footer = page.locator('footer').first();
|
const footer = page.locator('footer').first();
|
||||||
await expect(header).toBeVisible();
|
await expect(header).toBeVisible();
|
||||||
await expect(
|
|
||||||
header.getByRole('button', { name: /Language/ }),
|
const languageButton = page.getByRole('button', {
|
||||||
).toBeVisible();
|
name: /Language|Select language/,
|
||||||
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
|
});
|
||||||
|
await expect(languageButton).toBeVisible();
|
||||||
|
|
||||||
|
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||||
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
||||||
|
|
||||||
// Check the titles
|
// Check the titles
|
||||||
@@ -65,20 +68,31 @@ test.describe('Home page', () => {
|
|||||||
|
|
||||||
await page.goto('/docs/');
|
await page.goto('/docs/');
|
||||||
|
|
||||||
|
// Wait for the page to be fully loaded and responsive store to be initialized
|
||||||
|
await page.waitForLoadState('domcontentloaded');
|
||||||
|
|
||||||
|
// Wait a bit more for the responsive store to be initialized
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
// Check header content
|
// Check header content
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
const footer = page.locator('footer').first();
|
const footer = page.locator('footer').first();
|
||||||
await expect(header).toBeVisible();
|
await expect(header).toBeVisible();
|
||||||
await expect(
|
|
||||||
header.getByRole('button', { name: /Language/ }),
|
// Check for language picker - it should be visible on desktop
|
||||||
).toBeVisible();
|
// Use a more flexible selector that works with both Header and HomeHeader
|
||||||
|
const languageButton = page.getByRole('button', {
|
||||||
|
name: /Language|Select language/,
|
||||||
|
});
|
||||||
|
await expect(languageButton).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
header.getByRole('button', { name: 'Les services de La Suite numé' }),
|
header.getByRole('button', { name: 'Les services de La Suite numé' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
header.getByRole('img', { name: 'Gouvernement Logo' }),
|
header.getByRole('img', { name: 'Gouvernement Logo' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(header.getByRole('img', { name: 'Docs logo' })).toBeVisible();
|
await expect(header.getByTestId('header-icon-docs')).toBeVisible();
|
||||||
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
await expect(header.getByRole('heading', { name: 'Docs' })).toBeVisible();
|
||||||
|
|
||||||
// Check the titles
|
// Check the titles
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ test.describe('Language', () => {
|
|||||||
|
|
||||||
test('checks language switching', async ({ page }) => {
|
test('checks language switching', async ({ page }) => {
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
|
const languagePicker = header.locator('.--docs--language-picker-text');
|
||||||
|
|
||||||
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
|
await expect(page.locator('html')).toHaveAttribute('lang', 'en-us');
|
||||||
|
|
||||||
@@ -30,13 +31,49 @@ test.describe('Language', () => {
|
|||||||
|
|
||||||
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
|
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
|
||||||
|
|
||||||
await header.getByRole('button').getByText('Français').click();
|
// Switch to German using the utility function for consistency
|
||||||
await page.getByLabel('Deutsch').click();
|
await waitForLanguageSwitch(page, TestLanguage.German);
|
||||||
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
|
await expect(header.getByRole('button').getByText('Deutsch')).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByLabel('Abmelden')).toBeVisible();
|
await expect(page.getByLabel('Abmelden')).toBeVisible();
|
||||||
|
|
||||||
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
|
await expect(page.locator('html')).toHaveAttribute('lang', 'de');
|
||||||
|
|
||||||
|
await languagePicker.click();
|
||||||
|
|
||||||
|
await expect(page.locator('[role="menu"]')).toBeVisible();
|
||||||
|
|
||||||
|
const menuItems = page.getByRole('menuitem');
|
||||||
|
await expect(menuItems.first()).toBeVisible();
|
||||||
|
|
||||||
|
await menuItems.first().click();
|
||||||
|
|
||||||
|
await expect(page.locator('html')).toHaveAttribute('lang', 'en');
|
||||||
|
await expect(languagePicker).toContainText('English');
|
||||||
|
});
|
||||||
|
test('can switch language using only keyboard', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await waitForLanguageSwitch(page, TestLanguage.English);
|
||||||
|
|
||||||
|
const languagePicker = page.getByRole('button', {
|
||||||
|
name: /select language/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(languagePicker).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
await page.keyboard.press('Tab');
|
||||||
|
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
const menu = page.getByRole('menu');
|
||||||
|
await expect(menu).toBeVisible();
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowDown');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await expect(page.locator('html')).not.toHaveAttribute('lang', 'en-us');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('checks that backend uses the same language as the frontend', async ({
|
test('checks that backend uses the same language as the frontend', async ({
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ test.describe('Left panel mobile', () => {
|
|||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
const homeButton = page.getByTestId('home-button');
|
const homeButton = page.getByTestId('home-button');
|
||||||
const newDocButton = page.getByTestId('new-doc-button');
|
const newDocButton = page.getByTestId('new-doc-button');
|
||||||
const languageButton = page.getByRole('button', { name: /Language/ });
|
const languageButton = page.getByRole('button', {
|
||||||
|
name: 'Select language',
|
||||||
|
});
|
||||||
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
||||||
|
|
||||||
await expect(homeButton).not.toBeInViewport();
|
await expect(homeButton).not.toBeInViewport();
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
|
import { HorizontalSeparator } from '@gouvfr-lasuite/ui-kit';
|
||||||
import { Fragment, PropsWithChildren, useRef, useState } from 'react';
|
import {
|
||||||
|
Fragment,
|
||||||
|
PropsWithChildren,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
|
||||||
|
import { useDropdownKeyboardNav } from './hook/useDropdownKeyboardNav';
|
||||||
|
|
||||||
export type DropdownMenuOption = {
|
export type DropdownMenuOption = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -46,12 +55,40 @@ export const DropdownMenu = ({
|
|||||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||||
const [isOpen, setIsOpen] = useState(opened ?? false);
|
const [isOpen, setIsOpen] = useState(opened ?? false);
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||||
const blockButtonRef = useRef<HTMLDivElement>(null);
|
const blockButtonRef = useRef<HTMLDivElement>(null);
|
||||||
|
const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||||
|
|
||||||
const onOpenChange = (isOpen: boolean) => {
|
const onOpenChange = useCallback(
|
||||||
setIsOpen(isOpen);
|
(isOpen: boolean) => {
|
||||||
afterOpenChange?.(isOpen);
|
setIsOpen(isOpen);
|
||||||
};
|
setFocusedIndex(-1);
|
||||||
|
afterOpenChange?.(isOpen);
|
||||||
|
},
|
||||||
|
[afterOpenChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
useDropdownKeyboardNav({
|
||||||
|
isOpen,
|
||||||
|
focusedIndex,
|
||||||
|
options,
|
||||||
|
menuItemRefs,
|
||||||
|
setFocusedIndex,
|
||||||
|
onOpenChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus selected menu item when menu opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && menuItemRefs.current.length > 0) {
|
||||||
|
const selectedIndex = options.findIndex((option) => option.isSelected);
|
||||||
|
if (selectedIndex !== -1) {
|
||||||
|
setFocusedIndex(selectedIndex);
|
||||||
|
setTimeout(() => {
|
||||||
|
menuItemRefs.current[selectedIndex]?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, options]);
|
||||||
|
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return children;
|
return children;
|
||||||
@@ -95,6 +132,7 @@ export const DropdownMenu = ({
|
|||||||
$maxWidth="320px"
|
$maxWidth="320px"
|
||||||
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
|
$minWidth={`${blockButtonRef.current?.clientWidth}px`}
|
||||||
role="menu"
|
role="menu"
|
||||||
|
aria-label={label}
|
||||||
>
|
>
|
||||||
{topMessage && (
|
{topMessage && (
|
||||||
<Text
|
<Text
|
||||||
@@ -115,14 +153,20 @@ export const DropdownMenu = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||||
|
const isFocused = index === focusedIndex;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={option.label}>
|
<Fragment key={option.label}>
|
||||||
<BoxButton
|
<BoxButton
|
||||||
|
ref={(el) => {
|
||||||
|
menuItemRefs.current[index] = el;
|
||||||
|
}}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
aria-label={option.label}
|
aria-label={option.label}
|
||||||
data-testid={option.testId}
|
data-testid={option.testId}
|
||||||
$direction="row"
|
$direction="row"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
$hasTransition={false}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -158,6 +202,19 @@ export const DropdownMenu = ({
|
|||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--c--theme--colors--greyscale-050);
|
background-color: var(--c--theme--colors--greyscale-050);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||||
|
outline-offset: -2px;
|
||||||
|
background-color: var(--c--theme--colors--greyscale-050);
|
||||||
|
}
|
||||||
|
|
||||||
|
${isFocused &&
|
||||||
|
css`
|
||||||
|
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||||
|
outline-offset: -2px;
|
||||||
|
background-color: var(--c--theme--colors--greyscale-050);
|
||||||
|
`}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { DropdownMenuOption } from '../DropdownMenu';
|
||||||
|
|
||||||
|
type UseDropdownKeyboardNavProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
focusedIndex: number;
|
||||||
|
options: DropdownMenuOption[];
|
||||||
|
menuItemRefs: RefObject<(HTMLDivElement | null)[]>;
|
||||||
|
setFocusedIndex: (index: number) => void;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDropdownKeyboardNav = ({
|
||||||
|
isOpen,
|
||||||
|
focusedIndex,
|
||||||
|
options,
|
||||||
|
menuItemRefs,
|
||||||
|
setFocusedIndex,
|
||||||
|
onOpenChange,
|
||||||
|
}: UseDropdownKeyboardNavProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabledIndices = options
|
||||||
|
.map((option, index) =>
|
||||||
|
option.show !== false && !option.disabled ? index : -1,
|
||||||
|
)
|
||||||
|
.filter((index) => index !== -1);
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault();
|
||||||
|
const nextIndex =
|
||||||
|
focusedIndex < enabledIndices.length - 1 ? focusedIndex + 1 : 0;
|
||||||
|
const nextEnabledIndex = enabledIndices[nextIndex];
|
||||||
|
setFocusedIndex(nextIndex);
|
||||||
|
menuItemRefs.current[nextEnabledIndex]?.focus();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault();
|
||||||
|
const prevIndex =
|
||||||
|
focusedIndex > 0 ? focusedIndex - 1 : enabledIndices.length - 1;
|
||||||
|
const prevEnabledIndex = enabledIndices[prevIndex];
|
||||||
|
setFocusedIndex(prevIndex);
|
||||||
|
menuItemRefs.current[prevEnabledIndex]?.focus();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
event.preventDefault();
|
||||||
|
if (focusedIndex >= 0 && focusedIndex < enabledIndices.length) {
|
||||||
|
const selectedOptionIndex = enabledIndices[focusedIndex];
|
||||||
|
const selectedOption = options[selectedOptionIndex];
|
||||||
|
if (selectedOption && selectedOption.callback) {
|
||||||
|
onOpenChange(false);
|
||||||
|
void selectedOption.callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault();
|
||||||
|
onOpenChange(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
isOpen,
|
||||||
|
focusedIndex,
|
||||||
|
options,
|
||||||
|
menuItemRefs,
|
||||||
|
setFocusedIndex,
|
||||||
|
onOpenChange,
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box } from '../Box';
|
import { Box } from '../Box';
|
||||||
import { DropdownMenu, DropdownMenuOption } from '../DropdownMenu';
|
|
||||||
import { Icon } from '../Icon';
|
import { Icon } from '../Icon';
|
||||||
import { Text } from '../Text';
|
import { Text } from '../Text';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuOption,
|
||||||
|
} from '../dropdown-menu/DropdownMenu';
|
||||||
|
|
||||||
export type FilterDropdownProps = {
|
export type FilterDropdownProps = {
|
||||||
options: DropdownMenuOption[];
|
options: DropdownMenuOption[];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ export * from './Box';
|
|||||||
export * from './BoxButton';
|
export * from './BoxButton';
|
||||||
export * from './Card';
|
export * from './Card';
|
||||||
export * from './DropButton';
|
export * from './DropButton';
|
||||||
export * from './DropdownMenu';
|
export * from './dropdown-menu/DropdownMenu';
|
||||||
export * from './quick-search';
|
export * from './quick-search';
|
||||||
export * from './Icon';
|
export * from './Icon';
|
||||||
export * from './InfiniteScroll';
|
export * from './InfiniteScroll';
|
||||||
|
|||||||
@@ -76,9 +76,14 @@ export const DocsGridActions = ({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const documentTitle = doc.title || t('Untitled document');
|
||||||
|
const menuLabel = t('Open the menu of actions for the document: {{title}}', {
|
||||||
|
title: documentTitle,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DropdownMenu options={options}>
|
<DropdownMenu options={options} label={menuLabel}>
|
||||||
<Icon
|
<Icon
|
||||||
data-testid={`docs-grid-actions-button-${doc.id}`}
|
data-testid={`docs-grid-actions-button-${doc.id}`}
|
||||||
iconName="more_horiz"
|
iconName="more_horiz"
|
||||||
|
|||||||
@@ -39,12 +39,17 @@ export const LanguagePicker = () => {
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
options={optionsPicker}
|
options={optionsPicker}
|
||||||
showArrow
|
showArrow
|
||||||
|
label={t('Select language')}
|
||||||
buttonCss={css`
|
buttonCss={css`
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--c--components--button--primary-text--background--color-hover
|
--c--components--button--primary-text--background--color-hover
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--c--theme--colors--primary-500);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.5rem 0.6rem;
|
padding: 0.5rem 0.6rem;
|
||||||
& > div {
|
& > div {
|
||||||
|
|||||||
@@ -363,6 +363,7 @@
|
|||||||
"Your access request for this document is pending.": "Ihre Zugriffsanfrage für dieses Dokument steht noch aus.",
|
"Your access request for this document is pending.": "Ihre Zugriffsanfrage für dieses Dokument steht noch aus.",
|
||||||
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
|
"Your current document will revert to this version.": "Ihr aktuelles Dokument wird auf diese Version zurückgesetzt.",
|
||||||
"Your {{format}} was downloaded succesfully": "Ihr {{format}} wurde erfolgreich heruntergeladen",
|
"Your {{format}} was downloaded succesfully": "Ihr {{format}} wurde erfolgreich heruntergeladen",
|
||||||
|
"Open the menu of actions for the document: {{title}}": "Öffnen Sie das Aktionsmenü für das Dokument: {{title}}",
|
||||||
"home-content-open-source-part1": "Doms ist auf <2>Django Rest Framework</2> und <6>Next.js</6> aufgebaut. Wir verwenden auch <9>Yjs</9> und <13>BlockNote.js</13>, zwei Projekte, die wir mit Stolz sponsern.",
|
"home-content-open-source-part1": "Doms ist auf <2>Django Rest Framework</2> und <6>Next.js</6> aufgebaut. Wir verwenden auch <9>Yjs</9> und <13>BlockNote.js</13>, zwei Projekte, die wir mit Stolz sponsern.",
|
||||||
"home-content-open-source-part2": "Sie können Docs ganz einfach selbst hosten (lesen Sie unsere <2>Installationsdokumentation</2>).<br/>Docs verwendet eine <7>Lizenz</7> (MIT), die auf Innovation und Unternehmen zugeschnitten ist.<br/>Beiträge sind willkommen (lesen Sie unsere Roadmap <13>hier</13>).\n",
|
"home-content-open-source-part2": "Sie können Docs ganz einfach selbst hosten (lesen Sie unsere <2>Installationsdokumentation</2>).<br/>Docs verwendet eine <7>Lizenz</7> (MIT), die auf Innovation und Unternehmen zugeschnitten ist.<br/>Beiträge sind willkommen (lesen Sie unsere Roadmap <13>hier</13>).\n",
|
||||||
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird."
|
"home-content-open-source-part3": "Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wurde. “, „home-content-open-source-part3“: „Docs ist das Ergebnis einer gemeinsamen Anstrengung, die von der französischen Regierung 🇫🇷🥖 <1>(DINUM)</1> und der deutschen Regierung 🇩🇪🥨 <5>(ZenDiS)</5> geleitet wird."
|
||||||
@@ -374,6 +375,8 @@
|
|||||||
"Search docs": "Search docs",
|
"Search docs": "Search docs",
|
||||||
"More options": "More options",
|
"More options": "More options",
|
||||||
"Pinned documents": "Pinned documents",
|
"Pinned documents": "Pinned documents",
|
||||||
|
"Open actions menu for document: {{title}}": "Open actions menu for document: {{title}}",
|
||||||
|
"Open the menu of actions for the document: {{title}}": "Open the menu of actions for the document: {{title}}",
|
||||||
"Share with {{count}} users_one": "Share with {{count}} user",
|
"Share with {{count}} users_one": "Share with {{count}} user",
|
||||||
"Shared with {{count}} users_many": "Shared with {{count}} users",
|
"Shared with {{count}} users_many": "Shared with {{count}} users",
|
||||||
"Shared with {{count}} users_one": "Shared with {{count}} user",
|
"Shared with {{count}} users_one": "Shared with {{count}} user",
|
||||||
@@ -492,6 +495,8 @@
|
|||||||
"Offline ?!": "¿¡Sin conexión!?",
|
"Offline ?!": "¿¡Sin conexión!?",
|
||||||
"Only invited people can access": "Solo las personas invitadas pueden acceder",
|
"Only invited people can access": "Solo las personas invitadas pueden acceder",
|
||||||
"Open Source": "Código abierto",
|
"Open Source": "Código abierto",
|
||||||
|
"Open actions menu for document: {{title}}": "Abrir menú de acciones para el documento: {{title}}",
|
||||||
|
"Open the menu of actions for the document: {{title}}": "Abrir el menú de acciones para el documento: {{title}}",
|
||||||
"Open the document options": "Abrir las opciones del documento",
|
"Open the document options": "Abrir las opciones del documento",
|
||||||
"Open the header menu": "Abrir el menú de encabezado",
|
"Open the header menu": "Abrir el menú de encabezado",
|
||||||
"Organize": "Organiza",
|
"Organize": "Organiza",
|
||||||
@@ -668,6 +673,8 @@
|
|||||||
"Image 403": "Image 403",
|
"Image 403": "Image 403",
|
||||||
"Insufficient access rights to view the document.": "Droits d'accès insuffisants pour voir le document.",
|
"Insufficient access rights to view the document.": "Droits d'accès insuffisants pour voir le document.",
|
||||||
"Invite": "Inviter",
|
"Invite": "Inviter",
|
||||||
|
"Open actions menu for document: {{title}}": "Ouvrir le menu d'actions pour le document : {{title}}",
|
||||||
|
"Open the menu of actions for the document: {{title}}": "Ouvrir le menu d'actions du document",
|
||||||
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
|
"It is the card information about the document.": "Il s'agit de la carte d'information du document.",
|
||||||
"It is the document title": "Il s'agit du titre du document",
|
"It is the document title": "Il s'agit du titre du document",
|
||||||
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
|
||||||
@@ -1057,6 +1064,8 @@
|
|||||||
"Offline ?!": "Offline ?!",
|
"Offline ?!": "Offline ?!",
|
||||||
"Only invited people can access": "Alleen uitgenodigde gebruikers hebben toegang",
|
"Only invited people can access": "Alleen uitgenodigde gebruikers hebben toegang",
|
||||||
"Open Source": "Open Source",
|
"Open Source": "Open Source",
|
||||||
|
"Open actions menu for document: {{title}}": "Open actiemenu voor document: {{title}}",
|
||||||
|
"Open the menu of actions for the document: {{title}}": "Open het menu van acties voor het document: {{title}}",
|
||||||
"Open the document options": "Open document opties",
|
"Open the document options": "Open document opties",
|
||||||
"Open the header menu": "Open het hoofdmenu",
|
"Open the header menu": "Open het hoofdmenu",
|
||||||
"Organize": "Organiseer",
|
"Organize": "Organiseer",
|
||||||
|
|||||||
Reference in New Issue
Block a user