diff --git a/.github/workflows/haml-lint-problem-matcher.json b/.github/workflows/haml-lint-problem-matcher.json deleted file mode 100644 index 3523ea2951..0000000000 --- a/.github/workflows/haml-lint-problem-matcher.json +++ /dev/null @@ -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 - } - ] - } - ] -} diff --git a/.github/workflows/lint-haml.yml b/.github/workflows/lint-haml.yml index 637c211700..46cf1c25c1 100644 --- a/.github/workflows/lint-haml.yml +++ b/.github/workflows/lint-haml.yml @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index e08cc74ea4..65db509b1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/javascript/flavours/glitch/api/accounts.ts b/app/javascript/flavours/glitch/api/accounts.ts index 3752150448..623b18cc48 100644 --- a/app/javascript/flavours/glitch/api/accounts.ts +++ b/app/javascript/flavours/glitch/api/accounts.ts @@ -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(`v1/accounts/${id}/note`, { @@ -30,7 +37,19 @@ export const apiRemoveAccountFromFollowers = (id: string) => ); export const apiGetFeaturedTags = (id: string) => - apiRequestGet(`v1/accounts/${id}/featured_tags`); + apiRequestGet(`v1/accounts/${id}/featured_tags`); + +export const apiGetCurrentFeaturedTags = () => + apiRequestGet(`v1/featured_tags`); + +export const apiPostFeaturedTag = (name: string) => + apiRequestPost('v1/featured_tags', { name }); + +export const apiDeleteFeaturedTag = (id: string) => + apiRequestDelete(`v1/featured_tags/${id}`); + +export const apiGetTagSuggestions = () => + apiRequestGet('v1/featured_tags/suggestions'); export const apiGetEndorsedAccounts = (id: string) => apiRequestGet(`v1/accounts/${id}/endorsements`); diff --git a/app/javascript/flavours/glitch/api_types/tags.ts b/app/javascript/flavours/glitch/api_types/tags.ts index 3066b4f1f1..01d7f9e4b6 100644 --- a/app/javascript/flavours/glitch/api_types/tags.ts +++ b/app/javascript/flavours/glitch/api_types/tags.ts @@ -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, + }; +} diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss b/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss index 68c091a6d2..7947b698a5 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss +++ b/app/javascript/flavours/glitch/components/form_fields/combobox.module.scss @@ -3,7 +3,7 @@ } .input { - padding-right: 45px; + padding-inline-end: 45px; } .menuButton { diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx index 412428d345..2c4b82fdcd 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/combobox_field.stories.tsx @@ -82,11 +82,23 @@ const ComboboxDemo: React.FC = () => { const meta = { title: 'Components/Form Fields/ComboboxField', - component: ComboboxDemo, -} satisfies Meta; + component: ComboboxField, + render: () => , +} satisfies Meta; export default meta; type Story = StoryObj; -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, + }, +}; diff --git a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx index cfcfc1f5d7..7d5b38ab30 100644 --- a/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/combobox_field.tsx @@ -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 extends TextInputProps { + /** + * The value of the combobox's text input + */ value: string; + /** + * Change handler for the text input field + */ onChange: React.ChangeEventHandler; + /** + * 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 extends ComboboxProps, 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 = ( @@ -80,7 +113,7 @@ const ComboboxWithRef = ( value, isLoading = false, items, - getItemId, + getItemId = (item) => item.id, getIsItemDisabled, getIsItemSelected, disabled, @@ -88,6 +121,7 @@ const ComboboxWithRef = ( onSelectItem, onChange, onKeyDown, + icon = SearchIcon, className, ...otherProps }: ComboboxProps, @@ -306,6 +340,7 @@ const ComboboxWithRef = ( value={value} onChange={handleInputChange} onKeyDown={handleInputKeyDown} + icon={icon} className={classNames(classes.input, className)} ref={mergeRefs} /> diff --git a/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss b/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss index 2299068c5a..289ff1333a 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss +++ b/app/javascript/flavours/glitch/components/form_fields/text_input.module.scss @@ -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); +} diff --git a/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx b/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx index 2cf8613f68..8e8d7e9923 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/text_input_field.stories.tsx @@ -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 ; diff --git a/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx b/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx index 37cf150147..a95812c8d1 100644 --- a/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx +++ b/app/javascript/flavours/glitch/components/form_fields/text_input_field.tsx @@ -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( TextInputField.displayName = 'TextInputField'; -export const TextInput = forwardRef< - HTMLInputElement, - ComponentPropsWithoutRef<'input'> ->(({ type = 'text', className, ...otherProps }, ref) => ( - -)); +export const TextInput = forwardRef( + ({ type = 'text', icon, className, ...otherProps }, ref) => ( + + + + ), +); TextInput.displayName = 'TextInput'; + +const WrapFieldWithIcon: React.FC<{ + icon?: IconProp; + children: React.ReactElement; +}> = ({ icon, children }) => { + if (icon) { + return ( +
+ + {children} +
+ ); + } + + return children; +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/components/column.tsx b/app/javascript/flavours/glitch/features/account_edit/components/column.tsx new file mode 100644 index 0000000000..35f348e224 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/components/column.tsx @@ -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 ; + } + + return ( + + + + ); +}; + +export const AccountEditColumn: FC<{ + title: string; + to: string; + children: React.ReactNode; +}> = ({ to, title, children }) => { + const { multiColumn } = useColumnsContext(); + + return ( + + + + + } + /> + + {children} + + ); +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/components/edit_button.tsx b/app/javascript/flavours/glitch/features/account_edit/components/edit_button.tsx new file mode 100644 index 0000000000..da57322280 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/components/edit_button.tsx @@ -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 = ({ + 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 ( + + ); + } + + return ( + + ); +}; + +export const EditIconButton: FC<{ + onClick: MouseEventHandler; + title: string; + disabled?: boolean; +}> = ({ title, onClick, disabled }) => ( + +); + +export const DeleteIconButton: FC<{ + onClick: MouseEventHandler; + item: string; + disabled?: boolean; +}> = ({ onClick, item, disabled }) => { + const intl = useIntl(); + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/components/item_list.tsx b/app/javascript/flavours/glitch/features/account_edit/components/item_list.tsx new file mode 100644 index 0000000000..eb6cf590f5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/components/item_list.tsx @@ -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 { + renderItem?: (item: Item) => React.ReactNode; + items: Item[]; + onEdit?: (item: Item) => void; + onDelete?: (item: Item) => void; + disabled?: boolean; +} + +export const AccountEditItemList = ({ + renderItem, + items, + onEdit, + onDelete, + disabled, +}: AccountEditItemListProps) => { + if (items.length === 0) { + return null; + } + + return ( +
    + {items.map((item) => ( +
  • + {renderItem?.(item) ?? item.name} + +
  • + ))} +
+ ); +}; + +type AccountEditItemButtonsProps = Pick< + AccountEditItemListProps, + 'onEdit' | 'onDelete' | 'disabled' +> & { item: Item }; + +const AccountEditItemButtons = ({ + item, + onDelete, + onEdit, + disabled, +}: AccountEditItemButtonsProps) => { + const handleEdit = useCallback(() => { + onEdit?.(item); + }, [item, onEdit]); + const handleDelete = useCallback(() => { + onDelete?.(item); + }, [item, onDelete]); + + if (!onEdit && !onDelete) { + return null; + } + + return ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/components/section.tsx b/app/javascript/flavours/glitch/features/account_edit/components/section.tsx index 98f65cd89d..49643b51b1 100644 --- a/app/javascript/flavours/glitch/features/account_edit/components/section.tsx +++ b/app/javascript/flavours/glitch/features/account_edit/components/section.tsx @@ -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 = ({ title, description, showDescription, - onEdit, children, className, - extraButtons, + buttons, }) => { - const intl = useIntl(); return (

- {onEdit && ( - - )} - {extraButtons} + {buttons}
{showDescription && (

diff --git a/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx b/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx new file mode 100644 index 0000000000..2a25d5b5d3 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx @@ -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 = 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 ( + + ); +}; + +const renderItem = (item: ApiFeaturedTagJSON) =>

#{item.name}

; diff --git a/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx b/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx new file mode 100644 index 0000000000..dc9e6a84f7 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx @@ -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 ; + } + + return ( + +
+ + + {tagSuggestions.length > 0 && ( +
+ + {tagSuggestions.map((tag) => ( + + ))} +
+ )} + {isLoading && } + +
+
+ ); +}; + +function renderTag(tag: ApiFeaturedTagJSON) { + return ( +
+

#{tag.name}

+ {tag.statuses_count > 0 && ( + + )} +
+ ); +} + +const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({ + name, + disabled, +}) => { + const dispatch = useAppDispatch(); + const handleAddTag = useCallback(() => { + void dispatch(addFeaturedTag({ name })); + }, [dispatch, name]); + return ; +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/index.tsx b/app/javascript/flavours/glitch/features/account_edit/index.tsx index fbcb8db57f..f4f73ce6a9 100644 --- a/app/javascript/flavours/glitch/features/account_edit/index.tsx +++ b/app/javascript/flavours/glitch/features/account_edit/index.tsx @@ -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) => { 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 ; - } + const history = useHistory(); + const handleFeaturedTagsEdit = useCallback(() => { + history.push('/profile/featured_tags'); + }, [history]); - if (!account) { - return ( - - - - ); + if (!accountId || !account) { + return ; } 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 ( - - - - - } - /> +
{headerSrc && } @@ -129,8 +131,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + } > @@ -138,8 +146,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + } > @@ -153,14 +167,23 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + showDescription={!hasTags} + buttons={ + + } + > + {featuredTags.map((tag) => `#${tag.name}`).join(', ')} + - + ); }; diff --git a/app/javascript/flavours/glitch/features/account_edit/styles.module.scss b/app/javascript/flavours/glitch/features/account_edit/styles.module.scss index 8af244d38b..ee8603cc4f 100644 --- a/app/javascript/flavours/glitch/features/account_edit/styles.module.scss +++ b/app/javascript/flavours/glitch/features/account_edit/styles.module.scss @@ -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; diff --git a/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx b/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx index 152e346764..4d403b3523 100644 --- a/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx +++ b/app/javascript/flavours/glitch/features/collections/editor/accounts.tsx @@ -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 (
@@ -351,21 +354,12 @@ export const CollectionAccounts: React.FC<{ } /> )} - - } - hint={ - hasMaxAccounts ? ( - - ) : undefined - } + + + {hasMaxAccounts && ( + + )} {hasMinAccounts && ( diff --git a/app/javascript/flavours/glitch/features/ui/index.jsx b/app/javascript/flavours/glitch/features/ui/index.jsx index aded37ca4b..a75d3c64d2 100644 --- a/app/javascript/flavours/glitch/features/ui/index.jsx +++ b/app/javascript/flavours/glitch/features/ui/index.jsx @@ -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 = ; } - const profileRedesignEnabled = isServerFeatureEnabled('profile_redesign'); const profileRedesignRoutes = []; - if (profileRedesignEnabled) { + if (isServerFeatureEnabled('profile_redesign')) { profileRedesignRoutes.push( , ); @@ -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( + , + // If the redesign is not enabled but someone shares an /about link, redirect to the root. , ); } + if (isClientFeatureEnabled('profile_editing')) { + profileRedesignRoutes.push( + , + + ) + } else { + // If profile editing is not enabled, redirect to the home timeline as the current editing pages are outside React Router. + profileRedesignRoutes.push( + , + , + ); + } + return ( @@ -242,8 +256,6 @@ class SwitchingColumnsArea extends PureComponent { - {isClientFeatureEnabled('profile_editing') && } - @@ -251,8 +263,8 @@ class SwitchingColumnsArea extends PureComponent { - {!profileRedesignEnabled && } {...profileRedesignRoutes} + diff --git a/app/javascript/flavours/glitch/features/ui/util/async-components.js b/app/javascript/flavours/glitch/features/ui/util/async-components.js index 9f10c0602a..c9b0f452e4 100644 --- a/app/javascript/flavours/glitch/features/ui/util/async-components.js +++ b/app/javascript/flavours/glitch/features/ui/util/async-components.js @@ -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'); } diff --git a/app/javascript/flavours/glitch/reducers/slices/index.ts b/app/javascript/flavours/glitch/reducers/slices/index.ts index 06a384d562..8d4ecd552d 100644 --- a/app/javascript/flavours/glitch/reducers/slices/index.ts +++ b/app/javascript/flavours/glitch/reducers/slices/index.ts @@ -1,7 +1,9 @@ import { annualReport } from './annual_report'; import { collections } from './collections'; +import { profileEdit } from './profile_edit'; export const sliceReducers = { annualReport, collections, + profileEdit, }; diff --git a/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts b/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts new file mode 100644 index 0000000000..618a6e9d61 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts @@ -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) { + 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, +); diff --git a/app/javascript/flavours/glitch/reducers/user_lists.js b/app/javascript/flavours/glitch/reducers/user_lists.js index b027aba7c7..e37d7d971c 100644 --- a/app/javascript/flavours/glitch/reducers/user_lists.js +++ b/app/javascript/flavours/glitch/reducers/user_lists.js @@ -77,7 +77,8 @@ const initialState = ImmutableMap({ follow_requests: initialListState, blocks: initialListState, mutes: initialListState, - featured_tags: initialListState, + /** @type {ImmutableMap} */ + featured_tags: ImmutableMap(), }); const normalizeList = (state, path, accounts, next) => { diff --git a/app/javascript/flavours/glitch/styles/mastodon/admin.scss b/app/javascript/flavours/glitch/styles/mastodon/admin.scss index aeb7b7464c..d44443537b 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/admin.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/admin.scss @@ -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; diff --git a/app/javascript/flavours/glitch/styles/mastodon/components.scss b/app/javascript/flavours/glitch/styles/mastodon/components.scss index b5a3616ef8..b20be6a598 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/components.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/components.scss @@ -1733,7 +1733,7 @@ body > [data-popper-placement] { .detailed-status__display-name { color: var(--color-text-tertiary); - span { + span:not(.account__avatar) { display: inline; } diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index fb99978cad..9c35d619a4 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -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(`v1/accounts/${id}/note`, { @@ -30,7 +33,19 @@ export const apiRemoveAccountFromFollowers = (id: string) => ); export const apiGetFeaturedTags = (id: string) => - apiRequestGet(`v1/accounts/${id}/featured_tags`); + apiRequestGet(`v1/accounts/${id}/featured_tags`); + +export const apiGetCurrentFeaturedTags = () => + apiRequestGet(`v1/featured_tags`); + +export const apiPostFeaturedTag = (name: string) => + apiRequestPost('v1/featured_tags', { name }); + +export const apiDeleteFeaturedTag = (id: string) => + apiRequestDelete(`v1/featured_tags/${id}`); + +export const apiGetTagSuggestions = () => + apiRequestGet('v1/featured_tags/suggestions'); export const apiGetEndorsedAccounts = (id: string) => apiRequestGet(`v1/accounts/${id}/endorsements`); diff --git a/app/javascript/mastodon/api_types/tags.ts b/app/javascript/mastodon/api_types/tags.ts index 3066b4f1f1..01d7f9e4b6 100644 --- a/app/javascript/mastodon/api_types/tags.ts +++ b/app/javascript/mastodon/api_types/tags.ts @@ -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, + }; +} diff --git a/app/javascript/mastodon/components/form_fields/combobox.module.scss b/app/javascript/mastodon/components/form_fields/combobox.module.scss index 68c091a6d2..7947b698a5 100644 --- a/app/javascript/mastodon/components/form_fields/combobox.module.scss +++ b/app/javascript/mastodon/components/form_fields/combobox.module.scss @@ -3,7 +3,7 @@ } .input { - padding-right: 45px; + padding-inline-end: 45px; } .menuButton { diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx index 412428d345..2c4b82fdcd 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.stories.tsx @@ -82,11 +82,23 @@ const ComboboxDemo: React.FC = () => { const meta = { title: 'Components/Form Fields/ComboboxField', - component: ComboboxDemo, -} satisfies Meta; + component: ComboboxField, + render: () => , +} satisfies Meta; export default meta; type Story = StoryObj; -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, + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index 94b338c9a2..0c3af80883 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -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 extends TextInputProps { + /** + * The value of the combobox's text input + */ value: string; + /** + * Change handler for the text input field + */ onChange: React.ChangeEventHandler; + /** + * 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 extends ComboboxProps, 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 = ( @@ -80,7 +113,7 @@ const ComboboxWithRef = ( value, isLoading = false, items, - getItemId, + getItemId = (item) => item.id, getIsItemDisabled, getIsItemSelected, disabled, @@ -88,6 +121,7 @@ const ComboboxWithRef = ( onSelectItem, onChange, onKeyDown, + icon = SearchIcon, className, ...otherProps }: ComboboxProps, @@ -306,6 +340,7 @@ const ComboboxWithRef = ( value={value} onChange={handleInputChange} onKeyDown={handleInputKeyDown} + icon={icon} className={classNames(classes.input, className)} ref={mergeRefs} /> diff --git a/app/javascript/mastodon/components/form_fields/text_input.module.scss b/app/javascript/mastodon/components/form_fields/text_input.module.scss index 2299068c5a..289ff1333a 100644 --- a/app/javascript/mastodon/components/form_fields/text_input.module.scss +++ b/app/javascript/mastodon/components/form_fields/text_input.module.scss @@ -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); +} diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx index 2cf8613f68..8e8d7e9923 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx @@ -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 ; diff --git a/app/javascript/mastodon/components/form_fields/text_input_field.tsx b/app/javascript/mastodon/components/form_fields/text_input_field.tsx index 37cf150147..46676cd42f 100644 --- a/app/javascript/mastodon/components/form_fields/text_input_field.tsx +++ b/app/javascript/mastodon/components/form_fields/text_input_field.tsx @@ -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( TextInputField.displayName = 'TextInputField'; -export const TextInput = forwardRef< - HTMLInputElement, - ComponentPropsWithoutRef<'input'> ->(({ type = 'text', className, ...otherProps }, ref) => ( - -)); +export const TextInput = forwardRef( + ({ type = 'text', icon, className, ...otherProps }, ref) => ( + + + + ), +); TextInput.displayName = 'TextInput'; + +const WrapFieldWithIcon: React.FC<{ + icon?: IconProp; + children: React.ReactElement; +}> = ({ icon, children }) => { + if (icon) { + return ( +
+ + {children} +
+ ); + } + + return children; +}; diff --git a/app/javascript/mastodon/features/account_edit/components/column.tsx b/app/javascript/mastodon/features/account_edit/components/column.tsx new file mode 100644 index 0000000000..5f0ad929a1 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/components/column.tsx @@ -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 ; + } + + return ( + + + + ); +}; + +export const AccountEditColumn: FC<{ + title: string; + to: string; + children: React.ReactNode; +}> = ({ to, title, children }) => { + const { multiColumn } = useColumnsContext(); + + return ( + + + + + } + /> + + {children} + + ); +}; diff --git a/app/javascript/mastodon/features/account_edit/components/edit_button.tsx b/app/javascript/mastodon/features/account_edit/components/edit_button.tsx new file mode 100644 index 0000000000..f2fecf21d0 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/components/edit_button.tsx @@ -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 = ({ + 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 ( + + ); + } + + return ( + + ); +}; + +export const EditIconButton: FC<{ + onClick: MouseEventHandler; + title: string; + disabled?: boolean; +}> = ({ title, onClick, disabled }) => ( + +); + +export const DeleteIconButton: FC<{ + onClick: MouseEventHandler; + item: string; + disabled?: boolean; +}> = ({ onClick, item, disabled }) => { + const intl = useIntl(); + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/account_edit/components/item_list.tsx b/app/javascript/mastodon/features/account_edit/components/item_list.tsx new file mode 100644 index 0000000000..eb6cf590f5 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/components/item_list.tsx @@ -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 { + renderItem?: (item: Item) => React.ReactNode; + items: Item[]; + onEdit?: (item: Item) => void; + onDelete?: (item: Item) => void; + disabled?: boolean; +} + +export const AccountEditItemList = ({ + renderItem, + items, + onEdit, + onDelete, + disabled, +}: AccountEditItemListProps) => { + if (items.length === 0) { + return null; + } + + return ( +
    + {items.map((item) => ( +
  • + {renderItem?.(item) ?? item.name} + +
  • + ))} +
+ ); +}; + +type AccountEditItemButtonsProps = Pick< + AccountEditItemListProps, + 'onEdit' | 'onDelete' | 'disabled' +> & { item: Item }; + +const AccountEditItemButtons = ({ + item, + onDelete, + onEdit, + disabled, +}: AccountEditItemButtonsProps) => { + const handleEdit = useCallback(() => { + onEdit?.(item); + }, [item, onEdit]); + const handleDelete = useCallback(() => { + onDelete?.(item); + }, [item, onDelete]); + + if (!onEdit && !onDelete) { + return null; + } + + return ( +
+ {onEdit && ( + + )} + {onDelete && ( + + )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_edit/components/section.tsx b/app/javascript/mastodon/features/account_edit/components/section.tsx index 21046b601d..49643b51b1 100644 --- a/app/javascript/mastodon/features/account_edit/components/section.tsx +++ b/app/javascript/mastodon/features/account_edit/components/section.tsx @@ -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 = ({ title, description, showDescription, - onEdit, children, className, - extraButtons, + buttons, }) => { - const intl = useIntl(); return (

- {onEdit && ( - - )} - {extraButtons} + {buttons}
{showDescription && (

diff --git a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx new file mode 100644 index 0000000000..78eb981402 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx @@ -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 = 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 ( + + ); +}; + +const renderItem = (item: ApiFeaturedTagJSON) =>

#{item.name}

; diff --git a/app/javascript/mastodon/features/account_edit/featured_tags.tsx b/app/javascript/mastodon/features/account_edit/featured_tags.tsx new file mode 100644 index 0000000000..a123b90c57 --- /dev/null +++ b/app/javascript/mastodon/features/account_edit/featured_tags.tsx @@ -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 ; + } + + return ( + +
+ + + {tagSuggestions.length > 0 && ( +
+ + {tagSuggestions.map((tag) => ( + + ))} +
+ )} + {isLoading && } + +
+
+ ); +}; + +function renderTag(tag: ApiFeaturedTagJSON) { + return ( +
+

#{tag.name}

+ {tag.statuses_count > 0 && ( + + )} +
+ ); +} + +const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({ + name, + disabled, +}) => { + const dispatch = useAppDispatch(); + const handleAddTag = useCallback(() => { + void dispatch(addFeaturedTag({ name })); + }, [dispatch, name]); + return ; +}; diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index dc48641f37..2673c5363f 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -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) => { 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 ; - } + const history = useHistory(); + const handleFeaturedTagsEdit = useCallback(() => { + history.push('/profile/featured_tags'); + }, [history]); - if (!account) { - return ( - - - - ); + if (!accountId || !account) { + return ; } 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 ( - - - - - } - /> +
{headerSrc && } @@ -129,8 +131,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + } > @@ -138,8 +146,14 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + } > @@ -153,14 +167,23 @@ export const AccountEdit: FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + showDescription={!hasTags} + buttons={ + + } + > + {featuredTags.map((tag) => `#${tag.name}`).join(', ')} + - + ); }; diff --git a/app/javascript/mastodon/features/account_edit/styles.module.scss b/app/javascript/mastodon/features/account_edit/styles.module.scss index 8af244d38b..ee8603cc4f 100644 --- a/app/javascript/mastodon/features/account_edit/styles.module.scss +++ b/app/javascript/mastodon/features/account_edit/styles.module.scss @@ -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; diff --git a/app/javascript/mastodon/features/collections/editor/accounts.tsx b/app/javascript/mastodon/features/collections/editor/accounts.tsx index 02e7852828..cdebd37cdf 100644 --- a/app/javascript/mastodon/features/collections/editor/accounts.tsx +++ b/app/javascript/mastodon/features/collections/editor/accounts.tsx @@ -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 ( @@ -348,21 +354,12 @@ export const CollectionAccounts: React.FC<{ } /> )} - - } - hint={ - hasMaxAccounts ? ( - - ) : undefined - } + + + {hasMaxAccounts && ( + + )} {hasMinAccounts && ( diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 5a9cebe5f4..9e61158f14 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -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 = ; } - const profileRedesignEnabled = isServerFeatureEnabled('profile_redesign'); const profileRedesignRoutes = []; - if (profileRedesignEnabled) { + if (isServerFeatureEnabled('profile_redesign')) { profileRedesignRoutes.push( , ); @@ -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( + , + // If the redesign is not enabled but someone shares an /about link, redirect to the root. , ); } + if (isClientFeatureEnabled('profile_editing')) { + profileRedesignRoutes.push( + , + + ) + } else { + // If profile editing is not enabled, redirect to the home timeline as the current editing pages are outside React Router. + profileRedesignRoutes.push( + , + , + ); + } + return ( @@ -234,8 +248,6 @@ class SwitchingColumnsArea extends PureComponent { - {isClientFeatureEnabled('profile_editing') && } - @@ -243,8 +255,8 @@ class SwitchingColumnsArea extends PureComponent { - {!profileRedesignEnabled && } {...profileRedesignRoutes} + diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 2beedaba26..20ed6a6969 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -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'); } diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 58adf777b1..ecf49b95c5 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -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?", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 66007b7c03..e3c7981d7d 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -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?", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index cee790ef0a..a115801669 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -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?", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 37258ca098..d4cf5c42a1 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -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?", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 59453d2981..b4c38d7d72 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -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": "Ακολούθηση χρήστη;", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 00b8422a00..9fb2cae509 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -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?", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index c3dd1c010d..a157f0efe9 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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 page’s 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", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 6a660200ab..4a2935af8f 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -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?", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index cd9883cb95..bfeedaffdf 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -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?", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index d1015ea065..3a0a321f7b 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -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?", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 5d1fa49e5c..c70a573d63 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -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ää?", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 12cc8e2fa7..cb7f61dfc0 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -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?", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index cd3e86de96..37320bf22e 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -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 l’URL dans la barre d’adresse 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 s’est produite lors du chargement de cet écran.", "bundle_modal_error.retry": "Réessayer", "callout.dismiss": "Rejeter", "carousel.current": "Diapositive {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 d’utilisateur", + "domain_pill.username": "Nom d’utilisateur·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 .", - "domain_pill.who_you_are": "Comme un identifiant indique votre nom et le service vous hébergeant, vous pouvez interagir avec .", + "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 .", "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.", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index c37415adb3..5a04587c2b 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -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 l’URL dans la barre d’adresse 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 d’adresse 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 s’est produite lors du chargement de cet écran.", "bundle_modal_error.retry": "Réessayer", "callout.dismiss": "Rejeter", "carousel.current": "Diapositive {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 d’utilisateur", + "domain_pill.username": "Nom d’utilisateur·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 .", - "domain_pill.who_you_are": "Comme un identifiant indique votre nom et le service vous hébergeant, vous pouvez interagir avec .", + "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 .", "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", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 364503981d..e1d6d6f4a1 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -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?", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 0cd3ec29cd..2d90ad72f4 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -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": "לעקוב אחר המשתמש.ת?", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index 571eb20566..fcae96ed8b 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -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", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 1f19d68ffb..26b063bb0f 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -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?", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 4cdc133088..28c1bbcdc4 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -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?", diff --git a/app/javascript/mastodon/locales/nan-TW.json b/app/javascript/mastodon/locales/nan-TW.json index 221c1de8d7..93d5ad6876 100644 --- a/app/javascript/mastodon/locales/nan-TW.json +++ b/app/javascript/mastodon/locales/nan-TW.json @@ -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âng,kap 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 ê用者?", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 261e0869d2..b33c5c9982 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -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?", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index 9bbd2d013e..2e0792f359 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -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?", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 575803f31c..6fcb83d5b5 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -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?", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 401c46ee16..2ba7878e18 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -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?", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 2af545f38d..4ec5b9a032 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -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?", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 83d18f9bb8..c4d4aeda82 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -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": "确定要关注此用户?", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 7c36865f92..3a50dd53c4 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -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": "是否跟隨該使用者?", diff --git a/app/javascript/mastodon/reducers/slices/index.ts b/app/javascript/mastodon/reducers/slices/index.ts index 06a384d562..8d4ecd552d 100644 --- a/app/javascript/mastodon/reducers/slices/index.ts +++ b/app/javascript/mastodon/reducers/slices/index.ts @@ -1,7 +1,9 @@ import { annualReport } from './annual_report'; import { collections } from './collections'; +import { profileEdit } from './profile_edit'; export const sliceReducers = { annualReport, collections, + profileEdit, }; diff --git a/app/javascript/mastodon/reducers/slices/profile_edit.ts b/app/javascript/mastodon/reducers/slices/profile_edit.ts new file mode 100644 index 0000000000..c966325203 --- /dev/null +++ b/app/javascript/mastodon/reducers/slices/profile_edit.ts @@ -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) { + 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, +); diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 0393c06763..25afce54d0 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -77,7 +77,8 @@ const initialState = ImmutableMap({ follow_requests: initialListState, blocks: initialListState, mutes: initialListState, - featured_tags: initialListState, + /** @type {ImmutableMap} */ + featured_tags: ImmutableMap(), }); const normalizeList = (state, path, accounts, next) => { diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 9a8c565beb..93cae1d9b4 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -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; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index cfa5d068d3..5a906c93a2 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1670,7 +1670,7 @@ body > [data-popper-placement] { .detailed-status__display-name { color: var(--color-text-tertiary); - span { + span:not(.account__avatar) { display: inline; } diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 7ea690dc34..2d20b6a969 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -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/ diff --git a/config/locales/activerecord.de.yml b/config/locales/activerecord.de.yml index 4fc7133102..77bf75e0ff 100644 --- a/config/locales/activerecord.de.yml +++ b/config/locales/activerecord.de.yml @@ -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 diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 184abfc167..041f2756e6 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -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: diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 5eeac91ee0..dfe2d15ac7 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -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/neu’r 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" diff --git a/config/locales/da.yml b/config/locales/da.yml index 4240bf8562..0d6e6d8129 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -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: diff --git a/config/locales/de.yml b/config/locales/de.yml index 000b6e0aca..c94bef60e6 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -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 %{count} Tag lang erfolglos bleibt, werden keine weiteren Zustellversuche unternommen, bis eine Zustellung von 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} diff --git a/config/locales/doorkeeper.fr-CA.yml b/config/locales/doorkeeper.fr-CA.yml index 40575f9a9d..2c11dbf6c0 100644 --- a/config/locales/doorkeeper.fr-CA.yml +++ b/config/locales/doorkeeper.fr-CA.yml @@ -83,6 +83,10 @@ fr-CA: access_denied: Le/la propriétaire de la ressource ou le serveur d’autorisation 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 n’est pas configuré. invalid_client: L’authentification du client a échoué à cause d’un client inconnu, d’aucune authentification de client incluse ou d’une méthode d’authentification 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: L’autorisation accordée est invalide, expirée, révoquée, ne concorde pas avec l’URI de redirection utilisée dans la requête d’autorisation, ou a été délivrée à un autre client. invalid_redirect_uri: L’URI de redirection n’est pas valide. invalid_request: diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index 4c7d067a0f..7329f61391 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -83,6 +83,10 @@ fr: access_denied: Le propriétaire de la ressource ou le serveur d’autorisation 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 n’est pas configuré. invalid_client: L’authentification du client a échoué à cause d’un client inconnu, d’aucune authentification de client incluse ou d’une méthode d’authentification 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: L’autorisation accordée est invalide, expirée, annulée, ne concorde pas avec l’URL de redirection utilisée dans la requête d’autorisation, ou a été délivrée à un autre client. invalid_redirect_uri: L’URL de redirection n’est pas valide. invalid_request: diff --git a/config/locales/doorkeeper.nn.yml b/config/locales/doorkeeper.nn.yml index 3b8a9b0663..fb62a29a3c 100644 --- a/config/locales/doorkeeper.nn.yml +++ b/config/locales/doorkeeper.nn.yml @@ -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: diff --git a/config/locales/el.yml b/config/locales/el.yml index 9ecbfa4a58..f46ef9cb58 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -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: diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 190c5abe75..abef3aa441 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -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: diff --git a/config/locales/en.yml b/config/locales/en.yml index 71c559d738..93b8d4088e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 44e68de373..f19ba9971c 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -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: diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 53787863b9..3b2328137a 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -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: diff --git a/config/locales/es.yml b/config/locales/es.yml index 2f72f6c7ad..ce78f3e78a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -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: diff --git a/config/locales/fi.yml b/config/locales/fi.yml index c10a6b570d..d48ded9e22 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -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. diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 0bc27a8fd3..04ed3aa5b2 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -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: diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index 41670dfe40..751211e470 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -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: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 21099f82fe..5e0d57f820 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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: diff --git a/config/locales/ga.yml b/config/locales/ga.yml index f0460ddae2..6148e6950c 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -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: diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 3bd3eaab44..0ac225d728 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -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: diff --git a/config/locales/he.yml b/config/locales/he.yml index 6070455cca..c743d79568 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -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: diff --git a/config/locales/hu.yml b/config/locales/hu.yml index d9f52cac22..cc375a2254 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -689,7 +689,7 @@ hu: cancel: Mégse category: Kategória category_description_html: A fiók vagy tartalom bejelentésének oka a jelentett fiókkal kapcsolatos kommunikációban idézve lesz - collections: Gyűjtemények + collections: Gyűjtemények (%{count}) comment: none: Egyik sem comment_description_html: 'Hogy további információkat adjon, %{name} ezt írta:' @@ -725,7 +725,7 @@ hu: resolved_msg: A bejelentést sikeresen megoldottuk! skip_to_actions: Tovább az intézkedésekhez status: Állapot - statuses: Bejegyzések + statuses: Bejegyzések (%{count}) statuses_description_html: A sértő tartalmat idézni fogjuk a bejelentett fiókkal való kommunikáció során summary: action_preambles: diff --git a/config/locales/is.yml b/config/locales/is.yml index c247f37723..4bcda2ceee 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -689,7 +689,7 @@ is: cancel: Hætta við category: Flokkur category_description_html: Ástæðan fyrir því að þessi notandaaðgangur og/eða efni hans var kært mun verða tiltekin í samskiptum við kærðan notandaaðgang - collections: Söfn + collections: Söfn (%{count}) comment: none: Ekkert comment_description_html: 'Til að gefa nánari upplýsingar skrifaði %{name}:' @@ -725,7 +725,7 @@ is: resolved_msg: Það tókst að leysa kæruna! skip_to_actions: Sleppa og fara í aðgerðir status: Staða - statuses: Færslur + statuses: Færslur (%{count}) statuses_description_html: Óviðeigandi efni verður tiltekið í samskiptum við kærðan notandaaðgang summary: action_preambles: diff --git a/config/locales/it.yml b/config/locales/it.yml index b58882034b..a9e489f1a5 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -689,7 +689,7 @@ it: cancel: Annulla category: Categoria category_description_html: Il motivo per cui questo account e/o contenuto è stato segnalato sarà citato nella comunicazione con l'account segnalato - collections: Collezioni + collections: Collezioni (%{count}) comment: none: Nessuno comment_description_html: 'Per fornire ulteriori informazioni, %{name} ha scritto:' @@ -725,7 +725,7 @@ it: resolved_msg: Rapporto risolto! skip_to_actions: Passa alle azioni status: Stato - statuses: Post + statuses: Post (%{count}) statuses_description_html: Il contenuto offensivo sarà citato nella comunicazione con l'account segnalato summary: action_preambles: diff --git a/config/locales/nan-TW.yml b/config/locales/nan-TW.yml index ae3eb8e1c4..317bb0b1b8 100644 --- a/config/locales/nan-TW.yml +++ b/config/locales/nan-TW.yml @@ -53,6 +53,7 @@ nan-TW: label: 改角色 no_role: 無角色 title: 替 %{username} 改角色 + collections: 收藏 confirm: 確認 confirmed: 確認ah confirming: Teh確認 @@ -336,6 +337,14 @@ nan-TW: unpublish: 取消公佈 unpublished_msg: 公告成功取消ah! updated_msg: 公告成功更新ah! + collections: + accounts: 口座 + collection_title: "%{name} ê收藏" + contents: 內容 + number_of_accounts: + other: "%{count} ê口座" + open: 開 + view_publicly: 公開看 critical_update_pending: 愛處理ê重大更新 custom_emojis: assign_category: 分配類別 @@ -666,6 +675,7 @@ nan-TW: cancel: 取消 category: 類別 category_description_html: Tsit ê 受檢舉ê口座kap/á是內容,ē佇kap tsit ê口座ê聯絡內底引用。 + collections: 收藏 comment: none: 無 comment_description_html: 為著提供其他資訊,%{name} 寫: @@ -695,11 +705,13 @@ nan-TW: report: '檢舉 #%{id}' reported_account: 受檢舉ê口座 reported_by: 檢舉人 + reported_content: 受檢舉ê內容 reported_with_application: 用應用程式檢舉 resolved: 解決ah resolved_msg: 檢舉成功解決ah! skip_to_actions: 跳kàu行動 status: 狀態 + statuses: PO文 statuses_description_html: 冒犯ê內容ē引用tī kap受檢舉口座ê聯絡 summary: action_preambles: @@ -1859,8 +1871,26 @@ nan-TW: webauthn_authentication: 安全鎖匙 statuses: default_language: Kap界面ê語言sio kâng + visibilities: + private: Kan-ta hōo跟tuè ê lâng + public: 公開ê + public_long: 逐ê lâng(無論佇Mastodon以內á是以外) + unlisted: 恬靜公開 + unlisted_long: Mài顯示tī Mastodon ê tshiau-tshuē結果、趨勢kap公共ê時間線。 + statuses_cleanup: + enabled: 自動thâi掉舊ê PO文 + enabled_hint: PO文一下kàu指定ê期限,會自動thâi掉PO文,除非in符合下kha其中一个特例 + exceptions: 特例 + explanation: 自動thâi掉會行佇低優先級。佇kàu thâi掉ê期限kap thâi掉中間huân勢有延tshiân。 + ignore_favs: 忽略收藏數 + ignore_reblogs: 忽略轉PO數 + interaction_exceptions: Tshāi佇互動ê特例 + interaction_exceptions_explanation: 超過收藏kap轉送ê底限ê PO文可能會受保留,就算in後來降落。 + keep_direct: 保留私人ê短phue terms_of_service: title: 服務規定 + themes: + default: Mastodon two_factor_authentication: disable: 停止用雙因素認證 user_mailer: diff --git a/config/locales/nl.yml b/config/locales/nl.yml index db173adece..9c8649df8b 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -56,6 +56,7 @@ nl: label: Rol veranderen no_role: Geen rol title: Rol van %{username} veranderen + collections: Verzamelingen confirm: Bevestigen confirmed: Bevestigd confirming: Bevestiging @@ -340,6 +341,15 @@ nl: unpublish: Uitschakelen unpublished_msg: Ongedaan maken van gepubliceerde mededeling geslaagd! updated_msg: Bijwerken van mededeling geslaagd! + collections: + accounts: Accounts + collection_title: Verzameling van %{name} + contents: Inhoud + number_of_accounts: + one: 1 account + other: "%{count} accounts" + open: Openen + view_publicly: Openbaar bericht bekijken critical_update_pending: Kritieke update in behandeling custom_emojis: assign_category: Categorie toewijzen @@ -679,6 +689,7 @@ nl: cancel: Annuleren category: Category category_description_html: De reden waarom dit account en/of inhoud werd gerapporteerd wordt aan het gerapporteerde account medegedeeld + collections: Verzamelingen comment: none: Geen comment_description_html: 'Om meer informatie te verstrekken, schreef %{name}:' @@ -708,11 +719,13 @@ nl: report: 'Rapportage #%{id}' reported_account: Gerapporteerde account reported_by: Gerapporteerd door + reported_content: Gerapporteerde inhoud reported_with_application: Gerapporteerd met applicatie resolved: Opgelost resolved_msg: Rapportage succesvol opgelost! skip_to_actions: Ga direct naar de maatregelen status: Rapportages + statuses: Berichten statuses_description_html: De problematische inhoud wordt aan het gerapporteerde account medegedeeld summary: action_preambles: diff --git a/config/locales/nn.yml b/config/locales/nn.yml index adbf513ada..587a09dd3f 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -56,6 +56,7 @@ nn: label: Endre rolle no_role: Inga rolle title: Endre rolle for %{username} + collections: Samlingar confirm: Stadfest confirmed: Stadfesta confirming: Stadfestar @@ -340,6 +341,12 @@ nn: unpublish: Avpubliser unpublished_msg: Du avpubliserte kunngjeringa. updated_msg: Du oppdaterte kunngjeringa. + collections: + accounts: Kontoar + collection_title: Samling av %{name} + contents: Innhald + open: Opna + view_publicly: Vis offentleg critical_update_pending: Kritisk oppdatering ventar custom_emojis: assign_category: Vel kategori @@ -679,6 +686,7 @@ nn: cancel: Avbryt category: Kategori category_description_html: Årsaka til at kontoen og/eller innhaldet vart rapportert vil bli inkludert i kommunikasjonen med den rapporterte kontoen + collections: Samlingar comment: none: Ingen comment_description_html: 'For å gje meir informasjon, skreiv %{name}:' @@ -708,11 +716,13 @@ nn: report: 'Rapporter #%{id}' reported_account: Rapportert konto reported_by: Rapportert av + reported_content: Rapportert innhald reported_with_application: Rapportert med app resolved: Oppløyst resolved_msg: Rapporten er løyst! skip_to_actions: Gå til handlingar status: Status + statuses: Innlegg statuses_description_html: Støytande innhald vil bli inkludert i kommunikasjonen med den rapporterte kontoen summary: action_preambles: diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index cebca2fad4..ff50b446ee 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -689,7 +689,7 @@ pt-BR: cancel: Cancelar category: Categoria category_description_html: O motivo pelo qual esta conta e/ou conteúdo foi denunciado será citado na comunicação com a conta denunciada - collections: Coleções + collections: Coleções (%{count}) comment: none: Nenhum comment_description_html: 'Para fornecer mais informações, %{name} escreveu:' @@ -725,7 +725,7 @@ pt-BR: resolved_msg: Denúncia resolvida! skip_to_actions: Pular para ações status: Estado - statuses: Publicações + statuses: Publicações (%{count}) statuses_description_html: Conteúdo ofensivo será citado em comunicação com a conta denunciada summary: action_preambles: diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index dd065fc071..b44756798c 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -686,7 +686,7 @@ pt-PT: cancel: Cancelar category: Categoria category_description_html: A razão pela qual esta conta e/ou conteúdo foi denunciado será citada na comunicação com a conta denunciada - collections: Coleções + collections: Coleções (%{count}) comment: none: Nenhum comment_description_html: 'Para fornecer mais informações, %{name} escreveu:' @@ -722,7 +722,7 @@ pt-PT: resolved_msg: Denúncia resolvida com sucesso! skip_to_actions: Passar para as ações status: Estado - statuses: Publicações + statuses: Publicações (%{count}) statuses_description_html: O conteúdo ofensivo será citado na comunicação com a conta denunciada summary: action_preambles: diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index c41e41f690..1c85042bc0 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -159,10 +159,10 @@ de: other: Wir müssen sicherstellen, dass du mindestens %{count} Jahre alt bist, um %{domain} verwenden zu können. Wir werden diese Information nicht aufbewahren. role: Die Rolle bestimmt, welche Berechtigungen das Konto hat. user_role: - color: Farbe, die für diese Rolle in der gesamten Benutzerschnittstelle verwendet wird, als RGB im Hexadezimalsystem - highlighted: Dies macht die Rolle öffentlich im Profil sichtbar + color: Farbe, die für diese Rolle im Webinterface verwendet wird, als RGB im Hexadezimalsystem + highlighted: Moderative/administrative Rolle im öffentlichen Profil anzeigen name: Name der Rolle, der auch öffentlich als Badge angezeigt wird, sofern dies unten aktiviert ist - permissions_as_keys: Nutzer*innen mit dieser Rolle haben Zugriff auf … + permissions_as_keys: Konten mit dieser Rolle haben eine Berechtigung für … position: Eine höherrangige Rolle entscheidet in bestimmten Situationen über Konfliktlösungen. Einige Aktionen können jedoch nur mit untergeordneten Rollen durchgeführt werden require_2fa: Profile mit dieser Rolle müssen eine Zwei-Faktor-Authentisierung einrichten, um Mastodon verwenden zu können username_block: @@ -385,7 +385,7 @@ de: time_zone: Zeitzone user_role: color: Badge-Farbe - highlighted: Rolle als Badge im Profil anzeigen + highlighted: Badge im Profil name: Name permissions_as_keys: Berechtigungen position: Priorität diff --git a/config/locales/sq.yml b/config/locales/sq.yml index c4e61dd8cc..0673375638 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -685,7 +685,7 @@ sq: cancel: Anuloje category: Kategori category_description_html: Arsyeja pse kjo llogari dhe/ose lëndë raportohet do të citohet te komunikimi me llogarinë e raportuar - collections: Koleksione + collections: Koleksione (%{count}) comment: none: Asnjë comment_description_html: 'Për të dhënë më tepër informacion, %{name} shkroi:' @@ -721,7 +721,7 @@ sq: resolved_msg: Raportimi u zgjidh me sukses! skip_to_actions: Kaloni te veprimet status: Gjendje - statuses: Postime + statuses: Postime (%{count}) statuses_description_html: Lënda problematike do të citohet në komunikimin me llogarinë e raportuar summary: action_preambles: diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 3677b8f311..9fbc6e1b2f 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -716,7 +716,7 @@ sv: resolved_msg: Anmälan har lösts framgångsrikt! skip_to_actions: Hoppa till åtgärder status: Status - statuses: Inlägg + statuses: Inlägg (%{count}) statuses_description_html: Stötande innehåll kommer att citeras i kommunikationen med det rapporterade kontot summary: action_preambles: diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 1bc50e9192..f73a662169 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -56,6 +56,7 @@ tr: label: Rolü değiştir no_role: Rol yok title: "%{username} için rolü değiştir" + collections: Koleksiyonlar confirm: Onayla confirmed: Onaylandı confirming: Onaylanıyor @@ -340,6 +341,15 @@ tr: unpublish: Yayından kaldır unpublished_msg: Duyuru başarıyla yayından kaldırıldı! updated_msg: Duyuru başarıyla güncellendi! + collections: + accounts: Hesaplar + collection_title: "%{name} koleksiyonu" + contents: İçerikler + number_of_accounts: + one: 1 hesap + other: "%{count} hesap" + open: Aç + view_publicly: Herkese açık görüntüle critical_update_pending: Kritik güncelleme bekliyor custom_emojis: assign_category: Kategori ata @@ -679,6 +689,7 @@ tr: cancel: İptal et category: Kategori category_description_html: Bu hesap ve/veya içeriğin bildirilme gerekçesi, bildirilen hesapla iletişimde alıntılanacaktır + collections: Koleksiyonlar comment: none: Yok comment_description_html: 'Daha fazla bilgi vermek için %{name} şunu yazdı:' @@ -708,11 +719,13 @@ tr: report: 'Şikayet #%{id}' reported_account: Şikayet edilen hesap reported_by: Şikayet eden + reported_content: Bildirilen içerik reported_with_application: Uygulamayla bildirildi resolved: Giderildi resolved_msg: Şikayet başarıyla çözümlendi! skip_to_actions: İşlemlere atla status: Durum + statuses: Gönderiler statuses_description_html: İncitici içerik, bildirilen hesapla iletişimde alıntılanacaktır summary: action_preambles: diff --git a/config/locales/vi.yml b/config/locales/vi.yml index b39b92ec92..42af0b6603 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -675,7 +675,7 @@ vi: cancel: Hủy bỏ category: Phân loại category_description_html: Lý do tài khoản hoặc nội dung này bị báo cáo sẽ được trích dẫn khi giao tiếp với họ - collections: Collection + collections: Collection (%{count}) comment: none: Không có mô tả comment_description_html: "%{name} cho biết thêm:" @@ -711,7 +711,7 @@ vi: resolved_msg: Đã xử lý báo cáo xong! skip_to_actions: Kiểm duyệt status: Trạng thái - statuses: Tút + statuses: Tút (%{count}) statuses_description_html: Lý do tài khoản hoặc nội dung này bị báo cáo sẽ được trích dẫn khi giao tiếp với họ summary: action_preambles: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 6e7b0c9d8a..782b3bcea9 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -675,7 +675,7 @@ zh-CN: cancel: 取消 category: 类别 category_description_html: 在与被举报账号的通信时,将引用该账号和/或内容被举报的原因 - collections: 收藏列表 + collections: 收藏列表 (%{count}) comment: none: 没有 comment_description_html: "%{name} 补充道:" @@ -711,7 +711,7 @@ zh-CN: resolved_msg: 举报处理成功! skip_to_actions: 跳转到操作 status: 状态 - statuses: 嘟文 + statuses: 嘟文 (%{count}) statuses_description_html: 在与该账号的通信中将引用违规内容 summary: action_preambles: diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index cbe1337d92..9c0e621170 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -675,7 +675,7 @@ zh-TW: cancel: 取消 category: 分類 category_description_html: 此帳號及/或被檢舉內容之原因將被引用於檢舉帳號通知中 - collections: 收藏名單 + collections: 收藏名單 (%{count}) comment: none: 無 comment_description_html: 提供更多資訊,%{name} 寫道: @@ -711,7 +711,7 @@ zh-TW: resolved_msg: 檢舉報告已處理完成! skip_to_actions: 跳過行動 status: 嘟文 - statuses: 嘟文 + statuses: 嘟文 (%{count}) statuses_description_html: 侵犯性違規內容將被引用於檢舉帳號通知中 summary: action_preambles: diff --git a/spec/models/admin/status_batch_action_spec.rb b/spec/models/admin/status_batch_action_spec.rb new file mode 100644 index 0000000000..4db1a98582 --- /dev/null +++ b/spec/models/admin/status_batch_action_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::StatusBatchAction do + subject do + described_class.new( + current_account:, + type:, + status_ids:, + report_id:, + text: + ) + end + + let(:current_account) { Fabricate(:admin_user).account } + let(:target_account) { Fabricate(:account) } + let(:statuses) { Fabricate.times(2, :status, account: target_account) } + let(:status_ids) { statuses.map(&:id) } + let(:report) { Fabricate(:report, target_account:, status_ids:) } + let(:report_id) { report.id } + let(:text) { 'test' } + + describe '#save!' do + context 'when `type` is `delete`' do + let(:type) { 'delete' } + + it 'discards the statuses' do + subject.save! + + statuses.each do |status| + expect(status.reload).to be_discarded + end + expect(report.reload).to be_action_taken + end + end + + context 'when `type` is `mark_as_sensitive`' do + let(:type) { 'mark_as_sensitive' } + + before do + preview_card = Fabricate(:preview_card) + statuses.each do |status| + PreviewCardsStatus.create!(status:, preview_card:) + end + end + + it 'marks the statuses as sensitive' do + subject.save! + + statuses.each do |status| + expect(status.reload).to be_sensitive + end + expect(report.reload).to be_action_taken + end + end + + context 'when `type` is `report`' do + let(:report_id) { nil } + let(:type) { 'report' } + + it 'creates a report' do + expect { subject.save! }.to change(Report, :count).by(1) + + new_report = Report.last + expect(new_report.status_ids).to match_array(status_ids) + end + end + + context 'when `type` is `remove_from_report`' do + let(:type) { 'remove_from_report' } + + it 'removes the statuses from the report' do + subject.save! + + expect(report.reload.status_ids).to be_empty + end + end + end +end