Merge pull request #3414 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream changes up to ef6405ab28
This commit is contained in:
Claire
2026-02-23 20:55:20 +01:00
committed by GitHub
116 changed files with 2793 additions and 525 deletions

View File

@@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "haml-lint",
"severity": "warning",
"pattern": [
{
"regexp": "^(.*):(\\d+)\\s\\[W]\\s(.*):\\s(.*)$",
"file": 1,
"line": 2,
"code": 3,
"message": 4
}
]
}
]
}

View File

@@ -42,5 +42,4 @@ jobs:
- name: Run haml-lint
run: |
echo "::add-matcher::.github/workflows/haml-lint-problem-matcher.json"
bin/haml-lint --reporter github

View File

@@ -749,7 +749,7 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-sidekiq (5.2.0)
rspec-sidekiq (5.3.0)
rspec-core (~> 3.0)
rspec-expectations (~> 3.0)
rspec-mocks (~> 3.0)

View File

@@ -1,10 +1,17 @@
import { apiRequestPost, apiRequestGet } from 'flavours/glitch/api';
import {
apiRequestPost,
apiRequestGet,
apiRequestDelete,
} from 'flavours/glitch/api';
import type {
ApiAccountJSON,
ApiFamiliarFollowersJSON,
} from 'flavours/glitch/api_types/accounts';
import type { ApiRelationshipJSON } from 'flavours/glitch/api_types/relationships';
import type { ApiHashtagJSON } from 'flavours/glitch/api_types/tags';
import type {
ApiFeaturedTagJSON,
ApiHashtagJSON,
} from 'flavours/glitch/api_types/tags';
export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
@@ -30,7 +37,19 @@ export const apiRemoveAccountFromFollowers = (id: string) =>
);
export const apiGetFeaturedTags = (id: string) =>
apiRequestGet<ApiHashtagJSON>(`v1/accounts/${id}/featured_tags`);
apiRequestGet<ApiHashtagJSON[]>(`v1/accounts/${id}/featured_tags`);
export const apiGetCurrentFeaturedTags = () =>
apiRequestGet<ApiFeaturedTagJSON[]>(`v1/featured_tags`);
export const apiPostFeaturedTag = (name: string) =>
apiRequestPost<ApiFeaturedTagJSON>('v1/featured_tags', { name });
export const apiDeleteFeaturedTag = (id: string) =>
apiRequestDelete(`v1/featured_tags/${id}`);
export const apiGetTagSuggestions = () =>
apiRequestGet<ApiHashtagJSON[]>('v1/featured_tags/suggestions');
export const apiGetEndorsedAccounts = (id: string) =>
apiRequestGet<ApiAccountJSON>(`v1/accounts/${id}/endorsements`);

View File

