mirror of
https://github.com/suitenumerique/docs.git
synced 2026-10-15 20:55:25 +02:00
🌐(frontend) add Crowdin translator context
We add context for the translator Crowdin, to do so, we added a context description on the t function. Closes #2438
This commit is contained in:
@@ -47,18 +47,23 @@ export const ErrorPage = ({
|
||||
}: ErrorPageProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const errorTitle = t('An unexpected error occurred.');
|
||||
const errorTitle = t('An unexpected error occurred.', {
|
||||
description: 'Generic error page title and message',
|
||||
});
|
||||
const safeTarget = getSafeRefreshUrl(refreshTarget);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{errorTitle} - {t('Docs')}
|
||||
{errorTitle} -{' '}
|
||||
{t('Docs', {
|
||||
description: 'Product name, shown in page titles and the logo',
|
||||
})}
|
||||
</title>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${errorTitle} - ${t('Docs')}`}
|
||||
content={`${errorTitle} - ${t('Docs', { description: 'Product name, shown in page titles and the logo' })}`}
|
||||
key="title"
|
||||
/>
|
||||
</Head>
|
||||
@@ -69,7 +74,10 @@ export const ErrorPage = ({
|
||||
$padding={{ bottom: '2rem' }}
|
||||
>
|
||||
<Text as="h1" $textAlign="center" className="sr-only">
|
||||
{errorTitle} - {t('Docs')}
|
||||
{errorTitle} -{' '}
|
||||
{t('Docs', {
|
||||
description: 'Product name, shown in page titles and the logo',
|
||||
})}
|
||||
</Text>
|
||||
<Image
|
||||
src={image}
|
||||
@@ -104,7 +112,9 @@ export const ErrorPage = ({
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('Home')}
|
||||
{t('Home', {
|
||||
description: 'Button/link label to go back to the home page',
|
||||
})}
|
||||
</StyledButton>
|
||||
</StyledLink>
|
||||
|
||||
@@ -125,7 +135,9 @@ export const ErrorPage = ({
|
||||
: window.location.reload()
|
||||
}
|
||||
>
|
||||
{t('Refresh page')}
|
||||
{t('Refresh page', {
|
||||
description: 'Button on the error page to reload the page',
|
||||
})}
|
||||
</StyledButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -43,7 +43,10 @@ export const InfiniteScroll = ({
|
||||
variant="bordered"
|
||||
icon={<Icon iconName="arrow_downward" />}
|
||||
>
|
||||
{buttonLabel ?? t('Load more')}
|
||||
{buttonLabel ??
|
||||
t('Load more', {
|
||||
description: 'Button to load the next items of a list',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</InView>
|
||||
|
||||
@@ -30,7 +30,9 @@ export const LoadMoreText = ({
|
||||
$size="md"
|
||||
/>
|
||||
<Text $theme="brand" $variation="secondary">
|
||||
{t('Load more')}
|
||||
{t('Load more', {
|
||||
description: 'Button to load the next items of a list',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -76,7 +76,10 @@ export const SkipToContent = () => {
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{t('Go to content')}
|
||||
{t('Go to content', {
|
||||
description:
|
||||
'Skip link for keyboard users to jump to the main content',
|
||||
})}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -64,7 +64,10 @@ export const TextOnlyErrors = ({
|
||||
|
||||
{!causes && (
|
||||
<Text $theme="error" $textAlign="center" {...textProps}>
|
||||
{defaultMessage || t('Something bad happens, please retry.')}
|
||||
{defaultMessage ||
|
||||
t('Something bad happens, please retry.', {
|
||||
description: 'Generic error message shown when a request fails',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -25,7 +25,9 @@ export const Title = ({
|
||||
$color="var(--c--contextuals--content--logo1)"
|
||||
{...props}
|
||||
>
|
||||
{t('Docs')}
|
||||
{t('Docs', {
|
||||
description: 'Product name, shown in page titles and the logo',
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -131,8 +131,14 @@ export const Waffle = () => {
|
||||
>
|
||||
<LaGaufreV2
|
||||
{...waffleConfig}
|
||||
label={waffleConfig.label ?? t('Digital LaSuite services')}
|
||||
newWindowLabelSuffix={` (${t('new window')})`}
|
||||
label={
|
||||
waffleConfig.label ??
|
||||
t('Digital LaSuite services', {
|
||||
description:
|
||||
'Accessible label of the apps menu (waffle) listing La Suite services',
|
||||
})
|
||||
}
|
||||
newWindowLabelSuffix={` (${t('new window', { description: 'Screen reader suffix added to links opening in a new window' })})`}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -56,20 +56,27 @@ export const AlertModal = ({
|
||||
rightActions={
|
||||
<Box $direction="row" $gap="small">
|
||||
<Button
|
||||
aria-label={`${t('Cancel')} - ${title}`}
|
||||
aria-label={`${t('Cancel', { description: 'Button to dismiss a dialog without confirming' })} - ${title}`}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onClick={onClose}
|
||||
>
|
||||
{cancelLabel ?? t('Cancel')}
|
||||
{cancelLabel ??
|
||||
t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={confirmLabel ?? t('Confirm')}
|
||||
aria-label={
|
||||
confirmLabel ??
|
||||
t('Confirm', { description: 'Button to validate a dialog' })
|
||||
}
|
||||
color={themeCTA ?? 'error'}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmLabel ?? t('Confirm')}
|
||||
{confirmLabel ??
|
||||
t('Confirm', { description: 'Button to validate a dialog' })}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
|
||||
@@ -56,7 +56,10 @@ export const QuickSearchInput = ({
|
||||
autoFocus={true}
|
||||
aria-controls={listId}
|
||||
value={inputValue}
|
||||
placeholder={placeholder ?? t('Search')}
|
||||
placeholder={
|
||||
placeholder ??
|
||||
t('Search', { description: 'Placeholder of a search input' })
|
||||
}
|
||||
onValueChange={onFilter}
|
||||
maxLength={254}
|
||||
data-testid="quick-search-input"
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ButtonLogin = ({
|
||||
{...props}
|
||||
href={href}
|
||||
>
|
||||
{children ?? t('Sign in')}
|
||||
{children ?? t('Sign in', { description: 'Button to log in' })}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -44,7 +44,10 @@ export const ProConnectButton = () => {
|
||||
return (
|
||||
<BoxButton
|
||||
onClick={() => gotoLogin()}
|
||||
aria-label={t('Proconnect Login')}
|
||||
aria-label={t('Proconnect Login', {
|
||||
description:
|
||||
'Accessible name of the ProConnect (French government SSO) login button',
|
||||
})}
|
||||
$css={css`
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--brand--primary
|
||||
|
||||
@@ -55,8 +55,14 @@ export const UserReconciliation = ({
|
||||
$variation="primary"
|
||||
>
|
||||
{isError
|
||||
? t('An error occurred during email validation.')
|
||||
: t('Email Address Confirmed')}
|
||||
? t('An error occurred during email validation.', {
|
||||
description:
|
||||
'Title shown when the email confirmation of an account merge fails',
|
||||
})
|
||||
: t('Email Address Confirmed', {
|
||||
description:
|
||||
'Title shown after an email address is confirmed during an account merge',
|
||||
})}
|
||||
</Text>
|
||||
{!isError && (
|
||||
<Text
|
||||
@@ -70,6 +76,7 @@ export const UserReconciliation = ({
|
||||
>
|
||||
{t(
|
||||
'To complete the unification of your user accounts, please click the confirmation links sent to all the email addresses you provided.',
|
||||
{ description: 'Instruction of the account merge page' },
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
+32
-8
@@ -56,18 +56,26 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
|
||||
<Box $direction="row" $align="center" $justify="space-between">
|
||||
<Box $direction="row" $align="center" $gap="2xs">
|
||||
<Text as="h2" $weight="bold" $size="16px" $margin="0">
|
||||
{t('Comments')}
|
||||
{t('Comments', {
|
||||
description: 'Title of the comments side panel',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<DropdownMenu
|
||||
options={[
|
||||
{
|
||||
label: t('Open'),
|
||||
label: t('Open', {
|
||||
description:
|
||||
'Dropdown menu item to filter the comments sidebar to only show open (unresolved) comment threads. This is a state/adjective ("open comments"), not the verb "to open".',
|
||||
}),
|
||||
callback: () => setFilter('open'),
|
||||
isChecked: filter === 'open',
|
||||
},
|
||||
{
|
||||
label: t('Resolved'),
|
||||
label: t('Resolved', {
|
||||
description:
|
||||
'Dropdown menu item to filter the comments sidebar to only show resolved comment threads.',
|
||||
}),
|
||||
callback: () => setFilter('resolved'),
|
||||
isChecked: filter === 'resolved',
|
||||
},
|
||||
@@ -76,9 +84,18 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
|
||||
shouldCloseOnInteractOutside={() => true}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Tooltip content={t('Filter comments')} placement="bottom">
|
||||
<Tooltip
|
||||
content={t('Filter comments', {
|
||||
description:
|
||||
'Tooltip and accessible name of the button filtering comments by status',
|
||||
})}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Filter comments')}
|
||||
aria-label={t('Filter comments', {
|
||||
description:
|
||||
'Tooltip and accessible name of the button filtering comments by status',
|
||||
})}
|
||||
size="nano"
|
||||
icon={
|
||||
filter === 'open' ? (
|
||||
@@ -107,7 +124,10 @@ export const CommentSideBar = ({ onClose }: CommentSideBarProps) => {
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the comments sidebar')}
|
||||
aria-label={t('Close the comments sidebar', {
|
||||
description:
|
||||
'Accessible name of the button closing the comments panel',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -130,8 +150,12 @@ export const CommentSideBarButton = () => {
|
||||
|
||||
const isActive = isPanelOpen && activePanel === 'comments';
|
||||
const ariaLabel = isActive
|
||||
? t('Hide the comments sidebar')
|
||||
: t('Show the comments sidebar');
|
||||
? t('Hide the comments sidebar', {
|
||||
description: 'Accessible name of the toggle hiding the comments panel',
|
||||
})
|
||||
: t('Show the comments sidebar', {
|
||||
description: 'Accessible name of the toggle showing the comments panel',
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
+6
-2
@@ -56,8 +56,12 @@ export const CommentToolbarButton = () => {
|
||||
}}
|
||||
aria-haspopup="dialog"
|
||||
data-test="comment-toolbar-button"
|
||||
aria-label={t('Add comment')}
|
||||
mainTooltip={t('Add comment')}
|
||||
aria-label={t('Add comment', {
|
||||
description: 'Editor toolbar button to comment the selected text',
|
||||
})}
|
||||
mainTooltip={t('Add comment', {
|
||||
description: 'Editor toolbar button to comment the selected text',
|
||||
})}
|
||||
>
|
||||
<Icon
|
||||
iconName="comment"
|
||||
|
||||
@@ -67,7 +67,11 @@ export function useComments(
|
||||
|
||||
return {
|
||||
id: encodedURIUserId,
|
||||
username: fullName || t('Anonymous'),
|
||||
username:
|
||||
fullName ||
|
||||
t('Anonymous', {
|
||||
description: 'Display name of a user without a known name',
|
||||
}),
|
||||
avatarUrl: avatarUrlFromName(
|
||||
fullName,
|
||||
themeTokens?.font?.families?.base,
|
||||
|
||||
@@ -26,7 +26,12 @@ export const createDocAttachment = async ({
|
||||
// A proxy sitting in front of the API can enforce a lower limit than the application
|
||||
// does, and answers a 413 with an HTML body carrying no usable cause.
|
||||
if (response.status === 413 && !causes.cause?.length) {
|
||||
causes.cause = [t('This file is too large to be uploaded.')];
|
||||
causes.cause = [
|
||||
t('This file is too large to be uploaded.', {
|
||||
description:
|
||||
'Error shown when an uploaded file exceeds the size limit',
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
throw new APIError('Failed to upload on the doc', causes);
|
||||
|
||||
+50
-11
@@ -104,7 +104,10 @@ export function AIGroupButton() {
|
||||
className="bn-button bn-menu-item --docs--ai-actions-menu-trigger"
|
||||
data-test="ai-actions"
|
||||
label="AI"
|
||||
mainTooltip={t('AI Actions')}
|
||||
mainTooltip={t('AI Actions', {
|
||||
description:
|
||||
'Tooltip of the editor toolbar button opening AI actions',
|
||||
})}
|
||||
icon={<Icon iconName="auto_awesome" $size="md" />}
|
||||
/>
|
||||
</Components.Generic.Menu.Trigger>
|
||||
@@ -119,42 +122,59 @@ export function AIGroupButton() {
|
||||
docId={currentDoc.id}
|
||||
icon={<Icon iconName="text_fields" $size="s" />}
|
||||
>
|
||||
{t('Use as prompt')}
|
||||
{t('Use as prompt', {
|
||||
description:
|
||||
'AI menu action: use the selected text as the instruction for the AI',
|
||||
})}
|
||||
</AIMenuItemTransform>
|
||||
<AIMenuItemTransform
|
||||
action="rephrase"
|
||||
docId={currentDoc.id}
|
||||
icon={<Icon iconName="refresh" $size="s" />}
|
||||
>
|
||||
{t('Rephrase')}
|
||||
{t('Rephrase', {
|
||||
description:
|
||||
'AI menu action: rewrite the selected text differently',
|
||||
})}
|
||||
</AIMenuItemTransform>
|
||||
<AIMenuItemTransform
|
||||
action="summarize"
|
||||
docId={currentDoc.id}
|
||||
icon={<Icon iconName="summarize" $size="s" />}
|
||||
>
|
||||
{t('Summarize')}
|
||||
{t('Summarize', {
|
||||
description:
|
||||
'AI menu action: shorten the selected text into a summary',
|
||||
})}
|
||||
</AIMenuItemTransform>
|
||||
<AIMenuItemTransform
|
||||
action="correct"
|
||||
docId={currentDoc.id}
|
||||
icon={<Icon iconName="check" $size="s" />}
|
||||
>
|
||||
{t('Correct')}
|
||||
{t('Correct', {
|
||||
description:
|
||||
'AI menu action: fix spelling and grammar of the selected text',
|
||||
})}
|
||||
</AIMenuItemTransform>
|
||||
<AIMenuItemTransform
|
||||
action="beautify"
|
||||
docId={currentDoc.id}
|
||||
icon={<Icon iconName="draw" $size="s" />}
|
||||
>
|
||||
{t('Beautify')}
|
||||
{t('Beautify', {
|
||||
description:
|
||||
'AI menu action: improve the style and formatting of the selected text',
|
||||
})}
|
||||
</AIMenuItemTransform>
|
||||
<AIMenuItemTransform
|
||||
action="emojify"
|
||||
docId={currentDoc.id}
|
||||
icon={<Icon iconName="emoji_emotions" $size="s" />}
|
||||
>
|
||||
{t('Emojify')}
|
||||
{t('Emojify', {
|
||||
description: 'AI menu action: add emojis to the selected text',
|
||||
})}
|
||||
</AIMenuItemTransform>
|
||||
</>
|
||||
)}
|
||||
@@ -167,7 +187,10 @@ export function AIGroupButton() {
|
||||
>
|
||||
<Box $direction="row" $gap="0.6rem">
|
||||
<Icon iconName="translate" $size="s" />
|
||||
{t('Language')}
|
||||
{t('Language', {
|
||||
description:
|
||||
'AI menu entry to translate the selected text into another language',
|
||||
})}
|
||||
</Box>
|
||||
</Components.Generic.Menu.Item>
|
||||
</Components.Generic.Menu.Trigger>
|
||||
@@ -321,7 +344,13 @@ const AIMenuItem = ({
|
||||
];
|
||||
|
||||
if (!selectedBlocks?.length) {
|
||||
toast(t('No text selected'), VariantType.WARNING);
|
||||
toast(
|
||||
t('No text selected', {
|
||||
description:
|
||||
'Warning shown when an AI action is used with no text selected',
|
||||
}),
|
||||
VariantType.WARNING,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -359,10 +388,20 @@ const useHandleAIError = () => {
|
||||
|
||||
return (error: unknown) => {
|
||||
if (isAPIError(error) && error.status === 429) {
|
||||
toast(t('Too many requests. Please wait 60 seconds.'), VariantType.ERROR);
|
||||
toast(
|
||||
t('Too many requests. Please wait 60 seconds.', {
|
||||
description: 'Error shown when the AI rate limit is reached',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
toast(t('AI seems busy! Please try again.'), VariantType.ERROR);
|
||||
toast(
|
||||
t('AI seems busy! Please try again.', {
|
||||
description: 'Error shown when the AI service fails',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -148,7 +148,9 @@ export const AIMenu = (props: AIMenuProps) => {
|
||||
items.unshift({
|
||||
key: 'accept',
|
||||
icon: <Icon iconName="check_circle" $color="inherit" $size="18px" />,
|
||||
title: t('Accept anyway'),
|
||||
title: t('Accept anyway', {
|
||||
description: 'Button to accept the AI suggestion despite a warning',
|
||||
}),
|
||||
onItemClick: () => {
|
||||
ai.acceptChanges();
|
||||
ai.closeAIMenu();
|
||||
@@ -192,28 +194,47 @@ export const AIMenu = (props: AIMenuProps) => {
|
||||
|
||||
const placeholder = useMemo(() => {
|
||||
if (aiResponseStatus === 'thinking') {
|
||||
return t('Thinking...');
|
||||
return t('Thinking...', {
|
||||
description: 'Placeholder shown while the AI prepares an answer',
|
||||
});
|
||||
} else if (aiResponseStatus === 'ai-writing') {
|
||||
return t('Writing...');
|
||||
return t('Writing...', {
|
||||
description: 'Placeholder shown while the AI is writing its answer',
|
||||
});
|
||||
} else if (aiResponseStatus === 'error') {
|
||||
return t('An error occurred...');
|
||||
return t('An error occurred...', {
|
||||
description: 'Placeholder shown when the AI request failed',
|
||||
});
|
||||
}
|
||||
|
||||
return t('Ask anything...');
|
||||
return t('Ask anything...', {
|
||||
description: 'Placeholder of the AI prompt input',
|
||||
});
|
||||
}, [aiResponseStatus, t]);
|
||||
|
||||
const ariaLiveMessage = useMemo(() => {
|
||||
if (aiResponseStatus === 'thinking') {
|
||||
return t('AI is thinking');
|
||||
return t('AI is thinking', {
|
||||
description:
|
||||
'Screen reader announcement while the AI prepares an answer',
|
||||
});
|
||||
}
|
||||
if (aiResponseStatus === 'ai-writing') {
|
||||
return t('AI is writing');
|
||||
return t('AI is writing', {
|
||||
description:
|
||||
'Screen reader announcement while the AI writes its answer',
|
||||
});
|
||||
}
|
||||
if (aiResponseStatus === 'user-reviewing') {
|
||||
return t('AI response ready for review');
|
||||
return t('AI response ready for review', {
|
||||
description:
|
||||
'Screen reader announcement when the AI answer can be reviewed',
|
||||
});
|
||||
}
|
||||
if (aiResponseStatus === 'error') {
|
||||
return t('AI request failed');
|
||||
return t('AI request failed', {
|
||||
description: 'Screen reader announcement when the AI request failed',
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
@@ -257,7 +278,7 @@ export const AIMenu = (props: AIMenuProps) => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('Stop')}
|
||||
{t('Stop', { description: 'Button to stop the AI generation' })}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -92,7 +92,9 @@ export const AIToolbarButton = () => {
|
||||
$padding={{ right: '2xs' }}
|
||||
>
|
||||
<IconAI isHighlighted={isHighlighted} width="18px" />
|
||||
{t('Ask AI')}
|
||||
{t('Ask AI', {
|
||||
description: 'Editor toolbar button opening the AI prompt',
|
||||
})}
|
||||
</Box>
|
||||
</Components.Generic.Toolbar.Button>
|
||||
<Box
|
||||
|
||||
+8
-2
@@ -145,7 +145,11 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
const aiExtension = useAI?.(doc.id, aiBlockNoteAllowed);
|
||||
|
||||
const collabName = user?.full_name || user?.email;
|
||||
const cursorName = collabName || t('Anonymous');
|
||||
const cursorName =
|
||||
collabName ||
|
||||
t('Anonymous', {
|
||||
description: 'Display name of a user without a known name',
|
||||
});
|
||||
const showCursorLabels: 'always' | 'activity' | (string & {}) = 'activity';
|
||||
|
||||
// Comments
|
||||
@@ -363,7 +367,9 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
||||
sideMenu={false}
|
||||
theme="light"
|
||||
comments={false}
|
||||
aria-label={t('Document editor')}
|
||||
aria-label={t('Document editor', {
|
||||
description: 'Accessible name of the editor area',
|
||||
})}
|
||||
// To not clipped the floating part in the editor area
|
||||
portalElements={{ default: null }}
|
||||
>
|
||||
|
||||
+4
-1
@@ -82,7 +82,10 @@ export function MarkdownButton() {
|
||||
|
||||
return (
|
||||
<Components.FormattingToolbar.Button
|
||||
mainTooltip={t('Convert Markdown')}
|
||||
mainTooltip={t('Convert Markdown', {
|
||||
description:
|
||||
'Tooltip of the editor toolbar button converting selected Markdown text into formatted content',
|
||||
})}
|
||||
onClick={handleConvertMarkdown}
|
||||
className="--docs--editor-markdown-button"
|
||||
label="M"
|
||||
|
||||
+23
-8
@@ -19,19 +19,26 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
onClose={() => onClose()}
|
||||
aria-label={t('Warning')}
|
||||
aria-label={t('Warning', { description: 'Title of a warning dialog' })}
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Cancel the download')}
|
||||
aria-label={t('Cancel the download', {
|
||||
description:
|
||||
'Accessible name of the button cancelling a download',
|
||||
})}
|
||||
autoFocus
|
||||
variant="secondary"
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Download')}
|
||||
aria-label={t('Download', {
|
||||
description: 'Dropdown menu item to download the document',
|
||||
})}
|
||||
color="error"
|
||||
data-testid="modal-download-unsafe-button"
|
||||
onClick={() => {
|
||||
@@ -41,7 +48,9 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t('Download anyway')}
|
||||
{t('Download anyway', {
|
||||
description: 'Button to download a file flagged as unsafe',
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -57,7 +66,7 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
$margin="0"
|
||||
>
|
||||
<Icon iconName="warning" $theme="warning" />
|
||||
{t('Warning')}
|
||||
{t('Warning', { description: 'Title of a warning dialog' })}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
@@ -65,10 +74,16 @@ export const ModalConfirmDownloadUnsafe = ({
|
||||
<Box>
|
||||
<Box $direction="column" $gap="0.35rem" $margin={{ top: 'sm' }}>
|
||||
<Text $variation="secondary">
|
||||
{t('This file is flagged as unsafe.')}
|
||||
{t('This file is flagged as unsafe.', {
|
||||
description:
|
||||
'Warning shown before downloading a file detected as malicious',
|
||||
})}
|
||||
</Text>
|
||||
<Text $variation="secondary">
|
||||
{t('Please download it only if it comes from a trusted source.')}
|
||||
{t('Please download it only if it comes from a trusted source.', {
|
||||
description:
|
||||
'Advice shown before downloading a file flagged as unsafe',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
+10
-3
@@ -32,8 +32,12 @@ export const LinkToBlockItem = () => {
|
||||
|
||||
copyToClipboard(
|
||||
`${window.location.origin}${window.location.pathname}#${block.id}`,
|
||||
t('Link Copied !'),
|
||||
t('Failed to copy link'),
|
||||
t('Link Copied !', {
|
||||
description: 'Toast confirming a link was copied to the clipboard',
|
||||
}),
|
||||
t('Failed to copy link', {
|
||||
description: 'Toast shown when copying a link failed',
|
||||
}),
|
||||
);
|
||||
|
||||
if (!isMobile) {
|
||||
@@ -54,7 +58,10 @@ export const LinkToBlockItem = () => {
|
||||
>
|
||||
<Box $align="center" $gap="xxs" $direction="row">
|
||||
<LinkIcon width="16" height="16" aria-hidden="true" />
|
||||
{t('Copy link to block')}
|
||||
{t('Copy link to block', {
|
||||
description:
|
||||
'Editor side menu action copying a link to the current block',
|
||||
})}
|
||||
</Box>
|
||||
</Components.Generic.Menu.Item>
|
||||
);
|
||||
|
||||
+11
-3
@@ -207,7 +207,10 @@ export const getCalloutReactSlashMenuItems = (
|
||||
) => [
|
||||
{
|
||||
key: 'callout',
|
||||
title: t('Callout'),
|
||||
title: t('Callout', {
|
||||
description:
|
||||
'Name of the editor block type: a highlighted box with an emoji',
|
||||
}),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlockForSlashMenu(editor, {
|
||||
type: 'callout',
|
||||
@@ -216,14 +219,19 @@ export const getCalloutReactSlashMenuItems = (
|
||||
aliases: ['callout', 'encadré', 'hervorhebung', 'benadrukken'],
|
||||
group,
|
||||
icon: <Icon iconName="lightbulb" $size="18px" />,
|
||||
subtext: t('Add a callout block'),
|
||||
subtext: t('Add a callout block', {
|
||||
description: 'Description of the callout block in the editor slash menu',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const getCalloutFormattingToolbarItems = (
|
||||
t: TFunction<'translation', undefined>,
|
||||
): BlockTypeSelectItem => ({
|
||||
name: t('Callout'),
|
||||
name: t('Callout', {
|
||||
description:
|
||||
'Name of the editor block type: a highlighted box with an emoji',
|
||||
}),
|
||||
type: 'callout',
|
||||
icon: () => <Icon iconName="lightbulb" $size="16px" />,
|
||||
});
|
||||
|
||||
+43
-8
@@ -74,15 +74,21 @@ const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
|
||||
useEffect(() => {
|
||||
if (lang && locales[lang as keyof typeof locales]) {
|
||||
locales[lang as keyof typeof locales].file_blocks.add_button_text['pdf'] =
|
||||
t('Add PDF');
|
||||
t('Add PDF', {
|
||||
description: 'Button label to add a PDF file in the editor',
|
||||
});
|
||||
(
|
||||
locales[lang as keyof typeof locales].file_panel.embed
|
||||
.embed_button as Record<string, string>
|
||||
)['pdf'] = t('Add PDF');
|
||||
)['pdf'] = t('Add PDF', {
|
||||
description: 'Button label to add a PDF file in the editor',
|
||||
});
|
||||
(
|
||||
locales[lang as keyof typeof locales].file_panel.upload
|
||||
.file_placeholder as Record<string, string>
|
||||
)['pdf'] = t('Upload PDF');
|
||||
)['pdf'] = t('Upload PDF', {
|
||||
description: 'Placeholder text of the PDF block upload area',
|
||||
});
|
||||
}
|
||||
}, [lang, t]);
|
||||
|
||||
@@ -129,7 +135,10 @@ const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
|
||||
draggable={false}
|
||||
>
|
||||
<Warning />
|
||||
{t('Invalid or missing PDF file.')}
|
||||
{t('Invalid or missing PDF file.', {
|
||||
description:
|
||||
'Error shown in the PDF block when the file cannot be displayed',
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -153,7 +162,12 @@ const PdfBlockComponent = ({ editor, block }: PdfBlockComponentProps) => {
|
||||
$width="100%"
|
||||
$height="450px"
|
||||
src={pdfUrl}
|
||||
aria-label={block.props.name || t('PDF document')}
|
||||
aria-label={
|
||||
block.props.name ||
|
||||
t('PDF document', {
|
||||
description: 'Accessible name of an embedded PDF',
|
||||
})
|
||||
}
|
||||
contentEditable={false}
|
||||
draggable={false}
|
||||
/>
|
||||
@@ -191,13 +205,34 @@ export const getPdfReactSlashMenuItems = (
|
||||
group: string,
|
||||
) => [
|
||||
{
|
||||
title: t('PDF'),
|
||||
title: t('PDF', {
|
||||
description: 'Name of the PDF file format / editor block',
|
||||
}),
|
||||
onItemClick: () => {
|
||||
insertOrUpdateBlockForSlashMenu(editor, { type: 'pdf' });
|
||||
},
|
||||
aliases: [t('pdf'), t('document'), t('embed'), t('file')],
|
||||
aliases: [
|
||||
t('pdf', {
|
||||
description:
|
||||
'Search keyword for the PDF block in the editor slash menu',
|
||||
}),
|
||||
t('document', {
|
||||
description:
|
||||
'Search keyword for the PDF block in the editor slash menu',
|
||||
}),
|
||||
t('embed', {
|
||||
description:
|
||||
'Search keyword for the PDF block in the editor slash menu',
|
||||
}),
|
||||
t('file', {
|
||||
description:
|
||||
'Search keyword for the PDF block in the editor slash menu',
|
||||
}),
|
||||
],
|
||||
group,
|
||||
icon: <Icon iconName="picture_as_pdf" $size="18px" />,
|
||||
subtext: t('Embed a PDF file'),
|
||||
subtext: t('Embed a PDF file', {
|
||||
description: 'Description of the PDF block in the editor slash menu',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
+4
@@ -121,6 +121,10 @@ const UploadLoaderBlockComponent = ({
|
||||
type: 'warning',
|
||||
information: t(
|
||||
'The antivirus has detected an anomaly in your file.',
|
||||
{
|
||||
description:
|
||||
'Message shown in the editor when an uploaded file is flagged by the antivirus',
|
||||
},
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
+21
-5
@@ -117,7 +117,9 @@ export const getInterlinkinghMenuItems = (
|
||||
) => [
|
||||
{
|
||||
key: 'link-doc',
|
||||
title: t('Link a doc'),
|
||||
title: t('Link a doc', {
|
||||
description: 'Editor menu entry to insert a link to another doc',
|
||||
}),
|
||||
onItemClick: () => {
|
||||
editor.insertInlineContent([
|
||||
{
|
||||
@@ -131,16 +133,22 @@ export const getInterlinkinghMenuItems = (
|
||||
aliases: ['interlinking', 'link', 'anchor', 'a'],
|
||||
group,
|
||||
icon: <LinkPageIcon />,
|
||||
subtext: t('Link this doc to another doc'),
|
||||
subtext: t('Link this doc to another doc', {
|
||||
description: 'Description of the "Link a doc" editor menu entry',
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'new-sub-doc',
|
||||
title: t('New sub-doc'),
|
||||
title: t('New sub-doc', {
|
||||
description: 'Action to create a doc nested under the current doc',
|
||||
}),
|
||||
onItemClick: createPage,
|
||||
aliases: ['new sub-doc'],
|
||||
group,
|
||||
icon: <AddPageIcon />,
|
||||
subtext: t('Create a new sub-doc'),
|
||||
subtext: t('Create a new sub-doc', {
|
||||
description: 'Description of the "New sub-doc" editor menu entry',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -151,7 +159,15 @@ export const useGetInterlinkingMenuItems = () => {
|
||||
return (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => getInterlinkinghMenuItems(editor, t, t('Links'), createChildDoc);
|
||||
) =>
|
||||
getInterlinkinghMenuItems(
|
||||
editor,
|
||||
t,
|
||||
t('Links', {
|
||||
description: 'Group title of the link entries in the editor menu',
|
||||
}),
|
||||
createChildDoc,
|
||||
);
|
||||
};
|
||||
|
||||
const DisableInvalidInterlink = ({
|
||||
|
||||
+16
-4
@@ -165,13 +165,19 @@ export const SearchPage = ({
|
||||
as="input"
|
||||
name="doc-search-input"
|
||||
role="combobox"
|
||||
aria-label={t('Search for a document')}
|
||||
aria-label={t('Search for a document', {
|
||||
description:
|
||||
'Accessible name or placeholder of a document search field',
|
||||
})}
|
||||
aria-expanded={popoverOpened}
|
||||
aria-haspopup="listbox"
|
||||
aria-autocomplete="list"
|
||||
aria-controls={dropdownId}
|
||||
$padding={{ left: '3px' }}
|
||||
placeholder={t('mention a sub-doc...')}
|
||||
placeholder={t('mention a sub-doc...', {
|
||||
description:
|
||||
'Placeholder of the field to search a doc to link in the editor',
|
||||
})}
|
||||
$css={inputStyle}
|
||||
ref={inputRef}
|
||||
$display="inline-flex"
|
||||
@@ -189,7 +195,10 @@ export const SearchPage = ({
|
||||
ref={modalRef}
|
||||
id={dropdownId}
|
||||
role="listbox"
|
||||
aria-label={t('Search results')}
|
||||
aria-label={t('Search results', {
|
||||
description:
|
||||
'Accessible name or heading of the list of search results',
|
||||
})}
|
||||
$minWidth={isDesktop ? '330px' : '220px'}
|
||||
$width="fit-content"
|
||||
$zIndex="10"
|
||||
@@ -261,7 +270,10 @@ export const SearchPage = ({
|
||||
$padding="none"
|
||||
>
|
||||
<DocSearchContent
|
||||
groupName={t('Link a doc')}
|
||||
groupName={t('Link a doc', {
|
||||
description:
|
||||
'Editor menu entry to insert a link to another doc',
|
||||
})}
|
||||
search={search}
|
||||
parentDocId={treeContext?.root?.id}
|
||||
isSearchNotMandatory
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { announce } from '@react-aria/live-announcer';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -8,7 +9,7 @@ import { DocsBlockNoteEditor } from '../types';
|
||||
|
||||
const getFormattingShortcutLabel = (
|
||||
event: KeyboardEvent,
|
||||
t: (key: string) => string,
|
||||
t: TFunction<'translation', undefined>,
|
||||
): string | null => {
|
||||
const isMod = event.ctrlKey || event.metaKey;
|
||||
if (!isMod) {
|
||||
@@ -18,11 +19,20 @@ const getFormattingShortcutLabel = (
|
||||
if (event.altKey) {
|
||||
switch (event.code) {
|
||||
case 'Digit1':
|
||||
return t('Heading 1 applied');
|
||||
return t('Heading 1 applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a heading level 1',
|
||||
});
|
||||
case 'Digit2':
|
||||
return t('Heading 2 applied');
|
||||
return t('Heading 2 applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a heading level 2',
|
||||
});
|
||||
case 'Digit3':
|
||||
return t('Heading 3 applied');
|
||||
return t('Heading 3 applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a heading level 3',
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -31,17 +41,35 @@ const getFormattingShortcutLabel = (
|
||||
if (event.shiftKey) {
|
||||
switch (event.code) {
|
||||
case 'Digit0':
|
||||
return t('Paragraph applied');
|
||||
return t('Paragraph applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut turned a block into a paragraph',
|
||||
});
|
||||
case 'Digit6':
|
||||
return t('Toggle list applied');
|
||||
return t('Toggle list applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a collapsible list',
|
||||
});
|
||||
case 'Digit7':
|
||||
return t('Numbered list applied');
|
||||
return t('Numbered list applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a numbered list',
|
||||
});
|
||||
case 'Digit8':
|
||||
return t('Bulleted list applied');
|
||||
return t('Bulleted list applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a bulleted list',
|
||||
});
|
||||
case 'Digit9':
|
||||
return t('Checklist applied');
|
||||
return t('Checklist applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a checklist',
|
||||
});
|
||||
case 'KeyC':
|
||||
return t('Code block applied');
|
||||
return t('Code block applied', {
|
||||
description:
|
||||
'Screen reader announcement after a keyboard shortcut applied a code block',
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ export const useUploadFile = (docId: string) => {
|
||||
t(
|
||||
'The file "{{fileName}}" is too large. Maximum file size is {{maxFileSize}}.',
|
||||
{
|
||||
description:
|
||||
'Toast shown when an uploaded file exceeds the size limit',
|
||||
fileName: file.name,
|
||||
maxFileSize: formatFileSize(maxFileSize),
|
||||
},
|
||||
@@ -132,7 +134,10 @@ export const useUploadStatus = (editor: DocsBlockNoteEditor) => {
|
||||
{
|
||||
type: 'uploadLoader',
|
||||
props: {
|
||||
information: t('Analyzing file...'),
|
||||
information: t('Analyzing file...', {
|
||||
description:
|
||||
'Placeholder shown while an uploaded file is scanned by the antivirus',
|
||||
}),
|
||||
type: 'loading',
|
||||
blockUploadName,
|
||||
blockUploadType,
|
||||
|
||||
@@ -61,14 +61,21 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
const formatSelect = useMemo(() => {
|
||||
const formatOptions = (exportAGPL?.formats || []).concat([
|
||||
{
|
||||
label: t('Markdown'),
|
||||
label: t('Markdown', {
|
||||
description: 'Name of the Markdown export format',
|
||||
}),
|
||||
value: 'markdown',
|
||||
labelDescription: t('.md(zip)'),
|
||||
labelDescription: t('.md(zip)', {
|
||||
description:
|
||||
'File extension hint of the Markdown export, a zip archive',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: t('HTML'),
|
||||
label: t('HTML', { description: 'Name of the HTML export format' }),
|
||||
value: 'html',
|
||||
labelDescription: t('.html(zip)'),
|
||||
labelDescription: t('.html(zip)', {
|
||||
description: 'File extension hint of the HTML export, a zip archive',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -92,7 +99,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
/** Exports the selected format and always releases the loading state. */
|
||||
async function onSubmit() {
|
||||
if (!editor) {
|
||||
toast(t('The export failed'), VariantType.ERROR);
|
||||
toast(
|
||||
t('The export failed', {
|
||||
description: 'Toast shown when exporting the document fails',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,7 +181,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
}
|
||||
|
||||
if (!blobExport) {
|
||||
toast(t('The export failed'), VariantType.ERROR);
|
||||
toast(
|
||||
t('The export failed', {
|
||||
description: 'Toast shown when exporting the document fails',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,6 +194,8 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
|
||||
toast(
|
||||
t('Your {{format}} was downloaded succesfully', {
|
||||
description:
|
||||
'Toast shown after an export; format is PDF, Docx, ODT...',
|
||||
format,
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
@@ -184,7 +203,12 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
|
||||
shouldClose = true;
|
||||
} catch {
|
||||
toast(t('The export failed'), VariantType.ERROR);
|
||||
toast(
|
||||
t('The export failed', {
|
||||
description: 'Toast shown when exporting the document fails',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -206,16 +230,23 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Cancel the download')}
|
||||
aria-label={t('Cancel the download', {
|
||||
description:
|
||||
'Accessible name of the button cancelling a download',
|
||||
})}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="doc-export-download-button"
|
||||
aria-label={t('Download {{format}}', {
|
||||
description:
|
||||
'Accessible name of the export button; format is PDF, Docx, ODT...',
|
||||
format: formatSelect.formatLabels[format],
|
||||
})}
|
||||
variant="primary"
|
||||
@@ -223,7 +254,9 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
onClick={() => void onSubmit()}
|
||||
disabled={isExporting}
|
||||
>
|
||||
{t('Download')}
|
||||
{t('Download', {
|
||||
description: 'Dropdown menu item to download the document',
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -238,11 +271,16 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
$align="flex-start"
|
||||
data-testid="modal-export-title"
|
||||
>
|
||||
{t('Export')}
|
||||
{t('Export', {
|
||||
description: 'Title of the dialog to export a document',
|
||||
})}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 8px; right: 8px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the download modal')}
|
||||
aria-label={t('Close the download modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the export dialog',
|
||||
})}
|
||||
onClick={() => onClose()}
|
||||
disabled={isExporting}
|
||||
/>
|
||||
@@ -262,6 +300,7 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
id="modal-export-description"
|
||||
>
|
||||
{t('Export your document to download in {{format}} format.', {
|
||||
description: 'Explanation in the export dialog',
|
||||
format: formatSelect.allFormatsLabel,
|
||||
})}
|
||||
</Text>
|
||||
@@ -269,7 +308,9 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
|
||||
<Select
|
||||
clearable={false}
|
||||
fullWidth
|
||||
label={t('Format')}
|
||||
label={t('Format', {
|
||||
description: 'Label of the export format selector',
|
||||
})}
|
||||
options={formatSelect.formatOptions}
|
||||
value={format}
|
||||
onChange={(options) => setFormat(options.target.value as string)}
|
||||
|
||||
@@ -103,9 +103,33 @@ export const useExportAGPL = (doc: Doc, editor?: DocsBlockNoteEditor) => {
|
||||
|
||||
return {
|
||||
formats: [
|
||||
{ label: t('PDF'), value: 'pdf', labelDescription: t('.pdf') },
|
||||
{ label: t('Docx'), value: 'docx', labelDescription: t('.docx') },
|
||||
{ label: t('ODT'), value: 'odt', labelDescription: t('.odt') },
|
||||
{
|
||||
label: t('PDF', {
|
||||
description: 'Name of the PDF file format / editor block',
|
||||
}),
|
||||
value: 'pdf',
|
||||
labelDescription: t('.pdf', {
|
||||
description: 'File extension hint of the PDF export',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: t('Docx', {
|
||||
description: 'Name of the Microsoft Word export format',
|
||||
}),
|
||||
value: 'docx',
|
||||
labelDescription: t('.docx', {
|
||||
description: 'File extension hint of the Word export',
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: t('ODT', {
|
||||
description: 'Name of the OpenDocument text export format',
|
||||
}),
|
||||
value: 'odt',
|
||||
labelDescription: t('.odt', {
|
||||
description: 'File extension hint of the OpenDocument export',
|
||||
}),
|
||||
},
|
||||
],
|
||||
docToBlob,
|
||||
};
|
||||
|
||||
+44
-13
@@ -169,7 +169,10 @@ export const FindReplace = () => {
|
||||
return (
|
||||
<Card
|
||||
role="search"
|
||||
aria-label={t('Find and replace')}
|
||||
aria-label={t('Find and replace', {
|
||||
description:
|
||||
'Accessible name of the find and replace bar in the editor',
|
||||
})}
|
||||
onKeyDown={handlePanelKeyDown}
|
||||
$radius="var(--c--globals--spacings--xs)"
|
||||
$width="331px"
|
||||
@@ -191,8 +194,12 @@ export const FindReplace = () => {
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={handleFindKeyDown}
|
||||
placeholder={t('Find, replace...')}
|
||||
aria-label={t('Find in document')}
|
||||
placeholder={t('Find, replace...', {
|
||||
description: 'Placeholder of the find input',
|
||||
})}
|
||||
aria-label={t('Find in document', {
|
||||
description: 'Accessible name of the find input',
|
||||
})}
|
||||
aria-describedby="find-replace-status"
|
||||
/>
|
||||
<Text
|
||||
@@ -217,7 +224,10 @@ export const FindReplace = () => {
|
||||
{counterLabel}
|
||||
</Text>
|
||||
<Button
|
||||
aria-label={t('Previous match')}
|
||||
aria-label={t('Previous match', {
|
||||
description:
|
||||
'Accessible name of the button going to the previous search result',
|
||||
})}
|
||||
onClick={goToPrevious}
|
||||
disabled={!hasMatches}
|
||||
icon={<ArrowUpIcon aria-hidden="true" width="16" height="16" />}
|
||||
@@ -226,7 +236,10 @@ export const FindReplace = () => {
|
||||
size="nano"
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('Next match')}
|
||||
aria-label={t('Next match', {
|
||||
description:
|
||||
'Accessible name of the button going to the next search result',
|
||||
})}
|
||||
onClick={goToNext}
|
||||
disabled={!hasMatches}
|
||||
icon={<ArrowDownIcon aria-hidden="true" width="16" height="16" />}
|
||||
@@ -243,7 +256,10 @@ export const FindReplace = () => {
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
aria-label={t('Toggle replace')}
|
||||
aria-label={t('Toggle replace', {
|
||||
description:
|
||||
'Accessible name of the button showing or hiding the replace field',
|
||||
})}
|
||||
aria-expanded={isReplaceOpen}
|
||||
color={isReplaceOpen ? 'brand' : 'neutral'}
|
||||
variant={isReplaceOpen ? 'secondary' : 'tertiary'}
|
||||
@@ -254,7 +270,10 @@ export const FindReplace = () => {
|
||||
}
|
||||
/>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close find and replace')}
|
||||
aria-label={t('Close find and replace', {
|
||||
description:
|
||||
'Accessible name of the button closing the find and replace bar',
|
||||
})}
|
||||
onClick={handleClose}
|
||||
size="nano"
|
||||
iconProps={{ width: '16', height: '16' }}
|
||||
@@ -280,28 +299,40 @@ export const FindReplace = () => {
|
||||
value={replacement}
|
||||
onChange={(event) => setReplacement(event.target.value)}
|
||||
onKeyDown={handleReplaceKeyDown}
|
||||
placeholder={t('Replace by...')}
|
||||
aria-label={t('Replace with')}
|
||||
placeholder={t('Replace by...', {
|
||||
description: 'Placeholder of the replacement text input',
|
||||
})}
|
||||
aria-label={t('Replace with', {
|
||||
description: 'Accessible name of the replacement text input',
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
aria-label={t('Replace all')}
|
||||
aria-label={t('Replace all', {
|
||||
description: 'Button to replace every match of the search',
|
||||
})}
|
||||
onClick={handleReplaceAll}
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
size="nano"
|
||||
disabled={!hasMatches}
|
||||
>
|
||||
{t('Replace all')}
|
||||
{t('Replace all', {
|
||||
description: 'Button to replace every match of the search',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Replace')}
|
||||
aria-label={t('Replace', {
|
||||
description: 'Button to replace the current match of the search',
|
||||
})}
|
||||
onClick={handleReplaceCurrent}
|
||||
color="brand"
|
||||
variant="primary"
|
||||
size="nano"
|
||||
disabled={!hasMatches}
|
||||
>
|
||||
{t('Replace')}
|
||||
{t('Replace', {
|
||||
description: 'Button to replace the current match of the search',
|
||||
})}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -10,7 +10,10 @@ export const AlertOffline = () => {
|
||||
return (
|
||||
<Card
|
||||
className="--docs--alert-offline"
|
||||
aria-label={t('Alert offline document')}
|
||||
aria-label={t('Alert offline document', {
|
||||
description:
|
||||
'Accessible name of the banner telling the user is offline',
|
||||
})}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$padding="xs"
|
||||
$flex={1}
|
||||
@@ -28,6 +31,7 @@ export const AlertOffline = () => {
|
||||
<Text $theme="warning">
|
||||
{t(
|
||||
"You're offline. You can keep editing, and your changes will sync automatically once you're back online.",
|
||||
{ description: 'Banner shown when the connection is lost' },
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -28,13 +28,20 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
],
|
||||
options: {
|
||||
onSuccess: (_data) => {
|
||||
toast(t('The document has been restored.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('The document has been restored.', {
|
||||
description: 'Toast shown after a deleted document is restored',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
toast(
|
||||
t('An error occurred while restoring the document: {{error}}', {
|
||||
description: 'Toast shown when restoring a deleted document fails',
|
||||
error: error?.message,
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
@@ -49,7 +56,10 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
return (
|
||||
<Card
|
||||
className="--docs--alert-restore"
|
||||
aria-label={t('Alert deleted document')}
|
||||
aria-label={t('Alert deleted document', {
|
||||
description:
|
||||
'Accessible name of the banner shown on a deleted document',
|
||||
})}
|
||||
$radius={spacingsTokens['3xs']}
|
||||
$direction="row"
|
||||
$padding="xs"
|
||||
@@ -70,7 +80,9 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
iconName="delete"
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
{t('Document deleted')}
|
||||
{t('Document deleted', {
|
||||
description: 'Banner text shown on a document in the trashbin',
|
||||
})}
|
||||
</Box>
|
||||
{doc.abilities.restore && (
|
||||
<Button
|
||||
@@ -91,7 +103,10 @@ export const AlertRestore = ({ doc }: { doc: Doc }) => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t('Restore')}
|
||||
{t('Restore', {
|
||||
description:
|
||||
'Button to restore a deleted document or an older version',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -43,7 +43,10 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
<>
|
||||
<Box
|
||||
$width="100%"
|
||||
aria-label={t('It is the card information about the document.')}
|
||||
aria-label={t('It is the card information about the document.', {
|
||||
description:
|
||||
'Accessible name of the document header information block',
|
||||
})}
|
||||
className="--docs--doc-header"
|
||||
$minHeight="125px"
|
||||
$css={css`
|
||||
@@ -88,7 +91,15 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
isAprilFools ? '🐟' : '📄',
|
||||
);
|
||||
}}
|
||||
aria-label={emoji ? t('Remove emoji') : t('Add emoji')}
|
||||
aria-label={
|
||||
emoji
|
||||
? t('Remove emoji', {
|
||||
description: 'Button to remove the emoji of a document',
|
||||
})
|
||||
: t('Add emoji', {
|
||||
description: 'Button to add an emoji to a document',
|
||||
})
|
||||
}
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
icon={
|
||||
@@ -100,7 +111,13 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
}
|
||||
style={{ width: 'fit-content' }}
|
||||
>
|
||||
{emoji ? t('Remove emoji') : t('Add emoji')}
|
||||
{emoji
|
||||
? t('Remove emoji', {
|
||||
description: 'Button to remove the emoji of a document',
|
||||
})
|
||||
: t('Add emoji', {
|
||||
description: 'Button to add an emoji to a document',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
+28
-7
@@ -34,10 +34,16 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
|
||||
if (trashbinCutoff && doc.deleted_at) {
|
||||
const daysLeft = calculateDaysLeft(doc.deleted_at, trashbinCutoff);
|
||||
dateLabel = t('Days remaining:');
|
||||
dateValue = `${daysLeft} ${t('days', { count: daysLeft })}`;
|
||||
dateLabel = t('Days remaining:', {
|
||||
description:
|
||||
'Label before the number of days left before a trashed document is permanently deleted',
|
||||
});
|
||||
dateValue = `${daysLeft} ${t('days', { description: 'Unit after a number of days, plural form', count: daysLeft })}`;
|
||||
} else {
|
||||
dateLabel = t('Last update:');
|
||||
dateLabel = t('Last update:', {
|
||||
description:
|
||||
'Label before the date of the last modification of a document',
|
||||
});
|
||||
dateValue = relativeOnly;
|
||||
}
|
||||
|
||||
@@ -46,7 +52,10 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
{doc.is_favorite && (
|
||||
<>
|
||||
<Text as="dt" className="sr-only">
|
||||
{t('This document is starred')}
|
||||
{t('This document is starred', {
|
||||
description:
|
||||
"Screen reader text indicating the document is in the user's favorites",
|
||||
})}
|
||||
</Text>
|
||||
<Text
|
||||
as="dd"
|
||||
@@ -68,7 +77,10 @@ export const DocHeaderInfo = ({ doc }: DocHeaderInfoProps) => {
|
||||
</>
|
||||
)}
|
||||
<Text as="dt" className="sr-only">
|
||||
{t('Role')}
|
||||
{t('Role', {
|
||||
description:
|
||||
"Screen reader label before the user's role on the document",
|
||||
})}
|
||||
</Text>
|
||||
<Text
|
||||
as="dd"
|
||||
@@ -119,7 +131,11 @@ const VisibilityDoc = ({ doc }: { doc: Doc }) => {
|
||||
return (
|
||||
<>
|
||||
<PublicSVG aria-hidden="true" width="16" height="16" />
|
||||
{t('Public')} ·
|
||||
|
||||
{t('Public', {
|
||||
description: 'Document visibility: anyone with the link can access',
|
||||
})}
|
||||
·
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -128,7 +144,12 @@ const VisibilityDoc = ({ doc }: { doc: Doc }) => {
|
||||
return (
|
||||
<>
|
||||
<ProtedtedSVG aria-hidden="true" width="16" height="16" />
|
||||
{t('Internal')} ·
|
||||
|
||||
{t('Internal', {
|
||||
description:
|
||||
'Document visibility: any logged-in user with the link can access',
|
||||
})}
|
||||
·
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+8
-2
@@ -87,8 +87,14 @@ export const DocLeftPanelCollapseButton = () => {
|
||||
const docTitle = titleWithoutEmoji || untitledDocument;
|
||||
const shouldShowButtonTitle = !isPanelOpen && !isDocTitleVisible;
|
||||
const ariaLabel = isPanelOpen
|
||||
? t('Hide the side panel for {{title}}', { title: docTitle })
|
||||
: t('Show the side panel for {{title}}', { title: docTitle });
|
||||
? t('Hide the side panel for {{title}}', {
|
||||
description: 'Accessible name of the button collapsing the left panel',
|
||||
title: docTitle,
|
||||
})
|
||||
: t('Show the side panel for {{title}}', {
|
||||
description: 'Accessible name of the button expanding the left panel',
|
||||
title: docTitle,
|
||||
});
|
||||
|
||||
return (
|
||||
<LeftPanelCollapseButton
|
||||
|
||||
@@ -98,7 +98,9 @@ const DocTitleEmojiPicker = ({ doc }: DocTitleProps) => {
|
||||
width="25px"
|
||||
height="25px"
|
||||
aria-hidden="true"
|
||||
aria-label={t('Simple document icon')}
|
||||
aria-label={t('Simple document icon', {
|
||||
description: 'Accessible name of the default icon of a document',
|
||||
})}
|
||||
color={colorsTokens['brand-500']}
|
||||
/>
|
||||
}
|
||||
@@ -244,7 +246,7 @@ const DocTitleInput = ({ doc, onTitleUpdate }: DocTitleProps) => {
|
||||
defaultValue={titleDisplay || undefined}
|
||||
onKeyDownCapture={handleKeyDown}
|
||||
suppressContentEditableWarning={true}
|
||||
aria-label={`${t('Document title')}`}
|
||||
aria-label={`${t('Document title', { description: 'Accessible name of the editable document title' })}`}
|
||||
aria-multiline={false}
|
||||
onBlurCapture={(event) =>
|
||||
handleTitleSubmit(event.target.textContent || '')
|
||||
|
||||
+6
-2
@@ -46,13 +46,17 @@ export function useCreateFavoriteDoc({
|
||||
|
||||
syncDocInTree(treeContext, id, { is_favorite: true });
|
||||
|
||||
const message = t('Document starred successfully!');
|
||||
const message = t('Document starred successfully!', {
|
||||
description: 'Toast shown after adding a document to favorites',
|
||||
});
|
||||
announce(message, 'polite');
|
||||
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: () => {
|
||||
const message = t('Failed to star the document.');
|
||||
const message = t('Failed to star the document.', {
|
||||
description: 'Toast shown when adding a document to favorites failed',
|
||||
});
|
||||
announce(message, 'assertive');
|
||||
},
|
||||
});
|
||||
|
||||
+7
-2
@@ -46,13 +46,18 @@ export function useDeleteFavoriteDoc({
|
||||
|
||||
syncDocInTree(treeContext, id, { is_favorite: false });
|
||||
|
||||
const message = t('Document unstarred successfully!');
|
||||
const message = t('Document unstarred successfully!', {
|
||||
description: 'Toast shown after removing a document from favorites',
|
||||
});
|
||||
announce(message, 'polite');
|
||||
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: () => {
|
||||
const message = t('Failed to unstar the document.');
|
||||
const message = t('Failed to unstar the document.', {
|
||||
description:
|
||||
'Toast shown when removing a document from favorites failed',
|
||||
});
|
||||
announce(message, 'assertive');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -94,7 +94,9 @@ export function useDuplicateDoc(options?: DuplicateDocOptions) {
|
||||
void options?.onSuccess?.(data, variables, onMutateResult, context);
|
||||
},
|
||||
onError: (error, variables, onMutateResult, context) => {
|
||||
const message = t('Failed to duplicate the document...');
|
||||
const message = t('Failed to duplicate the document...', {
|
||||
description: 'Toast shown when duplicating a document failed',
|
||||
});
|
||||
toast(message, VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
@@ -122,6 +122,7 @@ export function useImportDoc(props?: UseImportDocOptions) {
|
||||
|
||||
toast(
|
||||
t('The document "{{documentName}}" has been successfully imported', {
|
||||
description: 'Toast shown after a file is imported as a document',
|
||||
documentName: importedDoc.title || '',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
@@ -132,6 +133,7 @@ export function useImportDoc(props?: UseImportDocOptions) {
|
||||
onError: (...errorProps) => {
|
||||
toast(
|
||||
t(`The document "{{documentName}}" import has failed`, {
|
||||
description: 'Toast shown when importing a file as a document failed',
|
||||
documentName: errorProps?.[1][0].name || '',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
|
||||
+51
-15
@@ -70,8 +70,13 @@ export const useDuplicatedDoc = ({
|
||||
|
||||
toast(
|
||||
isTopRoot && isCurrentDoc
|
||||
? t('Document duplicated to My docs')
|
||||
: t('Document duplicated'),
|
||||
? t('Document duplicated to My docs', {
|
||||
description:
|
||||
"Toast title after duplicating a root document into the user's own docs",
|
||||
})
|
||||
: t('Document duplicated', {
|
||||
description: 'Toast title after duplicating a document',
|
||||
}),
|
||||
VariantType.INFO,
|
||||
{
|
||||
duration: 10000,
|
||||
@@ -115,22 +120,32 @@ export const ConfirmationDuplicateModal = ({
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
onClose={onClose}
|
||||
aria-label={t('Confirmation to duplicate the document')}
|
||||
aria-label={t('Confirmation to duplicate the document', {
|
||||
description: 'Accessible name of the duplicate dialog',
|
||||
})}
|
||||
aria-labelledby="modal-duplicate-doc-title"
|
||||
aria-describedby={isTopRoot ? 'modal-duplicate-doc-desc' : undefined}
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Cancel the duplicate action')}
|
||||
aria-label={t('Cancel the duplicate action', {
|
||||
description:
|
||||
'Accessible name of the button cancelling the duplication',
|
||||
})}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Confirm the duplicate action')}
|
||||
aria-label={t('Confirm the duplicate action', {
|
||||
description:
|
||||
'Accessible name of the button confirming the duplication',
|
||||
})}
|
||||
fullWidth
|
||||
disabled={isDuplicatePending}
|
||||
onClick={() => {
|
||||
@@ -141,7 +156,9 @@ export const ConfirmationDuplicateModal = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Duplicate')}
|
||||
{t('Duplicate', {
|
||||
description: 'Dropdown menu item to duplicate the document',
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -155,11 +172,16 @@ export const ConfirmationDuplicateModal = ({
|
||||
$margin="0"
|
||||
$align="flex-start"
|
||||
>
|
||||
{t('Duplicate')}
|
||||
{t('Duplicate', {
|
||||
description: 'Dropdown menu item to duplicate the document',
|
||||
})}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 8px; right: 8px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the duplicate modal')}
|
||||
aria-label={t('Close the duplicate modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the duplicate dialog',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -176,12 +198,17 @@ export const ConfirmationDuplicateModal = ({
|
||||
as="p"
|
||||
$margin="0"
|
||||
>
|
||||
{t('The copy will be private and added to My docs.')}
|
||||
{t('The copy will be private and added to My docs.', {
|
||||
description: 'Explanation in the duplicate dialog',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
<Box $margin={{ vertical: 'base' }}>
|
||||
<Checkbox
|
||||
label={t('Duplicate subdocs')}
|
||||
label={t('Duplicate subdocs', {
|
||||
description:
|
||||
'Checkbox label: also duplicate the nested sub-documents',
|
||||
})}
|
||||
checked={isWithSubdocs}
|
||||
onChange={(e) => setIsWithSubdocs(e.target.checked)}
|
||||
/>
|
||||
@@ -221,9 +248,16 @@ export const ToastActions = ({
|
||||
onError: (error) => {
|
||||
// A 401 is already handled globally (redirect to login), avoid a duplicate toast.
|
||||
if (error.status !== 401) {
|
||||
toast(t('The document could not be deleted.'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('The document could not be deleted.', {
|
||||
description:
|
||||
'Toast shown when deleting a document failed (undo of a duplicate)',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -278,7 +312,9 @@ export const ToastActions = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Undo')}
|
||||
{t('Undo', {
|
||||
description: 'Toast button to cancel the previous action',
|
||||
})}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -58,10 +58,15 @@ export const DocIcon = ({
|
||||
|
||||
const emojiLabel = withEmojiPicker
|
||||
? emoji
|
||||
? t('Edit document emoji')
|
||||
: t('Add emoji')
|
||||
? t('Edit document emoji', {
|
||||
description:
|
||||
'Accessible name of the button changing a document emoji',
|
||||
})
|
||||
: t('Add emoji', { description: 'Button to add an emoji to a document' })
|
||||
: emoji
|
||||
? t('Document emoji')
|
||||
? t('Document emoji', {
|
||||
description: 'Accessible name of the emoji of a document',
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const toggleEmojiPicker = (e: MouseEvent) => {
|
||||
|
||||
+50
-12
@@ -129,7 +129,9 @@ export const DocMoveModal = ({
|
||||
})
|
||||
.catch(() => {
|
||||
toast(
|
||||
t(`An error occurred while moving the document.`),
|
||||
t(`An error occurred while moving the document.`, {
|
||||
description: 'Toast shown when moving a document failed',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
);
|
||||
});
|
||||
@@ -144,7 +146,9 @@ export const DocMoveModal = ({
|
||||
closeOnClickOutside
|
||||
size={isModal ? ModalSize.MEDIUM : ModalSize.FULL}
|
||||
hideCloseButton
|
||||
aria-label={t('Move Modal')}
|
||||
aria-label={t('Move Modal', {
|
||||
description: 'Accessible name of the dialog to move a document',
|
||||
})}
|
||||
rightActions={
|
||||
<Box
|
||||
$direction="row-reverse"
|
||||
@@ -152,7 +156,10 @@ export const DocMoveModal = ({
|
||||
$gap="small"
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Move the document to the selected location')}
|
||||
aria-label={t('Move the document to the selected location', {
|
||||
description:
|
||||
'Accessible name of the button confirming the move',
|
||||
})}
|
||||
variant="primary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
@@ -170,26 +177,39 @@ export const DocMoveModal = ({
|
||||
}}
|
||||
disabled={!docSelected}
|
||||
>
|
||||
{t('Move here')}
|
||||
{t('Move here', {
|
||||
description:
|
||||
'Button to move the document to the selected parent',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Cancel the move')}
|
||||
aria-label={t('Cancel the move', {
|
||||
description:
|
||||
'Accessible name of the button cancelling the move',
|
||||
})}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
title={
|
||||
<>
|
||||
<Text as="h2" $margin="0" $size="s" $align="flex-start">
|
||||
{t('Choose a new parent doc')}
|
||||
{t('Choose a new parent doc', {
|
||||
description: 'Heading of the dialog to move a document',
|
||||
})}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 8px; right: 8px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the move modal')}
|
||||
aria-label={t('Close the move modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the move dialog',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -211,8 +231,14 @@ export const DocMoveModal = ({
|
||||
}}
|
||||
>
|
||||
<QuickSearch
|
||||
label={t('Search for a doc...')}
|
||||
placeholder={t('Search for a doc...')}
|
||||
label={t('Search for a doc...', {
|
||||
description:
|
||||
'Label and placeholder of the doc search in the move dialog',
|
||||
})}
|
||||
placeholder={t('Search for a doc...', {
|
||||
description:
|
||||
'Label and placeholder of the doc search in the move dialog',
|
||||
})}
|
||||
loading={loading}
|
||||
onFilter={handleInputSearch}
|
||||
>
|
||||
@@ -223,7 +249,17 @@ export const DocMoveModal = ({
|
||||
>
|
||||
<Box>
|
||||
<DocSearchContent
|
||||
groupName={search ? t('Search results') : t('All docs')}
|
||||
groupName={
|
||||
search
|
||||
? t('Search results', {
|
||||
description:
|
||||
'Accessible name or heading of the list of search results',
|
||||
})
|
||||
: t('All docs', {
|
||||
description:
|
||||
'Group title listing every doc the user can access',
|
||||
})
|
||||
}
|
||||
search={search}
|
||||
filterResults={(docResults) => docResults.id !== doc.id}
|
||||
onSelect={handleSelect}
|
||||
@@ -303,7 +339,9 @@ export const DocMoveModal = ({
|
||||
onClose();
|
||||
}}
|
||||
targetDocumentTitle={docTargetTitle}
|
||||
title={t('Move document')}
|
||||
title={t('Move document', {
|
||||
description: 'Title of the dialog to move a document',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+26
-6
@@ -47,11 +47,17 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
<Head>
|
||||
<meta name="robots" content="noindex" />
|
||||
<title>
|
||||
{t('Access Denied - Error 403')} - {t('Docs')}
|
||||
{t('Access Denied - Error 403', {
|
||||
description: 'Page title when the user may not open a document',
|
||||
})}{' '}
|
||||
-{' '}
|
||||
{t('Docs', {
|
||||
description: 'Product name, shown in page titles and the logo',
|
||||
})}
|
||||
</title>
|
||||
<meta
|
||||
property="og:title"
|
||||
content={`${t('Access Denied - Error 403')} - ${t('Docs')}`}
|
||||
content={`${t('Access Denied - Error 403', { description: 'Page title when the user may not open a document' })} - ${t('Docs', { description: 'Product name, shown in page titles and the logo' })}`}
|
||||
key="title"
|
||||
/>
|
||||
</Head>
|
||||
@@ -73,7 +79,10 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
$theme="neutral"
|
||||
$variation="primary"
|
||||
>
|
||||
{t('Access denied')}
|
||||
{t('Access denied', {
|
||||
description:
|
||||
'Heading shown when the user may not open a document',
|
||||
})}
|
||||
</Text>
|
||||
<Text
|
||||
as="p"
|
||||
@@ -85,8 +94,13 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
$size="xs"
|
||||
>
|
||||
{hasRequested
|
||||
? t('Your access request for this document is pending.')
|
||||
: t('Insufficient access rights to view the document.')}
|
||||
? t('Your access request for this document is pending.', {
|
||||
description:
|
||||
'Message on the access denied page after requesting access',
|
||||
})
|
||||
: t('Insufficient access rights to view the document.', {
|
||||
description: 'Message on the access denied page',
|
||||
})}
|
||||
</Text>
|
||||
{isSubDocument && (
|
||||
<Text
|
||||
@@ -100,6 +114,10 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
>
|
||||
{t(
|
||||
"You're currently viewing a sub-document. To gain access, please request permission from the main document.",
|
||||
{
|
||||
description:
|
||||
'Message on the access denied page of a sub-document',
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
@@ -133,7 +151,9 @@ export const DocPage403 = ({ id }: DocProps) => {
|
||||
$variation="tertiary"
|
||||
$margin="0"
|
||||
>
|
||||
{t('Home')}
|
||||
{t('Home', {
|
||||
description: 'Button/link label to go back to the home page',
|
||||
})}
|
||||
</Text>
|
||||
</StyledLink>
|
||||
{!isSubDocument && (
|
||||
|
||||
+14
-3
@@ -287,7 +287,13 @@ const DocToolBoxComponent = ({
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: doc.is_favorite ? t('Unstar') : t('Star'),
|
||||
label: doc.is_favorite
|
||||
? t('Unstar', {
|
||||
description: 'Menu action to remove a document from favorites',
|
||||
})
|
||||
: t('Star', {
|
||||
description: 'Menu action to add a document to favorites',
|
||||
}),
|
||||
icon: doc.is_favorite ? (
|
||||
<StarSlashIcon width={18} height={18} aria-hidden="true" />
|
||||
) : (
|
||||
@@ -324,7 +330,9 @@ const DocToolBoxComponent = ({
|
||||
isHidden: !doc.abilities.duplicate,
|
||||
},
|
||||
{
|
||||
label: t('Move to my docs'),
|
||||
label: t('Move to my docs', {
|
||||
description: "Menu action to move a document into the user's own docs",
|
||||
}),
|
||||
isHidden: isTopParent || doc.user_role !== Role.OWNER,
|
||||
icon: <DocMoveOutIcon width={18} height={18} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
@@ -350,7 +358,9 @@ const DocToolBoxComponent = ({
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('Move into a doc'),
|
||||
label: t('Move into a doc', {
|
||||
description: 'Menu action to move a document under another doc',
|
||||
}),
|
||||
icon: <DocMoveInIcon width={18} height={18} aria-hidden="true" />,
|
||||
callback: () => {
|
||||
setIsModalMoveOpen(true);
|
||||
@@ -415,6 +425,7 @@ const DocToolBoxComponent = ({
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Open the document options: {{title}}', {
|
||||
description: 'Accessible name of the document actions menu button',
|
||||
title: doc.title || untitledDocument,
|
||||
})}
|
||||
size="small"
|
||||
|
||||
+9
-3
@@ -21,8 +21,12 @@ export const ModalConfirmationMoveDoc = ({
|
||||
<AlertModal
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
title={t('Move document')}
|
||||
aria-label={t('Modal confirmation for moving a document')}
|
||||
title={t('Move document', {
|
||||
description: 'Title of the dialog to move a document',
|
||||
})}
|
||||
aria-label={t('Modal confirmation for moving a document', {
|
||||
description: 'Accessible name of the move confirmation dialog',
|
||||
})}
|
||||
description={
|
||||
<Text $display="inline">
|
||||
<Trans
|
||||
@@ -34,7 +38,9 @@ export const ModalConfirmationMoveDoc = ({
|
||||
/>
|
||||
</Text>
|
||||
}
|
||||
confirmLabel={t('Move')}
|
||||
confirmLabel={t('Move', {
|
||||
description: 'Button confirming the move of a document',
|
||||
})}
|
||||
onConfirm={onConfirm}
|
||||
/>
|
||||
);
|
||||
|
||||
+38
-11
@@ -56,9 +56,15 @@ export const ModalRemoveDoc = ({
|
||||
|
||||
onClose();
|
||||
|
||||
toast(t('The document has been deleted.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('The document has been deleted.', {
|
||||
description: 'Toast shown after deleting a document',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -82,26 +88,38 @@ export const ModalRemoveDoc = ({
|
||||
closeOnClickOutside
|
||||
hideCloseButton
|
||||
onClose={onClose}
|
||||
aria-label={t('Delete a doc')}
|
||||
aria-label={t('Delete a doc', {
|
||||
description: 'Title and accessible name of the delete document dialog',
|
||||
})}
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
ref={cancelButtonRef}
|
||||
aria-label={t('Cancel the deletion')}
|
||||
aria-label={t('Cancel the deletion', {
|
||||
description:
|
||||
'Accessible name of the button cancelling the deletion',
|
||||
})}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Delete document')}
|
||||
aria-label={t('Delete document', {
|
||||
description:
|
||||
'Accessible name of the button confirming the deletion',
|
||||
})}
|
||||
color="error"
|
||||
fullWidth
|
||||
onClick={handleDelete}
|
||||
>
|
||||
{t('Delete')}
|
||||
{t('Delete', {
|
||||
description: 'Dropdown menu item to delete the document',
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -115,11 +133,17 @@ export const ModalRemoveDoc = ({
|
||||
$margin="0"
|
||||
$align="flex-start"
|
||||
>
|
||||
{t('Delete a doc')}
|
||||
{t('Delete a doc', {
|
||||
description:
|
||||
'Title and accessible name of the delete document dialog',
|
||||
})}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 8px; right: 8px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the delete modal')}
|
||||
aria-label={t('Close the delete modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the delete dialog',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -143,7 +167,10 @@ export const ModalRemoveDoc = ({
|
||||
) : (
|
||||
t(
|
||||
'This document will be placed in the trashbin. You can restore it within {{days}} days.',
|
||||
{ days: trashBinCutoffDays },
|
||||
{
|
||||
description: 'Explanation in the delete dialog',
|
||||
days: trashBinCutoffDays,
|
||||
},
|
||||
)
|
||||
)}
|
||||
</Text>
|
||||
|
||||
+11
-4
@@ -48,7 +48,7 @@ export const NewDocButton = ({ onClose }: NewDocButtonProps) => {
|
||||
}}
|
||||
>
|
||||
<Text $withThemeInherited $size="md" $weight="500">
|
||||
{t('New')}
|
||||
{t('New', { description: 'Button to create a new document' })}
|
||||
</Text>
|
||||
</ButtonLink>
|
||||
{isDropdownEnabled && <DropdownArrow />}
|
||||
@@ -92,7 +92,9 @@ export function DropdownArrow() {
|
||||
const options = useMemo<DropdownMenuItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: t('New sub-doc'),
|
||||
label: t('New sub-doc', {
|
||||
description: 'Action to create a doc nested under the current doc',
|
||||
}),
|
||||
icon: <SubDocIcon aria-hidden="true" width="24" height="24" />,
|
||||
callback: () => {
|
||||
if (currentDoc) {
|
||||
@@ -104,7 +106,9 @@ export function DropdownArrow() {
|
||||
isHidden: !currentDoc,
|
||||
},
|
||||
{
|
||||
label: t('Import a document'),
|
||||
label: t('Import a document', {
|
||||
description: 'Menu action to create a document from a file',
|
||||
}),
|
||||
icon: <UploadIcon aria-hidden="true" width="24" height="24" />,
|
||||
callback: openImport,
|
||||
isHidden: !isImportEnabled || !!currentDoc,
|
||||
@@ -121,7 +125,10 @@ export function DropdownArrow() {
|
||||
onOpenChange={setIsMenuOpen}
|
||||
>
|
||||
<Button
|
||||
aria-label={t('Open new document options')}
|
||||
aria-label={t('Open new document options', {
|
||||
description:
|
||||
'Accessible name of the button opening the new document menu',
|
||||
})}
|
||||
color="brand"
|
||||
variant="primary"
|
||||
iconPosition="left"
|
||||
|
||||
+3
-1
@@ -42,9 +42,11 @@ export const SimpleDocItem = ({
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title || '');
|
||||
const docTitle = titleWithoutEmoji || untitledDocument;
|
||||
const docRelativeUpdate = relativeDate(doc.updated_at);
|
||||
const itemAriaLabel = `${t('Open document {{title}}', { title: docTitle })}. ${t(
|
||||
const itemAriaLabel = `${t('Open document {{title}}', { description: 'Accessible name of a document link, an action to open it', title: docTitle })}. ${t(
|
||||
'Last update: {{update}}',
|
||||
{
|
||||
description:
|
||||
'Screen reader text giving the last modification date of a document',
|
||||
update: formatDate(doc.updated_at),
|
||||
},
|
||||
)}`;
|
||||
|
||||
+16
-5
@@ -11,7 +11,10 @@ export const useCopyCurrentEditorToClipboard = () => {
|
||||
|
||||
return async (asFormat: 'html' | 'markdown') => {
|
||||
if (!editor) {
|
||||
const message = t('Editor unavailable');
|
||||
const message = t('Editor unavailable', {
|
||||
description:
|
||||
'Toast shown when copying content while the editor is not ready',
|
||||
});
|
||||
toast(message, VariantType.ERROR, { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
@@ -24,16 +27,24 @@ export const useCopyCurrentEditorToClipboard = () => {
|
||||
await navigator.clipboard.writeText(editorContentFormatted);
|
||||
const successMessage =
|
||||
asFormat === 'markdown'
|
||||
? t('Copied as Markdown to clipboard')
|
||||
: t('Copied to clipboard');
|
||||
? t('Copied as Markdown to clipboard', {
|
||||
description: 'Toast after copying the document as Markdown',
|
||||
})
|
||||
: t('Copied to clipboard', {
|
||||
description: 'Toast after copying something to the clipboard',
|
||||
});
|
||||
|
||||
toast(successMessage, VariantType.SUCCESS, { duration: 3000 });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const errorMessage =
|
||||
asFormat === 'markdown'
|
||||
? t('Failed to copy as Markdown to clipboard')
|
||||
: t('Failed to copy to clipboard');
|
||||
? t('Failed to copy as Markdown to clipboard', {
|
||||
description: 'Toast when copying as Markdown failed',
|
||||
})
|
||||
: t('Failed to copy to clipboard', {
|
||||
description: 'Toast when copying to the clipboard failed',
|
||||
});
|
||||
|
||||
toast(errorMessage, VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
|
||||
@@ -12,8 +12,12 @@ export const useCopyDocLink = (docId: Doc['id']) => {
|
||||
return useCallback(() => {
|
||||
copyToClipboard(
|
||||
`${window.location.origin}/docs/${docId}/`,
|
||||
t('Link Copied !'),
|
||||
t('Failed to copy link'),
|
||||
t('Link Copied !', {
|
||||
description: 'Toast confirming a link was copied to the clipboard',
|
||||
}),
|
||||
t('Failed to copy link', {
|
||||
description: 'Toast shown when copying a link failed',
|
||||
}),
|
||||
);
|
||||
}, [copyToClipboard, docId, t]);
|
||||
};
|
||||
|
||||
@@ -62,6 +62,8 @@ export const useImport = ({ onDragOver, onImportSuccess }: UseImportProps) => {
|
||||
? `The document "{{documentName}}" import has failed (only {{allowedExtensions}} files are allowed)`
|
||||
: `The document "{{documentName}}" import has failed`,
|
||||
{
|
||||
description:
|
||||
'Toast shown when a file to import has an unsupported type',
|
||||
documentName: fileName,
|
||||
allowedExtensions,
|
||||
},
|
||||
@@ -108,6 +110,8 @@ export const useImport = ({ onDragOver, onImportSuccess }: UseImportProps) => {
|
||||
t(
|
||||
'The document "{{documentName}}" is too large. Maximum file size is {{maxFileSize}}.',
|
||||
{
|
||||
description:
|
||||
'Toast shown when an imported file exceeds the size limit',
|
||||
documentName: rejection.file.name,
|
||||
maxFileSize: MAX_FILE_SIZE.text,
|
||||
},
|
||||
|
||||
@@ -6,18 +6,34 @@ export const useTrans = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const translatedRoles = {
|
||||
[Role.READER]: t('Reader'),
|
||||
[Role.EDITOR]: t('Editor'),
|
||||
[Role.ADMIN]: t('Administrator'),
|
||||
[Role.OWNER]: t('Owner'),
|
||||
[Role.READER]: t('Reader', {
|
||||
description: 'Role on a document: can only read',
|
||||
}),
|
||||
[Role.EDITOR]: t('Editor', { description: 'Role on a document: can edit' }),
|
||||
[Role.ADMIN]: t('Administrator', {
|
||||
description: 'Role on a document: can edit and manage sharing',
|
||||
}),
|
||||
[Role.OWNER]: t('Owner', {
|
||||
description: 'Role on a document: full control, including deletion',
|
||||
}),
|
||||
};
|
||||
|
||||
const translatedFilters = {
|
||||
[DocDefaultFilter.ALL_DOCS]: t('Recent'),
|
||||
[DocDefaultFilter.MY_DOCS]: t('My docs'),
|
||||
[DocDefaultFilter.SHARED_WITH_ME]: t('Shared with me'),
|
||||
[DocDefaultFilter.STARRED]: t('Starred'),
|
||||
[DocDefaultFilter.TRASHBIN]: t('Trashbin'),
|
||||
[DocDefaultFilter.ALL_DOCS]: t('Recent', {
|
||||
description: 'Name of the filter listing recently opened docs',
|
||||
}),
|
||||
[DocDefaultFilter.MY_DOCS]: t('My docs', {
|
||||
description: 'Name of the filter listing docs owned by the user',
|
||||
}),
|
||||
[DocDefaultFilter.SHARED_WITH_ME]: t('Shared with me', {
|
||||
description: 'Name of the filter listing docs shared with the user',
|
||||
}),
|
||||
[DocDefaultFilter.STARRED]: t('Starred', {
|
||||
description: "Name of the filter listing the user's favorite docs",
|
||||
}),
|
||||
[DocDefaultFilter.TRASHBIN]: t('Trashbin', {
|
||||
description: 'Name of the filter listing deleted docs',
|
||||
}),
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -27,7 +43,9 @@ export const useTrans = () => {
|
||||
transFilter: (filter: DocDefaultFilter) => {
|
||||
return translatedFilters[filter];
|
||||
},
|
||||
untitledDocument: t('Untitled document'),
|
||||
untitledDocument: t('Untitled document', {
|
||||
description: 'Name displayed for a document without a title',
|
||||
}),
|
||||
translatedRoles,
|
||||
translatedFilters,
|
||||
};
|
||||
|
||||
+4
-1
@@ -48,7 +48,10 @@ export const PresentBlockItem = () => {
|
||||
>
|
||||
<Box $align="center" $gap="xxs" $direction="row">
|
||||
<PresentIcon width="16" height="16" aria-hidden="true" />
|
||||
{t('Present from here')}
|
||||
{t('Present from here', {
|
||||
description:
|
||||
'Editor side menu action starting the presentation from the current block',
|
||||
})}
|
||||
</Box>
|
||||
</Components.Generic.Menu.Item>
|
||||
);
|
||||
|
||||
+34
-8
@@ -138,12 +138,16 @@ export const PresenterFloatingBar = ({
|
||||
const actionOptions = useMemo<DropdownMenuItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: t('Copy link to slide'),
|
||||
label: t('Copy link to slide', {
|
||||
description: 'Menu action copying a link to the current slide',
|
||||
}),
|
||||
icon: <Link aria-hidden="true" width="16" height="16" />,
|
||||
callback: onCopyLink,
|
||||
},
|
||||
{
|
||||
label: t('Download PDF'),
|
||||
label: t('Download PDF', {
|
||||
description: 'Menu action downloading the presentation as PDF',
|
||||
}),
|
||||
icon: <Download aria-hidden="true" width="16" height="16" />,
|
||||
callback: onExportPdf,
|
||||
isDisabled: isExportingPdf,
|
||||
@@ -161,7 +165,9 @@ export const PresenterFloatingBar = ({
|
||||
$align="center"
|
||||
$css={barCss}
|
||||
role="toolbar"
|
||||
aria-label={t('Presenter controls')}
|
||||
aria-label={t('Presenter controls', {
|
||||
description: 'Accessible name of the presentation toolbar',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
size="nano"
|
||||
@@ -169,7 +175,10 @@ export const PresenterFloatingBar = ({
|
||||
variant="tertiary"
|
||||
aria-disabled={isFirst}
|
||||
onClick={isFirst ? undefined : onPrev}
|
||||
aria-label={t('Previous slide')}
|
||||
aria-label={t('Previous slide', {
|
||||
description:
|
||||
'Accessible name of the button going to the previous slide',
|
||||
})}
|
||||
icon={<ChevronLeft size="small" />}
|
||||
/>
|
||||
<Text as="span" $size="sm" $color="neutral" aria-hidden="true">
|
||||
@@ -181,7 +190,10 @@ export const PresenterFloatingBar = ({
|
||||
variant="tertiary"
|
||||
aria-disabled={isLast}
|
||||
onClick={isLast ? undefined : onNext}
|
||||
aria-label={t('Next slide')}
|
||||
aria-label={t('Next slide', {
|
||||
description:
|
||||
'Accessible name of the button going to the next slide',
|
||||
})}
|
||||
icon={<ChevronRight size="small" />}
|
||||
/>
|
||||
<Box $css={separatorCss} aria-hidden />
|
||||
@@ -197,7 +209,10 @@ export const PresenterFloatingBar = ({
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
onClick={toggleActions}
|
||||
aria-label={t('More options')}
|
||||
aria-label={t('More options', {
|
||||
description:
|
||||
'Accessible name of the button opening more presentation actions',
|
||||
})}
|
||||
aria-expanded={isActionsOpen}
|
||||
aria-haspopup="menu"
|
||||
icon={<Share size="small" />}
|
||||
@@ -209,7 +224,15 @@ export const PresenterFloatingBar = ({
|
||||
variant="tertiary"
|
||||
onClick={onToggleFullscreen}
|
||||
aria-label={
|
||||
isFullscreen ? t('Exit fullscreen') : t('Enter fullscreen')
|
||||
isFullscreen
|
||||
? t('Exit fullscreen', {
|
||||
description:
|
||||
'Accessible name of the button leaving fullscreen',
|
||||
})
|
||||
: t('Enter fullscreen', {
|
||||
description:
|
||||
'Accessible name of the button entering fullscreen',
|
||||
})
|
||||
}
|
||||
icon={
|
||||
isFullscreen ? <Minimize size="small" /> : <Maximize size="small" />
|
||||
@@ -220,7 +243,10 @@ export const PresenterFloatingBar = ({
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
onClick={onClose}
|
||||
aria-label={t('Close presenter')}
|
||||
aria-label={t('Close presenter', {
|
||||
description:
|
||||
'Accessible name of the button leaving the presentation mode',
|
||||
})}
|
||||
icon={<XMark size="small" />}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
+15
-2
@@ -80,7 +80,13 @@ export const PresenterOverlay = ({
|
||||
const contentSlides = useSlides(snapshotBlocks);
|
||||
const title = useMemo(() => {
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(doc.title ?? '');
|
||||
return [emoji, titleWithoutEmoji.trim() || t('Untitled document')]
|
||||
return [
|
||||
emoji,
|
||||
titleWithoutEmoji.trim() ||
|
||||
t('Untitled document', {
|
||||
description: 'Name displayed for a document without a title',
|
||||
}),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}, [doc.title, t]);
|
||||
@@ -184,11 +190,14 @@ export const PresenterOverlay = ({
|
||||
: getSlideTitle(currentSlide?.blocks ?? []);
|
||||
const message = slideTitle
|
||||
? t('Slide {{current}} of {{total}}: {{title}}', {
|
||||
description: 'Screen reader announcement of the current slide',
|
||||
current: currentIndex + 1,
|
||||
total,
|
||||
title: slideTitle,
|
||||
})
|
||||
: t('Slide {{current}} of {{total}}', {
|
||||
description:
|
||||
'Screen reader announcement or accessible name of a slide',
|
||||
current: currentIndex + 1,
|
||||
total,
|
||||
});
|
||||
@@ -206,7 +215,9 @@ export const PresenterOverlay = ({
|
||||
$css={overlayCss}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t('Presenter mode')}
|
||||
aria-label={t('Presenter mode', {
|
||||
description: 'Accessible name of the presentation mode',
|
||||
})}
|
||||
>
|
||||
<Box ref={frameRef} $css={slideAreaCss}>
|
||||
{mountedIndices.map((i) => (
|
||||
@@ -216,6 +227,8 @@ export const PresenterOverlay = ({
|
||||
isCurrent={i === currentIndex}
|
||||
slide={slides[i]}
|
||||
ariaLabel={t('Slide {{current}} of {{total}}', {
|
||||
description:
|
||||
'Screen reader announcement or accessible name of a slide',
|
||||
current: i + 1,
|
||||
total,
|
||||
})}
|
||||
|
||||
+10
-2
@@ -103,8 +103,16 @@ export const PresenterSlide = ({
|
||||
$css={outerCss}
|
||||
style={outerStyle}
|
||||
role="group"
|
||||
aria-roledescription={t('slide')}
|
||||
aria-label={ariaLabel ?? t('Presenter slide')}
|
||||
aria-roledescription={t('slide', {
|
||||
description:
|
||||
'Role description of a presentation slide for screen readers',
|
||||
})}
|
||||
aria-label={
|
||||
ariaLabel ??
|
||||
t('Presenter slide', {
|
||||
description: 'Accessible name of a presentation slide',
|
||||
})
|
||||
}
|
||||
aria-hidden={!isCurrent}
|
||||
>
|
||||
<Box $css={stageCss} style={stageStyle}>
|
||||
|
||||
+6
-2
@@ -17,8 +17,12 @@ export const useCopyPresenterLink = (docId: Doc['id']) => {
|
||||
(index: number) => {
|
||||
copyToClipboard(
|
||||
`${window.location.origin}/docs/${docId}/?view=present&slide=${index + 1}`,
|
||||
t('Link Copied !'),
|
||||
t('Failed to copy link'),
|
||||
t('Link Copied !', {
|
||||
description: 'Toast confirming a link was copied to the clipboard',
|
||||
}),
|
||||
t('Failed to copy link', {
|
||||
description: 'Toast shown when copying a link failed',
|
||||
}),
|
||||
);
|
||||
},
|
||||
[copyToClipboard, docId, t],
|
||||
|
||||
+3
-1
@@ -50,7 +50,9 @@ export const DocSearchButtonModal = ({ ...props }: ButtonProps) => {
|
||||
size="medium"
|
||||
color="brand"
|
||||
variant="tertiary"
|
||||
aria-label={t('Search docs')}
|
||||
aria-label={t('Search docs', {
|
||||
description: 'Accessible name of the button opening the doc search',
|
||||
})}
|
||||
icon={<SearchSVG aria-hidden="true" width={24} height={24} />}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
+19
-4
@@ -59,14 +59,23 @@ export const DocSearchContent = ({
|
||||
groupName: '',
|
||||
groupKey: 'docs',
|
||||
elements: [],
|
||||
emptyString: t('Loading documents...'),
|
||||
emptyString: t('Loading documents...', {
|
||||
description:
|
||||
'Placeholder and screen reader announcement while search results load',
|
||||
}),
|
||||
endActions: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
if (search || isSearchNotMandatory) {
|
||||
announce(t('Loading documents...'), 'polite');
|
||||
announce(
|
||||
t('Loading documents...', {
|
||||
description:
|
||||
'Placeholder and screen reader announcement while search results load',
|
||||
}),
|
||||
'polite',
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -101,8 +110,14 @@ export const DocSearchContent = ({
|
||||
if (search && !loading) {
|
||||
announce(
|
||||
elements.length === 0
|
||||
? t('No documents found')
|
||||
: t('{{count}} document found', { count: elements.length }),
|
||||
? t('No documents found', {
|
||||
description: 'Message when a search has no result',
|
||||
})
|
||||
: t('{{count}} document found', {
|
||||
description:
|
||||
'Screen reader announcement of the number of search results, plural form',
|
||||
count: elements.length,
|
||||
}),
|
||||
'polite',
|
||||
);
|
||||
}
|
||||
|
||||
+8
-2
@@ -21,7 +21,10 @@ export const DocSearchFilters = () => {
|
||||
<Box
|
||||
role="switch"
|
||||
aria-checked={isAll}
|
||||
aria-label={t('Search in all documents')}
|
||||
aria-label={t('Search in all documents', {
|
||||
description:
|
||||
'Accessible name of the filter searching the whole workspace',
|
||||
})}
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
@@ -52,11 +55,14 @@ export const DocSearchFilters = () => {
|
||||
>
|
||||
<Switch
|
||||
labelSide="right"
|
||||
label={t('All docs')}
|
||||
label={t('All docs', {
|
||||
description: 'Group title listing every doc the user can access',
|
||||
})}
|
||||
checked={isAll}
|
||||
onChange={toggle}
|
||||
aria-label={t(
|
||||
'Toggle to search in all documents or only in current document',
|
||||
{ description: 'Accessible name of the switch scoping the search' },
|
||||
)}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
|
||||
+31
-8
@@ -93,7 +93,10 @@ const DocSearchModalGlobal = ({
|
||||
closeOnClickOutside
|
||||
size={isLargeScreen ? ModalSize.LARGE : ModalSize.FULL}
|
||||
hideCloseButton
|
||||
aria-label={t('Search for a document')}
|
||||
aria-label={t('Search for a document', {
|
||||
description:
|
||||
'Accessible name or placeholder of a document search field',
|
||||
})}
|
||||
aria-labelledby="doc-search-modal-title"
|
||||
aria-describedby="doc-search-modal-description"
|
||||
title={
|
||||
@@ -105,16 +108,25 @@ const DocSearchModalGlobal = ({
|
||||
$align="flex-start"
|
||||
id="doc-search-modal-title"
|
||||
>
|
||||
{t('Search for a document')}
|
||||
{t('Search for a document', {
|
||||
description:
|
||||
'Accessible name or placeholder of a document search field',
|
||||
})}
|
||||
</Text>
|
||||
<Text id="doc-search-modal-description" className="sr-only">
|
||||
{t(
|
||||
'Search documents by name, navigate using arrows, and select a result with Enter.',
|
||||
{
|
||||
description: 'Screen reader instructions of the search dialog',
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 8px; right: 8px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the search modal')}
|
||||
aria-label={t('Close the search modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the search dialog',
|
||||
})}
|
||||
onClick={modalProps.onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -129,8 +141,12 @@ const DocSearchModalGlobal = ({
|
||||
$padding={{ bottom: 'base' }}
|
||||
>
|
||||
<QuickSearch
|
||||
label={t('Type the name of a document')}
|
||||
placeholder={t('Type the name of a document')}
|
||||
label={t('Type the name of a document', {
|
||||
description: 'Label and placeholder of the search input',
|
||||
})}
|
||||
placeholder={t('Type the name of a document', {
|
||||
description: 'Label and placeholder of the search input',
|
||||
})}
|
||||
loading={loading}
|
||||
onFilter={handleInputSearch}
|
||||
beforeList={
|
||||
@@ -237,15 +253,22 @@ const DocSearchStateText = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (hasResults && filter === 'all') {
|
||||
return t('Select a document');
|
||||
return t('Select a document', {
|
||||
description: 'Screen reader hint to pick one of the search results',
|
||||
});
|
||||
}
|
||||
|
||||
if (hasResults && filter === 'current') {
|
||||
return t('Select a sub-document');
|
||||
return t('Select a sub-document', {
|
||||
description:
|
||||
'Screen reader hint to pick one of the sub-doc search results',
|
||||
});
|
||||
}
|
||||
|
||||
if (isSearching && !hasResults) {
|
||||
return t('No documents found');
|
||||
return t('No documents found', {
|
||||
description: 'Message when a search has no result',
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
+16
-5
@@ -39,7 +39,10 @@ export const AlertModalRequestAccess = ({
|
||||
onClose={onClose}
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
aria-label={t('Request access modal')}
|
||||
aria-label={t('Request access modal', {
|
||||
description:
|
||||
'Accessible name of the dialog to request access to a document',
|
||||
})}
|
||||
description={
|
||||
<>
|
||||
<Text $display="inline">
|
||||
@@ -63,23 +66,31 @@ export const AlertModalRequestAccess = ({
|
||||
$margin={{ right: 'xxs' }}
|
||||
variant="symbols-outlined"
|
||||
/>
|
||||
{t('You have already requested access to this document.')}
|
||||
{t('You have already requested access to this document.', {
|
||||
description: 'Message in the request access dialog',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
confirmLabel={t('Request access')}
|
||||
confirmLabel={t('Request access', {
|
||||
description: 'Button to ask the owners for access to a document',
|
||||
})}
|
||||
onConfirm={onConfirm}
|
||||
rightActions={
|
||||
<Box $direction="row" $gap="small">
|
||||
<Button
|
||||
aria-label={t('Cancel')}
|
||||
aria-label={t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
onClick={onClose}
|
||||
autoFocus
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<ButtonAccessRequest docId={docId} onClick={onConfirm} />
|
||||
</Box>
|
||||
|
||||
+22
-7
@@ -47,7 +47,9 @@ export const ConfirmationLeaveModal = ({
|
||||
onClose={onClose}
|
||||
// TODO: add a fix on the Modal component on Cunningham side
|
||||
// If aria-label is not set the modal add by default [object Object]
|
||||
aria-label={t('Confirmation to leave the document')}
|
||||
aria-label={t('Confirmation to leave the document', {
|
||||
description: 'Accessible name of the leave document dialog',
|
||||
})}
|
||||
aria-labelledby="modal-leave-doc-title"
|
||||
aria-describedby="modal-leave-doc-desc"
|
||||
rightActions={
|
||||
@@ -78,11 +80,16 @@ export const ConfirmationLeaveModal = ({
|
||||
$margin="0"
|
||||
$align="flex-start"
|
||||
>
|
||||
{t('Leave a doc')}
|
||||
{t('Leave a doc', {
|
||||
description: 'Title of the dialog to leave a shared document',
|
||||
})}
|
||||
</Text>
|
||||
<Box $position="absolute" $css="top: 8px; right: 8px;">
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the leave modal')}
|
||||
aria-label={t('Close the leave modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the leave dialog',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -173,23 +180,31 @@ const ButtonsLeaveDoc = ({
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('Cancel leaving the document')}
|
||||
aria-label={t('Cancel leaving the document', {
|
||||
description: 'Accessible name of the button cancelling leaving',
|
||||
})}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onClick={onClose}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Confirm leaving the document')}
|
||||
aria-label={t('Confirm leaving the document', {
|
||||
description: 'Accessible name of the button confirming leaving',
|
||||
})}
|
||||
color="error"
|
||||
fullWidth
|
||||
onClick={leave}
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('Leave')}
|
||||
{t('Leave', {
|
||||
description: 'Dropdown menu item to leave the document',
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
+18
-5
@@ -23,9 +23,16 @@ export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
||||
const { mutate: updateDocLink } = useUpdateDocLink({
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
onSuccess: () => {
|
||||
toast(t('The document visibility restored.'), VariantType.SUCCESS, {
|
||||
duration: 2000,
|
||||
});
|
||||
toast(
|
||||
t('The document visibility restored.', {
|
||||
description:
|
||||
"Toast shown after resetting link sharing to the parent's settings",
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 2000,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,7 +53,10 @@ export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
||||
>
|
||||
<Desync />
|
||||
<Text $size="xs" $withThemeInherited $weight="400">
|
||||
{t('The link sharing rules differ from the parent document')}
|
||||
{t('The link sharing rules differ from the parent document', {
|
||||
description:
|
||||
'Banner shown on a sub-doc whose link settings differ from its parent',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
{doc.abilities.accesses_manage && (
|
||||
@@ -63,7 +73,10 @@ export const DocDesynchronized = ({ doc }: DocDesynchronizedProps) => {
|
||||
variant="tertiary"
|
||||
icon={<Undo />}
|
||||
>
|
||||
{t('Restore')}
|
||||
{t('Restore', {
|
||||
description:
|
||||
'Button to restore a deleted document or an older version',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
+4
-1
@@ -42,7 +42,10 @@ export const DocInheritedShareContent = ({
|
||||
>
|
||||
<Box $direction="row" $align="center" $gap={spacingsTokens['4xs']}>
|
||||
<Text $weight="bold" $size="sm">
|
||||
{t('People with access via the parent document')}
|
||||
{t('People with access via the parent document', {
|
||||
description:
|
||||
'Heading of the list of members inherited from the parent doc',
|
||||
})}
|
||||
</Text>
|
||||
<Box>
|
||||
<StyledLink href={`/docs/${rawAccesses[0].document.id}`}>
|
||||
|
||||
+23
-7
@@ -40,7 +40,10 @@ export const DocRoleDropdown = ({
|
||||
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
|
||||
onError: (error) => {
|
||||
toast(
|
||||
error?.data?.role?.[0] ?? t('Error during delete invitation'),
|
||||
error?.data?.role?.[0] ??
|
||||
t('Error during delete invitation', {
|
||||
description: 'Toast shown when deleting an invitation failed',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
@@ -51,9 +54,15 @@ export const DocRoleDropdown = ({
|
||||
|
||||
const { mutate: removeDocAccess } = useDeleteDocAccess({
|
||||
onError: () => {
|
||||
toast(t('Error while deleting invitation'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('Error while deleting invitation', {
|
||||
description: 'Toast shown when deleting an invitation failed',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,7 +92,9 @@ export const DocRoleDropdown = ({
|
||||
|
||||
if (rolesAllowed.length < allRoles.length) {
|
||||
let result = message ? `${message}\n\n` : '';
|
||||
result += t('This user has access inherited from a parent page.');
|
||||
result += t('This user has access inherited from a parent page.', {
|
||||
description: "Tooltip explaining why a member's role cannot be changed",
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -109,7 +120,9 @@ export const DocRoleDropdown = ({
|
||||
return (
|
||||
<Text
|
||||
$variation="tertiary"
|
||||
aria-label={t('Document role text')}
|
||||
aria-label={t('Document role text', {
|
||||
description: 'Accessible name of the role of a member',
|
||||
})}
|
||||
$weight={500}
|
||||
$size="s"
|
||||
>
|
||||
@@ -122,6 +135,7 @@ export const DocRoleDropdown = ({
|
||||
<DropdownMenu
|
||||
topMessage={topMessage}
|
||||
label={t('{{action}}, current role: {{role}}', {
|
||||
description: 'Accessible name of the role dropdown',
|
||||
action: ariaLabel,
|
||||
role: transRole(currentRole),
|
||||
})}
|
||||
@@ -134,7 +148,9 @@ export const DocRoleDropdown = ({
|
||||
options={[
|
||||
...roles,
|
||||
{
|
||||
label: t('Remove access'),
|
||||
label: t('Remove access', {
|
||||
description: 'Menu action removing a member from a document',
|
||||
}),
|
||||
disabled: !access?.abilities.destroy,
|
||||
callback: onRemove,
|
||||
},
|
||||
|
||||
+40
-11
@@ -59,9 +59,15 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
|
||||
const { mutate: removeDocAccess } = useDeleteDocAccessRequest({
|
||||
onError: () => {
|
||||
toast(t('Error while removing the request.'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('Error while removing the request.', {
|
||||
description: 'Toast shown when deleting an access request failed',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,6 +101,7 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
canUpdate={doc.abilities.accesses_manage}
|
||||
rolesAllowed={accessRequest.abilities.set_role_to}
|
||||
ariaLabel={t('Change role for {{name}}', {
|
||||
description: 'Accessible name of the role dropdown of a member',
|
||||
name: accessRequest.user.full_name || accessRequest.user.email,
|
||||
})}
|
||||
/>
|
||||
@@ -110,7 +117,9 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
}
|
||||
size={isSmallMobile ? 'nano' : 'small'}
|
||||
>
|
||||
{t('Approve')}
|
||||
{t('Approve', {
|
||||
description: 'Button to accept an access request',
|
||||
})}
|
||||
</Button>
|
||||
|
||||
{doc.abilities.accesses_manage && (
|
||||
@@ -121,7 +130,10 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
|
||||
docId: doc.id,
|
||||
})
|
||||
}
|
||||
aria-label={t('Close the access request modal')}
|
||||
aria-label={t('Close the access request modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the access request dialog',
|
||||
})}
|
||||
>
|
||||
<Icon iconName="close" $size="16px" />
|
||||
</BoxButton>
|
||||
@@ -148,7 +160,9 @@ export const QuickSearchGroupAccessRequest = ({
|
||||
accessRequestQuery.data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
return {
|
||||
groupName: t('Access Requests'),
|
||||
groupName: t('Access Requests', {
|
||||
description: 'Heading of the list of pending access requests',
|
||||
}),
|
||||
elements: accessRequests,
|
||||
endActions: accessRequestQuery.hasNextPage
|
||||
? [
|
||||
@@ -168,7 +182,9 @@ export const QuickSearchGroupAccessRequest = ({
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
aria-label={t('List request access card')}
|
||||
aria-label={t('List request access card', {
|
||||
description: 'Accessible name of the list of access requests',
|
||||
})}
|
||||
className="--docs--share-access-request"
|
||||
$padding={{ horizontal: 'base' }}
|
||||
>
|
||||
@@ -212,9 +228,15 @@ export const ButtonAccessRequest = ({
|
||||
const { toast } = useToast();
|
||||
const { mutate: createRequest } = useCreateDocAccessRequest({
|
||||
onSuccess: () => {
|
||||
toast(t('Access request sent successfully.'), VariantType.SUCCESS, {
|
||||
duration: 3000,
|
||||
});
|
||||
toast(
|
||||
t('Access request sent successfully.', {
|
||||
description: 'Toast shown after requesting access',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 3000,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -227,6 +249,10 @@ export const ButtonAccessRequest = ({
|
||||
<Text $maxWidth="320px" $textAlign="center" $size="sm">
|
||||
{t(
|
||||
'As this is a sub-document, please request access to the parent document to enable these features.',
|
||||
{
|
||||
description:
|
||||
'Message in the share dialog when the user lacks rights on a sub-doc',
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
@@ -249,7 +275,10 @@ export const ButtonAccessRequest = ({
|
||||
disabled={hasRequested}
|
||||
{...buttonProps}
|
||||
>
|
||||
{buttonProps.children || t('Request access')}
|
||||
{buttonProps.children ||
|
||||
t('Request access', {
|
||||
description: 'Button to ask the owners for access to a document',
|
||||
})}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
+18
-4
@@ -48,15 +48,19 @@ export const DocShareAddMemberList = ({
|
||||
let messageError =
|
||||
dataError['data']?.type === OptionType.INVITATION
|
||||
? t(`Failed to create the invitation for {{email}}.`, {
|
||||
description: 'Toast shown when inviting an email failed',
|
||||
email: dataError['data']?.value,
|
||||
})
|
||||
: t(`Failed to add the member in the document.`);
|
||||
: t(`Failed to add the member in the document.`, {
|
||||
description: 'Toast shown when adding a member failed',
|
||||
});
|
||||
|
||||
if (
|
||||
dataError.cause?.[0] ===
|
||||
'Document invitation with this Email address and Document already exists.'
|
||||
) {
|
||||
messageError = t('"{{email}}" is already invited to the document.', {
|
||||
description: 'Error shown when inviting an email already invited',
|
||||
email: dataError['data']?.value,
|
||||
});
|
||||
}
|
||||
@@ -66,6 +70,7 @@ export const DocShareAddMemberList = ({
|
||||
'This email is already associated to a registered user.'
|
||||
) {
|
||||
messageError = t('"{{email}}" is already member of the document.', {
|
||||
description: 'Error shown when inviting an existing member',
|
||||
email: dataError['data']?.value,
|
||||
});
|
||||
}
|
||||
@@ -108,9 +113,13 @@ export const DocShareAddMemberList = ({
|
||||
const inviteLabel =
|
||||
selectedUsers.length === 1
|
||||
? t('Invite {{name}}', {
|
||||
description: 'Button to invite one person',
|
||||
name: selectedUsers[0].full_name || selectedUsers[0].email,
|
||||
})
|
||||
: t('Invite {{count}} members', { count: selectedUsers.length });
|
||||
: t('Invite {{count}} members', {
|
||||
description: 'Button to invite several people, plural form',
|
||||
count: selectedUsers.length,
|
||||
});
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -151,7 +160,10 @@ export const DocShareAddMemberList = ({
|
||||
canUpdate={canShare}
|
||||
currentRole={invitationRole}
|
||||
onSelectRole={setInvitationRole}
|
||||
ariaLabel={t('Invite new members')}
|
||||
ariaLabel={t('Invite new members', {
|
||||
description:
|
||||
'Accessible name of the role selector for new invitees',
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => void onInvite()}
|
||||
@@ -160,7 +172,9 @@ export const DocShareAddMemberList = ({
|
||||
data-testid="doc-share-invite-button"
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
{t('Invite')}
|
||||
{t('Invite', {
|
||||
description: 'Button to invite people to a document',
|
||||
})}
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
|
||||
<BoxButton
|
||||
onClick={() => onRemoveUser?.(user)}
|
||||
aria-label={t('Remove {{name}} from the invite list', {
|
||||
description:
|
||||
'Accessible name of the button removing a person from the pending invitees',
|
||||
name: user.full_name || user.email,
|
||||
})}
|
||||
$withThemeInherited
|
||||
|
||||
+18
-2
@@ -71,10 +71,26 @@ export const DocShareButton = ({
|
||||
<SharedSVG width={24} height={24} aria-hidden="true" />
|
||||
) : undefined
|
||||
}
|
||||
aria-label={hasAccesses ? t('Shared') : t('Share')}
|
||||
aria-label={
|
||||
hasAccesses
|
||||
? t('Shared', {
|
||||
description:
|
||||
'Share button label when the document is already shared',
|
||||
})
|
||||
: t('Share', {
|
||||
description: 'Dropdown menu item to share the document',
|
||||
})
|
||||
}
|
||||
data-test="share-button"
|
||||
>
|
||||
{hasAccesses ? t('Shared') : t('Share')}
|
||||
{hasAccesses
|
||||
? t('Shared', {
|
||||
description:
|
||||
'Share button label when the document is already shared',
|
||||
})
|
||||
: t('Share', {
|
||||
description: 'Dropdown menu item to share the document',
|
||||
})}
|
||||
</Button>
|
||||
</CardFloatingBar>
|
||||
{modalShare.isOpen && (
|
||||
|
||||
+17
-4
@@ -49,7 +49,11 @@ export const DocShareInvitationItem = ({
|
||||
const { mutate: updateDocInvitation } = useUpdateDocInvitation({
|
||||
onError: (error) => {
|
||||
toast(
|
||||
error?.data?.role?.[0] ?? t('Error during update invitation'),
|
||||
error?.data?.role?.[0] ??
|
||||
t('Error during update invitation', {
|
||||
description:
|
||||
'Toast shown when changing the role of an invitation failed',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
@@ -85,6 +89,8 @@ export const DocShareInvitationItem = ({
|
||||
doc={doc}
|
||||
access={invitation}
|
||||
ariaLabel={t('Change role for {{email}}', {
|
||||
description:
|
||||
'Accessible name of the role dropdown of an invitation',
|
||||
email: invitation.email,
|
||||
})}
|
||||
/>
|
||||
@@ -123,7 +129,10 @@ export const DocShareModalInviteUserRow = ({
|
||||
$gap="var(--c--globals--spacings--xxxs)"
|
||||
>
|
||||
<Text $withThemeInherited $size="sm">
|
||||
{t('Add')}
|
||||
{t('Add', {
|
||||
description:
|
||||
'Label of the action adding people to the document',
|
||||
})}
|
||||
</Text>
|
||||
<Icon $withThemeInherited iconName="add" />
|
||||
</BoxButton>
|
||||
@@ -149,7 +158,9 @@ export const QuickSearchGroupInvitation = ({
|
||||
const invitations = data?.pages.flatMap((page) => page.results) || [];
|
||||
|
||||
return {
|
||||
groupName: t('Pending invitations'),
|
||||
groupName: t('Pending invitations', {
|
||||
description: 'Heading of the list of invitations not yet accepted',
|
||||
}),
|
||||
elements: invitations,
|
||||
endActions: hasNextPage
|
||||
? [
|
||||
@@ -169,7 +180,9 @@ export const QuickSearchGroupInvitation = ({
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
aria-label={t('List invitation card')}
|
||||
aria-label={t('List invitation card', {
|
||||
description: 'Accessible name of the list of invitations',
|
||||
})}
|
||||
$padding={{ horizontal: 'base' }}
|
||||
>
|
||||
<QuickSearchGroup
|
||||
|
||||
+22
-5
@@ -33,14 +33,24 @@ export const DocShareMemberItem = ({
|
||||
const message = isLastOwner
|
||||
? t(
|
||||
'You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.',
|
||||
{
|
||||
description:
|
||||
'Tooltip shown when the last owner cannot change their role',
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const { mutate: updateDocAccess } = useUpdateDocAccess({
|
||||
onError: () => {
|
||||
toast(t('Error while updating the member role.'), VariantType.ERROR, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('Error while updating the member role.', {
|
||||
description: 'Toast shown when changing a member role failed',
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,6 +88,7 @@ export const DocShareMemberItem = ({
|
||||
access={access}
|
||||
doc={doc}
|
||||
ariaLabel={t('Change role for {{name}}', {
|
||||
description: 'Accessible name of the role dropdown of a member',
|
||||
name: access.user.full_name || access.user.email,
|
||||
})}
|
||||
/>
|
||||
@@ -108,8 +119,12 @@ export const QuickSearchGroupMember = ({
|
||||
return {
|
||||
groupName:
|
||||
count === 1
|
||||
? t('Document owner')
|
||||
? t('Document owner', {
|
||||
description: 'Label of the single owner of a document',
|
||||
})
|
||||
: t('Share with {{count}} users', {
|
||||
description:
|
||||
'Number of people a document is shared with, plural form',
|
||||
count: count,
|
||||
}),
|
||||
elements: members,
|
||||
@@ -119,7 +134,9 @@ export const QuickSearchGroupMember = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
aria-label={t('List members card')}
|
||||
aria-label={t('List members card', {
|
||||
description: 'Accessible name of the list of members',
|
||||
})}
|
||||
$padding={{ horizontal: 'base', bottom: '3xs' }}
|
||||
>
|
||||
<QuickSearchGroup
|
||||
|
||||
@@ -98,6 +98,8 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
t(
|
||||
'{{name}} added to invite list. Add more members or press Tab to select role and invite.',
|
||||
{
|
||||
description:
|
||||
'Screen reader announcement after adding a person to the invitees',
|
||||
name: userName,
|
||||
},
|
||||
),
|
||||
@@ -133,6 +135,8 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
const userName = row.full_name || row.email;
|
||||
announce(
|
||||
t('{{name}} removed from invite list', {
|
||||
description:
|
||||
'Screen reader announcement after removing a person from the invitees',
|
||||
name: userName,
|
||||
}),
|
||||
'polite',
|
||||
@@ -180,7 +184,9 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
data-testid="doc-share-modal"
|
||||
aria-label={t('Share the document')}
|
||||
aria-label={t('Share the document', {
|
||||
description: 'Title and accessible name of the share dialog',
|
||||
})}
|
||||
size={isLargeScreen ? ModalSize.LARGE : ModalSize.FULL}
|
||||
aria-modal="true"
|
||||
onClose={onClose}
|
||||
@@ -194,10 +200,15 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
$weight="600"
|
||||
$margin="0"
|
||||
>
|
||||
{t('Share the document')}
|
||||
{t('Share the document', {
|
||||
description: 'Title and accessible name of the share dialog',
|
||||
})}
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the share modal')}
|
||||
aria-label={t('Close the share modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the share dialog',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -258,6 +269,10 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
>
|
||||
{t(
|
||||
'You can view this document but need additional access to see its members or modify settings.',
|
||||
{
|
||||
description:
|
||||
'Message in the share dialog for users with read-only access',
|
||||
},
|
||||
)}
|
||||
</Text>
|
||||
<ButtonAccessRequest
|
||||
@@ -269,7 +284,10 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
)}
|
||||
{canViewAccesses && (
|
||||
<QuickSearch
|
||||
label={t('Search results')}
|
||||
label={t('Search results', {
|
||||
description:
|
||||
'Accessible name or heading of the list of search results',
|
||||
})}
|
||||
onFilter={(str) => {
|
||||
setInputValue(str);
|
||||
onFilter(str);
|
||||
@@ -277,7 +295,10 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
|
||||
inputValue={inputValue}
|
||||
showInput={canShare}
|
||||
loading={searchUsersQuery.isLoading}
|
||||
placeholder={t('Type a name or email')}
|
||||
placeholder={t('Type a name or email', {
|
||||
description:
|
||||
'Placeholder of the field searching users to invite',
|
||||
})}
|
||||
>
|
||||
{showInheritedShareContent && (
|
||||
<DocInheritedShareContent
|
||||
@@ -335,17 +356,24 @@ const QuickSearchInviteInputSection = ({
|
||||
const hint = useMemo(() => {
|
||||
if (userQuery.length < minLength) {
|
||||
return t('Type at least {{minLength}} characters to display user names', {
|
||||
description: 'Hint shown before the user search starts',
|
||||
minLength,
|
||||
});
|
||||
}
|
||||
if (isValidEmail(userQuery)) {
|
||||
return t('Choose the email');
|
||||
return t('Choose the email', {
|
||||
description: 'Hint to pick the typed email address as invitee',
|
||||
});
|
||||
}
|
||||
if (!searchUsersRawData?.length) {
|
||||
return t('No results. Type a full email address to invite someone.');
|
||||
return t('No results. Type a full email address to invite someone.', {
|
||||
description: 'Message when the user search has no result',
|
||||
});
|
||||
}
|
||||
|
||||
return t('Choose a user');
|
||||
return t('Choose a user', {
|
||||
description: 'Hint to pick a user in the search results',
|
||||
});
|
||||
}, [minLength, searchUsersRawData?.length, t, userQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -385,7 +413,9 @@ const QuickSearchInviteInputSection = ({
|
||||
|
||||
return (
|
||||
<Box
|
||||
aria-label={t('List search user result card')}
|
||||
aria-label={t('List search user result card', {
|
||||
description: 'Accessible name of the list of user search results',
|
||||
})}
|
||||
$padding={{ horizontal: 'base', bottom: '3xs', top: 'base' }}
|
||||
>
|
||||
<QuickSearchGroup
|
||||
|
||||
+6
-2
@@ -41,9 +41,13 @@ export const DocShareModalFooter = ({
|
||||
variant="secondary"
|
||||
icon={<Icon iconName="add_link" $withThemeInherited />}
|
||||
>
|
||||
{t('Copy link')}
|
||||
{t('Copy link', {
|
||||
description: 'Dropdown menu item to copy the document link',
|
||||
})}
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
{t('OK', { description: 'Button closing a dialog' })}
|
||||
</Button>
|
||||
<Button onClick={onClose}>{t('OK')}</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,9 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
listInvalidQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
onSuccess: () => {
|
||||
toast(
|
||||
t('The document visibility has been updated.'),
|
||||
t('The document visibility has been updated.', {
|
||||
description: 'Toast shown after changing the link sharing settings',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 2000,
|
||||
@@ -131,12 +133,16 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
return (
|
||||
<Box
|
||||
$padding={{ horizontal: 'base' }}
|
||||
aria-label={t('Doc visibility card')}
|
||||
aria-label={t('Doc visibility card', {
|
||||
description: 'Accessible name of the link sharing settings block',
|
||||
})}
|
||||
$gap={spacingsTokens['base']}
|
||||
className="--docs--doc-visibility"
|
||||
>
|
||||
<Text as="h2" $weight="700" $size="sm" $margin="none">
|
||||
{t('Link settings')}
|
||||
{t('Link settings', {
|
||||
description: 'Heading of the link sharing settings',
|
||||
})}
|
||||
</Text>
|
||||
{isDesynchronized && <DocDesynchronized doc={doc} />}
|
||||
<Box
|
||||
@@ -155,7 +161,10 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
>
|
||||
<DropdownMenu
|
||||
testId="doc-visibility"
|
||||
label={t('Document visibility')}
|
||||
label={t('Document visibility', {
|
||||
description:
|
||||
'Label of the selector of who can access with the link',
|
||||
})}
|
||||
arrowCss={css`
|
||||
color: var(
|
||||
--c--contextuals--content--semantic--brand--tertiary
|
||||
@@ -172,6 +181,10 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
haveDisabledOptions
|
||||
? t(
|
||||
'You cannot restrict access to a subpage relative to its parent page.',
|
||||
{
|
||||
description:
|
||||
'Tooltip on a disabled link sharing option of a sub-doc',
|
||||
},
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
@@ -222,10 +235,17 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
haveDisabledLinkRoleOptions
|
||||
? t(
|
||||
'You cannot restrict access to a subpage relative to its parent page.',
|
||||
{
|
||||
description:
|
||||
'Tooltip on a disabled link sharing option of a sub-doc',
|
||||
},
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
label={t('Document access mode')}
|
||||
label={t('Document access mode', {
|
||||
description:
|
||||
'Label of the selector of what link users can do: read or edit',
|
||||
})}
|
||||
>
|
||||
<Text
|
||||
$weight="initial"
|
||||
|
||||
+29
-9
@@ -6,14 +6,24 @@ export const useTranslatedShareSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const linkReachTranslations = {
|
||||
[LinkReach.RESTRICTED]: t('Private'),
|
||||
[LinkReach.AUTHENTICATED]: t('Connected'),
|
||||
[LinkReach.PUBLIC]: t('Public'),
|
||||
[LinkReach.RESTRICTED]: t('Private', {
|
||||
description: 'Link sharing level: only invited people',
|
||||
}),
|
||||
[LinkReach.AUTHENTICATED]: t('Connected', {
|
||||
description: 'Link sharing level: any logged-in user',
|
||||
}),
|
||||
[LinkReach.PUBLIC]: t('Public', {
|
||||
description: 'Document visibility: anyone with the link can access',
|
||||
}),
|
||||
};
|
||||
|
||||
const linkModeTranslations = {
|
||||
[LinkRole.READER]: t('Reading'),
|
||||
[LinkRole.EDITOR]: t('Editing'),
|
||||
[LinkRole.READER]: t('Reading', {
|
||||
description: 'Link sharing role: read only',
|
||||
}),
|
||||
[LinkRole.EDITOR]: t('Editing', {
|
||||
description: 'Link sharing role: can edit',
|
||||
}),
|
||||
};
|
||||
|
||||
const linkReachChoices = {
|
||||
@@ -21,8 +31,12 @@ export const useTranslatedShareSettings = () => {
|
||||
label: linkReachTranslations[LinkReach.RESTRICTED],
|
||||
icon: 'lock',
|
||||
value: LinkReach.RESTRICTED,
|
||||
descriptionReadOnly: t('Only invited people can access'),
|
||||
descriptionEdit: t('Only invited people can access'),
|
||||
descriptionReadOnly: t('Only invited people can access', {
|
||||
description: 'Description of the private link sharing level',
|
||||
}),
|
||||
descriptionEdit: t('Only invited people can access', {
|
||||
description: 'Description of the private link sharing level',
|
||||
}),
|
||||
},
|
||||
[LinkReach.AUTHENTICATED]: {
|
||||
label: linkReachTranslations[LinkReach.AUTHENTICATED],
|
||||
@@ -30,17 +44,23 @@ export const useTranslatedShareSettings = () => {
|
||||
value: LinkReach.AUTHENTICATED,
|
||||
descriptionReadOnly: t(
|
||||
'Anyone with the link can view the document if they are logged in',
|
||||
{ description: 'Description of the connected level, read only' },
|
||||
),
|
||||
descriptionEdit: t(
|
||||
'Anyone with the link can edit the document if they are logged in',
|
||||
{ description: 'Description of the connected level, can edit' },
|
||||
),
|
||||
},
|
||||
[LinkReach.PUBLIC]: {
|
||||
label: linkReachTranslations[LinkReach.PUBLIC],
|
||||
icon: 'public',
|
||||
value: LinkReach.PUBLIC,
|
||||
descriptionReadOnly: t('Anyone with the link can see the document'),
|
||||
descriptionEdit: t('Anyone with the link can edit the document'),
|
||||
descriptionReadOnly: t('Anyone with the link can see the document', {
|
||||
description: 'Description of the public level, read only',
|
||||
}),
|
||||
descriptionEdit: t('Anyone with the link can edit the document', {
|
||||
description: 'Description of the public level, can edit',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+16
-4
@@ -102,10 +102,16 @@ export const TableContentSideBar = ({ onClose }: TableContentSideBarProps) => {
|
||||
$size="16px"
|
||||
$margin="0"
|
||||
>
|
||||
{t('Table of Contents')}
|
||||
{t('Table of Contents', {
|
||||
description:
|
||||
'Title of the side panel listing the document headings',
|
||||
})}
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the table of contents sidebar')}
|
||||
aria-label={t('Close the table of contents sidebar', {
|
||||
description:
|
||||
'Accessible name of the button closing the table of contents',
|
||||
})}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
@@ -161,8 +167,14 @@ export const TableContentSideBarButton = () => {
|
||||
|
||||
const isActive = isPanelOpen && activePanel === 'tableContent';
|
||||
const ariaLabel = isActive
|
||||
? t('Hide the table of contents sidebar')
|
||||
: t('Show the table of contents sidebar');
|
||||
? t('Hide the table of contents sidebar', {
|
||||
description:
|
||||
'Accessible name of the toggle hiding the table of contents',
|
||||
})
|
||||
: t('Show the table of contents sidebar', {
|
||||
description:
|
||||
'Accessible name of the toggle showing the table of contents',
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -91,7 +91,9 @@ const DocSubPageLoadMore = (props: TreeViewNodeProps<Doc>) => {
|
||||
$justify="center"
|
||||
$padding={{ vertical: 'xs' }}
|
||||
role="status"
|
||||
aria-label={t('Loading more documents')}
|
||||
aria-label={t('Loading more documents', {
|
||||
description: 'Screen reader status while more tree items load',
|
||||
})}
|
||||
>
|
||||
<Spinner size="sm" aria-hidden="true" />
|
||||
</Box>
|
||||
@@ -189,8 +191,15 @@ const DocSubPageItemContent = (props: TreeViewNodeProps<Doc>) => {
|
||||
tabIndex={-1}
|
||||
aria-label={
|
||||
isDeleted
|
||||
? t('{{title}} (deleted)', { title: displayTitle })
|
||||
: t('Open document {{title}}', { title: displayTitle })
|
||||
? t('{{title}} (deleted)', {
|
||||
description: 'Accessible name of a deleted doc in the tree',
|
||||
title: displayTitle,
|
||||
})
|
||||
: t('Open document {{title}}', {
|
||||
description:
|
||||
'Accessible name of a document link, an action to open it',
|
||||
title: displayTitle,
|
||||
})
|
||||
}
|
||||
aria-current={isCurrentPage ? 'page' : undefined}
|
||||
data-testid={`doc-sub-page-item-${doc.id}`}
|
||||
|
||||
@@ -173,7 +173,9 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
data-testid="doc-tree"
|
||||
$height="100%"
|
||||
role="tree"
|
||||
aria-label={t('Document tree')}
|
||||
aria-label={t('Document tree', {
|
||||
description: 'Accessible name of the tree of documents',
|
||||
})}
|
||||
aria-describedby="doc-tree-keyboard-instructions"
|
||||
$css={css`
|
||||
/**
|
||||
@@ -208,6 +210,10 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||
<Box id="doc-tree-keyboard-instructions" className="sr-only">
|
||||
{t(
|
||||
'Use the up and down arrow keys to move between documents, and Enter to open one. Press F2 to reach the actions of a document and to move between them, use Escape to go back to the document list.',
|
||||
{
|
||||
description:
|
||||
'Screen reader keyboard instructions of the document tree',
|
||||
},
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
|
||||
+5
-1
@@ -50,6 +50,8 @@ export const DocTreeItemActions = ({
|
||||
className={`${CLASS_TREE_ITEM_ACTIONS} --docs--doc-tree-item-actions actions`}
|
||||
role="toolbar"
|
||||
aria-label={t('Actions for {{title}}', {
|
||||
description:
|
||||
'Accessible name of the actions toolbar of a doc in the tree',
|
||||
title: doc.title || untitledDocument,
|
||||
})}
|
||||
tabIndex={-1}
|
||||
@@ -80,7 +82,9 @@ export const DocTreeItemActions = ({
|
||||
|
||||
createChildDoc({ parentId: doc.id });
|
||||
}}
|
||||
aria-label={t('Add a sub page')}
|
||||
aria-label={t('Add a sub page', {
|
||||
description: 'Accessible name of the button creating a sub-doc',
|
||||
})}
|
||||
data-testid="doc-tree-item-actions-add-child"
|
||||
tabIndex={-1}
|
||||
color="brand"
|
||||
|
||||
@@ -129,7 +129,10 @@ export const DocTreeRoot = ({
|
||||
ref={rootItemRef}
|
||||
data-testid="doc-tree-root-item"
|
||||
role="treeitem"
|
||||
aria-label={t('Root document {{title}}', { title })}
|
||||
aria-label={t('Root document {{title}}', {
|
||||
description: 'Accessible name of the root doc in the tree',
|
||||
title,
|
||||
})}
|
||||
aria-selected={isSelected}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -177,7 +180,7 @@ export const DocTreeRoot = ({
|
||||
selectRoot();
|
||||
void router.push(`/docs/${root.id}`);
|
||||
}}
|
||||
aria-label={`${t('Open root document')}: ${title}`}
|
||||
aria-label={`${t('Open root document', { description: 'Accessible name of the link opening the root doc' })}: ${title}`}
|
||||
tabIndex={-1} // the item itself is the tab stop
|
||||
>
|
||||
<Box $direction="row" $align="center" $width="100%">
|
||||
|
||||
+3
-1
@@ -15,7 +15,9 @@ export const DocVersionHeader = () => {
|
||||
$width="100%"
|
||||
$padding={{ vertical: 'base' }}
|
||||
$gap={spacingsTokens['base']}
|
||||
aria-label={t('It is the document title')}
|
||||
aria-label={t('It is the document title', {
|
||||
description: 'Accessible name of the title in the version history',
|
||||
})}
|
||||
className="--docs--doc-version-header"
|
||||
>
|
||||
<DocTitleText />
|
||||
|
||||
+23
-7
@@ -45,7 +45,12 @@ export const ModalConfirmationVersion = ({
|
||||
*/
|
||||
const { mutate: restoreVersion, isPending } = useRestoreDocVersion({
|
||||
onSuccess: () => {
|
||||
toast(t('Version restored successfully'), VariantType.SUCCESS);
|
||||
toast(
|
||||
t('Version restored successfully', {
|
||||
description: 'Toast shown after restoring an older version',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
);
|
||||
onSuccess();
|
||||
|
||||
threadStore?.refreshThreads();
|
||||
@@ -57,26 +62,34 @@ export const ModalConfirmationVersion = ({
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
onClose={() => onClose()}
|
||||
aria-label={t('Warning')}
|
||||
aria-label={t('Warning', { description: 'Title of a warning dialog' })}
|
||||
rightActions={
|
||||
<>
|
||||
<Button
|
||||
aria-label={`${t('Cancel')} - ${t('Warning')}`}
|
||||
aria-label={`${t('Cancel', { description: 'Button to dismiss a dialog without confirming' })} - ${t('Warning', { description: 'Title of a warning dialog' })}`}
|
||||
variant="secondary"
|
||||
fullWidth
|
||||
autoFocus
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t('Cancel')}
|
||||
{t('Cancel', {
|
||||
description: 'Button to dismiss a dialog without confirming',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t('Restore')}
|
||||
aria-label={t('Restore', {
|
||||
description:
|
||||
'Button to restore a deleted document or an older version',
|
||||
})}
|
||||
color="error"
|
||||
fullWidth
|
||||
disabled={isPending}
|
||||
onClick={() => restoreVersion({ docId, versionId })}
|
||||
>
|
||||
{t('Restore')}
|
||||
{t('Restore', {
|
||||
description:
|
||||
'Button to restore a deleted document or an older version',
|
||||
})}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -89,7 +102,9 @@ export const ModalConfirmationVersion = ({
|
||||
$size="h6"
|
||||
$align="flex-start"
|
||||
>
|
||||
{t('Restoring an older version')}
|
||||
{t('Restoring an older version', {
|
||||
description: 'Title of the dialog confirming restoring a version',
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
@@ -99,6 +114,7 @@ export const ModalConfirmationVersion = ({
|
||||
<Text $variation="secondary" as="p" $margin="none">
|
||||
{t(
|
||||
"The current document will be replaced, but you'll still find it in the version history.",
|
||||
{ description: 'Explanation in the restore version dialog' },
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
+24
-7
@@ -60,7 +60,9 @@ export const ModalSelectVersion = ({
|
||||
closeOnClickOutside={true}
|
||||
size={ModalSize.EXTRA_LARGE}
|
||||
onClose={onClose}
|
||||
aria-label={t('Version history')}
|
||||
aria-label={t('Version history', {
|
||||
description: 'Title of the dialog listing past versions',
|
||||
})}
|
||||
>
|
||||
<NoPaddingStyle />
|
||||
<Box
|
||||
@@ -76,7 +78,9 @@ export const ModalSelectVersion = ({
|
||||
id="modal-select-version-title"
|
||||
className="sr-only"
|
||||
>
|
||||
{t('Version history')}
|
||||
{t('Version history', {
|
||||
description: 'Title of the dialog listing past versions',
|
||||
})}
|
||||
</Text>
|
||||
<Box
|
||||
$css={css`
|
||||
@@ -101,7 +105,9 @@ export const ModalSelectVersion = ({
|
||||
{!selectedVersionId && (
|
||||
<Box $align="center" $justify="center" $height="100%">
|
||||
<Text $size="h6" $weight="bold">
|
||||
{t('Select a version on the right to restore')}
|
||||
{t('Select a version on the right to restore', {
|
||||
description: 'Hint in the version history dialog',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -119,7 +125,9 @@ export const ModalSelectVersion = ({
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
aria-label={t('Version list')}
|
||||
aria-label={t('Version list', {
|
||||
description: 'Accessible name of the list of versions',
|
||||
})}
|
||||
$css={css`
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
@@ -137,10 +145,16 @@ export const ModalSelectVersion = ({
|
||||
$padding="sm"
|
||||
>
|
||||
<Text $size="h6" $weight="bold">
|
||||
{t('History')}
|
||||
{t('History', {
|
||||
description:
|
||||
'Dropdown menu item to view the document history',
|
||||
})}
|
||||
</Text>
|
||||
<ButtonCloseModal
|
||||
aria-label={t('Close the version history modal')}
|
||||
aria-label={t('Close the version history modal', {
|
||||
description:
|
||||
'Accessible name of the button closing the version history',
|
||||
})}
|
||||
autoFocus
|
||||
onClick={onClose}
|
||||
size="nano"
|
||||
@@ -166,7 +180,10 @@ export const ModalSelectVersion = ({
|
||||
disabled={!selectedVersionId}
|
||||
onClick={restoreModal.open}
|
||||
>
|
||||
{t('Restore')}
|
||||
{t('Restore', {
|
||||
description:
|
||||
'Button to restore a deleted document or an older version',
|
||||
})}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
+4
-1
@@ -15,7 +15,10 @@ export const VersionItem = ({ text, isActive, onSelect }: VersionItemProps) => {
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
aria-label={t('Restore version of {{date}}', { date: text })}
|
||||
aria-label={t('Restore version of {{date}}', {
|
||||
description: 'Accessible name of a version item',
|
||||
date: text,
|
||||
})}
|
||||
aria-pressed={isActive}
|
||||
$width="100%"
|
||||
$css={`
|
||||
|
||||
+7
-2
@@ -121,7 +121,9 @@ export const VersionList = ({
|
||||
{versions?.length === 0 && (
|
||||
<Box $align="center" $margin="large">
|
||||
<Text $size="h6" $weight="bold">
|
||||
{t('No versions')}
|
||||
{t('No versions', {
|
||||
description: 'Message when a document has no past version',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -135,7 +137,10 @@ export const VersionList = ({
|
||||
</Box>
|
||||
<Text className="sr-only" aria-live="polite">
|
||||
{selectedVersionDate
|
||||
? t('Selected version {{date}}', { date: selectedVersionDate })
|
||||
? t('Selected version {{date}}', {
|
||||
description: 'Screen reader text of the selected version',
|
||||
date: selectedVersionDate,
|
||||
})
|
||||
: ''}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
+32
-6
@@ -121,11 +121,18 @@ export const DraggableDocGridContentList = ({
|
||||
screenReaderInstructions: {
|
||||
draggable: t(
|
||||
'To pick up a draggable item, press space or enter. While dragging, use the arrow keys to move the item. Press space or enter again to drop the item in its new position, or press escape to cancel.',
|
||||
{
|
||||
description:
|
||||
'Screen reader instructions for keyboard drag and drop',
|
||||
},
|
||||
),
|
||||
},
|
||||
announcements: {
|
||||
onDragStart({ active }: { active: { id: UniqueIdentifier } }) {
|
||||
return t('Picked up document {{id}}.', { id: active.id });
|
||||
return t('Picked up document {{id}}.', {
|
||||
description: 'Screen reader announcement when dragging starts',
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
onDragOver({
|
||||
active,
|
||||
@@ -136,11 +143,13 @@ export const DraggableDocGridContentList = ({
|
||||
}) {
|
||||
if (over) {
|
||||
return t('Document {{activeId}} is over document {{overId}}.', {
|
||||
description: 'Screen reader announcement while dragging',
|
||||
activeId: active.id,
|
||||
overId: over.id,
|
||||
});
|
||||
}
|
||||
return t('Document {{id}} is no longer over a droppable area.', {
|
||||
description: 'Screen reader announcement while dragging',
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
@@ -154,13 +163,23 @@ export const DraggableDocGridContentList = ({
|
||||
if (over) {
|
||||
return t(
|
||||
'Document {{activeId}} was dropped over document {{overId}}.',
|
||||
{ activeId: active.id, overId: over.id },
|
||||
{
|
||||
description:
|
||||
'Screen reader announcement when a doc is dropped on another',
|
||||
activeId: active.id,
|
||||
overId: over.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
return t('Document {{id}} was dropped.', { id: active.id });
|
||||
return t('Document {{id}} was dropped.', {
|
||||
description: 'Screen reader announcement when a doc is dropped',
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
onDragCancel({ active }: { active: { id: UniqueIdentifier } }) {
|
||||
return t('Dragging was cancelled. Document {{id}} was dropped.', {
|
||||
description:
|
||||
'Screen reader announcement when dragging is cancelled',
|
||||
id: active.id,
|
||||
});
|
||||
},
|
||||
@@ -171,10 +190,15 @@ export const DraggableDocGridContentList = ({
|
||||
|
||||
const overlayText = useMemo(() => {
|
||||
if (!canDrag) {
|
||||
return t('You must be the owner to move the document');
|
||||
return t('You must be the owner to move the document', {
|
||||
description: 'Tooltip on a doc that cannot be dragged',
|
||||
});
|
||||
}
|
||||
if (!canDrop) {
|
||||
return t('You must be at least the administrator of the target document');
|
||||
return t(
|
||||
'You must be at least the administrator of the target document',
|
||||
{ description: 'Tooltip on a drop target the user cannot move into' },
|
||||
);
|
||||
}
|
||||
|
||||
return selectedDoc?.title || untitledDocument;
|
||||
@@ -228,7 +252,9 @@ export const DraggableDocGridContentList = ({
|
||||
$radius="12px"
|
||||
data-testid="drag-doc-overlay"
|
||||
role="alert"
|
||||
aria-label={t('Drag and drop status')}
|
||||
aria-label={t('Drag and drop status', {
|
||||
description: 'Accessible name of the drag and drop live region',
|
||||
})}
|
||||
$theme={cannotMoveDoc ? 'error' : 'brand'}
|
||||
$variation="tertiary"
|
||||
$scope="semantic"
|
||||
|
||||
@@ -121,7 +121,9 @@ export const DocsGrid = ({
|
||||
>
|
||||
<FadeComponent isVisible={!!hasDocs}>
|
||||
<Box
|
||||
aria-label={t('Documents grid')}
|
||||
aria-label={t('Documents grid', {
|
||||
description: 'Accessible name of the list of documents',
|
||||
})}
|
||||
$display="grid"
|
||||
$css={css`
|
||||
grid-template-columns: ${
|
||||
@@ -158,7 +160,9 @@ export const DocsGrid = ({
|
||||
variant="tertiary"
|
||||
className="sr-only"
|
||||
>
|
||||
{t('More docs')}
|
||||
{t('More docs', {
|
||||
description: 'Button loading more documents',
|
||||
})}
|
||||
</Button>
|
||||
</InView>
|
||||
)}
|
||||
@@ -178,20 +182,30 @@ const DocGridTitleBar = ({
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
let title = t('Recent');
|
||||
let title = t('Recent', {
|
||||
description: 'Name of the filter listing recently opened docs',
|
||||
});
|
||||
let icon = <ClockIcon width={24} height={24} aria-hidden="true" />;
|
||||
if (target === DocDefaultFilter.MY_DOCS) {
|
||||
icon = <UserIcon width={24} height={24} aria-hidden="true" />;
|
||||
title = t('My docs');
|
||||
title = t('My docs', {
|
||||
description: 'Name of the filter listing docs owned by the user',
|
||||
});
|
||||
} else if (target === DocDefaultFilter.SHARED_WITH_ME) {
|
||||
icon = <SharedIcon width={24} height={24} aria-hidden="true" />;
|
||||
title = t('Shared with me');
|
||||
title = t('Shared with me', {
|
||||
description: 'Name of the filter listing docs shared with the user',
|
||||
});
|
||||
} else if (target === DocDefaultFilter.STARRED) {
|
||||
icon = <StarIcon width={24} height={24} aria-hidden="true" />;
|
||||
title = t('Starred');
|
||||
title = t('Starred', {
|
||||
description: "Name of the filter listing the user's favorite docs",
|
||||
});
|
||||
} else if (target === DocDefaultFilter.TRASHBIN) {
|
||||
icon = <TrashIcon width={24} height={24} aria-hidden="true" />;
|
||||
title = t('Trashbin');
|
||||
title = t('Trashbin', {
|
||||
description: 'Name of the filter listing deleted docs',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -229,18 +243,24 @@ const DocGridNoDocs = ({ target }: { target: DocDefaultFilter }) => {
|
||||
<>
|
||||
<DocsIcon width={56} height={56} aria-hidden="true" />
|
||||
<Text $size="sm" $weight="700">
|
||||
{t('No doc yet')}
|
||||
{t('No doc yet', {
|
||||
description: 'Empty state heading when the user has no doc',
|
||||
})}
|
||||
</Text>
|
||||
{[DocDefaultFilter.ALL_DOCS, DocDefaultFilter.MY_DOCS].includes(
|
||||
target,
|
||||
) && (
|
||||
<Text $size="sm" $weight="400" $variation="secondary">
|
||||
{t('Your docs will appear here.')}
|
||||
{t('Your docs will appear here.', {
|
||||
description: 'Empty state text of the docs list',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
{target === DocDefaultFilter.SHARED_WITH_ME && (
|
||||
<Text $size="sm" $weight="400" $variation="secondary">
|
||||
{t('Your shared docs will appear here.')}
|
||||
{t('Your shared docs will appear here.', {
|
||||
description: 'Empty state text of the shared docs list',
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
@@ -249,10 +269,14 @@ const DocGridNoDocs = ({ target }: { target: DocDefaultFilter }) => {
|
||||
<>
|
||||
<BinIcon width={56} height={56} aria-hidden="true" />
|
||||
<Text $size="sm" $weight="700">
|
||||
{t('No doc deleted')}
|
||||
{t('No doc deleted', {
|
||||
description: 'Empty state heading of the trashbin',
|
||||
})}
|
||||
</Text>
|
||||
<Text $size="sm" $weight="400" $variation="secondary">
|
||||
{t('Deleted docs will appear here.')}
|
||||
{t('Deleted docs will appear here.', {
|
||||
description: 'Empty state text of the trashbin',
|
||||
})}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
+13
-4
@@ -60,13 +60,20 @@ export const DocsGridTrashbinActions = ({
|
||||
],
|
||||
options: {
|
||||
onSuccess: (_data) => {
|
||||
toast(t('The document has been restored.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
toast(
|
||||
t('The document has been restored.', {
|
||||
description: 'Toast shown after a deleted document is restored',
|
||||
}),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast(
|
||||
t('An error occurred while restoring the document: {{error}}', {
|
||||
description: 'Toast shown when restoring a deleted document fails',
|
||||
error: error?.message,
|
||||
}),
|
||||
VariantType.ERROR,
|
||||
@@ -84,7 +91,9 @@ export const DocsGridTrashbinActions = ({
|
||||
|
||||
const options: DropdownMenuItem[] = [
|
||||
{
|
||||
label: t('Restore'),
|
||||
label: t('Restore', {
|
||||
description: 'Button to restore a deleted document or an older version',
|
||||
}),
|
||||
icon: (
|
||||
<Icon
|
||||
$size="20px"
|
||||
|
||||
+37
-9
@@ -70,8 +70,12 @@ const DocGridColumnWithSort = ({
|
||||
<>
|
||||
<Box $padding={{ all: '3xs' }}>
|
||||
<DocGridSortButton
|
||||
label={t('Name')}
|
||||
ariaLabel={t('Name')}
|
||||
label={t('Name', {
|
||||
description: 'Column header and sort button: document name',
|
||||
})}
|
||||
ariaLabel={t('Name', {
|
||||
description: 'Column header and sort button: document name',
|
||||
})}
|
||||
ordering={ordering}
|
||||
field="title"
|
||||
defaultOrdering={defaultOrdering.title}
|
||||
@@ -91,10 +95,16 @@ const DocGridColumnWithSort = ({
|
||||
$gap="2xs"
|
||||
>
|
||||
<ClockIcon width={16} height={16} aria-hidden="true" />{' '}
|
||||
{t('Last modified')}
|
||||
{t('Last modified', {
|
||||
description:
|
||||
'Column header and sort button: date of last modification',
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
ariaLabel={t('Last modified')}
|
||||
ariaLabel={t('Last modified', {
|
||||
description:
|
||||
'Column header and sort button: date of last modification',
|
||||
})}
|
||||
ordering={ordering}
|
||||
field="updated_at"
|
||||
defaultOrdering={defaultOrdering.updated_at}
|
||||
@@ -114,7 +124,9 @@ const DocGridColumnWithoutSort = ({ target }: { target: DocDefaultFilter }) => {
|
||||
<>
|
||||
<Box $padding={{ all: '3xs' }}>
|
||||
<Text $size="xs" $variation="secondary" $weight="500">
|
||||
{t('Name')}
|
||||
{t('Name', {
|
||||
description: 'Column header and sort button: document name',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
{!isSmallMobile && (
|
||||
@@ -129,8 +141,14 @@ const DocGridColumnWithoutSort = ({ target }: { target: DocDefaultFilter }) => {
|
||||
>
|
||||
<ClockIcon width={16} height={16} aria-hidden="true" />{' '}
|
||||
{target === DocDefaultFilter.STARRED
|
||||
? t('Last modified')
|
||||
: t('Days remaining')}
|
||||
? t('Last modified', {
|
||||
description:
|
||||
'Column header and sort button: date of last modification',
|
||||
})
|
||||
: t('Days remaining', {
|
||||
description:
|
||||
'Column header of the trashbin: days before permanent deletion',
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
@@ -171,11 +189,21 @@ const DocGridSortButton = ({
|
||||
? t(
|
||||
'Sorted documents by {{label}}, {{direction}}. Activate to reverse.',
|
||||
{
|
||||
description: 'Accessible name of a sort button, current sort',
|
||||
label: ariaLabel,
|
||||
direction: isDesc ? t('descending') : t('ascending'),
|
||||
direction: isDesc
|
||||
? t('descending', {
|
||||
description: 'Sort direction, from highest to lowest',
|
||||
})
|
||||
: t('ascending', {
|
||||
description: 'Sort direction, from lowest to highest',
|
||||
}),
|
||||
},
|
||||
)
|
||||
: t('Sort documents by {{label}}', { label: ariaLabel })
|
||||
: t('Sort documents by {{label}}', {
|
||||
description: 'Accessible name of a sort button',
|
||||
label: ariaLabel,
|
||||
})
|
||||
}
|
||||
iconPosition="right"
|
||||
icon={<ArrowUpDownIcon width={16} height={16} aria-hidden="true" />}
|
||||
|
||||
@@ -79,6 +79,7 @@ export const DocsGridItem = ({
|
||||
`}
|
||||
className="--docs--doc-grid-item"
|
||||
aria-label={t('Open document: {{title}}', {
|
||||
description: 'Accessible name of a document row, an action to open it',
|
||||
title: doc.title || untitledDocument,
|
||||
})}
|
||||
{...boxProps}
|
||||
@@ -132,6 +133,8 @@ export const DocsGridItem = ({
|
||||
href={`/docs/${doc.id}`}
|
||||
tabIndex={-1}
|
||||
aria-label={t('{{title}}, updated {{date}}', {
|
||||
description:
|
||||
'Accessible name of a document row with its last update',
|
||||
title: doc.title || untitledDocument,
|
||||
date: dateToDisplay,
|
||||
})}
|
||||
@@ -191,7 +194,12 @@ export const DocsGridItemTitle = ({
|
||||
$size="sm"
|
||||
icon={<StarIcon aria-hidden="true" width={16} height={16} />}
|
||||
/>
|
||||
<span className="sr-only">{t('This document is starred')}</span>
|
||||
<span className="sr-only">
|
||||
{t('This document is starred', {
|
||||
description:
|
||||
"Screen reader text indicating the document is in the user's favorites",
|
||||
})}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isShared && !withTooltip && <IconPublic isPublic={isPublic} />}
|
||||
@@ -200,8 +208,13 @@ export const DocsGridItemTitle = ({
|
||||
content={
|
||||
<Text $textAlign="center">
|
||||
{isPublic
|
||||
? t('Accessible to anyone')
|
||||
: t('Accessible to authenticated users')}
|
||||
? t('Accessible to anyone', {
|
||||
description: 'Tooltip: the document is public',
|
||||
})
|
||||
: t('Accessible to authenticated users', {
|
||||
description:
|
||||
'Tooltip: the document is open to logged-in users',
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
placement="top"
|
||||
@@ -227,8 +240,12 @@ const IconPublic = ({ isPublic }: { isPublic: boolean }) => {
|
||||
$size="sm"
|
||||
aria-label={
|
||||
isPublic
|
||||
? t('Accessible to anyone')
|
||||
: t('Accessible to authenticated users')
|
||||
? t('Accessible to anyone', {
|
||||
description: 'Tooltip: the document is public',
|
||||
})
|
||||
: t('Accessible to authenticated users', {
|
||||
description: 'Tooltip: the document is open to logged-in users',
|
||||
})
|
||||
}
|
||||
icon={
|
||||
isPublic ? (
|
||||
@@ -255,7 +272,7 @@ const useDateToDisplay = (doc: Doc, isInTrashbin: boolean) => {
|
||||
config.TRASHBIN_CUTOFF_DAYS,
|
||||
);
|
||||
|
||||
dateToDisplay = `${daysLeft} ${t('days', { count: daysLeft })}`;
|
||||
dateToDisplay = `${daysLeft} ${t('days', { description: 'Unit after a number of days, plural form', count: daysLeft })}`;
|
||||
isRelativeDate = false;
|
||||
}
|
||||
|
||||
|
||||
+9
-2
@@ -34,7 +34,11 @@ export const DocsGridItemSharedButton = ({ doc, disabled }: Props) => {
|
||||
<Tooltip
|
||||
content={
|
||||
<Text $textAlign="center">
|
||||
{t('Shared with {{count}} users', { count: sharedCount })}
|
||||
{t('Shared with {{count}} users', {
|
||||
description:
|
||||
'Tooltip: number of people a doc is shared with, plural form',
|
||||
count: sharedCount,
|
||||
})}
|
||||
</Text>
|
||||
}
|
||||
placement="top"
|
||||
@@ -42,7 +46,10 @@ export const DocsGridItemSharedButton = ({ doc, disabled }: Props) => {
|
||||
>
|
||||
<Button
|
||||
className="--docs--doc-grid-item-shared-button"
|
||||
aria-label={t('Open the sharing settings for the document')}
|
||||
aria-label={t('Open the sharing settings for the document', {
|
||||
description:
|
||||
'Accessible name of the button opening the share dialog',
|
||||
})}
|
||||
data-testid={`docs-grid-item-shared-button-${doc.id}`}
|
||||
style={{
|
||||
padding: `0 var(--c--globals--spacings--xxxs) 0 var(--c--globals--spacings--xxxs)`,
|
||||
|
||||
@@ -23,7 +23,9 @@ export const FooterActions = ({ withLogin }: FooterActionsProps) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
const userMenu = user || {
|
||||
full_name: t('Guest'),
|
||||
full_name: t('Guest', {
|
||||
description: 'Name displayed for a user not logged in',
|
||||
}),
|
||||
email: '',
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,12 @@ export const HeaderFloatingBar = (props: PropsWithChildren<BoxType>) => {
|
||||
return (
|
||||
<FloatingBar $align="center" {...props}>
|
||||
{isTablet && (
|
||||
<LeftPanelCollapseButton ariaLabel={t('Toggle left panel')} />
|
||||
<LeftPanelCollapseButton
|
||||
ariaLabel={t('Toggle left panel', {
|
||||
description:
|
||||
'Accessible name of the button showing or hiding the left panel',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<FadeComponent isVisible={isVisible}>
|
||||
<HeaderIcon />
|
||||
|
||||
@@ -75,7 +75,9 @@ export const HelpMenu = ({
|
||||
const options = useMemo<DropdownMenuItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: t('Get Support'),
|
||||
label: t('Get Support', {
|
||||
description: 'Help menu link to the support',
|
||||
}),
|
||||
icon: <BubbleTextIcon aria-hidden="true" width="24" height="24" />,
|
||||
callback: () => {
|
||||
if (supportMailto) {
|
||||
@@ -85,7 +87,9 @@ export const HelpMenu = ({
|
||||
isHidden: !supportMailto,
|
||||
},
|
||||
{
|
||||
label: t('Documentation'),
|
||||
label: t('Documentation', {
|
||||
description: 'Help menu link to the documentation',
|
||||
}),
|
||||
icon: <DocIcon aria-hidden="true" width="24" height="24" />,
|
||||
callback: () => {
|
||||
if (documentationUrl) {
|
||||
@@ -95,18 +99,22 @@ export const HelpMenu = ({
|
||||
isHidden: !documentationUrl,
|
||||
},
|
||||
{
|
||||
label: t('Onboarding'),
|
||||
label: t('Onboarding', {
|
||||
description: 'Help menu entry replaying the product tour',
|
||||
}),
|
||||
icon: <WandAndStarsIcon aria-hidden="true" width="24" height="24" />,
|
||||
callback: modalOnbording.open,
|
||||
isHidden: !onboardingEnabled,
|
||||
},
|
||||
{
|
||||
label: t('Legal'),
|
||||
label: t('Legal', { description: 'Help menu group of legal pages' }),
|
||||
icon: <LegalIcon aria-hidden="true" width="24" height="24" />,
|
||||
isHidden: !hasLegalLinks,
|
||||
children: [
|
||||
{
|
||||
label: t('Personal data and cookies'),
|
||||
label: t('Personal data and cookies', {
|
||||
description: 'Help menu link to the privacy policy',
|
||||
}),
|
||||
callback: () => {
|
||||
if (legalLinks?.personal_data) {
|
||||
openExternalLink(legalLinks.personal_data);
|
||||
@@ -115,7 +123,9 @@ export const HelpMenu = ({
|
||||
isHidden: !legalLinks?.personal_data,
|
||||
},
|
||||
{
|
||||
label: t('Terms of use'),
|
||||
label: t('Terms of use', {
|
||||
description: 'Help menu link to the terms of use',
|
||||
}),
|
||||
callback: () => {
|
||||
if (legalLinks?.terms_of_use) {
|
||||
openExternalLink(legalLinks.terms_of_use);
|
||||
@@ -124,7 +134,9 @@ export const HelpMenu = ({
|
||||
isHidden: !legalLinks?.terms_of_use,
|
||||
},
|
||||
{
|
||||
label: t('Accessibility statement'),
|
||||
label: t('Accessibility statement', {
|
||||
description: 'Help menu link to the accessibility statement',
|
||||
}),
|
||||
callback: () => {
|
||||
if (legalLinks?.accessibility_statement) {
|
||||
openExternalLink(legalLinks.accessibility_statement);
|
||||
@@ -133,7 +145,9 @@ export const HelpMenu = ({
|
||||
isHidden: !legalLinks?.accessibility_statement,
|
||||
},
|
||||
{
|
||||
label: t('Legal notice'),
|
||||
label: t('Legal notice', {
|
||||
description: 'Help menu link to the legal notice',
|
||||
}),
|
||||
callback: () => {
|
||||
if (legalLinks?.legal_notice) {
|
||||
openExternalLink(legalLinks.legal_notice);
|
||||
@@ -179,7 +193,10 @@ export const HelpMenu = ({
|
||||
>
|
||||
<Box $direction="row" $align="center">
|
||||
<Button
|
||||
aria-label={t('Open help menu')}
|
||||
aria-label={t('Open help menu', {
|
||||
description:
|
||||
'Accessible name of the button opening the help menu',
|
||||
})}
|
||||
color={colorButton || 'neutral'}
|
||||
variant="tertiary"
|
||||
iconPosition="left"
|
||||
|
||||
@@ -109,13 +109,19 @@ export const OnBoarding = (props: OnBoardingProps) => {
|
||||
{props.isOpen ? <OnBoardingStyle /> : null}
|
||||
<OnboardingModalFixed
|
||||
size={ModalSize.LARGE}
|
||||
appName={t('Discover Docs')}
|
||||
mainTitle={t('Learn the core principles')}
|
||||
appName={t('Discover Docs', {
|
||||
description: 'Title of the product tour dialog',
|
||||
})}
|
||||
mainTitle={t('Learn the core principles', {
|
||||
description: 'Subtitle of the product tour dialog',
|
||||
})}
|
||||
steps={steps}
|
||||
footerLink={
|
||||
learnMoreUrl
|
||||
? {
|
||||
label: t('Learn more docs features'),
|
||||
label: t('Learn more docs features', {
|
||||
description: 'Link at the end of the product tour',
|
||||
}),
|
||||
href: learnMoreUrl,
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -38,9 +38,12 @@ export const useOnboardingSteps = () => {
|
||||
<DragIndicatorIcon aria-hidden="true" />
|
||||
</OnboardingStepIcon>
|
||||
),
|
||||
title: t('Compose your doc easily'),
|
||||
title: t('Compose your doc easily', {
|
||||
description: 'Product tour step title',
|
||||
}),
|
||||
description: t(
|
||||
'Move, duplicate, and transform your texts, headings, lists, images without breaking your layout.',
|
||||
{ description: 'Product tour step description' },
|
||||
),
|
||||
content: (
|
||||
<video
|
||||
@@ -48,7 +51,9 @@ export const useOnboardingSteps = () => {
|
||||
description: 'URL of onboarding step 1 preview image',
|
||||
defaultValue: '/assets/on-boarding/step_1_EN.webm',
|
||||
})}
|
||||
aria-label={t('Compose your doc easily')}
|
||||
aria-label={t('Compose your doc easily', {
|
||||
description: 'Product tour step title',
|
||||
})}
|
||||
width={350}
|
||||
height={350}
|
||||
autoPlay
|
||||
@@ -69,9 +74,12 @@ export const useOnboardingSteps = () => {
|
||||
<FormatTextIcon aria-hidden="true" />
|
||||
</OnboardingStepIcon>
|
||||
),
|
||||
title: t('Format your content with the toolbar'),
|
||||
title: t('Format your content with the toolbar', {
|
||||
description: 'Product tour step title',
|
||||
}),
|
||||
description: t(
|
||||
'Apply styles, structure, and emphasis in one click—keep documents clean, consistent, and easy to scan.',
|
||||
{ description: 'Product tour step description' },
|
||||
),
|
||||
content: (
|
||||
<video
|
||||
@@ -79,7 +87,9 @@ export const useOnboardingSteps = () => {
|
||||
description: 'URL of onboarding step 2 preview image',
|
||||
defaultValue: '/assets/on-boarding/step_2_EN.webm',
|
||||
})}
|
||||
aria-label={t('Format your content with the toolbar')}
|
||||
aria-label={t('Format your content with the toolbar', {
|
||||
description: 'Product tour step title',
|
||||
})}
|
||||
width={350}
|
||||
height={350}
|
||||
autoPlay
|
||||
@@ -100,9 +110,12 @@ export const useOnboardingSteps = () => {
|
||||
<FileShareIcon aria-hidden="true" />
|
||||
</OnboardingStepIcon>
|
||||
),
|
||||
title: t('Share and collaborate with ease'),
|
||||
title: t('Share and collaborate with ease', {
|
||||
description: 'Product tour step title',
|
||||
}),
|
||||
description: t(
|
||||
'Decide exactly who can view, comment, edit—or simply use shareable links.',
|
||||
{ description: 'Product tour step description' },
|
||||
),
|
||||
content: (
|
||||
<Image
|
||||
@@ -110,7 +123,9 @@ export const useOnboardingSteps = () => {
|
||||
description: 'URL of onboarding step 3 preview image',
|
||||
defaultValue: '/assets/on-boarding/step_3_EN.webp',
|
||||
})}
|
||||
alt={t('Share and collaborate with ease')}
|
||||
alt={t('Share and collaborate with ease', {
|
||||
description: 'Product tour step title',
|
||||
})}
|
||||
width={350}
|
||||
height={350}
|
||||
priority
|
||||
@@ -129,7 +144,9 @@ export const useOnboardingSteps = () => {
|
||||
<StackTemplateIcon aria-hidden="true" />
|
||||
</OnboardingStepIcon>
|
||||
),
|
||||
title: t('Draw inspiration from the content library'),
|
||||
title: t('Draw inspiration from the content library', {
|
||||
description: 'Product tour step title',
|
||||
}),
|
||||
description: (
|
||||
<Trans
|
||||
t={t}
|
||||
@@ -140,7 +157,9 @@ export const useOnboardingSteps = () => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={readyTemplateUrl}
|
||||
aria-label={t('Ready-made templates (opens in a new tab)')}
|
||||
aria-label={t('Ready-made templates (opens in a new tab)', {
|
||||
description: 'Accessible name of the link to templates',
|
||||
})}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
@@ -152,7 +171,9 @@ export const useOnboardingSteps = () => {
|
||||
description: 'URL of onboarding step 4 preview image',
|
||||
defaultValue: '/assets/on-boarding/step_4_EN.webp',
|
||||
})}
|
||||
alt={t('Draw inspiration from the content library')}
|
||||
alt={t('Draw inspiration from the content library', {
|
||||
description: 'Product tour step title',
|
||||
})}
|
||||
width={350}
|
||||
height={350}
|
||||
priority
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user