Merge commit 'bd575a1dd69d87ca0f69873f7badf28d38e8b9ed' into glitch-soc/merge-upstream

th-downstream
Claire 11 months ago
commit 3ad92e8d0d

@ -0,0 +1,13 @@
coverage:
status:
project:
default:
# Github status check is not blocking
informational: true
patch:
default:
# Github status check is not blocking
informational: true
comment:
# Only write a comment in PR if there are changes
require_changes: true

@ -16,6 +16,8 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
current_user.update(user_params) if user_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e).as_json, status: 422
end
private

@ -3,45 +3,56 @@
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
before_action :require_user!
before_action :set_push_subscription, only: :update
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
after_action :update_session_with_subscription, only: :create
def create
active_session = current_session
@push_subscription = ::Web::PushSubscription.create!(web_push_subscription_params)
unless active_session.web_push_subscription.nil?
active_session.web_push_subscription.destroy!
active_session.update!(web_push_subscription: nil)
end
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
# Mobile devices do not support regular notifications, so we enable push notifications by default
alerts_enabled = active_session.detection.device.mobile? || active_session.detection.device.tablet?
def update
@push_subscription.update!(data: data_params)
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end
data = {
policy: 'all',
alerts: Notification::TYPES.index_with { alerts_enabled },
}
private
data.deep_merge!(data_params) if params[:data]
def active_session
@active_session ||= current_session
end
push_subscription = ::Web::PushSubscription.create!(
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
data: data,
user_id: active_session.user_id,
access_token_id: active_session.access_token_id
)
def destroy_previous_subscriptions
active_session.web_push_subscription.destroy!
active_session.update!(web_push_subscription: nil)
end
def prior_subscriptions?
active_session.web_push_subscription.present?
end
active_session.update!(web_push_subscription: push_subscription)
def subscription_data
default_subscription_data.tap do |data|
data.deep_merge!(data_params) if params[:data]
end
end
render json: push_subscription, serializer: REST::WebPushSubscriptionSerializer
def default_subscription_data
{
policy: 'all',
alerts: Notification::TYPES.index_with { alerts_enabled },
}
end
def update
@push_subscription.update!(data: data_params)
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
def alerts_enabled
# Mobile devices do not support regular notifications, so we enable push notifications by default
active_session.detection.device.mobile? || active_session.detection.device.tablet?
end
private
def update_session_with_subscription
active_session.update!(web_push_subscription: @push_subscription)
end
def set_push_subscription
@push_subscription = ::Web::PushSubscription.find(params[:id])
@ -51,6 +62,17 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end
def web_push_subscription_params
{
access_token_id: active_session.access_token_id,
data: subscription_data,
endpoint: subscription_params[:endpoint],
key_auth: subscription_params[:keys][:auth],
key_p256dh: subscription_params[:keys][:p256dh],
user_id: active_session.user_id,
}
end
def data_params
@data_params ||= params.require(:data).permit(:policy, alerts: Notification::TYPES)
end

@ -661,3 +661,18 @@ export function unpinAccountFail(error) {
error,
};
}
export const updateAccount = ({ displayName, note, avatar, header, discoverable, indexable }) => (dispatch, getState) => {
const data = new FormData();
data.append('display_name', displayName);
data.append('note', note);
if (avatar) data.append('avatar', avatar);
if (header) data.append('header', header);
data.append('discoverable', discoverable);
data.append('indexable', indexable);
return api(getState).patch('/api/v1/accounts/update_credentials', data).then(response => {
dispatch(importFetchedAccount(response.data));
});
};