@@ -4,11 +4,29 @@ interface ApiHistoryJSON {
uses: string;
}
export interface ApiHashtagJSON {
interface ApiHashtagBase {
id: string;
name: string;
url: string;
}
export interface ApiHashtagJSON extends ApiHashtagBase {
history: [ApiHistoryJSON, ...ApiHistoryJSON[]];
following?: boolean;
featuring?: boolean;
}
export interface ApiFeaturedTagJSON extends ApiHashtagBase {
statuses_count: number;
last_status_at: string | null;
}
export function hashtagToFeaturedTag(tag: ApiHashtagJSON): ApiFeaturedTagJSON {
return {
id: tag.id,
name: tag.name,
url: tag.url,
statuses_count: 0,
last_status_at: null,
};
}

View File

@@ -3,7 +3,7 @@
}
.input {
padding-right: 45px;
padding-inline-end: 45px;
}
.menuButton {

View File

@@ -82,11 +82,23 @@ const ComboboxDemo: React.FC = () => {
const meta = {
title: 'Components/Form Fields/ComboboxField',
component: ComboboxDemo,
} satisfies Meta<typeof ComboboxDemo>;
component: ComboboxField,
render: () => <ComboboxDemo />,
} satisfies Meta<typeof ComboboxField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Example: Story = {};
export const Example: Story = {
args: {
// Adding these types to keep TS happy, they're not passed on to `ComboboxDemo`
label: '',
value: '',
onChange: () => undefined,
items: [],
getItemId: () => '',
renderItem: () => <>Nothing</>,
onSelectItem: () => undefined,
},
};

View File

@@ -1,4 +1,3 @@
import type { ComponentPropsWithoutRef } from 'react';
import { forwardRef, useCallback, useId, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -9,6 +8,7 @@ import Overlay from 'react-overlays/Overlay';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { matchWidth } from 'flavours/glitch/components/dropdown/utils';
import { IconButton } from 'flavours/glitch/components/icon_button';
import { useOnClickOutside } from 'flavours/glitch/hooks/useOnClickOutside';
@@ -17,6 +17,7 @@ import classes from './combobox.module.scss';
import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper';
import { TextInput } from './text_input_field';
import type { TextInputProps } from './text_input_field';
interface ComboboxItem {
id: string;
@@ -27,17 +28,45 @@ export interface ComboboxItemState {
isDisabled: boolean;
}
interface ComboboxProps<
T extends ComboboxItem,
> extends ComponentPropsWithoutRef<'input'> {
interface ComboboxProps<T extends ComboboxItem> extends TextInputProps {
/**
* The value of the combobox's text input
*/
value: string;
/**
* Change handler for the text input field
*/
onChange: React.ChangeEventHandler<HTMLInputElement>;
/**
* Set this to true when the list of options is dynamic and currently loading.
* Causes a loading indicator to be displayed inside of the dropdown menu.
*/
isLoading?: boolean;
/**
* The set of options/suggestions that should be rendered in the dropdown menu.
*/
items: T[];
getItemId: (item: T) => string;
/**
* A function that must return a unique id for each option passed via `items`
*/
getItemId?: (item: T) => string;
/**
* Providing this function turns the combobox into a multi-select box that assumes
* multiple options to be selectable. Single-selection is handled automatically.
*/
getIsItemSelected?: (item: T) => boolean;
/**
* Use this function to mark items as disabled, if needed
*/
getIsItemDisabled?: (item: T) => boolean;
/**
* Customise the rendering of each option.
* The rendered content must not contain other interactive content!
*/
renderItem: (item: T, state: ComboboxItemState) => React.ReactElement;
/**
* The main selection handler, called when an option is selected or deselected.
*/
onSelectItem: (item: T) => void;
}
@@ -45,8 +74,12 @@ interface Props<T extends ComboboxItem>
extends ComboboxProps<T>, CommonFieldWrapperProps {}
/**
* The combobox field allows users to select one or multiple items
* from a large list of options by searching or filtering.
* The combobox field allows users to select one or more items
* by searching or filtering a large or dynamic list of options.
*
* It is an implementation of the [APG Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/),
* with inspiration taken from Sarah Higley's extensive combobox
* [research & implementations](https://sarahmhigley.com/writing/select-your-poison/).
*/
export const ComboboxFieldWithRef = <T extends ComboboxItem>(
@@ -80,7 +113,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
value,
isLoading = false,
items,
getItemId,
getItemId = (item) => item.id,
getIsItemDisabled,
getIsItemSelected,
disabled,
@@ -88,6 +121,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
onSelectItem,
onChange,
onKeyDown,
icon = SearchIcon,
className,
...otherProps
}: ComboboxProps<T>,
@@ -306,6 +340,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
value={value}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
icon={icon}
className={classNames(classes.input, className)}
ref={mergeRefs}
/>

View File

@@ -20,6 +20,15 @@
font-size: 16px;
}
.iconWrapper & {
// Make space for icon displayed at start of input
padding-inline-start: 36px;
}
&::placeholder {
color: var(--color-text-secondary);
}
&:focus {
outline-color: var(--color-text-brand);
}
@@ -40,3 +49,17 @@
cursor: not-allowed;
}
}
.iconWrapper {
position: relative;
}
.icon {
pointer-events: none;
position: absolute;
width: 22px;
height: 22px;
inset-inline-start: 10px;
inset-block-start: 10px;
color: var(--color-text-secondary);
}

View File

@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { TextInputField, TextInput } from './text_input_field';
const meta = {
@@ -42,6 +44,14 @@ export const WithError: Story = {
},
};
export const WithIcon: Story = {
args: {
label: 'Search',
hint: undefined,
icon: SearchIcon,
},
};
export const Plain: Story = {
render(args) {
return <TextInput {...args} />;

View File

@@ -3,12 +3,18 @@ import { forwardRef } from 'react';
import classNames from 'classnames';
import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon';
import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper';
import classes from './text_input.module.scss';
interface Props
extends ComponentPropsWithoutRef<'input'>, CommonFieldWrapperProps {}
export interface TextInputProps extends ComponentPropsWithoutRef<'input'> {
icon?: IconProp;
}
interface Props extends TextInputProps, CommonFieldWrapperProps {}
/**
* A simple form field for single-line text.
@@ -33,16 +39,33 @@ export const TextInputField = forwardRef<HTMLInputElement, Props>(
TextInputField.displayName = 'TextInputField';
export const TextInput = forwardRef<
HTMLInputElement,
ComponentPropsWithoutRef<'input'>
>(({ type = 'text', className, ...otherProps }, ref) => (
<input
type={type}
{...otherProps}
className={classNames(className, classes.input)}
ref={ref}
/>
));
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ type = 'text', icon, className, ...otherProps }, ref) => (
<WrapFieldWithIcon icon={icon}>
<input
type={type}
{...otherProps}
className={classNames(className, classes.input)}
ref={ref}
/>
</WrapFieldWithIcon>
),
);
TextInput.displayName = 'TextInput';
const WrapFieldWithIcon: React.FC<{
icon?: IconProp;
children: React.ReactElement;
}> = ({ icon, children }) => {
if (icon) {
return (
<div className={classes.iconWrapper}>
<Icon icon={icon} id='input-icon' className={classes.icon} />
{children}
</div>
);
}
return children;
};

View File

@@ -0,0 +1,57 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { Column } from '@/flavours/glitch/components/column';
import { ColumnHeader } from '@/flavours/glitch/components/column_header';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
import { useColumnsContext } from '../../ui/util/columns_context';
import classes from '../styles.module.scss';
export const AccountEditEmptyColumn: FC<{
notFound?: boolean;
}> = ({ notFound }) => {
const { multiColumn } = useColumnsContext();
if (notFound) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
};
export const AccountEditColumn: FC<{
title: string;
to: string;
children: React.ReactNode;
}> = ({ to, title, children }) => {
const { multiColumn } = useColumnsContext();
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={title}
className={classes.columnHeader}
showBackButton
extraButton={
<Link to={to} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
{children}
</Column>
);
};

View File

@@ -0,0 +1,100 @@
import type { FC, MouseEventHandler } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { Button } from '@/flavours/glitch/components/button';
import { IconButton } from '@/flavours/glitch/components/icon_button';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import classes from '../styles.module.scss';
const messages = defineMessages({
add: {
id: 'account_edit.button.add',
defaultMessage: 'Add {item}',
},
edit: {
id: 'account_edit.button.edit',
defaultMessage: 'Edit {item}',
},
delete: {
id: 'account_edit.button.delete',
defaultMessage: 'Delete {item}',
},
});
export interface EditButtonProps {
onClick: MouseEventHandler;
item: string | MessageDescriptor;
edit?: boolean;
icon?: boolean;
disabled?: boolean;
}
export const EditButton: FC<EditButtonProps> = ({
onClick,
item,
edit = false,
icon = edit,
disabled,
}) => {
const intl = useIntl();
const itemText = typeof item === 'string' ? item : intl.formatMessage(item);
const label = intl.formatMessage(messages[edit ? 'edit' : 'add'], {
item: itemText,
});
if (icon) {
return (
<EditIconButton title={label} onClick={onClick} disabled={disabled} />
);
}
return (
<Button
className={classes.editButton}
onClick={onClick}
disabled={disabled}
>
{label}
</Button>
);
};
export const EditIconButton: FC<{
onClick: MouseEventHandler;
title: string;
disabled?: boolean;
}> = ({ title, onClick, disabled }) => (
<IconButton
icon='pencil'
iconComponent={EditIcon}
onClick={onClick}
className={classes.editButton}
title={title}
disabled={disabled}
/>
);
export const DeleteIconButton: FC<{
onClick: MouseEventHandler;
item: string;
disabled?: boolean;
}> = ({ onClick, item, disabled }) => {
const intl = useIntl();
return (
<IconButton
icon='delete'
iconComponent={DeleteIcon}
onClick={onClick}
className={classNames(classes.editButton, classes.deleteButton)}
title={intl.formatMessage(messages.delete, { item })}
disabled={disabled}
/>
);
};

View File

@@ -0,0 +1,89 @@
import { useCallback } from 'react';
import classes from '../styles.module.scss';
import { DeleteIconButton, EditButton } from './edit_button';
interface AnyItem {
id: string;
name: string;
}
interface AccountEditItemListProps<Item extends AnyItem = AnyItem> {
renderItem?: (item: Item) => React.ReactNode;
items: Item[];
onEdit?: (item: Item) => void;
onDelete?: (item: Item) => void;
disabled?: boolean;
}
export const AccountEditItemList = <Item extends AnyItem>({
renderItem,
items,
onEdit,
onDelete,
disabled,
}: AccountEditItemListProps<Item>) => {
if (items.length === 0) {
return null;
}
return (
<ul className={classes.itemList}>
{items.map((item) => (
<li key={item.id}>
<span>{renderItem?.(item) ?? item.name}</span>
<AccountEditItemButtons
item={item}
onEdit={onEdit}
onDelete={onDelete}
disabled={disabled}
/>
</li>
))}
</ul>
);
};
type AccountEditItemButtonsProps<Item extends AnyItem = AnyItem> = Pick<
AccountEditItemListProps<Item>,
'onEdit' | 'onDelete' | 'disabled'
> & { item: Item };
const AccountEditItemButtons = <Item extends AnyItem>({
item,
onDelete,
onEdit,
disabled,
}: AccountEditItemButtonsProps<Item>) => {
const handleEdit = useCallback(() => {
onEdit?.(item);
}, [item, onEdit]);
const handleDelete = useCallback(() => {
onDelete?.(item);
}, [item, onDelete]);
if (!onEdit && !onDelete) {
return null;
}
return (
<div className={classes.itemListButtons}>
{onEdit && (
<EditButton
edit
item={item.name}
disabled={disabled}
onClick={handleEdit}
/>
)}
{onDelete && (
<DeleteIconButton
item={item.name}
disabled={disabled}
onClick={handleDelete}
/>
)}
</div>
);
};

View File

@@ -1,55 +1,36 @@
import type { FC, ReactNode } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { IconButton } from '@/flavours/glitch/components/icon_button';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import classes from '../styles.module.scss';
const buttonMessage = defineMessage({
id: 'account_edit.section_edit_button',
defaultMessage: 'Edit',
});
interface AccountEditSectionProps {
title: MessageDescriptor;
description?: MessageDescriptor;
showDescription?: boolean;
onEdit?: () => void;
children?: ReactNode;
className?: string;
extraButtons?: ReactNode;
buttons?: ReactNode;
}
export const AccountEditSection: FC<AccountEditSectionProps> = ({
title,
description,
showDescription,
onEdit,
children,
className,
extraButtons,
buttons,
}) => {
const intl = useIntl();
return (
<section className={classNames(className, classes.section)}>
<header className={classes.sectionHeader}>
<h3 className={classes.sectionTitle}>
<FormattedMessage {...title} />
</h3>
{onEdit && (
<IconButton
icon='pencil'
iconComponent={EditIcon}
onClick={onEdit}
title={`${intl.formatMessage(buttonMessage)} ${intl.formatMessage(title)}`}
/>
)}
{extraButtons}
{buttons}
</header>
{showDescription && (
<p className={classes.sectionSubtitle}>

View File

@@ -0,0 +1,60 @@
import type { ChangeEventHandler, FC } from 'react';
import { useCallback } from 'react';
import { useIntl } from 'react-intl';
import type { ApiFeaturedTagJSON } from '@/flavours/glitch/api_types/tags';
import { Combobox } from '@/flavours/glitch/components/form_fields';
import {
addFeaturedTag,
clearSearch,
updateSearchQuery,
} from '@/flavours/glitch/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import classes from '../styles.module.scss';
export const AccountEditTagSearch: FC = () => {
const { query, isLoading, results } = useAppSelector(
(state) => state.profileEdit.search,
);
const dispatch = useAppDispatch();
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
void dispatch(updateSearchQuery(e.target.value));
},
[dispatch],
);
const intl = useIntl();
const handleSelect = useCallback(
(item: ApiFeaturedTagJSON) => {
void dispatch(clearSearch());
void dispatch(addFeaturedTag({ name: item.name }));
},
[dispatch],
);
return (
<Combobox
value={query}
onChange={handleSearchChange}
placeholder={intl.formatMessage({
id: 'account_edit_tags.search_placeholder',
defaultMessage: 'Enter a hashtag…',
})}
items={results ?? []}
isLoading={isLoading}
renderItem={renderItem}
onSelectItem={handleSelect}
className={classes.autoComplete}
icon={SearchIcon}
type='search'
/>
);
};
const renderItem = (item: ApiFeaturedTagJSON) => <p>#{item.name}</p>;

View File

@@ -0,0 +1,117 @@
import { useCallback, useEffect } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import type { ApiFeaturedTagJSON } from '@/flavours/glitch/api_types/tags';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import { Tag } from '@/flavours/glitch/components/tags/tag';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import {
addFeaturedTag,
deleteFeaturedTag,
fetchFeaturedTags,
fetchSuggestedTags,
} from '@/flavours/glitch/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
import { AccountEditItemList } from './components/item_list';
import { AccountEditTagSearch } from './components/tag_search';
import classes from './styles.module.scss';
const messages = defineMessages({
columnTitle: {
id: 'account_edit_tags.column_title',
defaultMessage: 'Edit featured hashtags',
},
});
export const AccountEditFeaturedTags: FC = () => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
const { tags, tagSuggestions, isLoading, isPending } = useAppSelector(
(state) => state.profileEdit,
);
const dispatch = useAppDispatch();
useEffect(() => {
void dispatch(fetchFeaturedTags());
void dispatch(fetchSuggestedTags());
}, [dispatch]);
const handleDeleteTag = useCallback(
({ id }: { id: string }) => {
void dispatch(deleteFeaturedTag({ tagId: id }));
},
[dispatch],
);
if (!accountId || !account) {
return <AccountEditEmptyColumn notFound={!accountId} />;
}
return (
<AccountEditColumn
title={intl.formatMessage(messages.columnTitle)}
to='/profile/edit'
>
<div className={classes.wrapper}>
<FormattedMessage
id='account_edit_tags.help_text'
defaultMessage='Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile pages Activity view.'
tagName='p'
/>
<AccountEditTagSearch />
{tagSuggestions.length > 0 && (
<div className={classes.tagSuggestions}>
<FormattedMessage
id='account_edit_tags.suggestions'
defaultMessage='Suggestions:'
/>
{tagSuggestions.map((tag) => (
<SuggestedTag name={tag.name} key={tag.id} disabled={isPending} />
))}
</div>
)}
{isLoading && <LoadingIndicator />}
<AccountEditItemList
items={tags}
disabled={isPending}
renderItem={renderTag}
onDelete={handleDeleteTag}
/>
</div>
</AccountEditColumn>
);
};
function renderTag(tag: ApiFeaturedTagJSON) {
return (
<div className={classes.tagItem}>
<h4>#{tag.name}</h4>
{tag.statuses_count > 0 && (
<FormattedMessage
id='account_edit_tags.tag_status_count'
defaultMessage='{count} posts'
values={{ count: tag.statuses_count }}
tagName='p'
/>
)}
</div>
);
}
const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({
name,
disabled,
}) => {
const dispatch = useAppDispatch();
const handleAddTag = useCallback(() => {
void dispatch(addFeaturedTag({ name }));
}, [dispatch, name]);
return <Tag name={name} onClick={handleAddTag} disabled={disabled} />;
};

View File

@@ -1,28 +1,31 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import type { ModalType } from '@/flavours/glitch/actions/modal';
import { openModal } from '@/flavours/glitch/actions/modal';
import { AccountBio } from '@/flavours/glitch/components/account_bio';
import { Avatar } from '@/flavours/glitch/components/avatar';
import { Column } from '@/flavours/glitch/components/column';
import { ColumnHeader } from '@/flavours/glitch/components/column_header';
import { DisplayNameSimple } from '@/flavours/glitch/components/display_name/simple';
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { autoPlayGif } from '@/flavours/glitch/initial_state';
import { useAppDispatch } from '@/flavours/glitch/store';
import { fetchFeaturedTags } from '@/flavours/glitch/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
import { EditButton } from './components/edit_button';
import { AccountEditSection } from './components/section';
import classes from './styles.module.scss';
const messages = defineMessages({
columnTitle: {
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
},
displayNameTitle: {
id: 'account_edit.display_name.title',
defaultMessage: 'Display name',
@@ -58,6 +61,10 @@ const messages = defineMessages({
defaultMessage:
'Help others identify, and have quick access to, your favorite topics.',
},
featuredHashtagsItem: {
id: 'account_edit.featured_hashtags.item',
defaultMessage: 'hashtags',
},
profileTabTitle: {
id: 'account_edit.profile_tab.title',
defaultMessage: 'Profile tab settings',
@@ -68,12 +75,20 @@ const messages = defineMessages({
},
});
export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
export const AccountEdit: FC = () => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
const dispatch = useAppDispatch();
const { tags: featuredTags, isLoading: isTagsLoading } = useAppSelector(
(state) => state.profileEdit,
);
useEffect(() => {
void dispatch(fetchFeaturedTags());
}, [dispatch]);
const handleOpenModal = useCallback(
(type: ModalType, props?: Record<string, unknown>) => {
dispatch(openModal({ modalType: type, modalProps: props ?? {} }));
@@ -87,38 +102,25 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
handleOpenModal('ACCOUNT_EDIT_BIO');
}, [handleOpenModal]);
if (!accountId) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
const history = useHistory();
const handleFeaturedTagsEdit = useCallback(() => {
history.push('/profile/featured_tags');
}, [history]);
if (!account) {
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
if (!accountId || !account) {
return <AccountEditEmptyColumn notFound={!accountId} />;
}
const headerSrc = autoPlayGif ? account.header : account.header_static;
const hasName = !!account.display_name;
const hasBio = !!account.note_plain;
const hasTags = !isTagsLoading && featuredTags.length > 0;
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={intl.formatMessage({
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
})}
className={classes.columnHeader}
showBackButton
extraButton={
<Link to={`/@${account.acct}`} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
<AccountEditColumn
title={intl.formatMessage(messages.columnTitle)}
to={`/@${account.acct}`}
>
<header>
<div className={classes.profileImage}>
{headerSrc && <img src={headerSrc} alt='' />}
@@ -129,8 +131,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<AccountEditSection
title={messages.displayNameTitle}
description={messages.displayNamePlaceholder}
showDescription={account.display_name.length === 0}
onEdit={handleNameEdit}
showDescription={!hasName}
buttons={
<EditButton
onClick={handleNameEdit}
item={messages.displayNameTitle}
edit={hasName}
/>
}
>
<DisplayNameSimple account={account} />
</AccountEditSection>
@@ -138,8 +146,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<AccountEditSection
title={messages.bioTitle}
description={messages.bioPlaceholder}
showDescription={!account.note_plain}
onEdit={handleBioEdit}
showDescription={!hasBio}
buttons={
<EditButton
onClick={handleBioEdit}
item={messages.bioTitle}
edit={hasBio}
/>
}
>
<AccountBio accountId={accountId} />
</AccountEditSection>
@@ -153,14 +167,23 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<AccountEditSection
title={messages.featuredHashtagsTitle}
description={messages.featuredHashtagsPlaceholder}
showDescription
/>
showDescription={!hasTags}
buttons={
<EditButton
onClick={handleFeaturedTagsEdit}
edit={hasTags}
item={messages.featuredHashtagsItem}
/>
}
>
{featuredTags.map((tag) => `#${tag.name}`).join(', ')}
</AccountEditSection>
<AccountEditSection
title={messages.profileTabTitle}
description={messages.profileTabSubtitle}
showDescription
/>
</Column>
</AccountEditColumn>
);
};

View File

@@ -1,15 +1,4 @@
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.columnHeader {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
// Profile Edit Page
.profileImage {
height: 120px;
@@ -35,40 +24,41 @@
border: 1px solid var(--color-border-primary);
}
.section {
padding: 20px;
border-bottom: 1px solid var(--color-border-primary);
font-size: 15px;
// Featured Tags Page
.wrapper {
padding: 24px;
}
.sectionHeader {
.autoComplete,
.tagSuggestions {
margin: 12px 0;
}
.tagSuggestions {
display: flex;
gap: 4px;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 8px;
> button {
border: 1px solid var(--color-border-primary);
border-radius: 8px;
box-sizing: border-box;
padding: 4px;
svg {
width: 20px;
height: 20px;
}
// Add more padding to the suggestions label
> span {
margin-right: 4px;
}
}
.sectionTitle {
flex-grow: 1;
font-size: 17px;
font-weight: 600;
.tagItem {
> h4 {
font-size: 15px;
font-weight: 500;
}
> p {
color: var(--color-text-secondary);
}
}
.sectionSubtitle {
color: var(--color-text-secondary);
}
// Modals
.inputWrapper {
position: relative;
@@ -100,6 +90,104 @@ textarea.inputText {
}
}
// Column component
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.columnHeader {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
// Edit button component
.editButton {
border: 1px solid var(--color-border-primary);
border-radius: 8px;
box-sizing: border-box;
padding: 4px;
transition:
color 0.2s ease-in-out,
background-color 0.2s ease-in-out;
&:global(.button) {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
padding: 4px 8px;
&:active,
&:focus,
&:hover {
background-color: var(--color-bg-brand-softer);
}
}
svg {
width: 20px;
height: 20px;
}
}
.deleteButton {
--default-icon-color: var(--color-text-error);
--hover-bg-color: var(--color-bg-error-base-hover);
--hover-icon-color: var(--color-text-on-error-base);
}
// Item list component
.itemList {
> li {
display: flex;
align-items: center;
padding: 12px 0;
> :first-child {
flex-grow: 1;
}
}
}
.itemListButtons {
display: flex;
align-items: center;
gap: 4px;
}
// Section component
.section {
padding: 20px;
border-bottom: 1px solid var(--color-border-primary);
font-size: 15px;
}
.sectionHeader {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.sectionTitle {
flex-grow: 1;
font-size: 17px;
font-weight: 600;
}
.sectionSubtitle {
color: var(--color-text-secondary);
}
// Counter component
.counter {
margin-top: 4px;
font-size: 13px;

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useId, useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
@@ -18,10 +18,7 @@ import { Button } from 'flavours/glitch/components/button';
import { Callout } from 'flavours/glitch/components/callout';
import { DisplayName } from 'flavours/glitch/components/display_name';
import { EmptyState } from 'flavours/glitch/components/empty_state';
import {
FormStack,
ComboboxField,
} from 'flavours/glitch/components/form_fields';
import { FormStack, Combobox } from 'flavours/glitch/components/form_fields';
import { Icon } from 'flavours/glitch/components/icon';
import { IconButton } from 'flavours/glitch/components/icon_button';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
@@ -331,6 +328,12 @@ export const CollectionAccounts: React.FC<{
[canSubmit, id, history, accountIds],
);
const inputId = useId();
const inputLabel = intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for accounts to add…',
});
return (
<form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}>
@@ -351,21 +354,12 @@ export const CollectionAccounts: React.FC<{
}
/>
)}
<ComboboxField
label={
<FormattedMessage
id='collections.search_accounts_label'
defaultMessage='Search for accounts to add…'
/>
}
hint={
hasMaxAccounts ? (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
) : undefined
}
<label htmlFor={inputId} className='sr-only'>
{inputLabel}
</label>
<Combobox
id={inputId}
placeholder={inputLabel}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
@@ -379,6 +373,12 @@ export const CollectionAccounts: React.FC<{
isEditMode ? instantToggleAccountItem : toggleAccountItem
}
/>
{hasMaxAccounts && (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
)}
{hasMinAccounts && (
<Callout>

View File

@@ -85,6 +85,7 @@ import {
AccountFeatured,
AccountAbout,
AccountEdit,
AccountEditFeaturedTags,
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
@@ -172,9 +173,8 @@ class SwitchingColumnsArea extends PureComponent {
redirect = <Redirect from='/' to='/about' exact />;
}
const profileRedesignEnabled = isServerFeatureEnabled('profile_redesign');
const profileRedesignRoutes = [];
if (profileRedesignEnabled) {
if (isServerFeatureEnabled('profile_redesign')) {
profileRedesignRoutes.push(
<WrappedRoute key="posts" path={['/@:acct/posts', '/accounts/:id/posts']} exact component={AccountTimeline} content={children} />,
);
@@ -196,13 +196,27 @@ class SwitchingColumnsArea extends PureComponent {
);
}
} else {
// If the redesign is not enabled but someone shares an /about link, redirect to the root.
profileRedesignRoutes.push(
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />,
// If the redesign is not enabled but someone shares an /about link, redirect to the root.
<Redirect key="about-acct-redirect" from='/@:acct/about' to='/@:acct' exact />,
<Redirect key="about-id-redirect" from='/accounts/:id/about' to='/accounts/:id' exact />
);
}
if (isClientFeatureEnabled('profile_editing')) {
profileRedesignRoutes.push(
<WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />,
<WrappedRoute key="featured_tags" path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
)
} else {
// If profile editing is not enabled, redirect to the home timeline as the current editing pages are outside React Router.
profileRedesignRoutes.push(
<Redirect key="edit-redirect" from='/profile/edit' to='/' exact />,
<Redirect key="featured-tags-redirect" from='/profile/featured_tags' to='/' exact />,
);
}
return (
<ColumnsContextProvider multiColumn={!singleColumn}>
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
@@ -242,8 +256,6 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
{isClientFeatureEnabled('profile_editing') && <WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />}
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />
@@ -251,8 +263,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
{!profileRedesignEnabled && <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />}
{...profileRedesignRoutes}
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />

View File

@@ -103,6 +103,11 @@ export function AccountEdit() {
.then((module) => ({ default: module.AccountEdit }));
}
export function AccountEditFeaturedTags() {
return import('../../account_edit/featured_tags')
.then((module) => ({ default: module.AccountEditFeaturedTags }));
}
export function Followers () {
return import('../../followers');
}

View File

@@ -1,7 +1,9 @@
import { annualReport } from './annual_report';
import { collections } from './collections';
import { profileEdit } from './profile_edit';
export const sliceReducers = {
annualReport,
collections,
profileEdit,
};

View File

@@ -0,0 +1,178 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { debounce } from 'lodash';
import {
apiDeleteFeaturedTag,
apiGetCurrentFeaturedTags,
apiGetTagSuggestions,
apiPostFeaturedTag,
} from '@/flavours/glitch/api/accounts';
import { apiGetSearch } from '@/flavours/glitch/api/search';
import { hashtagToFeaturedTag } from '@/flavours/glitch/api_types/tags';
import type { ApiFeaturedTagJSON } from '@/flavours/glitch/api_types/tags';
import type { AppDispatch } from '@/flavours/glitch/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from '@/flavours/glitch/store/typed_functions';
interface ProfileEditState {
tags: ApiFeaturedTagJSON[];
tagSuggestions: ApiFeaturedTagJSON[];
isLoading: boolean;
isPending: boolean;
search: {
query: string;
isLoading: boolean;
results?: ApiFeaturedTagJSON[];
};
}
const initialState: ProfileEditState = {
tags: [],
tagSuggestions: [],
isLoading: true,
isPending: false,
search: {
query: '',
isLoading: false,
},
};
const profileEditSlice = createSlice({
name: 'profileEdit',
initialState,
reducers: {
setSearchQuery(state, action: PayloadAction<string>) {
if (state.search.query === action.payload) {
return;
}
state.search.query = action.payload;
state.search.isLoading = false;
state.search.results = undefined;
},
clearSearch(state) {
state.search.query = '';
state.search.isLoading = false;
state.search.results = undefined;
},
},
extraReducers(builder) {
builder.addCase(fetchSuggestedTags.fulfilled, (state, action) => {
state.tagSuggestions = action.payload.map(hashtagToFeaturedTag);
state.isLoading = false;
});
builder.addCase(fetchFeaturedTags.fulfilled, (state, action) => {
state.tags = action.payload;
state.isLoading = false;
});
builder.addCase(addFeaturedTag.pending, (state) => {
state.isPending = true;
});
builder.addCase(addFeaturedTag.rejected, (state) => {
state.isPending = false;
});
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
state.tags = [...state.tags, action.payload].toSorted(
(a, b) => b.statuses_count - a.statuses_count,
);
state.tagSuggestions = state.tagSuggestions.filter(
(tag) => tag.name !== action.meta.arg.name,
);
state.isPending = false;
});
builder.addCase(deleteFeaturedTag.pending, (state) => {
state.isPending = true;
});
builder.addCase(deleteFeaturedTag.rejected, (state) => {
state.isPending = false;
});
builder.addCase(deleteFeaturedTag.fulfilled, (state, action) => {
state.tags = state.tags.filter((tag) => tag.id !== action.meta.arg.tagId);
state.isPending = false;
});
builder.addCase(fetchSearchResults.pending, (state) => {
state.search.isLoading = true;
});
builder.addCase(fetchSearchResults.rejected, (state) => {
state.search.isLoading = false;
state.search.results = undefined;
});
builder.addCase(fetchSearchResults.fulfilled, (state, action) => {
state.search.isLoading = false;
const searchResults: ApiFeaturedTagJSON[] = [];
const currentTags = new Set(state.tags.map((tag) => tag.name));
for (const tag of action.payload) {
if (currentTags.has(tag.name)) {
continue;
}
searchResults.push(hashtagToFeaturedTag(tag));
if (searchResults.length >= 10) {
break;
}
}
state.search.results = searchResults;
});
},
});
export const profileEdit = profileEditSlice.reducer;
export const { clearSearch } = profileEditSlice.actions;
export const fetchFeaturedTags = createDataLoadingThunk(
`${profileEditSlice.name}/fetchFeaturedTags`,
apiGetCurrentFeaturedTags,
{ useLoadingBar: false },
);
export const fetchSuggestedTags = createDataLoadingThunk(
`${profileEditSlice.name}/fetchSuggestedTags`,
apiGetTagSuggestions,
{ useLoadingBar: false },
);
export const addFeaturedTag = createDataLoadingThunk(
`${profileEditSlice.name}/addFeaturedTag`,
({ name }: { name: string }) => apiPostFeaturedTag(name),
{
condition(arg, { getState }) {
const state = getState();
return !state.profileEdit.tags.some((tag) => tag.name === arg.name);
},
},
);
export const deleteFeaturedTag = createDataLoadingThunk(
`${profileEditSlice.name}/deleteFeaturedTag`,
({ tagId }: { tagId: string }) => apiDeleteFeaturedTag(tagId),
);
const debouncedFetchSearchResults = debounce(
async (dispatch: AppDispatch, query: string) => {
await dispatch(fetchSearchResults({ q: query }));
},
300,
);
export const updateSearchQuery = createAppAsyncThunk(
`${profileEditSlice.name}/updateSearchQuery`,
(query: string, { dispatch }) => {
dispatch(profileEditSlice.actions.setSearchQuery(query));
if (query.trim().length > 0) {
void debouncedFetchSearchResults(dispatch, query);
}
},
);
export const fetchSearchResults = createDataLoadingThunk(
`${profileEditSlice.name}/fetchSearchResults`,
({ q }: { q: string }) => apiGetSearch({ q, type: 'hashtags', limit: 11 }),
(result) => result.hashtags,
);

View File

@@ -77,7 +77,8 @@ const initialState = ImmutableMap({
follow_requests: initialListState,
blocks: initialListState,
mutes: initialListState,
featured_tags: initialListState,
/** @type {ImmutableMap<string, typeof initialListState>} */
featured_tags: ImmutableMap(),
});
const normalizeList = (state, path, accounts, next) => {

View File

@@ -377,6 +377,15 @@ $content-width: 840px;
}
}
details > summary {
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
color: var(--color-text-secondary);
padding-top: 24px;
margin-bottom: 8px;
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;

View File

@@ -1733,7 +1733,7 @@ body > [data-popper-placement] {
.detailed-status__display-name {
color: var(--color-text-tertiary);
span {
span:not(.account__avatar) {
display: inline;
}

View File

@@ -1,10 +1,13 @@
import { apiRequestPost, apiRequestGet } from 'mastodon/api';
import { apiRequestPost, apiRequestGet, apiRequestDelete } from 'mastodon/api';
import type {
ApiAccountJSON,
ApiFamiliarFollowersJSON,
} from 'mastodon/api_types/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import type {
ApiFeaturedTagJSON,
ApiHashtagJSON,
} from 'mastodon/api_types/tags';
export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
@@ -30,7 +33,19 @@ export const apiRemoveAccountFromFollowers = (id: string) =>
);
export const apiGetFeaturedTags = (id: string) =>
apiRequestGet<ApiHashtagJSON>(`v1/accounts/${id}/featured_tags`);
apiRequestGet<ApiHashtagJSON[]>(`v1/accounts/${id}/featured_tags`);
export const apiGetCurrentFeaturedTags = () =>
apiRequestGet<ApiFeaturedTagJSON[]>(`v1/featured_tags`);
export const apiPostFeaturedTag = (name: string) =>
apiRequestPost<ApiFeaturedTagJSON>('v1/featured_tags', { name });
export const apiDeleteFeaturedTag = (id: string) =>
apiRequestDelete(`v1/featured_tags/${id}`);
export const apiGetTagSuggestions = () =>
apiRequestGet<ApiHashtagJSON[]>('v1/featured_tags/suggestions');
export const apiGetEndorsedAccounts = (id: string) =>
apiRequestGet<ApiAccountJSON>(`v1/accounts/${id}/endorsements`);

View File

@@ -4,11 +4,29 @@ interface ApiHistoryJSON {
uses: string;
}
export interface ApiHashtagJSON {
interface ApiHashtagBase {
id: string;
name: string;
url: string;
}
export interface ApiHashtagJSON extends ApiHashtagBase {
history: [ApiHistoryJSON, ...ApiHistoryJSON[]];
following?: boolean;
featuring?: boolean;
}
export interface ApiFeaturedTagJSON extends ApiHashtagBase {
statuses_count: number;
last_status_at: string | null;
}
export function hashtagToFeaturedTag(tag: ApiHashtagJSON): ApiFeaturedTagJSON {
return {
id: tag.id,
name: tag.name,
url: tag.url,
statuses_count: 0,
last_status_at: null,
};
}

View File

@@ -3,7 +3,7 @@
}
.input {
padding-right: 45px;
padding-inline-end: 45px;
}
.menuButton {

View File

@@ -82,11 +82,23 @@ const ComboboxDemo: React.FC = () => {
const meta = {
title: 'Components/Form Fields/ComboboxField',
component: ComboboxDemo,
} satisfies Meta<typeof ComboboxDemo>;
component: ComboboxField,
render: () => <ComboboxDemo />,
} satisfies Meta<typeof ComboboxField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Example: Story = {};
export const Example: Story = {
args: {
// Adding these types to keep TS happy, they're not passed on to `ComboboxDemo`
label: '',
value: '',
onChange: () => undefined,
items: [],
getItemId: () => '',
renderItem: () => <>Nothing</>,
onSelectItem: () => undefined,
},
};

View File

@@ -1,4 +1,3 @@
import type { ComponentPropsWithoutRef } from 'react';
import { forwardRef, useCallback, useId, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
@@ -9,6 +8,7 @@ import Overlay from 'react-overlays/Overlay';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { matchWidth } from 'mastodon/components/dropdown/utils';
import { IconButton } from 'mastodon/components/icon_button';
import { useOnClickOutside } from 'mastodon/hooks/useOnClickOutside';
@@ -17,6 +17,7 @@ import classes from './combobox.module.scss';
import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper';
import { TextInput } from './text_input_field';
import type { TextInputProps } from './text_input_field';
interface ComboboxItem {
id: string;
@@ -27,17 +28,45 @@ export interface ComboboxItemState {
isDisabled: boolean;
}
interface ComboboxProps<
T extends ComboboxItem,
> extends ComponentPropsWithoutRef<'input'> {
interface ComboboxProps<T extends ComboboxItem> extends TextInputProps {
/**
* The value of the combobox's text input
*/
value: string;
/**
* Change handler for the text input field
*/
onChange: React.ChangeEventHandler<HTMLInputElement>;
/**
* Set this to true when the list of options is dynamic and currently loading.
* Causes a loading indicator to be displayed inside of the dropdown menu.
*/
isLoading?: boolean;
/**
* The set of options/suggestions that should be rendered in the dropdown menu.
*/
items: T[];
getItemId: (item: T) => string;
/**
* A function that must return a unique id for each option passed via `items`
*/
getItemId?: (item: T) => string;
/**
* Providing this function turns the combobox into a multi-select box that assumes
* multiple options to be selectable. Single-selection is handled automatically.
*/
getIsItemSelected?: (item: T) => boolean;
/**
* Use this function to mark items as disabled, if needed
*/
getIsItemDisabled?: (item: T) => boolean;
/**
* Customise the rendering of each option.
* The rendered content must not contain other interactive content!
*/
renderItem: (item: T, state: ComboboxItemState) => React.ReactElement;
/**
* The main selection handler, called when an option is selected or deselected.
*/
onSelectItem: (item: T) => void;
}
@@ -45,8 +74,12 @@ interface Props<T extends ComboboxItem>
extends ComboboxProps<T>, CommonFieldWrapperProps {}
/**
* The combobox field allows users to select one or multiple items
* from a large list of options by searching or filtering.
* The combobox field allows users to select one or more items
* by searching or filtering a large or dynamic list of options.
*
* It is an implementation of the [APG Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/),
* with inspiration taken from Sarah Higley's extensive combobox
* [research & implementations](https://sarahmhigley.com/writing/select-your-poison/).
*/
export const ComboboxFieldWithRef = <T extends ComboboxItem>(
@@ -80,7 +113,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
value,
isLoading = false,
items,
getItemId,
getItemId = (item) => item.id,
getIsItemDisabled,
getIsItemSelected,
disabled,
@@ -88,6 +121,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
onSelectItem,
onChange,
onKeyDown,
icon = SearchIcon,
className,
...otherProps
}: ComboboxProps<T>,
@@ -306,6 +340,7 @@ const ComboboxWithRef = <T extends ComboboxItem>(
value={value}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
icon={icon}
className={classNames(classes.input, className)}
ref={mergeRefs}
/>

View File

@@ -20,6 +20,15 @@
font-size: 16px;
}
.iconWrapper & {
// Make space for icon displayed at start of input
padding-inline-start: 36px;
}
&::placeholder {
color: var(--color-text-secondary);
}
&:focus {
outline-color: var(--color-text-brand);
}
@@ -40,3 +49,17 @@
cursor: not-allowed;
}
}
.iconWrapper {
position: relative;
}
.icon {
pointer-events: none;
position: absolute;
width: 22px;
height: 22px;
inset-inline-start: 10px;
inset-block-start: 10px;
color: var(--color-text-secondary);
}

View File

@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import { TextInputField, TextInput } from './text_input_field';
const meta = {
@@ -42,6 +44,14 @@ export const WithError: Story = {
},
};
export const WithIcon: Story = {
args: {
label: 'Search',
hint: undefined,
icon: SearchIcon,
},
};
export const Plain: Story = {
render(args) {
return <TextInput {...args} />;

View File

@@ -3,12 +3,18 @@ import { forwardRef } from 'react';
import classNames from 'classnames';
import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
import { FormFieldWrapper } from './form_field_wrapper';
import type { CommonFieldWrapperProps } from './form_field_wrapper';
import classes from './text_input.module.scss';
interface Props
extends ComponentPropsWithoutRef<'input'>, CommonFieldWrapperProps {}
export interface TextInputProps extends ComponentPropsWithoutRef<'input'> {
icon?: IconProp;
}
interface Props extends TextInputProps, CommonFieldWrapperProps {}
/**
* A simple form field for single-line text.
@@ -33,16 +39,33 @@ export const TextInputField = forwardRef<HTMLInputElement, Props>(
TextInputField.displayName = 'TextInputField';
export const TextInput = forwardRef<
HTMLInputElement,
ComponentPropsWithoutRef<'input'>
>(({ type = 'text', className, ...otherProps }, ref) => (
<input
type={type}
{...otherProps}
className={classNames(className, classes.input)}
ref={ref}
/>
));
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
({ type = 'text', icon, className, ...otherProps }, ref) => (
<WrapFieldWithIcon icon={icon}>
<input
type={type}
{...otherProps}
className={classNames(className, classes.input)}
ref={ref}
/>
</WrapFieldWithIcon>
),
);
TextInput.displayName = 'TextInput';
const WrapFieldWithIcon: React.FC<{
icon?: IconProp;
children: React.ReactElement;
}> = ({ icon, children }) => {
if (icon) {
return (
<div className={classes.iconWrapper}>
<Icon icon={icon} id='input-icon' className={classes.icon} />
{children}
</div>
);
}
return children;
};

View File

@@ -0,0 +1,57 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { Column } from '@/mastodon/components/column';
import { ColumnHeader } from '@/mastodon/components/column_header';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { useColumnsContext } from '../../ui/util/columns_context';
import classes from '../styles.module.scss';
export const AccountEditEmptyColumn: FC<{
notFound?: boolean;
}> = ({ notFound }) => {
const { multiColumn } = useColumnsContext();
if (notFound) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
};
export const AccountEditColumn: FC<{
title: string;
to: string;
children: React.ReactNode;
}> = ({ to, title, children }) => {
const { multiColumn } = useColumnsContext();
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={title}
className={classes.columnHeader}
showBackButton
extraButton={
<Link to={to} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
{children}
</Column>
);
};

View File

@@ -0,0 +1,100 @@
import type { FC, MouseEventHandler } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { Button } from '@/mastodon/components/button';
import { IconButton } from '@/mastodon/components/icon_button';
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import classes from '../styles.module.scss';
const messages = defineMessages({
add: {
id: 'account_edit.button.add',
defaultMessage: 'Add {item}',
},
edit: {
id: 'account_edit.button.edit',
defaultMessage: 'Edit {item}',
},
delete: {
id: 'account_edit.button.delete',
defaultMessage: 'Delete {item}',
},
});
export interface EditButtonProps {
onClick: MouseEventHandler;
item: string | MessageDescriptor;
edit?: boolean;
icon?: boolean;
disabled?: boolean;
}
export const EditButton: FC<EditButtonProps> = ({
onClick,
item,
edit = false,
icon = edit,
disabled,
}) => {
const intl = useIntl();
const itemText = typeof item === 'string' ? item : intl.formatMessage(item);
const label = intl.formatMessage(messages[edit ? 'edit' : 'add'], {
item: itemText,
});
if (icon) {
return (
<EditIconButton title={label} onClick={onClick} disabled={disabled} />
);
}
return (
<Button
className={classes.editButton}
onClick={onClick}
disabled={disabled}
>
{label}
</Button>
);
};
export const EditIconButton: FC<{
onClick: MouseEventHandler;
title: string;
disabled?: boolean;
}> = ({ title, onClick, disabled }) => (
<IconButton
icon='pencil'
iconComponent={EditIcon}
onClick={onClick}
className={classes.editButton}
title={title}
disabled={disabled}
/>
);
export const DeleteIconButton: FC<{
onClick: MouseEventHandler;
item: string;
disabled?: boolean;
}> = ({ onClick, item, disabled }) => {
const intl = useIntl();
return (
<IconButton
icon='delete'
iconComponent={DeleteIcon}
onClick={onClick}
className={classNames(classes.editButton, classes.deleteButton)}
title={intl.formatMessage(messages.delete, { item })}
disabled={disabled}
/>
);
};

View File

@@ -0,0 +1,89 @@
import { useCallback } from 'react';
import classes from '../styles.module.scss';
import { DeleteIconButton, EditButton } from './edit_button';
interface AnyItem {
id: string;
name: string;
}
interface AccountEditItemListProps<Item extends AnyItem = AnyItem> {
renderItem?: (item: Item) => React.ReactNode;
items: Item[];
onEdit?: (item: Item) => void;
onDelete?: (item: Item) => void;
disabled?: boolean;
}
export const AccountEditItemList = <Item extends AnyItem>({
renderItem,
items,
onEdit,
onDelete,
disabled,
}: AccountEditItemListProps<Item>) => {
if (items.length === 0) {
return null;
}
return (
<ul className={classes.itemList}>
{items.map((item) => (
<li key={item.id}>
<span>{renderItem?.(item) ?? item.name}</span>
<AccountEditItemButtons
item={item}
onEdit={onEdit}
onDelete={onDelete}
disabled={disabled}
/>
</li>
))}
</ul>
);
};
type AccountEditItemButtonsProps<Item extends AnyItem = AnyItem> = Pick<
AccountEditItemListProps<Item>,
'onEdit' | 'onDelete' | 'disabled'
> & { item: Item };
const AccountEditItemButtons = <Item extends AnyItem>({
item,
onDelete,
onEdit,
disabled,
}: AccountEditItemButtonsProps<Item>) => {
const handleEdit = useCallback(() => {
onEdit?.(item);
}, [item, onEdit]);
const handleDelete = useCallback(() => {
onDelete?.(item);
}, [item, onDelete]);
if (!onEdit && !onDelete) {
return null;
}
return (
<div className={classes.itemListButtons}>
{onEdit && (
<EditButton
edit
item={item.name}
disabled={disabled}
onClick={handleEdit}
/>
)}
{onDelete && (
<DeleteIconButton
item={item.name}
disabled={disabled}
onClick={handleDelete}
/>
)}
</div>
);
};

View File

@@ -1,55 +1,36 @@
import type { FC, ReactNode } from 'react';
import type { MessageDescriptor } from 'react-intl';
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { IconButton } from '@/mastodon/components/icon_button';
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import classes from '../styles.module.scss';
const buttonMessage = defineMessage({
id: 'account_edit.section_edit_button',
defaultMessage: 'Edit',
});
interface AccountEditSectionProps {
title: MessageDescriptor;
description?: MessageDescriptor;
showDescription?: boolean;
onEdit?: () => void;
children?: ReactNode;
className?: string;
extraButtons?: ReactNode;
buttons?: ReactNode;
}
export const AccountEditSection: FC<AccountEditSectionProps> = ({
title,
description,
showDescription,
onEdit,
children,
className,
extraButtons,
buttons,
}) => {
const intl = useIntl();
return (
<section className={classNames(className, classes.section)}>
<header className={classes.sectionHeader}>
<h3 className={classes.sectionTitle}>
<FormattedMessage {...title} />
</h3>
{onEdit && (
<IconButton
icon='pencil'
iconComponent={EditIcon}
onClick={onEdit}
title={`${intl.formatMessage(buttonMessage)} ${intl.formatMessage(title)}`}
/>
)}
{extraButtons}
{buttons}
</header>
{showDescription && (
<p className={classes.sectionSubtitle}>

View File

@@ -0,0 +1,60 @@
import type { ChangeEventHandler, FC } from 'react';
import { useCallback } from 'react';
import { useIntl } from 'react-intl';
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
import { Combobox } from '@/mastodon/components/form_fields';
import {
addFeaturedTag,
clearSearch,
updateSearchQuery,
} from '@/mastodon/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
import classes from '../styles.module.scss';
export const AccountEditTagSearch: FC = () => {
const { query, isLoading, results } = useAppSelector(
(state) => state.profileEdit.search,
);
const dispatch = useAppDispatch();
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
void dispatch(updateSearchQuery(e.target.value));
},
[dispatch],
);
const intl = useIntl();
const handleSelect = useCallback(
(item: ApiFeaturedTagJSON) => {
void dispatch(clearSearch());
void dispatch(addFeaturedTag({ name: item.name }));
},
[dispatch],
);
return (
<Combobox
value={query}
onChange={handleSearchChange}
placeholder={intl.formatMessage({
id: 'account_edit_tags.search_placeholder',
defaultMessage: 'Enter a hashtag…',
})}
items={results ?? []}
isLoading={isLoading}
renderItem={renderItem}
onSelectItem={handleSelect}
className={classes.autoComplete}
icon={SearchIcon}
type='search'
/>
);
};
const renderItem = (item: ApiFeaturedTagJSON) => <p>#{item.name}</p>;

View File

@@ -0,0 +1,117 @@
import { useCallback, useEffect } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { Tag } from '@/mastodon/components/tags/tag';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import {
addFeaturedTag,
deleteFeaturedTag,
fetchFeaturedTags,
fetchSuggestedTags,
} from '@/mastodon/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
import { AccountEditItemList } from './components/item_list';
import { AccountEditTagSearch } from './components/tag_search';
import classes from './styles.module.scss';
const messages = defineMessages({
columnTitle: {
id: 'account_edit_tags.column_title',
defaultMessage: 'Edit featured hashtags',
},
});
export const AccountEditFeaturedTags: FC = () => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
const { tags, tagSuggestions, isLoading, isPending } = useAppSelector(
(state) => state.profileEdit,
);
const dispatch = useAppDispatch();
useEffect(() => {
void dispatch(fetchFeaturedTags());
void dispatch(fetchSuggestedTags());
}, [dispatch]);
const handleDeleteTag = useCallback(
({ id }: { id: string }) => {
void dispatch(deleteFeaturedTag({ tagId: id }));
},
[dispatch],
);
if (!accountId || !account) {
return <AccountEditEmptyColumn notFound={!accountId} />;
}
return (
<AccountEditColumn
title={intl.formatMessage(messages.columnTitle)}
to='/profile/edit'
>
<div className={classes.wrapper}>
<FormattedMessage
id='account_edit_tags.help_text'
defaultMessage='Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile pages Activity view.'
tagName='p'
/>
<AccountEditTagSearch />
{tagSuggestions.length > 0 && (
<div className={classes.tagSuggestions}>
<FormattedMessage
id='account_edit_tags.suggestions'
defaultMessage='Suggestions:'
/>
{tagSuggestions.map((tag) => (
<SuggestedTag name={tag.name} key={tag.id} disabled={isPending} />
))}
</div>
)}
{isLoading && <LoadingIndicator />}
<AccountEditItemList
items={tags}
disabled={isPending}
renderItem={renderTag}
onDelete={handleDeleteTag}
/>
</div>
</AccountEditColumn>
);
};
function renderTag(tag: ApiFeaturedTagJSON) {
return (
<div className={classes.tagItem}>
<h4>#{tag.name}</h4>
{tag.statuses_count > 0 && (
<FormattedMessage
id='account_edit_tags.tag_status_count'
defaultMessage='{count} posts'
values={{ count: tag.statuses_count }}
tagName='p'
/>
)}
</div>
);
}
const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({
name,
disabled,
}) => {
const dispatch = useAppDispatch();
const handleAddTag = useCallback(() => {
void dispatch(addFeaturedTag({ name }));
}, [dispatch, name]);
return <Tag name={name} onClick={handleAddTag} disabled={disabled} />;
};

View File

@@ -1,28 +1,31 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import type { FC } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import type { ModalType } from '@/mastodon/actions/modal';
import { openModal } from '@/mastodon/actions/modal';
import { AccountBio } from '@/mastodon/components/account_bio';
import { Avatar } from '@/mastodon/components/avatar';
import { Column } from '@/mastodon/components/column';
import { ColumnHeader } from '@/mastodon/components/column_header';
import { DisplayNameSimple } from '@/mastodon/components/display_name/simple';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { autoPlayGif } from '@/mastodon/initial_state';
import { useAppDispatch } from '@/mastodon/store';
import { fetchFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
import { EditButton } from './components/edit_button';
import { AccountEditSection } from './components/section';
import classes from './styles.module.scss';
const messages = defineMessages({
columnTitle: {
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
},
displayNameTitle: {
id: 'account_edit.display_name.title',
defaultMessage: 'Display name',
@@ -58,6 +61,10 @@ const messages = defineMessages({
defaultMessage:
'Help others identify, and have quick access to, your favorite topics.',
},
featuredHashtagsItem: {
id: 'account_edit.featured_hashtags.item',
defaultMessage: 'hashtags',
},
profileTabTitle: {
id: 'account_edit.profile_tab.title',
defaultMessage: 'Profile tab settings',
@@ -68,12 +75,20 @@ const messages = defineMessages({
},
});
export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
export const AccountEdit: FC = () => {
const accountId = useCurrentAccountId();
const account = useAccount(accountId);
const intl = useIntl();
const dispatch = useAppDispatch();
const { tags: featuredTags, isLoading: isTagsLoading } = useAppSelector(
(state) => state.profileEdit,
);
useEffect(() => {
void dispatch(fetchFeaturedTags());
}, [dispatch]);
const handleOpenModal = useCallback(
(type: ModalType, props?: Record<string, unknown>) => {
dispatch(openModal({ modalType: type, modalProps: props ?? {} }));
@@ -87,38 +102,25 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
handleOpenModal('ACCOUNT_EDIT_BIO');
}, [handleOpenModal]);
if (!accountId) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
const history = useHistory();
const handleFeaturedTagsEdit = useCallback(() => {
history.push('/profile/featured_tags');
}, [history]);
if (!account) {
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<LoadingIndicator />
</Column>
);
if (!accountId || !account) {
return <AccountEditEmptyColumn notFound={!accountId} />;
}
const headerSrc = autoPlayGif ? account.header : account.header_static;
const hasName = !!account.display_name;
const hasBio = !!account.note_plain;
const hasTags = !isTagsLoading && featuredTags.length > 0;
return (
<Column bindToDocument={!multiColumn} className={classes.column}>
<ColumnHeader
title={intl.formatMessage({
id: 'account_edit.column_title',
defaultMessage: 'Edit Profile',
})}
className={classes.columnHeader}
showBackButton
extraButton={
<Link to={`/@${account.acct}`} className='button'>
<FormattedMessage
id='account_edit.column_button'
defaultMessage='Done'
/>
</Link>
}
/>
<AccountEditColumn
title={intl.formatMessage(messages.columnTitle)}
to={`/@${account.acct}`}
>
<header>
<div className={classes.profileImage}>
{headerSrc && <img src={headerSrc} alt='' />}
@@ -129,8 +131,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<AccountEditSection
title={messages.displayNameTitle}
description={messages.displayNamePlaceholder}
showDescription={account.display_name.length === 0}
onEdit={handleNameEdit}
showDescription={!hasName}
buttons={
<EditButton
onClick={handleNameEdit}
item={messages.displayNameTitle}
edit={hasName}
/>
}
>
<DisplayNameSimple account={account} />
</AccountEditSection>
@@ -138,8 +146,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<AccountEditSection
title={messages.bioTitle}
description={messages.bioPlaceholder}
showDescription={!account.note_plain}
onEdit={handleBioEdit}
showDescription={!hasBio}
buttons={
<EditButton
onClick={handleBioEdit}
item={messages.bioTitle}
edit={hasBio}
/>
}
>
<AccountBio accountId={accountId} />
</AccountEditSection>
@@ -153,14 +167,23 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
<AccountEditSection
title={messages.featuredHashtagsTitle}
description={messages.featuredHashtagsPlaceholder}
showDescription
/>
showDescription={!hasTags}
buttons={
<EditButton
onClick={handleFeaturedTagsEdit}
edit={hasTags}
item={messages.featuredHashtagsItem}
/>
}
>
{featuredTags.map((tag) => `#${tag.name}`).join(', ')}
</AccountEditSection>
<AccountEditSection
title={messages.profileTabTitle}
description={messages.profileTabSubtitle}
showDescription
/>
</Column>
</AccountEditColumn>
);
};

View File

@@ -1,15 +1,4 @@
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.columnHeader {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
// Profile Edit Page
.profileImage {
height: 120px;
@@ -35,40 +24,41 @@
border: 1px solid var(--color-border-primary);
}
.section {
padding: 20px;
border-bottom: 1px solid var(--color-border-primary);
font-size: 15px;
// Featured Tags Page
.wrapper {
padding: 24px;
}
.sectionHeader {
.autoComplete,
.tagSuggestions {
margin: 12px 0;
}
.tagSuggestions {
display: flex;
gap: 4px;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 8px;
> button {
border: 1px solid var(--color-border-primary);
border-radius: 8px;
box-sizing: border-box;
padding: 4px;
svg {
width: 20px;
height: 20px;
}
// Add more padding to the suggestions label
> span {
margin-right: 4px;
}
}
.sectionTitle {
flex-grow: 1;
font-size: 17px;
font-weight: 600;
.tagItem {
> h4 {
font-size: 15px;
font-weight: 500;
}
> p {
color: var(--color-text-secondary);
}
}
.sectionSubtitle {
color: var(--color-text-secondary);
}
// Modals
.inputWrapper {
position: relative;
@@ -100,6 +90,104 @@ textarea.inputText {
}
}
// Column component
.column {
border: 1px solid var(--color-border-primary);
border-top-width: 0;
}
.columnHeader {
:global(.column-header__buttons) {
align-items: center;
padding-inline-end: 16px;
height: auto;
}
}
// Edit button component
.editButton {
border: 1px solid var(--color-border-primary);
border-radius: 8px;
box-sizing: border-box;
padding: 4px;
transition:
color 0.2s ease-in-out,
background-color 0.2s ease-in-out;
&:global(.button) {
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
padding: 4px 8px;
&:active,
&:focus,
&:hover {
background-color: var(--color-bg-brand-softer);
}
}
svg {
width: 20px;
height: 20px;
}
}
.deleteButton {
--default-icon-color: var(--color-text-error);
--hover-bg-color: var(--color-bg-error-base-hover);
--hover-icon-color: var(--color-text-on-error-base);
}
// Item list component
.itemList {
> li {
display: flex;
align-items: center;
padding: 12px 0;
> :first-child {
flex-grow: 1;
}
}
}
.itemListButtons {
display: flex;
align-items: center;
gap: 4px;
}
// Section component
.section {
padding: 20px;
border-bottom: 1px solid var(--color-border-primary);
font-size: 15px;
}
.sectionHeader {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.sectionTitle {
flex-grow: 1;
font-size: 17px;
font-weight: 600;
}
.sectionSubtitle {
color: var(--color-text-secondary);
}
// Counter component
.counter {
margin-top: 4px;
font-size: 13px;

View File

@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useId, useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
@@ -18,7 +18,7 @@ import { Button } from 'mastodon/components/button';
import { Callout } from 'mastodon/components/callout';
import { DisplayName } from 'mastodon/components/display_name';
import { EmptyState } from 'mastodon/components/empty_state';
import { FormStack, ComboboxField } from 'mastodon/components/form_fields';
import { FormStack, Combobox } from 'mastodon/components/form_fields';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
import ScrollableList from 'mastodon/components/scrollable_list';
@@ -328,6 +328,12 @@ export const CollectionAccounts: React.FC<{
[canSubmit, id, history, accountIds],
);
const inputId = useId();
const inputLabel = intl.formatMessage({
id: 'collections.search_accounts_label',
defaultMessage: 'Search for accounts to add…',
});
return (
<form onSubmit={handleSubmit} className={classes.form}>
<FormStack className={classes.formFieldStack}>
@@ -348,21 +354,12 @@ export const CollectionAccounts: React.FC<{
}
/>
)}
<ComboboxField
label={
<FormattedMessage
id='collections.search_accounts_label'
defaultMessage='Search for accounts to add…'
/>
}
hint={
hasMaxAccounts ? (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
) : undefined
}
<label htmlFor={inputId} className='sr-only'>
{inputLabel}
</label>
<Combobox
id={inputId}
placeholder={inputLabel}
value={hasMaxAccounts ? '' : searchValue}
onChange={handleSearchValueChange}
onKeyDown={handleSearchKeyDown}
@@ -376,6 +373,12 @@ export const CollectionAccounts: React.FC<{
isEditMode ? instantToggleAccountItem : toggleAccountItem
}
/>
{hasMaxAccounts && (
<FormattedMessage
id='collections.search_accounts_max_reached'
defaultMessage='You have added the maximum number of accounts'
/>
)}
{hasMinAccounts && (
<Callout>

View File

@@ -82,6 +82,7 @@ import {
AccountFeatured,
AccountAbout,
AccountEdit,
AccountEditFeaturedTags,
Quotes,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
@@ -164,9 +165,8 @@ class SwitchingColumnsArea extends PureComponent {
redirect = <Redirect from='/' to='/about' exact />;
}
const profileRedesignEnabled = isServerFeatureEnabled('profile_redesign');
const profileRedesignRoutes = [];
if (profileRedesignEnabled) {
if (isServerFeatureEnabled('profile_redesign')) {
profileRedesignRoutes.push(
<WrappedRoute key="posts" path={['/@:acct/posts', '/accounts/:id/posts']} exact component={AccountTimeline} content={children} />,
);
@@ -188,13 +188,27 @@ class SwitchingColumnsArea extends PureComponent {
);
}
} else {
// If the redesign is not enabled but someone shares an /about link, redirect to the root.
profileRedesignRoutes.push(
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />,
// If the redesign is not enabled but someone shares an /about link, redirect to the root.
<Redirect key="about-acct-redirect" from='/@:acct/about' to='/@:acct' exact />,
<Redirect key="about-id-redirect" from='/accounts/:id/about' to='/accounts/:id' exact />
);
}
if (isClientFeatureEnabled('profile_editing')) {
profileRedesignRoutes.push(
<WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />,
<WrappedRoute key="featured_tags" path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
)
} else {
// If profile editing is not enabled, redirect to the home timeline as the current editing pages are outside React Router.
profileRedesignRoutes.push(
<Redirect key="edit-redirect" from='/profile/edit' to='/' exact />,
<Redirect key="featured-tags-redirect" from='/profile/featured_tags' to='/' exact />,
);
}
return (
<ColumnsContextProvider multiColumn={!singleColumn}>
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
@@ -234,8 +248,6 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
{isClientFeatureEnabled('profile_editing') && <WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />}
<WrappedRoute path={['/start', '/start/profile']} exact component={OnboardingProfile} content={children} />
<WrappedRoute path='/start/follows' component={OnboardingFollows} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />
@@ -243,8 +255,8 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/search' component={Search} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
{!profileRedesignEnabled && <WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />}
{...profileRedesignRoutes}
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />

View File

@@ -103,6 +103,11 @@ export function AccountEdit() {
.then((module) => ({ default: module.AccountEdit }));
}
export function AccountEditFeaturedTags() {
return import('../../account_edit/featured_tags')
.then((module) => ({ default: module.AccountEditFeaturedTags }));
}
export function Followers () {
return import('../../followers');
}

View File

@@ -145,6 +145,7 @@
"account_edit.char_counter": "{currentLength}/{maxLength} znaků",
"account_edit.column_button": "Hotovo",
"account_edit.column_title": "Upravit profil",
"account_edit.custom_fields.placeholder": "Přidejte svá zájmena, externí odkazy nebo cokoliv jiného, co chcete sdílet.",
"account_edit.custom_fields.title": "Vlastní pole",
"account_edit.save": "Uložit",
"account_edit.section_edit_button": "Upravit",
@@ -250,12 +251,14 @@
"closed_registrations_modal.find_another_server": "Najít jiný server",
"closed_registrations_modal.preamble": "Mastodon je decentralizovaný, takže bez ohledu na to, kde vytvoříte svůj účet, budete moci sledovat a komunikovat s kýmkoli na tomto serveru. Můžete ho dokonce hostovat!",
"closed_registrations_modal.title": "Registrace na Mastodon",
"collections.collection_description": "Popis",
"collections.collection_name": "Název",
"collections.collection_topic": "Téma",
"collections.confirm_account_removal": "Jste si jisti, že chcete odstranit tento účet z této sbírky?",
"collections.content_warning": "Varování o obsahu",
"collections.continue": "Pokračovat",
"collections.mark_as_sensitive_hint": "Skryje popis kolekce a účty za varováním obsahu. Název kolekce bude stále viditelný.",
"collections.name_length_hint": "Limit 40 znaků",
"collections.new_collection": "Nová sbírka",
"collections.no_collections_yet": "Ještě nemáte žádné sbírky.",
"collections.remove_account": "Odstranit tento účet",
@@ -335,6 +338,8 @@
"confirmations.delete.confirm": "Smazat",
"confirmations.delete.message": "Opravdu chcete smazat tento příspěvek?",
"confirmations.delete.title": "Smazat příspěvek?",
"confirmations.delete_collection.confirm": "Smazat",
"confirmations.delete_collection.message": "Tuto akci nelze vrátit zpět.",
"confirmations.delete_collection.title": "Smazat „{name}“?",
"confirmations.delete_list.confirm": "Smazat",
"confirmations.delete_list.message": "Opravdu chcete tento seznam navždy smazat?",

View File

@@ -390,6 +390,9 @@
"confirmations.discard_draft.post.title": "Dileu drafft eich postiad?",
"confirmations.discard_edit_media.confirm": "Dileu",
"confirmations.discard_edit_media.message": "Mae gennych newidiadau heb eu cadw i'r disgrifiad cyfryngau neu'r rhagolwg - eu dileu beth bynnag?",
"confirmations.follow_to_collection.confirm": "Dilyn ac ychwanegu at y casgliad",
"confirmations.follow_to_collection.message": "Mae angen i chi fod yn dilyn {name} i'w hychwanegu at gasgliad.",
"confirmations.follow_to_collection.title": "Dilyn cyfrif?",
"confirmations.follow_to_list.confirm": "Dilyn ac ychwanegu at y rhestr",
"confirmations.follow_to_list.message": "Mae angen i chi fod yn dilyn {name} i'w ychwanegu at restr.",
"confirmations.follow_to_list.title": "Dilyn defnyddiwr?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "Begrænset til 40 tegn",
"collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Ingen samlinger endnu.",
"collections.old_last_post_note": "Seneste indlæg er fra over en uge siden",
"collections.remove_account": "Fjern denne konto",
"collections.search_accounts_label": "Søg efter konti for at tilføje…",
"collections.search_accounts_max_reached": "Du har tilføjet det maksimale antal konti",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Kassér dit indlægsudkast?",
"confirmations.discard_edit_media.confirm": "Kassér",
"confirmations.discard_edit_media.message": "Der er ugemte ændringer i mediebeskrivelsen eller forhåndsvisningen, kassér dem alligevel?",
"confirmations.follow_to_collection.confirm": "Følg og føj til samling",
"confirmations.follow_to_collection.message": "Du skal følge {name} for at føje vedkommende til en samling.",
"confirmations.follow_to_collection.title": "Følg konto?",
"confirmations.follow_to_list.confirm": "Følg og føj til liste",
"confirmations.follow_to_list.message": "Du skal følge {name} for at føje vedkommende til en liste.",
"confirmations.follow_to_list.title": "Følg bruger?",

View File

@@ -152,7 +152,7 @@
"account_edit.custom_fields.title": "Zusatzfelder",
"account_edit.display_name.placeholder": "Dein Anzeigename wird auf deinem Profil und in Timelines angezeigt.",
"account_edit.display_name.title": "Anzeigename",
"account_edit.featured_hashtags.placeholder": "Hilf anderen dabei, deine Lieblingsthemen zu identifizieren und diese leicht zugänglich zu machen.",
"account_edit.featured_hashtags.placeholder": "Präsentiere deine Lieblingsthemen und ermögliche anderen einen schnellen Zugriff darauf.",
"account_edit.featured_hashtags.title": "Vorgestellte Hashtags",
"account_edit.name_modal.add_title": "Anzeigenamen hinzufügen",
"account_edit.name_modal.edit_title": "Anzeigenamen bearbeiten",
@@ -296,6 +296,7 @@
"collections.name_length_hint": "Maximal 40 Zeichen",
"collections.new_collection": "Neue Sammlung",
"collections.no_collections_yet": "Bisher keine Sammlungen vorhanden.",
"collections.old_last_post_note": "Neuester Beitrag mehr als eine Woche alt",
"collections.remove_account": "Dieses Konto entfernen",
"collections.search_accounts_label": "Suche nach Konten, um sie hinzuzufügen …",
"collections.search_accounts_max_reached": "Du hast die Höchstzahl an Konten hinzugefügt",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Entwurf verwerfen?",
"confirmations.discard_edit_media.confirm": "Verwerfen",
"confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?",
"confirmations.follow_to_collection.confirm": "Folgen und zur Sammlung hinzufügen",
"confirmations.follow_to_collection.message": "Du musst {name} folgen, um das Profil zu einer Sammlung hinzufügen zu können.",
"confirmations.follow_to_collection.title": "Konto folgen?",
"confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen",
"confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.",
"confirmations.follow_to_list.title": "Profil folgen?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "Όριο 40 χαρακτήρων",
"collections.new_collection": "Νέα συλλογή",
"collections.no_collections_yet": "Καμία συλλογή ακόμη.",
"collections.old_last_post_note": "Τελευταία ανάρτηση πριν από μια εβδομάδα",
"collections.remove_account": "Αφαίρεση λογαριασμού",
"collections.search_accounts_label": "Αναζήτηση λογαριασμών για προσθήκη…",
"collections.search_accounts_max_reached": "Έχετε προσθέσει τον μέγιστο αριθμό λογαριασμών",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Απόρριψη της πρόχειρης ανάρτησης σας;",
"confirmations.discard_edit_media.confirm": "Απόρριψη",
"confirmations.discard_edit_media.message": "Έχεις μη αποθηκευμένες αλλαγές στην περιγραφή πολυμέσων ή στην προεπισκόπηση, απόρριψη ούτως ή άλλως;",
"confirmations.follow_to_collection.confirm": "Ακολουθήστε και προσθέστε στη συλλογή",
"confirmations.follow_to_collection.message": "Πρέπει να ακολουθήσετε τον χρήστη {name} για να τους προσθέσετε σε μια συλλογή.",
"confirmations.follow_to_collection.title": "Ακολουθήστε τον λογαριασμό;",
"confirmations.follow_to_list.confirm": "Ακολούθησε και πρόσθεσε στη λίστα",
"confirmations.follow_to_list.message": "Πρέπει να ακολουθήσεις τον χρήστη {name} για να τον προσθέσεις σε μια λίστα.",
"confirmations.follow_to_list.title": "Ακολούθηση χρήστη;",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "40 characters limit",
"collections.new_collection": "New collection",
"collections.no_collections_yet": "No collections yet.",
"collections.old_last_post_note": "Last posted over a week ago",
"collections.remove_account": "Remove this account",
"collections.search_accounts_label": "Search for accounts to add…",
"collections.search_accounts_max_reached": "You have added the maximum number of accounts",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Discard your draft post?",
"confirmations.discard_edit_media.confirm": "Discard",
"confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?",
"confirmations.follow_to_collection.confirm": "Follow and add to collection",
"confirmations.follow_to_collection.message": "You need to be following {name} to add them to a collection.",
"confirmations.follow_to_collection.title": "Follow account?",
"confirmations.follow_to_list.confirm": "Follow and add to list",
"confirmations.follow_to_list.message": "You need to be following {name} to add them to a list.",
"confirmations.follow_to_list.title": "Follow user?",

View File

@@ -145,6 +145,9 @@
"account_edit.bio.title": "Bio",
"account_edit.bio_modal.add_title": "Add bio",
"account_edit.bio_modal.edit_title": "Edit bio",
"account_edit.button.add": "Add {item}",
"account_edit.button.delete": "Delete {item}",
"account_edit.button.edit": "Edit {item}",
"account_edit.char_counter": "{currentLength}/{maxLength} characters",
"account_edit.column_button": "Done",
"account_edit.column_title": "Edit Profile",
@@ -152,6 +155,7 @@
"account_edit.custom_fields.title": "Custom fields",
"account_edit.display_name.placeholder": "Your display name is how your name appears on your profile and in timelines.",
"account_edit.display_name.title": "Display name",
"account_edit.featured_hashtags.item": "hashtags",
"account_edit.featured_hashtags.placeholder": "Help others identify, and have quick access to, your favorite topics.",
"account_edit.featured_hashtags.title": "Featured hashtags",
"account_edit.name_modal.add_title": "Add display name",
@@ -159,7 +163,11 @@
"account_edit.profile_tab.subtitle": "Customize the tabs on your profile and what they display.",
"account_edit.profile_tab.title": "Profile tab settings",
"account_edit.save": "Save",
"account_edit.section_edit_button": "Edit",
"account_edit_tags.column_title": "Edit featured hashtags",
"account_edit_tags.help_text": "Featured hashtags help users discover and interact with your profile. They appear as filters on your Profile pages Activity view.",
"account_edit_tags.search_placeholder": "Enter a hashtag…",
"account_edit_tags.suggestions": "Suggestions:",
"account_edit_tags.tag_status_count": "{count} posts",
"account_note.placeholder": "Click to add note",
"admin.dashboard.daily_retention": "User retention rate by day after sign-up",
"admin.dashboard.monthly_retention": "User retention rate by month after sign-up",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "Límite de 40 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "No hay colecciones aún.",
"collections.old_last_post_note": "Último mensaje hace más de una semana",
"collections.remove_account": "Eliminar esta cuenta",
"collections.search_accounts_label": "Buscar cuentas para agregar…",
"collections.search_accounts_max_reached": "Agregaste el número máximo de cuentas",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "¿Descartar tu borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tenés cambios sin guardar en la descripción de medios o en la vista previa, ¿querés descartarlos de todos modos?",
"confirmations.follow_to_collection.confirm": "Seguir y agregar a la colección",
"confirmations.follow_to_collection.message": "Necesitás seguir a {name} para agregarle a una colección.",
"confirmations.follow_to_collection.title": "¿Seguir cuenta?",
"confirmations.follow_to_list.confirm": "Seguir y agregar a la lista",
"confirmations.follow_to_list.message": "Necesitás seguir a {name} para agregarle a una lista.",
"confirmations.follow_to_list.title": "¿Querés seguirle?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "Limitado a 40 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "No hay colecciones todavía.",
"collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Eliminar esta cuenta",
"collections.search_accounts_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "¿Deseas descartar tu borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo, ¿deseas descartarlos de cualquier manera?",
"confirmations.follow_to_collection.confirm": "Seguir y añadir a la colección",
"confirmations.follow_to_collection.message": "Debes seguir a {name} para agregarlo a una colección.",
"confirmations.follow_to_collection.title": "¿Seguir cuenta?",
"confirmations.follow_to_list.confirm": "Seguir y agregar a la lista",
"confirmations.follow_to_list.message": "Tienes que seguir a {name} para añadirlo a una lista.",
"confirmations.follow_to_list.title": "¿Seguir a usuario?",

View File

@@ -141,8 +141,25 @@
"account.unmute": "Dejar de silenciar a @{name}",
"account.unmute_notifications_short": "Dejar de silenciar notificaciones",
"account.unmute_short": "Dejar de silenciar",
"account_edit.bio.placeholder": "Añade una breve introducción para ayudar a los demás a identificarte.",
"account_edit.bio.title": "Biografía",
"account_edit.bio_modal.add_title": "Añadir biografía",
"account_edit.bio_modal.edit_title": "Editar biografía",
"account_edit.char_counter": "{currentLength}/{maxLength} caracteres",
"account_edit.column_button": "Hecho",
"account_edit.column_title": "Editar perfil",
"account_edit.custom_fields.placeholder": "Añade tus pronombres, enlaces externos o cualquier otra cosa que quieras compartir.",
"account_edit.custom_fields.title": "Campos personalizados",
"account_edit.display_name.placeholder": "Tu nombre de usuario es el nombre que aparece en tu perfil y en las cronologías.",
"account_edit.display_name.title": "Nombre para mostrar",
"account_edit.featured_hashtags.placeholder": "Ayuda a otros a identificar tus temas favoritos y a acceder rápidamente a ellos.",
"account_edit.featured_hashtags.title": "Etiquetas destacadas",
"account_edit.name_modal.add_title": "Añadir nombre para mostrar",
"account_edit.name_modal.edit_title": "Editar nombre para mostrar",
"account_edit.profile_tab.subtitle": "Personaliza las pestañas de tu perfil y lo que muestran.",
"account_edit.profile_tab.title": "Configuración de la pestaña de perfil",
"account_edit.save": "Guardar",
"account_edit.section_edit_button": "Editar",
"account_note.placeholder": "Haz clic para añadir nota",
"admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después del registro",
"admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después del registro",
@@ -267,6 +284,7 @@
"collections.detail.curated_by_you": "Seleccionado por ti",
"collections.detail.loading": "Cargando colección…",
"collections.detail.share": "Compartir esta colección",
"collections.edit_details": "Editar detalles",
"collections.error_loading_collections": "Se ha producido un error al intentar cargar tus colecciones.",
"collections.hints.accounts_counter": "{count} / {max} cuentas",
"collections.hints.add_more_accounts": "¡Añade al menos {count, plural, one {# cuenta} other {# cuentas}} para continuar",
@@ -275,11 +293,14 @@
"collections.manage_accounts": "Administrar cuentas",
"collections.mark_as_sensitive": "Marcar como sensible",
"collections.mark_as_sensitive_hint": "Oculta la descripción de la colección y las cuentas detrás de una advertencia de contenido. El nombre de la colección seguirá siendo visible.",
"collections.name_length_hint": "Límite de 40 caracteres",
"collections.new_collection": "Nueva colección",
"collections.no_collections_yet": "Aún no hay colecciones.",
"collections.old_last_post_note": "Última publicación hace más de una semana",
"collections.remove_account": "Borrar esta cuenta",
"collections.search_accounts_label": "Buscar cuentas para añadir…",
"collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Añadir una etiqueta que ayude a otros a entender el tema principal de esta colección.",
"collections.view_collection": "Ver colección",
"collections.visibility_public": "Pública",
@@ -370,6 +391,9 @@
"confirmations.discard_draft.post.title": "¿Descartar tu borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tienes cambios sin guardar en la descripción o vista previa del archivo audiovisual, ¿descartarlos de todos modos?",
"confirmations.follow_to_collection.confirm": "Seguir y añadir a la colección",
"confirmations.follow_to_collection.message": "Debes seguir a {name} para añadirlo a una colección.",
"confirmations.follow_to_collection.title": "¿Seguir cuenta?",
"confirmations.follow_to_list.confirm": "Seguir y añadir a la lista",
"confirmations.follow_to_list.message": "Necesitas seguir a {name} para agregarlo a una lista.",
"confirmations.follow_to_list.title": "¿Seguir usuario?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "40 merkin rajoitus",
"collections.new_collection": "Uusi kokoelma",
"collections.no_collections_yet": "Ei vielä kokoelmia.",
"collections.old_last_post_note": "Julkaissut viimeksi yli viikko sitten",
"collections.remove_account": "Poista tämä tili",
"collections.search_accounts_label": "Hae lisättäviä tilejä…",
"collections.search_accounts_max_reached": "Olet lisännyt enimmäismäärän tilejä",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?",
"confirmations.discard_edit_media.confirm": "Hylkää",
"confirmations.discard_edit_media.message": "Sinulla on tallentamattomia muutoksia median kuvaukseen tai esikatseluun. Hylätäänkö ne silti?",
"confirmations.follow_to_collection.confirm": "Seuraa ja lisää kokoelmaan",
"confirmations.follow_to_collection.message": "Sinun on seurattava tiliä {name}, jotta voit lisätä sen kokoelmaan.",
"confirmations.follow_to_collection.title": "Seurataanko tiliä?",
"confirmations.follow_to_list.confirm": "Seuraa ja lisää listaan",
"confirmations.follow_to_list.message": "Sinun on seurattava käyttäjää {name}, jotta voit lisätä hänet listaan.",
"confirmations.follow_to_list.title": "Seurataanko käyttäjää?",

View File

@@ -141,8 +141,25 @@
"account.unmute": "Doyv ikki @{name}",
"account.unmute_notifications_short": "Tendra fráboðanir",
"account.unmute_short": "Doyv ikki",
"account_edit.bio.placeholder": "Legg eina stutta kunning afturat soleiðis at onnur kunnu eyðmerkja teg.",
"account_edit.bio.title": "Ævilýsing",
"account_edit.bio_modal.add_title": "Legg ævilýsing afturat",
"account_edit.bio_modal.edit_title": "Rætta ævilýsing",
"account_edit.char_counter": "{currentLength}/{maxLength} tekn",
"account_edit.column_button": "Liðugt",
"account_edit.column_title": "Rætta vanga",
"account_edit.custom_fields.placeholder": "Legg tíni forheiti, uttanhýsis leinki ella okkurt annað, sum tú kundi hugsað tær at deilt.",
"account_edit.custom_fields.title": "Serfelt",
"account_edit.display_name.placeholder": "Títt vísta navn er soleiðis sum navnið hjá tær verður víst á vanganum og á tíðarrásum.",
"account_edit.display_name.title": "Víst navn",
"account_edit.featured_hashtags.placeholder": "Hjálp øðrum at eyðmekja og hava skjóta atgongd til tíni yndisevni.",
"account_edit.featured_hashtags.title": "Sermerkt frámerki",
"account_edit.name_modal.add_title": "Legg víst navn afturat",
"account_edit.name_modal.edit_title": "Rætta víst navn",
"account_edit.profile_tab.subtitle": "Tillaga spjøldrini á vanganum hjá tær og tað, tey vísa.",
"account_edit.profile_tab.title": "Stillingar fyri spjøldur á vanga",
"account_edit.save": "Goym",
"account_edit.section_edit_button": "Rætta",
"account_note.placeholder": "Klikka fyri at leggja viðmerking afturat",
"admin.dashboard.daily_retention": "Hvussu nógvir brúkarar eru eftir, síðani tey skrásettu seg, roknað í døgum",
"admin.dashboard.monthly_retention": "Hvussu nógvir brúkarar eru eftir síðani tey skrásettu seg, roknað í mánaðum",
@@ -262,6 +279,12 @@
"collections.create_collection": "Ger savn",
"collections.delete_collection": "Strika savn",
"collections.description_length_hint": "Í mesta lagi 100 tekn",
"collections.detail.accounts_heading": "Kontur",
"collections.detail.curated_by_author": "Snikkað til av {author}",
"collections.detail.curated_by_you": "Snikkað til av tær",
"collections.detail.loading": "Innlesi savn…",
"collections.detail.share": "Deil hetta savnið",
"collections.edit_details": "Rætta smálutir",
"collections.error_loading_collections": "Ein feilur hendi, tá tú royndi at finna fram søvnini hjá tær.",
"collections.hints.accounts_counter": "{count} / {max} kontur",
"collections.hints.add_more_accounts": "Legg minst {count, plural, one {# kontu} other {# kontur}} afturat fyri at halda fram",
@@ -270,11 +293,14 @@
"collections.manage_accounts": "Umsit kontur",
"collections.mark_as_sensitive": "Merk sum viðkvæmt",
"collections.mark_as_sensitive_hint": "Fjalið lýsingina av og konturnar hjá savninum aftan fyri eina innihaldsávaring. Savnsnavnið verður framvegis sjónligt.",
"collections.name_length_hint": "Í mesta lagi 40 tekn",
"collections.new_collection": "Nýtt savn",
"collections.no_collections_yet": "Eingi søvn enn.",
"collections.old_last_post_note": "Postaði seinast fyri meira enn einari viku síðani",
"collections.remove_account": "Strika hesa kontuna",
"collections.search_accounts_label": "Leita eftir kontum at leggja afturat…",
"collections.search_accounts_max_reached": "Tú hevur lagt afturat mesta talið av kontum",
"collections.sensitive": "Viðkvæmt",
"collections.topic_hint": "Legg afturat eitt frámerki, sum hjálpir øðrum at skilja høvuðevnið í hesum savninum.",
"collections.view_collection": "Vís savn",
"collections.visibility_public": "Alment",
@@ -365,6 +391,9 @@
"confirmations.discard_draft.post.title": "Vraka kladdupostin?",
"confirmations.discard_edit_media.confirm": "Vraka",
"confirmations.discard_edit_media.message": "Tú hevur broytingar í miðlalýsingini ella undansýningini, sum ikki eru goymdar. Vilt tú kortini vraka?",
"confirmations.follow_to_collection.confirm": "Fylg og legg afturat savni",
"confirmations.follow_to_collection.message": "Tú mást fylgja {name} fyri at leggja tey afturat einum savni.",
"confirmations.follow_to_collection.title": "Fylg kontu?",
"confirmations.follow_to_list.confirm": "Fylg og legg afturat lista",
"confirmations.follow_to_list.message": "Tú mást fylgja {name} fyri at leggja tey afturat einum lista.",
"confirmations.follow_to_list.title": "Fylg brúkara?",

View File

@@ -141,15 +141,25 @@
"account.unmute": "Ne plus masquer @{name}",
"account.unmute_notifications_short": "Ne plus masquer les notifications",
"account.unmute_short": "Ne plus masquer",
"account_edit.bio.placeholder": "Ajouter une courte introduction pour aider à vous connaître.",
"account_edit.bio.placeholder": "Ajouter une courte introduction pour aider les autres à vous connaître.",
"account_edit.bio.title": "Présentation",
"account_edit.bio_modal.add_title": "Ajouter une présentation",
"account_edit.bio_modal.edit_title": "Modifier la présentation",
"account_edit.char_counter": "{currentLength}/{maxLength} caractères",
"account_edit.column_button": "Terminé",
"account_edit.column_title": "Modifier le profil",
"account_edit.custom_fields.placeholder": "Ajouter vos pronoms, vos sites, ou tout ce que vous voulez partager.",
"account_edit.custom_fields.title": "Champs personnalisés",
"account_edit.display_name.placeholder": "Votre nom public est le nom qui apparaît sur votre profil et dans les fils d'actualités.",
"account_edit.display_name.title": "Nom public",
"account_edit.featured_hashtags.placeholder": "Aider les autres à identifier et à accéder rapidement à vos sujets préférés.",
"account_edit.featured_hashtags.title": "Hashtags mis en avant",
"account_edit.name_modal.add_title": "Ajouter un nom public",
"account_edit.name_modal.edit_title": "Modifier le nom public",
"account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.",
"account_edit.profile_tab.title": "Paramètres de l'onglet du profil",
"account_edit.save": "Enregistrer",
"account_edit.section_edit_button": "Éditer",
"account_edit.section_edit_button": "Modifier",
"account_note.placeholder": "Cliquez pour ajouter une note",
"admin.dashboard.daily_retention": "Taux de rétention des comptes par jour après inscription",
"admin.dashboard.monthly_retention": "Taux de rétention des comptes par mois après inscription",
@@ -242,7 +252,7 @@
"bundle_column_error.routing.body": "La page demandée est introuvable. Êtes-vous sûr que lURL dans la barre dadresse est correcte?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Fermer",
"bundle_modal_error.message": "Un problème s'est produit lors du chargement de cet écran.",
"bundle_modal_error.message": "Une erreur sest produite lors du chargement de cet écran.",
"bundle_modal_error.retry": "Réessayer",
"callout.dismiss": "Rejeter",
"carousel.current": "<sr>Diapositive</sr> {current, number} / {max, number}",
@@ -274,6 +284,7 @@
"collections.detail.curated_by_you": "Organisée par vous",
"collections.detail.loading": "Chargement de la collection…",
"collections.detail.share": "Partager la collection",
"collections.edit_details": "Modifier les détails",
"collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.",
"collections.hints.accounts_counter": "{count} / {max} comptes",
"collections.hints.add_more_accounts": "Ajouter au moins {count, plural, one {# compte} other {# comptes}} pour continuer",
@@ -282,11 +293,14 @@
"collections.manage_accounts": "Gérer les comptes",
"collections.mark_as_sensitive": "Marquer comme sensible",
"collections.mark_as_sensitive_hint": "Masque la description et les comptes de la collection derrière un avertissement au public. Le titre reste visible.",
"collections.name_length_hint": "Maximum 40 caractères",
"collections.new_collection": "Nouvelle collection",
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.old_last_post_note": "Dernière publication il y a plus d'une semaine",
"collections.remove_account": "Supprimer ce compte",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.",
"collections.view_collection": "Voir la collection",
"collections.visibility_public": "Publique",
@@ -332,7 +346,7 @@
"community.column_settings.local_only": "Local seulement",
"community.column_settings.media_only": "Média seulement",
"community.column_settings.remote_only": "À distance seulement",
"compose.error.blank_post": "Le message ne peut être laissé vide.",
"compose.error.blank_post": "Le message ne peut pas être vide.",
"compose.language.change": "Changer de langue",
"compose.language.search": "Rechercher des langues…",
"compose.published.body": "Publiée.",
@@ -369,24 +383,27 @@
"confirmations.delete_list.message": "Voulez-vous vraiment supprimer définitivement cette liste?",
"confirmations.delete_list.title": "Supprimer la liste ?",
"confirmations.discard_draft.confirm": "Effacer et continuer",
"confirmations.discard_draft.edit.cancel": "Retour vers l'éditeur",
"confirmations.discard_draft.edit.message": "Continued va perdre les changements que vous avez faits dans le message courant.",
"confirmations.discard_draft.edit.title": "Jeter les changements faits au message?",
"confirmations.discard_draft.edit.cancel": "Reprendre l'édition",
"confirmations.discard_draft.edit.message": "Si vous continuez, toutes les modifications apportées au message en cours d'édition seront annulées.",
"confirmations.discard_draft.edit.title": "Annuler les modifications apportées à votre message?",
"confirmations.discard_draft.post.cancel": "Retour au brouillon",
"confirmations.discard_draft.post.message": "En continuant, vous perdez le message que vous êtes en train d'écrire.",
"confirmations.discard_draft.post.title": "Jeter le brouillon de message?",
"confirmations.discard_draft.post.message": "Si vous continuez, vous supprimerez le message que vous êtes en train de composer.",
"confirmations.discard_draft.post.title": "Abandonner votre brouillon ?",
"confirmations.discard_edit_media.confirm": "Rejeter",
"confirmations.discard_edit_media.message": "Vous avez des modifications non enregistrées de la description ou de l'aperçu du média, voulez-vous quand même les supprimer?",
"confirmations.follow_to_collection.confirm": "Suivre et ajouter à la collection",
"confirmations.follow_to_collection.message": "Vous devez suivre {name} pour l'ajouter à une collection.",
"confirmations.follow_to_collection.title": "Suivre le compte ?",
"confirmations.follow_to_list.confirm": "Suivre et ajouter à la liste",
"confirmations.follow_to_list.message": "Vous devez suivre {name} pour l'ajouter à une liste.",
"confirmations.follow_to_list.title": "Suivre l'utilisateur ?",
"confirmations.follow_to_list.title": "Suivre l'utilisateur·rice ?",
"confirmations.logout.confirm": "Se déconnecter",
"confirmations.logout.message": "Voulez-vous vraiment vous déconnecter?",
"confirmations.logout.title": "Se déconnecter ?",
"confirmations.missing_alt_text.confirm": "Ajouter un texte alternatif",
"confirmations.missing_alt_text.message": "Votre post contient des médias sans texte alternatif. Ajouter des descriptions rend votre contenu accessible à un plus grand nombre de personnes.",
"confirmations.missing_alt_text.secondary": "Publier quand-même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.missing_alt_text.message": "Votre message contient des médias sans texte alternatif. L'ajout de descriptions aide à rendre votre contenu accessible à plus de personnes.",
"confirmations.missing_alt_text.secondary": "Publier quand même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.mute.confirm": "Masquer",
"confirmations.private_quote_notify.cancel": "Retour à l'édition",
"confirmations.private_quote_notify.confirm": "Publier",
@@ -395,17 +412,17 @@
"confirmations.private_quote_notify.title": "Partager avec les personnes abonnées et mentionnées?",
"confirmations.quiet_post_quote_info.dismiss": "Ne plus me rappeler",
"confirmations.quiet_post_quote_info.got_it": "Compris",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public silencieux, votre message sera caché des fils tendances.",
"confirmations.quiet_post_quote_info.title": "Citation de messages publics silencieux",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public discret, votre message sera caché des fils tendances.",
"confirmations.quiet_post_quote_info.title": "Citation d'un message public discret",
"confirmations.redraft.confirm": "Supprimer et réécrire",
"confirmations.redraft.message": "Êtes-vous sûr·e de vouloir effacer cette publication pour la réécrire? Ses ses mises en favori et boosts seront perdus et ses réponses seront orphelines.",
"confirmations.redraft.title": "Supprimer et réécrire le message ?",
"confirmations.remove_from_followers.confirm": "Supprimer l'abonné·e",
"confirmations.remove_from_followers.message": "{name} cessera de vous suivre. Êtes-vous sûr de vouloir continuer ?",
"confirmations.remove_from_followers.message": "{name} cessera de vous suivre. Voulez-vous vraiment continuer ?",
"confirmations.remove_from_followers.title": "Supprimer l'abonné·e ?",
"confirmations.revoke_quote.confirm": "Retirer la publication",
"confirmations.revoke_quote.confirm": "Retirer le message",
"confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.",
"confirmations.revoke_quote.title": "Retirer la publication ?",
"confirmations.revoke_quote.title": "Retirer le message ?",
"confirmations.unblock.confirm": "Débloquer",
"confirmations.unblock.title": "Débloquer {name} ?",
"confirmations.unfollow.confirm": "Ne plus suivre",
@@ -430,7 +447,7 @@
"disabled_account_banner.text": "Votre compte {disabledAccount} est présentement désactivé.",
"dismissable_banner.community_timeline": "Voici les publications publiques les plus récentes de personnes dont les comptes sont hébergés par {domain}.",
"dismissable_banner.dismiss": "Rejeter",
"dismissable_banner.public_timeline": "Il s'agit des messages publics les plus récents publiés par des personnes sur le fediverse que les personnes sur {domain} suivent.",
"dismissable_banner.public_timeline": "Il s'agit des messages publics les plus récents publiés par des personnes sur le fédivers que les personnes sur {domain} suivent.",
"domain_block_modal.block": "Bloquer le serveur",
"domain_block_modal.block_account_instead": "Bloquer @{name} à la place",
"domain_block_modal.they_can_interact_with_old_posts": "Les personnes de ce serveur peuvent interagir avec vos anciens messages.",
@@ -444,12 +461,12 @@
"domain_pill.activitypub_like_language": "ActivityPub est comme une langue que Mastodon utilise pour communiquer avec les autres réseaux sociaux.",
"domain_pill.server": "Serveur",
"domain_pill.their_handle": "Son identifiant :",
"domain_pill.their_server": "Son foyer numérique, là où tous ses posts résident.",
"domain_pill.their_server": "Son foyer numérique, là où tous ses messages résident.",
"domain_pill.their_username": "Son identifiant unique sur leur serveur. Il est possible de rencontrer des utilisateur·rice·s avec le même nom sur différents serveurs.",
"domain_pill.username": "Nom dutilisateur",
"domain_pill.username": "Nom dutilisateur·rice",
"domain_pill.whats_in_a_handle": "Qu'est-ce qu'un identifiant ?",
"domain_pill.who_they_are": "Comme un identifiant contient le nom et le service hébergeant une personne, vous pouvez interagir sur <button>les plateformes sociales implémentant ActivityPub</button>.",
"domain_pill.who_you_are": "Comme un identifiant indique votre nom et le service vous hébergeant, vous pouvez interagir avec <button>les autres plateformes sociales implémentant ActivityPub</button>.",
"domain_pill.who_you_are": "Comme un identifiant indique votre nom et le service vous hébergeant, tout le monde peut interagir avec vous à l'aide de <button>plateformes sociales implémentant ActivityPub</button>.",
"domain_pill.your_handle": "Votre identifiant :",
"domain_pill.your_server": "Votre foyer numérique, là où vos messages résident. Vous souhaitez changer ? Lancez un transfert vers un autre serveur quand vous le voulez et vos abonné·e·s suivront automatiquement.",
"domain_pill.your_username": "Votre identifiant unique sur ce serveur. Il est possible de rencontrer des utilisateur·rice·s ayant le même nom d'utilisateur sur différents serveurs.",
@@ -1018,7 +1035,7 @@
"server_banner.about_active_users": "Personnes utilisant ce serveur au cours des 30 derniers jours (Comptes actifs mensuellement)",
"server_banner.active_users": "comptes actifs",
"server_banner.administered_by": "Administré par:",
"server_banner.is_one_of_many": "{domain} est l'un des nombreux serveurs Mastodon indépendants que vous pouvez utiliser pour participer au fédiverse.",
"server_banner.is_one_of_many": "{domain} est l'un des nombreux serveurs Mastodon indépendants que vous pouvez utiliser pour participer au fédivers.",
"server_banner.server_stats": "Statistiques du serveur:",
"sign_in_banner.create_account": "Créer un compte",
"sign_in_banner.follow_anyone": "Suivez n'importe qui à travers le fédivers et affichez tout dans un ordre chronologique. Ni algorithmes, ni publicités, ni appâts à clics en perspective.",

View File

@@ -141,15 +141,25 @@
"account.unmute": "Ne plus masquer @{name}",
"account.unmute_notifications_short": "Réactiver les notifications",
"account.unmute_short": "Ne plus masquer",
"account_edit.bio.placeholder": "Ajouter une courte introduction pour aider à vous connaître.",
"account_edit.bio.placeholder": "Ajouter une courte introduction pour aider les autres à vous connaître.",
"account_edit.bio.title": "Présentation",
"account_edit.bio_modal.add_title": "Ajouter une présentation",
"account_edit.bio_modal.edit_title": "Modifier la présentation",
"account_edit.char_counter": "{currentLength}/{maxLength} caractères",
"account_edit.column_button": "Terminé",
"account_edit.column_title": "Modifier le profil",
"account_edit.custom_fields.placeholder": "Ajouter vos pronoms, vos sites, ou tout ce que vous voulez partager.",
"account_edit.custom_fields.title": "Champs personnalisés",
"account_edit.display_name.placeholder": "Votre nom public est le nom qui apparaît sur votre profil et dans les fils d'actualités.",
"account_edit.display_name.title": "Nom public",
"account_edit.featured_hashtags.placeholder": "Aider les autres à identifier et à accéder rapidement à vos sujets préférés.",
"account_edit.featured_hashtags.title": "Hashtags mis en avant",
"account_edit.name_modal.add_title": "Ajouter un nom public",
"account_edit.name_modal.edit_title": "Modifier le nom public",
"account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.",
"account_edit.profile_tab.title": "Paramètres de l'onglet du profil",
"account_edit.save": "Enregistrer",
"account_edit.section_edit_button": "Éditer",
"account_edit.section_edit_button": "Modifier",
"account_note.placeholder": "Cliquez pour ajouter une note",
"admin.dashboard.daily_retention": "Taux de rétention des utilisateur·rice·s par jour après inscription",
"admin.dashboard.monthly_retention": "Taux de rétention des utilisateur·rice·s par mois après inscription",
@@ -157,7 +167,7 @@
"admin.dashboard.retention.cohort": "Mois d'inscription",
"admin.dashboard.retention.cohort_size": "Nouveaux comptes",
"admin.impact_report.instance_accounts": "Profils de comptes que cela supprimerait",
"admin.impact_report.instance_followers": "Abonnées que nos utilisateurs perdraient",
"admin.impact_report.instance_followers": "Abonné·e·s que nos utilisateur·rice·s perdraient",
"admin.impact_report.instance_follows": "Abonné·e·s que leurs utilisateur·rice·s perdraient",
"admin.impact_report.title": "Résumé de l'impact",
"alert.rate_limited.message": "Veuillez réessayer après {retry_time, time, medium}.",
@@ -238,11 +248,11 @@
"bundle_column_error.network.body": "Une erreur s'est produite lors du chargement de cette page. Cela peut être dû à un problème temporaire avec votre connexion internet ou avec ce serveur.",
"bundle_column_error.network.title": "Erreur réseau",
"bundle_column_error.retry": "Réessayer",
"bundle_column_error.return": "Retour à l'accueil",
"bundle_column_error.routing.body": "La page demandée est introuvable. Êtes-vous sûr que lURL dans la barre dadresse est correcte ?",
"bundle_column_error.return": "Retourner à l'accueil",
"bundle_column_error.routing.body": "La page demandée est introuvable. Est-ce que l'URL dans la barre dadresse est correcte ?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Fermer",
"bundle_modal_error.message": "Un problème s'est produit lors du chargement de cet écran.",
"bundle_modal_error.message": "Une erreur sest produite lors du chargement de cet écran.",
"bundle_modal_error.retry": "Réessayer",
"callout.dismiss": "Rejeter",
"carousel.current": "<sr>Diapositive</sr> {current, number} / {max, number}",
@@ -274,6 +284,7 @@
"collections.detail.curated_by_you": "Organisée par vous",
"collections.detail.loading": "Chargement de la collection…",
"collections.detail.share": "Partager la collection",
"collections.edit_details": "Modifier les détails",
"collections.error_loading_collections": "Une erreur s'est produite durant le chargement de vos collections.",
"collections.hints.accounts_counter": "{count} / {max} comptes",
"collections.hints.add_more_accounts": "Ajouter au moins {count, plural, one {# compte} other {# comptes}} pour continuer",
@@ -282,11 +293,14 @@
"collections.manage_accounts": "Gérer les comptes",
"collections.mark_as_sensitive": "Marquer comme sensible",
"collections.mark_as_sensitive_hint": "Masque la description et les comptes de la collection derrière un avertissement au public. Le titre reste visible.",
"collections.name_length_hint": "Maximum 40 caractères",
"collections.new_collection": "Nouvelle collection",
"collections.no_collections_yet": "Aucune collection pour le moment.",
"collections.old_last_post_note": "Dernière publication il y a plus d'une semaine",
"collections.remove_account": "Supprimer ce compte",
"collections.search_accounts_label": "Chercher des comptes à ajouter…",
"collections.search_accounts_max_reached": "Vous avez ajouté le nombre maximum de comptes",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Ajouter un hashtag pour aider les autres personnes à comprendre le sujet de la collection.",
"collections.view_collection": "Voir la collection",
"collections.visibility_public": "Publique",
@@ -295,10 +309,10 @@
"collections.visibility_unlisted": "Non listée",
"collections.visibility_unlisted_hint": "Visible pour les personnes ayant le lien. N'apparaît pas dans les résultats de recherche et les recommandations.",
"column.about": "À propos",
"column.blocks": "Utilisateurs bloqués",
"column.blocks": "Comptes bloqués",
"column.bookmarks": "Marque-pages",
"column.collections": "Mes collections",
"column.community": "Fil public local",
"column.community": "Fil local",
"column.create_list": "Créer une liste",
"column.direct": "Mentions privées",
"column.directory": "Parcourir les profils",
@@ -332,10 +346,10 @@
"community.column_settings.local_only": "Local seulement",
"community.column_settings.media_only": "Média uniquement",
"community.column_settings.remote_only": "Distant seulement",
"compose.error.blank_post": "Le message ne peut être laissé vide.",
"compose.language.change": "Changer de langue",
"compose.language.search": "Rechercher des langues...",
"compose.published.body": "Message Publié.",
"compose.error.blank_post": "Le message ne peut pas être vide.",
"compose.language.change": "Modifier la langue",
"compose.language.search": "Rechercher langue",
"compose.published.body": "Message publié.",
"compose.published.open": "Ouvrir",
"compose.saved.body": "Message enregistré.",
"compose_form.direct_message_warning_learn_more": "En savoir plus",
@@ -348,8 +362,8 @@
"compose_form.poll.multiple": "Choix multiple",
"compose_form.poll.option_placeholder": "Option {number}",
"compose_form.poll.single": "Choix unique",
"compose_form.poll.switch_to_multiple": "Changer le sondage pour autoriser plusieurs choix",
"compose_form.poll.switch_to_single": "Modifier le sondage pour autoriser qu'un seul choix",
"compose_form.poll.switch_to_multiple": "Modifier le sondage pour autoriser plusieurs choix",
"compose_form.poll.switch_to_single": "Modifier le sondage pour autoriser un seul choix",
"compose_form.poll.type": "Style",
"compose_form.publish": "Publier",
"compose_form.reply": "Répondre",
@@ -369,24 +383,27 @@
"confirmations.delete_list.message": "Voulez-vous vraiment supprimer définitivement cette liste?",
"confirmations.delete_list.title": "Supprimer la liste ?",
"confirmations.discard_draft.confirm": "Effacer et continuer",
"confirmations.discard_draft.edit.cancel": "Retour vers l'éditeur",
"confirmations.discard_draft.edit.message": "Continued va perdre les changements que vous avez faits dans le message courant.",
"confirmations.discard_draft.edit.title": "Jeter les changements faits au message?",
"confirmations.discard_draft.edit.cancel": "Reprendre l'édition",
"confirmations.discard_draft.edit.message": "Si vous continuez, toutes les modifications apportées au message en cours d'édition seront annulées.",
"confirmations.discard_draft.edit.title": "Annuler les modifications apportées à votre message?",
"confirmations.discard_draft.post.cancel": "Retour au brouillon",
"confirmations.discard_draft.post.message": "En continuant, vous perdez le message que vous êtes en train d'écrire.",
"confirmations.discard_draft.post.title": "Jeter le brouillon de message?",
"confirmations.discard_edit_media.confirm": "Rejeter",
"confirmations.discard_edit_media.message": "Vous avez des modifications non enregistrées de la description ou de l'aperçu du média, les supprimer quand même ?",
"confirmations.discard_draft.post.message": "Si vous continuez, vous supprimerez le message que vous êtes en train de composer.",
"confirmations.discard_draft.post.title": "Abandonner votre brouillon ?",
"confirmations.discard_edit_media.confirm": "Supprimer",
"confirmations.discard_edit_media.message": "Vous avez des modifications non enregistrées de la description ou de l'aperçu du média. Voulez-vous les supprimer?",
"confirmations.follow_to_collection.confirm": "Suivre et ajouter à la collection",
"confirmations.follow_to_collection.message": "Vous devez suivre {name} pour l'ajouter à une collection.",
"confirmations.follow_to_collection.title": "Suivre le compte ?",
"confirmations.follow_to_list.confirm": "Suivre et ajouter à la liste",
"confirmations.follow_to_list.message": "Vous devez suivre {name} pour l'ajouter à une liste.",
"confirmations.follow_to_list.title": "Suivre l'utilisateur ?",
"confirmations.follow_to_list.title": "Suivre l'utilisateur·rice ?",
"confirmations.logout.confirm": "Se déconnecter",
"confirmations.logout.message": "Voulez-vous vraiment vous déconnecter ?",
"confirmations.logout.title": "Se déconnecter ?",
"confirmations.missing_alt_text.confirm": "Ajouter un texte alternatif",
"confirmations.missing_alt_text.message": "Votre post contient des médias sans texte alternatif. Ajouter des descriptions rend votre contenu accessible à un plus grand nombre de personnes.",
"confirmations.missing_alt_text.secondary": "Publier quand-même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.missing_alt_text.message": "Votre message contient des médias sans texte alternatif. L'ajout de descriptions aide à rendre votre contenu accessible à plus de personnes.",
"confirmations.missing_alt_text.secondary": "Publier quand même",
"confirmations.missing_alt_text.title": "Ajouter un texte alternatif?",
"confirmations.mute.confirm": "Masquer",
"confirmations.private_quote_notify.cancel": "Retour à l'édition",
"confirmations.private_quote_notify.confirm": "Publier",
@@ -395,17 +412,17 @@
"confirmations.private_quote_notify.title": "Partager avec les personnes abonnées et mentionnées?",
"confirmations.quiet_post_quote_info.dismiss": "Ne plus me rappeler",
"confirmations.quiet_post_quote_info.got_it": "Compris",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public silencieux, votre message sera caché des fils tendances.",
"confirmations.quiet_post_quote_info.title": "Citation de messages publics silencieux",
"confirmations.redraft.confirm": "Supprimer et ré-écrire",
"confirmations.redraft.message": "Voulez-vous vraiment supprimer le message pour le réécrire? Ses partages ainsi que ses mises en favori seront perdues, et ses réponses seront orphelines.",
"confirmations.quiet_post_quote_info.message": "Lorsque vous citez un message public discret, votre message sera caché des fils tendances.",
"confirmations.quiet_post_quote_info.title": "Citation d'un message public discret",
"confirmations.redraft.confirm": "Supprimer et réécrire",
"confirmations.redraft.message": "Voulez-vous vraiment supprimer le message pour le réécrire? Ses partages ainsi que ses mises en favori seront perdus, et ses réponses seront orphelines.",
"confirmations.redraft.title": "Supprimer et réécrire le message ?",
"confirmations.remove_from_followers.confirm": "Supprimer l'abonné·e",
"confirmations.remove_from_followers.message": "{name} cessera de vous suivre. Êtes-vous sûr de vouloir continuer ?",
"confirmations.remove_from_followers.message": "{name} cessera de vous suivre. Voulez-vous vraiment continuer ?",
"confirmations.remove_from_followers.title": "Supprimer l'abonné·e ?",
"confirmations.revoke_quote.confirm": "Retirer la publication",
"confirmations.revoke_quote.confirm": "Retirer le message",
"confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.",
"confirmations.revoke_quote.title": "Retirer la publication ?",
"confirmations.revoke_quote.title": "Retirer le message ?",
"confirmations.unblock.confirm": "Débloquer",
"confirmations.unblock.title": "Débloquer {name} ?",
"confirmations.unfollow.confirm": "Ne plus suivre",
@@ -422,15 +439,15 @@
"copy_icon_button.copied": "Copié dans le presse-papier",
"copypaste.copied": "Copié",
"copypaste.copy_to_clipboard": "Copier dans le presse-papiers",
"directory.federated": "Du fédiverse connu",
"directory.federated": "Du fédivers connu",
"directory.local": "De {domain} seulement",
"directory.new_arrivals": "Inscrit·e·s récemment",
"directory.recently_active": "Actif·ve·s récemment",
"disabled_account_banner.account_settings": "Paramètres du compte",
"disabled_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé.",
"dismissable_banner.community_timeline": "Voici les messages publics les plus récents des comptes hébergés par {domain}.",
"dismissable_banner.dismiss": "Rejeter",
"dismissable_banner.public_timeline": "Il s'agit des messages publics les plus récents publiés par des personnes sur le fediverse que les personnes sur {domain} suivent.",
"dismissable_banner.dismiss": "Fermer",
"dismissable_banner.public_timeline": "Il s'agit des messages publics les plus récents publiés par des personnes sur le fédivers que les personnes sur {domain} suivent.",
"domain_block_modal.block": "Bloquer le serveur",
"domain_block_modal.block_account_instead": "Bloquer @{name} à la place",
"domain_block_modal.they_can_interact_with_old_posts": "Les personnes de ce serveur peuvent interagir avec vos anciens messages.",
@@ -444,17 +461,17 @@
"domain_pill.activitypub_like_language": "ActivityPub est comme une langue que Mastodon utilise pour communiquer avec les autres réseaux sociaux.",
"domain_pill.server": "Serveur",
"domain_pill.their_handle": "Son identifiant :",
"domain_pill.their_server": "Son foyer numérique, là où tous ses posts résident.",
"domain_pill.their_server": "Son foyer numérique, là où tous ses messages résident.",
"domain_pill.their_username": "Son identifiant unique sur leur serveur. Il est possible de rencontrer des utilisateur·rice·s avec le même nom sur différents serveurs.",
"domain_pill.username": "Nom dutilisateur",
"domain_pill.username": "Nom dutilisateur·rice",
"domain_pill.whats_in_a_handle": "Qu'est-ce qu'un identifiant ?",
"domain_pill.who_they_are": "Comme un identifiant contient le nom et le service hébergeant une personne, vous pouvez interagir sur <button>les plateformes sociales implémentant ActivityPub</button>.",
"domain_pill.who_you_are": "Comme un identifiant indique votre nom et le service vous hébergeant, vous pouvez interagir avec <button>les autres plateformes sociales implémentant ActivityPub</button>.",
"domain_pill.who_you_are": "Comme un identifiant indique votre nom et le service vous hébergeant, tout le monde peut interagir avec vous à l'aide de <button>plateformes sociales implémentant ActivityPub</button>.",
"domain_pill.your_handle": "Votre identifiant :",
"domain_pill.your_server": "Votre foyer numérique, là où vos messages résident. Vous souhaitez changer ? Lancez un transfert vers un autre serveur quand vous le voulez et vos abonné·e·s suivront automatiquement.",
"domain_pill.your_username": "Votre identifiant unique sur ce serveur. Il est possible de rencontrer des utilisateur·rice·s ayant le même nom d'utilisateur sur différents serveurs.",
"dropdown.empty": "Sélectionner une option",
"embed.instructions": "Intégrez ce message à votre site en copiant le code ci-dessous.",
"embed.instructions": "Intégrer ce message à votre site en copiant le code ci-dessous.",
"embed.preview": "Il apparaîtra comme cela:",
"emoji_button.activity": "Activités",
"emoji_button.clear": "Effacer",
@@ -1018,7 +1035,7 @@
"server_banner.about_active_users": "Personnes utilisant ce serveur au cours des 30 derniers jours (Comptes actifs mensuellement)",
"server_banner.active_users": "comptes actifs",
"server_banner.administered_by": "Administré par :",
"server_banner.is_one_of_many": "{domain} est l'un des nombreux serveurs Mastodon indépendants que vous pouvez utiliser pour participer au fédiverse.",
"server_banner.is_one_of_many": "{domain} est l'un des nombreux serveurs Mastodon indépendants que vous pouvez utiliser pour participer au fédivers.",
"server_banner.server_stats": "Statistiques du serveur :",
"sign_in_banner.create_account": "Créer un compte",
"sign_in_banner.follow_anyone": "Suivez n'importe qui à travers le fédivers et affichez tout dans un ordre chronologique. Ni algorithmes, ni publicités, ni appâts à clics en perspective.",
@@ -1120,7 +1137,7 @@
"status.unpin": "Retirer du profil",
"subscribed_languages.lead": "Seuls les messages dans les langues sélectionnées apparaîtront sur votre fil principal et vos listes de fils après le changement. Sélectionnez aucune pour recevoir les messages dans toutes les langues.",
"subscribed_languages.save": "Enregistrer les modifications",
"subscribed_languages.target": "Changer les langues abonnées pour {target}",
"subscribed_languages.target": "Modifier les langues d'abonnements pour {target}",
"tabs_bar.home": "Accueil",
"tabs_bar.menu": "Menu",
"tabs_bar.notifications": "Notifications",

View File

@@ -141,8 +141,25 @@
"account.unmute": "Deixar de silenciar a @{name}",
"account.unmute_notifications_short": "Reactivar notificacións",
"account.unmute_short": "Non silenciar",
"account_edit.bio.placeholder": "Escribe unha breve presentación para que te coñezan mellor.",
"account_edit.bio.title": "Sobre ti",
"account_edit.bio_modal.add_title": "Engadir biografía",
"account_edit.bio_modal.edit_title": "Editar biografía",
"account_edit.char_counter": "{currentLength}/{maxLength} caracteres",
"account_edit.column_button": "Feito",
"account_edit.column_title": "Editar perfil",
"account_edit.custom_fields.placeholder": "Engade os teus pronomes, ligazóns externas, ou o que queiras compartir.",
"account_edit.custom_fields.title": "Campos personalizados",
"account_edit.display_name.placeholder": "O nome público é o nome que aparece no perfil e nas cronoloxías.",
"account_edit.display_name.title": "Nome público",
"account_edit.featured_hashtags.placeholder": "Facilita que te identifiquen, e da acceso rápido aos teus intereses favoritos.",
"account_edit.featured_hashtags.title": "Cancelos destacados",
"account_edit.name_modal.add_title": "Engadir nome público",
"account_edit.name_modal.edit_title": "Editar o nome público",
"account_edit.profile_tab.subtitle": "Personaliza as pestanas e o seu contido no teu perfil.",
"account_edit.profile_tab.title": "Perfil e axustes das pestanas",
"account_edit.save": "Gardar",
"account_edit.section_edit_button": "Editar",
"account_note.placeholder": "Preme para engadir nota",
"admin.dashboard.daily_retention": "Ratio de retención de usuarias diaria após rexistrarse",
"admin.dashboard.monthly_retention": "Ratio de retención de usuarias mensual após o rexistro",
@@ -267,6 +284,7 @@
"collections.detail.curated_by_you": "Seleccionadas por ti",
"collections.detail.loading": "Cargando colección…",
"collections.detail.share": "Compartir esta colección",
"collections.edit_details": "Editar detalles",
"collections.error_loading_collections": "Houbo un erro ao intentar cargar as túas coleccións.",
"collections.hints.accounts_counter": "{count} / {max} contas",
"collections.hints.add_more_accounts": "Engade polo menos {count, plural, one {# conta} other {# contas}} para continuar",
@@ -275,11 +293,14 @@
"collections.manage_accounts": "Xestionar contas",
"collections.mark_as_sensitive": "Marcar como sensible",
"collections.mark_as_sensitive_hint": "Oculta a descrición e contas da colección detrás dun aviso sobre o contido. O nome da colección permanece visible.",
"collections.name_length_hint": "Límite de 40 caracteres",
"collections.new_collection": "Nova colección",
"collections.no_collections_yet": "Aínda non tes coleccións.",
"collections.old_last_post_note": "Hai máis dunha semana da última publicación",
"collections.remove_account": "Retirar esta conta",
"collections.search_accounts_label": "Buscar contas para engadir…",
"collections.search_accounts_max_reached": "Acadaches o máximo de contas permitidas",
"collections.sensitive": "Sensible",
"collections.topic_hint": "Engadir un cancelo para que axudar a que outras persoas coñezan a temática desta colección.",
"collections.view_collection": "Ver colección",
"collections.visibility_public": "Pública",
@@ -370,6 +391,9 @@
"confirmations.discard_draft.post.title": "Desbotar o borrador?",
"confirmations.discard_edit_media.confirm": "Descartar",
"confirmations.discard_edit_media.message": "Tes cambios sen gardar para a vista previa ou descrición do multimedia, descartamos os cambios?",
"confirmations.follow_to_collection.confirm": "Seguir e engadir á colección",
"confirmations.follow_to_collection.message": "Tes que seguir a {name} para poder engadila a unha colección.",
"confirmations.follow_to_collection.title": "Seguir a conta?",
"confirmations.follow_to_list.confirm": "Seguir e engadir á lista",
"confirmations.follow_to_list.message": "Tes que seguir a {name} para poder engadila a unha lista.",
"confirmations.follow_to_list.title": "Seguir á usuaria?",

View File

@@ -141,8 +141,25 @@
"account.unmute": "הפסקת השתקת @{name}",
"account.unmute_notifications_short": "הפעלת הודעות",
"account.unmute_short": "ביטול השתקה",
"account_edit.bio.placeholder": "הוסיפו הצגה קצרה כדי לעזור לאחרים לזהות אותך.",
"account_edit.bio.title": "ביוגרפיה",
"account_edit.bio_modal.add_title": "הוסיפו ביוגרפיה",
"account_edit.bio_modal.edit_title": "עריכת ביוגרפיה",
"account_edit.char_counter": "{currentLength}/{maxLength} תווים",
"account_edit.column_button": "סיום",
"account_edit.column_title": "עריכת הפרופיל",
"account_edit.custom_fields.placeholder": "הוסיפו צורת פניה, קישורים חיצוניים וכל דבר שתרצו לשתף.",
"account_edit.custom_fields.title": "שדות בהתאמה אישית",
"account_edit.display_name.placeholder": "שם התצוגה שלכן הוא איך שהשם יופיע בפרופיל ובצירי הזמנים.",
"account_edit.display_name.title": "שם תצוגה",
"account_edit.featured_hashtags.placeholder": "עזרו לאחרים לזהות ולגשת בקלות לנושאים החביבים עליכם.",
"account_edit.featured_hashtags.title": "תגיות נבחרות",
"account_edit.name_modal.add_title": "הוספת שם תצוגה",
"account_edit.name_modal.edit_title": "עריכת שם תצוגה",
"account_edit.profile_tab.subtitle": "התאימו את הטאבים בפרופיל שלכם ומה שהם יציגו.",
"account_edit.profile_tab.title": "הגדרות טאבים לפרופיל",
"account_edit.save": "שמירה",
"account_edit.section_edit_button": "עריכה",
"account_note.placeholder": "יש ללחוץ כדי להוסיף הערות",
"admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה",
"admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה",
@@ -267,6 +284,7 @@
"collections.detail.curated_by_you": "נאצר על ידיך",
"collections.detail.loading": "טוען אוסף…",
"collections.detail.share": "שיתוף אוסף",
"collections.edit_details": "עריכת פרטים",
"collections.error_loading_collections": "חלה שגיאה בנסיון לטעון את אוספיך.",
"collections.hints.accounts_counter": "{count} \\ {max} חשבונות",
"collections.hints.add_more_accounts": "הוסיפו לפחות {count, plural,one {חשבון אחד}other {# חשבונות}} כדי להמשיך",
@@ -275,11 +293,14 @@
"collections.manage_accounts": "ניהול חשבונות",
"collections.mark_as_sensitive": "מסומנים כרגישים",
"collections.mark_as_sensitive_hint": "הסתרת תיאור וחשבונות האוסף מאחורי אזהרת תוכן. שם האוסף עדיין ישאר גלוי.",
"collections.name_length_hint": "מגבלה של 40 תווים",
"collections.new_collection": "אוסף חדש",
"collections.no_collections_yet": "עוד אין אוספים.",
"collections.old_last_post_note": "פרסמו לאחרונה לפני יותר משבוע",
"collections.remove_account": "הסר חשבון זה",
"collections.search_accounts_label": "לחפש חשבונות להוספה…",
"collections.search_accounts_max_reached": "הגעת למספר החשבונות המירבי",
"collections.sensitive": "רגיש",
"collections.topic_hint": "הוספת תגית שמסייעת לאחרים להבין את הנושא הראשי של האוסף.",
"collections.view_collection": "צפיה באוסף",
"collections.visibility_public": "פומבי",
@@ -370,6 +391,9 @@
"confirmations.discard_draft.post.title": "לוותר על הטיוטא?",
"confirmations.discard_edit_media.confirm": "השלך",
"confirmations.discard_edit_media.message": "יש לך שינויים לא שמורים לתיאור המדיה. להשליך אותם בכל זאת?",
"confirmations.follow_to_collection.confirm": "עקיבה והוספה לאוסף",
"confirmations.follow_to_collection.message": "כדי להכניס את {name} לאוסף, ראשית יש לעקוב אחריהם.",
"confirmations.follow_to_collection.title": "לעקוב אחר החשבון?",
"confirmations.follow_to_list.confirm": "עקיבה והוספה לרשימה",
"confirmations.follow_to_list.message": "כדי להכניס את {name} לרשימה, ראשית יש לעקוב אחריהם.",
"confirmations.follow_to_list.title": "לעקוב אחר המשתמש.ת?",

View File

@@ -8,10 +8,13 @@
"about.domain_blocks.silenced.title": "Ograničen",
"about.domain_blocks.suspended.explanation": "Podatci s ovog poslužitelja neće se obrađivati, pohranjivati ili razmjenjivati, što onemogućuje bilo kakvu interakciju ili komunikaciju s korisnicima s ovog poslužitelja.",
"about.domain_blocks.suspended.title": "Suspendiran",
"about.language_label": "Jezik",
"about.not_available": "Te informacije nisu dostupne na ovom poslužitelju.",
"about.powered_by": "Decentralizirani društveni mediji koje pokreće {mastodon}",
"about.rules": "Pravila servera",
"account.activity": "Aktivnost",
"account.add_or_remove_from_list": "Dodaj ili ukloni s liste",
"account.badges.admin": "Admin",
"account.badges.bot": "Bot",
"account.badges.group": "Grupa",
"account.block": "Blokiraj @{name}",
@@ -23,12 +26,16 @@
"account.direct": "Privatno spomeni @{name}",
"account.disable_notifications": "Nemoj me obavjestiti kada @{name} napravi objavu",
"account.edit_profile": "Uredi profil",
"account.edit_profile_short": "Uredi",
"account.enable_notifications": "Obavjesti me kada @{name} napravi objavu",
"account.endorse": "Istakni na profilu",
"account.featured_tags.last_status_at": "Zadnji post {date}",
"account.featured_tags.last_status_never": "Nema postova",
"account.follow": "Prati",
"account.follow_back": "Slijedi natrag",
"account.follow_request_cancel": "Poništi zahtjev",
"account.follow_request_cancel_short": "Odustani",
"account.follow_request_short": "Zatraži",
"account.followers": "Pratitelji",
"account.followers.empty": "Nitko još ne prati korisnika/cu.",
"account.following": "Pratim",
@@ -42,11 +49,19 @@
"account.locked_info": "Status privatnosti ovog računa postavljen je na zaključano. Vlasnik ručno pregledava tko ih može pratiti.",
"account.media": "Medijski sadržaj",
"account.mention": "Spomeni @{name}",
"account.menu.block": "Blokiraj račun",
"account.menu.block_domain": "Blokiraj {domain}",
"account.menu.copy": "Kopiraj poveznicu",
"account.menu.mention": "Spomeni",
"account.menu.remove_follower": "Ukloni pratitelja",
"account.menu.report": "Prijavi račun",
"account.mute": "Utišaj @{name}",
"account.mute_notifications_short": "Utišaj obavijesti",
"account.mute_short": "Utišaj",
"account.muted": "Utišano",
"account.no_bio": "Nije dan opis.",
"account.node_modal.save": "Spremi",
"account.note.edit_button": "Uredi",
"account.open_original_page": "Otvori originalnu stranicu",
"account.posts": "Objave",
"account.posts_with_replies": "Objave i odgovori",
@@ -62,6 +77,17 @@
"account.unmute": "Poništi utišavanje @{name}",
"account.unmute_notifications_short": "Uključi utišane obavijesti",
"account.unmute_short": "Poništi utišavanje",
"account_edit.bio.title": "Biografija",
"account_edit.bio_modal.add_title": "Dodaj biografiju",
"account_edit.bio_modal.edit_title": "Uredi biografiju",
"account_edit.char_counter": "{currentLength}/{maxLength} znakova",
"account_edit.column_button": "Završi",
"account_edit.column_title": "Uredi profil",
"account_edit.custom_fields.title": "Prilagođena polja",
"account_edit.featured_hashtags.title": "Istaknuti hashtagovi",
"account_edit.profile_tab.title": "Prikaz kartice profila",
"account_edit.save": "Spremi",
"account_edit.section_edit_button": "Uredi",
"account_note.placeholder": "Kliknite za dodavanje bilješke",
"admin.dashboard.daily_retention": "Stopa zadržavanja korisnika po danu nakon prijave",
"admin.dashboard.monthly_retention": "Stopa zadržavanja korisnika po mjesecu nakon prijave",
@@ -76,7 +102,15 @@
"alert.rate_limited.title": "Ograničenje učestalosti",
"alert.unexpected.message": "Dogodila se neočekivana greška.",
"alert.unexpected.title": "Ups!",
"alt_text_badge.title": "Alternativni tekst",
"alt_text_modal.add_alt_text": "Dodaj altenativni tekst",
"alt_text_modal.add_text_from_image": "Dodaj tekst iz slike",
"alt_text_modal.cancel": "Odustani",
"alt_text_modal.done": "Gotovo",
"announcement.announcement": "Najava",
"annual_report.announcement.action_dismiss": "Ne hvala",
"annual_report.nav_item.badge": "Novi",
"annual_report.shared_page.donate": "Doniraj",
"attachments_list.unprocessed": "(neobrađeno)",
"audio.hide": "Sakrij audio",
"boost_modal.combo": "Možete pritisnuti {combo} kako biste preskočili ovo sljedeći put",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "40 stafa takmörk",
"collections.new_collection": "Nýtt safn",
"collections.no_collections_yet": "Engin söfn ennþá.",
"collections.old_last_post_note": "Birti síðast fyrir meira en viku síðan",
"collections.remove_account": "Fjarlægja þennan aðgang",
"collections.search_accounts_label": "Leita að aðgöngum til að bæta við…",
"collections.search_accounts_max_reached": "Þú hefur þegar bætt við leyfilegum hámarksfjölda aðganga",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Henda drögum að færslunni þinni?",
"confirmations.discard_edit_media.confirm": "Henda",
"confirmations.discard_edit_media.message": "Þú ert með óvistaðar breytingar á lýsingu myndefnis eða forskoðunar, henda þeim samt?",
"confirmations.follow_to_collection.confirm": "Fylgjast með og bæta í safn",
"confirmations.follow_to_collection.message": "Þú þarft að fylgjast með {name} til að geta bætt viðkomandi í safn.",
"confirmations.follow_to_collection.title": "Fylgjast með notandaaðgangnum?",
"confirmations.follow_to_list.confirm": "Fylgjast með og bæta á lista",
"confirmations.follow_to_list.message": "Þú þarft að fylgjast með {name} til að bæta viðkomandi á lista.",
"confirmations.follow_to_list.title": "Fylgjast með notanda?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "Limite di 40 caratteri",
"collections.new_collection": "Nuova collezione",
"collections.no_collections_yet": "Nessuna collezione ancora.",
"collections.old_last_post_note": "Ultimo post più di una settimana fa",
"collections.remove_account": "Rimuovi questo account",
"collections.search_accounts_label": "Cerca account da aggiungere…",
"collections.search_accounts_max_reached": "Hai aggiunto il numero massimo di account",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Scartare la tua bozza del post?",
"confirmations.discard_edit_media.confirm": "Scarta",
"confirmations.discard_edit_media.message": "Hai delle modifiche non salvate alla descrizione o anteprima del media, scartarle comunque?",
"confirmations.follow_to_collection.confirm": "Segui e aggiungi alla collezione",
"confirmations.follow_to_collection.message": "Devi seguire {name} per aggiungerlo/a ad una collezione.",
"confirmations.follow_to_collection.title": "Seguire l'account?",
"confirmations.follow_to_list.confirm": "Segui e aggiungi alla lista",
"confirmations.follow_to_list.message": "Devi seguire {name} per aggiungerli a una lista.",
"confirmations.follow_to_list.title": "Seguire l'utente?",

View File

@@ -141,6 +141,25 @@
"account.unmute": "取消消音 @{name}",
"account.unmute_notifications_short": "Kā通知取消消音",
"account.unmute_short": "取消消音",
"account_edit.bio.placeholder": "加一段短紹介幫tsān別lâng認捌lí。",
"account_edit.bio.title": "個人紹介",
"account_edit.bio_modal.add_title": "加添個人紹介",
"account_edit.bio_modal.edit_title": "編個人紹介",
"account_edit.char_counter": "{currentLength}/{maxLength} ê字",
"account_edit.column_button": "做好ah",
"account_edit.column_title": "編輯個人資料",
"account_edit.custom_fields.placeholder": "加lí ê代名詞、外部連結á是其他lí beh分享ê。",
"account_edit.custom_fields.title": "自訂欄",
"account_edit.display_name.placeholder": "Lí ê顯示ê名是lí ê名佇lí ê個人資料kap時間線出現ê方式。",
"account_edit.display_name.title": "顯示ê名",
"account_edit.featured_hashtags.placeholder": "幫tsān別lâng認捌kap緊緊接近使用lí收藏ê主題。",
"account_edit.featured_hashtags.title": "特色ê hashtag",
"account_edit.name_modal.add_title": "加添顯示ê名",
"account_edit.name_modal.edit_title": "編顯示ê名",
"account_edit.profile_tab.subtitle": "自訂lí ê個人資料ê分頁kap顯示ê內容。",
"account_edit.profile_tab.title": "個人資料分頁設定",
"account_edit.save": "儲存",
"account_edit.section_edit_button": "編輯",
"account_note.placeholder": "Tshi̍h tse加註kha",
"admin.dashboard.daily_retention": "註冊以後ê用者維持率用kang計算",
"admin.dashboard.monthly_retention": "註冊以後ê用者維持率",
@@ -244,9 +263,12 @@
"closed_registrations_modal.preamble": "因為Mastodon非中心化所以bô論tī tá tsi̍t ê服侍器建立口座lí lóng ē當跟tuè tsi̍t ê服侍器ê逐ê lângkap hām in交流。Lí iā ē當ka-tī起tsi̍t ê站!",
"closed_registrations_modal.title": "註冊 Mastodon ê口座",
"collections.account_count": "{count, plural, other {# ê口座}}",
"collections.accounts.empty_description": "加lí跟tuè ê口座上tsē {count} ê",
"collections.accounts.empty_title": "收藏內底無半項",
"collections.collection_description": "說明",
"collections.collection_name": "名",
"collections.collection_topic": "主題",
"collections.confirm_account_removal": "Lí確定beh對收藏suá掉tsit ê口座?",
"collections.content_warning": "內容警告",
"collections.continue": "繼續",
"collections.create.accounts_subtitle": "Kan-ta通加lí所綴而且選擇加入探索ê。",
@@ -257,13 +279,28 @@
"collections.create_collection": "建立收藏",
"collections.delete_collection": "Thâi掉收藏",
"collections.description_length_hint": "限制 100 字",
"collections.detail.accounts_heading": "口座",
"collections.detail.curated_by_author": "{author} 揀ê",
"collections.detail.curated_by_you": "Lí揀ê",
"collections.detail.loading": "載入收藏……",
"collections.detail.share": "分享tsit ê收藏",
"collections.edit_details": "編輯詳細",
"collections.error_loading_collections": "佇載入lí ê收藏ê時陣出tshê。",
"collections.hints.accounts_counter": "{count} / {max} ê口座",
"collections.hints.add_more_accounts": "加上無 {count, plural, other {# ê口座}}來繼續",
"collections.hints.can_not_remove_more_accounts": "收藏定著愛上無 {count, plural, other {# ê口座}}。Bē當suá掉koh較tsē口座。",
"collections.last_updated_at": "上尾更新tī{date}",
"collections.manage_accounts": "管理口座",
"collections.mark_as_sensitive": "標做敏感ê",
"collections.mark_as_sensitive_hint": "Kā收藏ê描述kap口座tshàng佇內容警告ê後壁。收藏ê名猶原會當看。",
"collections.name_length_hint": "限制 40 字",
"collections.new_collection": "新ê收藏",
"collections.no_collections_yet": "Iáu無收藏。",
"collections.old_last_post_note": "頂改佇超過一禮拜進前PO文",
"collections.remove_account": "Suá掉tsit ê口座",
"collections.search_accounts_label": "Tshuē口座來加添……",
"collections.search_accounts_max_reached": "Lí已經加kàu口座數ê盡磅ah。",
"collections.sensitive": "敏感ê",
"collections.topic_hint": "加 hashtag幫tsān別lâng了解tsit ê收藏ê主題。",
"collections.view_collection": "看收藏",
"collections.visibility_public": "公共ê",
@@ -354,6 +391,9 @@
"confirmations.discard_draft.post.title": "Kám beh棄sak lí PO文ê草稿",
"confirmations.discard_edit_media.confirm": "棄sak",
"confirmations.discard_edit_media.message": "Lí佇媒體敘述á是先看māi ê所在有iáu buē儲存ê改變kám beh kā in棄sak",
"confirmations.follow_to_collection.confirm": "跟tuè加入kàu收藏",
"confirmations.follow_to_collection.message": "Beh kā {name} 加添kàu收藏lí tio̍h先跟tuè伊。",
"confirmations.follow_to_collection.title": "敢beh跟tuè口座",
"confirmations.follow_to_list.confirm": "跟tuè加入kàu列單",
"confirmations.follow_to_list.message": "Beh kā {name} 加添kàu列單lí tio̍h先跟tuè伊。",
"confirmations.follow_to_list.title": "Kám beh跟tuè tsit ê用者?",

View File

@@ -141,8 +141,25 @@
"account.unmute": "@{name} niet langer negeren",
"account.unmute_notifications_short": "Meldingen niet langer negeren",
"account.unmute_short": "Niet langer negeren",
"account_edit.bio.placeholder": "Voeg een korte introductie toe om anderen te helpen je te identificeren.",
"account_edit.bio.title": "Biografie",
"account_edit.bio_modal.add_title": "Biografie toevoegen",
"account_edit.bio_modal.edit_title": "Biografie bewerken",
"account_edit.char_counter": "{currentLength}/{maxLength} tekens",
"account_edit.column_button": "Klaar",
"account_edit.column_title": "Profiel bewerken",
"account_edit.custom_fields.placeholder": "Voeg je voornaamwoorden, externe links of iets anders toe dat je wilt delen.",
"account_edit.custom_fields.title": "Aangepaste velden",
"account_edit.display_name.placeholder": "Je weergavenaam wordt weergegeven op jouw profiel en in tijdlijnen.",
"account_edit.display_name.title": "Weergavenaam",
"account_edit.featured_hashtags.placeholder": "Help anderen je favoriete onderwerpen te identificeren en er snel toegang toe te hebben.",
"account_edit.featured_hashtags.title": "Uitgelichte hashtags",
"account_edit.name_modal.add_title": "Weergavenaam toevoegen",
"account_edit.name_modal.edit_title": "Weergavenaam bewerken",
"account_edit.profile_tab.subtitle": "Pas de tabbladen op je profiel aan en wat ze weergeven.",
"account_edit.profile_tab.title": "Instellingen voor tabblad Profiel",
"account_edit.save": "Opslaan",
"account_edit.section_edit_button": "Bewerken",
"account_note.placeholder": "Klik om een opmerking toe te voegen",
"admin.dashboard.daily_retention": "Retentiegraad van gebruikers per dag, vanaf registratie",
"admin.dashboard.monthly_retention": "Retentiegraad van gebruikers per maand, vanaf registratie",
@@ -267,6 +284,7 @@
"collections.detail.curated_by_you": "Samengesteld door jou",
"collections.detail.loading": "Verzameling laden…",
"collections.detail.share": "Deze verzameling delen",
"collections.edit_details": "Gegevens bewerken",
"collections.error_loading_collections": "Er is een fout opgetreden bij het laden van je verzamelingen.",
"collections.hints.accounts_counter": "{count} / {max} accounts",
"collections.hints.add_more_accounts": "Voeg ten minste {count, plural, one {# account} other {# accounts}} toe om door te gaan",
@@ -275,12 +293,15 @@
"collections.manage_accounts": "Accounts beheren",
"collections.mark_as_sensitive": "Als gevoelig markeren",
"collections.mark_as_sensitive_hint": "Verbergt de omschrijving en de accounts van de verzameling achter een waarschuwing. De naam van de verzameling blijft zichtbaar.",
"collections.name_length_hint": "Limiet van 40 tekens",
"collections.new_collection": "Nieuwe verzameling",
"collections.no_collections_yet": "Nog geen verzamelingen.",
"collections.old_last_post_note": "Laatst gepost over een week geleden",
"collections.remove_account": "Deze account verwijderen",
"collections.search_accounts_label": "Zoek naar accounts om toe te voegen…",
"collections.search_accounts_max_reached": "Je hebt het maximum aantal accounts toegevoegd",
"collections.topic_hint": "Voeg een hashtag toe die anderen helpt het hoofdonderwerp van deze collectie te begrijpen.",
"collections.sensitive": "Gevoelig",
"collections.topic_hint": "Voeg een hashtag toe die anderen helpt het hoofdonderwerp van deze verzameling te begrijpen.",
"collections.view_collection": "Verzameling bekijken",
"collections.visibility_public": "Openbaar",
"collections.visibility_public_hint": "Te zien onder zoekresultaten en in andere gebieden waar aanbevelingen verschijnen.",
@@ -370,6 +391,9 @@
"confirmations.discard_draft.post.title": "Conceptbericht verwijderen?",
"confirmations.discard_edit_media.confirm": "Verwijderen",
"confirmations.discard_edit_media.message": "Je hebt niet-opgeslagen wijzigingen in de mediabeschrijving of voorvertonning, wil je deze toch verwijderen?",
"confirmations.follow_to_collection.confirm": "Volgen en toevoegen aan verzameling",
"confirmations.follow_to_collection.message": "Je moet {name} volgen om ze aan een verzameling toe te voegen.",
"confirmations.follow_to_collection.title": "Account volgen?",
"confirmations.follow_to_list.confirm": "Volgen en toevoegen aan de lijst",
"confirmations.follow_to_list.message": "Je moet {name} volgen om ze toe te voegen aan een lijst.",
"confirmations.follow_to_list.title": "Gebruiker volgen?",

View File

@@ -141,6 +141,25 @@
"account.unmute": "Opphev demping av @{name}",
"account.unmute_notifications_short": "Opphev demping av varslingar",
"account.unmute_short": "Opphev demping",
"account_edit.bio.placeholder": "Skriv ei kort innleiing slik at andre kan sjå kven du er.",
"account_edit.bio.title": "Om meg",
"account_edit.bio_modal.add_title": "Skriv om deg sjølv",
"account_edit.bio_modal.edit_title": "Endre bio",
"account_edit.char_counter": "{currentLength}/{maxLength} teikn",
"account_edit.column_button": "Ferdig",
"account_edit.column_title": "Rediger profil",
"account_edit.custom_fields.placeholder": "Legg til pronomen, lenkjer eller kva du elles vil dela.",
"account_edit.custom_fields.title": "Eigne felt",
"account_edit.display_name.placeholder": "Det synlege namnet ditt er det som syner på profilen din og i tidsliner.",
"account_edit.display_name.title": "Synleg namn",
"account_edit.featured_hashtags.placeholder": "Hjelp andre å finna og få rask tilgang til favorittemna dine.",
"account_edit.featured_hashtags.title": "Utvalde emneknaggar",
"account_edit.name_modal.add_title": "Legg til synleg namn",
"account_edit.name_modal.edit_title": "Endre synleg namn",
"account_edit.profile_tab.subtitle": "Tilpass fanene på profilen din og kva dei syner.",
"account_edit.profile_tab.title": "Innstillingar for profilfane",
"account_edit.save": "Lagre",
"account_edit.section_edit_button": "Rediger",
"account_note.placeholder": "Klikk for å leggja til merknad",
"admin.dashboard.daily_retention": "Mengda brukarar aktive ved dagar etter registrering",
"admin.dashboard.monthly_retention": "Mengda brukarar aktive ved månader etter registrering",
@@ -260,6 +279,12 @@
"collections.create_collection": "Lag ei samling",
"collections.delete_collection": "Slett samlinga",
"collections.description_length_hint": "Maks 100 teikn",
"collections.detail.accounts_heading": "Kontoar",
"collections.detail.curated_by_author": "Kuratert av {author}",
"collections.detail.curated_by_you": "Kuratert av deg",
"collections.detail.loading": "Lastar inn samling…",
"collections.detail.share": "Del denne samlinga",
"collections.edit_details": "Rediger detaljar",
"collections.error_loading_collections": "Noko gjekk gale då me prøvde å henta samlingane dine.",
"collections.hints.accounts_counter": "{count} av {max} kontoar",
"collections.hints.add_more_accounts": "Legg til minst {count, plural, one {# konto} other {# kontoar}} for å halda fram",
@@ -268,11 +293,14 @@
"collections.manage_accounts": "Handter kontoar",
"collections.mark_as_sensitive": "Merk som ømtolig",
"collections.mark_as_sensitive_hint": "Gøymer skildringa og kontoane i samlinga bak ei innhaldsåtvaring. Namnet på samlinga blir framleis synleg.",
"collections.name_length_hint": "Maks 40 teikn",
"collections.new_collection": "Ny samling",
"collections.no_collections_yet": "Du har ingen samlingar enno.",
"collections.old_last_post_note": "Sist lagt ut for over ei veke sidan",
"collections.remove_account": "Fjern denne kontoen",
"collections.search_accounts_label": "Søk etter kontoar å leggja til…",
"collections.search_accounts_max_reached": "Du har nådd grensa for kor mange kontoar du kan leggja til",
"collections.sensitive": "Ømtolig",
"collections.topic_hint": "Legg til ein emneknagg som hjelper andre å forstå hovudemnet for denne samlinga.",
"collections.view_collection": "Sjå samlinga",
"collections.visibility_public": "Offentleg",
@@ -363,6 +391,9 @@
"confirmations.discard_draft.post.title": "Forkast kladden?",
"confirmations.discard_edit_media.confirm": "Forkast",
"confirmations.discard_edit_media.message": "Du har ulagra endringar i mediaskildringa eller førehandsvisinga. Vil du forkasta dei likevel?",
"confirmations.follow_to_collection.confirm": "Fylg og legg til samlinga",
"confirmations.follow_to_collection.message": "Du må fylgja {name} for å leggja dei til ei samling.",
"confirmations.follow_to_collection.title": "Fylg kontoen?",
"confirmations.follow_to_list.confirm": "Fylg og legg til lista",
"confirmations.follow_to_list.message": "Du må fylgja {name} for å leggja dei til ei liste.",
"confirmations.follow_to_list.title": "Vil du fylgja brukaren?",

View File

@@ -293,6 +293,7 @@
"collections.name_length_hint": "Kufi 40 shenja",
"collections.new_collection": "Koleksion i ri",
"collections.no_collections_yet": "Ende pa koleksione.",
"collections.old_last_post_note": "Të postuarat e fundit gjatë një jave më parë",
"collections.remove_account": "Hiqe këtë llogari",
"collections.search_accounts_label": "Kërkoni për llogari për shtim…",
"collections.search_accounts_max_reached": "Keni shtuar numrin maksimum të llogarive",
@@ -387,6 +388,9 @@
"confirmations.discard_draft.post.title": "Të hidhet tej skica e postimit tuaj?",
"confirmations.discard_edit_media.confirm": "Hidhe tej",
"confirmations.discard_edit_media.message": "Keni ndryshime të paruajtura te përshkrimi ose paraparja e medias, të hidhen tej, sido qoftë?",
"confirmations.follow_to_collection.confirm": "Ndiqe dhe shtoje në koleksion",
"confirmations.follow_to_collection.message": "Që ta shtoni te një koleksion, duhet të jeni duke e ndjekur {name}.",
"confirmations.follow_to_collection.title": "Të ndiqet llogaria?",
"confirmations.follow_to_list.confirm": "Ndiqe dhe shtoje te listë",
"confirmations.follow_to_list.message": "Lypset të jeni duke e ndjekur {name}, që të shtohte te një listë.",
"confirmations.follow_to_list.title": "Të ndiqet përdoruesi?",

View File

@@ -141,8 +141,25 @@
"account.unmute": "@{name} adlı kişinin sesini aç",
"account.unmute_notifications_short": "Bildirimlerin sesini aç",
"account.unmute_short": "Susturmayı kaldır",
"account_edit.bio.placeholder": "Diğerlerinin sizi tanımasına yardımcı olmak için kısa bir tanıtım ekleyin.",
"account_edit.bio.title": "Kişisel bilgiler",
"account_edit.bio_modal.add_title": "Kişisel bilgi ekle",
"account_edit.bio_modal.edit_title": "Kişisel bilgiyi düzenle",
"account_edit.char_counter": "{currentLength}/{maxLength} karakter",
"account_edit.column_button": "Tamamlandı",
"account_edit.column_title": "Profili Düzenle",
"account_edit.custom_fields.placeholder": "Zamirlerinizi, harici bağlantılarınızı veya paylaşmak istediğiniz diğer bilgileri ekleyin.",
"account_edit.custom_fields.title": "Özel alanlar",
"account_edit.display_name.placeholder": "Görünen adınız profilinizde ve zaman akışlarında adınızın nasıl göründüğüdür.",
"account_edit.display_name.title": "Görünen ad",
"account_edit.featured_hashtags.placeholder": "Başkalarının favori konularınızı tanımlamasına ve bunlara hızlı bir şekilde erişmesine yardımcı olun.",
"account_edit.featured_hashtags.title": "Öne çıkan etiketler",
"account_edit.name_modal.add_title": "Görünen ad ekle",
"account_edit.name_modal.edit_title": "Görünen adı düzenle",
"account_edit.profile_tab.subtitle": "Profilinizdeki sekmeleri ve bunların görüntülediği bilgileri özelleştirin.",
"account_edit.profile_tab.title": "Profil sekme ayarları",
"account_edit.save": "Kaydet",
"account_edit.section_edit_button": "Düzenle",
"account_note.placeholder": "Not eklemek için tıklayın",
"admin.dashboard.daily_retention": "Kayıttan sonra günlük kullanıcı saklama oranı",
"admin.dashboard.monthly_retention": "Kayıttan sonra aylık kullanıcı saklama oranı",
@@ -267,6 +284,7 @@
"collections.detail.curated_by_you": "Sizin derledikleriniz",
"collections.detail.loading": "Koleksiyon yükleniyor…",
"collections.detail.share": "Bu koleksiyonu paylaş",
"collections.edit_details": "Ayrıntıları düzenle",
"collections.error_loading_collections": "Koleksiyonlarınızı yüklemeye çalışırken bir hata oluştu.",
"collections.hints.accounts_counter": "{count} / {max} hesap",
"collections.hints.add_more_accounts": "Devam etmek için en az {count, plural, one {# hesap} other {# hesap}} ekleyin",
@@ -275,11 +293,14 @@
"collections.manage_accounts": "Hesapları yönet",
"collections.mark_as_sensitive": "Hassas olarak işaretle",
"collections.mark_as_sensitive_hint": "Koleksiyonun açıklamasını ve hesaplarını içerik uyarısının arkasında gizler. Koleksiyon adı hala görünür olacaktır.",
"collections.name_length_hint": "40 karakterle sınırlı",
"collections.new_collection": "Yeni koleksiyon",
"collections.no_collections_yet": "Henüz hiçbir koleksiyon yok.",
"collections.old_last_post_note": "Son gönderi bir haftadan önce",
"collections.remove_account": "Bu hesabı çıkar",
"collections.search_accounts_label": "Eklemek için hesap arayın…",
"collections.search_accounts_max_reached": "Maksimum hesabı eklediniz",
"collections.sensitive": "Hassas",
"collections.topic_hint": "Bu koleksiyonun ana konusunu başkalarının anlamasına yardımcı olacak bir etiket ekleyin.",
"collections.view_collection": "Koleksiyonu görüntüle",
"collections.visibility_public": "Herkese açık",
@@ -370,6 +391,9 @@
"confirmations.discard_draft.post.title": "Taslak gönderiniz silinsin mi?",
"confirmations.discard_edit_media.confirm": "Vazgeç",
"confirmations.discard_edit_media.message": "Medya açıklaması veya ön izlemede kaydedilmemiş değişiklikleriniz var, yine de vazgeçmek istiyor musunuz?",
"confirmations.follow_to_collection.confirm": "Takip et ve koleksiyona ekle",
"confirmations.follow_to_collection.message": "Bir koleksiyona eklemek için {name} kişisini takip etmeniz gerekiyor.",
"confirmations.follow_to_collection.title": "Hesabı takip et?",
"confirmations.follow_to_list.confirm": "Takip et ve yapılacaklar listesine ekle",
"confirmations.follow_to_list.message": "Bir listeye eklemek için {name} kişisini takip etmeniz gerekiyor.",
"confirmations.follow_to_list.title": "Kullanıcıyı takip et?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "Giới hạn 40 ký tự",
"collections.new_collection": "Collection mới",
"collections.no_collections_yet": "Chưa có collection.",
"collections.old_last_post_note": "Đăng lần cuối hơn một tuần trước",
"collections.remove_account": "Gỡ tài khoản này",
"collections.search_accounts_label": "Tìm tài khoản để thêm…",
"collections.search_accounts_max_reached": "Bạn đã đạt đến số lượng tài khoản tối đa",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "Bỏ tút đang soạn?",
"confirmations.discard_edit_media.confirm": "Bỏ qua",
"confirmations.discard_edit_media.message": "Bạn chưa lưu thay đổi của phần mô tả hoặc bản xem trước của media, vẫn bỏ qua?",
"confirmations.follow_to_collection.confirm": "Theo dõi & thêm vào collection",
"confirmations.follow_to_collection.message": "Bạn cần theo dõi {name} trước khi thêm họ vào collection.",
"confirmations.follow_to_collection.title": "Theo dõi tài khoản?",
"confirmations.follow_to_list.confirm": "Theo dõi & thêm vào danh sách",
"confirmations.follow_to_list.message": "Bạn cần theo dõi {name} trước khi thêm họ vào danh sách.",
"confirmations.follow_to_list.title": "Theo dõi tài khoản?",

View File

@@ -141,8 +141,25 @@
"account.unmute": "不再隐藏 @{name}",
"account.unmute_notifications_short": "恢复通知",
"account.unmute_short": "取消隐藏",
"account_edit.bio.placeholder": "添加一段简短介绍,帮助其他人认识你。",
"account_edit.bio.title": "简介",
"account_edit.bio_modal.add_title": "添加个人简介",
"account_edit.bio_modal.edit_title": "编辑个人简介",
"account_edit.char_counter": "{currentLength}/{maxLength} 个字",
"account_edit.column_button": "完成",
"account_edit.column_title": "修改个人资料",
"account_edit.custom_fields.placeholder": "添加你的人称代词、外部链接,或其他你想分享的内容。",
"account_edit.custom_fields.title": "自定义字段",
"account_edit.display_name.placeholder": "你的显示名称是指你的名字在个人资料及时间线上出现时的样子。",
"account_edit.display_name.title": "显示名称",
"account_edit.featured_hashtags.placeholder": "帮助其他人认识并快速访问你最喜欢的话题。",
"account_edit.featured_hashtags.title": "精选话题标签",
"account_edit.name_modal.add_title": "添加显示名称",
"account_edit.name_modal.edit_title": "编辑显示名称",
"account_edit.profile_tab.subtitle": "自定义你个人资料的标签页及其显示的内容。",
"account_edit.profile_tab.title": "个人资料标签页设置",
"account_edit.save": "保存",
"account_edit.section_edit_button": "编辑",
"account_note.placeholder": "点击添加备注",
"admin.dashboard.daily_retention": "注册后用户留存率(按日计算)",
"admin.dashboard.monthly_retention": "注册后用户留存率(按月计算)",
@@ -267,6 +284,7 @@
"collections.detail.curated_by_you": "由你精心挑选",
"collections.detail.loading": "正在加载收藏列表…",
"collections.detail.share": "分享此收藏列表",
"collections.edit_details": "编辑详情",
"collections.error_loading_collections": "加载你的收藏列表时发生错误。",
"collections.hints.accounts_counter": "{count} / {max} 个账号",
"collections.hints.add_more_accounts": "添加至少 {count, plural, other {# 个账号}}以继续",
@@ -275,11 +293,14 @@
"collections.manage_accounts": "管理账户",
"collections.mark_as_sensitive": "标记为敏感内容",
"collections.mark_as_sensitive_hint": "将此收藏列表的说明用内容警告隐藏。此收藏列表的名称仍将可见。",
"collections.name_length_hint": "40 字限制",
"collections.new_collection": "新建收藏列表",
"collections.no_collections_yet": "尚无收藏列表。",
"collections.old_last_post_note": "上次发言于一周多以前",
"collections.remove_account": "移除此账号",
"collections.search_accounts_label": "搜索要添加的账号…",
"collections.search_accounts_max_reached": "你添加的账号数量已达上限",
"collections.sensitive": "敏感内容",
"collections.topic_hint": "添加话题标签,帮助他人了解此收藏列表的主题。",
"collections.view_collection": "查看收藏列表",
"collections.visibility_public": "公开",
@@ -370,6 +391,9 @@
"confirmations.discard_draft.post.title": "丢弃你的嘟文草稿?",
"confirmations.discard_edit_media.confirm": "丢弃",
"confirmations.discard_edit_media.message": "你还有未保存的媒体描述或预览修改,仍要丢弃吗?",
"confirmations.follow_to_collection.confirm": "关注并添加到收藏列表",
"confirmations.follow_to_collection.message": "你需要先关注 {name},才能将其添加到收藏列表。",
"confirmations.follow_to_collection.title": "要关注账号吗?",
"confirmations.follow_to_list.confirm": "关注并添加到列表",
"confirmations.follow_to_list.message": "你需要先关注 {name},才能将其添加到列表。",
"confirmations.follow_to_list.title": "确定要关注此用户?",

View File

@@ -296,6 +296,7 @@
"collections.name_length_hint": "40 字限制",
"collections.new_collection": "新增收藏名單",
"collections.no_collections_yet": "您沒有任何收藏名單。",
"collections.old_last_post_note": "上次發表嘟文已超過一週",
"collections.remove_account": "移除此帳號",
"collections.search_accounts_label": "搜尋帳號以加入...",
"collections.search_accounts_max_reached": "您新增之帳號數已達上限",
@@ -390,6 +391,9 @@
"confirmations.discard_draft.post.title": "是否捨棄您的嘟文草稿?",
"confirmations.discard_edit_media.confirm": "捨棄",
"confirmations.discard_edit_media.message": "您於媒體描述或預覽區塊有未儲存的變更。是否要捨棄這些變更?",
"confirmations.follow_to_collection.confirm": "跟隨並加入至收藏名單",
"confirmations.follow_to_collection.message": "您必須先跟隨 {name} 以將其加入至收藏名單。",
"confirmations.follow_to_collection.title": "是否跟隨此帳號?",
"confirmations.follow_to_list.confirm": "跟隨並加入至列表",
"confirmations.follow_to_list.message": "您必須先跟隨 {name} 以將其加入至列表。",
"confirmations.follow_to_list.title": "是否跟隨該使用者?",

View File

@@ -1,7 +1,9 @@
import { annualReport } from './annual_report';
import { collections } from './collections';
import { profileEdit } from './profile_edit';
export const sliceReducers = {
annualReport,
collections,
profileEdit,
};

View File

@@ -0,0 +1,178 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { debounce } from 'lodash';
import {
apiDeleteFeaturedTag,
apiGetCurrentFeaturedTags,
apiGetTagSuggestions,
apiPostFeaturedTag,
} from '@/mastodon/api/accounts';
import { apiGetSearch } from '@/mastodon/api/search';
import { hashtagToFeaturedTag } from '@/mastodon/api_types/tags';
import type { ApiFeaturedTagJSON } from '@/mastodon/api_types/tags';
import type { AppDispatch } from '@/mastodon/store';
import {
createAppAsyncThunk,
createDataLoadingThunk,
} from '@/mastodon/store/typed_functions';
interface ProfileEditState {
tags: ApiFeaturedTagJSON[];
tagSuggestions: ApiFeaturedTagJSON[];
isLoading: boolean;
isPending: boolean;
search: {
query: string;
isLoading: boolean;
results?: ApiFeaturedTagJSON[];
};
}
const initialState: ProfileEditState = {
tags: [],
tagSuggestions: [],
isLoading: true,
isPending: false,
search: {
query: '',
isLoading: false,
},
};
const profileEditSlice = createSlice({
name: 'profileEdit',
initialState,
reducers: {
setSearchQuery(state, action: PayloadAction<string>) {
if (state.search.query === action.payload) {
return;
}
state.search.query = action.payload;
state.search.isLoading = false;
state.search.results = undefined;
},
clearSearch(state) {
state.search.query = '';
state.search.isLoading = false;
state.search.results = undefined;
},
},
extraReducers(builder) {
builder.addCase(fetchSuggestedTags.fulfilled, (state, action) => {
state.tagSuggestions = action.payload.map(hashtagToFeaturedTag);
state.isLoading = false;
});
builder.addCase(fetchFeaturedTags.fulfilled, (state, action) => {
state.tags = action.payload;
state.isLoading = false;
});
builder.addCase(addFeaturedTag.pending, (state) => {
state.isPending = true;
});
builder.addCase(addFeaturedTag.rejected, (state) => {
state.isPending = false;
});
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
state.tags = [...state.tags, action.payload].toSorted(
(a, b) => b.statuses_count - a.statuses_count,
);
state.tagSuggestions = state.tagSuggestions.filter(
(tag) => tag.name !== action.meta.arg.name,
);
state.isPending = false;
});
builder.addCase(deleteFeaturedTag.pending, (state) => {
state.isPending = true;
});
builder.addCase(deleteFeaturedTag.rejected, (state) => {
state.isPending = false;
});
builder.addCase(deleteFeaturedTag.fulfilled, (state, action) => {
state.tags = state.tags.filter((tag) => tag.id !== action.meta.arg.tagId);
state.isPending = false;
});
builder.addCase(fetchSearchResults.pending, (state) => {
state.search.isLoading = true;
});
builder.addCase(fetchSearchResults.rejected, (state) => {
state.search.isLoading = false;
state.search.results = undefined;
});
builder.addCase(fetchSearchResults.fulfilled, (state, action) => {
state.search.isLoading = false;
const searchResults: ApiFeaturedTagJSON[] = [];
const currentTags = new Set(state.tags.map((tag) => tag.name));
for (const tag of action.payload) {
if (currentTags.has(tag.name)) {
continue;
}
searchResults.push(hashtagToFeaturedTag(tag));
if (searchResults.length >= 10) {
break;
}
}
state.search.results = searchResults;
});
},
});
export const profileEdit = profileEditSlice.reducer;
export const { clearSearch } = profileEditSlice.actions;
export const fetchFeaturedTags = createDataLoadingThunk(
`${profileEditSlice.name}/fetchFeaturedTags`,
apiGetCurrentFeaturedTags,
{ useLoadingBar: false },
);
export const fetchSuggestedTags = createDataLoadingThunk(
`${profileEditSlice.name}/fetchSuggestedTags`,
apiGetTagSuggestions,
{ useLoadingBar: false },
);
export const addFeaturedTag = createDataLoadingThunk(
`${profileEditSlice.name}/addFeaturedTag`,
({ name }: { name: string }) => apiPostFeaturedTag(name),
{
condition(arg, { getState }) {
const state = getState();
return !state.profileEdit.tags.some((tag) => tag.name === arg.name);
},
},
);
export const deleteFeaturedTag = createDataLoadingThunk(
`${profileEditSlice.name}/deleteFeaturedTag`,
({ tagId }: { tagId: string }) => apiDeleteFeaturedTag(tagId),
);
const debouncedFetchSearchResults = debounce(
async (dispatch: AppDispatch, query: string) => {
await dispatch(fetchSearchResults({ q: query }));
},
300,
);
export const updateSearchQuery = createAppAsyncThunk(
`${profileEditSlice.name}/updateSearchQuery`,
(query: string, { dispatch }) => {
dispatch(profileEditSlice.actions.setSearchQuery(query));
if (query.trim().length > 0) {
void debouncedFetchSearchResults(dispatch, query);
}
},
);
export const fetchSearchResults = createDataLoadingThunk(
`${profileEditSlice.name}/fetchSearchResults`,
({ q }: { q: string }) => apiGetSearch({ q, type: 'hashtags', limit: 11 }),
(result) => result.hashtags,
);

View File

@@ -77,7 +77,8 @@ const initialState = ImmutableMap({
follow_requests: initialListState,
blocks: initialListState,
mutes: initialListState,
featured_tags: initialListState,
/** @type {ImmutableMap<string, typeof initialListState>} */
featured_tags: ImmutableMap(),
});
const normalizeList = (state, path, accounts, next) => {

View File

@@ -378,6 +378,15 @@ $content-width: 840px;
}
}
details > summary {
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
color: var(--color-text-secondary);
padding-top: 24px;
margin-bottom: 8px;
}
@media screen and (max-width: $no-columns-breakpoint) {
display: block;

View File

@@ -1670,7 +1670,7 @@ body > [data-popper-placement] {
.detailed-status__display-name {
color: var(--color-text-tertiary);
span {
span:not(.account__avatar) {
display: inline;
}

View File

@@ -41,41 +41,43 @@
%p
= t 'admin.reports.statuses_description_html'
%h4
= t 'admin.reports.statuses'
%details{ open: @report.status_ids.any? }
%summary
= t 'admin.reports.statuses', count: @report.status_ids.size
= form_with model: @form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id) do |f|
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]),
admin_account_statuses_path(@report.target_account_id, report_id: @report.id),
class: 'table-action-link'
- if !@statuses.empty? && @report.unresolved?
= f.button safe_join([material_symbol('close'), t('admin.statuses.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
.batch-table__body
- if @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
- if Mastodon::Feature.collections_enabled?
%h4
= t 'admin.reports.collections'
%form
= form_with model: @form, url: batch_admin_account_statuses_path(@report.target_account_id, report_id: @report.id) do |f|
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
-# = check_box_tag :batch_checkbox_all, nil, false
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]),
admin_account_statuses_path(@report.target_account_id, report_id: @report.id),
class: 'table-action-link'
- if !@statuses.empty? && @report.unresolved?
= f.button safe_join([material_symbol('close'), t('admin.statuses.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
.batch-table__body
- if @report.collections.empty?
- if @statuses.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/shared/collection_batch_row', collection: @report.collections, as: :collection
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
- if Mastodon::Feature.collections_enabled?
%details{ open: @report.collections.any? }
%summary
= t 'admin.reports.collections', count: @report.collections.size
%form
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
-# = check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
.batch-table__body
- if @report.collections.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/shared/collection_batch_row', collection: @report.collections, as: :collection
- if @report.unresolved?
%hr.spacer/

View File

@@ -64,7 +64,7 @@ de:
date_of_birth:
below_limit: liegt unterhalb des Mindestalters
email:
blocked: verwendet einen unerlaubten E-Mail-Anbieter
blocked: verwendet einen unerlaubten E-Mail-Provider
unreachable: scheint nicht zu existieren
role_id:
elevated: kann nicht höher als deine derzeitige Rolle sein

View File

@@ -717,7 +717,7 @@ cs:
cancel: Zrušit
category: Kategorie
category_description_html: Důvod nahlášení tohoto účtu a/nebo obsahu bude uveden v komunikaci s nahlášeným účtem
collections: Sbírky
collections: Sbírky (%{count})
comment:
none: Žádné
comment_description_html: 'Pro upřesnění uživatel %{name} napsal:'
@@ -753,7 +753,7 @@ cs:
resolved_msg: Hlášení úspěšně vyřešeno!
skip_to_actions: Přeskočit k akcím
status: Stav
statuses: Příspěvky
statuses: Příspěvky (%{count})
statuses_description_html: Obsah porušující pravidla bude uveden v komunikaci s nahlášeným účtem
summary:
action_preambles:

View File

@@ -30,12 +30,12 @@ cy:
pin_errors:
following: Rhaid i chi fod yn dilyn y person rydych am ei gymeradwyo, yn barod
posts:
few: Postiadau
many: Postiadau
few: Postiad
many: Postiad
one: Postiad
other: Postiadau
two: Postiadau
zero: Postiadau
other: Postiad
two: Bostiad
zero: Postiad
posts_tab_heading: Postiadau
self_follow_error: Chewch chi ddim dilyn eich cyfrif eich hun
admin:
@@ -745,7 +745,7 @@ cy:
cancel: Canslo
category: Categori
category_description_html: Bydd y rheswm dros adrodd am y cyfrif a/neur cynnwys hwn yn cael ei ddyfynnu wrth gyfathrebu âr cyfrif a adroddwyd
collections: Casgliadau
collections: Casgliadau (%{count})
comment:
none: Dim
comment_description_html: 'I ddarparu rhagor o wybodaeth, ysgrifennodd %{name}:'
@@ -781,7 +781,7 @@ cy:
resolved_msg: Llwyddwyd i ddatrys yr adroddiad!
skip_to_actions: Mynd i gamau gweithredu
status: Statws
statuses: Postiadau
statuses: Postiadau (%{count})
statuses_description_html: Bydd cynnwys tramgwyddus yn cael ei ddyfynnu wrth gyfathrebu â'r cyfrif a adroddwyd
summary:
action_preambles:
@@ -2213,6 +2213,8 @@ cy:
past_preamble_html: Rydym wedi newid ein telerau gwasanaeth ers eich ymweliad diwethaf. Rydym yn eich annog i ddarllen y telerau wedi'u diweddaru.
review_link: Darllen y telerau gwasanaeth
title: Mae telerau gwasanaeth %{domain} yn newid
themes:
default: Mastodon
time:
formats:
default: "%b %d, %Y, %H:%M"

View File

@@ -689,7 +689,7 @@ da:
cancel: Afbryd
category: Kategori
category_description_html: Årsagen til anmeldelsen af denne konto og/eller indhold refereres i kommunikationen med den anmeldte konto
collections: Samlinger
collections: Samlinger (%{count})
comment:
none: Ingen
comment_description_html: 'For at give mere information, skrev %{name}:'
@@ -725,7 +725,7 @@ da:
resolved_msg: Anmeldelse løst!
skip_to_actions: Overspring til foranstaltninger
status: Status
statuses: Indlæg
statuses: Indlæg (%{count})
statuses_description_html: Krænkende indhold citeres i kommunikationen med den anmeldte konto
summary:
action_preambles:

View File

@@ -541,7 +541,7 @@ de:
instances:
audit_log:
title: Neueste Protokolle
view_all: Alle Protokolle anzeigen
view_all: Protokolle
availability:
description_html:
one: Wenn die Zustellung an die Domain <strong>%{count} Tag</strong> lang erfolglos bleibt, werden keine weiteren Zustellversuche unternommen, bis eine Zustellung <em>von</em> der Domain empfangen wird.
@@ -689,7 +689,7 @@ de:
cancel: Abbrechen
category: Kategorie
category_description_html: Der Grund, weshalb dieses Konto und/oder der Inhalt gemeldet worden ist, wird in der Kommunikation mit dem gemeldeten Konto erwähnt
collections: Sammlungen
collections: Sammlungen (%{count})
comment:
none: Ohne ergänzenden Kommentar
comment_description_html: "%{name} ergänzte die Meldung um folgende Hinweis:"
@@ -725,7 +725,7 @@ de:
resolved_msg: Meldung erfolgreich geklärt!
skip_to_actions: Zur Maßnahme springen
status: Status
statuses: Beiträge
statuses: Beiträge (%{count})
statuses_description_html: Beanstandete Inhalte werden in der Kommunikation mit dem gemeldeten Konto erwähnt
summary:
action_preambles:
@@ -773,47 +773,47 @@ de:
other: "%{count} Berechtigungen"
privileges:
administrator: Administrator*in
administrator_description: Benutzer*innen mit dieser Berechtigung werden alle Beschränkungen umgehen
administrator_description: Beschränkung aller Berechtigungen umgehen
delete_user_data: Kontodaten löschen
delete_user_data_description: Erlaubt Benutzer*innen, die Daten anderer Benutzer*innen sofort zu löschen
invite_users: Leute einladen
delete_user_data_description: Daten anderer Profile ohne Verzögerung löschen
invite_users: Einladungen
invite_users_description: Erlaubt bereits registrierten Benutzer*innen, neue Leute zum Server einzuladen
manage_announcements: Ankündigungen verwalten
manage_announcements_description: Erlaubt Profilen, Ankündigungen auf dem Server zu verwalten
manage_appeals: Einsprüche verwalten
manage_appeals_description: Erlaubt es Benutzer*innen, Entscheidungen der Moderator*innen zu widersprechen
manage_blocks: Sperrungen verwalten
manage_blocks_description: Erlaubt Nutzer*innen das Sperren von E-Mail-Anbietern und IP-Adressen
manage_custom_emojis: Eigene Emojis verwalten
manage_custom_emojis_description: Erlaubt es Benutzer*innen, eigene Emojis auf dem Server zu verwalten
manage_federation: Föderation verwalten
manage_federation_description: Erlaubt Benutzer*innen, Domains anderer Mastodon-Server zu sperren oder zuzulassen und die Zustellbarkeit zu steuern
manage_invites: Einladungen verwalten
manage_invites_description: Erlaubt es Benutzer*innen, Einladungslinks zu durchsuchen und zu deaktivieren
manage_reports: Meldungen verwalten
manage_reports_description: Erlaubt es Benutzer*innen, Meldungen zu überprüfen und Vorfälle zu moderieren
manage_announcements: Ankündigungen
manage_announcements_description: Ankündigungen dieses Servers verwalten
manage_appeals: Einsprüche
manage_appeals_description: Entscheidungen von Moderator*innen bzgl. Einsprüchen von Benutzer*innen überarbeiten
manage_blocks: Sperren
manage_blocks_description: E-Mail-Provider und IP-Adressen sperren
manage_custom_emojis: Emojis
manage_custom_emojis_description: Spezielle Emojis dieses Servers verwalten
manage_federation: Föderation
manage_federation_description: Domains anderer Mastodon-Server sperren/zulassen und Zustellbarkeit kontrollieren
manage_invites: Einladungen
manage_invites_description: Einladungslinks durchsuchen und deaktivieren
manage_reports: Meldungen
manage_reports_description: Meldungen überprüfen und Vorfälle moderieren
manage_roles: Rollen verwalten
manage_roles_description: Erlaubt es Benutzer*innen, Rollen, die sich unterhalb der eigenen Rolle befinden, zu verwalten und zuzuweisen
manage_rules: Serverregeln verwalten
manage_rules_description: Erlaubt es Benutzer*innen, Serverregeln zu ändern
manage_settings: Einstellungen verwalten
manage_settings_description: Erlaubt Nutzer*innen, Einstellungen dieses Servers zu ändern
manage_taxonomies: Hashtags verwalten
manage_taxonomies_description: Ermöglicht Benutzer*innen, die Trends zu überprüfen und die Hashtag-Einstellungen zu aktualisieren
manage_user_access: Kontozugriff verwalten
manage_user_access_description: Erlaubt Nutzer*innen, die Zwei-Faktor-Authentisierung anderer zu deaktivieren, ihre E-Mail-Adresse zu ändern und ihr Passwort zurückzusetzen
manage_users: Konten verwalten
manage_roles_description: Rollen, die sich unterhalb der eigenen Rolle befinden, verwalten und zuweisen
manage_rules: Serverregeln
manage_rules_description: Serverregeln ändern
manage_settings: Einstellungen
manage_settings_description: Einstellungen dieses Servers ändern
manage_taxonomies: Hashtags
manage_taxonomies_description: Trends überprüfen und Einstellungen für Hashtags
manage_user_access: Kontozugriff
manage_user_access_description: Zwei-Faktor-Authentisierungen anderer können deaktiviert, E-Mail-Adressen geändert und Passwörter zurückgesetzt werden
manage_users: Konten
manage_users_description: Erlaubt es Benutzer*innen, die Details anderer Profile einzusehen und diese Accounts zu moderieren
manage_webhooks: Webhooks verwalten
manage_webhooks_description: Erlaubt es Benutzer*innen, Webhooks für administrative Vorkommnisse einzurichten
manage_webhooks: Webhooks
manage_webhooks_description: Webhooks für administrative Vorgänge einrichten
view_audit_log: Protokoll anzeigen
view_audit_log_description: Erlaubt es Benutzer*innen, den Verlauf der administrativen Handlungen auf diesem Server einzusehen
view_dashboard: Dashboard anzeigen
view_dashboard_description: Gewährt Benutzer*innen den Zugriff auf das Dashboard und verschiedene Metriken
view_audit_log_description: Verlauf der administrativen Vorgänge auf diesem Server
view_dashboard: Dashboard
view_dashboard_description: Dashboard und verschiedene Metriken
view_devops: DevOps
view_devops_description: Erlaubt es Benutzer*innen, auf die Sidekiq- und pgHero-Dashboards zuzugreifen
view_devops_description: Auf Sidekiq- und pgHero-Dashboards zugreifen
view_feeds: Live-Feeds und Hashtags anzeigen
view_feeds_description: Ermöglicht Nutzer*innen unabhängig von den Servereinstellungen den Zugriff auf die Live-Feeds und Hashtags
view_feeds_description: Zugriff auf Live-Feeds und Hashtags unabhängig der Servereinstellungen
requires_2fa: Zwei-Faktor-Authentisierung erforderlich
title: Rollen
rules:
@@ -1972,7 +1972,7 @@ de:
pending_approval: Veröffentlichung ausstehend
revoked: Beitrag durch Autor*in entfernt
quote_policies:
followers: Nur Follower und ich
followers: Nur Follower
nobody: Nur ich
public: Alle
quote_post_author: Zitierte %{acct}

View File

@@ -83,6 +83,10 @@ fr-CA:
access_denied: Le/la propriétaire de la ressource ou le serveur dautorisation a refusé la requête.
credential_flow_not_configured: Le flux des identifiants du mot de passe du/de la propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials nest pas configuré.
invalid_client: Lauthentification du client a échoué à cause dun client inconnu, daucune authentification de client incluse ou dune méthode dauthentification non prise en charge.
invalid_code_challenge_method:
one: Le code de la méthode de défi doit être %{challenge_methods}.
other: 'Le code de la méthode de défi doit être l''une de ces valeurs : %{challenge_methods}.'
zero: Le serveur d'autorisation ne supporte pas PKCE car il n'y a aucune valeur de code de méthode de défi acceptée.
invalid_grant: Lautorisation accordée est invalide, expirée, révoquée, ne concorde pas avec lURI de redirection utilisée dans la requête dautorisation, ou a été délivrée à un autre client.
invalid_redirect_uri: LURI de redirection nest pas valide.
invalid_request:

View File

@@ -83,6 +83,10 @@ fr:
access_denied: Le propriétaire de la ressource ou le serveur dautorisation a refusé la requête.
credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials nest pas configuré.
invalid_client: Lauthentification du client a échoué à cause dun client inconnu, daucune authentification de client incluse ou dune méthode dauthentification non prise en charge.
invalid_code_challenge_method:
one: Le code de la méthode de défi doit être %{challenge_methods}.
other: 'Le code de la méthode de défi doit être l''une de ces valeurs : %{challenge_methods}.'
zero: Le serveur d'autorisation ne supporte pas PKCE car il n'y a aucune valeur de code de méthode de défi acceptée.
invalid_grant: Lautorisation accordée est invalide, expirée, annulée, ne concorde pas avec lURL de redirection utilisée dans la requête dautorisation, ou a été délivrée à un autre client.
invalid_redirect_uri: LURL de redirection nest pas valide.
invalid_request:

View File

@@ -83,6 +83,10 @@ nn:
access_denied: Ressurseigaren eller autorisasjonstenaren avviste førespurnaden.
credential_flow_not_configured: Flyten «Resource Owner Password Credentials» kunne ikkje fullførast sidan «Doorkeeper.configure.resource_owner_from_credentials» ikkje er konfigurert.
invalid_client: Klientautentisering feila på grunn av ukjent klient, ingen inkludert autentisering, eller ikkje støtta autentiseringsmetode.
invalid_code_challenge_method:
one: code_challenge_method må vera %{challenge_methods}.
other: code_challenge_method må vera ein av %{challenge_methods}.
zero: Godkjenningstenaren støttar ikkje PKCE fordi det ikkje er nokon aksepterte verdiar av code_challenge_method.
invalid_grant: Autoriseringa er ugyldig, utløpt, oppheva, stemmer ikkje med omdirigerings-URIen eller var tildelt ein annan klient.
invalid_redirect_uri: Omdirigerings-URLen er ikkje gyldig.
invalid_request:

View File

@@ -689,7 +689,7 @@ el:
cancel: Άκυρο
category: Κατηγορία
category_description_html: Ο λόγος για τον οποίο αναφέρθηκε αυτός ο λογαριασμός και/ή το περιεχόμενο θα εσωκλείεται σε επικοινωνία με τον αναφερόμενο λογαριασμό
collections: Συλλογές
collections: Συλλογές (%{count})
comment:
none: Κανένα
comment_description_html: 'Για να δώσει περισσότερες πληροφορίες, ο/η %{name} έγραψε:'
@@ -725,7 +725,7 @@ el:
resolved_msg: Η αναφορά επιλύθηκε επιτυχώς!
skip_to_actions: Μετάβαση στις ενέργειες
status: Κατάσταση
statuses: Αναρτήσεις
statuses: Αναρτήσεις (%{count})
statuses_description_html: Το προσβλητικό περιεχόμενο θα εσωκλείεται στην επικοινωνία με τον αναφερόμενο λογαριασμό
summary:
action_preambles:

View File

@@ -689,7 +689,7 @@ en-GB:
cancel: Cancel
category: Category
category_description_html: The reason this account and/or content was reported will be cited in communication with the reported account
collections: Collections
collections: Collections (%{count})
comment:
none: None
comment_description_html: 'To provide more information, %{name} wrote:'
@@ -725,7 +725,7 @@ en-GB:
resolved_msg: Report successfully resolved!
skip_to_actions: Skip to actions
status: Status
statuses: Posts
statuses: Posts (%{count})
statuses_description_html: Offending content will be cited in communication with the reported account
summary:
action_preambles:

View File

@@ -689,7 +689,7 @@ en:
cancel: Cancel
category: Category
category_description_html: The reason this account and/or content was reported will be cited in communication with the reported account
collections: Collections
collections: Collections (%{count})
comment:
none: None
comment_description_html: 'To provide more information, %{name} wrote:'
@@ -725,7 +725,7 @@ en:
resolved_msg: Report successfully resolved!
skip_to_actions: Skip to actions
status: Status
statuses: Posts
statuses: Posts (%{count})
statuses_description_html: Offending content will be cited in communication with the reported account
summary:
action_preambles:

View File

@@ -689,7 +689,7 @@ es-AR:
cancel: Cancelar
category: Categoría
category_description_html: El motivo por el que se denunció esta cuenta o contenido será citado en las comunicaciones con la cuenta denunciada
collections: Colecciones
collections: Colecciones (%{count})
comment:
none: Ninguno
comment_description_html: 'Para proporcionar más información, %{name} escribió:'
@@ -725,7 +725,7 @@ es-AR:
resolved_msg: "¡Denuncia exitosamente resuelta!"
skip_to_actions: Ir directamente a las acciones
status: Estado
statuses: Mensajes
statuses: Mensajes (%{count})
statuses_description_html: El contenido ofensivo se citará en la comunicación con la cuenta denunciada
summary:
action_preambles:

View File

@@ -689,7 +689,7 @@ es-MX:
cancel: Cancelar
category: Categoría
category_description_html: La razón por la que se reportó esta cuenta o contenido será citada en las comunicaciones con la cuenta reportada
collections: Colecciones
collections: Colecciones (%{count})
comment:
none: Ninguno
comment_description_html: 'Para proporcionar más información, %{name} escribió:'
@@ -725,7 +725,7 @@ es-MX:
resolved_msg: "¡La denuncia se ha resuelto correctamente!"
skip_to_actions: Ir directamente a las acciones
status: Estado
statuses: Publicaciones
statuses: Publicaciones (%{count})
statuses_description_html: El contenido ofensivo se citará en comunicación con la cuenta reportada
summary:
action_preambles:

View File

@@ -348,8 +348,8 @@ es:
number_of_accounts:
one: 1 cuenta
other: "%{count} cuentas"
open: Pública
view_publicly: Vista pública
open: Abrir
view_publicly: Ver públicamente
critical_update_pending: Actualización crítica pendiente
custom_emojis:
assign_category: Asignar categoría
@@ -689,7 +689,7 @@ es:
cancel: Cancelar
category: Categoría
category_description_html: La razón por la que se reportó esta cuenta o contenido será citada en las comunicaciones con la cuenta reportada
collections: Colecciones
collections: Colecciones (%{count})
comment:
none: Ninguno
comment_description_html: 'Para proporcionar más información, %{name} escribió:'
@@ -725,7 +725,7 @@ es:
resolved_msg: "¡La denuncia se ha resuelto correctamente!"
skip_to_actions: Ir directamente a las acciones
status: Estado
statuses: Publicaciones
statuses: Publicaciones (%{count})
statuses_description_html: El contenido ofensivo se citará en la comunicación con la cuenta reportada
summary:
action_preambles:

View File

@@ -689,7 +689,7 @@ fi:
cancel: Peruuta
category: Luokka
category_description_html: Syy siihen, miksi tämä tili ja/tai sisältö raportoitiin, mainitaan ilmoitetun tilin kanssa viestiessä
collections: Kokoelmat
collections: Kokoelmat (%{count})
comment:
none: Ei mitään
comment_description_html: 'Antaakseen lisätietoja %{name} kirjoitti:'
@@ -725,7 +725,7 @@ fi:
resolved_msg: Raportin ratkaisu onnistui!
skip_to_actions: Siirry toimiin
status: Tila
statuses: Julkaisut
statuses: Julkaisut (%{count})
statuses_description_html: Loukkaava sisältö mainitaan raportoidun tilin yhteydessä
summary:
action_preambles:
@@ -2090,7 +2090,7 @@ fi:
subject: Kaksivaiheisen todennuksen virhe
title: Kaksivaiheisen todennuksen toinen vaihe epäonnistui
suspicious_sign_in:
change_password: vaihda salasanasi
change_password: vaihdat salasanasi
details: 'Tässä on tiedot kirjautumisesta:'
explanation: Olemme havainneet kirjautumisen tilillesi uudesta IP-osoitteesta.
further_actions_html: Jos tämä et ollut sinä, suosittelemme, että %{action} heti ja otat käyttöön kaksivaiheisen todennuksen pitääksesi tilisi turvassa.

View File

@@ -53,6 +53,7 @@ fo:
label: Broyt leiklut
no_role: Eingin leiklutur
title: Broyt leiklut hjá %{username}
collections: Søvn
confirm: Vátta
confirmed: Váttað
confirming: Váttar
@@ -337,6 +338,12 @@ fo:
unpublish: Tak útgávu aftur
unpublished_msg: Kunngerð tikin aftur!
updated_msg: Kunngerð dagførd!
collections:
accounts: Kontur
collection_title: Savn hjá %{name}
contents: Innihald
open: Opin
view_publicly: Vís fyri øllum
critical_update_pending: Kritisk dagføring bíðar
custom_emojis:
assign_category: Tilluta bólk
@@ -676,6 +683,7 @@ fo:
cancel: Angra
category: Bólkur
category_description_html: Orsøkin, at hendan kontan og/ella tilfarið var melda verður fráboðað í samskifti við meldaðu kontuni
collections: Søvn
comment:
none: Eingin
comment_description_html: 'Fyri at veita fleiri upplýsingar skrivaði %{name}:'
@@ -705,11 +713,13 @@ fo:
report: 'Melding #%{id}'
reported_account: Meldað konta
reported_by: Meldað av
reported_content: Meldað innihald
reported_with_application: Fráboðað við umsókn
resolved: Loyst
resolved_msg: Melding avgreidd!
skip_to_actions: Leyp til atgerðir
status: Støða
statuses: Postar
statuses_description_html: Tilfarið, sum brotið viðvíkur, fer at vera siterað í samskifti við meldaðu kontuni
summary:
action_preambles:

View File

@@ -692,7 +692,7 @@ fr-CA:
cancel: Annuler
category: Catégorie
category_description_html: La raison pour laquelle ce compte et/ou ce contenu a été signalé sera citée dans la communication avec le compte signalé
collections: Collections
collections: Collections (%{count})
comment:
none: Aucun
comment_description_html: 'Pour fournir plus d''informations, %{name} a écrit :'
@@ -728,7 +728,7 @@ fr-CA:
resolved_msg: Signalement résolu avec succès!
skip_to_actions: Passer aux actions
status: Statut
statuses: Messages
statuses: Messages (%{count})
statuses_description_html: Le contenu offensant sera cité dans la communication avec le compte signalé
summary:
action_preambles:

View File

@@ -59,7 +59,7 @@ fr:
collections: Collections
confirm: Confirmer
confirmed: Confirmé
confirming: Confirmation
confirming: Confirmation en attente
custom: Personnaliser
delete: Supprimer les données
deleted: Supprimé
@@ -692,7 +692,7 @@ fr:
cancel: Annuler
category: Catégorie
category_description_html: La raison pour laquelle ce compte et/ou ce contenu a été signalé sera citée dans la communication avec le compte signalé
collections: Collections
collections: Collections (%{count})
comment:
none: Aucun
comment_description_html: 'Pour fournir plus d''informations, %{name} a écrit :'
@@ -728,7 +728,7 @@ fr:
resolved_msg: Signalement résolu avec succès!
skip_to_actions: Passer aux actions
status: Statut
statuses: Messages
statuses: Messages (%{count})
statuses_description_html: Le contenu offensant sera cité dans la communication avec le compte signalé
summary:
action_preambles:

View File

@@ -731,7 +731,7 @@ ga:
cancel: Cealaigh
category: Catagóir
category_description_html: Luafar an chúis ar tuairiscíodh an cuntas seo agus/nó an t-ábhar seo i gcumarsáid leis an gcuntas tuairiscithe
collections: Bailiúcháin
collections: Bailiúcháin (%{count})
comment:
none: Dada
comment_description_html: 'Chun tuilleadh eolais a sholáthar, scríobh %{name}:'
@@ -767,7 +767,7 @@ ga:
resolved_msg: D'éirigh le réiteach an tuairisc!
skip_to_actions: Léim ar ghníomhartha
status: Stádas
statuses: Poist
statuses: Poist (%{count})
statuses_description_html: Luafar ábhar ciontach i gcumarsáid leis an gcuntas tuairiscithe
summary:
action_preambles:

View File

@@ -689,7 +689,7 @@ gl:
cancel: Cancelar
category: Categoría
category_description_html: A razón para denunciar esta conta ou contido será citada na comunicación coa conta denunciada
collections: Coleccións
collections: Coleccións (%{count})
comment:
none: Ningún
comment_description_html: 'Como información engadida, %{name} escribiu:'
@@ -725,7 +725,7 @@ gl:
resolved_msg: Resolveuse con éxito a denuncia!
skip_to_actions: Ir a accións
status: Estado
statuses: Publicacións
statuses: Publicacións (%{count})
statuses_description_html: O contido ofensivo será citado na comunicación coa conta denunciada
summary:
action_preambles:

View File

@@ -62,6 +62,7 @@ he:
label: שינוי תפקיד
no_role: ללא תפקיד
title: שינוי תפקיד עבור %{username}
collections: אוספים
confirm: אישור
confirmed: אושר
confirming: בתהליך אישור
@@ -348,6 +349,17 @@ he:
unpublish: ביטול פרסום
unpublished_msg: פרסום ההכרזה בוטל בהצלחה!
updated_msg: ההכרזה עודכנה בהצלחה!
collections:
accounts: חשבונות
collection_title: אוספים מאת %{name}
contents: תוכן
number_of_accounts:
many: "%{count} חשבונות"
one: חשבון אחד
other: "%{count} חשבונות"
two: חשבונותיים
open: פתיחה
view_publicly: צפיה בפומבי
critical_update_pending: עידכון קריטי ממתין
custom_emojis:
assign_category: הקצאת קטגוריה
@@ -705,6 +717,7 @@ he:
cancel: ביטול
category: קטגוריה
category_description_html: הסיבה בגללה חשבון זה ו/או תוכנו דווחו תצוטט בתקשורת עם החשבון המדווח
collections: אוספים
comment:
none: ללא
comment_description_html: 'על מנת לספק עוד מידע, %{name} כתב\ה:'
@@ -734,11 +747,13 @@ he:
report: 'דווח על #%{id}'
reported_account: חשבון מדווח
reported_by: דווח על ידי
reported_content: התוכן עליו דווח
reported_with_application: דיווחים באמצעות יישומון
resolved: פתור
resolved_msg: הדו"ח נפתר בהצלחה!
skip_to_actions: דלג/י לפעולות
status: מצב
statuses: הודעות
statuses_description_html: התוכן הפוגע יצוטט בתקשורת עם החשבון המדווח
summary:
action_preambles:

Some files were not shown because too many files have changed in this diff Show More