@ -20,6 +20,7 @@ export interface ApiAccountJSON {
bot: boolean;
created_at: string;
discoverable: boolean;
indexable: boolean;
display_name: string;
emojis: ApiCustomEmojiJSON[];
fields: ApiAccountFieldJSON[];

@ -51,7 +51,7 @@ export default class Retention extends PureComponent {
let content;
if (loading) {
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading...' />;
content = <FormattedMessage id='loading_indicator.label' defaultMessage='Loading' />;
} else {
content = (
<table className='retention__table'>

@ -1,7 +1,23 @@
import { useIntl, defineMessages } from 'react-intl';
import { CircularProgress } from './circular_progress';
export const LoadingIndicator: React.FC = () => (
<div className='loading-indicator'>
<CircularProgress size={50} strokeWidth={6} />
</div>
);
const messages = defineMessages({
loading: { id: 'loading_indicator.label', defaultMessage: 'Loading…' },
});
export const LoadingIndicator: React.FC = () => {
const intl = useIntl();
return (
<div
className='loading-indicator'
role='progressbar'
aria-busy
aria-live='polite'
aria-label={intl.formatMessage(messages.loading)}
>
<CircularProgress size={50} strokeWidth={6} />
</div>
);
};

@ -1,29 +0,0 @@
import PropTypes from 'prop-types';
import { Fragment } from 'react';
import classNames from 'classnames';
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
import { Icon } from 'mastodon/components/icon';
const ProgressIndicator = ({ steps, completed }) => (
<div className='onboarding__progress-indicator'>
{(new Array(steps)).fill().map((_, i) => (
<Fragment key={i}>
{i > 0 && <div className={classNames('onboarding__progress-indicator__line', { active: completed > i })} />}
<div className={classNames('onboarding__progress-indicator__step', { active: completed > i })}>
{completed > i && <Icon icon={CheckIcon} />}
</div>
</Fragment>
))}
</div>
);
ProgressIndicator.propTypes = {
steps: PropTypes.number.isRequired,
completed: PropTypes.number,
};
export default ProgressIndicator;

@ -1,11 +1,13 @@
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
import { ReactComponent as CheckIcon } from '@material-symbols/svg-600/outlined/done.svg';
import { Icon } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon';
const Step = ({ label, description, icon, iconComponent, completed, onClick, href }) => {
export const Step = ({ label, description, icon, iconComponent, completed, onClick, href, to }) => {
const content = (
<>
<div className='onboarding__steps__item__icon'>
@ -29,6 +31,12 @@ const Step = ({ label, description, icon, iconComponent, completed, onClick, hre
{content}
</a>
);
} else if (to) {
return (
<Link to={to} className='onboarding__steps__item'>
{content}
</Link>
);
}
return (
@ -45,7 +53,6 @@ Step.propTypes = {
iconComponent: PropTypes.func,
completed: PropTypes.bool,
href: PropTypes.string,
to: PropTypes.string,
onClick: PropTypes.func,
};
export default Step;

@ -1,79 +1,62 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { markAsPartial } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { EmptyAccount } from 'mastodon/components/empty_account';
import Account from 'mastodon/containers/account_container';
import { useAppSelector } from 'mastodon/store';
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
});
class Follows extends PureComponent {
static propTypes = {
onBack: PropTypes.func,
dispatch: PropTypes.func.isRequired,
suggestions: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
};
export const Follows = () => {
const dispatch = useDispatch();
const isLoading = useAppSelector(state => state.getIn(['suggestions', 'isLoading']));
const suggestions = useAppSelector(state => state.getIn(['suggestions', 'items']));
componentDidMount () {
const { dispatch } = this.props;
useEffect(() => {
dispatch(fetchSuggestions(true));
}
componentWillUnmount () {
const { dispatch } = this.props;
dispatch(markAsPartial('home'));
}
render () {
const { onBack, isLoading, suggestions } = this.props;
return () => {
dispatch(markAsPartial('home'));
};
}, [dispatch]);
let loadedContent;
let loadedContent;
if (isLoading) {
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
}
return (
<Column>
<ColumnBackButton onClick={onBack} />
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
</div>
if (isLoading) {
loadedContent = (new Array(8)).fill().map((_, i) => <EmptyAccount key={i} />);
} else if (suggestions.isEmpty()) {
loadedContent = <div className='follow-recommendations__empty'><FormattedMessage id='onboarding.follows.empty' defaultMessage='Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.' /></div>;
} else {
loadedContent = suggestions.map(suggestion => <Account id={suggestion.get('account')} key={suggestion.get('account')} withBio />);
}
<div className='follow-recommendations'>
{loadedContent}
</div>
return (
<>
<ColumnBackButton />
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.follows.title' defaultMessage='Popular on Mastodon' /></h3>
<p><FormattedMessage id='onboarding.follows.lead' defaultMessage='You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!' /></p>
</div>
<div className='onboarding__footer'>
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></button>
</div>
<div className='follow-recommendations'>
{loadedContent}
</div>
</Column>
);
}
}
<p className='onboarding__lead'><FormattedMessage id='onboarding.tips.accounts_from_other_servers' defaultMessage='<strong>Did you know?</strong> Since Mastodon is decentralized, some profiles you come across will be hosted on servers other than yours. And yet you can interact with them seamlessly! Their server is in the second half of their username!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p>
export default connect(mapStateToProps)(Follows);
<div className='onboarding__footer'>
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.actions.back' defaultMessage='Take me back' /></Link>
</div>
</div>
</>
);
};

@ -1,152 +1,90 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet';
import { Link, withRouter } from 'react-router-dom';
import { Link, Switch, Route, useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import { ReactComponent as AccountCircleIcon } from '@material-symbols/svg-600/outlined/account_circle.svg';
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
import { ReactComponent as EditNoteIcon } from '@material-symbols/svg-600/outlined/edit_note.svg';
import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outlined/person_add.svg';
import { debounce } from 'lodash';
import illustration from 'mastodon/../images/elephant_ui_conversation.svg';
import { fetchAccount } from 'mastodon/actions/accounts';
import { focusCompose } from 'mastodon/actions/compose';
import { closeOnboarding } from 'mastodon/actions/onboarding';
import { Icon } from 'mastodon/components/icon';
import Column from 'mastodon/features/ui/components/column';
import { me } from 'mastodon/initial_state';
import { makeGetAccount } from 'mastodon/selectors';
import { useAppSelector } from 'mastodon/store';
import { assetHost } from 'mastodon/utils/config';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import Step from './components/step';
import Follows from './follows';
import Share from './share';
import { Step } from './components/step';
import { Follows } from './follows';
import { Profile } from './profile';
import { Share } from './share';
const messages = defineMessages({
template: { id: 'onboarding.compose.template', defaultMessage: 'Hello #Mastodon!' },
});
const mapStateToProps = () => {
const getAccount = makeGetAccount();
return state => ({
account: getAccount(state, me),
});
};
class Onboarding extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.record,
...WithRouterPropTypes,
};
state = {
step: null,
profileClicked: false,
shareClicked: false,
};
handleClose = () => {
const { dispatch, history } = this.props;
dispatch(closeOnboarding());
history.push('/home');
};
handleProfileClick = () => {
this.setState({ profileClicked: true });
};
handleFollowClick = () => {
this.setState({ step: 'follows' });
};
handleComposeClick = () => {
const { dispatch, intl, history } = this.props;
const Onboarding = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const dispatch = useDispatch();
const intl = useIntl();
const history = useHistory();
const handleComposeClick = useCallback(() => {
dispatch(focusCompose(history, intl.formatMessage(messages.template)));
};
handleShareClick = () => {
this.setState({ step: 'share', shareClicked: true });
};
handleBackClick = () => {
this.setState({ step: null });
};
handleWindowFocus = debounce(() => {
const { dispatch, account } = this.props;
dispatch(fetchAccount(account.get('id')));
}, 1000, { trailing: true });
componentDidMount () {
window.addEventListener('focus', this.handleWindowFocus, false);
}
componentWillUnmount () {
window.removeEventListener('focus', this.handleWindowFocus);
}
render () {
const { account } = this.props;
const { step, shareClicked } = this.state;
switch(step) {
case 'follows':
return <Follows onBack={this.handleBackClick} />;
case 'share':
return <Share onBack={this.handleBackClick} />;
}
return (
<Column>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<img src={illustration} alt='' className='onboarding__illustration' />
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
}, [dispatch, intl, history]);
return (
<Column>
<Switch>
<Route path='/start' exact>
<div className='scrollable privacy-policy'>
<div className='column-title'>
<img src={illustration} alt='' className='onboarding__illustration' />
<h3><FormattedMessage id='onboarding.start.title' defaultMessage="You've made it!" /></h3>
<p><FormattedMessage id='onboarding.start.lead' defaultMessage="Your new Mastodon account is ready to go. Here's how you can make the most of it:" /></p>
</div>
<div className='onboarding__steps'>
<Step to='/start/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step to='/start/follows' completed={(account.get('following_count') * 1) >= 1} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step to='/start/share' icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div>
</Route>
<Route path='/start/profile' component={Profile} />
<Route path='/start/follows' component={Follows} />
<Route path='/start/share' component={Share} />
</Switch>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
};
<div className='onboarding__steps'>
<Step onClick={this.handleProfileClick} href='/settings/profile' completed={(!account.get('avatar').endsWith('missing.png')) || (account.get('display_name').length > 0 && account.get('note').length > 0)} icon='address-book-o' iconComponent={AccountCircleIcon} label={<FormattedMessage id='onboarding.steps.setup_profile.title' defaultMessage='Customize your profile' />} description={<FormattedMessage id='onboarding.steps.setup_profile.body' defaultMessage='Others are more likely to interact with you with a filled out profile.' />} />
<Step onClick={this.handleFollowClick} completed={(account.get('following_count') * 1) >= 7} icon='user-plus' iconComponent={PersonAddIcon} label={<FormattedMessage id='onboarding.steps.follow_people.title' defaultMessage='Find at least {count, plural, one {one person} other {# people}} to follow' values={{ count: 7 }} />} description={<FormattedMessage id='onboarding.steps.follow_people.body' defaultMessage="You curate your own home feed. Let's fill it with interesting people." />} />
<Step onClick={this.handleComposeClick} completed={(account.get('statuses_count') * 1) >= 1} icon='pencil-square-o' iconComponent={EditNoteIcon} label={<FormattedMessage id='onboarding.steps.publish_status.title' defaultMessage='Make your first post' />} description={<FormattedMessage id='onboarding.steps.publish_status.body' defaultMessage='Say hello to the world.' values={{ emoji: <img className='emojione' alt='🐘' src={`${assetHost}/emoji/1f418.svg`} /> }} />} />
<Step onClick={this.handleShareClick} completed={shareClicked} icon='copy' iconComponent={ContentCopyIcon} label={<FormattedMessage id='onboarding.steps.share_profile.title' defaultMessage='Share your profile' />} description={<FormattedMessage id='onboarding.steps.share_profile.body' defaultMessage='Let your friends know how to find you on Mastodon!' />} />
</div>
<p className='onboarding__lead'><FormattedMessage id='onboarding.start.skip' defaultMessage="Don't need help getting started?" /></p>
<div className='onboarding__links'>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
</div>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default withRouter(connect(mapStateToProps)(injectIntl(Onboarding)));
export default Onboarding;

@ -0,0 +1,162 @@
import { useState, useMemo, useCallback, createRef } from 'react';
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { ReactComponent as AddPhotoAlternateIcon } from '@material-symbols/svg-600/outlined/add_photo_alternate.svg';
import { ReactComponent as EditIcon } from '@material-symbols/svg-600/outlined/edit.svg';
import Toggle from 'react-toggle';
import { updateAccount } from 'mastodon/actions/accounts';
import { Button } from 'mastodon/components/button';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { me } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store';
import { unescapeHTML } from 'mastodon/utils/html';
const messages = defineMessages({
uploadHeader: { id: 'onboarding.profile.upload_header', defaultMessage: 'Upload profile header' },
uploadAvatar: { id: 'onboarding.profile.upload_avatar', defaultMessage: 'Upload profile picture' },
});
export const Profile = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const [displayName, setDisplayName] = useState(account.get('display_name'));
const [note, setNote] = useState(unescapeHTML(account.get('note')));
const [avatar, setAvatar] = useState(null);
const [header, setHeader] = useState(null);
const [discoverable, setDiscoverable] = useState(account.get('discoverable'));
const [indexable, setIndexable] = useState(account.get('indexable'));
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState();
const avatarFileRef = createRef();
const headerFileRef = createRef();
const dispatch = useDispatch();
const intl = useIntl();
const history = useHistory();
const handleDisplayNameChange = useCallback(e => {
setDisplayName(e.target.value);
}, [setDisplayName]);
const handleNoteChange = useCallback(e => {
setNote(e.target.value);
}, [setNote]);
const handleDiscoverableChange = useCallback(e => {
setDiscoverable(e.target.checked);
}, [setDiscoverable]);
const handleIndexableChange = useCallback(e => {
setIndexable(e.target.checked);
}, [setIndexable]);
const handleAvatarChange = useCallback(e => {
setAvatar(e.target?.files?.[0]);
}, [setAvatar]);
const handleHeaderChange = useCallback(e => {
setHeader(e.target?.files?.[0]);
}, [setHeader]);
const avatarPreview = useMemo(() => avatar ? URL.createObjectURL(avatar) : account.get('avatar'), [avatar, account]);
const headerPreview = useMemo(() => header ? URL.createObjectURL(header) : account.get('header'), [header, account]);
const handleSubmit = useCallback(() => {
setIsSaving(true);
dispatch(updateAccount({
displayName,
note,
avatar,
header,
discoverable,
indexable,
})).then(() => history.push('/start/follows')).catch(err => {
setIsSaving(false);
setErrors(err.response.data.details);
});
}, [dispatch, displayName, note, avatar, header, discoverable, indexable, history]);
return (
<>
<ColumnBackButton />
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.profile.title' defaultMessage='Profile setup' /></h3>
<p><FormattedMessage id='onboarding.profile.lead' defaultMessage='You can always complete this later in the settings, where even more customization options are available.' /></p>
</div>
<div className='simple_form'>
<div className='onboarding__profile'>
<label className={classNames('app-form__header-input', { selected: !!headerPreview, invalid: !!errors?.header })} title={intl.formatMessage(messages.uploadHeader)}>
<input
type='file'
hidden
ref={headerFileRef}
accept='image/*'
onChange={handleHeaderChange}
/>
{headerPreview && <img src={headerPreview} alt='' />}
<Icon icon={headerPreview ? EditIcon : AddPhotoAlternateIcon} />
</label>
<label className={classNames('app-form__avatar-input', { selected: !!avatarPreview, invalid: !!errors?.avatar })} title={intl.formatMessage(messages.uploadAvatar)}>
<input
type='file'
hidden
ref={avatarFileRef}
accept='image/*'
onChange={handleAvatarChange}
/>
{avatarPreview && <img src={avatarPreview} alt='' />}
<Icon icon={avatarPreview ? EditIcon : AddPhotoAlternateIcon} />
</label>
</div>
<div className={classNames('input with_block_label', { field_with_errors: !!errors?.display_name })}>
<label htmlFor='display_name'><FormattedMessage id='onboarding.profile.display_name' defaultMessage='Display name' /></label>
<span className='hint'><FormattedMessage id='onboarding.profile.display_name_hint' defaultMessage='Your full name or your fun name…' /></span>
<div className='label_input'>
<input id='display_name' type='text' value={displayName} onChange={handleDisplayNameChange} maxLength={30} />
</div>
</div>
<div className={classNames('input with_block_label', { field_with_errors: !!errors?.note })}>
<label htmlFor='note'><FormattedMessage id='onboarding.profile.note' defaultMessage='Bio' /></label>
<span className='hint'><FormattedMessage id='onboarding.profile.note_hint' defaultMessage='You can @mention other people or #hashtags…' /></span>
<div className='label_input'>
<textarea id='note' value={note} onChange={handleNoteChange} maxLength={500} />
</div>
</div>
</div>
<label className='report-dialog-modal__toggle'>
<Toggle checked={discoverable} onChange={handleDiscoverableChange} />
<FormattedMessage id='onboarding.profile.discoverable' defaultMessage='Feature profile and posts in discovery algorithms' />
</label>
<label className='report-dialog-modal__toggle'>
<Toggle checked={indexable} onChange={handleIndexableChange} />
<FormattedMessage id='onboarding.profile.indexable' defaultMessage='Include public posts in search results' />
</label>
<div className='onboarding__footer'>
<Button block onClick={handleSubmit} disabled={isSaving}>{isSaving ? <LoadingIndicator /> : <FormattedMessage id='onboarding.profile.save_and_continue' defaultMessage='Save and continue' />}</Button>
</div>
</div>
</>
);
};

@ -1,31 +1,25 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { ReactComponent as ArrowRightAltIcon } from '@material-symbols/svg-600/outlined/arrow_right_alt.svg';
import { ReactComponent as ContentCopyIcon } from '@material-symbols/svg-600/outlined/content_copy.svg';
import SwipeableViews from 'react-swipeable-views';
import Column from 'mastodon/components/column';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { Icon } from 'mastodon/components/icon';
import { me, domain } from 'mastodon/initial_state';
import { useAppSelector } from 'mastodon/store';
const messages = defineMessages({
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
});
const mapStateToProps = state => ({
account: state.getIn(['accounts', me]),
});
class CopyPasteText extends PureComponent {
static propTypes = {
@ -141,59 +135,47 @@ class TipCarousel extends PureComponent {
}
class Share extends PureComponent {
export const Share = () => {
const account = useAppSelector(state => state.getIn(['accounts', me]));
const intl = useIntl();
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
static propTypes = {
onBack: PropTypes.func,
account: ImmutablePropTypes.record,
intl: PropTypes.object,
};
return (
<>
<ColumnBackButton />
render () {
const { onBack, account, intl } = this.props;
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
<p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
</div>
const url = (new URL(`/@${account.get('username')}`, document.baseURI)).href;
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
return (
<Column>
<ColumnBackButton onClick={onBack} />
<div className='scrollable privacy-policy'>
<div className='column-title'>
<h3><FormattedMessage id='onboarding.share.title' defaultMessage='Share your profile' /></h3>
<p><FormattedMessage id='onboarding.share.lead' defaultMessage='Let people know how they can find you on Mastodon!' /></p>
</div>
<CopyPasteText value={intl.formatMessage(messages.shareableMessage, { username: `@${account.get('username')}@${domain}`, url })} />
<TipCarousel>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
</TipCarousel>
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
<div className='onboarding__links'>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
<div className='onboarding__footer'>
<button className='link-button' onClick={onBack}><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></button>
</div>
</div>
</Column>
);
}
<TipCarousel>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.verification' defaultMessage='<strong>Did you know?</strong> You can verify your account by putting a link to your Mastodon profile on your own website and adding the website to your profile. No fees or documents necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.migration' defaultMessage='<strong>Did you know?</strong> If you feel like {domain} is not a great server choice for you in the future, you can move to another Mastodon server without losing your followers. You can even host your own server!' values={{ domain, strong: chunks => <strong>{chunks}</strong> }} /></p></div>
<div><p className='onboarding__lead'><FormattedMessage id='onboarding.tips.2fa' defaultMessage='<strong>Did you know?</strong> You can secure your account by setting up two-factor authentication in your account settings. It works with any TOTP app of your choice, no phone number necessary!' values={{ strong: chunks => <strong>{chunks}</strong> }} /></p></div>
</TipCarousel>
}
<p className='onboarding__lead'><FormattedMessage id='onboarding.share.next_steps' defaultMessage='Possible next steps:' /></p>
<div className='onboarding__links'>
<Link to='/home' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_home' defaultMessage='Take me to my home feed' />
<Icon icon={ArrowRightAltIcon} />
</Link>
export default connect(mapStateToProps)(injectIntl(Share));
<Link to='/explore' className='onboarding__link'>
<FormattedMessage id='onboarding.actions.go_to_explore' defaultMessage='Take me to trending' />
<Icon icon={ArrowRightAltIcon} />
</Link>
</div>
<div className='onboarding__footer'>
<Link className='link-button' to='/start'><FormattedMessage id='onboarding.action.back' defaultMessage='Take me back' /></Link>
</div>
</div>
</>
);
};

@ -166,7 +166,7 @@ class Footer extends ImmutablePureComponent {
onClose();
}
history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
};
render () {

@ -210,7 +210,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/bookmarks' component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
<WrappedRoute path='/start' exact component={Onboarding} content={children} />
<WrappedRoute path='/start' component={Onboarding} content={children} />
<WrappedRoute path='/directory' component={Directory} content={children} />
<WrappedRoute path={['/explore', '/search']} component={Explore} content={children} />
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />

@ -222,6 +222,7 @@
"emoji_button.search_results": "Výsledky hledání",
"emoji_button.symbols": "Symboly",
"emoji_button.travel": "Cestování a místa",
"empty_column.account_hides_collections": "Tento uživatel se rozhodl nezveřejňovat tuto informaci",
"empty_column.account_suspended": "Účet je pozastaven",
"empty_column.account_timeline": "Nejsou tu žádné příspěvky!",
"empty_column.account_unavailable": "Profil není dostupný",

@ -390,7 +390,7 @@
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"load_pending": "{count, plural, one {# new item} other {# new items}}",
"loading_indicator.label": "Loading...",
"loading_indicator.label": "Loading",
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
"mute_modal.duration": "Duration",
@ -479,6 +479,17 @@
"onboarding.follows.empty": "Unfortunately, no results can be shown right now. You can try using search or browsing the explore page to find people to follow, or try again later.",
"onboarding.follows.lead": "Your home feed is the primary way to experience Mastodon. The more people you follow, the more active and interesting it will be. To get you started, here are some suggestions:",
"onboarding.follows.title": "Personalize your home feed",
"onboarding.profile.discoverable": "Feature profile and posts in discovery algorithms",
"onboarding.profile.display_name": "Display name",
"onboarding.profile.display_name_hint": "Your full name or your fun name…",
"onboarding.profile.indexable": "Include public posts in search results",
"onboarding.profile.lead": "You can always complete this later in the settings, where even more customization options are available.",
"onboarding.profile.note": "Bio",
"onboarding.profile.note_hint": "You can @mention other people or #hashtags…",
"onboarding.profile.save_and_continue": "Save and continue",
"onboarding.profile.title": "Profile setup",
"onboarding.profile.upload_avatar": "Upload profile picture",
"onboarding.profile.upload_header": "Upload profile header",
"onboarding.share.lead": "Let people know how they can find you on Mastodon!",
"onboarding.share.message": "I'm {username} on #Mastodon! Come follow me at {url}",
"onboarding.share.next_steps": "Possible next steps:",

@ -389,7 +389,7 @@
"lists.replies_policy.title": "Erakutsi erantzunak:",
"lists.search": "Bilatu jarraitzen dituzun pertsonen artean",
"lists.subheading": "Zure zerrendak",
"load_pending": "{count, plural, one {eleentuberri #} other {# elementu berri}}",
"load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
"loading_indicator.label": "Kargatzen...",
"media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
"moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",

@ -252,7 +252,7 @@
"explore.search_results": "Risultati della ricerca",
"explore.suggested_follows": "Persone",
"explore.title": "Esplora",
"explore.trending_links": "Novità",
"explore.trending_links": "Notizie",
"explore.trending_statuses": "Post",
"explore.trending_tags": "Hashtag",
"filter_modal.added.context_mismatch_explanation": "La categoria di questo filtro non si applica al contesto in cui hai acceduto a questo post. Se desideri che il post sia filtrato anche in questo contesto, dovrai modificare il filtro.",

@ -1,7 +1,7 @@
{
"about.blocks": "Prižiūrimi serveriai",
"about.contact": "Kontaktuoti:",
"about.disclaimer": "Mastodon nemokama atvirojo šaltinio programa ir Mastodon gGmbH prekės ženklas.",
"about.disclaimer": "Mastodon nemokama atvirojo kodo programa ir Mastodon gGmbH prekės ženklas.",
"about.domain_blocks.no_reason_available": "Priežastis nežinoma",
"about.domain_blocks.preamble": "Mastodon paprastai leidžia peržiūrėti turinį ir bendrauti su naudotojais iš bet kurio kito fediverse esančio serverio. Šios yra išimtys, kurios buvo padarytos šiame konkrečiame serveryje.",
"about.domain_blocks.silenced.explanation": "Paprastai nematysi profilių ir turinio iš šio serverio, nebent jį aiškiai ieškosi arba pasirinksi jį sekdamas (-a).",
@ -33,28 +33,46 @@
"account.followers.empty": "Šio naudotojo dar niekas neseka.",
"account.followers_counter": "{count, plural, one {{counter} sekėjas (-a)} few {{counter} sekėjai} many {{counter} sekėjo} other {{counter} sekėjų}}",
"account.following": "Seka",
"account.follows.empty": "Šis naudotojas (-a) dar nieko neseka.",
"account.follows.empty": "Šis (-i) naudotojas (-a) dar nieko neseka.",
"account.follows_you": "Seka tave",
"account.go_to_profile": "Eiti į profilį",
"account.in_memoriam": "Atminimui.",
"account.joined_short": "Prisijungė",
"account.languages": "Keisti prenumeruojamas kalbas",
"account.link_verified_on": "Šios nuorodos nuosavybė buvo patikrinta {date}",
"account.locked_info": "Šios paskyros privatumo būsena nustatyta kaip užrakinta. Savininkas (-ė) rankiniu būdu peržiūri, kas gali sekti.",
"account.media": "Medija",
"account.mention": "Paminėti @{name}",
"account.moved_to": "{name} nurodė, kad dabar jų nauja paskyra yra:",
"account.mute": "Užtildyti @{name}",
"account.mute_notifications_short": "Nutildyti pranešimus",
"account.mute_short": "Nutildyti",
"account.muted": "Užtildytas",
"account.posts": "Toots",
"account.posts_with_replies": "Toots and replies",
"account.report": "Pranešti apie @{name}",
"account.requested": "Awaiting approval",
"account.no_bio": "Nėra pateikto aprašymo.",
"account.open_original_page": "Atidaryti originalinį tinklalapį",
"account.posts": "Įrašai",
"account.posts_with_replies": "Įrašai ir atsakymai",
"account.report": "Pranešti @{name}",
"account.requested": "Laukiama patvirtinimo. Spausk, kad atšaukti sekimo užklausą.",
"account.requested_follow": "{name} paprašė tave sekti",
"account.share": "Bendrinti @{name} profilį",
"account.statuses_counter": "{count, plural, one {{counter} Toot} other {{counter} Toots}}",
"account.unblock_domain": "Unhide {domain}",
"account.unblock_short": "Atblokuoti",
"account.unfollow": "Nebesekti",
"account.unmute": "Atitildyti @{name}",
"account.unmute_notifications_short": "Atitildyti pranešimus",
"account.unmute_short": "Atitildyti",
"account_note.placeholder": "Click to add a note",
"account_note.placeholder": "Spausk norėdamas (-a) pridėti pastabą",
"admin.dashboard.retention.average": "Vidurkis",
"admin.dashboard.retention.cohort": "Registravimo mėnuo",
"admin.dashboard.retention.cohort_size": "Nauji naudotojai",
"admin.impact_report.instance_accounts": "Paskyrų profiliai, kuriuos tai ištrintų",
"admin.impact_report.instance_followers": "Sekėjai, kuriuos prarastų mūsų naudotojai",
"admin.impact_report.instance_follows": "Sekėjai, kuriuos prarastų jų naudotojai",
"admin.impact_report.title": "Poveikio apibendrinimas",
"alert.rate_limited.message": "Pabandyk vėliau po {retry_time, time, medium}.",
"alert.rate_limited.title": "Spartos ribojimas",
"alert.unexpected.message": "Įvyko netikėta klaida.",
"alert.unexpected.title": "Ups!",
"announcement.announcement": "Skelbimas",
@ -65,6 +83,14 @@
"bundle_column_error.copy_stacktrace": "Kopijuoti klaidos ataskaitą",
"bundle_column_error.error.body": "Užklausos puslapio nepavyko atvaizduoti. Tai gali būti dėl mūsų kodo klaidos arba naršyklės suderinamumo problemos.",
"bundle_column_error.error.title": "O, ne!",
"bundle_column_error.network.body": "Bandant užkrauti šį puslapį įvyko klaida. Tai galėjo atsitikti dėl laikinos tavo interneto ryšio arba šio serverio problemos.",
"bundle_column_error.network.title": "Tinklo klaida",
"bundle_column_error.retry": "Bandyti dar kartą",
"bundle_column_error.return": "Grįžti į pradžią",
"bundle_column_error.routing.body": "Prašyto puslapio nepavyko rasti. Ar esi tikras (-a), kad adreso juostoje nurodytas URL adresas yra teisingas?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Uždaryti",
"closed_registrations_modal.find_another_server": "Rasti kitą serverį",
"column.domain_blocks": "Hidden domains",
"column.lists": "Sąrašai",
"column.mutes": "Užtildyti vartotojai",
@ -81,18 +107,32 @@
"compose.published.body": "Įrašas paskelbtas.",
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.",
"compose_form.placeholder": "What is on your mind?",
"compose_form.placeholder": "Kas tavo mintyse?",
"compose_form.poll.add_option": "Pridėti pasirinkimą",
"compose_form.poll.duration": "Apklausos trukmė",
"compose_form.poll.option_placeholder": "Pasirinkimas {number}",
"compose_form.poll.remove_option": "Pašalinti šį pasirinkimą",
"compose_form.poll.switch_to_multiple": "Keisti apklausą, kad būtų galima pasirinkti kelis pasirinkimus",
"compose_form.publish_form": "Publish",
"compose_form.sensitive.hide": "{count, plural, one {Žymėti mediją kaip jautrią} few {Žymėti medijas kaip jautrias} many {Žymėti medijos kaip jautrios} other {Žymėti medijų kaip jautrių}}",
"compose_form.sensitive.marked": "{count, plural, one {Medija pažymėta kaip jautri} few {Medijos pažymėtos kaip jautrios} many {Medijos pažymėta kaip jautrios} other {Medijų pažymėtos kaip jautrios}}",
"compose_form.sensitive.unmarked": "{count, plural, one {Medija nepažymėta kaip jautri} few {Medijos nepažymėtos kaip jautrios} many {Medijos nepažymėta kaip jautri} other {Medijų nepažymėta kaip jautrios}}",
"compose_form.spoiler.marked": "Text is hidden behind warning",
"compose_form.spoiler.unmarked": "Text is not hidden",
"compose_form.spoiler.unmarked": "Pridėti turinio įspėjimą",
"compose_form.spoiler_placeholder": "Rašyk savo įspėjimą čia",
"confirmation_modal.cancel": "Atšaukti",
"confirmations.block.block_and_report": "Blokuoti ir pranešti",
"confirmations.block.confirm": "Blokuoti",
"confirmations.block.message": "Ar tikrai nori užblokuoti {name}?",
"confirmations.delete.confirm": "Ištrinti",
"confirmations.delete.message": "Are you sure you want to delete this status?",
"confirmations.discard_edit_media.confirm": "Atmesti",
"confirmations.discard_edit_media.message": "Turi neišsaugotų medijos aprašymo ar peržiūros pakeitimų, vis tiek juos atmesti?",
"confirmations.domain_block.confirm": "Hide entire domain",
"confirmations.logout.confirm": "Atsijungti",
"confirmations.logout.message": "Ar tikrai nori atsijungti?",
"confirmations.mute.confirm": "Nutildyti",
"confirmations.mute.explanation": "Tai paslėps jų įrašus ir įrašus, kuriuose jie menėmi, tačiau jie vis tiek galės matyti tavo įrašus ir sekti.",
"confirmations.reply.confirm": "Atsakyti",
"confirmations.reply.message": "Atsakydamas (-a) dabar perrašysi šiuo metu rašomą žinutę. Ar tikrai nori tęsti?",
"confirmations.unfollow.confirm": "Nebesekti",
@ -219,6 +259,8 @@
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
"upload_form.audio_description": "Describe for people with hearing loss",
"upload_form.description": "Describe for the visually impaired",
"upload_form.description_missing": "Nėra pridėto aprašymo",
"upload_form.edit": "Redaguoti",
"upload_form.video_description": "Describe for people with hearing loss or visual impairment",
"upload_modal.edit_media": "Redaguoti mediją",
"upload_progress.label": "Uploading…"

@ -12,7 +12,7 @@
"about.powered_by": "由 {mastodon} 提供的去中心化社群媒體",
"about.rules": "伺服器規則",
"account.account_note_header": "備註",
"account.add_or_remove_from_list": "列表中新增或移除",
"account.add_or_remove_from_list": "列表中新增或移除",
"account.badges.bot": "機器人",
"account.badges.group": "群組",
"account.block": "封鎖 @{name}",
@ -26,7 +26,7 @@
"account.domain_blocked": "已封鎖網域",
"account.edit_profile": "編輯個人檔案",
"account.enable_notifications": "當 @{name} 嘟文時通知我",
"account.endorse": "個人檔案推薦對方",
"account.endorse": "個人檔案推薦對方",
"account.featured_tags.last_status_at": "上次嘟文於 {date}",
"account.featured_tags.last_status_never": "沒有嘟文",
"account.featured_tags.title": "{name} 的推薦主題標籤",
@ -65,7 +65,7 @@
"account.unblock": "解除封鎖 @{name}",
"account.unblock_domain": "解除封鎖網域 {domain}",
"account.unblock_short": "解除封鎖",
"account.unendorse": "取消個人檔案推薦對方",
"account.unendorse": "取消個人檔案推薦對方",
"account.unfollow": "取消跟隨",
"account.unmute": "解除靜音 @{name}",
"account.unmute_notifications_short": "取消靜音推播通知",
@ -102,7 +102,7 @@
"bundle_modal_error.message": "載入此元件時發生錯誤。",
"bundle_modal_error.retry": "重試",
"closed_registrations.other_server_instructions": "因為 Mastodon 是去中心化的,所以您也能於其他伺服器上建立帳號,並仍然與這個伺服器互動。",
"closed_registrations_modal.description": "目前無法 {domain} 建立新帳號,但也請別忘了,您並不一定需要有 {domain} 伺服器的帳號,也能使用 Mastodon 。",
"closed_registrations_modal.description": "目前無法 {domain} 建立新帳號,但也請別忘了,您並不一定需要有 {domain} 伺服器的帳號,也能使用 Mastodon 。",
"closed_registrations_modal.find_another_server": "尋找另一個伺服器",
"closed_registrations_modal.preamble": "Mastodon 是去中心化的,所以無論您於哪個伺服器新增帳號,都可以與此伺服器上的任何人跟隨及互動。您甚至能自行架一個自己的伺服器!",
"closed_registrations_modal.title": "註冊 Mastodon",
@ -171,7 +171,7 @@
"confirmations.delete_list.confirm": "刪除",
"confirmations.delete_list.message": "您確定要永久刪除此列表嗎?",
"confirmations.discard_edit_media.confirm": "捨棄",
"confirmations.discard_edit_media.message": "您媒體描述或預覽區塊有未儲存的變更。是否要捨棄這些變更?",
"confirmations.discard_edit_media.message": "您媒體描述或預覽區塊有未儲存的變更。是否要捨棄這些變更?",
"confirmations.domain_block.confirm": "封鎖整個網域",
"confirmations.domain_block.message": "您真的非常確定要封鎖整個 {domain} 網域嗎?大部分情況下,封鎖或靜音少數特定的帳號就能滿足需求了。您將不能在任何公開的時間軸及通知中看到來自此網域的內容。您來自該網域的跟隨者也將被移除。",
"confirmations.edit.confirm": "編輯",
@ -205,7 +205,7 @@
"dismissable_banner.explore_statuses": "這些於此伺服器以及去中心化網路中其他伺服器發出的嘟文正在被此伺服器上的人們熱烈討論著。越多不同人轉嘟及最愛排名更高。",
"dismissable_banner.explore_tags": "這些主題標籤正在被此伺服器以及去中心化網路上的人們熱烈討論著。越多不同人所嘟出的主題標籤排名更高。",
"dismissable_banner.public_timeline": "這些是來自 {domain} 使用者們跟隨中帳號所發表之最新公開嘟文。",
"embed.instructions": "要在您的網站嵌入此嘟文,請複製以下程式碼。",
"embed.instructions": "若您欲於您的網站嵌入此嘟文,請複製以下程式碼。",
"embed.preview": "它將顯示成這樣:",
"emoji_button.activity": "活動",
"emoji_button.clear": "清除",
@ -218,7 +218,7 @@
"emoji_button.objects": "物件",
"emoji_button.people": "人物",
"emoji_button.recent": "最常使用",
"emoji_button.search": "搜尋",
"emoji_button.search": "搜尋...",
"emoji_button.search_results": "搜尋結果",
"emoji_button.symbols": "符號",
"emoji_button.travel": "旅遊與地點",
@ -259,7 +259,7 @@
"filter_modal.added.context_mismatch_title": "不符合情境!",
"filter_modal.added.expired_explanation": "此過濾器類別已失效,您需要更新過期日期以套用。",
"filter_modal.added.expired_title": "過期的過濾器!",
"filter_modal.added.review_and_configure": "要檢視進一步設定此過濾器類別,請至 {settings_link}。",
"filter_modal.added.review_and_configure": "要檢視進一步設定此過濾器類別,請至 {settings_link}。",
"filter_modal.added.review_and_configure_title": "過濾器設定",
"filter_modal.added.settings_link": "設定頁面",
"filter_modal.added.short_explanation": "此嘟文已被新增至以下過濾器類別:{title}。",
@ -362,7 +362,7 @@
"keyboard_shortcuts.search": "將游標移至搜尋框",
"keyboard_shortcuts.spoilers": "顯示或隱藏內容警告之嘟文",
"keyboard_shortcuts.start": "開啟「開始使用」欄位",
"keyboard_shortcuts.toggle_hidden": "顯示或隱藏內容警告之後的嘟文",
"keyboard_shortcuts.toggle_hidden": "顯示或隱藏內容警告之後的嘟文",
"keyboard_shortcuts.toggle_sensitivity": "顯示或隱藏媒體",
"keyboard_shortcuts.toot": "發個新嘟文",
"keyboard_shortcuts.unfocus": "跳離文字撰寫區塊或搜尋框",
@ -376,7 +376,7 @@
"limited_account_hint.title": "此個人檔案已被 {domain} 的管理員隱藏。",
"link_preview.author": "由 {name} 提供",
"lists.account.add": "新增至列表",
"lists.account.remove": "列表中移除",
"lists.account.remove": "列表中移除",
"lists.delete": "刪除列表",
"lists.edit": "編輯列表",
"lists.edit.submit": "變更標題",
@ -469,7 +469,7 @@
"notifications.permission_denied_alert": "由於之前瀏覽器權限被拒絕,無法啟用桌面通知",
"notifications.permission_required": "由於尚未授予所需的權限,因此無法使用桌面通知。",
"notifications_permission_banner.enable": "啟用桌面通知",
"notifications_permission_banner.how_to_control": "啟用桌面通知以在 Mastodon 沒有開啟的時候接收通知。在已經啟用桌面通知的時候,您可以透過上面的 {icon} 按鈕準確的控制哪些類型的互動會產生桌面通知。",
"notifications_permission_banner.how_to_control": "啟用桌面通知以於 Mastodon 沒有開啟的時候接收通知。啟用桌面通知後,您可以透過上面的 {icon} 按鈕準確的控制哪些類型的互動會產生桌面通知。",
"notifications_permission_banner.title": "不要錯過任何東西!",
"onboarding.action.back": "返回",
"onboarding.actions.back": "返回",
@ -490,7 +490,7 @@
"onboarding.steps.follow_people.title": "客製化您的首頁時間軸",
"onboarding.steps.publish_status.body": "向新世界打聲招呼吧。",
"onboarding.steps.publish_status.title": "撰寫您第一則嘟文",
"onboarding.steps.setup_profile.body": "若您完整填寫個人檔案,其他人比較願意您互動。",
"onboarding.steps.setup_profile.body": "若您完整填寫個人檔案,其他人比較願意您互動。",
"onboarding.steps.setup_profile.title": "客製化您的個人檔案",
"onboarding.steps.share_profile.body": "讓您的朋友們知道如何於 Mastodon 找到您!",
"onboarding.steps.share_profile.title": "分享您的 Mastodon 個人檔案",
@ -614,10 +614,10 @@
"sign_in_banner.create_account": "新增帳號",
"sign_in_banner.sign_in": "登入",
"sign_in_banner.sso_redirect": "登入或註冊",
"sign_in_banner.text": "登入以跟隨個人檔案和主題標籤,或收藏、分享和回覆嘟文。您也可以使用您的帳號在其他伺服器上進行互動。",
"sign_in_banner.text": "登入以跟隨個人檔案與主題標籤,或收藏、分享及回覆嘟文。您也可以使用您的帳號於其他伺服器進行互動。",
"status.admin_account": "開啟 @{name} 的管理介面",
"status.admin_domain": "開啟 {domain} 的管理介面",
"status.admin_status": "管理介面開啟此嘟文",
"status.admin_status": "管理介面開啟此嘟文",
"status.block": "封鎖 @{name}",
"status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉嘟",
@ -672,8 +672,8 @@
"status.translated_from_with": "透過 {provider} 翻譯 {lang}",
"status.uncached_media_warning": "無法預覽",
"status.unmute_conversation": "解除此對話的靜音",
"status.unpin": "個人檔案頁面取消釘選",
"subscribed_languages.lead": "僅選定語言的嘟文才會出現在您的首頁上,並在變更後列出時間軸。選取「無」以接收所有語言的嘟文。",
"status.unpin": "個人檔案頁面取消釘選",
"subscribed_languages.lead": "僅選定語言的嘟文才會出現於您的首頁上,並於變更後列出時間軸。選取「無」以接收所有語言的嘟文。",
"subscribed_languages.save": "儲存變更",
"subscribed_languages.target": "變更 {target} 的訂閱語言",
"tabs_bar.home": "首頁",
@ -696,7 +696,7 @@
"upload_area.title": "拖放來上傳",
"upload_button.label": "上傳圖片、影片、或者音樂檔案",
"upload_error.limit": "已達到檔案上傳限制。",
"upload_error.poll": "不允許在投票中上傳檔案。",
"upload_error.poll": "不允許於投票時上傳檔案。",
"upload_form.audio_description": "為聽障人士增加文字說明",
"upload_form.description": "為視障人士增加文字說明",
"upload_form.description_missing": "沒有任何描述",
@ -706,7 +706,7 @@
"upload_form.video_description": "為聽障或視障人士增加文字說明",
"upload_modal.analyzing_picture": "正在分析圖片…",
"upload_modal.apply": "套用",
"upload_modal.applying": "正在套用⋯⋯",
"upload_modal.applying": "正在套用...",
"upload_modal.choose_image": "選擇圖片",
"upload_modal.description_placeholder": "我能吞下玻璃而不傷身體",
"upload_modal.detect_text": "從圖片中偵測文字",

@ -67,6 +67,7 @@ export const accountDefaultValues: AccountShape = {
bot: false,
created_at: '',
discoverable: false,
indexable: false,
display_name: '',
display_name_html: '',
emojis: List<CustomEmoji>(),

@ -2552,7 +2552,7 @@ $ui-header-height: 55px;
.column-title {
text-align: center;
padding-bottom: 40px;
padding-bottom: 32px;
h3 {
font-size: 24px;
@ -2743,58 +2743,6 @@ $ui-header-height: 55px;
}
}
.onboarding__progress-indicator {
display: flex;
align-items: center;
margin-bottom: 30px;
position: sticky;
background: $ui-base-color;
@media screen and (width >= 600) {
padding: 0 40px;
}
&__line {
height: 4px;
flex: 1 1 auto;
background: lighten($ui-base-color, 4%);
}
&__step {
flex: 0 0 auto;
width: 30px;
height: 30px;
background: lighten($ui-base-color, 4%);
border-radius: 50%;
color: $primary-text-color;
display: flex;
align-items: center;
justify-content: center;
svg {
width: 15px;
height: auto;
}
&.active {
background: $valid-value-color;
}
}
&__step.active,
&__line.active {
background: $valid-value-color;
background-image: linear-gradient(
90deg,
$valid-value-color,
lighten($valid-value-color, 8%),
$valid-value-color
);
background-size: 200px 100%;
animation: skeleton 1.2s ease-in-out infinite;
}
}
.follow-recommendations {
background: darken($ui-base-color, 4%);
border-radius: 8px;
@ -2871,6 +2819,28 @@ $ui-header-height: 55px;
}
}
.onboarding__profile {
position: relative;
margin-bottom: 40px + 20px;
.app-form__avatar-input {
border: 2px solid $ui-base-color;
position: absolute;
inset-inline-start: -2px;
bottom: -40px;
z-index: 2;
}
.app-form__header-input {
margin: 0 -20px;
border-radius: 0;
img {
border-radius: 0;
}
}
}
.compose-form__highlightable {
display: flex;
flex-direction: column;
@ -3145,6 +3115,7 @@ $ui-header-height: 55px;
cursor: pointer;
background-color: transparent;
border: 0;
border-radius: 10px;
padding: 0;
user-select: none;
-webkit-tap-highlight-color: rgba($base-overlay-background, 0);
@ -3169,81 +3140,41 @@ $ui-header-height: 55px;
}
.react-toggle-track {
width: 50px;
height: 24px;
width: 32px;
height: 20px;
padding: 0;
border-radius: 30px;
background-color: $ui-base-color;
transition: background-color 0.2s ease;
border-radius: 10px;
background-color: #626982;
}
.react-toggle:is(:hover, :focus-within):not(.react-toggle--disabled)
.react-toggle-track {
background-color: darken($ui-base-color, 10%);
.react-toggle--focus {
outline: $ui-button-focus-outline;
}
.react-toggle--checked .react-toggle-track {
background-color: darken($ui-highlight-color, 2%);
}
.react-toggle--checked:is(:hover, :focus-within):not(.react-toggle--disabled)
.react-toggle-track {
background-color: $ui-highlight-color;
}
.react-toggle-track-check {
position: absolute;
width: 14px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
inset-inline-start: 8px;
opacity: 0;
transition: opacity 0.25s ease;
}
.react-toggle--checked .react-toggle-track-check {
opacity: 1;
transition: opacity 0.25s ease;
}
.react-toggle-track-check,
.react-toggle-track-x {
position: absolute;
width: 10px;
height: 10px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
line-height: 0;
inset-inline-end: 10px;
opacity: 1;
transition: opacity 0.25s ease;
}
.react-toggle--checked .react-toggle-track-x {
opacity: 0;
display: none;
}
.react-toggle-thumb {
position: absolute;
top: 1px;
inset-inline-start: 1px;
width: 22px;
height: 22px;
border: 1px solid $ui-base-color;
top: 2px;
inset-inline-start: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: darken($simple-background-color, 2%);
background-color: $primary-text-color;
box-sizing: border-box;
transition: all 0.25s ease;
transition-property: border-color, left;
}
.react-toggle--checked .react-toggle-thumb {
inset-inline-start: 27px;
inset-inline-start: 32px - 16px - 2px;
border-color: $ui-highlight-color;
}
@ -4066,6 +3997,17 @@ a.status-card {
justify-content: center;
}
.button .loading-indicator {
position: static;
transform: none;
.circular-progress {
color: $primary-text-color;
width: 22px;
height: 22px;
}
}
.circular-progress {
color: lighten($ui-base-color, 26%);
animation: 1.4s linear 0s infinite normal none running simple-rotate;
@ -5799,12 +5741,14 @@ a.status-card {
&__toggle {
display: flex;
align-items: center;
margin-bottom: 10px;
margin-bottom: 16px;
gap: 8px;
& > span {
font-size: 17px;
display: block;
font-size: 14px;
font-weight: 500;
margin-inline-start: 10px;
line-height: 20px;
}
}

@ -36,7 +36,7 @@ code {
}
.input {
margin-bottom: 15px;
margin-bottom: 16px;
overflow: hidden;
&.hidden {
@ -266,12 +266,13 @@ code {
font-size: 14px;
color: $primary-text-color;
display: block;
font-weight: 500;
padding-top: 5px;
font-weight: 600;
line-height: 20px;
}
.hint {
margin-bottom: 15px;
line-height: 16px;
margin-bottom: 12px;
}
ul {
@ -427,7 +428,8 @@ code {
input[type='datetime-local'],
textarea {
box-sizing: border-box;
font-size: 16px;
font-size: 14px;
line-height: 20px;
color: $primary-text-color;
display: block;
width: 100%;
@ -435,9 +437,9 @@ code {
font-family: inherit;
resize: vertical;
background: darken($ui-base-color, 10%);
border: 1px solid darken($ui-base-color, 14%);
border-radius: 4px;
padding: 10px;
border: 1px solid darken($ui-base-color, 10%);
border-radius: 8px;
padding: 10px 16px;
&::placeholder {
color: lighten($darker-text-color, 4%);
@ -451,14 +453,13 @@ code {
border-color: $valid-value-color;
}
&:hover {
border-color: darken($ui-base-color, 20%);
}
&:active,
&:focus {
border-color: $highlight-text-color;
background: darken($ui-base-color, 8%);
}
@media screen and (width <= 600px) {
font-size: 16px;
}
}
@ -524,12 +525,11 @@ code {
border-radius: 4px;
background: $ui-button-background-color;
color: $ui-button-color;
font-size: 18px;
line-height: inherit;
font-size: 15px;
line-height: 22px;
height: auto;
padding: 10px;
padding: 7px 18px;
text-decoration: none;
text-transform: uppercase;
text-align: center;
box-sizing: border-box;
cursor: pointer;
@ -1220,3 +1220,74 @@ code {
background: $highlight-text-color;
}
}
.app-form {
& > * {
margin-bottom: 16px;
}
&__avatar-input,
&__header-input {
display: block;
border-radius: 8px;
background: var(--dropdown-background-color);
position: relative;
cursor: pointer;
img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
z-index: 0;
}
.icon {
position: absolute;
inset-inline-start: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: $darker-text-color;
z-index: 3;
}
&.selected .icon {
color: $primary-text-color;
transform: none;
inset-inline-start: auto;
inset-inline-end: 8px;
top: auto;
bottom: 8px;
}
&.invalid img {
outline: 1px solid $error-value-color;
outline-offset: -1px;
}
&.invalid::before {
display: block;
content: '';
width: 100%;
height: 100%;
position: absolute;
background: rgba($error-value-color, 0.25);
z-index: 2;
border-radius: 8px;
}
&:hover {
background-color: var(--dropdown-border-color);
}
}
&__avatar-input {
width: 80px;
height: 80px;
}
&__header-input {
aspect-ratio: 580/193;
}
}

@ -6,7 +6,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
# Please update `app/javascript/mastodon/api_types/accounts.ts` when making changes to the attributes
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :group, :created_at,
attributes :id, :username, :acct, :display_name, :locked, :bot, :discoverable, :indexable, :group, :created_at,
:note, :url, :uri, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count, :last_status_at, :hide_collections
@ -116,6 +116,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
object.suspended? ? false : object.discoverable
end
def indexable
object.suspended? ? false : object.indexable
end
def moved_to_account
object.suspended? ? nil : AccountDecorator.new(object.moved_to_account)
end

@ -22,7 +22,7 @@
= t('admin.reports.resolved')
- else
= t('admin.reports.unresolved')
- unless report.target_account.local?
- if report.account.local? && !report.target_account.local?
.report-header__details__item
.report-header__details__item__header
%strong= t('admin.reports.forwarded')

@ -7,6 +7,9 @@
- else
= link_to t('admin.reports.mark_as_unresolved'), reopen_admin_report_path(@report), method: :post, class: 'button'
- unless @report.account.local? || @report.target_account.local?
.flash-message= t('admin.reports.forwarded_replies_explanation')
.report-header
= render 'admin/reports/header_card', report: @report
= render 'admin/reports/header_details', report: @report

@ -1,6 +1,19 @@
---
lt:
activerecord:
attributes:
poll:
expires_at: Galutinė data
options: Pasirinkimai
user:
agreement: Paslaugos sutartis
email: El. laiško adresas
locale: Lokali
password: Slaptažodis
user/account:
username: Naudotojo vardas
user/invite_request:
text: Priežastis
errors:
models:
account:
@ -12,3 +25,35 @@ lt:
attributes:
url:
invalid: nėra tinkamas URL adresas.
doorkeeper/application:
attributes:
website:
invalid: nėra tinkamas URL adresas.
import:
attributes:
data:
malformed: yra netaisyklinga.
status:
attributes:
reblog:
taken: įrašas jau egzistuoja.
user:
attributes:
email:
blocked: naudoja neleidžiamą el. laiško paslaugų teikėją.
unreachable: neatrodo, kad egzistuoja.
role_id:
elevated: negali būti didesnis nei tavo dabartinis vaidmuo.
user_role:
attributes:
permissions_as_keys:
dangerous: apima leidimus, kurie nėra saugūs pagrindiniam vaidmeniui.
elevated: negali apimti leidimų, kurių neturi tavo dabartinis vaidmuo.
own_role: negali būti pakeistas tavo dabartinis vaidmuo.
position:
elevated: negali būti didesnis nei tavo dabartinis vaidmuo.
own_role: negali būti pakeistas tavo dabartinis vaidmuo.
webhook:
attributes:
events:
invalid_permissions: negali įtraukti įvykių, į kuriuos neturi teisių.

@ -1052,7 +1052,7 @@ be:
localization:
body: Mastodon перакладаецца добраахвотнікамі.
guide_link: https://be.crowdin.com/project/mastodon/be
guide_link_text: Кожны можа зрабіць унёсак.
guide_link_text: Кожны і кожная можа зрабіць унёсак.
sensitive_content: Далікатны змест
application_mailer:
notification_preferences: Змяніць налады эл. пошты
@ -1575,7 +1575,7 @@ be:
duration_too_short: гэта занадта хутка
expired: Апытанне ўжо скончана
invalid_choice: Абраны варыянт апытання не існуе
over_character_limit: не можа быць даўжэй за %{max} сімвалаў кожны
over_character_limit: кожны не можа быць даўжэй за %{max} сімвалаў
self_vote: Вы не можаце галасаваць ва ўласных апытаннях
too_few_options: павінна быць болей за адзін варыянт
too_many_options: колькасць варыянтаў не можа перавышаць %{max}
@ -1764,7 +1764,7 @@ be:
public: Публічны
public_long: Усе могуць бачыць
unlisted: Не ў спісе
unlisted_long: Кожны можа ўбачыць гэты допіс, але ён не паказваецца ў публічных стужках
unlisted_long: Усе могуць пабачыць гэты допіс, але ён не паказваецца ў публічных стужках
statuses_cleanup:
enabled: Аўтаматычна выдаляць старыя допісы
enabled_hint: Аўтаматычна выдаляць вашыя допісы, калі яны дасягаюць вызначанага тэрміну, акрамя наступных выпадкаў

@ -1074,6 +1074,10 @@ cs:
hint_html: Ještě jedna věc! Musíme potvrdit, že jste člověk (to proto, abychom drželi stranou spam!). Vyřešte CAPTCHA níže a klikněte na "Pokračovat".
title: Bezpečnostní kontrola
confirmations:
awaiting_review_title: Vaše registrace se ověřuje
clicking_this_link: kliknutím na tento odkaz
registration_complete: Vaše registrace na %{domain} je hotová!
welcome_title: Vítám uživatele %{name}!
wrong_email_hint: Pokud není e-mail správný, můžete si ho změnit v nastavení účtu.
delete_account: Odstranit účet
delete_account_html: Chcete-li odstranit svůj účet, <a href="%{path}">pokračujte zde</a>. Budete požádáni o potvrzení.
@ -1770,6 +1774,9 @@ cs:
default: "%d. %b %Y, %H:%M"
month: "%b %Y"
time: "%H:%M"
translation:
errors:
too_many_requests: Na překladatelskou službu bylo zasláno v poslední době příliš mnoho požadavků.
two_factor_authentication:
add: Přidat
disable: Vypnout 2FA

@ -9,7 +9,7 @@ zh-TW:
already_authenticated: 您已登入。
inactive: 您的帳號尚未啟用。
invalid: 無效的 %{authentication_keys} 或密碼。
last_attempt: 帳號鎖定前,您還有最後一次嘗試機會。
last_attempt: 帳號鎖定前,您還有最後一次嘗試機會。
locked: 已鎖定您的帳號。
not_found_in_database: 無效的 %{authentication_keys} 或密碼。
pending: 您的帳號仍在審核中。
@ -20,8 +20,8 @@ zh-TW:
confirmation_instructions:
action: 驗證電子郵件地址
action_with_app: 確認並返回 %{app}
explanation: 您已經在 %{host} 上以此電子郵件地址建立了一支帳號。您距離啟用它只剩一點之遙了。若這不是您,請忽略此信件。
explanation_when_pending: 您使用此電子郵件地址申請了 %{host} 的邀請。當您確認電子郵件地址後我們將審核您的申請。您可以在登入後變更詳細資訊或刪除您的帳號,但直到您的帳號被核准之前,您無法操作大部分的功能。若您的申請遭拒絕,您的資料將被移除而不必做後續動作。如果這不是您本人,請忽略此郵件。
explanation: 您已 %{host} 上以此電子郵件地址建立了一支帳號。您距離啟用它只剩一點之遙了。若這不是您,請忽略此信件。
explanation_when_pending: 您使用此電子郵件地址申請了 %{host} 的邀請。當您確認電子郵件地址後我們將審核您的申請。您能於登入後變更詳細資訊或刪除您的帳號,但直到您的帳號被核准之前,您無法操作大部分的功能。若您的申請遭拒絕,您的資料將被移除而不必做後續動作。如果這不是您本人,請忽略此郵件。
extra_html: 同時也請看看<a href="%{terms_path}">伺服器規則</a>與<a href="%{policy_path}">服務條款</a>。
subject: Mastodon%{instance} 確認說明
title: 驗證電子郵件地址
@ -37,7 +37,7 @@ zh-TW:
title: 密碼已變更
reconfirmation_instructions:
explanation: 請確認新的電子郵件地址以變更。
extra: 若此次變更不是由您起始的請忽略此信件。Mastodon 帳號的電子郵件地址您存取上面的連結前不會變更。
extra: 若此次變更不是由您起始的請忽略此信件。Mastodon 帳號的電子郵件地址您存取上面的連結前不會變更。
subject: Mastodon確認 %{instance} 的電子郵件地址
title: 驗證電子郵件地址
reset_password_instructions:
@ -106,7 +106,7 @@ zh-TW:
errors:
messages:
already_confirmed: 已經確認,請嘗試登入
confirmation_period_expired: 需要在 %{period} 內完成驗證。請重新申請
confirmation_period_expired: 您需要於 %{period} 內完成驗證。請重新申請
expired: 已經過期,請重新請求
not_found: 找不到
not_locked: 並未鎖定

@ -72,7 +72,7 @@ zh-TW:
revoke: 您確定嗎?
index:
authorized_at: 於 %{date} 授權
description_html: 這些應用程式能透過 API 存取您的帳號。若有您不認得之應用程式,或應用程式行為異常,您可以於此註銷其存取權限。
description_html: 這些應用程式能透過 API 存取您的帳號。若有您不認得之應用程式,或應用程式行為異常,您於此註銷其存取權限。
last_used_at: 上次使用時間 %{date}
never_used: 從未使用
scopes: 權限

@ -611,6 +611,7 @@ en:
created_at: Reported
delete_and_resolve: Delete posts
forwarded: Forwarded
forwarded_replies_explanation: This report is from a remote user and about remote content. It has been forwarded to you because the reported content is in reply to one of your users.
forwarded_to: Forwarded to %{domain}
mark_as_resolved: Mark as resolved
mark_as_sensitive: Mark as sensitive

@ -419,8 +419,8 @@ pl:
hint: Blokada domen nie zabroni tworzenia wpisów kont w bazie danych, ale pozwoli na automatyczną moderację kont do nich należących.
severity:
desc_html: "<strong>Wyciszenie</strong> uczyni wpisy użytkowników z tej domeny widoczne tylko dla osób, które go obserwują. <strong>Zawieszenie</strong> spowoduje usunięcie całej zawartości dodanej przez użytkownika. Użyj <strong>Żadne</strong>, jeżeli chcesz jedynie odrzucać zawartość multimedialną."
noop: Nic nie rób
silence: Limit
noop: Żadne
silence: Wycisz
suspend: Zawieś
title: Nowa blokada domen
no_domain_block_selected: Nie zmieniono żadnych bloków domen, gdyż żadna nie została wybrana

@ -15,25 +15,25 @@ zh-TW:
account_migration:
acct: 指定要移動至的帳號的「使用者名稱@網域名稱」
account_warning_preset:
text: 您可使用嘟文語法,例如網址、「#」標籤提及功能
title: 可選。不會向收件者顯示
text: 您可使用嘟文語法,例如網址、「#」標籤提及功能
title: 可選。不會向收件者顯示
admin_account_action:
include_statuses: 使用者可看到導致檢舉或警告的嘟文
send_email_notification: 使用者將收到帳號發生之事情的解釋
text_html: 選用。您能使用嘟文語法。您可 <a href="%{path}">新增警告預設</a> 來節省時間
text_html: 可選的。您能使用嘟文語法。您可 <a href="%{path}">新增警告預設</a> 來節省時間
type_html: 設定要使用 <strong>%{acct}</strong> 做的事
types:
disable: 禁止該使用者使用他們的帳號,但是不刪除或隱藏他們的內容。
none: 使用這個寄送警告給該使用者,而不進行其他動作。
sensitive: 強制標記此使用者所有多媒體附加檔案為敏感內容。
silence: 禁止該使用者發公開嘟文,從無跟隨他們的帳號中隱藏嘟文通知。關閉所有對此帳號之檢舉報告。
silence: 禁止該使用者發公開嘟文,從無跟隨他們的帳號中隱藏嘟文通知。關閉所有對此帳號之檢舉報告。
suspend: 禁止所有對該帳號任何互動,並且刪除其內容。三十天內可以撤銷此動作。關閉所有對此帳號之檢舉報告。
warning_preset_id: 選用。您仍可在預設的結尾新增自訂文字
warning_preset_id: 可選的。您仍可於預設的結尾新增自訂文字
announcement:
all_day: 當選取時,僅顯示出時間範圍中的日期部分
ends_at: 可選的公告會於該時間點自動取消發布
ends_at: 可選的公告會於該時間點自動取消發布
scheduled_at: 空白則立即發布公告
starts_at: 可選的,讓公告在特定時間範圍內顯示
starts_at: 可選的。讓公告於特定時間範圍內顯示
text: 您可以使用嘟文語法,但請小心別讓公告太鴨霸而佔據使用者的整個版面。
appeal:
text: 您只能對警示提出一次申訴
@ -44,12 +44,12 @@ zh-TW:
context: 此過濾器應套用於以下一項或多項情境
current_password: 因安全因素,請輸入目前帳號的密碼
current_username: 請輸入目前帳號的使用者名稱以確認
digest: 在您長時間未登入且在未登入期間收到私訊時傳送
digest: 於您長時間未登入且於未登入期間收到私訊時傳送
email: 您將收到一封確認電子郵件
header: 支援 PNG、GIF 或 JPG 圖片格式,檔案最大為 %{size},會等比例縮減至 %{dimensions} 像素
inbox_url: 從您想要使用的中繼首頁複製網址
irreversible: 已過濾的嘟文將會不可逆地消失,即便之後移除過濾器也一樣
locale: 使用者介面、電子郵件推播通知的語言
locale: 使用者介面、電子郵件推播通知的語言
password: 使用至少 8 個字元
phrase: 無論是嘟文的本文或是內容警告都會被過濾
scopes: 允許讓應用程式存取的 API。 若您選擇最高階範圍,則無須選擇個別項目。
@ -62,12 +62,12 @@ zh-TW:
setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節將變得模糊
setting_use_pending_items: 關閉自動捲動更新,時間軸僅於點擊後更新
username: 您可以使用字幕、數字與底線
whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只在符合整個單字的時候才會套用
whole_word: 如果關鍵字或詞組僅有字母與數字,則其將只於符合整個單字時才會套用
domain_allow:
domain: 此網域將能夠攫取本站資料,而自該網域發出的資料也會於本站處理留存。
domain: 此網域將能夠攫取本站資料,而自該網域發出的資料也會於本站處理留存。
email_domain_block:
domain: 這可以是顯示電子郵件中的網域名稱,或是其使用的 MX 紀錄。其將於註冊時檢查。
with_dns_records: Mastodon 會嘗試解析所網域的 DNS 記錄,解析結果一致者將一併封鎖
domain: 這可以是顯示電子郵件中的網域名稱,或是其使用的 MX 紀錄。其將於註冊時檢查。
with_dns_records: Mastodon 會嘗試解析所提供之網域的 DNS 記錄,解析結果一致者將一併封鎖
featured_tag:
name: 這些是您最近使用的一些主題標籤:
filters:
@ -97,7 +97,7 @@ zh-TW:
theme: 未登入之訪客或新使用者所見之佈景主題。
thumbnail: 大約 2:1 圖片會顯示於您伺服器資訊之旁。
timeline_preview: 未登入之訪客能夠瀏覽此伺服器上最新的公開嘟文。
trendable_by_default: 跳過手動審核熱門內容。仍能登上熱門趨勢後移除個別內容。
trendable_by_default: 跳過手動審核熱門內容。仍能登上熱門趨勢後移除個別內容。
trends: 熱門趨勢將顯示於您伺服器上正在吸引大量注意力的嘟文、主題標籤、或者新聞。
trends_as_landing_page: 顯示熱門趨勢內容給未登入使用者及訪客而不是關於此伺服器之描述。需要啟用熱門趨勢。
form_challenge:
@ -107,9 +107,9 @@ zh-TW:
invite_request:
text: 這會協助我們審核您的申請
ip_block:
comment: 可選的但請記得您為何添加這項規則。
comment: 可選的但請記得您為何添加這項規則。
expires_in: IP 位址是經常共用或轉手的有限資源,不建議無限期地封鎖特定 IP 位址。
ip: 請輸入 IPv4 或 IPv6 位址,亦可以用 CIDR 語法以封鎖整個 IP 區段。小心不要將自己一併封鎖掉囉!
ip: 請輸入 IPv4 或 IPv6 位址,亦可以用 CIDR 語法以封鎖整個 IP 區段。小心不要將自己一併封鎖掉囉!
severities:
no_access: 封鎖對所有資源存取
sign_up_block: 無法註冊新帳號
@ -129,11 +129,11 @@ zh-TW:
chosen_languages: 當選取時,只有選取語言之嘟文會於公開時間軸中顯示
role: 角色控制使用者有哪些權限
user_role:
color: 整個使用者介面中用於角色的顏色,十六進位格式的 RGB
color: 整個使用者介面中用於角色的顏色,十六進位格式的 RGB
highlighted: 這會讓角色公開可見
name: 角色的公開名稱,如果角色設定為顯示為徽章
permissions_as_keys: 有此角色的使用者將有權存取……
position: 某些情況下,衝突的解決方式由更高階的角色決定。某些動作只能由優先程度較低的角色執行
permissions_as_keys: 有此角色的使用者將有權存取...
position: 某些情況下,衝突的解決方式由更高階的角色決定。某些動作只能由優先程度較低的角色執行
webhook:
events: 請選擇要傳送的事件
template: 使用變數代換組合您自己的 JSON payload。留白以使用預設 JSON 。
@ -155,7 +155,7 @@ zh-TW:
text: 預設文字
title: 標題
admin_account_action:
include_statuses: 在電子郵件中加入檢舉的嘟文
include_statuses: 於電子郵件中加入檢舉之嘟文內容
send_email_notification: 透過電子郵件通知使用者
text: 自訂警告
type: 動作
@ -230,7 +230,7 @@ zh-TW:
username_or_email: 使用者名稱或電子郵件地址
whole_word: 整個詞彙
email_domain_block:
with_dns_records: 包括網域的 MX 記錄 IP 位址
with_dns_records: 包括網域的 MX 記錄 IP 位址
featured_tag:
name: "「#」主題標籤"
filters:
@ -287,7 +287,7 @@ zh-TW:
favourite: 當有使用者將您的嘟文加入最愛時,傳送電子郵件通知
follow: 當有使用者跟隨您時,傳送電子郵件通知
follow_request: 當有使用者請求跟隨您時,傳送電子郵件通知
mention: 當有使用者嘟文提及您時,傳送電子郵件通知
mention: 當有使用者嘟文提及您時,傳送電子郵件通知
pending_account: 有新的帳號需要審核
reblog: 當有使用者轉嘟您的嘟文時,傳送電子郵件通知
report: 新回報已遞交
@ -304,16 +304,16 @@ zh-TW:
indexable: 於搜尋引擎中包含個人檔案頁面
show_application: 顯示您發嘟文之應用程式
tag:
listable: 允許此主題標籤搜尋及個人檔案目錄中顯示
listable: 允許此主題標籤搜尋及個人檔案目錄中顯示
name: 主題標籤
trendable: 允許此主題標籤熱門趨勢下顯示
trendable: 允許此主題標籤熱門趨勢下顯示
usable: 允許嘟文使用此主題標籤
user:
role: 角色
time_zone: 時區
user_role:
color: 識別顏色
highlighted: 在使用者個人檔案上將角色顯示為徽章
highlighted: 於使用者個人檔案中顯示角色徽章
name: 名稱
permissions_as_keys: 權限
position: 優先權

@ -1343,6 +1343,7 @@ vi:
'86400': 1 ngày
expires_in_prompt: Không giới hạn
generate: Tạo lời mời
invalid: Lời mời không hợp lệ
invited_by: 'Bạn đã được mời bởi:'
max_uses:
other: "%{count} lần dùng"

@ -1,10 +1,10 @@
---
zh-TW:
about:
about_mastodon_html: Mastodon (長毛象)是一個<em>自由、開放原始碼</em>的社群網站。它是一個分散式的服務,避免您的通訊被單一商業機構壟斷操控。請您選擇一家您信任的 Mastodon 站點,在上面建立帳號,然後您就可以和任一 Mastodon 站點上的使用者互通,享受無縫的<em>社群網路</em>交流。
about_mastodon_html: Mastodon (長毛象)是一個<em>自由、開放原始碼</em>的社群網站。它是一個分散式的服務,避免您的通訊被單一商業機構壟斷操控。請您選擇一家您信任的 Mastodon 站點,於其建立帳號,您就能與任一 Mastodon 站點上的使用者互通,享受無縫的<em>社群網路</em>交流。
contact_missing: 未設定
contact_unavailable: 未公開
hosted_on: 在 %{domain} 運作的 Mastodon 站點
hosted_on: 於 %{domain} 託管之 Mastodon 站點
title: 關於本站
accounts:
follow: 跟隨
@ -13,7 +13,7 @@ zh-TW:
following: 正在跟隨
instance_actor_flash: 此帳號是用來代表此伺服器的虛擬執行者,而非個別使用者。它的用途為維繫聯邦宇宙,且不應被停權。
last_active: 上次活躍時間
link_verified_on: 此連結的所有權已在 %{date} 檢查過
link_verified_on: 此連結之所有權已於 %{date} 檢查過
nothing_here: 暫時沒有內容可供顯示!
pin_errors:
following: 您只能推薦您正在跟隨的使用者。
@ -115,7 +115,7 @@ zh-TW:
reject: 拒絕
rejected_msg: 已成功婉拒 %{username} 的新帳號申請
remote_suspension_irreversible: 此帳號之資料已被不可逆地刪除。
remote_suspension_reversible_hint_html: 這個帳號已於此伺服器被停權,所有資料將會於 %{date} 被刪除。此之前,遠端伺服器可以完全回復此的帳號。如果您想即時刪除這個帳號的資料,您可以在下面進行操作。
remote_suspension_reversible_hint_html: 這個帳號已於此伺服器被停權,所有資料將會於 %{date} 被刪除。此之前,遠端伺服器可以完全回復此的帳號。如果您想即時刪除這個帳號的資料,您能於下面進行操作。
remove_avatar: 取消大頭貼
remove_header: 移除開頭
removed_avatar_msg: 已成功刪除 %{username} 的大頭貼
@ -149,7 +149,7 @@ zh-TW:
suspend: 停權
suspended: 已停權
suspension_irreversible: 已永久刪除此帳號的資料。您可以取消這個帳號的停權狀態,但無法還原已刪除的資料。
suspension_reversible_hint_html: 這個帳號已被暫停,所有數據將於 %{date} 被刪除。在此之前,您可以完全回復您的帳號。如果您想即時刪除這個帳號的數據,您可以在下面進行操作。
suspension_reversible_hint_html: 這個帳號已被暫停,所有數據將於 %{date} 被刪除。於此之前,您可以完全回復您的帳號。如果您想即時刪除這個帳號的數據,您能於下面進行操作。
title: 帳號
unblock_email: 解除封鎖電子郵件地址
unblocked_email_msg: 成功解除封鎖 %{username} 的電子郵件地址
@ -333,7 +333,7 @@ zh-TW:
not_permitted: 您無權執行此操作
overwrite: 覆蓋
shortcode: 短代碼
shortcode_hint: 至少 2 個字元,只能使用字母、數字下劃線
shortcode_hint: 至少 2 個字元,只能使用字母、數字下劃線
title: 自訂表情符號
uncategorized: 未分類
unlist: 不公開
@ -370,10 +370,10 @@ zh-TW:
domain_allows:
add_new: 將網域加入聯邦宇宙白名單
created_msg: 網域已成功加入聯邦宇宙白名單
destroyed_msg: 網域已成功聯邦宇宙白名單移除
destroyed_msg: 網域已成功聯邦宇宙白名單移除
export: 匯出
import: 匯入
undo: 聯邦宇宙白名單移除
undo: 聯邦宇宙白名單移除
domain_blocks:
add_new: 新增網域黑名單
confirm_suspension:
@ -397,7 +397,7 @@ zh-TW:
create: 新增封鎖
hint: 站點封鎖動作並不會阻止帳號紀錄被新增至資料庫,但會自動回溯性地對那些帳號套用特定管理設定。
severity:
desc_html: "「<strong>靜音</strong>」令該站點下使用者的嘟文,設定為只對跟隨者顯示,沒有跟隨的人會看不到。「<strong>停權</strong>」會刪除將該站點下使用者的嘟文、媒體檔案個人檔案。「<strong>無</strong>」則會拒絕接收來自該站點的媒體檔案。"
desc_html: "「<strong>靜音</strong>」令該站點下使用者的嘟文,設定為只對跟隨者顯示,沒有跟隨的人會看不到。「<strong>停權</strong>」會刪除將該站點下使用者的嘟文、媒體檔案個人檔案。「<strong>無</strong>」則會拒絕接收來自該站點的媒體檔案。"
noop:
silence: 靜音
suspend: 停權
@ -451,7 +451,7 @@ zh-TW:
title: 匯入網域黑名單
no_file: 尚未選擇檔案
follow_recommendations:
description_html: "<strong>跟隨建議幫助新使用者們快速找到有趣的內容</strong>。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動高本地跟隨者數量帳號而每日重新更新。"
description_html: "<strong>跟隨建議幫助新使用者們快速找到有趣的內容</strong>。當使用者沒有與其他帳號有足夠多的互動以建立個人化跟隨建議時,這些帳號將會被推薦。這些帳號將基於某選定語言之高互動高本地跟隨者數量帳號而每日重新更新。"
language: 對於語言
status: 狀態
suppress: 取消跟隨建議
@ -461,7 +461,7 @@ zh-TW:
instances:
availability:
description_html:
other: 在<strong>%{count}天</strong>向某個網域遞送失敗,除非收到某個網域的遞送<em>表單</em>,否則不會繼續嘗試遞送。
other: 於 <strong>%{count} 天</strong>向某個網域遞送失敗,除非收到某個網域的遞送<em>表單</em>,否則不會繼續嘗試遞送。
failure_threshold_reached: 錯誤門檻於 %{date}。
failures_recorded:
other: 錯誤嘗試於 %{count} 天。
@ -590,7 +590,7 @@ zh-TW:
by_target_domain: 檢舉帳號之網域
cancel: 取消
category: 分類
category_description_html: 此帳號及/或被檢舉內容之原因會被引用在檢舉帳號通知中
category_description_html: 此帳號及/或被檢舉內容之原因將被引用於檢舉帳號通知中
comment:
none:
comment_description_html: 提供更多資訊,%{name} 寫道:
@ -611,7 +611,7 @@ zh-TW:
delete: 刪除
placeholder: 記錄已執行的動作,或其他相關的更新...
title: 備註
notes_description_html: 檢視及留下些給其他管理員未來的自己的備註
notes_description_html: 檢視及留下些給其他管理員未來的自己的備註
processed_msg: '檢舉報告 #%{id} 已被成功處理'
quick_actions_description_html: 採取一個快速行動,或者下捲以檢視檢舉內容:
remote_user_placeholder: 來自 %{instance} 之遠端使用者
@ -624,7 +624,7 @@ zh-TW:
skip_to_actions: 跳過行動
status: 嘟文
statuses: 被檢舉的內容
statuses_description_html: 侵犯性違規內容會被引用在檢舉帳號通知中
statuses_description_html: 侵犯性違規內容將被引用於檢舉帳號通知中
summary:
action_preambles:
delete_html: 您將要 <strong>移除</strong> 某些 <strong>@%{acct}</strong> 之嘟文。此將會:
@ -677,7 +677,7 @@ zh-TW:
manage_announcements: 管理公告
manage_announcements_description: 允許使用者管理伺服器上的公告
manage_appeals: 管理解封申訴系統
manage_appeals_description: 允許使用者審閱針對站務動作申訴
manage_appeals_description: 允許使用者審閱針對站務動作申訴
manage_blocks: 管理封鎖
manage_blocks_description: 允許使用者封鎖電子郵件提供者與 IP 位置
manage_custom_emojis: 管理自訂表情符號
@ -741,7 +741,7 @@ zh-TW:
title: 預設將使用者排除於搜尋引擎索引
discovery:
follow_recommendations: 跟隨建議
preamble: 呈現有趣的內容有助於 Mastodon 上一人不識的新手上路。控制各種不同的分類您伺服器上如何被探索到。
preamble: 呈現有趣的內容有助於 Mastodon 上一人不識的新手上路。控制各種不同的分類您伺服器上如何被探索到。
profile_directory: 個人檔案目錄
public_timelines: 公開時間軸
publish_discovered_servers: 公開已知伺服器列表
@ -989,11 +989,11 @@ zh-TW:
created_msg: 成功建立別名。您可以自舊帳號開始轉移。
deleted_msg: 成功移除別名。您將無法再由舊帳號轉移至目前的帳號。
empty: 您目前沒有任何別名。
hint_html: 如果想由其他帳號轉移至此帳號,您可以於此處新增別名,稍後系統將容許您將跟隨者由舊帳號轉移至此。此項作業是<strong>無害且可復原的</strong>。 <strong>帳號的遷移程序需要舊帳號啟動</strong>。
hint_html: 如果想由其他帳號轉移至此帳號,您於此處新增別名,稍後系統將容許您將跟隨者由舊帳號轉移至此。此項作業是<strong>無害且可復原的</strong>。 <strong>帳號的遷移程序需要舊帳號啟動</strong>。
remove: 取消連結別名
appearance:
advanced_web_interface: 進階網頁介面
advanced_web_interface_hint: 進階網頁介面可讓您設定許多不同的欄位來善用螢幕空間,依需要同時查看許多不同的資訊如:首頁、通知、聯邦宇宙時間軸、任意數量的列表主題標籤。
advanced_web_interface_hint: 進階網頁介面可讓您設定許多不同的欄位來善用螢幕空間,依需要同時查看許多不同的資訊如:首頁、通知、聯邦宇宙時間軸、任意數量的列表主題標籤。
animations_and_accessibility: 動畫與無障礙設定
confirmation_dialogs: 確認對話框
discovery: 探索
@ -1033,13 +1033,13 @@ zh-TW:
redirect_to_app_html: 您應被重新導向至 <strong>%{app_name}</strong> 應用程式。如尚未重新導向,請嘗試 %{clicking_this_link} 或手動回到應用程式。
registration_complete: 您於 %{domain} 之註冊申請已完成!
welcome_title: 歡迎,%{name}
wrong_email_hint: 若電子郵件地址不正確,您可以於帳號設定中更改。
wrong_email_hint: 若電子郵件地址不正確,您於帳號設定中更改。
delete_account: 刪除帳號
delete_account_html: 如果您欲刪除您的帳號,請<a href="%{path}">點擊這裡繼續</a>。您需要再三確認您的操作。
description:
prefix_invited_by_user: "@%{name} 邀請您加入這個 Mastodon 伺服器!"
prefix_sign_up: 馬上註冊 Mastodon 帳號吧!
suffix: 有了帳號,就可以任何 Mastodon 伺服器跟隨任何人、發發廢嘟,並且與任何 Mastodon 伺服器的使用者交流,以及更多!
suffix: 有了帳號,就可以任何 Mastodon 伺服器跟隨任何人、發發廢嘟,並且與任何 Mastodon 伺服器的使用者交流,以及更多!
didnt_get_confirmation: 沒有收到確認連結嗎?
dont_have_your_security_key: 找不到您的安全金鑰?
forgot_password: 忘記密碼?
@ -1085,7 +1085,7 @@ zh-TW:
preamble_html: 請使用您於 <strong>%{domain}</strong> 的帳號密碼登入。若您的帳號託管於其他伺服器,您將無法於此登入。
title: 登入 %{domain}
sign_up:
manual_review: "%{domain} 上的註冊由我們的管理員進行人工審核。為協助我們處理您的註冊,請寫一些關於您自己的資訊以及您想要在 %{domain} 上註冊帳號的原因。"
manual_review: "%{domain} 上的註冊由我們的管理員進行人工審核。為協助我們處理您的註冊,請寫一些關於您自己的資訊以及您欲於 %{domain} 上註冊帳號之原因。"
preamble: 於此 Mastodon 伺服器擁有帳號的話,您將能跟隨聯邦宇宙網路中任何一份子,無論他們的帳號託管於何處。
title: 讓我們一起設定 %{domain} 吧!
status:
@ -1100,7 +1100,7 @@ zh-TW:
use_security_key: 使用安全金鑰
challenge:
confirm: 繼續
hint_html: "<strong>温馨小提醒:</strong> 我們接下來一小時內不會再要求您輸入密碼。"
hint_html: "<strong>温馨小提醒:</strong> 我們接下來一小時內不會再要求您輸入密碼。"
invalid_password: 密碼錯誤
prompt: 輸入密碼以繼續
crypto:
@ -1134,8 +1134,8 @@ zh-TW:
warning:
before: 在進行下一步驟之前,請詳細閱讀以下説明:
caches: 已被其他節點快取的內容可能會殘留其中
data_removal: 您的嘟文和其他資料將會被永久刪除
email_change_html: 可以在不刪除帳號的情況下<a href="%{path}">變更您的電子郵件地址</a>
data_removal: 您的嘟文與其他資料將被永久刪除
email_change_html: 能於不刪除帳號的情況下<a href="%{path}">變更您的電子郵件地址</a>
email_contact_html: 如果您仍然沒有收到郵件,請寄信至 <a href="mailto:%{email}">%{email}</a> 以獲得協助
email_reconfirmation_html: 如果您沒有收到確認郵件,可以<a href="%{path}">請求再次發送</a>
irreversible: 您將無法復原或重新啟用您的帳號
@ -1176,7 +1176,7 @@ zh-TW:
invalid_domain: 並非一個有效網域
edit_profile:
basic_information: 基本資訊
hint_html: "<strong>自訂人們可以於您個人檔案及嘟文內容。</strong>當您完成填寫個人檔案以及設定大頭貼後,其他人們比較願意跟隨您並與您互動。"
hint_html: "<strong>自訂人們能於您個人檔案及嘟文旁所見之內容。</strong>當您完成填寫個人檔案以及設定大頭貼後,其他人們比較願意跟隨您並與您互動。"
other: 其他
errors:
'400': 您所送出的請求無效或格式不正確。
@ -1194,13 +1194,13 @@ zh-TW:
'503': 此頁面因伺服器暫時發生錯誤而無法提供。
noscript_html: 使用 Mastodon 網頁版應用需要啟用 JavaScript。您也可以選擇適用於您的平台的 <a href="%{apps_path}">Mastodon 應用</a>。
existing_username_validator:
not_found: 無法在本站找到這個名稱的使用者
not_found: 無法於本伺服器找到此使用者帳號
not_found_multiple: 揣嘸 %{usernames}
exports:
archive_takeout:
date: 日期
download: 下載檔案
hint_html: 您可以下載包含您的<strong>文章和媒體</strong>的檔案。資料以 ActivityPub 格式儲存,可用於相容的軟體。每次允許存檔的間隔至少 7 天。
hint_html: 您可以下載包含您的<strong>嘟文與媒體</strong>的檔案。資料以 ActivityPub 格式儲存,可用於相容之軟體。每次允許存檔的間隔至少 7 天。
in_progress: 正在準備您的存檔...
request: 下載存檔
size: 大小
@ -1304,7 +1304,7 @@ zh-TW:
following_html: 您將要 <strong>跟隨</strong> 自 <strong>%{filename}</strong> 中之 <strong>%{total_items} 個帳號</strong>。
lists_html: 您將自 <strong>%{filename}</strong> 新增 <strong>%{total_items} 個帳號</strong>至您的<strong>列表</strong>。若不存在列表用以新增帳號,則會建立新列表。
muting_html: 您將要 <strong>靜音</strong> 自 <strong>%{filename}</strong> 中之 <strong>%{total_items} 個帳號</strong>。
preface: 您能於此匯入您其他伺服器所匯出的資料檔,包括跟隨中的使用者、封鎖的使用者名單等。
preface: 您能於此匯入您其他伺服器所匯出的資料檔,包括跟隨中的使用者、封鎖的使用者名單等。
recent_imports: 最近匯入的
states:
finished: 已完成
@ -1414,12 +1414,12 @@ zh-TW:
warning:
backreference_required: 新的帳號必須先設定為反向參照到目前帳號
before: 在進行下一步驟之前,請詳細閱讀以下説明:
cooldown: 轉移帳號後會有一段等待時間,等待時間內您將無法再次轉移
cooldown: 轉移帳號後會有一段等待時間,等待時間內您將無法再次轉移
disabled_account: 之後您的目前帳號將完全無法使用。但您可以存取資料匯出與重新啟用。
followers: 此動作將會將目前帳號的所有跟隨者轉移至新帳號
only_redirect_html: 或者,您也可以<a href="%{path}">僅您的個人檔案中設定重新導向</a>。
only_redirect_html: 或者,您也可以<a href="%{path}">僅您的個人檔案中設定重新導向</a>。
other_data: 其他資料並不會自動轉移
redirect: 您目前的帳號將於個人檔案頁面新增重新導向公告,並會被排除搜尋結果之外
redirect: 您目前的帳號將於個人檔案頁面新增重新導向公告,並會被排除搜尋結果之外
moderation:
title: 站務
move_handler:
@ -1449,8 +1449,8 @@ zh-TW:
title: 新的跟隨請求
mention:
action: 回覆
body: "%{name} 嘟文中提及您:"
subject: "%{name} 嘟文中提及您"
body: "%{name} 嘟文中提及您:"
subject: "%{name} 嘟文中提及您"
title: 新的提及
poll:
subject: 由 %{name} 發起的投票已結束
@ -1510,7 +1510,7 @@ zh-TW:
privacy:
hint_html: "<strong>自訂您希望如何讓您的個人檔案及嘟文被發現。</strong>藉由啟用一系列 Mastodon 功能以幫助您觸及更廣的受眾。煩請花些時間確認您是否欲啟用這些設定。"
privacy: 隱私權
privacy_hint_html: 控制您希望向其他人揭露之內容。人們透過瀏覽其他人的跟隨者與其發嘟之應用程式發現有趣的個人檔案酷炫的 Mastodon 應用程式,但您能選擇將其隱藏。
privacy_hint_html: 控制您希望向其他人揭露之內容。人們透過瀏覽其他人的跟隨者與其發嘟之應用程式發現有趣的個人檔案酷炫的 Mastodon 應用程式,但您能選擇將其隱藏。
reach: 觸及
reach_hint_html: 控制您希望被新使用者探索或跟隨之方式。想讓您的嘟文出現於探索頁面嗎?想讓其他人透過他們的跟隨建議找到您嗎?想自動接受所有新跟隨者嗎?或是想逐一控制跟隨請求嗎?
search: 搜尋
@ -1669,7 +1669,7 @@ zh-TW:
private_long: 只有跟隨您的人能看到
public: 公開
public_long: 所有人都能看到
unlisted: 公開時間軸顯示
unlisted: 公開時間軸顯示
unlisted_long: 所有人都能看到,但不會出現在公開時間軸上
statuses_cleanup:
enabled: 自動刪除舊嘟文
@ -1679,7 +1679,7 @@ zh-TW:
ignore_favs: 忽略最愛數
ignore_reblogs: 忽略轉嘟數
interaction_exceptions: 基於互動的例外規則
interaction_exceptions_explanation: 請注意嘟文是無法保證被刪除的,如果一次處理過後嘟文低於最愛或轉嘟的門檻。
interaction_exceptions_explanation: 請注意嘟文是無法保證被刪除的,如果一次處理過後嘟文低於最愛或轉嘟的門檻。
keep_direct: 保留私訊
keep_direct_hint: 不會刪除任何您的私訊
keep_media: 保留包含多媒體附加檔案之嘟文
@ -1735,7 +1735,7 @@ zh-TW:
enabled: 兩階段認證已啟用
enabled_success: 已成功啟用兩階段認證
generate_recovery_codes: 產生備用驗證碼
lost_recovery_codes: 讓您可以在遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。
lost_recovery_codes: 讓您能於遺失手機時,使用備用驗證碼登入。若您已遺失備用驗證碼,可於此產生一批新的,舊有的備用驗證碼將會失效。
methods: 兩步驟方式
otp: 驗證應用程式
recovery_codes: 備份備用驗證碼
@ -1745,12 +1745,12 @@ zh-TW:
user_mailer:
appeal_approved:
action: 前往您的帳號
explanation: 在 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示的申訴已獲批准。您的帳號再次享有良好的信譽。
subject: 在 %{date} 提出的申訴已獲批准
explanation: 於 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示之申訴已獲批准。您的帳號再次享有良好的信譽。
subject: 於 %{date} 提出之申訴已獲批准
title: 申訴已批准
appeal_rejected:
explanation: 在 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示的申訴已被駁回。
subject: 在 %{date} 提出的申訴已被駁回
explanation: 於 %{appeal_date} 遞交的針對您帳號的 %{strike_date} 警示之申訴已被駁回。
subject: 於 %{date} 提出之申訴已被駁回
title: 申訴被駁回
backup_ready:
explanation: 您要求的 Mastodon 帳號完整備份檔案現已就緒,可供下載!
@ -1772,7 +1772,7 @@ zh-TW:
explanation:
delete_statuses: 您的某些嘟文被發現已違反一項或多項社群準則,隨後已被 %{instance} 的管理員刪除。
disable: 您無法繼續使用您的帳號,但您的個人頁面及其他資料內容保持不變。您可以要求一份您的資料備份,帳號異動設定,或是刪除帳號。
mark_statuses_as_sensitive: 您的部份嘟文已被 %{instance} 的管理員標記為敏感內容。這代表了人們必須在顯示預覽前點擊嘟文中的媒體。您可以在將來嘟文時自己將媒體標記為敏感內容。
mark_statuses_as_sensitive: 您的部份嘟文已被 %{instance} 的管理員標記為敏感內容。這代表了人們必須於顯示預覽前點擊嘟文中的媒體。您能於將來嘟文時自己將媒體標記為敏感內容。
sensitive: 由此刻起,您所有上傳的媒體檔案將被標記為敏感內容,並且隱藏於點擊警告之後。
silence: 您仍然能使用您的帳號,但僅有已跟隨您的人才能見到您於此伺服器之嘟文,您也可能會從各式探索功能中被排除。但其他人仍可手動跟隨您。
suspend: 您將不能使用您的帳號,您的個人檔案頁面及其他資料將不再能被存取。您仍可於約 30 日內資料被完全刪除前要求下載您的資料,但我們仍會保留一部份基本資料,以防止有人規避停權處罰。
@ -1781,9 +1781,9 @@ zh-TW:
subject:
delete_statuses: 您於 %{acct} 之嘟文已被移除
disable: 您的帳號 %{acct} 已被凍結
mark_statuses_as_sensitive: %{acct} 上的嘟文已被標記為敏感內容
mark_statuses_as_sensitive: %{acct} 上的嘟文已被標記為敏感內容
none: 對 %{acct} 的警告
sensitive: 從現在開始,您在 %{acct} 上的嘟文將會被標記為敏感內容
sensitive: 從現在開始,您於 %{acct} 上之嘟文將會被標記為敏感內容
silence: 您的帳號 %{acct} 已被限制
suspend: 您的帳號 %{acct} 已被停權
title:
@ -1796,10 +1796,10 @@ zh-TW:
suspend: 帳號己被停權
welcome:
edit_profile_action: 設定個人檔案
edit_profile_step: 您可以設定您的個人檔案,包括上傳大頭貼、變更顯示名稱等等。您也可以選擇新的跟隨者跟隨前,先對他們進行審核。
edit_profile_step: 您可以設定您的個人檔案,包括上傳大頭貼、變更顯示名稱等等。您也可以選擇新的跟隨者跟隨前,先對他們進行審核。
explanation: 下面是幾個小幫助,希望它們能幫到您
final_action: 開始嘟嘟
final_step: '開始嘟嘟吧!即使您現在沒有跟隨者,其他人仍然能本站時間軸、主題標籤等地方,看到您的公開嘟文。試著用 #introductions 這個主題標籤介紹一下自己吧。'
final_step: '開始嘟嘟吧!即使您現在沒有跟隨者,其他人仍然能本站時間軸、主題標籤等地方,看到您的公開嘟文。試著用 #introductions 這個主題標籤介紹一下自己吧。'
full_handle: 您的完整帳號名稱
full_handle_hint: 您需要將這告訴您的朋友們,這樣他們就能從另一個伺服器向您發送訊息或跟隨您。
subject: 歡迎來到 Mastodon

@ -32,7 +32,7 @@ Rails.application.routes.draw do
/favourites
/bookmarks
/pinned
/start
/start/(*any)
/directory
/explore/(*any)
/search

@ -37,37 +37,49 @@ describe Api::Web::PushSubscriptionsController do
}
end
describe 'POST #create' do
it 'saves push subscriptions' do
sign_in(user)
before do
sign_in(user)
stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
end
describe 'POST #create' do
it 'saves push subscriptions' do
post :create, format: :json, params: create_payload
expect(response).to have_http_status(200)
user.reload
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
expect(created_push_subscription).to have_attributes(
endpoint: eq(create_payload[:subscription][:endpoint]),
key_p256dh: eq(create_payload[:subscription][:keys][:p256dh]),
key_auth: eq(create_payload[:subscription][:keys][:auth])
)
expect(user.session_activations.first.web_push_subscription).to eq(created_push_subscription)
end
context 'with a user who has a session with a prior subscription' do
let!(:prior_subscription) { Fabricate(:web_push_subscription, session_activation: user.session_activations.last) }
it 'destroys prior subscription when creating new one' do
post :create, format: :json, params: create_payload
expect(push_subscription['endpoint']).to eq(create_payload[:subscription][:endpoint])
expect(push_subscription['key_p256dh']).to eq(create_payload[:subscription][:keys][:p256dh])
expect(push_subscription['key_auth']).to eq(create_payload[:subscription][:keys][:auth])
expect(response).to have_http_status(200)
expect { prior_subscription.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'with initial data' do
it 'saves alert settings' do
sign_in(user)
stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
post :create, format: :json, params: create_payload.merge(alerts_payload)
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
expect(response).to have_http_status(200)
expect(push_subscription.data['policy']).to eq 'all'
expect(created_push_subscription.data['policy']).to eq 'all'
%w(follow follow_request favourite reblog mention poll status).each do |type|
expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
end
end
end
@ -75,23 +87,23 @@ describe Api::Web::PushSubscriptionsController do
describe 'PUT #update' do
it 'changes alert settings' do
sign_in(user)
stub_request(:post, create_payload[:subscription][:endpoint]).to_return(status: 200)
post :create, format: :json, params: create_payload
alerts_payload[:id] = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint]).id
expect(response).to have_http_status(200)
put :update, format: :json, params: alerts_payload
alerts_payload[:id] = created_push_subscription.id
push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
put :update, format: :json, params: alerts_payload
expect(push_subscription.data['policy']).to eq 'all'
expect(created_push_subscription.data['policy']).to eq 'all'
%w(follow follow_request favourite reblog mention poll status).each do |type|
expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
expect(created_push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
end
end
end
def created_push_subscription
Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
end
end

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'The /.well-known/change-password request' do
it 'redirects to the change password page' do
get '/.well-known/change-password'
expect(response).to redirect_to '/auth/edit'
end
end

@ -2,9 +2,14 @@
if ENV['DISABLE_SIMPLECOV'] != 'true'
require 'simplecov'
require 'simplecov-lcov'
SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
if ENV['CI']
require 'simplecov-lcov'
SimpleCov::Formatter::LcovFormatter.config.report_with_single_file = true
SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
else
SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter
end
SimpleCov.start 'rails' do
enable_coverage :branch
enable_coverage_for_eval

@ -1,7 +1,7 @@
{
"name": "@mastodon/streaming",
"license": "AGPL-3.0-or-later",
"packageManager": "yarn@4.0.1",
"packageManager": "yarn@4.0.2",
"engines": {
"node": ">=18"
},

@ -4700,13 +4700,13 @@ __metadata:
linkType: hard
"axios@npm:^1.4.0":
version: 1.6.1
resolution: "axios@npm:1.6.1"
version: 1.6.2
resolution: "axios@npm:1.6.2"
dependencies:
follow-redirects: "npm:^1.15.0"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: ca2c6f56659a7f19e4a99082f549fe151952f6fd8aa72ed148559ab2d6a32ce37cd5dc72ce6d4d3cd91f0c1e2617c7c95c20077e5e244a79f319a6c0ce41204f
checksum: 9b77e030e85e4f9cbcba7bb52fbff67d6ce906c92d213e0bd932346a50140faf83733bf786f55bd58301bd92f9973885c7b87d6348023e10f7eaf286d0791a1d
languageName: node
linkType: hard

Loading…
Cancel
Save