From ac83c9c18c9c75a963f77cd45dad13117975e586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 31 Jul 2022 11:57:31 +0200 Subject: [PATCH 01/48] Support mutes duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/accounts.ts | 22 ++++++++-- app/soapbox/actions/mutes.ts | 11 +++++ app/soapbox/components/account.tsx | 8 ++++ .../features/ui/components/mute_modal.tsx | 41 +++++++++++++++++-- app/soapbox/normalizers/account.ts | 1 + app/soapbox/reducers/__tests__/mutes.test.ts | 5 +++ app/soapbox/reducers/mutes.ts | 4 ++ app/soapbox/utils/features.ts | 9 ++++ 8 files changed, 95 insertions(+), 6 deletions(-) diff --git a/app/soapbox/actions/accounts.ts b/app/soapbox/actions/accounts.ts index 0b7f4e0cc..de6f78884 100644 --- a/app/soapbox/actions/accounts.ts +++ b/app/soapbox/actions/accounts.ts @@ -1,5 +1,5 @@ import { isLoggedIn } from 'soapbox/utils/auth'; -import { getFeatures } from 'soapbox/utils/features'; +import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; import api, { getLinks } from '../api'; @@ -354,14 +354,30 @@ const unblockAccountFail = (error: AxiosError) => ({ error, }); -const muteAccount = (id: string, notifications?: boolean) => +const muteAccount = (id: string, notifications?: boolean, duration = 0) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; dispatch(muteAccountRequest(id)); + const params: Record = { + notifications, + }; + + if (duration) { + const state = getState(); + const instance = state.instance; + const v = parseVersion(instance.version); + + if (v.software === PLEROMA) { + params.expires_in = duration; + } else { + params.duration = duration; + } + } + return api(getState) - .post(`/api/v1/accounts/${id}/mute`, { notifications }) + .post(`/api/v1/accounts/${id}/mute`, params) .then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers return dispatch(muteAccountSuccess(response.data, getState().statuses)); diff --git a/app/soapbox/actions/mutes.ts b/app/soapbox/actions/mutes.ts index 050a513f0..bb684b0d6 100644 --- a/app/soapbox/actions/mutes.ts +++ b/app/soapbox/actions/mutes.ts @@ -21,6 +21,7 @@ const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; const fetchMutes = () => (dispatch: AppDispatch, getState: () => RootState) => { @@ -103,6 +104,14 @@ const toggleHideNotifications = () => dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); }; +const changeMuteDuration = (duration: number) => + (dispatch: AppDispatch) => { + dispatch({ + type: MUTES_CHANGE_DURATION, + duration, + }); + }; + export { MUTES_FETCH_REQUEST, MUTES_FETCH_SUCCESS, @@ -112,6 +121,7 @@ export { MUTES_EXPAND_FAIL, MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, + MUTES_CHANGE_DURATION, fetchMutes, fetchMutesRequest, fetchMutesSuccess, @@ -122,4 +132,5 @@ export { expandMutesFail, initMuteModal, toggleHideNotifications, + changeMuteDuration, }; diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index fc4b290b6..183731297 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -235,6 +235,14 @@ const Account = ({ ) : null} + + {actionType === 'muting' && account.mute_expires_at ? ( + <> + · + + + + ) : null} {withAccountNote && ( diff --git a/app/soapbox/features/ui/components/mute_modal.tsx b/app/soapbox/features/ui/components/mute_modal.tsx index b2b5e25b2..32909c5db 100644 --- a/app/soapbox/features/ui/components/mute_modal.tsx +++ b/app/soapbox/features/ui/components/mute_modal.tsx @@ -4,9 +4,10 @@ import Toggle from 'react-toggle'; import { muteAccount } from 'soapbox/actions/accounts'; import { closeModal } from 'soapbox/actions/modals'; -import { toggleHideNotifications } from 'soapbox/actions/mutes'; +import { toggleHideNotifications, changeMuteDuration } from 'soapbox/actions/mutes'; import { Modal, HStack, Stack, Text } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import DurationSelector from 'soapbox/features/compose/components/polls/duration-selector'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; const getAccount = makeGetAccount(); @@ -16,12 +17,14 @@ const MuteModal = () => { const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!)); const notifications = useAppSelector((state) => state.mutes.new.notifications); + const duration = useAppSelector((state) => state.mutes.new.duration); + const mutesDuration = useFeatures().mutesDuration; if (!account) return null; const handleClick = () => { dispatch(closeModal()); - dispatch(muteAccount(account.id, notifications)); + dispatch(muteAccount(account.id, notifications, duration)); }; const handleCancel = () => { @@ -32,6 +35,12 @@ const MuteModal = () => { dispatch(toggleHideNotifications()); }; + const handleChangeMuteDuration = (expiresIn: number): void => { + dispatch(changeMuteDuration(expiresIn)); + }; + + const toggleAutoExpire = () => handleChangeMuteDuration(duration ? 0 : 2 * 60 * 60 * 24); + return ( { /> + + {mutesDuration && ( + <> + + + {duration !== 0 && ( + + : + + + + )} + + )} ); diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index 1a519b8a8..830b51953 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -42,6 +42,7 @@ export const AccountRecord = ImmutableRecord({ location: '', locked: false, moved: null as EmbeddedEntity, + mute_expires_at: null as Date | null, note: '', pleroma: ImmutableMap(), source: ImmutableMap(), diff --git a/app/soapbox/reducers/__tests__/mutes.test.ts b/app/soapbox/reducers/__tests__/mutes.test.ts index 66a866958..27d38c002 100644 --- a/app/soapbox/reducers/__tests__/mutes.test.ts +++ b/app/soapbox/reducers/__tests__/mutes.test.ts @@ -14,6 +14,7 @@ describe('mutes reducer', () => { isSubmitting: false, accountId: null, notifications: true, + duration: 0, }, }); }); @@ -24,6 +25,7 @@ describe('mutes reducer', () => { isSubmitting: false, accountId: null, notifications: true, + duration: 0, })(), })(); const action = { @@ -35,6 +37,7 @@ describe('mutes reducer', () => { isSubmitting: false, accountId: 'account1', notifications: true, + duration: 0, }, }); }); @@ -45,6 +48,7 @@ describe('mutes reducer', () => { isSubmitting: false, accountId: null, notifications: true, + duration: 0, })(), })(); const action = { @@ -55,6 +59,7 @@ describe('mutes reducer', () => { isSubmitting: false, accountId: null, notifications: false, + duration: 0, }, }); }); diff --git a/app/soapbox/reducers/mutes.ts b/app/soapbox/reducers/mutes.ts index e232d4039..4c0b08c39 100644 --- a/app/soapbox/reducers/mutes.ts +++ b/app/soapbox/reducers/mutes.ts @@ -3,6 +3,7 @@ import { Record as ImmutableRecord } from 'immutable'; import { MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, + MUTES_CHANGE_DURATION, } from '../actions/mutes'; import type { AnyAction } from 'redux'; @@ -11,6 +12,7 @@ const NewMuteRecord = ImmutableRecord({ isSubmitting: false, accountId: null, notifications: true, + duration: 0, }); const ReducerRecord = ImmutableRecord({ @@ -29,6 +31,8 @@ export default function mutes(state: State = ReducerRecord(), action: AnyAction) }); case MUTES_TOGGLE_HIDE_NOTIFICATIONS: return state.updateIn(['new', 'notifications'], (old) => !old); + case MUTES_CHANGE_DURATION: + return state.setIn(['new', 'duration'], action.duration); default: return state; } diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index e3683def3..43c5bea88 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -391,6 +391,15 @@ const getInstanceFeatures = (instance: Instance) => { */ muteStrangers: v.software === PLEROMA, + /** + * Ability to specify how long the account mute should last. + * @see PUT /api/v1/accounts/:id/mute + */ + mutesDuration: any([ + v.software === PLEROMA && gte(v.version, '2.3.0'), + v.software === MASTODON && gte(v.compatVersion, '3.3.0'), + ]), + /** * Add private notes to accounts. * @see POST /api/v1/accounts/:id/note From d7727b727faed8637bf508524c752a5887ea66a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 23 Oct 2022 20:52:45 +0200 Subject: [PATCH 02/48] Make ActionsModal items full width, minor style improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/ui/components/actions_modal.tsx | 4 +-- app/styles/components/modal.scss | 29 +------------------ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/app/soapbox/features/ui/components/actions_modal.tsx b/app/soapbox/features/ui/components/actions_modal.tsx index aaa8a6209..27faaf909 100644 --- a/app/soapbox/features/ui/components/actions_modal.tsx +++ b/app/soapbox/features/ui/components/actions_modal.tsx @@ -37,10 +37,10 @@ const ActionsModal: React.FC = ({ status, actions, onClick, onClo {...compProps} rel='noopener' data-index={i} - className={classNames({ active, destructive })} + className={classNames('w-full', { active, destructive })} data-method={isLogout ? 'delete' : null} > - {icon && } + {icon && }
{text}
{meta}
diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index 4410bdae0..f663c26ff 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -285,7 +285,7 @@ } .actions-modal { - @apply w-full max-h-full max-w-lg mt-auto mb-2 bg-white dark:bg-gray-800; + @apply w-full max-h-full max-w-lg m-auto mb-2 bg-white dark:bg-gray-800; .status { overflow-y: auto; @@ -386,30 +386,3 @@ width: 330px !important; } } - -.column-inline-form { - padding: 7px 15px; - padding-right: 5px; - display: flex; - justify-content: flex-start; - align-items: center; - background: var(--brand-color--faint); - - label { - flex: 1 1 auto; - - input { - width: 100%; - margin-bottom: 6px; - - &:focus { - outline: 0; - } - } - } - - .icon-button { - flex: 0 0 auto; - margin: 0 5px; - } -} From cfedc89e7ef8a477156589e6be8bda7ac440d793 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Oct 2022 12:03:32 -0500 Subject: [PATCH 03/48] Support Docker local development --- Dockerfile.dev | 18 ++++++++++++++++++ compose-dev.yaml | 10 ++++++++++ 2 files changed, 28 insertions(+) create mode 100644 Dockerfile.dev create mode 100644 compose-dev.yaml diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..8d1655db0 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,18 @@ +FROM node:18 + +RUN apt-get update &&\ + apt-get install -y inotify-tools &&\ + # clean up apt + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +ENV NODE_ENV=development + +COPY package.json . +COPY yarn.lock . +RUN yarn + +COPY . . + +ENV DEVSERVER_URL=http://0.0.0.0:3036 +CMD yarn dev \ No newline at end of file diff --git a/compose-dev.yaml b/compose-dev.yaml new file mode 100644 index 000000000..2359ac5ba --- /dev/null +++ b/compose-dev.yaml @@ -0,0 +1,10 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + image: soapbox-dev + ports: + - "3036:3036" + volumes: + - .:/app \ No newline at end of file From 3448022965bea8fae6d3b8b88c9fc47bd62b52fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 27 Oct 2022 19:46:03 +0200 Subject: [PATCH 04/48] Support translation feature on Mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/instance.ts | 20 ++++++- app/soapbox/actions/statuses.ts | 34 +++++++++++ app/soapbox/components/status.tsx | 4 ++ app/soapbox/components/status_content.tsx | 7 ++- app/soapbox/components/translate-button.tsx | 59 +++++++++++++++++++ app/soapbox/components/ui/stack/stack.tsx | 5 +- .../status/components/detailed-status.tsx | 4 ++ app/soapbox/locales/pl.json | 3 + app/soapbox/normalizers/status.ts | 1 + app/soapbox/precheck.ts | 4 +- app/soapbox/reducers/instance.ts | 13 ++++ app/soapbox/reducers/statuses.ts | 8 ++- app/soapbox/utils/features.ts | 12 ++++ 13 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 app/soapbox/components/translate-button.tsx diff --git a/app/soapbox/actions/instance.ts b/app/soapbox/actions/instance.ts index 60a6b2e89..3a45abf49 100644 --- a/app/soapbox/actions/instance.ts +++ b/app/soapbox/actions/instance.ts @@ -1,5 +1,6 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import get from 'lodash/get'; +import { gte } from 'semver'; import KVStore from 'soapbox/storage/kv_store'; import { RootState } from 'soapbox/store'; @@ -37,6 +38,12 @@ const needsNodeinfo = (instance: Record): boolean => { return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']); }; +/** Mastodon exposes features availabiliy under /api/v2/instance since 4.0.0 */ +const supportsInstanceV2 = (instance: Record): boolean => { + const v = parseVersion(get(instance, 'version')); + return v.software === 'Mastodon' && gte(v.compatVersion, '4.0.0'); +}; + export const fetchInstance = createAsyncThunk( 'instance/fetch', async(_arg, { dispatch, getState, rejectWithValue }) => { @@ -45,6 +52,9 @@ export const fetchInstance = createAsyncThunk( if (needsNodeinfo(instance)) { dispatch(fetchNodeinfo()); } + if (supportsInstanceV2(instance)) { + dispatch(fetchInstanceV2()); + } return instance; } catch (e) { return rejectWithValue(e); @@ -64,9 +74,15 @@ export const loadInstance = createAsyncThunk( }, ); -export const fetchNodeinfo = createAsyncThunk( +export const fetchInstanceV2 = createAsyncThunk( 'nodeinfo/fetch', async(_arg, { getState }) => { - return await api(getState).get('/nodeinfo/2.1.json'); + const { data: instance } = await api(getState).get('/api/v2/instance'); + return instance; }, ); + +export const fetchNodeinfo = createAsyncThunk( + 'nodeinfo/fetch', + async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'), +); diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts index b1bced1f0..5ebf1c95c 100644 --- a/app/soapbox/actions/statuses.ts +++ b/app/soapbox/actions/statuses.ts @@ -43,6 +43,11 @@ const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; const STATUS_REVEAL = 'STATUS_REVEAL'; const STATUS_HIDE = 'STATUS_HIDE'; +const STATUS_TRANSLATE_REQUEST = 'STATUS_TRANSLATE_REQUEST'; +const STATUS_TRANSLATE_SUCCESS = 'STATUS_TRANSLATE_SUCCESS'; +const STATUS_TRANSLATE_FAIL = 'STATUS_TRANSLATE_FAIL'; +const STATUS_TRANSLATE_UNDO = 'STATUS_TRANSLATE_UNDO'; + const statusExists = (getState: () => RootState, statusId: string) => { return (getState().statuses.get(statusId) || null) !== null; }; @@ -305,6 +310,29 @@ const toggleStatusHidden = (status: Status) => { } }; +const translateStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { + dispatch({ type: STATUS_TRANSLATE_REQUEST, id }); + + api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => { + dispatch({ + type: STATUS_TRANSLATE_SUCCESS, + id, + translation: response.data, + }); + }).catch(error => { + dispatch({ + type: STATUS_TRANSLATE_FAIL, + id, + error, + }); + }); +}; + +const undoStatusTranslation = (id: string) => ({ + type: STATUS_TRANSLATE_UNDO, + id, +}); + export { STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, @@ -329,6 +357,10 @@ export { STATUS_UNMUTE_FAIL, STATUS_REVEAL, STATUS_HIDE, + STATUS_TRANSLATE_REQUEST, + STATUS_TRANSLATE_SUCCESS, + STATUS_TRANSLATE_FAIL, + STATUS_TRANSLATE_UNDO, createStatus, editStatus, fetchStatus, @@ -345,4 +377,6 @@ export { hideStatus, revealStatus, toggleStatusHidden, + translateStatus, + undoStatusTranslation, }; diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 35827020f..12d74e6f9 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -9,6 +9,7 @@ import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { toggleStatusHidden } from 'soapbox/actions/statuses'; import Icon from 'soapbox/components/icon'; +import TranslateButton from 'soapbox/components/translate-button'; import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; import { useAppDispatch, useSettings } from 'soapbox/hooks'; @@ -385,8 +386,11 @@ const Status: React.FC = (props) => { expanded={!status.hidden} onExpandedToggle={handleExpandedToggle} collapsable + translatable /> + + void, onClick?: () => void, collapsable?: boolean, + translatable?: boolean, } /** Renders the text content of a status */ -const StatusContent: React.FC = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false }) => { +const StatusContent: React.FC = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false, translatable }) => { const history = useHistory(); const [hidden, setHidden] = useState(true); @@ -199,14 +200,14 @@ const StatusContent: React.FC = ({ status, expanded = false, onE }; const parsedHtml = useMemo((): string => { - const { contentHtml: html } = status; + const html = translatable && status.translation ? status.translation.get('content')! : status.contentHtml; if (greentext) { return addGreentext(html); } else { return html; } - }, [status.contentHtml]); + }, [status.contentHtml, status.translation]); if (status.content.length === 0) { return null; diff --git a/app/soapbox/components/translate-button.tsx b/app/soapbox/components/translate-button.tsx new file mode 100644 index 000000000..5c6334fd3 --- /dev/null +++ b/app/soapbox/components/translate-button.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { FormattedMessage, useIntl } from 'react-intl'; + +import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses'; +import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; + +import { Stack } from './ui'; + +import type { Status } from 'soapbox/types/entities'; + +interface ITranslateButton { + status: Status, +} + +const TranslateButton: React.FC = ({ status }) => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const features = useFeatures(); + + const me = useAppSelector((state) => state.me); + + const renderTranslate = /* translationEnabled && */ me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language; + + const handleTranslate: React.MouseEventHandler = (e) => { + e.stopPropagation(); + + if (status.translation) { + dispatch(undoStatusTranslation(status.id)); + } else { + dispatch(translateStatus(status.id)); + } + }; + + if (!features.translations || !renderTranslate) return null; + + if (status.translation) { + const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' }); + const languageName = languageNames.of(status.language!); + const provider = status.translation.get('provider'); + + return ( + + + + + + ); + } + + return ( + + ); +}; + +export default TranslateButton; diff --git a/app/soapbox/components/ui/stack/stack.tsx b/app/soapbox/components/ui/stack/stack.tsx index 660f46e36..bc7728af5 100644 --- a/app/soapbox/components/ui/stack/stack.tsx +++ b/app/soapbox/components/ui/stack/stack.tsx @@ -19,14 +19,17 @@ const justifyContentOptions = { }; const alignItemsOptions = { + top: 'items-start', + bottom: 'items-end', center: 'items-center', + start: 'items-start', }; interface IStack extends React.HTMLAttributes { /** Size of the gap between elements. */ space?: keyof typeof spaces /** Horizontal alignment of children. */ - alignItems?: 'center' + alignItems?: keyof typeof alignItemsOptions /** Vertical alignment of children. */ justifyContent?: keyof typeof justifyContentOptions /** Extra class names on the
element. */ diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 52bc578af..cb44518da 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -7,6 +7,7 @@ import StatusMedia from 'soapbox/components/status-media'; import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; import StatusContent from 'soapbox/components/status_content'; import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay'; +import TranslateButton from 'soapbox/components/translate-button'; import { HStack, Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container'; @@ -109,8 +110,11 @@ const DetailedStatus: React.FC = ({ status={actualStatus} expanded={!actualStatus.hidden} onExpandedToggle={handleExpandedToggle} + translatable /> + + | null, }); const normalizeAttachments = (status: ImmutableMap) => { diff --git a/app/soapbox/precheck.ts b/app/soapbox/precheck.ts index 79d523d87..03fea80a1 100644 --- a/app/soapbox/precheck.ts +++ b/app/soapbox/precheck.ts @@ -3,10 +3,10 @@ * @module soapbox/precheck */ -/** Whether pre-rendered data exists in Mastodon's format. */ +/** Whether pre-rendered data exists in Pleroma's format. */ const hasPrerenderPleroma = Boolean(document.getElementById('initial-results')); -/** Whether pre-rendered data exists in Pleroma's format. */ +/** Whether pre-rendered data exists in Mastodon's format. */ const hasPrerenderMastodon = Boolean(document.getElementById('initial-state')); /** Whether initial data was loaded into the page by server-side-rendering (SSR). */ diff --git a/app/soapbox/reducers/instance.ts b/app/soapbox/reducers/instance.ts index 4c1456dc4..ad6b5ae2f 100644 --- a/app/soapbox/reducers/instance.ts +++ b/app/soapbox/reducers/instance.ts @@ -10,6 +10,7 @@ import { rememberInstance, fetchInstance, fetchNodeinfo, + fetchInstanceV2, } from '../actions/instance'; import type { AnyAction } from 'redux'; @@ -32,10 +33,20 @@ const nodeinfoToInstance = (nodeinfo: ImmutableMap) => { })); }; +const instanceV2ToInstance = (instanceV2: ImmutableMap) => + normalizeInstance(ImmutableMap({ + configuration: instanceV2.get('configuration'), + })); + const importInstance = (_state: typeof initialState, instance: ImmutableMap) => { return normalizeInstance(instance); }; +const importInstanceV2 = (state: typeof initialState, instanceV2: ImmutableMap) => { + console.log(instanceV2.toJS()); + return state.mergeDeep(instanceV2ToInstance(instanceV2)); +}; + const importNodeinfo = (state: typeof initialState, nodeinfo: ImmutableMap) => { return nodeinfoToInstance(nodeinfo).mergeDeep(state); }; @@ -120,6 +131,8 @@ export default function instance(state = initialState, action: AnyAction) { case fetchInstance.fulfilled.type: persistInstance(action.payload); return importInstance(state, ImmutableMap(fromJS(action.payload))); + case fetchInstanceV2.fulfilled.type: + return importInstanceV2(state, ImmutableMap(fromJS(action.payload))); case fetchInstance.rejected.type: return handleInstanceFetchFail(state, action.error); case fetchNodeinfo.fulfilled.type: diff --git a/app/soapbox/reducers/statuses.ts b/app/soapbox/reducers/statuses.ts index 49c946aa0..088191edd 100644 --- a/app/soapbox/reducers/statuses.ts +++ b/app/soapbox/reducers/statuses.ts @@ -1,5 +1,5 @@ import escapeTextContentForBrowser from 'escape-html'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import emojify from 'soapbox/features/emoji/emoji'; import { normalizeStatus } from 'soapbox/normalizers'; @@ -30,6 +30,8 @@ import { STATUS_HIDE, STATUS_DELETE_REQUEST, STATUS_DELETE_FAIL, + STATUS_TRANSLATE_SUCCESS, + STATUS_TRANSLATE_UNDO, } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; @@ -255,6 +257,10 @@ export default function statuses(state = initialState, action: AnyAction): State return decrementReplyCount(state, action.params); case STATUS_DELETE_FAIL: return incrementReplyCount(state, action.params); + case STATUS_TRANSLATE_SUCCESS: + return state.setIn([action.id, 'translation'], fromJS(action.translation)); + case STATUS_TRANSLATE_UNDO: + return state.deleteIn([action.id, 'translation']); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); default: diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index f220f5e67..716809612 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -353,6 +353,12 @@ const getInstanceFeatures = (instance: Instance) => { */ importData: v.software === PLEROMA && gte(v.version, '2.2.0'), + /** + * Supports V2 instance endpoint. + * @see GET /api/v2/instance + */ + instanceV2: v.software === MASTODON && gte(v.compatVersion, '4.0.0'), + /** * Can create, view, and manage lists. * @see {@link https://docs.joinmastodon.org/methods/timelines/lists/} @@ -608,6 +614,12 @@ const getInstanceFeatures = (instance: Instance) => { features.includes('v2_suggestions'), ]), + /** + * Can translate statuses. + * @see POST /api/v1/statuses/:id/translate + */ + translations: v.software === MASTODON && instance.configuration.getIn(['translation', 'enabled'], false), + /** * Trending statuses. * @see GET /api/v1/trends/statuses From 2effafd1f63d37bfdb84377e1b302b64ab8896bd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 27 Oct 2022 14:29:40 -0500 Subject: [PATCH 05/48] Remove favicon from index.html --- app/index.ejs | 1 - 1 file changed, 1 deletion(-) diff --git a/app/index.ejs b/app/index.ejs index 495eeaff7..b0d109e1f 100644 --- a/app/index.ejs +++ b/app/index.ejs @@ -8,7 +8,6 @@ - <%= snippets %> From 643e59d426d2b6daed79922be8f271cc90387c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 29 Oct 2022 23:12:08 +0200 Subject: [PATCH 06/48] Fix typo in Polish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/locales/pl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 12d21681d..4385b4446 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -915,7 +915,7 @@ "preferences.fields.expand_spoilers_label": "Zawsze rozwijaj wpisy z ostrzeżeniami o zawartości", "preferences.fields.language_label": "Język", "preferences.fields.media_display_label": "Wyświetlanie zawartości multimedialnej", - "preferences.fields.missing_description_modal_label": "SPokazuj prośbę o potwierdzenie przed wysłaniem wpisu bez opisu multimediów", + "preferences.fields.missing_description_modal_label": "Pokazuj prośbę o potwierdzenie przed wysłaniem wpisu bez opisu multimediów", "preferences.fields.privacy_label": "Prywatność wpisów", "preferences.fields.reduce_motion_label": "Ogranicz ruch w animacjach", "preferences.fields.system_font_label": "Używaj domyślnej czcionki systemu", @@ -1255,6 +1255,7 @@ "time_remaining.seconds": "{number, plural, one {Pozostała # sekunda} few {Pozostały # sekundy} many {Pozostało # sekund} other {Pozostało # sekund}}", "trends.count_by_accounts": "{count} {rawCount, plural, one {osoba rozmawia} few {osoby rozmawiają} other {osób rozmawia}} o tym", "trends.title": "Trendy", + "trendsPanel.viewAll": "Pokaż wszystkie", "ui.beforeunload": "Utracisz tworzony wpis, jeżeli opuścisz Soapbox.", "unauthorized_modal.footer": "Masz już konto? {login}.", "unauthorized_modal.text": "Musisz się zalogować, aby to zrobić.", From e3352b89d8d2481ada62cc7227d22b6cd41d2bd7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 15:18:40 -0500 Subject: [PATCH 07/48] Remove "show more" CW button, display SensitiveContentOverlay instead --- app/soapbox/components/status.tsx | 8 +- app/soapbox/components/status_content.tsx | 81 +------------------ .../statuses/sensitive-content-overlay.tsx | 8 ++ .../components/scheduled_status.tsx | 1 - .../status/components/detailed-status.tsx | 12 +-- .../modals/report-modal/report-modal.tsx | 1 - .../features/ui/components/pending_status.tsx | 1 - app/soapbox/normalizers/status.ts | 8 ++ 8 files changed, 21 insertions(+), 99 deletions(-) diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 35827020f..3e30a3032 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -105,10 +105,6 @@ const Status: React.FC = (props) => { } }; - const handleExpandedToggle = (): void => { - dispatch(toggleStatusHidden(actualStatus)); - }; - const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { const status = actualStatus; const firstAttachment = status.media_attachments.first(); @@ -301,7 +297,7 @@ const Status: React.FC = (props) => { const accountAction = props.accountAction || reblogElement; const inReview = status.visibility === 'self'; - const isSensitive = status.sensitive; + const isSensitive = status.hidden; return ( @@ -382,8 +378,6 @@ const Status: React.FC = (props) => { diff --git a/app/soapbox/components/status_content.tsx b/app/soapbox/components/status_content.tsx index 7d771ec37..d2adc181e 100644 --- a/app/soapbox/components/status_content.tsx +++ b/app/soapbox/components/status_content.tsx @@ -35,49 +35,16 @@ const ReadMoreButton: React.FC = ({ onClick }) => ( ); -interface ISpoilerButton { - onClick: React.MouseEventHandler, - hidden: boolean, - tabIndex?: number, -} - -/** Button to expand status text behind a content warning */ -const SpoilerButton: React.FC = ({ onClick, hidden, tabIndex }) => ( - -); - interface IStatusContent { status: Status, - expanded?: boolean, - onExpandedToggle?: () => void, onClick?: () => void, collapsable?: boolean, } /** Renders the text content of a status */ -const StatusContent: React.FC = ({ status, expanded = false, onExpandedToggle, onClick, collapsable = false }) => { +const StatusContent: React.FC = ({ status, onClick, collapsable = false }) => { const history = useHistory(); - const [hidden, setHidden] = useState(true); const [collapsed, setCollapsed] = useState(false); const [onlyEmoji, setOnlyEmoji] = useState(false); @@ -186,18 +153,6 @@ const StatusContent: React.FC = ({ status, expanded = false, onE startXY.current = undefined; }; - const handleSpoilerClick: React.EventHandler = (e) => { - e.preventDefault(); - e.stopPropagation(); - - if (onExpandedToggle) { - // The parent manages the state - onExpandedToggle(); - } else { - setHidden(!hidden); - } - }; - const parsedHtml = useMemo((): string => { const { contentHtml: html } = status; @@ -212,13 +167,11 @@ const StatusContent: React.FC = ({ status, expanded = false, onE return null; } - const isHidden = onExpandedToggle ? !expanded : hidden; const withSpoiler = status.spoiler_text.length > 0; const baseClassName = 'text-gray-900 dark:text-gray-100 break-words text-ellipsis overflow-hidden relative focus:outline-none'; const content = { __html: parsedHtml }; - const spoilerContent = { __html: status.spoilerHtml }; const directionStyle: React.CSSProperties = { direction: 'ltr' }; const className = classNames(baseClassName, 'status-content', { 'cursor-pointer': onClick, @@ -231,37 +184,7 @@ const StatusContent: React.FC = ({ status, expanded = false, onE directionStyle.direction = 'rtl'; } - if (status.spoiler_text.length > 0) { - return ( -
-

- - -

- -
- - {!isHidden && status.poll && typeof status.poll === 'string' && ( - - )} -
- ); - } else if (onClick) { + if (onClick) { const output = [
{ {intl.formatMessage(isUnderReview ? messages.underReviewSubtitle : messages.sensitiveSubtitle)} + + {status.spoiler_text && ( +
+ + “” + +
+ )}
diff --git a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx index 865bbaff4..b9b90ef27 100644 --- a/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx +++ b/app/soapbox/features/scheduled_statuses/components/scheduled_status.tsx @@ -44,7 +44,6 @@ const ScheduledStatus: React.FC = ({ statusId, ...other }) => diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 52bc578af..4896d1a91 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -37,10 +37,6 @@ const DetailedStatus: React.FC = ({ const intl = useIntl(); const node = useRef(null); - const handleExpandedToggle = () => { - onToggleHidden(status); - }; - const handleOpenCompareHistoryModal = () => { onOpenCompareHistoryModal(status); }; @@ -51,7 +47,7 @@ const DetailedStatus: React.FC = ({ if (!account || typeof account !== 'object') return null; const isUnderReview = actualStatus.visibility === 'self'; - const isSensitive = actualStatus.sensitive; + const isSensitive = actualStatus.hidden; let statusTypeIcon = null; @@ -105,11 +101,7 @@ const DetailedStatus: React.FC = ({ /> ) : null} - + { diff --git a/app/soapbox/features/ui/components/pending_status.tsx b/app/soapbox/features/ui/components/pending_status.tsx index 76b8bb47e..64f0d168b 100644 --- a/app/soapbox/features/ui/components/pending_status.tsx +++ b/app/soapbox/features/ui/components/pending_status.tsx @@ -79,7 +79,6 @@ const PendingStatus: React.FC = ({ idempotencyKey, className, mu diff --git a/app/soapbox/normalizers/status.ts b/app/soapbox/normalizers/status.ts index 4788758d3..ea971c52f 100644 --- a/app/soapbox/normalizers/status.ts +++ b/app/soapbox/normalizers/status.ts @@ -149,6 +149,13 @@ const fixFiltered = (status: ImmutableMap) => { status.delete('filtered'); }; +/** If the status contains spoiler text, treat it as sensitive. */ +const fixSensitivity = (status: ImmutableMap) => { + if (status.get('spoiler_text')) { + status.set('sensitive', true); + } +}; + export const normalizeStatus = (status: Record) => { return StatusRecord( ImmutableMap(fromJS(status)).withMutations(status => { @@ -161,6 +168,7 @@ export const normalizeStatus = (status: Record) => { addSelfMention(status); fixQuote(status); fixFiltered(status); + fixSensitivity(status); }), ); }; From aea7bdcaa03fba8ed2d20500032c097b61aa92ce Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 15:22:25 -0500 Subject: [PATCH 08/48] Preferences: media display --> sensitive content --- app/soapbox/features/preferences/index.tsx | 14 +++++++------- app/soapbox/locales/en.json | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/soapbox/features/preferences/index.tsx b/app/soapbox/features/preferences/index.tsx index bb81eb773..971582812 100644 --- a/app/soapbox/features/preferences/index.tsx +++ b/app/soapbox/features/preferences/index.tsx @@ -77,9 +77,9 @@ const languages = { const messages = defineMessages({ heading: { id: 'column.preferences', defaultMessage: 'Preferences' }, - display_media_default: { id: 'preferences.fields.display_media.default', defaultMessage: 'Hide media marked as sensitive' }, - display_media_hide_all: { id: 'preferences.fields.display_media.hide_all', defaultMessage: 'Always hide media' }, - display_media_show_all: { id: 'preferences.fields.display_media.show_all', defaultMessage: 'Always show media' }, + displayPostsDefault: { id: 'preferences.fields.display_media.default', defaultMessage: 'Hide posts marked as sensitive' }, + displayPostsHideAll: { id: 'preferences.fields.display_media.hide_all', defaultMessage: 'Always hide posts' }, + displayPostsShowAll: { id: 'preferences.fields.display_media.show_all', defaultMessage: 'Always show posts' }, privacy_public: { id: 'preferences.options.privacy_public', defaultMessage: 'Public' }, privacy_unlisted: { id: 'preferences.options.privacy_unlisted', defaultMessage: 'Unlisted' }, privacy_followers_only: { id: 'preferences.options.privacy_followers_only', defaultMessage: 'Followers-only' }, @@ -102,9 +102,9 @@ const Preferences = () => { }; const displayMediaOptions = React.useMemo(() => ({ - default: intl.formatMessage(messages.display_media_default), - hide_all: intl.formatMessage(messages.display_media_hide_all), - show_all: intl.formatMessage(messages.display_media_show_all), + default: intl.formatMessage(messages.displayPostsDefault), + hide_all: intl.formatMessage(messages.displayPostsHideAll), + show_all: intl.formatMessage(messages.displayPostsShowAll), }), []); const defaultPrivacyOptions = React.useMemo(() => ({ @@ -149,7 +149,7 @@ const Preferences = () => { /> - }> + }> Date: Mon, 31 Oct 2022 15:38:23 -0500 Subject: [PATCH 09/48] Compose: send `sensitive: true` when spoiler is enabled --- app/soapbox/reducers/compose.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index 4523edab9..dcdc4491f 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -296,11 +296,8 @@ export default function compose(state = initialState, action: AnyAction) { return updateCompose(state, action.id, compose => compose.withMutations(map => { map.set('spoiler_text', ''); map.set('spoiler', !compose.spoiler); + map.set('sensitive', !compose.spoiler); map.set('idempotencyKey', uuid()); - - if (!compose.sensitive && compose.media_attachments.size >= 1) { - map.set('sensitive', true); - } })); case COMPOSE_SPOILER_TEXT_CHANGE: return updateCompose(state, action.id, compose => compose From f51f2984a5bf6741dabb6992b1b36cfcb219da89 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 16:19:51 -0500 Subject: [PATCH 10/48] Compose: overhaul Spoiler feature --- app/soapbox/components/autosuggest_input.tsx | 2 +- .../compose/components/compose-form.tsx | 45 +++-------- .../compose/components/spoiler-input.tsx | 79 +++++++++++++++++++ app/soapbox/locales/en.json | 2 +- 4 files changed, 93 insertions(+), 35 deletions(-) create mode 100644 app/soapbox/features/compose/components/spoiler-input.tsx diff --git a/app/soapbox/components/autosuggest_input.tsx b/app/soapbox/components/autosuggest_input.tsx index 277d87f96..155cd428a 100644 --- a/app/soapbox/components/autosuggest_input.tsx +++ b/app/soapbox/components/autosuggest_input.tsx @@ -45,7 +45,7 @@ const textAtCursorMatchesToken = (str: string, caretPosition: number, searchToke } }; -interface IAutosuggestInput extends Pick, 'onChange' | 'onKeyUp' | 'onKeyDown'> { +export interface IAutosuggestInput extends Pick, 'onChange' | 'onKeyUp' | 'onKeyDown'> { value: string, suggestions: ImmutableList, disabled?: boolean, diff --git a/app/soapbox/features/compose/components/compose-form.tsx b/app/soapbox/features/compose/components/compose-form.tsx index b9968ad3e..c0b64f1d1 100644 --- a/app/soapbox/features/compose/components/compose-form.tsx +++ b/app/soapbox/features/compose/components/compose-form.tsx @@ -10,7 +10,6 @@ import { clearComposeSuggestions, fetchComposeSuggestions, selectComposeSuggestion, - changeComposeSpoilerText, insertEmojiCompose, uploadCompose, } from 'soapbox/actions/compose'; @@ -38,6 +37,7 @@ import UploadButtonContainer from '../containers/upload_button_container'; import WarningContainer from '../containers/warning_container'; import { countableText } from '../util/counter'; +import SpoilerInput from './spoiler-input'; import TextCharacterCounter from './text_character_counter'; import VisualCharacterCounter from './visual_character_counter'; @@ -48,7 +48,7 @@ const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u20 const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What\'s on your mind?' }, pollPlaceholder: { id: 'compose_form.poll_placeholder', defaultMessage: 'Add a poll topic...' }, - spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, + spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' }, publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, message: { id: 'compose_form.message', defaultMessage: 'Message' }, @@ -165,10 +165,6 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text'])); }; - const handleChangeSpoilerText: React.ChangeEventHandler = (e) => { - dispatch(changeComposeSpoilerText(id, e.target.value)); - }; - const setCursor = (start: number, end: number = start) => { if (!autosuggestTextareaRef.current?.textarea) return; autosuggestTextareaRef.current.textarea.setSelectionRange(start, end); @@ -265,7 +261,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab } return ( - + {scheduledStatusCount > 0 && ( ({ id, shouldCondense, autoFocus, clickab {!shouldCondense && } -
- -
- ({ id, shouldCondense, autoFocus, clickab > { !condensed && -
+ -
+ + +
} diff --git a/app/soapbox/features/compose/components/spoiler-input.tsx b/app/soapbox/features/compose/components/spoiler-input.tsx new file mode 100644 index 000000000..129b972c8 --- /dev/null +++ b/app/soapbox/features/compose/components/spoiler-input.tsx @@ -0,0 +1,79 @@ +import classNames from 'clsx'; +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { changeComposeSpoilerness, changeComposeSpoilerText } from 'soapbox/actions/compose'; +import AutosuggestInput, { IAutosuggestInput } from 'soapbox/components/autosuggest_input'; +import { Divider, Stack, Text } from 'soapbox/components/ui'; +import { useAppDispatch, useCompose } from 'soapbox/hooks'; + +const messages = defineMessages({ + title: { id: 'compose_form.spoiler_title', defaultMessage: 'Sensitive content' }, + placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here (optional)' }, + remove: { id: 'compose_form.spoiler_remove', defaultMessage: 'Remove sensitive' }, +}); + +interface ISpoilerInput extends Pick { + composeId: string extends 'default' ? never : string, +} + +/** Text input for content warning in composer. */ +const SpoilerInput: React.FC = ({ + composeId, + onSuggestionsFetchRequested, + onSuggestionsClearRequested, + onSuggestionSelected, +}) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const compose = useCompose(composeId); + + const handleChangeSpoilerText: React.ChangeEventHandler = (e) => { + dispatch(changeComposeSpoilerText(composeId, e.target.value)); + }; + + const handleRemove = () => { + dispatch(changeComposeSpoilerness(composeId)); + }; + + return ( + + + + + + {intl.formatMessage(messages.title)} + + + + +
+ +
+
+
+ ); +}; + +export default SpoilerInput; \ No newline at end of file diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 7397cc38d..f2eaf32c8 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -288,7 +288,7 @@ "compose_form.sensitive.unmarked": "Media is not marked as sensitive", "compose_form.spoiler.marked": "Text is hidden behind warning", "compose_form.spoiler.unmarked": "Text is not hidden", - "compose_form.spoiler_placeholder": "Write your warning here", + "compose_form.spoiler_placeholder": "Write your warning here (optional)", "confirmation_modal.cancel": "Cancel", "confirmations.admin.deactivate_user.confirm": "Deactivate @{name}", "confirmations.admin.deactivate_user.heading": "Deactivate @{acct}", From 69157097dda2e5859310e53f32f2cde5db8322cd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 16:23:17 -0500 Subject: [PATCH 11/48] SpoilerInput: fix ref --- app/soapbox/features/compose/components/compose-form.tsx | 1 + app/soapbox/features/compose/components/spoiler-input.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/soapbox/features/compose/components/compose-form.tsx b/app/soapbox/features/compose/components/compose-form.tsx index c0b64f1d1..f9eb98204 100644 --- a/app/soapbox/features/compose/components/compose-form.tsx +++ b/app/soapbox/features/compose/components/compose-form.tsx @@ -316,6 +316,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab onSuggestionsFetchRequested={onSuggestionsFetchRequested} onSuggestionsClearRequested={onSuggestionsClearRequested} onSuggestionSelected={onSpoilerSuggestionSelected} + ref={spoilerTextRef} />
} diff --git a/app/soapbox/features/compose/components/spoiler-input.tsx b/app/soapbox/features/compose/components/spoiler-input.tsx index 129b972c8..e6f53d04c 100644 --- a/app/soapbox/features/compose/components/spoiler-input.tsx +++ b/app/soapbox/features/compose/components/spoiler-input.tsx @@ -18,12 +18,12 @@ interface ISpoilerInput extends Pick = ({ +const SpoilerInput = React.forwardRef(({ composeId, onSuggestionsFetchRequested, onSuggestionsClearRequested, onSuggestionSelected, -}) => { +}, ref) => { const intl = useIntl(); const dispatch = useAppDispatch(); const compose = useCompose(composeId); @@ -63,6 +63,7 @@ const SpoilerInput: React.FC = ({ searchTokens={[':']} id='cw-spoiler-input' className='rounded-md dark:!bg-transparent !bg-transparent' + ref={ref} autoFocus /> @@ -74,6 +75,6 @@ const SpoilerInput: React.FC = ({ ); -}; +}); export default SpoilerInput; \ No newline at end of file From 9ae8fc4e03d04eeafde9342bbe85010ca24d9a0a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 16:26:58 -0500 Subject: [PATCH 12/48] Compose: clean up unused sensitivity actions --- app/soapbox/actions/compose.ts | 8 ---- .../compose/components/sensitive-button.tsx | 48 ------------------- .../compose/components/upload_form.tsx | 3 -- .../reducers/__tests__/compose.test.ts | 25 +--------- app/soapbox/reducers/compose.ts | 9 ---- 5 files changed, 2 insertions(+), 91 deletions(-) delete mode 100644 app/soapbox/features/compose/components/sensitive-button.tsx diff --git a/app/soapbox/actions/compose.ts b/app/soapbox/actions/compose.ts index d397f4026..57e7909a1 100644 --- a/app/soapbox/actions/compose.ts +++ b/app/soapbox/actions/compose.ts @@ -54,7 +54,6 @@ const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE'; const COMPOSE_TAG_HISTORY_UPDATE = 'COMPOSE_TAG_HISTORY_UPDATE'; -const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE'; const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; @@ -600,11 +599,6 @@ const insertIntoTagHistory = (composeId: string, recognizedTags: APIEntity[], te dispatch(updateTagHistory(composeId, newHistory)); }; -const changeComposeSensitivity = (composeId: string) => ({ - type: COMPOSE_SENSITIVITY_CHANGE, - id: composeId, -}); - const changeComposeSpoilerness = (composeId: string) => ({ type: COMPOSE_SPOILERNESS_CHANGE, id: composeId, @@ -741,7 +735,6 @@ export { COMPOSE_SUGGESTION_SELECT, COMPOSE_SUGGESTION_TAGS_UPDATE, COMPOSE_TAG_HISTORY_UPDATE, - COMPOSE_SENSITIVITY_CHANGE, COMPOSE_SPOILERNESS_CHANGE, COMPOSE_TYPE_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE, @@ -796,7 +789,6 @@ export { selectComposeSuggestion, updateSuggestionTags, updateTagHistory, - changeComposeSensitivity, changeComposeSpoilerness, changeComposeContentType, changeComposeSpoilerText, diff --git a/app/soapbox/features/compose/components/sensitive-button.tsx b/app/soapbox/features/compose/components/sensitive-button.tsx deleted file mode 100644 index 87d5103c5..000000000 --- a/app/soapbox/features/compose/components/sensitive-button.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import { useIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import { changeComposeSensitivity } from 'soapbox/actions/compose'; -import { FormGroup, Checkbox } from 'soapbox/components/ui'; -import { useAppDispatch, useCompose } from 'soapbox/hooks'; - -const messages = defineMessages({ - marked: { id: 'compose_form.sensitive.marked', defaultMessage: 'Media is marked as sensitive' }, - unmarked: { id: 'compose_form.sensitive.unmarked', defaultMessage: 'Media is not marked as sensitive' }, -}); - -interface ISensitiveButton { - composeId: string, -} - -/** Button to mark own media as sensitive. */ -const SensitiveButton: React.FC = ({ composeId }) => { - const intl = useIntl(); - const dispatch = useAppDispatch(); - - const compose = useCompose(composeId); - - const active = compose.sensitive === true; - const disabled = compose.spoiler === true; - - const onClick = () => { - dispatch(changeComposeSensitivity(composeId)); - }; - - return ( -
- } - labelTitle={intl.formatMessage(active ? messages.marked : messages.unmarked)} - > - - -
- ); -}; - -export default SensitiveButton; diff --git a/app/soapbox/features/compose/components/upload_form.tsx b/app/soapbox/features/compose/components/upload_form.tsx index 7ad300528..e9c116b24 100644 --- a/app/soapbox/features/compose/components/upload_form.tsx +++ b/app/soapbox/features/compose/components/upload_form.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { useCompose } from 'soapbox/hooks'; -import SensitiveButton from './sensitive-button'; import Upload from './upload'; import UploadProgress from './upload-progress'; @@ -28,8 +27,6 @@ const UploadForm: React.FC = ({ composeId }) => { ))}
- - {!mediaIds.isEmpty() && }
); }; diff --git a/app/soapbox/reducers/__tests__/compose.test.ts b/app/soapbox/reducers/__tests__/compose.test.ts index c8da1c744..1270fb6af 100644 --- a/app/soapbox/reducers/__tests__/compose.test.ts +++ b/app/soapbox/reducers/__tests__/compose.test.ts @@ -181,30 +181,8 @@ describe('compose reducer', () => { }); }); - it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => { - const state = initialState.set('home', ReducerCompose({ spoiler: true, sensitive: true, idempotencyKey: '' })); - const action = { - type: actions.COMPOSE_SENSITIVITY_CHANGE, - id: 'home', - }; - expect(reducer(state, action).toJS().home).toMatchObject({ - sensitive: true, - }); - }); - - it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => { - const state = initialState.set('home', ReducerCompose({ spoiler: false, sensitive: true })); - const action = { - type: actions.COMPOSE_SENSITIVITY_CHANGE, - id: 'home', - }; - expect(reducer(state, action).toJS().home).toMatchObject({ - sensitive: false, - }); - }); - it('should handle COMPOSE_SPOILERNESS_CHANGE on CW button click', () => { - const state = initialState.set('home', ReducerCompose({ spoiler_text: 'spoiler text', spoiler: true, media_attachments: ImmutableList() })); + const state = initialState.set('home', ReducerCompose({ spoiler_text: 'spoiler text', spoiler: true, sensitive: true, media_attachments: ImmutableList() })); const action = { type: actions.COMPOSE_SPOILERNESS_CHANGE, id: 'home', @@ -212,6 +190,7 @@ describe('compose reducer', () => { expect(reducer(state, action).toJS().home).toMatchObject({ spoiler: false, spoiler_text: '', + sensitive: false, }); }); diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index dcdc4491f..bdfcf3009 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -26,7 +26,6 @@ import { COMPOSE_SUGGESTION_SELECT, COMPOSE_SUGGESTION_TAGS_UPDATE, COMPOSE_TAG_HISTORY_UPDATE, - COMPOSE_SENSITIVITY_CHANGE, COMPOSE_SPOILERNESS_CHANGE, COMPOSE_TYPE_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE, @@ -279,14 +278,6 @@ export const initialState: State = ImmutableMap({ export default function compose(state = initialState, action: AnyAction) { switch (action.type) { - case COMPOSE_SENSITIVITY_CHANGE: - return updateCompose(state, action.id, compose => compose.withMutations(map => { - if (!compose.spoiler) { - map.set('sensitive', !compose.sensitive); - } - - map.set('idempotencyKey', uuid()); - })); case COMPOSE_TYPE_CHANGE: return updateCompose(state, action.id, compose => compose.withMutations(map => { map.set('content_type', action.value); From 18f73aae7c2723ee443e54bed09941398573f9f4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 16:50:06 -0500 Subject: [PATCH 13/48] Compose: don't inherit spoilerness of post being replied to --- app/soapbox/reducers/compose.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/soapbox/reducers/compose.ts b/app/soapbox/reducers/compose.ts index bdfcf3009..3f02f8cac 100644 --- a/app/soapbox/reducers/compose.ts +++ b/app/soapbox/reducers/compose.ts @@ -316,14 +316,6 @@ export default function compose(state = initialState, action: AnyAction) { map.set('caretPosition', null); map.set('idempotencyKey', uuid()); map.set('content_type', defaultCompose.content_type); - - if (action.status.get('spoiler_text', '').length > 0) { - map.set('spoiler', true); - map.set('spoiler_text', action.status.spoiler_text); - } else { - map.set('spoiler', false); - map.set('spoiler_text', ''); - } })); case COMPOSE_QUOTE: return updateCompose(state, 'compose-modal', compose => compose.withMutations(map => { From 20f47c72c943c3235169972e4271276b98587fd9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 31 Oct 2022 18:02:55 -0500 Subject: [PATCH 14/48] Preferences: "sensitive contnt" typofix --- app/soapbox/features/preferences/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/preferences/index.tsx b/app/soapbox/features/preferences/index.tsx index 971582812..3adb90861 100644 --- a/app/soapbox/features/preferences/index.tsx +++ b/app/soapbox/features/preferences/index.tsx @@ -149,7 +149,7 @@ const Preferences = () => { /> - }> + }> Date: Tue, 1 Nov 2022 13:16:25 -0500 Subject: [PATCH 15/48] Display only know notification types --- app/soapbox/actions/notifications.ts | 17 ++++++++--------- .../notifications/components/notification.tsx | 2 +- app/soapbox/reducers/notifications.js | 6 ++++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/soapbox/actions/notifications.ts b/app/soapbox/actions/notifications.ts index 79994bde5..4edd76c22 100644 --- a/app/soapbox/actions/notifications.ts +++ b/app/soapbox/actions/notifications.ts @@ -1,5 +1,4 @@ import { - List as ImmutableList, Map as ImmutableMap, } from 'immutable'; import IntlMessageFormat from 'intl-messageformat'; @@ -12,6 +11,7 @@ import { getFilters, regexFromFilters } from 'soapbox/selectors'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; import { unescapeHTML } from 'soapbox/utils/html'; +import { NOTIFICATION_TYPES } from 'soapbox/utils/notification'; import { joinPublicPath } from 'soapbox/utils/static'; import { fetchRelationships } from './accounts'; @@ -168,11 +168,8 @@ const dequeueNotifications = () => dispatch(markReadNotifications()); }; -// const excludeTypesFromSettings = (getState: () => RootState) => (getSettings(getState()).getIn(['notifications', 'shows']) as ImmutableMap).filter(enabled => !enabled).keySeq().toJS(); - const excludeTypesFromFilter = (filter: string) => { - const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'status', 'poll', 'move', 'pleroma:emoji_reaction']); - return allTypes.filterNot(item => item === filter).toJS(); + return NOTIFICATION_TYPES.filter(item => item !== filter); }; const noOp = () => new Promise(f => f(undefined)); @@ -182,6 +179,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an if (!isLoggedIn(getState)) return dispatch(noOp); const state = getState(); + const features = getFeatures(state.instance); const activeFilter = getSettings(state).getIn(['notifications', 'quickFilter', 'active']) as string; const notifications = state.notifications; const isLoadingMore = !!maxId; @@ -195,10 +193,11 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an max_id: maxId, }; - if (activeFilter !== 'all') { - const instance = state.instance; - const features = getFeatures(instance); - + if (activeFilter === 'all') { + if (features.notificationsIncludeTypes) { + params.types = NOTIFICATION_TYPES; + } + } else { if (features.notificationsIncludeTypes) { params.types = [activeFilter]; } else { diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index e1fa87c5a..1871b3a89 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -268,7 +268,7 @@ const Notification: React.FC = (props) => { }; const renderContent = () => { - switch (type) { + switch (type as NotificationType) { case 'follow': case 'user_approved': return account && typeof account === 'object' ? ( diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.js index d7697d4dd..4d5dd9921 100644 --- a/app/soapbox/reducers/notifications.js +++ b/app/soapbox/reducers/notifications.js @@ -5,6 +5,7 @@ import { } from 'immutable'; import { normalizeNotification } from 'soapbox/normalizers/notification'; +import { validType } from 'soapbox/utils/notification'; import { ACCOUNT_BLOCK_SUCCESS, @@ -67,6 +68,11 @@ const fixNotification = notification => { const isValid = notification => { try { + // Ensure the notification is a known type + if (!validType(notification.type)) { + return false; + } + // https://gitlab.com/soapbox-pub/soapbox/-/issues/424 if (!notification.account.id) { return false; From 882eab40d5edaae9af4c905c7a3840c3e1e10117 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 2 Nov 2022 18:42:30 -0500 Subject: [PATCH 16/48] Upgrade eslint-plugin-jsdoc, fix build on Node 19.x --- package.json | 2 +- yarn.lock | 37 ++++++++++++++++++++++--------------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 19f11ede4..2d25a828b 100644 --- a/package.json +++ b/package.json @@ -223,7 +223,7 @@ "eslint": "^7.0.0", "eslint-plugin-compat": "^4.0.2", "eslint-plugin-import": "^2.25.4", - "eslint-plugin-jsdoc": "^39.2.9", + "eslint-plugin-jsdoc": "^39.6.2", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.25.1", diff --git a/yarn.lock b/yarn.lock index 506498f79..423516f95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,14 +1389,14 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3" integrity sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA== -"@es-joy/jsdoccomment@~0.29.0": - version "0.29.0" - resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.29.0.tgz#527c7eefadeaf5c5d0c3b2721b5fa425d2119e98" - integrity sha512-4yKy5t+/joLihG+ei6CCU6sc08sjUdEdXCQ2U+9h9VP13EiqHQ4YMgDC18ys/AsLdJDBX3KRx/AWY6PR7hn52Q== +"@es-joy/jsdoccomment@~0.36.0": + version "0.36.0" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.0.tgz#e3898aad334281a10ceb3c0ec406297a79f2b043" + integrity sha512-u0XZyvUF6Urb2cSivSXA8qXIpT/CxkHcdtZKoWusAzgzmsTWpg0F2FpWXsolHmMUyVY3dLWaoy+0ccJ5uf2QjA== dependencies: comment-parser "1.3.1" esquery "^1.4.0" - jsdoc-type-pratt-parser "~3.0.1" + jsdoc-type-pratt-parser "~3.1.0" "@eslint/eslintrc@^0.4.3": version "0.4.3" @@ -5522,17 +5522,17 @@ eslint-plugin-import@^2.25.4: resolve "^1.20.0" tsconfig-paths "^3.12.0" -eslint-plugin-jsdoc@^39.2.9: - version "39.2.9" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.2.9.tgz#bc351de403f1862f1ef8c440d12dedc28e74cbbb" - integrity sha512-gaPYJT94rWlWyQcisQyyEJHtLaaJqN4baFlLCEr/LcXVibS9wzQTL2dskqk327ggwqQopR+Xecu2Lng1IJ9Ypw== +eslint-plugin-jsdoc@^39.6.2: + version "39.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.6.2.tgz#dcc86cec7cce47aa1a646e38debd5bdf76f63742" + integrity sha512-dvgY/W7eUFoAIIiaWHERIMI61ZWqcz9YFjEeyTzdPlrZc3TY/3aZm5aB91NUoTLWYZmO/vFlYSuQi15tF7uE5A== dependencies: - "@es-joy/jsdoccomment" "~0.29.0" + "@es-joy/jsdoccomment" "~0.36.0" comment-parser "1.3.1" debug "^4.3.4" escape-string-regexp "^4.0.0" esquery "^1.4.0" - semver "^7.3.7" + semver "^7.3.8" spdx-expression-parse "^3.0.1" eslint-plugin-jsx-a11y@^6.4.1: @@ -7705,10 +7705,10 @@ js2xmlparser@^4.0.1: dependencies: xmlcreate "^2.0.3" -jsdoc-type-pratt-parser@~3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.0.1.tgz#ccbc7a4180bc8748af64d2cc431aaa92f88175bb" - integrity sha512-vqMCdAFVIiFhVgBYE/X8naf3L/7qiJsaYWTfUJZZZ124dR3OUz9HrmaMUGpYIYAN4VSuodf6gIZY0e8ktPw9cg== +jsdoc-type-pratt-parser@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz#a4a56bdc6e82e5865ffd9febc5b1a227ff28e67e" + integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw== jsdoc@~3.6.7: version "3.6.7" @@ -10692,6 +10692,13 @@ semver@^7.3.7: dependencies: lru-cache "^6.0.0" +semver@^7.3.8: + version "7.3.8" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" From 9bbffb69575d1cb198cd3a47fa6d21a1ce0e8742 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 2 Nov 2022 19:55:39 -0500 Subject: [PATCH 17/48] Fix video not visible in modal --- app/soapbox/features/ui/components/media-modal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/ui/components/media-modal.tsx b/app/soapbox/features/ui/components/media-modal.tsx index 17bddabeb..92b5ecad9 100644 --- a/app/soapbox/features/ui/components/media-modal.tsx +++ b/app/soapbox/features/ui/components/media-modal.tsx @@ -202,6 +202,7 @@ const MediaModal: React.FC = (props) => { link={link} alt={attachment.description} key={attachment.url} + visible /> ); } else if (attachment.type === 'audio') { From 7599876e136c8eb614d8351945ef845447eab003 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 14:14:02 -0500 Subject: [PATCH 18/48] Refactor sensitive content overlay to work with reposts --- app/soapbox/components/quoted-status.tsx | 25 +++++++++++++------ app/soapbox/components/status.tsx | 8 +++--- .../status/components/detailed-status.tsx | 5 ++-- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index d7ecfaf05..098a4ecb2 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -10,6 +10,7 @@ import { useSettings } from 'soapbox/hooks'; import { defaultMediaVisibility } from 'soapbox/utils/status'; import OutlineBox from './outline-box'; +import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; @@ -127,7 +128,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => return ( @@ -152,12 +153,22 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => dangerouslySetInnerHTML={{ __html: status.contentHtml }} /> - + + {status.hidden && ( + + )} + + + ); diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 3e30a3032..2a1b6ae92 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -296,8 +296,8 @@ const Status: React.FC = (props) => { const accountAction = props.accountAction || reblogElement; - const inReview = status.visibility === 'self'; - const isSensitive = status.hidden; + const inReview = actualStatus.visibility === 'self'; + const isSensitive = actualStatus.hidden; return ( @@ -356,13 +356,13 @@ const Status: React.FC = (props) => { }) } > - {(inReview || isSensitive) ? ( + {(inReview || isSensitive) && ( - ) : null} + )} {!group && actualStatus.group && (
diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 4896d1a91..91fde1127 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -29,7 +29,6 @@ interface IDetailedStatus { const DetailedStatus: React.FC = ({ status, - onToggleHidden, onOpenCompareHistoryModal, onToggleMediaVisibility, showMedia, @@ -93,13 +92,13 @@ const DetailedStatus: React.FC = ({ }) } > - {(isUnderReview || isSensitive) ? ( + {(isUnderReview || isSensitive) && ( - ) : null} + )} From 6a864d126df4528926a985eb80ff81b8db7690c7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 14:57:33 -0500 Subject: [PATCH 19/48] Refactor spacing of statuses --- app/soapbox/components/quoted-status.tsx | 99 ++++++------------- app/soapbox/components/status-content.css | 12 +-- app/soapbox/components/status.tsx | 46 ++++----- .../status/components/detailed-status.tsx | 20 ++-- app/styles/components/media-gallery.scss | 1 - app/styles/components/reply-mentions.scss | 1 - app/styles/components/status.scss | 5 - app/styles/components/video-player.scss | 1 - 8 files changed, 68 insertions(+), 117 deletions(-) diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index 098a4ecb2..9755e7120 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -1,15 +1,17 @@ import classNames from 'clsx'; -import React, { useState } from 'react'; -import { defineMessages, useIntl, FormattedMessage, FormattedList } from 'react-intl'; +import React, { MouseEventHandler, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import StatusMedia from 'soapbox/components/status-media'; -import { Stack, Text } from 'soapbox/components/ui'; +import { Stack } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import { useSettings } from 'soapbox/hooks'; import { defaultMediaVisibility } from 'soapbox/utils/status'; import OutlineBox from './outline-box'; +import StatusReplyMentions from './status-reply-mentions'; +import StatusContent from './status_content'; import SensitiveContentOverlay from './statuses/sensitive-content-overlay'; import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities'; @@ -37,7 +39,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); - const handleExpandClick = (e: React.MouseEvent) => { + const handleExpandClick: MouseEventHandler = (e) => { if (!status) return; const account = status.account as AccountEntity; @@ -58,57 +60,6 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => setShowMedia(!showMedia); }; - const renderReplyMentions = () => { - if (!status?.in_reply_to_id) { - return null; - } - - const account = status.account as AccountEntity; - const to = status.mentions || []; - - if (to.size === 0) { - if (status.in_reply_to_account_id === account.id) { - return ( -
- -
- ); - } else { - return ( -
- -
- ); - } - } - - const accounts = to.slice(0, 2).map(account => <>@{account.username}).toArray(); - - if (to.size > 2) { - accounts.push( - , - ); - } - - return ( -
- , - }} - /> -
- ); - }; - if (!status) { return null; } @@ -128,7 +79,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => return ( @@ -145,16 +96,13 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => withLinkToProfile={!compose} /> - {renderReplyMentions()} + - - - - {status.hidden && ( + + {(status.hidden) && ( = ({ status, onCancel, compose }) => /> )} - + + + + {(status.media_attachments.size > 0) && ( + + )} + diff --git a/app/soapbox/components/status-content.css b/app/soapbox/components/status-content.css index 7c1317d16..997df73f4 100644 --- a/app/soapbox/components/status-content.css +++ b/app/soapbox/components/status-content.css @@ -1,9 +1,9 @@ .status-content p { - @apply mb-5 whitespace-pre-wrap; + @apply mb-4 whitespace-pre-wrap; } .status-content p:last-child { - @apply mb-0.5; + @apply mb-0; } .status-content a { @@ -20,7 +20,7 @@ .status-content ul, .status-content ol { - @apply pl-10 mb-5; + @apply pl-10 mb-4; } .status-content ul { @@ -32,7 +32,7 @@ } .status-content blockquote { - @apply py-1 pl-4 mb-5 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400; + @apply py-1 pl-4 mb-4 border-l-4 border-solid border-gray-400 text-gray-500 dark:text-gray-400; } .status-content code { @@ -51,7 +51,7 @@ /* Code block */ .status-content pre { - @apply py-2 px-3 mb-5 leading-6 overflow-x-auto rounded-md break-all; + @apply py-2 px-3 mb-4 leading-6 overflow-x-auto rounded-md break-all; } .status-content pre:last-child { @@ -60,7 +60,7 @@ /* Markdown images */ .status-content img:not(.emojione):not([width][height]) { - @apply w-full h-72 object-contain rounded-lg overflow-hidden my-5 block; + @apply w-full h-72 object-contain rounded-lg overflow-hidden my-4 block; } /* User setting to underline links */ diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 2a1b6ae92..02eca62fe 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -65,7 +65,6 @@ const Status: React.FC = (props) => { hidden, featured, unread, - group, hideActionBar, variant = 'rounded', withDismiss, @@ -349,6 +348,8 @@ const Status: React.FC = (props) => {
+ + = (props) => { /> )} - {!group && actualStatus.group && ( -
- Posted in {String(actualStatus.getIn(['group', 'title']))} -
- )} + + - + {(quote || actualStatus.media_attachments.size > 0) && ( + + - - - - - {quote} + {quote} + + )} +
{!hideActionBar && ( diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 91fde1127..28c2d020c 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -100,15 +100,21 @@ const DetailedStatus: React.FC = ({ /> )} - + + - + {(quote || actualStatus.media_attachments.size > 0) && ( + + - {quote} + {quote} + + )} + diff --git a/app/styles/components/media-gallery.scss b/app/styles/components/media-gallery.scss index 311af3b64..e9bb14425 100644 --- a/app/styles/components/media-gallery.scss +++ b/app/styles/components/media-gallery.scss @@ -1,6 +1,5 @@ .media-gallery { box-sizing: border-box; - margin-top: 8px; overflow: hidden; border-radius: 10px; position: relative; diff --git a/app/styles/components/reply-mentions.scss b/app/styles/components/reply-mentions.scss index 39b70a585..e767e5a03 100644 --- a/app/styles/components/reply-mentions.scss +++ b/app/styles/components/reply-mentions.scss @@ -10,7 +10,6 @@ .detailed-status { .reply-mentions { display: block; - margin: 4px 0 0 0; span { cursor: pointer; diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index a8c8e4561..4a2c57d3e 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -14,11 +14,6 @@ opacity: 1; animation: fade 150ms linear; - .video-player, - .audio-player { - margin-top: 8px; - } - &.light { .display-name { strong { diff --git a/app/styles/components/video-player.scss b/app/styles/components/video-player.scss index ada5b26f5..b4e87e9b6 100644 --- a/app/styles/components/video-player.scss +++ b/app/styles/components/video-player.scss @@ -7,7 +7,6 @@ flex-direction: column; height: 100%; justify-content: center; - margin-top: 8px; position: relative; text-align: center; z-index: 100; From fb907b07c358663c8d071f8a6342eda7694d3a9c Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 15:18:31 -0500 Subject: [PATCH 20/48] Convert InstanceRestrictions to TSX+FC --- ...trictions.js => instance_restrictions.tsx} | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) rename app/soapbox/features/federation_restrictions/components/{instance_restrictions.js => instance_restrictions.tsx} (80%) diff --git a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js b/app/soapbox/features/federation_restrictions/components/instance_restrictions.tsx similarity index 80% rename from app/soapbox/features/federation_restrictions/components/instance_restrictions.js rename to app/soapbox/features/federation_restrictions/components/instance_restrictions.tsx index fdb3cd328..ac86039fd 100644 --- a/app/soapbox/features/federation_restrictions/components/instance_restrictions.js +++ b/app/soapbox/features/federation_restrictions/components/instance_restrictions.tsx @@ -1,39 +1,29 @@ 'use strict'; -import PropTypes from 'prop-types'; import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; -import { connect } from 'react-redux'; import Icon from 'soapbox/components/icon'; import { Text } from 'soapbox/components/ui'; +import { useAppSelector } from 'soapbox/hooks'; -const hasRestrictions = remoteInstance => { +import type { Map as ImmutableMap } from 'immutable'; + +const hasRestrictions = (remoteInstance: ImmutableMap): boolean => { return remoteInstance .get('federation') .deleteAll(['accept', 'reject_deletes', 'report_removal']) - .reduce((acc, value) => acc || value, false); + .reduce((acc: boolean, value: boolean) => acc || value, false); }; -const mapStateToProps = state => { - return { - instance: state.get('instance'), - }; -}; +interface IInstanceRestrictions { + remoteInstance: ImmutableMap, +} -export default @connect(mapStateToProps) -class InstanceRestrictions extends ImmutablePureComponent { +const InstanceRestrictions: React.FC = ({ remoteInstance }) => { + const instance = useAppSelector(state => state.instance); - static propTypes = { - intl: PropTypes.object.isRequired, - remoteInstance: ImmutablePropTypes.map.isRequired, - instance: ImmutablePropTypes.map, - }; - - renderRestrictions = () => { - const { remoteInstance } = this.props; + const renderRestrictions = () => { const items = []; const { @@ -105,10 +95,9 @@ class InstanceRestrictions extends ImmutablePureComponent { } return items; - } + }; - renderContent = () => { - const { instance, remoteInstance } = this.props; + const renderContent = () => { if (!instance || !remoteInstance) return null; const host = remoteInstance.get('host'); @@ -136,7 +125,7 @@ class InstanceRestrictions extends ImmutablePureComponent { /> ), - this.renderRestrictions(), + renderRestrictions(), ]; } else { return ( @@ -150,14 +139,13 @@ class InstanceRestrictions extends ImmutablePureComponent { ); } - } + }; - render() { - return ( -
- {this.renderContent()} -
- ); - } + return ( +
+ {renderContent()} +
+ ); +}; -} +export default InstanceRestrictions; From 00023642ca33bf1347d293d20a44c8a1d6930c84 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 15:33:51 -0500 Subject: [PATCH 21/48] Remove @ decorators --- app/soapbox/components/column_header.js | 3 +- app/soapbox/components/filter_bar.js | 3 +- app/soapbox/features/admin/user_index.js | 4 +- .../components/column_settings.js | 3 +- .../components/column_settings.js | 3 +- .../components/column_settings.js | 3 +- .../components/follow_request.js | 3 +- .../components/column_settings.js | 3 +- .../components/icon-picker-menu.js | 154 ++++++++++++++++++ .../components/icon_picker_dropdown.js | 150 +---------------- babel.config.js | 1 - package.json | 1 - tsconfig.json | 1 - yarn.lock | 24 --- 14 files changed, 174 insertions(+), 182 deletions(-) create mode 100644 app/soapbox/features/soapbox_config/components/icon-picker-menu.js diff --git a/app/soapbox/components/column_header.js b/app/soapbox/components/column_header.js index 42f8eebb8..83a61b7ea 100644 --- a/app/soapbox/components/column_header.js +++ b/app/soapbox/components/column_header.js @@ -14,7 +14,6 @@ import SubNavigation from 'soapbox/components/sub_navigation'; // hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, // }); -export default @withRouter class ColumnHeader extends React.PureComponent { static propTypes = { @@ -126,3 +125,5 @@ class ColumnHeader extends React.PureComponent { // } } + +export default withRouter(ColumnHeader); \ No newline at end of file diff --git a/app/soapbox/components/filter_bar.js b/app/soapbox/components/filter_bar.js index 601ca04a2..2d6b71d9d 100644 --- a/app/soapbox/components/filter_bar.js +++ b/app/soapbox/components/filter_bar.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { withRouter } from 'react-router-dom'; -export default @withRouter class FilterBar extends React.PureComponent { static propTypes = { @@ -153,3 +152,5 @@ class FilterBar extends React.PureComponent { } } + +export default withRouter(FilterBar); \ No newline at end of file diff --git a/app/soapbox/features/admin/user_index.js b/app/soapbox/features/admin/user_index.js index 213041f98..032ed6dc2 100644 --- a/app/soapbox/features/admin/user_index.js +++ b/app/soapbox/features/admin/user_index.js @@ -18,8 +18,6 @@ const messages = defineMessages({ searchPlaceholder: { id: 'admin.user_index.search_input_placeholder', defaultMessage: 'Who are you looking for?' }, }); -export default @connect() -@injectIntl class UserIndex extends ImmutablePureComponent { static propTypes = { @@ -130,3 +128,5 @@ class UserIndex extends ImmutablePureComponent { } } + +export default injectIntl(connect()(UserIndex)); \ No newline at end of file diff --git a/app/soapbox/features/community_timeline/components/column_settings.js b/app/soapbox/features/community_timeline/components/column_settings.js index 52781f1f4..a9d2b983a 100644 --- a/app/soapbox/features/community_timeline/components/column_settings.js +++ b/app/soapbox/features/community_timeline/components/column_settings.js @@ -11,7 +11,6 @@ const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); -export default @injectIntl class ColumnSettings extends React.PureComponent { static propTypes = { @@ -49,3 +48,5 @@ class ColumnSettings extends React.PureComponent { } } + +export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/home_timeline/components/column_settings.js b/app/soapbox/features/home_timeline/components/column_settings.js index a643c7e17..aa335060d 100644 --- a/app/soapbox/features/home_timeline/components/column_settings.js +++ b/app/soapbox/features/home_timeline/components/column_settings.js @@ -11,7 +11,6 @@ const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); -export default @injectIntl class ColumnSettings extends React.PureComponent { static propTypes = { @@ -53,3 +52,5 @@ class ColumnSettings extends React.PureComponent { } } + +export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/notifications/components/column_settings.js b/app/soapbox/features/notifications/components/column_settings.js index b71e8fa30..9656e67fc 100644 --- a/app/soapbox/features/notifications/components/column_settings.js +++ b/app/soapbox/features/notifications/components/column_settings.js @@ -13,7 +13,6 @@ const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); -export default @injectIntl class ColumnSettings extends React.PureComponent { static propTypes = { @@ -190,3 +189,5 @@ class ColumnSettings extends React.PureComponent { } } + +export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/notifications/components/follow_request.js b/app/soapbox/features/notifications/components/follow_request.js index be781a948..6fa90b648 100644 --- a/app/soapbox/features/notifications/components/follow_request.js +++ b/app/soapbox/features/notifications/components/follow_request.js @@ -14,7 +14,6 @@ const messages = defineMessages({ reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, }); -export default @injectIntl class FollowRequest extends ImmutablePureComponent { static propTypes = { @@ -58,3 +57,5 @@ class FollowRequest extends ImmutablePureComponent { } } + +export default injectIntl(FollowRequest); \ No newline at end of file diff --git a/app/soapbox/features/public_timeline/components/column_settings.js b/app/soapbox/features/public_timeline/components/column_settings.js index 1d7022fa4..b9f2de6ae 100644 --- a/app/soapbox/features/public_timeline/components/column_settings.js +++ b/app/soapbox/features/public_timeline/components/column_settings.js @@ -11,7 +11,6 @@ const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, }); -export default @injectIntl class ColumnSettings extends React.PureComponent { static propTypes = { @@ -53,3 +52,5 @@ class ColumnSettings extends React.PureComponent { } } + +export default injectIntl(ColumnSettings); \ No newline at end of file diff --git a/app/soapbox/features/soapbox_config/components/icon-picker-menu.js b/app/soapbox/features/soapbox_config/components/icon-picker-menu.js new file mode 100644 index 000000000..c89b00488 --- /dev/null +++ b/app/soapbox/features/soapbox_config/components/icon-picker-menu.js @@ -0,0 +1,154 @@ +import classNames from 'clsx'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import Picker from 'emoji-mart/dist-es/components/picker/picker'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; + +const messages = defineMessages({ + emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' }, + emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' }, + emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' }, + custom: { id: 'icon_button.icons', defaultMessage: 'Icons' }, + search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, +}); + +const backgroundImageFn = () => ''; +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +const categoriesSort = ['custom']; + + +class IconPickerMenu extends React.PureComponent { + + static propTypes = { + custom_emojis: PropTypes.object, + loading: PropTypes.bool, + onClose: PropTypes.func.isRequired, + onPick: PropTypes.func.isRequired, + style: PropTypes.object, + placement: PropTypes.string, + arrowOffsetLeft: PropTypes.string, + arrowOffsetTop: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + static defaultProps = { + style: {}, + loading: true, + }; + + state = { + modifierOpen: false, + placement: null, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + componentDidMount() { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + + if (!c) return; + + // Nice and dirty hack to display the icons + c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => { + const newIcon = document.createElement('span'); + newIcon.innerHTML = ``; + elem.parentNode.replaceChild(newIcon, elem); + }); + } + + getI18n = () => { + const { intl } = this.props; + + return { + search: intl.formatMessage(messages.emoji_search), + notfound: intl.formatMessage(messages.emoji_not_found), + categories: { + search: intl.formatMessage(messages.search_results), + custom: intl.formatMessage(messages.custom), + }, + }; + } + + handleClick = emoji => { + emoji.native = emoji.colons; + + this.props.onClose(); + this.props.onPick(emoji); + } + + buildIcons = (customEmojis, autoplay = false) => { + const emojis = []; + + Object.values(customEmojis).forEach(category => { + category.forEach(function(icon) { + const name = icon.replace('fa fa-', ''); + if (icon !== 'email' && icon !== 'memo') { + emojis.push({ + id: name, + name, + short_names: [name], + emoticons: [], + keywords: [name], + imageUrl: '', + }); + } + }); + }); + + return emojis; + }; + + render() { + const { loading, style, intl, custom_emojis } = this.props; + + if (loading) { + return
; + } + + const data = { compressed: true, categories: [], aliases: [], emojis: [] }; + const title = intl.formatMessage(messages.emoji); + const { modifierOpen } = this.state; + + return ( +
+ +
+ ); + } + +} + +export default injectIntl(IconPickerMenu); diff --git a/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js b/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js index cc2986a7b..5393f0dc8 100644 --- a/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js +++ b/app/soapbox/features/soapbox_config/components/icon_picker_dropdown.js @@ -1,6 +1,3 @@ -import classNames from 'clsx'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import Picker from 'emoji-mart/dist-es/components/picker/picker'; import PropTypes from 'prop-types'; import React from 'react'; import { defineMessages, injectIntl } from 'react-intl'; @@ -8,153 +5,12 @@ import Overlay from 'react-overlays/lib/Overlay'; import Icon from 'soapbox/components/icon'; +import IconPickerMenu from './icon-picker-menu'; + const messages = defineMessages({ emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' }, - emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' }, - emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' }, - custom: { id: 'icon_button.icons', defaultMessage: 'Icons' }, - search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, }); -const backgroundImageFn = () => ''; -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const categoriesSort = ['custom']; - -@injectIntl -class IconPickerMenu extends React.PureComponent { - - static propTypes = { - custom_emojis: PropTypes.object, - loading: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onPick: PropTypes.func.isRequired, - style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, - intl: PropTypes.object.isRequired, - }; - - static defaultProps = { - style: {}, - loading: true, - }; - - state = { - modifierOpen: false, - placement: null, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - } - - componentDidMount() { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - - if (!c) return; - - // Nice and dirty hack to display the icons - c.querySelectorAll('button.emoji-mart-emoji > img').forEach(elem => { - const newIcon = document.createElement('span'); - newIcon.innerHTML = ``; - elem.parentNode.replaceChild(newIcon, elem); - }); - } - - getI18n = () => { - const { intl } = this.props; - - return { - search: intl.formatMessage(messages.emoji_search), - notfound: intl.formatMessage(messages.emoji_not_found), - categories: { - search: intl.formatMessage(messages.search_results), - custom: intl.formatMessage(messages.custom), - }, - }; - } - - handleClick = emoji => { - emoji.native = emoji.colons; - - this.props.onClose(); - this.props.onPick(emoji); - } - - buildIcons = (customEmojis, autoplay = false) => { - const emojis = []; - - Object.values(customEmojis).forEach(category => { - category.forEach(function(icon) { - const name = icon.replace('fa fa-', ''); - if (icon !== 'email' && icon !== 'memo') { - emojis.push({ - id: name, - name, - short_names: [name], - emoticons: [], - keywords: [name], - imageUrl: '', - }); - } - }); - }); - - return emojis; - }; - - render() { - const { loading, style, intl, custom_emojis } = this.props; - - if (loading) { - return
; - } - - const data = { compressed: true, categories: [], aliases: [], emojis: [] }; - const title = intl.formatMessage(messages.emoji); - const { modifierOpen } = this.state; - - return ( -
- -
- ); - } - -} - -export default @injectIntl class IconPickerDropdown extends React.PureComponent { static propTypes = { @@ -243,3 +99,5 @@ class IconPickerDropdown extends React.PureComponent { } } + +export default injectIntl(IconPickerDropdown); \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 17af1a21c..f999f83ef 100644 --- a/babel.config.js +++ b/babel.config.js @@ -17,7 +17,6 @@ module.exports = (api) => { plugins: [ '@babel/syntax-dynamic-import', ['@babel/proposal-object-rest-spread', { useBuiltIns: true }], - ['@babel/proposal-decorators', { legacy: true }], '@babel/proposal-class-properties', ['react-intl', { messagesDir: './build/messages/' }], 'preval', diff --git a/package.json b/package.json index 2d25a828b..5c4491321 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,6 @@ "dependencies": { "@babel/core": "^7.18.2", "@babel/plugin-proposal-class-properties": "^7.17.12", - "@babel/plugin-proposal-decorators": "^7.18.2", "@babel/plugin-proposal-object-rest-spread": "^7.18.0", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-react-inline-elements": "^7.16.7", diff --git a/tsconfig.json b/tsconfig.json index 04ec4a470..c47b1a33f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,6 @@ "allowJs": true, "moduleResolution": "node", "resolveJsonModule": true, - "experimentalDecorators": true, "esModuleInterop": true, "typeRoots": [ "./types", "./node_modules/@types"] }, diff --git a/yarn.lock b/yarn.lock index 423516f95..2b4a3a0cf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -538,18 +538,6 @@ "@babel/helper-plugin-utils" "^7.17.12" "@babel/plugin-syntax-class-static-block" "^7.14.5" -"@babel/plugin-proposal-decorators@^7.18.2": - version "7.18.2" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.18.2.tgz#dbe4086d2d42db489399783c3aa9272e9700afd4" - integrity sha512-kbDISufFOxeczi0v4NQP3p5kIeW6izn/6klfWBrIIdGZZe4UpHR+QU03FAoWjGGd9SUXAwbw2pup1kaL4OQsJQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.0" - "@babel/helper-plugin-utils" "^7.17.12" - "@babel/helper-replace-supers" "^7.18.2" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/plugin-syntax-decorators" "^7.17.12" - charcodes "^0.2.0" - "@babel/plugin-proposal-dynamic-import@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz#c19c897eaa46b27634a00fee9fb7d829158704b2" @@ -688,13 +676,6 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-decorators@^7.17.12": - version "7.17.12" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.17.12.tgz#02e8f678602f0af8222235271efea945cfdb018a" - integrity sha512-D1Hz0qtGTza8K2xGyEdVNCYLdVHukAcbQr4K3/s6r/esadyEriZovpJimQOpu8ju4/jV8dW/1xdaE0UpDroidw== - dependencies: - "@babel/helper-plugin-utils" "^7.17.12" - "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -4196,11 +4177,6 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== -charcodes@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4" - integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ== - cheerio-select@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823" From dec89cb2366a7e1561c90f792776240190980af0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 15:52:25 -0500 Subject: [PATCH 22/48] Remove unused .js files, remove groups --- app/soapbox/actions/group_editor.ts | 143 ---- app/soapbox/actions/groups.ts | 550 -------------- app/soapbox/components/avatar_composite.js | 89 --- app/soapbox/components/filter_bar.js | 155 ---- app/soapbox/components/setting_text.js | 34 - .../components/column_settings.js | 55 -- .../containers/column_settings_container.js | 26 - .../components/column_settings.js | 192 ----- .../components/follow_request.js | 60 -- .../components/multi_setting_toggle.js | 39 - .../containers/column_settings_container.js | 55 -- .../containers/follow_request_container.js | 28 - .../ui/components/account_list_panel.js | 57 -- .../reducers/__tests__/group_editor.test.ts | 16 - .../reducers/__tests__/group_lists.test.ts | 13 - .../__tests__/group_relationships.test.ts | 9 - app/soapbox/reducers/__tests__/groups.test.ts | 9 - .../reducers/__tests__/list_editor-test.js | 153 ---- .../reducers/__tests__/notifications-test.js | 673 ------------------ app/soapbox/reducers/__tests__/search-test.js | 131 ---- app/soapbox/reducers/group_editor.js | 58 -- app/soapbox/reducers/group_lists.js | 22 - app/soapbox/reducers/group_relationships.js | 27 - app/soapbox/reducers/groups.js | 34 - app/soapbox/reducers/index.ts | 8 - app/soapbox/reducers/timelines.ts | 7 - app/soapbox/reducers/user_lists.ts | 21 +- 27 files changed, 1 insertion(+), 2663 deletions(-) delete mode 100644 app/soapbox/actions/group_editor.ts delete mode 100644 app/soapbox/actions/groups.ts delete mode 100644 app/soapbox/components/avatar_composite.js delete mode 100644 app/soapbox/components/filter_bar.js delete mode 100644 app/soapbox/components/setting_text.js delete mode 100644 app/soapbox/features/home_timeline/components/column_settings.js delete mode 100644 app/soapbox/features/home_timeline/containers/column_settings_container.js delete mode 100644 app/soapbox/features/notifications/components/column_settings.js delete mode 100644 app/soapbox/features/notifications/components/follow_request.js delete mode 100644 app/soapbox/features/notifications/components/multi_setting_toggle.js delete mode 100644 app/soapbox/features/notifications/containers/column_settings_container.js delete mode 100644 app/soapbox/features/notifications/containers/follow_request_container.js delete mode 100644 app/soapbox/features/ui/components/account_list_panel.js delete mode 100644 app/soapbox/reducers/__tests__/group_editor.test.ts delete mode 100644 app/soapbox/reducers/__tests__/group_lists.test.ts delete mode 100644 app/soapbox/reducers/__tests__/group_relationships.test.ts delete mode 100644 app/soapbox/reducers/__tests__/groups.test.ts delete mode 100644 app/soapbox/reducers/__tests__/list_editor-test.js delete mode 100644 app/soapbox/reducers/__tests__/notifications-test.js delete mode 100644 app/soapbox/reducers/__tests__/search-test.js delete mode 100644 app/soapbox/reducers/group_editor.js delete mode 100644 app/soapbox/reducers/group_lists.js delete mode 100644 app/soapbox/reducers/group_relationships.js delete mode 100644 app/soapbox/reducers/groups.js diff --git a/app/soapbox/actions/group_editor.ts b/app/soapbox/actions/group_editor.ts deleted file mode 100644 index 23f3491ad..000000000 --- a/app/soapbox/actions/group_editor.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { isLoggedIn } from 'soapbox/utils/auth'; - -import api from '../api'; - -import type { AxiosError } from 'axios'; -import type { History } from 'history'; -import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity } from 'soapbox/types/entities'; - -const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; -const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; -const GROUP_CREATE_FAIL = 'GROUP_CREATE_FAIL'; - -const GROUP_UPDATE_REQUEST = 'GROUP_UPDATE_REQUEST'; -const GROUP_UPDATE_SUCCESS = 'GROUP_UPDATE_SUCCESS'; -const GROUP_UPDATE_FAIL = 'GROUP_UPDATE_FAIL'; - -const GROUP_EDITOR_VALUE_CHANGE = 'GROUP_EDITOR_VALUE_CHANGE'; -const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET'; -const GROUP_EDITOR_SETUP = 'GROUP_EDITOR_SETUP'; - -const submit = (routerHistory: History) => - (dispatch: AppDispatch, getState: () => RootState) => { - const groupId = getState().group_editor.get('groupId') as string; - const title = getState().group_editor.get('title') as string; - const description = getState().group_editor.get('description') as string; - const coverImage = getState().group_editor.get('coverImage') as any; - - if (groupId === null) { - dispatch(create(title, description, coverImage, routerHistory)); - } else { - dispatch(update(groupId, title, description, coverImage, routerHistory)); - } - }; - -const create = (title: string, description: string, coverImage: File, routerHistory: History) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(createRequest()); - - const formData = new FormData(); - formData.append('title', title); - formData.append('description', description); - - if (coverImage !== null) { - formData.append('cover_image', coverImage); - } - - api(getState).post('/api/v1/groups', formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => { - dispatch(createSuccess(data)); - routerHistory.push(`/groups/${data.id}`); - }).catch(err => dispatch(createFail(err))); - }; - -const createRequest = (id?: string) => ({ - type: GROUP_CREATE_REQUEST, - id, -}); - -const createSuccess = (group: APIEntity) => ({ - type: GROUP_CREATE_SUCCESS, - group, -}); - -const createFail = (error: AxiosError) => ({ - type: GROUP_CREATE_FAIL, - error, -}); - -const update = (groupId: string, title: string, description: string, coverImage: File, routerHistory: History) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(updateRequest(groupId)); - - const formData = new FormData(); - formData.append('title', title); - formData.append('description', description); - - if (coverImage !== null) { - formData.append('cover_image', coverImage); - } - - api(getState).put(`/api/v1/groups/${groupId}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }).then(({ data }) => { - dispatch(updateSuccess(data)); - routerHistory.push(`/groups/${data.id}`); - }).catch(err => dispatch(updateFail(err))); - }; - -const updateRequest = (id: string) => ({ - type: GROUP_UPDATE_REQUEST, - id, -}); - -const updateSuccess = (group: APIEntity) => ({ - type: GROUP_UPDATE_SUCCESS, - group, -}); - -const updateFail = (error: AxiosError) => ({ - type: GROUP_UPDATE_FAIL, - error, -}); - -const changeValue = (field: string, value: string | File) => ({ - type: GROUP_EDITOR_VALUE_CHANGE, - field, - value, -}); - -const reset = () => ({ - type: GROUP_EDITOR_RESET, -}); - -const setUp = (group: string) => ({ - type: GROUP_EDITOR_SETUP, - group, -}); - -export { - GROUP_CREATE_REQUEST, - GROUP_CREATE_SUCCESS, - GROUP_CREATE_FAIL, - GROUP_UPDATE_REQUEST, - GROUP_UPDATE_SUCCESS, - GROUP_UPDATE_FAIL, - GROUP_EDITOR_VALUE_CHANGE, - GROUP_EDITOR_RESET, - GROUP_EDITOR_SETUP, - submit, - create, - createRequest, - createSuccess, - createFail, - update, - updateRequest, - updateSuccess, - updateFail, - changeValue, - reset, - setUp, -}; diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts deleted file mode 100644 index 808cc3204..000000000 --- a/app/soapbox/actions/groups.ts +++ /dev/null @@ -1,550 +0,0 @@ -import { AxiosError } from 'axios'; - -import { isLoggedIn } from 'soapbox/utils/auth'; - -import api, { getLinks } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; - -import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity } from 'soapbox/types/entities'; - -const GROUP_FETCH_REQUEST = 'GROUP_FETCH_REQUEST'; -const GROUP_FETCH_SUCCESS = 'GROUP_FETCH_SUCCESS'; -const GROUP_FETCH_FAIL = 'GROUP_FETCH_FAIL'; - -const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST'; -const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS'; -const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL'; - -const GROUPS_FETCH_REQUEST = 'GROUPS_FETCH_REQUEST'; -const GROUPS_FETCH_SUCCESS = 'GROUPS_FETCH_SUCCESS'; -const GROUPS_FETCH_FAIL = 'GROUPS_FETCH_FAIL'; - -const GROUP_JOIN_REQUEST = 'GROUP_JOIN_REQUEST'; -const GROUP_JOIN_SUCCESS = 'GROUP_JOIN_SUCCESS'; -const GROUP_JOIN_FAIL = 'GROUP_JOIN_FAIL'; - -const GROUP_LEAVE_REQUEST = 'GROUP_LEAVE_REQUEST'; -const GROUP_LEAVE_SUCCESS = 'GROUP_LEAVE_SUCCESS'; -const GROUP_LEAVE_FAIL = 'GROUP_LEAVE_FAIL'; - -const GROUP_MEMBERS_FETCH_REQUEST = 'GROUP_MEMBERS_FETCH_REQUEST'; -const GROUP_MEMBERS_FETCH_SUCCESS = 'GROUP_MEMBERS_FETCH_SUCCESS'; -const GROUP_MEMBERS_FETCH_FAIL = 'GROUP_MEMBERS_FETCH_FAIL'; - -const GROUP_MEMBERS_EXPAND_REQUEST = 'GROUP_MEMBERS_EXPAND_REQUEST'; -const GROUP_MEMBERS_EXPAND_SUCCESS = 'GROUP_MEMBERS_EXPAND_SUCCESS'; -const GROUP_MEMBERS_EXPAND_FAIL = 'GROUP_MEMBERS_EXPAND_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST = 'GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_FETCH_FAIL = 'GROUP_REMOVED_ACCOUNTS_FETCH_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST = 'GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL = 'GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL = 'GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL'; - -const GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST = 'GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST'; -const GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS = 'GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS'; -const GROUP_REMOVED_ACCOUNTS_CREATE_FAIL = 'GROUP_REMOVED_ACCOUNTS_CREATE_FAIL'; - -const GROUP_REMOVE_STATUS_REQUEST = 'GROUP_REMOVE_STATUS_REQUEST'; -const GROUP_REMOVE_STATUS_SUCCESS = 'GROUP_REMOVE_STATUS_SUCCESS'; -const GROUP_REMOVE_STATUS_FAIL = 'GROUP_REMOVE_STATUS_FAIL'; - -const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchGroupRelationships([id])); - - if (getState().groups.get(id)) { - return; - } - - dispatch(fetchGroupRequest(id)); - - api(getState).get(`/api/v1/groups/${id}`) - .then(({ data }) => dispatch(fetchGroupSuccess(data))) - .catch(err => dispatch(fetchGroupFail(id, err))); -}; - -const fetchGroupRequest = (id: string) => ({ - type: GROUP_FETCH_REQUEST, - id, -}); - -const fetchGroupSuccess = (group: APIEntity) => ({ - type: GROUP_FETCH_SUCCESS, - group, -}); - -const fetchGroupFail = (id: string, error: AxiosError) => ({ - type: GROUP_FETCH_FAIL, - id, - error, -}); - -const fetchGroupRelationships = (groupIds: string[]) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const loadedRelationships = getState().group_relationships; - const newGroupIds = groupIds.filter(id => loadedRelationships.get(id, null) === null); - - if (newGroupIds.length === 0) { - return; - } - - dispatch(fetchGroupRelationshipsRequest(newGroupIds)); - - api(getState).get(`/api/v1/groups/${newGroupIds[0]}/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => { - dispatch(fetchGroupRelationshipsSuccess(response.data)); - }).catch(error => { - dispatch(fetchGroupRelationshipsFail(error)); - }); - }; - -const fetchGroupRelationshipsRequest = (ids: string[]) => ({ - type: GROUP_RELATIONSHIPS_FETCH_REQUEST, - ids, - skipLoading: true, -}); - -const fetchGroupRelationshipsSuccess = (relationships: APIEntity[]) => ({ - type: GROUP_RELATIONSHIPS_FETCH_SUCCESS, - relationships, - skipLoading: true, -}); - -const fetchGroupRelationshipsFail = (error: AxiosError) => ({ - type: GROUP_RELATIONSHIPS_FETCH_FAIL, - error, - skipLoading: true, -}); - -const fetchGroups = (tab: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchGroupsRequest()); - - api(getState).get('/api/v1/groups?tab=' + tab) - .then(({ data }) => { - dispatch(fetchGroupsSuccess(data, tab)); - dispatch(fetchGroupRelationships(data.map((item: APIEntity) => item.id))); - }) - .catch(err => dispatch(fetchGroupsFail(err))); -}; - -const fetchGroupsRequest = () => ({ - type: GROUPS_FETCH_REQUEST, -}); - -const fetchGroupsSuccess = (groups: APIEntity[], tab: string) => ({ - type: GROUPS_FETCH_SUCCESS, - groups, - tab, -}); - -const fetchGroupsFail = (error: AxiosError) => ({ - type: GROUPS_FETCH_FAIL, - error, -}); - -const joinGroup = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(joinGroupRequest(id)); - - api(getState).post(`/api/v1/groups/${id}/accounts`).then(response => { - dispatch(joinGroupSuccess(response.data)); - }).catch(error => { - dispatch(joinGroupFail(id, error)); - }); - }; - -const leaveGroup = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(leaveGroupRequest(id)); - - api(getState).delete(`/api/v1/groups/${id}/accounts`).then(response => { - dispatch(leaveGroupSuccess(response.data)); - }).catch(error => { - dispatch(leaveGroupFail(id, error)); - }); - }; - -const joinGroupRequest = (id: string) => ({ - type: GROUP_JOIN_REQUEST, - id, -}); - -const joinGroupSuccess = (relationship: APIEntity) => ({ - type: GROUP_JOIN_SUCCESS, - relationship, -}); - -const joinGroupFail = (id: string, error: AxiosError) => ({ - type: GROUP_JOIN_FAIL, - id, - error, -}); - -const leaveGroupRequest = (id: string) => ({ - type: GROUP_LEAVE_REQUEST, - id, -}); - -const leaveGroupSuccess = (relationship: APIEntity) => ({ - type: GROUP_LEAVE_SUCCESS, - relationship, -}); - -const leaveGroupFail = (id: string, error: AxiosError) => ({ - type: GROUP_LEAVE_FAIL, - id, - error, -}); - -const fetchMembers = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchMembersRequest(id)); - - api(getState).get(`/api/v1/groups/${id}/accounts`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchMembersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(fetchMembersFail(id, error)); - }); - }; - -const fetchMembersRequest = (id: string) => ({ - type: GROUP_MEMBERS_FETCH_REQUEST, - id, -}); - -const fetchMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_MEMBERS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -const fetchMembersFail = (id: string, error: AxiosError) => ({ - type: GROUP_MEMBERS_FETCH_FAIL, - id, - error, -}); - -const expandMembers = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const url = getState().user_lists.groups.get(id)!.next; - - if (url === null) { - return; - } - - dispatch(expandMembersRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(expandMembersSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(expandMembersFail(id, error)); - }); - }; - -const expandMembersRequest = (id: string) => ({ - type: GROUP_MEMBERS_EXPAND_REQUEST, - id, -}); - -const expandMembersSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_MEMBERS_EXPAND_SUCCESS, - id, - accounts, - next, -}); - -const expandMembersFail = (id: string, error: AxiosError) => ({ - type: GROUP_MEMBERS_EXPAND_FAIL, - id, - error, -}); - -const fetchRemovedAccounts = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(fetchRemovedAccountsRequest(id)); - - api(getState).get(`/api/v1/groups/${id}/removed_accounts`).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(fetchRemovedAccountsSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(fetchRemovedAccountsFail(id, error)); - }); - }; - -const fetchRemovedAccountsRequest = (id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST, - id, -}); - -const fetchRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, - id, - accounts, - next, -}); - -const fetchRemovedAccountsFail = (id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_FETCH_FAIL, - id, - error, -}); - -const expandRemovedAccounts = (id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const url = getState().user_lists.groups_removed_accounts.get(id)!.next; - - if (url === null) { - return; - } - - dispatch(expandRemovedAccountsRequest(id)); - - api(getState).get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - - dispatch(importFetchedAccounts(response.data)); - dispatch(expandRemovedAccountsSuccess(id, response.data, next ? next.uri : null)); - dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id))); - }).catch(error => { - dispatch(expandRemovedAccountsFail(id, error)); - }); - }; - -const expandRemovedAccountsRequest = (id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST, - id, -}); - -const expandRemovedAccountsSuccess = (id: string, accounts: APIEntity[], next: string | null) => ({ - type: GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, - id, - accounts, - next, -}); - -const expandRemovedAccountsFail = (id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL, - id, - error, -}); - -const removeRemovedAccount = (groupId: string, id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(removeRemovedAccountRequest(groupId, id)); - - api(getState).delete(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => { - dispatch(removeRemovedAccountSuccess(groupId, id)); - }).catch(error => { - dispatch(removeRemovedAccountFail(groupId, id, error)); - }); - }; - -const removeRemovedAccountRequest = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST, - groupId, - id, -}); - -const removeRemovedAccountSuccess = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, - groupId, - id, -}); - -const removeRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL, - groupId, - id, - error, -}); - -const createRemovedAccount = (groupId: string, id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(createRemovedAccountRequest(groupId, id)); - - api(getState).post(`/api/v1/groups/${groupId}/removed_accounts?account_id=${id}`).then(response => { - dispatch(createRemovedAccountSuccess(groupId, id)); - }).catch(error => { - dispatch(createRemovedAccountFail(groupId, id, error)); - }); - }; - -const createRemovedAccountRequest = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST, - groupId, - id, -}); - -const createRemovedAccountSuccess = (groupId: string, id: string) => ({ - type: GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS, - groupId, - id, -}); - -const createRemovedAccountFail = (groupId: string, id: string, error: AxiosError) => ({ - type: GROUP_REMOVED_ACCOUNTS_CREATE_FAIL, - groupId, - id, - error, -}); - -const groupRemoveStatus = (groupId: string, id: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(groupRemoveStatusRequest(groupId, id)); - - api(getState).delete(`/api/v1/groups/${groupId}/statuses/${id}`).then(response => { - dispatch(groupRemoveStatusSuccess(groupId, id)); - }).catch(error => { - dispatch(groupRemoveStatusFail(groupId, id, error)); - }); - }; - -const groupRemoveStatusRequest = (groupId: string, id: string) => ({ - type: GROUP_REMOVE_STATUS_REQUEST, - groupId, - id, -}); - -const groupRemoveStatusSuccess = (groupId: string, id: string) => ({ - type: GROUP_REMOVE_STATUS_SUCCESS, - groupId, - id, -}); - -const groupRemoveStatusFail = (groupId: string, id: string, error: AxiosError) => ({ - type: GROUP_REMOVE_STATUS_FAIL, - groupId, - id, - error, -}); - -export { - GROUP_FETCH_REQUEST, - GROUP_FETCH_SUCCESS, - GROUP_FETCH_FAIL, - GROUP_RELATIONSHIPS_FETCH_REQUEST, - GROUP_RELATIONSHIPS_FETCH_SUCCESS, - GROUP_RELATIONSHIPS_FETCH_FAIL, - GROUPS_FETCH_REQUEST, - GROUPS_FETCH_SUCCESS, - GROUPS_FETCH_FAIL, - GROUP_JOIN_REQUEST, - GROUP_JOIN_SUCCESS, - GROUP_JOIN_FAIL, - GROUP_LEAVE_REQUEST, - GROUP_LEAVE_SUCCESS, - GROUP_LEAVE_FAIL, - GROUP_MEMBERS_FETCH_REQUEST, - GROUP_MEMBERS_FETCH_SUCCESS, - GROUP_MEMBERS_FETCH_FAIL, - GROUP_MEMBERS_EXPAND_REQUEST, - GROUP_MEMBERS_EXPAND_SUCCESS, - GROUP_MEMBERS_EXPAND_FAIL, - GROUP_REMOVED_ACCOUNTS_FETCH_REQUEST, - GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, - GROUP_REMOVED_ACCOUNTS_FETCH_FAIL, - GROUP_REMOVED_ACCOUNTS_EXPAND_REQUEST, - GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, - GROUP_REMOVED_ACCOUNTS_EXPAND_FAIL, - GROUP_REMOVED_ACCOUNTS_REMOVE_REQUEST, - GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, - GROUP_REMOVED_ACCOUNTS_REMOVE_FAIL, - GROUP_REMOVED_ACCOUNTS_CREATE_REQUEST, - GROUP_REMOVED_ACCOUNTS_CREATE_SUCCESS, - GROUP_REMOVED_ACCOUNTS_CREATE_FAIL, - GROUP_REMOVE_STATUS_REQUEST, - GROUP_REMOVE_STATUS_SUCCESS, - GROUP_REMOVE_STATUS_FAIL, - fetchGroup, - fetchGroupRequest, - fetchGroupSuccess, - fetchGroupFail, - fetchGroupRelationships, - fetchGroupRelationshipsRequest, - fetchGroupRelationshipsSuccess, - fetchGroupRelationshipsFail, - fetchGroups, - fetchGroupsRequest, - fetchGroupsSuccess, - fetchGroupsFail, - joinGroup, - leaveGroup, - joinGroupRequest, - joinGroupSuccess, - joinGroupFail, - leaveGroupRequest, - leaveGroupSuccess, - leaveGroupFail, - fetchMembers, - fetchMembersRequest, - fetchMembersSuccess, - fetchMembersFail, - expandMembers, - expandMembersRequest, - expandMembersSuccess, - expandMembersFail, - fetchRemovedAccounts, - fetchRemovedAccountsRequest, - fetchRemovedAccountsSuccess, - fetchRemovedAccountsFail, - expandRemovedAccounts, - expandRemovedAccountsRequest, - expandRemovedAccountsSuccess, - expandRemovedAccountsFail, - removeRemovedAccount, - removeRemovedAccountRequest, - removeRemovedAccountSuccess, - removeRemovedAccountFail, - createRemovedAccount, - createRemovedAccountRequest, - createRemovedAccountSuccess, - createRemovedAccountFail, - groupRemoveStatus, - groupRemoveStatusRequest, - groupRemoveStatusSuccess, - groupRemoveStatusFail, -}; diff --git a/app/soapbox/components/avatar_composite.js b/app/soapbox/components/avatar_composite.js deleted file mode 100644 index 59e4bab96..000000000 --- a/app/soapbox/components/avatar_composite.js +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -import StillImage from 'soapbox/components/still_image'; - -export default class AvatarComposite extends React.PureComponent { - - static propTypes = { - accounts: ImmutablePropTypes.list.isRequired, - size: PropTypes.number.isRequired, - }; - - renderItem(account, size, index) { - - let width = 50; - let height = 100; - let top = 'auto'; - let left = 'auto'; - let bottom = 'auto'; - let right = 'auto'; - - if (size === 1) { - width = 100; - } - - if (size === 4 || (size === 3 && index > 0)) { - height = 50; - } - - if (size === 2) { - if (index === 0) { - right = '2px'; - } else { - left = '2px'; - } - } else if (size === 3) { - if (index === 0) { - right = '2px'; - } else if (index > 0) { - left = '2px'; - } - - if (index === 1) { - bottom = '2px'; - } else if (index > 1) { - top = '2px'; - } - } else if (size === 4) { - if (index === 0 || index === 2) { - right = '2px'; - } - - if (index === 1 || index === 3) { - left = '2px'; - } - - if (index < 2) { - bottom = '2px'; - } else { - top = '2px'; - } - } - - const style = { - left: left, - top: top, - right: right, - bottom: bottom, - width: `${width}%`, - height: `${height}%`, - }; - - return ( - - ); - } - - render() { - const { accounts, size } = this.props; - - return ( -
- {accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))} -
- ); - } - -} diff --git a/app/soapbox/components/filter_bar.js b/app/soapbox/components/filter_bar.js deleted file mode 100644 index 601ca04a2..000000000 --- a/app/soapbox/components/filter_bar.js +++ /dev/null @@ -1,155 +0,0 @@ -import classNames from 'clsx'; -import debounce from 'lodash/debounce'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { withRouter } from 'react-router-dom'; - -export default @withRouter -class FilterBar extends React.PureComponent { - - static propTypes = { - items: PropTypes.array.isRequired, - active: PropTypes.string, - className: PropTypes.string, - history: PropTypes.object, - }; - - state = { - mounted: false, - }; - - componentDidMount() { - this.node.addEventListener('keydown', this.handleKeyDown, false); - window.addEventListener('resize', this.handleResize, { passive: true }); - - const { left, width } = this.getActiveTabIndicationSize(); - this.setState({ mounted: true, left, width }); - } - - componentWillUnmount() { - this.node.removeEventListener('keydown', this.handleKeyDown, false); - document.removeEventListener('resize', this.handleResize, false); - } - - handleResize = debounce(() => { - this.setState(this.getActiveTabIndicationSize()); - }, 300, { - trailing: true, - }); - - componentDidUpdate(prevProps) { - if (this.props.active !== prevProps.active) { - this.setState(this.getActiveTabIndicationSize()); - } - } - - setRef = c => { - this.node = c; - } - - setFocusRef = c => { - this.focusedItem = c; - } - - handleKeyDown = e => { - const items = Array.from(this.node.getElementsByTagName('a')); - const index = items.indexOf(document.activeElement); - let element = null; - - switch (e.key) { - case 'ArrowRight': - element = items[index + 1] || items[0]; - break; - case 'ArrowLeft': - element = items[index - 1] || items[items.length - 1]; - break; - } - - if (element) { - element.focus(); - e.preventDefault(); - e.stopPropagation(); - } - } - - handleItemKeyPress = e => { - if (e.key === 'Enter' || e.key === ' ') { - this.handleClick(e); - } - } - - handleClick = e => { - const i = Number(e.currentTarget.getAttribute('data-index')); - const { action, to } = this.props.items[i]; - - if (typeof action === 'function') { - e.preventDefault(); - action(e); - } else if (to) { - e.preventDefault(); - this.props.history.push(to); - } - } - - getActiveTabIndicationSize() { - const { active, items } = this.props; - - if (!active || !this.node) return { width: null }; - - const index = items.findIndex(({ name }) => name === active); - const elements = Array.from(this.node.getElementsByTagName('a')); - const element = elements[index]; - - if (!element) return { width: null }; - - const left = element.offsetLeft; - const { width } = element.getBoundingClientRect(); - - return { left, width }; - } - - renderActiveTabIndicator() { - const { left, width } = this.state; - - return ( -
- ); - } - - renderItem(option, i) { - if (option === null) { - return
  • ; - } - - const { name, text, href, to, title } = option; - - return ( - - {text} - - ); - } - - render() { - const { className, items } = this.props; - const { mounted } = this.state; - - return ( -
    - {mounted && this.renderActiveTabIndicator()} - {items.map((option, i) => this.renderItem(option, i))} -
    - ); - } - -} diff --git a/app/soapbox/components/setting_text.js b/app/soapbox/components/setting_text.js deleted file mode 100644 index e4972f102..000000000 --- a/app/soapbox/components/setting_text.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class SettingText extends React.PureComponent { - - static propTypes = { - settings: ImmutablePropTypes.map.isRequired, - settingKey: PropTypes.array.isRequired, - label: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - }; - - handleChange = (e) => { - this.props.onChange(this.props.settingKey, e.target.value); - } - - render() { - const { settings, settingKey, label } = this.props; - - return ( - - ); - } - -} diff --git a/app/soapbox/features/home_timeline/components/column_settings.js b/app/soapbox/features/home_timeline/components/column_settings.js deleted file mode 100644 index a643c7e17..000000000 --- a/app/soapbox/features/home_timeline/components/column_settings.js +++ /dev/null @@ -1,55 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import IconButton from 'soapbox/components/icon_button'; - -import SettingToggle from '../../notifications/components/setting_toggle'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -export default @injectIntl -class ColumnSettings extends React.PureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - settings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - render() { - const { intl, settings, onChange, onClose } = this.props; - - return ( -
    -
    -

    - -

    -
    - -
    -
    - -
    -
    - } /> -
    - -
    - } /> -
    - -
    - } /> -
    -
    -
    - ); - } - -} diff --git a/app/soapbox/features/home_timeline/containers/column_settings_container.js b/app/soapbox/features/home_timeline/containers/column_settings_container.js deleted file mode 100644 index 0bcbafae8..000000000 --- a/app/soapbox/features/home_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,26 +0,0 @@ -import { connect } from 'react-redux'; - -import { - getSettings, - changeSetting, - saveSettings, -} from '../../../actions/settings'; -import ColumnSettings from '../components/column_settings'; - -const mapStateToProps = state => ({ - settings: getSettings(state).get('home'), -}); - -const mapDispatchToProps = dispatch => ({ - - onChange(key, checked) { - dispatch(changeSetting(['home', ...key], checked)); - }, - - onSave() { - dispatch(saveSettings()); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/soapbox/features/notifications/components/column_settings.js b/app/soapbox/features/notifications/components/column_settings.js deleted file mode 100644 index b71e8fa30..000000000 --- a/app/soapbox/features/notifications/components/column_settings.js +++ /dev/null @@ -1,192 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import IconButton from 'soapbox/components/icon_button'; - -import ClearColumnButton from './clear_column_button'; -import MultiSettingToggle from './multi_setting_toggle'; -import SettingToggle from './setting_toggle'; - -const messages = defineMessages({ - close: { id: 'lightbox.close', defaultMessage: 'Close' }, -}); - -export default @injectIntl -class ColumnSettings extends React.PureComponent { - - static propTypes = { - intl: PropTypes.object.isRequired, - settings: ImmutablePropTypes.map.isRequired, - pushSettings: ImmutablePropTypes.map.isRequired, - onChange: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - supportsEmojiReacts: PropTypes.bool, - supportsBirthdays: PropTypes.bool, - }; - - onPushChange = (path, checked) => { - this.props.onChange(['push', ...path], checked); - } - - onAllSoundsChange = (path, checked) => { - const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']]; - - for (let i = 0; i < soundSettings.length; i++) { - this.props.onChange(soundSettings[i], checked); - } - } - - render() { - const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts, supportsBirthdays } = this.props; - - const filterShowStr = ; - const filterAdvancedStr = ; - const alertStr = ; - const allSoundsStr = ; - const showStr = ; - const soundStr = ; - const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']]; - const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); - const pushStr = showPushSettings && ; - const birthdaysStr = ; - - return ( -
    -
    -

    - -

    -
    - -
    -
    - -
    -
    - -
    - -
    - - - - -
    - -
    - - - -
    - - -
    -
    - - {supportsBirthdays && -
    - - - -
    - -
    -
    - } - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - - {supportsEmojiReacts &&
    - - -
    - - {showPushSettings && } - - -
    -
    } - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    - -
    - - -
    - - {showPushSettings && } - - -
    -
    -
    -
    - ); - } - -} diff --git a/app/soapbox/features/notifications/components/follow_request.js b/app/soapbox/features/notifications/components/follow_request.js deleted file mode 100644 index be781a948..000000000 --- a/app/soapbox/features/notifications/components/follow_request.js +++ /dev/null @@ -1,60 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl } from 'react-intl'; - -import Avatar from 'soapbox/components/avatar'; -import DisplayName from 'soapbox/components/display-name'; -import IconButton from 'soapbox/components/icon_button'; -import Permalink from 'soapbox/components/permalink'; - -const messages = defineMessages({ - authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, - reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, -}); - -export default @injectIntl -class FollowRequest extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - onAuthorize: PropTypes.func.isRequired, - onReject: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render() { - const { intl, hidden, account, onAuthorize, onReject } = this.props; - - if (!account) { - return
    ; - } - - if (hidden) { - return ( - - {account.get('display_name')} - {account.get('username')} - - ); - } - - return ( -
    -
    - -
    - -
    - -
    - - -
    -
    -
    - ); - } - -} diff --git a/app/soapbox/features/notifications/components/multi_setting_toggle.js b/app/soapbox/features/notifications/components/multi_setting_toggle.js deleted file mode 100644 index 68c382872..000000000 --- a/app/soapbox/features/notifications/components/multi_setting_toggle.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Toggle from 'react-toggle'; - -export default class MultiSettingToggle extends React.PureComponent { - - static propTypes = { - prefix: PropTypes.string, - settings: ImmutablePropTypes.map.isRequired, - settingPaths: PropTypes.array.isRequired, - label: PropTypes.node, - onChange: PropTypes.func.isRequired, - ariaLabel: PropTypes.string, - } - - onChange = ({ target }) => { - for (let i = 0; i < this.props.settingPaths.length; i++) { - this.props.onChange(this.props.settingPaths[i], target.checked); - } - } - - areTrue = (settingPath) => { - return this.props.settings.getIn(settingPath) === true; - } - - render() { - const { prefix, settingPaths, label, ariaLabel } = this.props; - const id = ['setting-toggle', prefix].filter(Boolean).join('-'); - - return ( -
    - - {label && ()} -
    - ); - } - -} diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js deleted file mode 100644 index 6375e59f5..000000000 --- a/app/soapbox/features/notifications/containers/column_settings_container.js +++ /dev/null @@ -1,55 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { openModal } from 'soapbox/actions/modals'; -import { clearNotifications, setFilter } from 'soapbox/actions/notifications'; -import { changeAlerts as changePushNotifications } from 'soapbox/actions/push_notifications'; -import { getSettings, changeSetting } from 'soapbox/actions/settings'; -import { getFeatures } from 'soapbox/utils/features'; - -import ColumnSettings from '../components/column_settings'; - -const messages = defineMessages({ - clearHeading: { id: 'notifications.clear_heading', defaultMessage: 'Clear notifications' }, - clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, - clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, -}); - -const mapStateToProps = state => { - const instance = state.get('instance'); - const features = getFeatures(instance); - - return { - settings: getSettings(state).get('notifications'), - pushSettings: state.get('push_notifications'), - supportsEmojiReacts: features.emojiReacts, - supportsBirthdays: features.birthdays, - }; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - - onChange(path, checked) { - if (path[0] === 'push') { - dispatch(changePushNotifications(path.slice(1), checked)); - } else if (path[0] === 'quickFilter') { - dispatch(changeSetting(['notifications', ...path], checked)); - dispatch(setFilter('all')); - } else { - dispatch(changeSetting(['notifications', ...path], checked)); - } - }, - - onClear() { - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/eraser.svg'), - heading: intl.formatMessage(messages.clearHeading), - message: intl.formatMessage(messages.clearMessage), - confirm: intl.formatMessage(messages.clearConfirm), - onConfirm: () => dispatch(clearNotifications()), - })); - }, - -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); diff --git a/app/soapbox/features/notifications/containers/follow_request_container.js b/app/soapbox/features/notifications/containers/follow_request_container.js deleted file mode 100644 index c793ac3de..000000000 --- a/app/soapbox/features/notifications/containers/follow_request_container.js +++ /dev/null @@ -1,28 +0,0 @@ -import { connect } from 'react-redux'; - -import { authorizeFollowRequest, rejectFollowRequest } from 'soapbox/actions/accounts'; -import { makeGetAccount } from 'soapbox/selectors'; - -import FollowRequest from '../components/follow_request'; - -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, props) => ({ - account: getAccount(state, props.id), - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { id }) => ({ - onAuthorize() { - dispatch(authorizeFollowRequest(id)); - }, - - onReject() { - dispatch(rejectFollowRequest(id)); - }, -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(FollowRequest); diff --git a/app/soapbox/features/ui/components/account_list_panel.js b/app/soapbox/features/ui/components/account_list_panel.js deleted file mode 100644 index 61b703e4f..000000000 --- a/app/soapbox/features/ui/components/account_list_panel.js +++ /dev/null @@ -1,57 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { Link } from 'react-router-dom'; - -import Icon from 'soapbox/components/icon'; -import AccountContainer from 'soapbox/containers/account_container'; - -export default class AccountListPanel extends ImmutablePureComponent { - - static propTypes = { - title: PropTypes.node.isRequired, - accountIds: ImmutablePropTypes.orderedSet.isRequired, - icon: PropTypes.string.isRequired, - limit: PropTypes.number, - total: PropTypes.number, - expandMessage: PropTypes.string, - expandRoute: PropTypes.string, - }; - - static defaultProps = { - limit: Infinity, - } - - render() { - const { title, icon, accountIds, limit, total, expandMessage, expandRoute, ...props } = this.props; - - if (!accountIds || accountIds.isEmpty()) { - return null; - } - - const canExpand = expandMessage && expandRoute && (accountIds.size < total); - - return ( -
    -
    - - - {title} - -
    -
    -
    - {accountIds.take(limit).map(accountId => ( - - ))} -
    -
    - {canExpand && - {expandMessage} - } -
    - ); - } - -} diff --git a/app/soapbox/reducers/__tests__/group_editor.test.ts b/app/soapbox/reducers/__tests__/group_editor.test.ts deleted file mode 100644 index 516b6df43..000000000 --- a/app/soapbox/reducers/__tests__/group_editor.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from '../group_editor'; - -describe('group_editor reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({ - groupId: null, - isSubmitting: false, - isChanged: false, - title: '', - description: '', - coverImage: null, - })); - }); -}); diff --git a/app/soapbox/reducers/__tests__/group_lists.test.ts b/app/soapbox/reducers/__tests__/group_lists.test.ts deleted file mode 100644 index 46527f682..000000000 --- a/app/soapbox/reducers/__tests__/group_lists.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -import reducer from '../group_lists'; - -describe('group_lists reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap({ - featured: ImmutableList(), - member: ImmutableList(), - admin: ImmutableList(), - })); - }); -}); diff --git a/app/soapbox/reducers/__tests__/group_relationships.test.ts b/app/soapbox/reducers/__tests__/group_relationships.test.ts deleted file mode 100644 index 31e3e354f..000000000 --- a/app/soapbox/reducers/__tests__/group_relationships.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from '../group_relationships'; - -describe('group_relationships reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); - }); -}); diff --git a/app/soapbox/reducers/__tests__/groups.test.ts b/app/soapbox/reducers/__tests__/groups.test.ts deleted file mode 100644 index 05b88402f..000000000 --- a/app/soapbox/reducers/__tests__/groups.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import reducer from '../groups'; - -describe('groups reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {} as any)).toEqual(ImmutableMap()); - }); -}); diff --git a/app/soapbox/reducers/__tests__/list_editor-test.js b/app/soapbox/reducers/__tests__/list_editor-test.js deleted file mode 100644 index 1b351bca1..000000000 --- a/app/soapbox/reducers/__tests__/list_editor-test.js +++ /dev/null @@ -1,153 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable'; - -import * as actions from 'soapbox/actions/lists'; - -import reducer from '../list_editor'; - -describe('list_editor reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {})).toMatchObject({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: { - items: ImmutableList(), - loaded: false, - isLoading: false, - }, - - suggestions: { - value: '', - items: ImmutableList(), - }, - }); - }); - - it('should handle LIST_EDITOR_RESET', () => { - const state = ImmutableRecord({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: ImmutableRecord({ - items: ImmutableList(), - loaded: false, - isLoading: false, - })(), - - suggestions: ImmutableRecord({ - value: '', - items: ImmutableList(), - })(), - })(); - const action = { - type: actions.LIST_EDITOR_RESET, - }; - expect(reducer(state, action)).toMatchObject({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: { - items: ImmutableList(), - loaded: false, - isLoading: false, - }, - - suggestions: { - value: '', - items: ImmutableList(), - }, - }); - }); - - it('should handle LIST_EDITOR_SETUP', () => { - const state = ImmutableRecord({ - listId: null, - isSubmitting: false, - isChanged: false, - title: '', - - accounts: ImmutableRecord({ - items: ImmutableList(), - loaded: false, - isLoading: false, - })(), - - suggestions: ImmutableRecord({ - value: '', - items: ImmutableList(), - })(), - })(); - const action = { - type: actions.LIST_EDITOR_SETUP, - list: ImmutableMap({ - id: '22', - title: 'list 1', - }), - }; - expect(reducer(state, action)).toMatchObject({ - listId: '22', - isSubmitting: false, - isChanged: false, - title: 'list 1', - - accounts: { - items: ImmutableList(), - loaded: false, - isLoading: false, - }, - - suggestions: { - value: '', - items: ImmutableList(), - }, - }); - }); - - it('should handle LIST_EDITOR_TITLE_CHANGE', () => { - const state = ImmutableMap({ - title: 'list 1', - isChanged: false, - }); - const action = { - type: actions.LIST_EDITOR_TITLE_CHANGE, - value: 'list 1 edited', - }; - expect(reducer(state, action).toJS()).toMatchObject({ - isChanged: true, - title: 'list 1 edited', - }); - }); - - it('should handle LIST_UPDATE_REQUEST', () => { - const state = ImmutableMap({ - isSubmitting: false, - isChanged: true, - }); - const action = { - type: actions.LIST_UPDATE_REQUEST, - }; - expect(reducer(state, action).toJS()).toMatchObject({ - isSubmitting: true, - isChanged: false, - }); - }); - - it('should handle LIST_UPDATE_FAIL', () => { - const state = ImmutableMap({ - isSubmitting: true, - }); - const action = { - type: actions.LIST_UPDATE_FAIL, - }; - expect(reducer(state, action).toJS()).toMatchObject({ - isSubmitting: false, - }); - }); - -}); diff --git a/app/soapbox/reducers/__tests__/notifications-test.js b/app/soapbox/reducers/__tests__/notifications-test.js deleted file mode 100644 index 1ea296c60..000000000 --- a/app/soapbox/reducers/__tests__/notifications-test.js +++ /dev/null @@ -1,673 +0,0 @@ -import { - Map as ImmutableMap, - OrderedMap as ImmutableOrderedMap, - Record as ImmutableRecord, -} from 'immutable'; -import take from 'lodash/take'; - -import intlMessages from 'soapbox/__fixtures__/intlMessages.json'; -import notification from 'soapbox/__fixtures__/notification.json'; -import notifications from 'soapbox/__fixtures__/notifications.json'; -import relationship from 'soapbox/__fixtures__/relationship.json'; -import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts'; -import { - MARKER_FETCH_SUCCESS, - MARKER_SAVE_REQUEST, - MARKER_SAVE_SUCCESS, -} from 'soapbox/actions/markers'; -import { - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_SCROLL_TOP, - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_UPDATE_QUEUE, - NOTIFICATIONS_DEQUEUE, - NOTIFICATIONS_CLEAR, - NOTIFICATIONS_MARK_READ_REQUEST, -} from 'soapbox/actions/notifications'; -import { TIMELINE_DELETE } from 'soapbox/actions/timelines'; -import { applyActions } from 'soapbox/jest/test-helpers'; - -import reducer from '../notifications'; - -const initialState = reducer(undefined, {}); - -describe('notifications reducer', () => { - it('should return the initial state', () => { - const expected = { - items: {}, - hasMore: true, - top: false, - unread: 0, - isLoading: false, - queuedNotifications: {}, - totalQueuedNotificationsCount: 0, - lastRead: -1, - }; - - expect(ImmutableRecord.isRecord(initialState)).toBe(true); - expect(initialState.toJS()).toMatchObject(expected); - }); - - describe('NOTIFICATIONS_EXPAND_SUCCESS', () => { - it('imports the notifications', () => { - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: null, - skipLoading: true, - }; - - const result = reducer(undefined, action); - - // The items are parsed as records - expect(ImmutableOrderedMap.isOrderedMap(result.items)).toBe(true); - expect(ImmutableRecord.isRecord(result.items.get('10743'))).toBe(true); - - // We can get an item - expect(result.items.get('10744').emoji).toEqual('😢'); - - // hasMore is set to false because `next` is null - expect(result.hasMore).toBe(false); - }); - - it('drops invalid notifications', () => { - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - { id: '1', type: 'mention', status: null, account: { id: '10' } }, - { id: '2', type: 'reblog', status: null, account: { id: '9' } }, - { id: '3', type: 'favourite', status: null, account: { id: '8' } }, - { id: '4', type: 'mention', status: { id: 'a' }, account: { id: '7' } }, - { id: '5', type: 'reblog', status: { id: 'b' }, account: null }, - ], - next: null, - skipLoading: true, - }; - - const result = reducer(undefined, action); - - // Only '4' is valid - expect(result.items.size).toEqual(1); - expect(result.items.get('4').id).toEqual('4'); - }); - - it('imports move notification', () => { - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - require('soapbox/__fixtures__/pleroma-notification-move.json'), - ], - next: null, - skipLoading: true, - }; - - const result = reducer(undefined, action).items.get('406814'); - - expect(result.account).toEqual('AFmHQ18XZ7Lco68MW8'); - expect(result.target).toEqual('A5c5LK7EJTFR0u26Pg'); - }); - }); - - describe('NOTIFICATIONS_EXPAND_REQUEST', () => { - it('sets isLoading to true', () => { - const state = initialState.set('isLoading', false); - const action = { type: NOTIFICATIONS_EXPAND_REQUEST }; - - expect(reducer(state, action).isLoading).toBe(true); - }); - }); - - describe('NOTIFICATIONS_EXPAND_FAIL', () => { - it('sets isLoading to false', () => { - const state = initialState.set('isLoading', true); - const action = { type: NOTIFICATIONS_EXPAND_FAIL }; - - expect(reducer(state, action).isLoading).toBe(false); - }); - }); - - describe('NOTIFICATIONS_FILTER_SET', () => { - it('clears the items', () => { - const actions = [{ - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - { id: '1', type: 'mention', status: { id: '4' }, account: { id: '7' } }, - { id: '2', type: 'mention', status: { id: '5' }, account: { id: '8' } }, - { id: '3', type: 'mention', status: { id: '6' }, account: { id: '9' } }, - ], - next: null, - skipLoading: true, - }, { - type: NOTIFICATIONS_FILTER_SET, - }]; - - // Setup by expanding, then calling `NOTIFICATIONS_FILTER_SET` - const result = applyActions(initialState, actions, reducer); - - // Setting the filter wipes notifications - expect(result.items.isEmpty()).toBe(true); - }); - - it('sets hasMore to true', () => { - const state = initialState.set('hasMore', false); - const action = { type: NOTIFICATIONS_FILTER_SET }; - const result = reducer(state, action); - - expect(result.hasMore).toBe(true); - }); - }); - - describe('NOTIFICATIONS_SCROLL_TOP', () => { - it('resets `unread` counter to 0 when top is true (ie, scrolled to the top)', () => { - const state = initialState.set('unread', 1); - const action = { type: NOTIFICATIONS_SCROLL_TOP, top: true }; - const result = reducer(state, action); - - expect(result.unread).toEqual(0); - expect(result.top).toBe(true); - }); - - it('leaves `unread` alone when top is false (ie, not scrolled to top)', () => { - const state = initialState.set('unread', 3); - const action = { type: NOTIFICATIONS_SCROLL_TOP, top: false }; - const result = reducer(state, action); - - expect(result.unread).toEqual(3); - expect(result.top).toBe(false); - }); - }); - - describe('NOTIFICATIONS_UPDATE', () => { - it('imports the notification', () => { - const action = { type: NOTIFICATIONS_UPDATE, notification }; - const result = reducer(initialState, action); - - expect(result.items.get('10743').type).toEqual('favourite'); - }); - - it('imports follow_request notification', () => { - const action = { - type: NOTIFICATIONS_UPDATE, - notification: require('soapbox/__fixtures__/notification-follow_request.json'), - }; - - const result = reducer(initialState, action); - expect(result.items.get('87967').type).toEqual('follow_request'); - }); - - it('increments `unread` counter when top is false', () => { - const action = { type: NOTIFICATIONS_UPDATE, notification }; - const result = reducer(initialState, action); - - expect(result.unread).toEqual(1); - }); - }); - - describe('NOTIFICATIONS_UPDATE_QUEUE', () => { - it('adds the notification to the queue (and increases the counter)', () => { - const action = { - type: NOTIFICATIONS_UPDATE_QUEUE, - notification, - intlMessages, - intlLocale: 'en', - }; - - const result = reducer(initialState, action); - - // Doesn't add it as a regular item - expect(result.items.isEmpty()).toBe(true); - - // Adds it to the queued items - expect(result.queuedNotifications.size).toEqual(1); - expect(result.totalQueuedNotificationsCount).toEqual(1); - expect(result.queuedNotifications.getIn(['10743', 'notification', 'type'])).toEqual('favourite'); - }); - }); - - describe('NOTIFICATIONS_DEQUEUE', () => { - it('resets the queued counter to 0', () => { - const state = initialState.set('totalQueuedNotificationsCount', 1); - const action = { type: NOTIFICATIONS_DEQUEUE }; - const result = reducer(state, action); - - expect(result.totalQueuedNotificationsCount).toEqual(0); - }); - }); - - describe('NOTIFICATIONS_EXPAND_SUCCESS', () => { - it('with non-empty items and next set true', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10734', ImmutableMap({ - id: '10734', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ]), - unread: 1, - hasMore: true, - isLoading: false, - }); - - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: true, - }; - - const expected = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - total_count: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ['10734', ImmutableMap({ - id: '10734', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ]), - unread: 1, - hasMore: true, - isLoading: false, - }); - - expect(reducer(state, action).toJS()).toEqual(expected.toJS()); - }); - - it('with empty items and next set true', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap(), - unread: 1, - hasMore: true, - isLoading: false, - }); - - const action = { - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: take(notifications, 3), - next: true, - }; - - const expected = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - total_count: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - total_count: null, - })], - ]), - unread: 1, - hasMore: true, - isLoading: false, - }); - - expect(reducer(state, action).toJS()).toEqual(expected.toJS()); - }); - }); - - describe('ACCOUNT_BLOCK_SUCCESS', () => { - it('should handle', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - }); - const action = { - type: ACCOUNT_BLOCK_SUCCESS, - relationship, - }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableOrderedMap([ - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - })); - }); - }); - - describe('ACCOUNT_MUTE_SUCCESS', () => { - it('should handle', () => { - const state = ImmutableMap({ - items: ImmutableOrderedMap([ - ['10744', ImmutableMap({ - id: '10744', - type: 'pleroma:emoji_reaction', - account: '9vMAje101ngtjlMj7w', - target: null, - created_at: '2020-06-10T02:54:39.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: '😢', - chat_message: null, - })], - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - }); - const action = { - type: ACCOUNT_MUTE_SUCCESS, - relationship: relationship, - }; - expect(reducer(state, action)).toEqual(ImmutableMap({ - items: ImmutableOrderedMap([ - ['10743', ImmutableMap({ - id: '10743', - type: 'favourite', - account: '9v5c6xSEgAi3Zu1Lv6', - target: null, - created_at: '2020-06-10T02:51:05.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ['10741', ImmutableMap({ - id: '10741', - type: 'favourite', - account: '9v5cKMOPGqPcgfcWp6', - target: null, - created_at: '2020-06-10T02:05:06.000Z', - status: '9vvNxoo5EFbbnfdXQu', - emoji: null, - chat_message: null, - })], - ]), - })); - }); - }); - - describe('NOTIFICATIONS_CLEAR', () => { - it('clears the items', () => { - const state = initialState.set('items', ImmutableOrderedMap([['1', {}], ['2', {}]])); - const action = { type: NOTIFICATIONS_CLEAR }; - const result = reducer(state, action); - - expect(result.items.isEmpty()).toBe(true); - }); - }); - - describe('NOTIFICATIONS_MARK_READ_REQUEST', () => { - it('sets lastRead to the one in the action', () => { - const action = { type: NOTIFICATIONS_MARK_READ_REQUEST, lastRead: '1234' }; - const result = reducer(undefined, action); - - expect(result.lastRead).toEqual('1234'); - }); - }); - - describe('TIMELINE_DELETE', () => { - it('deletes notifications corresponding to the status ID', () => { - const actions = [{ - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications: [ - { id: '1', type: 'mention', status: { id: '4' }, account: { id: '7' } }, - { id: '2', type: 'mention', status: { id: '5' }, account: { id: '8' } }, - { id: '3', type: 'mention', status: { id: '6' }, account: { id: '9' } }, - { id: '4', type: 'mention', status: { id: '5' }, account: { id: '7' } }, - ], - next: null, - skipLoading: true, - }, { - type: TIMELINE_DELETE, - id: '5', - }]; - - // Setup by expanding, then calling `NOTIFICATIONS_FILTER_SET` - const result = applyActions(initialState, actions, reducer); - - expect(result.items.size).toEqual(2); - expect(result.items.get('5')).toBe(undefined); - }); - }); - - describe('MARKER_FETCH_SUCCESS', () => { - it('sets lastRead', () => { - const action = { - type: MARKER_FETCH_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '1234', - }, - }, - }; - - expect(reducer(undefined, action).get('lastRead')).toEqual('1234'); - }); - - it('updates the unread count', () => { - const action = { - type: MARKER_FETCH_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '5678', - }, - }, - }; - - const state = ImmutableMap({ - items: ImmutableOrderedMap({ - '9012': ImmutableMap({ id: '9012' }), - '5678': ImmutableMap({ id: '5678' }), - '1234': ImmutableMap({ id: '1234' }), - }), - unread: 3, - }); - - expect(reducer(state, action).get('unread')).toEqual(1); - }); - }); - - describe('MARKER_SAVE_REQUEST', () => { - it('sets lastRead', () => { - const action = { - type: MARKER_SAVE_REQUEST, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '1234', - }, - }, - }; - - expect(reducer(undefined, action).get('lastRead')).toEqual('1234'); - }); - - it('updates the unread count', () => { - const action = { - type: MARKER_SAVE_REQUEST, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '5678', - }, - }, - }; - - const state = ImmutableMap({ - items: ImmutableOrderedMap({ - '9012': ImmutableMap({ id: '9012' }), - '5678': ImmutableMap({ id: '5678' }), - '1234': ImmutableMap({ id: '1234' }), - }), - unread: 3, - }); - - expect(reducer(state, action).get('unread')).toEqual(1); - }); - }); - - describe('MARKER_SAVE_SUCCESS', () => { - it('sets lastRead', () => { - const action = { - type: MARKER_SAVE_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '5678', - }, - }, - }; - - expect(reducer(undefined, action).get('lastRead')).toEqual('5678'); - }); - - it('updates the unread count', () => { - const action = { - type: MARKER_SAVE_SUCCESS, - timeline: ['notifications'], - marker: { - notifications: { - last_read_id: '9012', - }, - }, - }; - - const state = ImmutableMap({ - items: ImmutableOrderedMap({ - '9012': ImmutableMap({ id: '9012' }), - '5678': ImmutableMap({ id: '5678' }), - '1234': ImmutableMap({ id: '1234' }), - }), - unread: 3, - }); - - expect(reducer(state, action).get('unread')).toEqual(0); - }); - }); -}); diff --git a/app/soapbox/reducers/__tests__/search-test.js b/app/soapbox/reducers/__tests__/search-test.js deleted file mode 100644 index 497f5b08c..000000000 --- a/app/soapbox/reducers/__tests__/search-test.js +++ /dev/null @@ -1,131 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable'; - -import { - SEARCH_CHANGE, - SEARCH_CLEAR, - SEARCH_EXPAND_SUCCESS, -} from 'soapbox/actions/search'; - -import reducer from '../search'; - -describe('search reducer', () => { - it('should return the initial state', () => { - expect(reducer(undefined, {}).toJS()).toEqual({ - value: '', - submitted: false, - submittedValue: '', - hidden: false, - results: { - accounts: [], - statuses: [], - hashtags: [], - accountsHasMore: false, - statusesHasMore: false, - hashtagsHasMore: false, - accountsLoaded: false, - statusesLoaded: false, - hashtagsLoaded: false, - }, - filter: 'accounts', - accountId: null, - }); - }); - - describe('SEARCH_CHANGE', () => { - it('sets the value', () => { - const state = ImmutableMap({ value: 'hell' }); - const action = { type: SEARCH_CHANGE, value: 'hello' }; - expect(reducer(state, action).get('value')).toEqual('hello'); - }); - }); - - describe('SEARCH_CLEAR', () => { - it('resets the state', () => { - const state = ImmutableRecord({ - value: 'hello world', - submitted: true, - submittedValue: 'hello world', - hidden: false, - results: ImmutableRecord({})(), - filter: 'statuses', - })(); - - const action = { type: SEARCH_CLEAR }; - - const expected = { - value: '', - submitted: false, - submittedValue: '', - hidden: false, - results: { - accounts: [], - statuses: [], - hashtags: [], - accountsHasMore: false, - statusesHasMore: false, - hashtagsHasMore: false, - accountsLoaded: false, - statusesLoaded: false, - hashtagsLoaded: false, - }, - filter: 'accounts', - accountId: null, - }; - - expect(reducer(state, action).toJS()).toEqual(expected); - }); - }); - - describe(SEARCH_EXPAND_SUCCESS, () => { - it('imports hashtags as maps', () => { - const state = ImmutableRecord({ - value: 'artist', - submitted: true, - submittedValue: 'artist', - hidden: false, - results: ImmutableRecord({ - hashtags: ImmutableList(), - hashtagsHasMore: false, - hashtagsLoaded: true, - })(), - filter: 'hashtags', - })(); - - const action = { - type: SEARCH_EXPAND_SUCCESS, - results: { - accounts: [], - statuses: [], - hashtags: [{ - name: 'artist', - url: 'https://gleasonator.com/tags/artist', - history: [], - }], - }, - searchTerm: 'artist', - searchType: 'hashtags', - }; - - const expected = { - value: 'artist', - submitted: true, - submittedValue: 'artist', - hidden: false, - results: { - hashtags: [ - { - name: 'artist', - url: 'https://gleasonator.com/tags/artist', - history: [], - }, - ], - hashtagsHasMore: false, - hashtagsLoaded: true, - }, - filter: 'hashtags', - }; - - expect(reducer(state, action).toJS()).toEqual(expected); - }); - }); -}); diff --git a/app/soapbox/reducers/group_editor.js b/app/soapbox/reducers/group_editor.js deleted file mode 100644 index 109182b1f..000000000 --- a/app/soapbox/reducers/group_editor.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; - -import { - GROUP_CREATE_REQUEST, - GROUP_CREATE_FAIL, - GROUP_CREATE_SUCCESS, - GROUP_UPDATE_REQUEST, - GROUP_UPDATE_FAIL, - GROUP_UPDATE_SUCCESS, - GROUP_EDITOR_RESET, - GROUP_EDITOR_SETUP, - GROUP_EDITOR_VALUE_CHANGE, -} from '../actions/group_editor'; - -const initialState = ImmutableMap({ - groupId: null, - isSubmitting: false, - isChanged: false, - title: '', - description: '', - coverImage: null, -}); - -export default function groupEditorReducer(state = initialState, action) { - switch (action.type) { - case GROUP_EDITOR_RESET: - return initialState; - case GROUP_EDITOR_SETUP: - return state.withMutations(map => { - map.set('groupId', action.group.get('id')); - map.set('title', action.group.get('title')); - map.set('description', action.group.get('description')); - map.set('isSubmitting', false); - }); - case GROUP_EDITOR_VALUE_CHANGE: - return state.withMutations(map => { - map.set(action.field, action.value); - map.set('isChanged', true); - }); - case GROUP_CREATE_REQUEST: - case GROUP_UPDATE_REQUEST: - return state.withMutations(map => { - map.set('isSubmitting', true); - map.set('isChanged', false); - }); - case GROUP_CREATE_FAIL: - case GROUP_UPDATE_FAIL: - return state.set('isSubmitting', false); - case GROUP_CREATE_SUCCESS: - case GROUP_UPDATE_SUCCESS: - return state.withMutations(map => { - map.set('isSubmitting', false); - map.set('groupId', action.group.id); - }); - default: - return state; - } -} diff --git a/app/soapbox/reducers/group_lists.js b/app/soapbox/reducers/group_lists.js deleted file mode 100644 index 25b62648c..000000000 --- a/app/soapbox/reducers/group_lists.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; - -import { GROUPS_FETCH_SUCCESS } from '../actions/groups'; - -const initialState = ImmutableMap({ - featured: ImmutableList(), - member: ImmutableList(), - admin: ImmutableList(), -}); - -const normalizeList = (state, type, id, groups) => { - return state.set(type, ImmutableList(groups.map(item => item.id))); -}; - -export default function groupLists(state = initialState, action) { - switch (action.type) { - case GROUPS_FETCH_SUCCESS: - return normalizeList(state, action.tab, action.id, action.groups); - default: - return state; - } -} diff --git a/app/soapbox/reducers/group_relationships.js b/app/soapbox/reducers/group_relationships.js deleted file mode 100644 index f74fdb02e..000000000 --- a/app/soapbox/reducers/group_relationships.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { GROUP_RELATIONSHIPS_FETCH_SUCCESS, GROUP_JOIN_SUCCESS, GROUP_LEAVE_SUCCESS } from '../actions/groups'; - -const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); - -const normalizeRelationships = (state, relationships) => { - relationships.forEach(relationship => { - state = normalizeRelationship(state, relationship); - }); - - return state; -}; - -const initialState = ImmutableMap(); - -export default function group_relationships(state = initialState, action) { - switch (action.type) { - case GROUP_JOIN_SUCCESS: - case GROUP_LEAVE_SUCCESS: - return normalizeRelationship(state, action.relationship); - case GROUP_RELATIONSHIPS_FETCH_SUCCESS: - return normalizeRelationships(state, action.relationships); - default: - return state; - } -} diff --git a/app/soapbox/reducers/groups.js b/app/soapbox/reducers/groups.js deleted file mode 100644 index 8e301e468..000000000 --- a/app/soapbox/reducers/groups.js +++ /dev/null @@ -1,34 +0,0 @@ -import { Map as ImmutableMap, fromJS } from 'immutable'; - -import { GROUP_UPDATE_SUCCESS } from '../actions/group_editor'; -import { - GROUP_FETCH_SUCCESS, - GROUP_FETCH_FAIL, - GROUPS_FETCH_SUCCESS, -} from '../actions/groups'; - -const initialState = ImmutableMap(); - -const normalizeGroup = (state, group) => state.set(group.id, fromJS(group)); - -const normalizeGroups = (state, groups) => { - groups.forEach(group => { - state = normalizeGroup(state, group); - }); - - return state; -}; - -export default function groups(state = initialState, action) { - switch (action.type) { - case GROUP_FETCH_SUCCESS: - case GROUP_UPDATE_SUCCESS: - return normalizeGroup(state, action.group); - case GROUPS_FETCH_SUCCESS: - return normalizeGroups(state, action.groups); - case GROUP_FETCH_FAIL: - return state.set(action.id, false); - default: - return state; - } -} diff --git a/app/soapbox/reducers/index.ts b/app/soapbox/reducers/index.ts index 87750381b..8c59c014f 100644 --- a/app/soapbox/reducers/index.ts +++ b/app/soapbox/reducers/index.ts @@ -25,10 +25,6 @@ import custom_emojis from './custom_emojis'; import domain_lists from './domain_lists'; import dropdown_menu from './dropdown_menu'; import filters from './filters'; -import group_editor from './group_editor'; -import group_lists from './group_lists'; -import group_relationships from './group_relationships'; -import groups from './groups'; import history from './history'; import instance from './instance'; import listAdder from './list_adder'; @@ -95,10 +91,6 @@ const reducers = { suggestions, polls, trends, - groups, - group_relationships, - group_lists, - group_editor, sidebar, patron, soapbox, diff --git a/app/soapbox/reducers/timelines.ts b/app/soapbox/reducers/timelines.ts index def257407..97975d658 100644 --- a/app/soapbox/reducers/timelines.ts +++ b/app/soapbox/reducers/timelines.ts @@ -12,7 +12,6 @@ import { ACCOUNT_MUTE_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, } from '../actions/accounts'; -import { GROUP_REMOVE_STATUS_SUCCESS } from '../actions/groups'; import { STATUS_CREATE_REQUEST, STATUS_CREATE_SUCCESS, @@ -210,10 +209,6 @@ const filterTimelines = (state: State, relationship: APIEntity, statuses: Immuta }); }; -const removeStatusFromGroup = (state: State, groupId: string, statusId: string) => { - return state.updateIn([`group:${groupId}`, 'items'], ImmutableOrderedSet(), ids => (ids as ImmutableOrderedSet).delete(statusId)); -}; - const timelineDequeue = (state: State, timelineId: string) => { const top = state.getIn([timelineId, 'top']); @@ -348,8 +343,6 @@ export default function timelines(state: State = initialState, action: AnyAction return timelineConnect(state, action.timeline); case TIMELINE_DISCONNECT: return timelineDisconnect(state, action.timeline); - case GROUP_REMOVE_STATUS_SUCCESS: - return removeStatusFromGroup(state, action.groupId, action.id); case TIMELINE_REPLACE: return state .update('home', TimelineRecord(), timeline => timeline.withMutations(timeline => { diff --git a/app/soapbox/reducers/user_lists.ts b/app/soapbox/reducers/user_lists.ts index 38017f0bb..4c84f1836 100644 --- a/app/soapbox/reducers/user_lists.ts +++ b/app/soapbox/reducers/user_lists.ts @@ -32,13 +32,6 @@ import { import { FAMILIAR_FOLLOWERS_FETCH_SUCCESS, } from '../actions/familiar_followers'; -import { - GROUP_MEMBERS_FETCH_SUCCESS, - GROUP_MEMBERS_EXPAND_SUCCESS, - GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS, - GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS, - GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS, -} from '../actions/groups'; import { REBLOGS_FETCH_SUCCESS, FAVOURITES_FETCH_SUCCESS, @@ -82,8 +75,6 @@ export const ReducerRecord = ImmutableRecord({ blocks: ListRecord(), mutes: ListRecord(), directory: ListRecord({ isLoading: true }), - groups: ImmutableMap(), - groups_removed_accounts: ImmutableMap(), pinned: ImmutableMap(), birthday_reminders: ImmutableMap(), familiar_followers: ImmutableMap(), @@ -94,7 +85,7 @@ export type List = ReturnType; type Reaction = ReturnType; type ReactionList = ReturnType; type Items = ImmutableOrderedSet; -type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'groups' | 'groups_removed_accounts' | 'pinned' | 'birthday_reminders' | 'familiar_followers', string]; +type NestedListPath = ['followers' | 'following' | 'reblogged_by' | 'favourited_by' | 'reactions' | 'pinned' | 'birthday_reminders' | 'familiar_followers', string]; type ListPath = ['follow_requests' | 'blocks' | 'mutes' | 'directory']; const normalizeList = (state: State, path: NestedListPath | ListPath, accounts: APIEntity[], next?: string | null) => { @@ -170,16 +161,6 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) { case DIRECTORY_FETCH_FAIL: case DIRECTORY_EXPAND_FAIL: return state.setIn(['directory', 'isLoading'], false); - case GROUP_MEMBERS_FETCH_SUCCESS: - return normalizeList(state, ['groups', action.id], action.accounts, action.next); - case GROUP_MEMBERS_EXPAND_SUCCESS: - return appendToList(state, ['groups', action.id], action.accounts, action.next); - case GROUP_REMOVED_ACCOUNTS_FETCH_SUCCESS: - return normalizeList(state, ['groups_removed_accounts', action.id], action.accounts, action.next); - case GROUP_REMOVED_ACCOUNTS_EXPAND_SUCCESS: - return appendToList(state, ['groups_removed_accounts', action.id], action.accounts, action.next); - case GROUP_REMOVED_ACCOUNTS_REMOVE_SUCCESS: - return removeFromList(state, ['groups_removed_accounts', action.groupId], action.id); case PINNED_ACCOUNTS_FETCH_SUCCESS: return normalizeList(state, ['pinned', action.id], action.accounts, action.next); case BIRTHDAY_REMINDERS_FETCH_SUCCESS: From fd6899b6cba22bc67b0198aa11408dc03ea992b8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 16:20:20 -0500 Subject: [PATCH 23/48] Fix reducers/user_lists test --- app/soapbox/reducers/__tests__/user_lists.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/reducers/__tests__/user_lists.test.ts b/app/soapbox/reducers/__tests__/user_lists.test.ts index 5fbd5f1ea..fa23f845f 100644 --- a/app/soapbox/reducers/__tests__/user_lists.test.ts +++ b/app/soapbox/reducers/__tests__/user_lists.test.ts @@ -14,8 +14,6 @@ describe('user_lists reducer', () => { blocks: { next: null, items: ImmutableOrderedSet(), isLoading: false }, mutes: { next: null, items: ImmutableOrderedSet(), isLoading: false }, directory: { next: null, items: ImmutableOrderedSet(), isLoading: true }, - groups: {}, - groups_removed_accounts: {}, pinned: {}, birthday_reminders: {}, familiar_followers: {}, From 66cd92970bbcab62ee006005f2f7b34ff90a7b5a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 18:48:10 -0500 Subject: [PATCH 24/48] Developers: add Service Worker debug page --- app/soapbox/components/error_boundary.tsx | 11 +- .../developers/components/indicator.tsx | 24 +++ .../features/developers/developers-menu.tsx | 8 + .../developers/service-worker-info.tsx | 140 ++++++++++++++++++ app/soapbox/features/ui/index.tsx | 2 + .../features/ui/util/async-components.ts | 4 + app/soapbox/utils/sw.ts | 15 ++ package.json | 2 +- yarn.lock | 8 +- 9 files changed, 199 insertions(+), 15 deletions(-) create mode 100644 app/soapbox/features/developers/components/indicator.tsx create mode 100644 app/soapbox/features/developers/service-worker-info.tsx create mode 100644 app/soapbox/utils/sw.ts diff --git a/app/soapbox/components/error_boundary.tsx b/app/soapbox/components/error_boundary.tsx index 436b40134..76adf9728 100644 --- a/app/soapbox/components/error_boundary.tsx +++ b/app/soapbox/components/error_boundary.tsx @@ -8,6 +8,7 @@ import { Text, Stack } from 'soapbox/components/ui'; import { captureException } from 'soapbox/monitoring'; import KVStore from 'soapbox/storage/kv_store'; import sourceCode from 'soapbox/utils/code'; +import { unregisterSw } from 'soapbox/utils/sw'; import SiteLogo from './site-logo'; @@ -15,16 +16,6 @@ import type { RootState } from 'soapbox/store'; const goHome = () => location.href = '/'; -/** Unregister the ServiceWorker */ -// https://stackoverflow.com/a/49771828/8811886 -const unregisterSw = async(): Promise => { - if (navigator.serviceWorker) { - const registrations = await navigator.serviceWorker.getRegistrations(); - const unregisterAll = registrations.map(r => r.unregister()); - await Promise.all(unregisterAll); - } -}; - const mapStateToProps = (state: RootState) => { const { links, logo } = getSoapboxConfig(state); diff --git a/app/soapbox/features/developers/components/indicator.tsx b/app/soapbox/features/developers/components/indicator.tsx new file mode 100644 index 000000000..5cb7763b5 --- /dev/null +++ b/app/soapbox/features/developers/components/indicator.tsx @@ -0,0 +1,24 @@ +import classNames from 'clsx'; +import React from 'react'; + +interface IIndicator { + state?: 'active' | 'pending' | 'error' | 'inactive', + size?: 'sm', +} + +/** Indicator dot component. */ +const Indicator: React.FC = ({ state = 'inactive', size = 'sm' }) => { + return ( +
    + ); +}; + +export default Indicator; \ No newline at end of file diff --git a/app/soapbox/features/developers/developers-menu.tsx b/app/soapbox/features/developers/developers-menu.tsx index 76320e530..a94eb09a6 100644 --- a/app/soapbox/features/developers/developers-menu.tsx +++ b/app/soapbox/features/developers/developers-menu.tsx @@ -89,6 +89,14 @@ const Developers: React.FC = () => { + + + + + + + + diff --git a/app/soapbox/features/developers/service-worker-info.tsx b/app/soapbox/features/developers/service-worker-info.tsx new file mode 100644 index 000000000..18800f1de --- /dev/null +++ b/app/soapbox/features/developers/service-worker-info.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useState } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import List, { ListItem } from 'soapbox/components/list'; +import { HStack, Text, Column, FormActions, Button, Stack, Icon } from 'soapbox/components/ui'; +import { unregisterSw } from 'soapbox/utils/sw'; + +import Indicator from './components/indicator'; + +const messages = defineMessages({ + heading: { id: 'column.developers.service_worker', defaultMessage: 'Service Worker' }, + status: { id: 'sw.status', defaultMessage: 'Status' }, + url: { id: 'sw.url', defaultMessage: 'Script URL' }, +}); + +/** Hook that returns the active ServiceWorker registration. */ +const useRegistration = () => { + const [isLoading, setLoading] = useState(true); + const [registration, setRegistration] = useState(); + + const isSupported = 'serviceWorker' in navigator; + + useEffect(() => { + if (isSupported) { + navigator.serviceWorker.getRegistration() + .then(r => { + setRegistration(r); + setLoading(false); + }) + .catch(() => setLoading(false)); + } else { + setLoading(false); + } + }, []); + + return { + isLoading, + registration, + }; +}; + +interface IServiceWorkerInfo { +} + +/** Mini ServiceWorker debugging component. */ +const ServiceWorkerInfo: React.FC = () => { + const intl = useIntl(); + const { isLoading, registration } = useRegistration(); + + const url = registration?.active?.scriptURL; + + const getState = () => { + if (registration?.active) { + return 'active'; + } else if (registration?.waiting) { + return 'pending'; + } else { + return 'inactive'; + } + }; + + const getMessage = () => { + if (isLoading) { + return ( + + ); + } else if (!isLoading && !registration) { + return ( + + ); + } else if (registration?.active) { + return ( + + ); + } else if (registration?.waiting) { + return ( + + ); + } else { + return ( + + ); + } + }; + + const handleRestart = async() => { + await unregisterSw(); + window.location.reload(); + }; + + return ( + + + + + + + {getMessage()} + + + + {url && ( + + + {url} + + + + )} + + + + + + + + ); +}; + +export default ServiceWorkerInfo; \ No newline at end of file diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 08d96e920..973a2187c 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -112,6 +112,7 @@ import { TestTimeline, LogoutPage, AuthTokenList, + ServiceWorkerInfo, } from './util/async-components'; import { WrappedRoute } from './util/react_router_helpers'; @@ -311,6 +312,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { + new Promise((_resolve, reject) => reject())} content={children} /> diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index f416c859a..773eed795 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -470,6 +470,10 @@ export function TestTimeline() { return import(/* webpackChunkName: "features/test_timeline" */'../../test_timeline'); } +export function ServiceWorkerInfo() { + return import(/* webpackChunkName: "features/developers" */'../../developers/service-worker-info'); +} + export function DatePicker() { return import(/* webpackChunkName: "date_picker" */'../../birthdays/date_picker'); } diff --git a/app/soapbox/utils/sw.ts b/app/soapbox/utils/sw.ts new file mode 100644 index 000000000..910d7189a --- /dev/null +++ b/app/soapbox/utils/sw.ts @@ -0,0 +1,15 @@ +/** Unregister the ServiceWorker */ +// https://stackoverflow.com/a/49771828/8811886 +const unregisterSw = async(): Promise => { + if (navigator.serviceWorker) { + // FIXME: this only works if using a single tab. + // Send a message to sw.js instead to refresh all tabs. + const registrations = await navigator.serviceWorker.getRegistrations(); + const unregisterAll = registrations.map(r => r.unregister()); + await Promise.all(unregisterAll); + } +}; + +export { + unregisterSw, +}; \ No newline at end of file diff --git a/package.json b/package.json index 2d25a828b..a41d9fae2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@sentry/browser": "^7.11.1", "@sentry/react": "^7.11.1", "@sentry/tracing": "^7.11.1", - "@tabler/icons": "^1.73.0", + "@tabler/icons": "^1.109.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.7", "@tanstack/react-query": "^4.0.10", diff --git a/yarn.lock b/yarn.lock index 423516f95..c7f1b6e8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2290,10 +2290,10 @@ remark "^13.0.0" unist-util-find-all-after "^3.0.2" -"@tabler/icons@^1.73.0": - version "1.73.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.73.0.tgz#26d81858baf41be939504e1f9b4b32835eda6fdb" - integrity sha512-MhAHFzVj79ZWlAIRD++7Mk55PZsdlEdkfkjO3DD257mqj8iJZQRAQtkx2UFJXVs2mMrcOUu1qtj4rlVC8BfnKA== +"@tabler/icons@^1.109.0": + version "1.109.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.109.0.tgz#11626c3fc097f2f70c4c197e4b9909fb05380752" + integrity sha512-B0YetE4pB6HY2Wa57v/LJ3NgkJzKYPze4U0DurIqPoKSptatKv2ga76FZSkO6EUpkYfHMtGPM6QjpJljfuCmAQ== "@tailwindcss/forms@^0.5.3": version "0.5.3" From d385c0d80c744505ee82dc13970856276c9dfcc8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 19:00:15 -0500 Subject: [PATCH 25/48] Developers, SW: check 'waiting' before 'active' --- .../developers/service-worker-info.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/soapbox/features/developers/service-worker-info.tsx b/app/soapbox/features/developers/service-worker-info.tsx index 18800f1de..9b7751587 100644 --- a/app/soapbox/features/developers/service-worker-info.tsx +++ b/app/soapbox/features/developers/service-worker-info.tsx @@ -50,10 +50,10 @@ const ServiceWorkerInfo: React.FC = () => { const url = registration?.active?.scriptURL; const getState = () => { - if (registration?.active) { - return 'active'; - } else if (registration?.waiting) { + if (registration?.waiting) { return 'pending'; + } else if (registration?.active) { + return 'active'; } else { return 'inactive'; } @@ -74,13 +74,6 @@ const ServiceWorkerInfo: React.FC = () => { defaultMessage='Unavailable' /> ); - } else if (registration?.active) { - return ( - - ); } else if (registration?.waiting) { return ( = () => { defaultMessage='Waiting' /> ); + } else if (registration?.active) { + return ( + + ); } else { return ( Date: Thu, 3 Nov 2022 19:36:14 -0500 Subject: [PATCH 26/48] Don't check react-notification into the repo --- app/soapbox/actions/alerts.ts | 2 +- .../ui/containers/notifications_container.tsx | 2 +- .../react-notification/defaultPropTypes.js | 31 ---- app/soapbox/react-notification/index.d.ts | 88 --------- app/soapbox/react-notification/index.js | 2 - .../react-notification/notification.js | 175 ------------------ .../react-notification/notificationStack.js | 95 ---------- .../react-notification/stackedNotification.js | 69 ------- package.json | 1 + yarn.lock | 7 + 10 files changed, 10 insertions(+), 462 deletions(-) delete mode 100644 app/soapbox/react-notification/defaultPropTypes.js delete mode 100644 app/soapbox/react-notification/index.d.ts delete mode 100644 app/soapbox/react-notification/index.js delete mode 100644 app/soapbox/react-notification/notification.js delete mode 100644 app/soapbox/react-notification/notificationStack.js delete mode 100644 app/soapbox/react-notification/stackedNotification.js diff --git a/app/soapbox/actions/alerts.ts b/app/soapbox/actions/alerts.ts index 3e5aed4b3..8f200563a 100644 --- a/app/soapbox/actions/alerts.ts +++ b/app/soapbox/actions/alerts.ts @@ -5,7 +5,7 @@ import { httpErrorMessages } from 'soapbox/utils/errors'; import type { SnackbarActionSeverity } from './snackbar'; import type { AnyAction } from '@reduxjs/toolkit'; import type { AxiosError } from 'axios'; -import type { NotificationObject } from 'soapbox/react-notification'; +import type { NotificationObject } from 'react-notification'; const messages = defineMessages({ unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' }, diff --git a/app/soapbox/features/ui/containers/notifications_container.tsx b/app/soapbox/features/ui/containers/notifications_container.tsx index 79d8724c1..2119228bf 100644 --- a/app/soapbox/features/ui/containers/notifications_container.tsx +++ b/app/soapbox/features/ui/containers/notifications_container.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { useIntl, MessageDescriptor } from 'react-intl'; +import { NotificationStack, NotificationObject, StyleFactoryFn } from 'react-notification'; import { useHistory } from 'react-router-dom'; import { dismissAlert } from 'soapbox/actions/alerts'; import { Button } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; -import { NotificationStack, NotificationObject, StyleFactoryFn } from 'soapbox/react-notification'; import type { Alert } from 'soapbox/reducers/alerts'; diff --git a/app/soapbox/react-notification/defaultPropTypes.js b/app/soapbox/react-notification/defaultPropTypes.js deleted file mode 100644 index 1a3dd9d4e..000000000 --- a/app/soapbox/react-notification/defaultPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -export default { - message: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]).isRequired, - action: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - PropTypes.node, - ]), - onClick: PropTypes.func, - style: PropTypes.bool, - actionStyle: PropTypes.object, - titleStyle: PropTypes.object, - barStyle: PropTypes.object, - activeBarStyle: PropTypes.object, - dismissAfter: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.number, - ]), - onDismiss: PropTypes.func, - className: PropTypes.string, - activeClassName: PropTypes.string, - isActive: PropTypes.bool, - title: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.node, - ]), -}; diff --git a/app/soapbox/react-notification/index.d.ts b/app/soapbox/react-notification/index.d.ts deleted file mode 100644 index 22f0211c8..000000000 --- a/app/soapbox/react-notification/index.d.ts +++ /dev/null @@ -1,88 +0,0 @@ -declare module 'soapbox/react-notification' { - import { Component, ReactElement } from 'react'; - - interface StyleFactoryFn { - (index: number, style: object | void, notification: NotificationProps): object; - } - - interface OnClickNotificationProps { - /** - * Callback function to run when the action is clicked. - * @param notification Notification currently being clicked - * @param deactivate Function that can be called to set the notification to inactive. - * Used to activate notification exit animation on click. - */ - onClick?(notification: NotificationProps, deactivate: () => void): void; - } - - interface NotificationProps extends OnClickNotificationProps { - /** The name of the action, e.g., "close" or "undo". */ - action?: string; - /** Custom action styles. */ - actionStyle?: object; - /** Custom snackbar styles when the bar is active. */ - activeBarStyle?: object; - /** - * Custom class to apply to the top-level component when active. - * @default 'notification-bar-active' - */ - activeClassName?: string; - /** Custom snackbar styles. */ - barStyle?: object; - /** Custom class to apply to the top-level component. */ - className?: string; - /** - * Timeout for onDismiss event. - * @default 2000 - */ - dismissAfter?: boolean | number; - /** - * If true, the notification is visible. - * @default false - */ - isActive?: boolean; - /** The message or component for the notification. */ - message: string | ReactElement; - /** Setting this prop to `false` will disable all inline styles. */ - style?: boolean; - /** The title for the notification. */ - title?: string | ReactElement; - /** Custom title styles. */ - titleStyle?: object; - - /** - * Callback function to run when dismissAfter timer runs out - * @param notification Notification currently being dismissed. - */ - onDismiss?(notification: NotificationProps): void; - } - - interface NotificationStackProps extends OnClickNotificationProps { - /** Create the style of the actions. */ - actionStyleFactory?: StyleFactoryFn; - /** Create the style of the active notification. */ - activeBarStyleFactory?: StyleFactoryFn; - /** Create the style of the notification. */ - barStyleFactory?: StyleFactoryFn; - /** - * If false, notification dismiss timers start immediately. - * @default true - */ - dismissInOrder?: boolean; - /** Array of notifications to render. */ - notifications: NotificationObject[]; - /** - * Callback function to run when dismissAfter timer runs out - * @param notification Notification currently being dismissed. - */ - onDismiss?(notification: NotificationObject): void; - } - - export interface NotificationObject extends NotificationProps { - key: number | string; - } - - export class Notification extends Component {} - - export class NotificationStack extends Component {} -} diff --git a/app/soapbox/react-notification/index.js b/app/soapbox/react-notification/index.js deleted file mode 100644 index 3d7da7cee..000000000 --- a/app/soapbox/react-notification/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Notification } from './notification'; -export { default as NotificationStack } from './notificationStack'; diff --git a/app/soapbox/react-notification/notification.js b/app/soapbox/react-notification/notification.js deleted file mode 100644 index ab1cddf9b..000000000 --- a/app/soapbox/react-notification/notification.js +++ /dev/null @@ -1,175 +0,0 @@ -/* linting temp disabled while working on updates */ -/* eslint-disable */ -import React, { Component } from 'react'; -import defaultPropTypes from './defaultPropTypes'; - -class Notification extends Component { - constructor(props) { - super(props); - - this.getBarStyle = this.getBarStyle.bind(this); - this.getActionStyle = this.getActionStyle.bind(this); - this.getTitleStyle = this.getTitleStyle.bind(this); - this.handleClick = this.handleClick.bind(this); - - if (props.onDismiss && props.isActive) { - this.dismissTimeout = setTimeout( - props.onDismiss, - props.dismissAfter - ); - } - } - - componentWillReceiveProps(nextProps) { - if (nextProps.dismissAfter === false) return; - - // See http://eslint.org/docs/rules/no-prototype-builtins - if (!{}.hasOwnProperty.call(nextProps, 'isLast')) { - clearTimeout(this.dismissTimeout); - } - - if (nextProps.onDismiss) { - if ( - (nextProps.isActive && !this.props.isActive) || - (nextProps.dismissAfter && this.props.dismissAfter === false) - ) { - this.dismissTimeout = setTimeout( - nextProps.onDismiss, - nextProps.dismissAfter - ); - } - } - } - - componentWillUnmount() { - if (this.props.dismissAfter) clearTimeout(this.dismissTimeout); - } - - /* - * @description Dynamically get the styles for the bar. - * @returns {object} result The style. - */ - getBarStyle() { - if (this.props.style === false) return {}; - - const { isActive, barStyle, activeBarStyle } = this.props; - - const baseStyle = { - position: 'fixed', - bottom: '2rem', - left: '-100%', - width: 'auto', - padding: '1rem', - margin: 0, - color: '#fafafa', - font: '1rem normal Roboto, sans-serif', - borderRadius: '5px', - background: '#212121', - borderSizing: 'border-box', - boxShadow: '0 0 1px 1px rgba(10, 10, 11, .125)', - cursor: 'default', - WebKitTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - MozTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - msTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - OTransition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - transition: '.5s cubic-bezier(0.89, 0.01, 0.5, 1.1)', - WebkitTransform: 'translatez(0)', - MozTransform: 'translatez(0)', - msTransform: 'translatez(0)', - OTransform: 'translatez(0)', - transform: 'translatez(0)' - }; - - return isActive ? - Object.assign({}, baseStyle, { left: '1rem' }, barStyle, activeBarStyle) : - Object.assign({}, baseStyle, barStyle); - } - - /* - * @function getActionStyle - * @description Dynamically get the styles for the action text. - * @returns {object} result The style. - */ - getActionStyle() { - return this.props.style !== false ? Object.assign({}, { - padding: '0.125rem', - marginLeft: '1rem', - color: '#f44336', - font: '.75rem normal Roboto, sans-serif', - lineHeight: '1rem', - letterSpacing: '.125ex', - textTransform: 'uppercase', - borderRadius: '5px', - cursor: 'pointer' - }, this.props.actionStyle) : {}; - } - - /* - * @function getTitleStyle - * @description Dynamically get the styles for the title. - * @returns {object} result The style. - */ - getTitleStyle() { - return this.props.style !== false ? Object.assign({}, { - fontWeight: '700', - marginRight: '.5rem' - }, this.props.titleStyle) : {}; - } - - /* - * @function handleClick - * @description Handle click events on the action button. - */ - handleClick() { - if (this.props.onClick && typeof this.props.onClick === 'function') { - return this.props.onClick(); - } - } - - render() { - let className = 'notification-bar'; - - if (this.props.isActive) className += ` ${this.props.activeClassName}`; - if (this.props.className) className += ` ${this.props.className}`; - - return ( -
    -
    - {this.props.title ? ( - - {this.props.title} - - ) : null} - - {/* eslint-disable */} - - {this.props.message} - - - {this.props.action ? ( - - {this.props.action} - - ) : null} -
    -
    - ); - } -} - -Notification.propTypes = defaultPropTypes; - -Notification.defaultProps = { - isActive: false, - dismissAfter: 2000, - activeClassName: 'notification-bar-active' -}; - -export default Notification; diff --git a/app/soapbox/react-notification/notificationStack.js b/app/soapbox/react-notification/notificationStack.js deleted file mode 100644 index dc9c2459b..000000000 --- a/app/soapbox/react-notification/notificationStack.js +++ /dev/null @@ -1,95 +0,0 @@ -/* linting temp disabled while working on updates */ -/* eslint-disable */ -import React from 'react'; -import PropTypes from 'prop-types'; -import StackedNotification from './stackedNotification'; -import defaultPropTypes from './defaultPropTypes'; - -function defaultBarStyleFactory(index, style) { - return Object.assign( - {}, - style, - { bottom: `${2 + (index * 4)}rem` } - ); -} - -function defaultActionStyleFactory(index, style) { - return Object.assign( - {}, - style, - {} - ); -} - -/** -* The notification list does not have any state, so use a -* pure function here. It just needs to return the stacked array -* of notification components. -*/ -const NotificationStack = props => ( -
    - {props.notifications.map((notification, index) => { - const isLast = index === 0 && props.notifications.length === 1; - const dismissNow = isLast || !props.dismissInOrder; - - // Handle styles - const barStyle = props.barStyleFactory(index, notification.barStyle, notification); - const actionStyle = props.actionStyleFactory(index, notification.actionStyle, notification); - const activeBarStyle = props.activeBarStyleFactory( - index, - notification.activeBarStyle, - notification - ); - - // Allow onClick from notification stack or individual notifications - const onClick = notification.onClick || props.onClick; - const onDismiss = props.onDismiss; - - let { dismissAfter } = notification; - - if (dismissAfter !== false) { - if (dismissAfter == null) dismissAfter = props.dismissAfter; - if (!dismissNow) dismissAfter += index * 1000; - } - - return ( - - ); - })} -
    -); - -/* eslint-disable react/no-unused-prop-types, react/forbid-prop-types */ -NotificationStack.propTypes = { - activeBarStyleFactory: PropTypes.func, - barStyleFactory: PropTypes.func, - actionStyleFactory: PropTypes.func, - dismissInOrder: PropTypes.bool, - notifications: PropTypes.array.isRequired, - onDismiss: PropTypes.func.isRequired, - onClick: PropTypes.func, - action: defaultPropTypes.action -}; - -NotificationStack.defaultProps = { - activeBarStyleFactory: defaultBarStyleFactory, - barStyleFactory: defaultBarStyleFactory, - actionStyleFactory: defaultActionStyleFactory, - dismissInOrder: true, - dismissAfter: 1000, - onClick: () => {} -}; -/* eslint-enable no-alert, no-console */ - -export default NotificationStack; diff --git a/app/soapbox/react-notification/stackedNotification.js b/app/soapbox/react-notification/stackedNotification.js deleted file mode 100644 index c8d7200d4..000000000 --- a/app/soapbox/react-notification/stackedNotification.js +++ /dev/null @@ -1,69 +0,0 @@ -/* linting temp disabled while working on updates */ -/* eslint-disable */ -import React, { Component } from 'react'; -import defaultPropTypes from './defaultPropTypes'; -import Notification from './notification'; - -class StackedNotification extends Component { - constructor(props) { - super(props); - - this.state = { - isActive: false - }; - - this.handleClick = this.handleClick.bind(this); - } - - componentDidMount() { - this.activeTimeout = setTimeout(this.setState.bind(this, { - isActive: true - }), 1); - - this.dismiss(this.props.dismissAfter); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.dismissAfter !== this.props.dismissAfter) { - this.dismiss(nextProps.dismissAfter); - } - } - - componentWillUnmount() { - clearTimeout(this.activeTimeout); - clearTimeout(this.dismissTimeout); - } - - dismiss(dismissAfter) { - if (dismissAfter === false) return; - - this.dismissTimeout = setTimeout(this.setState.bind(this, { - isActive: false - }), dismissAfter); - } - - /* - * @function handleClick - * @description Bind deactivate Notification function to Notification click handler - */ - handleClick() { - if (this.props.onClick && typeof this.props.onClick === 'function') { - return this.props.onClick(this.setState.bind(this, { isActive: false })); - } - } - - render() { - return ( - setTimeout(this.props.onDismiss, 300)} - isActive={this.state.isActive} - /> - ); - } -} - -StackedNotification.propTypes = defaultPropTypes; - -export default StackedNotification; diff --git a/package.json b/package.json index 2d25a828b..feead919f 100644 --- a/package.json +++ b/package.json @@ -167,6 +167,7 @@ "react-inlinesvg": "^3.0.0", "react-intl": "^5.0.0", "react-motion": "^0.5.2", + "react-notification": "^6.8.5", "react-otp-input": "^2.4.0", "react-overlays": "^0.9.0", "react-popper": "^2.3.0", diff --git a/yarn.lock b/yarn.lock index 423516f95..88d812b0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9946,6 +9946,13 @@ react-motion@^0.5.2: prop-types "^15.5.8" raf "^3.1.0" +react-notification@^6.8.5: + version "6.8.5" + resolved "https://registry.yarnpkg.com/react-notification/-/react-notification-6.8.5.tgz#7ea90a633bb2a280d899e30c93cf372265cce4f0" + integrity sha512-3pJPhSsWNYizpyeMeWuC+jVthqE9WKqQ6rHq2naiiP4fLGN4irwL2Xp2Q8Qn7agW/e4BIDxarab6fJOUp1cKUw== + dependencies: + prop-types "^15.6.2" + react-onclickoutside@^6.12.0: version "6.12.1" resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz#92dddd28f55e483a1838c5c2930e051168c1e96b" From 29d320a658e9472719dec3596a524870e14d3aff Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 3 Nov 2022 19:56:03 -0500 Subject: [PATCH 27/48] Fix spacing around column headings, remove unused column settings --- app/soapbox/components/sub_navigation.tsx | 46 +--------------- .../components/column_settings.js | 51 ----------------- .../containers/column_settings_container.js | 18 ------ .../features/community_timeline/index.tsx | 7 ++- .../features/hashtag-timeline/index.tsx | 10 ++-- .../components/column_settings.js | 55 ------------------- .../containers/column_settings_container.js | 18 ------ .../features/public_timeline/index.tsx | 8 ++- 8 files changed, 17 insertions(+), 196 deletions(-) delete mode 100644 app/soapbox/features/community_timeline/components/column_settings.js delete mode 100644 app/soapbox/features/community_timeline/containers/column_settings_container.js delete mode 100644 app/soapbox/features/public_timeline/components/column_settings.js delete mode 100644 app/soapbox/features/public_timeline/containers/column_settings_container.js diff --git a/app/soapbox/components/sub_navigation.tsx b/app/soapbox/components/sub_navigation.tsx index b8e2b310d..1e6afb85a 100644 --- a/app/soapbox/components/sub_navigation.tsx +++ b/app/soapbox/components/sub_navigation.tsx @@ -4,34 +4,22 @@ import { defineMessages, useIntl } from 'react-intl'; // import { connect } from 'react-redux'; import { useHistory } from 'react-router-dom'; -// import { openModal } from 'soapbox/actions/modals'; -// import { useAppDispatch } from 'soapbox/hooks'; - import { CardHeader, CardTitle } from './ui'; const messages = defineMessages({ back: { id: 'column_back_button.label', defaultMessage: 'Back' }, - settings: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, }); interface ISubNavigation { - message: String, + message: React.ReactNode, + /** @deprecated Unused. */ settings?: React.ComponentType, } const SubNavigation: React.FC = ({ message }) => { const intl = useIntl(); - // const dispatch = useAppDispatch(); const history = useHistory(); - // const ref = useRef(null); - - // const [scrolled, setScrolled] = useState(false); - - // const onOpenSettings = () => { - // dispatch(openModal('COMPONENT', { component: Settings })); - // }; - const handleBackClick = () => { if (window.history && window.history.length === 1) { history.push('/'); @@ -40,36 +28,6 @@ const SubNavigation: React.FC = ({ message }) => { } }; - // const handleBackKeyUp = (e) => { - // if (e.key === 'Enter') { - // handleClick(); - // } - // } - - // const handleOpenSettings = () => { - // onOpenSettings(); - // } - - // useEffect(() => { - // const handleScroll = throttle(() => { - // if (this.node) { - // const { offsetTop } = this.node; - - // if (offsetTop > 0) { - // setScrolled(true); - // } else { - // setScrolled(false); - // } - // } - // }, 150, { trailing: true }); - - // window.addEventListener('scroll', handleScroll); - - // return () => { - // window.removeEventListener('scroll', handleScroll); - // }; - // }, []); - return ( -
    -

    - -

    -
    - -
    -
    - -
    -
    - } /> -
    - -
    - } /> -
    -
    -
    - ); - } - -} diff --git a/app/soapbox/features/community_timeline/containers/column_settings_container.js b/app/soapbox/features/community_timeline/containers/column_settings_container.js deleted file mode 100644 index d20838089..000000000 --- a/app/soapbox/features/community_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; - -import { getSettings, changeSetting } from '../../../actions/settings'; -import ColumnSettings from '../components/column_settings'; - -const mapStateToProps = state => ({ - settings: getSettings(state).get('community'), -}); - -const mapDispatchToProps = (dispatch) => { - return { - onChange(key, checked) { - dispatch(changeSetting(['community', ...key], checked)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/soapbox/features/community_timeline/index.tsx b/app/soapbox/features/community_timeline/index.tsx index 8c0adc2cb..4fd4e1d0a 100644 --- a/app/soapbox/features/community_timeline/index.tsx +++ b/app/soapbox/features/community_timeline/index.tsx @@ -10,8 +10,6 @@ import { useAppDispatch, useSettings } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; -import ColumnSettings from './containers/column_settings_container'; - const messages = defineMessages({ title: { id: 'column.community', defaultMessage: 'Local timeline' }, }); @@ -44,7 +42,10 @@ const CommunityTimeline = () => { return ( - +
    + +
    + = ({ params }) => { const tags = params?.tags || { any: [], all: [], none: [] }; const dispatch = useAppDispatch(); - const hasUnread = useAppSelector(state => (state.timelines.getIn([`hashtag:${id}`, 'unread']) as number) > 0); const disconnects = useRef<(() => void)[]>([]); // Mastodon supports displaying results from multiple hashtags. @@ -100,7 +99,10 @@ export const HashtagTimeline: React.FC = ({ params }) => { return ( - +
    + +
    + -
    -

    - -

    -
    - -
    -
    - -
    -
    - } /> -
    - -
    - } /> -
    - -
    - } /> -
    -
    -
    - ); - } - -} diff --git a/app/soapbox/features/public_timeline/containers/column_settings_container.js b/app/soapbox/features/public_timeline/containers/column_settings_container.js deleted file mode 100644 index 63a629007..000000000 --- a/app/soapbox/features/public_timeline/containers/column_settings_container.js +++ /dev/null @@ -1,18 +0,0 @@ -import { connect } from 'react-redux'; - -import { getSettings, changeSetting } from '../../../actions/settings'; -import ColumnSettings from '../components/column_settings'; - -const mapStateToProps = state => ({ - settings: getSettings(state).get('public'), -}); - -const mapDispatchToProps = (dispatch) => { - return { - onChange(key, checked) { - dispatch(changeSetting(['public', ...key], checked)); - }, - }; -}; - -export default connect(mapStateToProps, mapDispatchToProps)(ColumnSettings); diff --git a/app/soapbox/features/public_timeline/index.tsx b/app/soapbox/features/public_timeline/index.tsx index 6fe5afec8..34017a0d3 100644 --- a/app/soapbox/features/public_timeline/index.tsx +++ b/app/soapbox/features/public_timeline/index.tsx @@ -14,8 +14,6 @@ import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker'; import Timeline from '../ui/components/timeline'; -import ColumnSettings from './containers/column_settings_container'; - const messages = defineMessages({ title: { id: 'column.public', defaultMessage: 'Fediverse timeline' }, dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' }, @@ -65,8 +63,12 @@ const CommunityTimeline = () => { return ( - +
    + +
    + + {showExplanationBox &&
    } From cc76dcfe698ae3f32af98eddf93f56ba5baf9cc7 Mon Sep 17 00:00:00 2001 From: Fleetclip Date: Fri, 4 Nov 2022 10:17:12 +0000 Subject: [PATCH 28/48] translations: update zh-CN, zh-HK & zh-TW --- app/soapbox/locales/defaultMessages.json | 2380 +++++++++++++--------- app/soapbox/locales/zh-CN.json | 880 ++++---- app/soapbox/locales/zh-HK.json | 2055 ++++++++++--------- app/soapbox/locales/zh-TW.json | 1785 ++++++++-------- 4 files changed, 3817 insertions(+), 3283 deletions(-) diff --git a/app/soapbox/locales/defaultMessages.json b/app/soapbox/locales/defaultMessages.json index 2dd513e59..6d81a395c 100644 --- a/app/soapbox/locales/defaultMessages.json +++ b/app/soapbox/locales/defaultMessages.json @@ -48,6 +48,10 @@ "defaultMessage": "Video exceeds the current file size limit ({limit})", "id": "upload_error.video_size_limit" }, + { + "defaultMessage": "Video exceeds the current duration limit ({limit} seconds)", + "id": "upload_error.video_duration_limit" + }, { "defaultMessage": "You must schedule a post at least 5 minutes out.", "id": "compose.invalid_schedule" @@ -56,6 +60,10 @@ "defaultMessage": "Your post was sent", "id": "compose.submit_success" }, + { + "defaultMessage": "Your post was edited", + "id": "compose.edit_success" + }, { "defaultMessage": "File upload limit exceeded.", "id": "upload_error.limit" @@ -67,6 +75,14 @@ { "defaultMessage": "View", "id": "snackbar.view" + }, + { + "defaultMessage": "Reply", + "id": "confirmations.reply.confirm" + }, + { + "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "id": "confirmations.reply.message" } ], "path": "app/soapbox/actions/compose.json" @@ -265,7 +281,7 @@ "id": "status_list.queue_label" } ], - "path": "app/soapbox/components/__tests__/timeline_queue_button_header-test.json" + "path": "app/soapbox/components/__tests__/scroll-top-button.test.json" }, { "descriptors": [ @@ -285,15 +301,6 @@ ], "path": "app/soapbox/components/account_search.json" }, - { - "descriptors": [ - { - "defaultMessage": "Follows you", - "id": "account.follows_you" - } - ], - "path": "app/soapbox/components/account.json" - }, { "descriptors": [ { @@ -319,23 +326,6 @@ ], "path": "app/soapbox/components/birthday_input.json" }, - { - "descriptors": [ - { - "defaultMessage": "{name} has a birthday today", - "id": "notification.birthday" - }, - { - "defaultMessage": "{name} and {more} have birthday today", - "id": "notification.birthday_plural" - }, - { - "defaultMessage": "{count} more {count, plural, one {friend} other {friends}}", - "id": "notification.birthday.more" - } - ], - "path": "app/soapbox/components/birthday_reminders.json" - }, { "descriptors": [ { @@ -431,15 +421,6 @@ ], "path": "app/soapbox/components/load_more.json" }, - { - "descriptors": [ - { - "defaultMessage": "Loading…", - "id": "loading_indicator.label" - } - ], - "path": "app/soapbox/components/loading_indicator.json" - }, { "descriptors": [ { @@ -532,56 +513,91 @@ { "descriptors": [ { - "defaultMessage": "Home", - "id": "tabs_bar.home" + "defaultMessage": "Closed", + "id": "poll.closed" }, { - "defaultMessage": "Search", - "id": "navigation.search" + "defaultMessage": "Other instances may display the options you voted for", + "id": "poll.non_anonymous.label" }, { - "defaultMessage": "Notifications", - "id": "tabs_bar.notifications" + "defaultMessage": "{count, plural, one {# person} other {# people}}", + "id": "poll.total_people" }, { - "defaultMessage": "Chats", - "id": "tabs_bar.chats" + "defaultMessage": "{count, plural, one {# vote} other {# votes}}", + "id": "poll.total_votes" }, { - "defaultMessage": "Messages", - "id": "navigation.direct_messages" + "defaultMessage": "Vote", + "id": "poll.vote" }, { - "defaultMessage": "Dashboard", - "id": "tabs_bar.dashboard" + "defaultMessage": "Public poll", + "id": "poll.non_anonymous" }, { - "defaultMessage": "Invites", - "id": "navigation.invites" - }, - { - "defaultMessage": "Developers", - "id": "navigation.developers" - }, - { - "defaultMessage": "All", - "id": "tabs_bar.all" - }, - { - "defaultMessage": "Fediverse", - "id": "tabs_bar.fediverse" + "defaultMessage": "Refresh", + "id": "poll.refresh" } ], - "path": "app/soapbox/components/primary_navigation.json" + "path": "app/soapbox/components/polls/poll-footer.json" }, { "descriptors": [ + { + "defaultMessage": "You voted for this answer", + "id": "poll.voted" + }, + { + "defaultMessage": "{votes, plural, one {# vote} other {# votes}}", + "id": "poll.votes" + } + ], + "path": "app/soapbox/components/polls/poll-option.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Choose as many as you'd like.", + "id": "poll.choose_multiple" + } + ], + "path": "app/soapbox/components/polls/poll.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Joined {date}", + "id": "account.member_since" + }, { "defaultMessage": "Follows you", "id": "account.follows_you" } ], - "path": "app/soapbox/components/profile_hover_card.json" + "path": "app/soapbox/components/profile-hover-card.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Cancel", + "id": "reply_indicator.cancel" + }, + { + "defaultMessage": "Replying to {accounts}", + "id": "reply_mentions.reply" + }, + { + "defaultMessage": "Replying to post", + "id": "reply_mentions.reply_empty" + }, + { + "defaultMessage": "{count} more", + "id": "reply_mentions.more" + } + ], + "path": "app/soapbox/components/quoted-status.json" }, { "descriptors": [ @@ -628,6 +644,51 @@ ], "path": "app/soapbox/components/relative_timestamp.json" }, + { + "descriptors": [ + { + "defaultMessage": "now", + "id": "relative_time.just_now" + }, + { + "defaultMessage": "{number}s", + "id": "relative_time.seconds" + }, + { + "defaultMessage": "{number}m", + "id": "relative_time.minutes" + }, + { + "defaultMessage": "{number}h", + "id": "relative_time.hours" + }, + { + "defaultMessage": "{number}d", + "id": "relative_time.days" + }, + { + "defaultMessage": "Moments remaining", + "id": "time_remaining.moments" + }, + { + "defaultMessage": "{number, plural, one {# second} other {# seconds}} left", + "id": "time_remaining.seconds" + }, + { + "defaultMessage": "{number, plural, one {# minute} other {# minutes}} left", + "id": "time_remaining.minutes" + }, + { + "defaultMessage": "{number, plural, one {# hour} other {# hours}} left", + "id": "time_remaining.hours" + }, + { + "defaultMessage": "{number, plural, one {# day} other {# days}} left", + "id": "time_remaining.days" + } + ], + "path": "app/soapbox/components/relative-timestamp.json" + }, { "descriptors": [ { @@ -687,10 +748,18 @@ "defaultMessage": "Move account", "id": "navigation_bar.account_migration" }, + { + "defaultMessage": "Account aliases", + "id": "navigation_bar.account_aliases" + }, { "defaultMessage": "Logout", "id": "navigation_bar.logout" }, + { + "defaultMessage": "Switch accounts", + "id": "tabs_bar.switch_accounts" + }, { "defaultMessage": "Bookmarks", "id": "column.bookmarks" @@ -736,10 +805,6 @@ "defaultMessage": "Lists", "id": "column.lists" }, - { - "defaultMessage": "Invites", - "id": "navigation.invites" - }, { "defaultMessage": "Developers", "id": "navigation.developers" @@ -769,13 +834,17 @@ "id": "tabs_bar.home" }, { - "defaultMessage": "Profile", - "id": "tabs_bar.profile" + "defaultMessage": "Search", + "id": "tabs_bar.search" }, { "defaultMessage": "Notifications", "id": "tabs_bar.notifications" }, + { + "defaultMessage": "Profile", + "id": "tabs_bar.profile" + }, { "defaultMessage": "Settings", "id": "tabs_bar.settings" @@ -797,6 +866,10 @@ "defaultMessage": "Delete & re-draft", "id": "status.redraft" }, + { + "defaultMessage": "Edit", + "id": "status.edit" + }, { "defaultMessage": "Direct message @{name}", "id": "status.direct" @@ -965,24 +1038,12 @@ { "defaultMessage": "Read more", "id": "status.read_more" - }, - { - "defaultMessage": "Show more", - "id": "status.show_more" - }, - { - "defaultMessage": "Show less", - "id": "status.show_less" } ], "path": "app/soapbox/components/status_content.json" }, { "descriptors": [ - { - "defaultMessage": "Click to see {count} new {count, plural, one {post} other {posts}}", - "id": "status_list.queue_label" - }, { "defaultMessage": "Loading…", "id": "regeneration_indicator.label" @@ -1001,7 +1062,7 @@ "id": "reply_mentions.reply_empty" }, { - "defaultMessage": "Replying to {accounts}{more}", + "defaultMessage": "Replying to {accounts}{more}", "id": "reply_mentions.reply" }, { @@ -1014,24 +1075,299 @@ { "descriptors": [ { - "defaultMessage": "Filtered", - "id": "status.filtered" + "defaultMessage": "Delete", + "id": "status.delete" }, { - "defaultMessage": "Pinned post", - "id": "status.pinned" + "defaultMessage": "Delete & re-draft", + "id": "status.redraft" }, + { + "defaultMessage": "Edit", + "id": "status.edit" + }, + { + "defaultMessage": "Direct message @{name}", + "id": "status.direct" + }, + { + "defaultMessage": "Chat with @{name}", + "id": "status.chat" + }, + { + "defaultMessage": "Mention @{name}", + "id": "status.mention" + }, + { + "defaultMessage": "Mute @{name}", + "id": "account.mute" + }, + { + "defaultMessage": "Block @{name}", + "id": "account.block" + }, + { + "defaultMessage": "Reply", + "id": "status.reply" + }, + { + "defaultMessage": "Share", + "id": "status.share" + }, + { + "defaultMessage": "More", + "id": "status.more" + }, + { + "defaultMessage": "Reply to thread", + "id": "status.replyAll" + }, + { + "defaultMessage": "Repost", + "id": "status.reblog" + }, + { + "defaultMessage": "Repost to original audience", + "id": "status.reblog_private" + }, + { + "defaultMessage": "Un-repost", + "id": "status.cancel_reblog_private" + }, + { + "defaultMessage": "This post cannot be reposted", + "id": "status.cannot_reblog" + }, + { + "defaultMessage": "Like", + "id": "status.favourite" + }, + { + "defaultMessage": "Expand this post", + "id": "status.open" + }, + { + "defaultMessage": "Bookmark", + "id": "status.bookmark" + }, + { + "defaultMessage": "Remove bookmark", + "id": "status.unbookmark" + }, + { + "defaultMessage": "Report @{name}", + "id": "status.report" + }, + { + "defaultMessage": "Mute conversation", + "id": "status.mute_conversation" + }, + { + "defaultMessage": "Unmute conversation", + "id": "status.unmute_conversation" + }, + { + "defaultMessage": "Pin on profile", + "id": "status.pin" + }, + { + "defaultMessage": "Unpin from profile", + "id": "status.unpin" + }, + { + "defaultMessage": "Embed", + "id": "status.embed" + }, + { + "defaultMessage": "Moderate @{name}", + "id": "status.admin_account" + }, + { + "defaultMessage": "Open this post in the moderation interface", + "id": "status.admin_status" + }, + { + "defaultMessage": "Copy link to post", + "id": "status.copy" + }, + { + "defaultMessage": "Remove account from group", + "id": "status.remove_account_from_group" + }, + { + "defaultMessage": "Remove post from group", + "id": "status.remove_post_from_group" + }, + { + "defaultMessage": "Deactivate @{name}", + "id": "admin.users.actions.deactivate_user" + }, + { + "defaultMessage": "Delete @{name}", + "id": "admin.users.actions.delete_user" + }, + { + "defaultMessage": "Delete post", + "id": "admin.statuses.actions.delete_status" + }, + { + "defaultMessage": "Mark post sensitive", + "id": "admin.statuses.actions.mark_status_sensitive" + }, + { + "defaultMessage": "Mark post not sensitive", + "id": "admin.statuses.actions.mark_status_not_sensitive" + }, + { + "defaultMessage": "Like", + "id": "status.reactions.like" + }, + { + "defaultMessage": "Love", + "id": "status.reactions.heart" + }, + { + "defaultMessage": "Haha", + "id": "status.reactions.laughing" + }, + { + "defaultMessage": "Wow", + "id": "status.reactions.open_mouth" + }, + { + "defaultMessage": "Sad", + "id": "status.reactions.cry" + }, + { + "defaultMessage": "Weary", + "id": "status.reactions.weary" + }, + { + "defaultMessage": "Quote post", + "id": "status.quote" + }, + { + "defaultMessage": "Delete", + "id": "confirmations.delete.confirm" + }, + { + "defaultMessage": "Delete post", + "id": "confirmations.delete.heading" + }, + { + "defaultMessage": "Are you sure you want to delete this post?", + "id": "confirmations.delete.message" + }, + { + "defaultMessage": "Delete & redraft", + "id": "confirmations.redraft.confirm" + }, + { + "defaultMessage": "Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.", + "id": "confirmations.redraft.message" + }, + { + "defaultMessage": "Block", + "id": "confirmations.block.confirm" + }, + { + "defaultMessage": "Reply", + "id": "confirmations.reply.confirm" + }, + { + "defaultMessage": "Delete & redraft", + "id": "confirmations.redraft.heading" + }, + { + "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "id": "confirmations.reply.message" + }, + { + "defaultMessage": "Block & Report", + "id": "confirmations.block.block_and_report" + }, + { + "defaultMessage": "Block @{name}", + "id": "confirmations.block.heading" + }, + { + "defaultMessage": "Are you sure you want to block {name}?", + "id": "confirmations.block.message" + } + ], + "path": "app/soapbox/components/status-action-bar.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Replying to post", + "id": "reply_mentions.reply_empty" + }, + { + "defaultMessage": "{count} more", + "id": "reply_mentions.more" + }, + { + "defaultMessage": "Replying to {accounts}", + "id": "reply_mentions.reply" + } + ], + "path": "app/soapbox/components/status-reply-mentions.json" + }, + { + "descriptors": [ { "defaultMessage": "{name} reposted", "id": "status.reblogged_by" }, + { + "defaultMessage": "Filtered", + "id": "status.filtered" + }, { "defaultMessage": "Post is unavailable.", "id": "statuses.quote_tombstone" + }, + { + "defaultMessage": "Pinned post", + "id": "status.pinned" } ], "path": "app/soapbox/components/status.json" }, + { + "descriptors": [ + { + "defaultMessage": "Hide content", + "id": "moderation_overlay.hide" + }, + { + "defaultMessage": "Sensitive content", + "id": "status.sensitive_warning" + }, + { + "defaultMessage": "Content Under Review", + "id": "moderation_overlay.title" + }, + { + "defaultMessage": "This Post has been sent to Moderation for review and is only visible to you. If you believe this is an error please contact Support.", + "id": "moderation_overlay.subtitle" + }, + { + "defaultMessage": "This content may not be suitable for all audiences.", + "id": "status.sensitive_warning.subtitle" + }, + { + "defaultMessage": "Contact", + "id": "moderation_overlay.contact" + }, + { + "defaultMessage": "Show Content", + "id": "moderation_overlay.show" + } + ], + "path": "app/soapbox/components/statuses/sensitive-content-overlay.json" + }, { "descriptors": [ { @@ -1074,6 +1410,15 @@ ], "path": "app/soapbox/components/thumb_navigation.json" }, + { + "descriptors": [ + { + "defaultMessage": "One or more posts are unavailable.", + "id": "statuses.tombstone" + } + ], + "path": "app/soapbox/components/tombstone.json" + }, { "descriptors": [ { @@ -1083,6 +1428,23 @@ ], "path": "app/soapbox/components/ui/card/card.json" }, + { + "descriptors": [ + { + "defaultMessage": "Month", + "id": "datepicker.month" + }, + { + "defaultMessage": "Day", + "id": "datepicker.day" + }, + { + "defaultMessage": "Year", + "id": "datepicker.year" + } + ], + "path": "app/soapbox/components/ui/datepicker/datepicker.json" + }, { "descriptors": [ { @@ -1118,6 +1480,28 @@ ], "path": "app/soapbox/components/ui/spinner/spinner.json" }, + { + "descriptors": [ + { + "defaultMessage": "Add", + "id": "streamfield.add" + }, + { + "defaultMessage": "Remove", + "id": "streamfield.remove" + } + ], + "path": "app/soapbox/components/ui/streamfield/streamfield.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Uploading…", + "id": "upload_progress.label" + } + ], + "path": "app/soapbox/components/upload-progress.json" + }, { "descriptors": [ { @@ -1144,23 +1528,6 @@ ], "path": "app/soapbox/containers/account_container.json" }, - { - "descriptors": [ - { - "defaultMessage": "Hide entire domain", - "id": "confirmations.domain_block.confirm" - }, - { - "defaultMessage": "Block {domain}", - "id": "confirmations.domain_block.heading" - }, - { - "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.", - "id": "confirmations.domain_block.message" - } - ], - "path": "app/soapbox/containers/domain_container.json" - }, { "descriptors": [ { @@ -1236,31 +1603,6 @@ ], "path": "app/soapbox/features/account_gallery/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Account timeline settings", - "id": "account.column_settings.title" - }, - { - "defaultMessage": "These settings apply to all account timelines.", - "id": "account.column_settings.description" - }, - { - "defaultMessage": "Show pinned posts", - "id": "account_timeline.column_settings.show_pinned" - }, - { - "defaultMessage": "Show reposts", - "id": "home.column_settings.show_reblogs" - } - ], - "path": "app/soapbox/features/account_timeline/components/column_settings.json" - }, { "descriptors": [ { @@ -1467,7 +1809,11 @@ "id": "account.unendorse" }, { - "defaultMessage": "Open moderation interface for @{name}", + "defaultMessage": "Remove this follower", + "id": "account.remove_from_followers" + }, + { + "defaultMessage": "Moderate @{name}", "id": "status.admin_account" }, { @@ -1475,60 +1821,76 @@ "id": "account.add_or_remove_from_list" }, { - "defaultMessage": "Deactivate @{name}", - "id": "admin.users.actions.deactivate_user" + "defaultMessage": "Search from @{name}", + "id": "account.search" }, { - "defaultMessage": "Delete @{name}", - "id": "admin.users.actions.delete_user" + "defaultMessage": "Search your posts", + "id": "account.search_self" }, { - "defaultMessage": "Verify @{name}", - "id": "admin.users.actions.verify_user" + "defaultMessage": "Unfollow", + "id": "confirmations.unfollow.confirm" }, { - "defaultMessage": "Unverify @{name}", - "id": "admin.users.actions.unverify_user" + "defaultMessage": "Block", + "id": "confirmations.block.confirm" }, { - "defaultMessage": "Set @{name} as a donor", - "id": "admin.users.actions.set_donor" + "defaultMessage": "Hide entire domain", + "id": "confirmations.domain_block.confirm" }, { - "defaultMessage": "Remove @{name} as a donor", - "id": "admin.users.actions.remove_donor" + "defaultMessage": "Block & Report", + "id": "confirmations.block.block_and_report" }, { - "defaultMessage": "Promote @{name} to an admin", - "id": "admin.users.actions.promote_to_admin" + "defaultMessage": "Remove", + "id": "confirmations.remove_from_followers.confirm" }, { - "defaultMessage": "Promote @{name} to a moderator", - "id": "admin.users.actions.promote_to_moderator" + "defaultMessage": "You are now featuring @{acct} on your profile", + "id": "account.endorse.success" }, { - "defaultMessage": "Demote @{name} to a moderator", - "id": "admin.users.actions.demote_to_moderator" + "defaultMessage": "You are no longer featuring @{acct}", + "id": "account.unendorse.success" }, { - "defaultMessage": "Demote @{name} to a regular user", - "id": "admin.users.actions.demote_to_user" + "defaultMessage": "Block @{name}", + "id": "confirmations.block.heading" }, { - "defaultMessage": "Subscribe to notifications from @{name}", - "id": "account.subscribe" + "defaultMessage": "Are you sure you want to block {name}?", + "id": "confirmations.block.message" }, { - "defaultMessage": "Unsubscribe to notifications from @{name}", - "id": "account.unsubscribe" + "defaultMessage": "Block {domain}", + "id": "confirmations.domain_block.heading" }, { - "defaultMessage": "Suggest @{name}", - "id": "admin.users.actions.suggest_user" + "defaultMessage": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.", + "id": "confirmations.domain_block.message" }, { - "defaultMessage": "Unsuggest @{name}", - "id": "admin.users.actions.unsuggest_user" + "defaultMessage": "Are you sure you want to remove {name} from your followers?", + "id": "confirmations.remove_from_followers.message" + }, + { + "defaultMessage": "Follows you", + "id": "account.follows_you" + }, + { + "defaultMessage": "Blocked", + "id": "account.blocked" + }, + { + "defaultMessage": "Muted", + "id": "account.muted" + }, + { + "defaultMessage": "Domain hidden", + "id": "account.domain_blocked" } ], "path": "app/soapbox/features/account/components/header.json" @@ -1800,6 +2162,23 @@ ], "path": "app/soapbox/features/admin/user_index.json" }, + { + "descriptors": [ + { + "defaultMessage": "Sponsored post", + "id": "sponsored.subtitle" + }, + { + "defaultMessage": "Why am I seeing this ad?", + "id": "sponsored.info.title" + }, + { + "defaultMessage": "{siteTitle} displays ads to help fund our service.", + "id": "sponsored.info.message" + } + ], + "path": "app/soapbox/features/ads/components/ad.json" + }, { "descriptors": [ { @@ -1894,6 +2273,10 @@ }, { "descriptors": [ + { + "defaultMessage": "Enter the pictured text", + "id": "registration.captcha.placeholder" + }, { "defaultMessage": "Click the image to get a new captcha", "id": "registration.captcha.hint" @@ -1901,6 +2284,24 @@ ], "path": "app/soapbox/features/auth_login/components/captcha.json" }, + { + "descriptors": [ + { + "defaultMessage": "Sign in with {provider}", + "id": "oauth_consumer.tooltip" + } + ], + "path": "app/soapbox/features/auth_login/components/consumer-button.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Other ways to sign in", + "id": "oauth_consumers.title" + } + ], + "path": "app/soapbox/features/auth_login/components/consumers-list.json" + }, { "descriptors": [ { @@ -1911,6 +2312,10 @@ "defaultMessage": "Password", "id": "login.fields.password_placeholder" }, + { + "defaultMessage": "Sign In", + "id": "login_form.header" + }, { "defaultMessage": "Trouble logging in?", "id": "login.reset_password_hint" @@ -1932,6 +2337,10 @@ "defaultMessage": "Two-factor code:", "id": "login.fields.otp_code_label" }, + { + "defaultMessage": "Invalid code, please try again.", + "id": "login.otp_log_in.fail" + }, { "defaultMessage": "OTP Login", "id": "login.otp_log_in" @@ -1945,6 +2354,10 @@ }, { "descriptors": [ + { + "defaultMessage": "Expired token, please try again.", + "id": "reset_password.fail" + }, { "defaultMessage": "Set New Password", "id": "reset_password.header" @@ -1983,6 +2396,10 @@ "defaultMessage": "Only letters, numbers, and underscores are allowed.", "id": "registration.fields.username_hint" }, + { + "defaultMessage": "Username is already taken.", + "id": "registration.username_unavailable" + }, { "defaultMessage": "E-Mail address", "id": "registration.fields.email_placeholder" @@ -1991,6 +2408,10 @@ "defaultMessage": "Password", "id": "registration.fields.password_placeholder" }, + { + "defaultMessage": "Passwords don't match.", + "id": "registration.password_mismatch" + }, { "defaultMessage": "Password (again)", "id": "registration.fields.confirm_placeholder" @@ -2027,14 +2448,6 @@ "defaultMessage": "Your account will be manually approved by an admin. Please be patient while we review your details.", "id": "confirmations.register.needs_approval" }, - { - "defaultMessage": "Username is already taken.", - "id": "registration.username_unavailable" - }, - { - "defaultMessage": "Passwords don't match.", - "id": "registration.password_mismatch" - }, { "defaultMessage": "Why do you want to join?", "id": "registration.reason" @@ -2050,6 +2463,19 @@ ], "path": "app/soapbox/features/auth_login/components/registration_form.json" }, + { + "descriptors": [ + { + "defaultMessage": "Sessions", + "id": "security.headers.tokens" + }, + { + "defaultMessage": "Revoke", + "id": "security.tokens.revoke" + } + ], + "path": "app/soapbox/features/auth_token_list/index.json" + }, { "descriptors": [ { @@ -2274,6 +2700,10 @@ "defaultMessage": "Schedule", "id": "compose_form.schedule" }, + { + "defaultMessage": "Save changes", + "id": "compose_form.save_changes" + }, { "defaultMessage": "You have scheduled posts. {click_here} to see them.", "id": "compose_form.scheduled_statuses.message" @@ -2285,6 +2715,51 @@ ], "path": "app/soapbox/features/compose/components/compose_form.json" }, + { + "descriptors": [ + { + "defaultMessage": "What's on your mind?", + "id": "compose_form.placeholder" + }, + { + "defaultMessage": "Add a poll topic...", + "id": "compose_form.poll_placeholder" + }, + { + "defaultMessage": "Write your warning here (optional)", + "id": "compose_form.spoiler_placeholder" + }, + { + "defaultMessage": "Post", + "id": "compose_form.publish" + }, + { + "defaultMessage": "{publish}!", + "id": "compose_form.publish_loud" + }, + { + "defaultMessage": "Message", + "id": "compose_form.message" + }, + { + "defaultMessage": "Schedule", + "id": "compose_form.schedule" + }, + { + "defaultMessage": "Save changes", + "id": "compose_form.save_changes" + }, + { + "defaultMessage": "You have scheduled posts. {click_here} to see them.", + "id": "compose_form.scheduled_statuses.message" + }, + { + "defaultMessage": "Click here", + "id": "compose_form.scheduled_statuses.click_here" + } + ], + "path": "app/soapbox/features/compose/components/compose-form.json" + }, { "descriptors": [ { @@ -2346,6 +2821,128 @@ ], "path": "app/soapbox/features/compose/components/emoji_picker_dropdown.json" }, + { + "descriptors": [ + { + "defaultMessage": "Insert emoji", + "id": "emoji_button.label" + }, + { + "defaultMessage": "Search…", + "id": "emoji_button.search" + }, + { + "defaultMessage": "No emoji's found.", + "id": "emoji_button.not_found" + }, + { + "defaultMessage": "Custom", + "id": "emoji_button.custom" + }, + { + "defaultMessage": "Frequently used", + "id": "emoji_button.recent" + }, + { + "defaultMessage": "Search results", + "id": "emoji_button.search_results" + }, + { + "defaultMessage": "People", + "id": "emoji_button.people" + }, + { + "defaultMessage": "Nature", + "id": "emoji_button.nature" + }, + { + "defaultMessage": "Food & Drink", + "id": "emoji_button.food" + }, + { + "defaultMessage": "Activity", + "id": "emoji_button.activity" + }, + { + "defaultMessage": "Travel & Places", + "id": "emoji_button.travel" + }, + { + "defaultMessage": "Objects", + "id": "emoji_button.objects" + }, + { + "defaultMessage": "Symbols", + "id": "emoji_button.symbols" + }, + { + "defaultMessage": "Flags", + "id": "emoji_button.flags" + } + ], + "path": "app/soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Insert emoji", + "id": "emoji_button.label" + }, + { + "defaultMessage": "Search…", + "id": "emoji_button.search" + }, + { + "defaultMessage": "No emoji's found.", + "id": "emoji_button.not_found" + }, + { + "defaultMessage": "Custom", + "id": "emoji_button.custom" + }, + { + "defaultMessage": "Frequently used", + "id": "emoji_button.recent" + }, + { + "defaultMessage": "Search results", + "id": "emoji_button.search_results" + }, + { + "defaultMessage": "People", + "id": "emoji_button.people" + }, + { + "defaultMessage": "Nature", + "id": "emoji_button.nature" + }, + { + "defaultMessage": "Food & Drink", + "id": "emoji_button.food" + }, + { + "defaultMessage": "Activity", + "id": "emoji_button.activity" + }, + { + "defaultMessage": "Travel & Places", + "id": "emoji_button.travel" + }, + { + "defaultMessage": "Objects", + "id": "emoji_button.objects" + }, + { + "defaultMessage": "Symbols", + "id": "emoji_button.symbols" + }, + { + "defaultMessage": "Flags", + "id": "emoji_button.flags" + } + ], + "path": "app/soapbox/features/compose/components/emoji-picker/emoji-picker-menu.json" + }, { "descriptors": [ { @@ -2411,7 +3008,77 @@ "id": "intervals.full.days" } ], - "path": "app/soapbox/features/compose/components/poll-form.json" + "path": "app/soapbox/features/compose/components/poll_form.json" + }, + { + "descriptors": [ + { + "defaultMessage": "{number, plural, one {# minute} other {# minutes}}", + "id": "intervals.full.minutes" + }, + { + "defaultMessage": "{number, plural, one {# hour} other {# hours}}", + "id": "intervals.full.hours" + }, + { + "defaultMessage": "{number, plural, one {# day} other {# days}}", + "id": "intervals.full.days" + } + ], + "path": "app/soapbox/features/compose/components/polls/duration-selector.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Answer #{number}", + "id": "compose_form.poll.option_placeholder" + }, + { + "defaultMessage": "Add an answer", + "id": "compose_form.poll.add_option" + }, + { + "defaultMessage": "Remove this answer", + "id": "compose_form.poll.remove_option" + }, + { + "defaultMessage": "Duration", + "id": "compose_form.poll.duration" + }, + { + "defaultMessage": "Remove poll", + "id": "compose_form.poll.remove" + }, + { + "defaultMessage": "Change poll to allow multiple answers", + "id": "compose_form.poll.switch_to_multiple" + }, + { + "defaultMessage": "Change poll to allow for a single answer", + "id": "compose_form.poll.switch_to_single" + }, + { + "defaultMessage": "{number, plural, one {# minute} other {# minutes}}", + "id": "intervals.full.minutes" + }, + { + "defaultMessage": "{number, plural, one {# hour} other {# hours}}", + "id": "intervals.full.hours" + }, + { + "defaultMessage": "{number, plural, one {# day} other {# days}}", + "id": "intervals.full.days" + }, + { + "defaultMessage": "Multi-Select", + "id": "compose_form.poll.multiselect" + }, + { + "defaultMessage": "Allow users to select multiple answers", + "id": "compose_form.poll.multiselect_detail" + } + ], + "path": "app/soapbox/features/compose/components/polls/poll-form.json" }, { "descriptors": [ @@ -2454,15 +3121,6 @@ ], "path": "app/soapbox/features/compose/components/privacy_dropdown.json" }, - { - "descriptors": [ - { - "defaultMessage": "Cancel", - "id": "reply_indicator.cancel" - } - ], - "path": "app/soapbox/features/compose/components/reply_indicator.json" - }, { "descriptors": [ { @@ -2470,12 +3128,12 @@ "id": "reply_mentions.reply_empty" }, { - "defaultMessage": "Replying to {accounts}{more}", - "id": "reply_mentions.reply" + "defaultMessage": "{count} more", + "id": "reply_mentions.more" }, { - "defaultMessage": "and {count} more", - "id": "reply_mentions.more" + "defaultMessage": "Replying to {accounts}", + "id": "reply_mentions.reply" } ], "path": "app/soapbox/features/compose/components/reply_mentions.json" @@ -2565,6 +3223,23 @@ ], "path": "app/soapbox/features/compose/components/spoiler_button.json" }, + { + "descriptors": [ + { + "defaultMessage": "Sensitive content", + "id": "compose_form.spoiler_title" + }, + { + "defaultMessage": "Write your warning here (optional)", + "id": "compose_form.spoiler_placeholder" + }, + { + "defaultMessage": "Remove sensitive", + "id": "compose_form.spoiler_remove" + } + ], + "path": "app/soapbox/features/compose/components/spoiler-input.json" + }, { "descriptors": [ { @@ -2574,15 +3249,6 @@ ], "path": "app/soapbox/features/compose/components/upload_button.json" }, - { - "descriptors": [ - { - "defaultMessage": "Uploading…", - "id": "upload_progress.label" - } - ], - "path": "app/soapbox/features/compose/components/upload_progress.json" - }, { "descriptors": [ { @@ -2618,23 +3284,6 @@ ], "path": "app/soapbox/features/compose/components/visual_character_counter.json" }, - { - "descriptors": [ - { - "defaultMessage": "Media is marked as sensitive", - "id": "compose_form.sensitive.marked" - }, - { - "defaultMessage": "Media is not marked as sensitive", - "id": "compose_form.sensitive.unmarked" - }, - { - "defaultMessage": "Mark media as sensitive", - "id": "compose_form.sensitive.hide" - } - ], - "path": "app/soapbox/features/compose/containers/sensitive_button_container.json" - }, { "descriptors": [ { @@ -3160,6 +3809,14 @@ "defaultMessage": "Opt-in to news and marketing updates.", "id": "edit_profile.hints.accepts_email_list" }, + { + "defaultMessage": "Profile fields", + "id": "edit_profile.fields.meta_fields_label" + }, + { + "defaultMessage": "You can have up to {count, plural, one {# custom field} other {# custom fields}} displayed on your profile.", + "id": "edit_profile.hints.meta_fields" + }, { "defaultMessage": "Save", "id": "edit_profile.save" @@ -3216,23 +3873,6 @@ ], "path": "app/soapbox/features/export_data/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Instance", - "id": "login.fields.instance_label" - }, - { - "defaultMessage": "example.com", - "id": "login.fields.instance_placeholder" - }, - { - "defaultMessage": "Log in", - "id": "login.log_in" - } - ], - "path": "app/soapbox/features/external_login/components/external_login_form.json" - }, { "descriptors": [ { @@ -3333,6 +3973,28 @@ ], "path": "app/soapbox/features/federation_restrictions/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "Something isn't right. Try reloading the page.", + "id": "common.error" + } + ], + "path": "app/soapbox/features/feed-filtering/feed-carousel.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Suggested Profiles", + "id": "feed_suggestions.heading" + }, + { + "defaultMessage": "View all", + "id": "feed_suggestions.view_all" + } + ], + "path": "app/soapbox/features/feed-suggestions/feed-suggestions.json" + }, { "descriptors": [ { @@ -3671,66 +4333,6 @@ ], "path": "app/soapbox/features/groups/removed_accounts/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Groups You're In", - "id": "groups.sidebar-panel.title" - }, - { - "defaultMessage": "Show all", - "id": "groups.sidebar-panel.show_all" - } - ], - "path": "app/soapbox/features/groups/sidebar_panel/index.json" - }, - { - "descriptors": [ - { - "defaultMessage": "new posts", - "id": "groups.sidebar-panel.item.view" - }, - { - "defaultMessage": "No recent activity", - "id": "groups.sidebar-panel.item.no_recent_activity" - } - ], - "path": "app/soapbox/features/groups/sidebar_panel/item.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Join group", - "id": "groups.join" - }, - { - "defaultMessage": "Leave group", - "id": "groups.leave" - }, - { - "defaultMessage": "Removed Accounts", - "id": "groups.removed_accounts" - }, - { - "defaultMessage": "Edit", - "id": "groups.edit" - } - ], - "path": "app/soapbox/features/groups/timeline/components/header.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Archived group", - "id": "group.detail.archived_group" - }, - { - "defaultMessage": "You're an admin", - "id": "groups.detail.role_admin" - } - ], - "path": "app/soapbox/features/groups/timeline/components/panel.json" - }, { "descriptors": [ { @@ -3761,31 +4363,6 @@ ], "path": "app/soapbox/features/hashtag_timeline/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Home settings", - "id": "home.column_settings.title" - }, - { - "defaultMessage": "Show reposts", - "id": "home.column_settings.show_reblogs" - }, - { - "defaultMessage": "Show replies", - "id": "home.column_settings.show_replies" - }, - { - "defaultMessage": "Show direct messages", - "id": "home.column_settings.show_direct" - } - ], - "path": "app/soapbox/features/home_timeline/components/column_settings.json" - }, { "descriptors": [ { @@ -3793,7 +4370,7 @@ "id": "column.home" }, { - "defaultMessage": "Or you can visit {public} to get started and meet other users.", + "defaultMessage": "Your home timeline is empty! Visit {public} to get started and meet other users.", "id": "empty_column.home" }, { @@ -3852,63 +4429,6 @@ ], "path": "app/soapbox/features/import_data/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "First steps", - "id": "introduction.welcome.headline" - }, - { - "defaultMessage": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "id": "introduction.welcome.text" - }, - { - "defaultMessage": "Let's go!", - "id": "introduction.welcome.action" - }, - { - "defaultMessage": "Home", - "id": "introduction.federation.home.headline" - }, - { - "defaultMessage": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "id": "introduction.federation.home.text" - }, - { - "defaultMessage": "Next", - "id": "introduction.federation.action" - }, - { - "defaultMessage": "Reply", - "id": "introduction.interactions.reply.headline" - }, - { - "defaultMessage": "You can reply to other people's and your own posts, which will chain them together in a conversation.", - "id": "introduction.interactions.reply.text" - }, - { - "defaultMessage": "Repost", - "id": "introduction.interactions.reblog.headline" - }, - { - "defaultMessage": "You can share other people's posts with your followers by reposting them.", - "id": "introduction.interactions.reblog.text" - }, - { - "defaultMessage": "Favorite", - "id": "introduction.interactions.favourite.headline" - }, - { - "defaultMessage": "You can save a post for later, and let the author know that you liked it, by favoriting it.", - "id": "introduction.interactions.favourite.text" - }, - { - "defaultMessage": "Finish tutorial!", - "id": "introduction.interactions.action" - } - ], - "path": "app/soapbox/features/introduction/index.json" - }, { "descriptors": [ { @@ -3918,6 +4438,22 @@ { "defaultMessage": "{instance} is not accepting new members.", "id": "registration.closed_message" + }, + { + "defaultMessage": "Let's get started!", + "id": "registrations.get_started" + }, + { + "defaultMessage": "Sign in with {provider}", + "id": "oauth_consumer.tooltip" + }, + { + "defaultMessage": "Social Media Without Discrimination", + "id": "registrations.tagline" + }, + { + "defaultMessage": "Create an account", + "id": "registrations.create_account" } ], "path": "app/soapbox/features/landing_page/index.json" @@ -4154,104 +4690,6 @@ ], "path": "app/soapbox/features/mutes/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Clear notifications", - "id": "notifications.clear" - } - ], - "path": "app/soapbox/features/notifications/components/clear_column_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "lightbox.close" - }, - { - "defaultMessage": "Show", - "id": "notifications.column_settings.filter_bar.show" - }, - { - "defaultMessage": "Display all categories", - "id": "notifications.column_settings.filter_bar.advanced" - }, - { - "defaultMessage": "Desktop notifications", - "id": "notifications.column_settings.alert" - }, - { - "defaultMessage": "Play sound for all notifications", - "id": "notifications.column_settings.sounds.all_sounds" - }, - { - "defaultMessage": "Show in column", - "id": "notifications.column_settings.show" - }, - { - "defaultMessage": "Play sound", - "id": "notifications.column_settings.sound" - }, - { - "defaultMessage": "Push notifications", - "id": "notifications.column_settings.push" - }, - { - "defaultMessage": "Show birthday reminders", - "id": "notifications.column_settings.birthdays.show" - }, - { - "defaultMessage": "Notification settings", - "id": "notifications.column_settings.title" - }, - { - "defaultMessage": "Sounds", - "id": "notifications.column_settings.sounds" - }, - { - "defaultMessage": "Quick filter bar", - "id": "notifications.column_settings.filter_bar.category" - }, - { - "defaultMessage": "Birthdays", - "id": "notifications.column_settings.birthdays.category" - }, - { - "defaultMessage": "New followers:", - "id": "notifications.column_settings.follow" - }, - { - "defaultMessage": "New follow requests:", - "id": "notifications.column_settings.follow_request" - }, - { - "defaultMessage": "Likes:", - "id": "notifications.column_settings.favourite" - }, - { - "defaultMessage": "Emoji reacts:", - "id": "notifications.column_settings.emoji_react" - }, - { - "defaultMessage": "Mentions:", - "id": "notifications.column_settings.mention" - }, - { - "defaultMessage": "Reposts:", - "id": "notifications.column_settings.reblog" - }, - { - "defaultMessage": "Poll results:", - "id": "notifications.column_settings.poll" - }, - { - "defaultMessage": "Moves:", - "id": "notifications.column_settings.move" - } - ], - "path": "app/soapbox/features/notifications/components/column_settings.json" - }, { "descriptors": [ { @@ -4293,19 +4731,6 @@ ], "path": "app/soapbox/features/notifications/components/filter_bar.json" }, - { - "descriptors": [ - { - "defaultMessage": "Authorize", - "id": "follow_request.authorize" - }, - { - "defaultMessage": "Reject", - "id": "follow_request.reject" - } - ], - "path": "app/soapbox/features/notifications/components/follow_request.json" - }, { "descriptors": [ { @@ -4317,12 +4742,8 @@ "id": "notification.follow_request" }, { - "defaultMessage": "{name} sent you a message", - "id": "notification.pleroma:chat_mention" - }, - { - "defaultMessage": "{name} reacted to your post", - "id": "notification.pleroma:emoji_reaction" + "defaultMessage": "{name} mentioned you", + "id": "notification.mentioned" }, { "defaultMessage": "{name} liked your post", @@ -4343,27 +4764,30 @@ { "defaultMessage": "{name} moved to {targetName}", "id": "notification.move" + }, + { + "defaultMessage": "{name} sent you a message", + "id": "notification.pleroma:chat_mention" + }, + { + "defaultMessage": "{name} reacted to your post", + "id": "notification.pleroma:emoji_reaction" + }, + { + "defaultMessage": "Welcome to {instance}!", + "id": "notification.user_approved" + }, + { + "defaultMessage": "{name} edited a post you interacted with", + "id": "notification.update" + }, + { + "defaultMessage": " + {count} {count, plural, one {other} other {others}}", + "id": "notification.others" } ], "path": "app/soapbox/features/notifications/components/notification.json" }, - { - "descriptors": [ - { - "defaultMessage": "Clear notifications", - "id": "notifications.clear_heading" - }, - { - "defaultMessage": "Are you sure you want to permanently clear all your notifications?", - "id": "notifications.clear_confirmation" - }, - { - "defaultMessage": "Clear notifications", - "id": "notifications.clear" - } - ], - "path": "app/soapbox/features/notifications/containers/column_settings_container.json" - }, { "descriptors": [ { @@ -4550,6 +4974,10 @@ "defaultMessage": "Show replies", "id": "home.column_settings.show_replies" }, + { + "defaultMessage": "Theme", + "id": "preferences.fields.theme" + }, { "defaultMessage": "Language", "id": "preferences.fields.language_label" @@ -4610,19 +5038,18 @@ { "defaultMessage": "Forgot password?", "id": "header.login.forgot_password" + }, + { + "defaultMessage": "Preview Timeline", + "id": "header.preview_timeline.label" + }, + { + "defaultMessage": "Help Center", + "id": "landing_page_modal.helpCenter" } ], "path": "app/soapbox/features/public_layout/components/header.json" }, - { - "descriptors": [ - { - "defaultMessage": "Close", - "id": "pre_header.close" - } - ], - "path": "app/soapbox/features/public_layout/components/pre_header.json" - }, { "descriptors": [ { @@ -4754,15 +5181,6 @@ ], "path": "app/soapbox/features/scheduled_statuses/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Top", - "id": "search_results.top" - } - ], - "path": "app/soapbox/features/search/components/header.json" - }, { "descriptors": [ { @@ -4772,111 +5190,6 @@ ], "path": "app/soapbox/features/search/index.json" }, - { - "descriptors": [ - { - "defaultMessage": "Security", - "id": "column.security" - }, - { - "defaultMessage": "Save changes", - "id": "security.submit" - }, - { - "defaultMessage": "Email successfully updated.", - "id": "security.update_email.success" - }, - { - "defaultMessage": "Update email failed.", - "id": "security.update_email.fail" - }, - { - "defaultMessage": "Password successfully updated.", - "id": "security.update_password.success" - }, - { - "defaultMessage": "Update password failed.", - "id": "security.update_password.fail" - }, - { - "defaultMessage": "Email address", - "id": "security.fields.email.label" - }, - { - "defaultMessage": "Password", - "id": "security.fields.password.label" - }, - { - "defaultMessage": "Current password", - "id": "security.fields.old_password.label" - }, - { - "defaultMessage": "New password", - "id": "security.fields.new_password.label" - }, - { - "defaultMessage": "New password (again)", - "id": "security.fields.password_confirmation.label" - }, - { - "defaultMessage": "Revoke", - "id": "security.tokens.revoke" - }, - { - "defaultMessage": "Change Email", - "id": "security.headers.update_email" - }, - { - "defaultMessage": "Change Password", - "id": "security.headers.update_password" - }, - { - "defaultMessage": "Sessions", - "id": "security.headers.tokens" - }, - { - "defaultMessage": "Delete Account", - "id": "security.headers.delete" - }, - { - "defaultMessage": "To delete your account, enter your password then click Delete Account. This is a permanent action that cannot be undone. Your account will be destroyed from this server, and a deletion request will be sent to other servers. It's not guaranteed that all servers will purge your account.", - "id": "security.text.delete" - }, - { - "defaultMessage": "Delete Account", - "id": "security.submit.delete" - }, - { - "defaultMessage": "Account successfully deleted.", - "id": "security.delete_account.success" - }, - { - "defaultMessage": "Account deletion failed.", - "id": "security.delete_account.fail" - }, - { - "defaultMessage": "Set up 2-Factor Auth", - "id": "security.mfa" - }, - { - "defaultMessage": "Configure multi-factor authentication with OTP", - "id": "security.mfa_setup_hint" - }, - { - "defaultMessage": "You have multi-factor authentication set up with OTP.", - "id": "security.mfa_enabled" - }, - { - "defaultMessage": "Disable", - "id": "security.disable_mfa" - }, - { - "defaultMessage": "Authorization Methods", - "id": "security.mfa_header" - } - ], - "path": "app/soapbox/features/security/index.json" - }, { "descriptors": [ { @@ -5040,6 +5353,10 @@ "defaultMessage": "Configure MFA", "id": "settings.configure_mfa" }, + { + "defaultMessage": "Active sessions", + "id": "settings.sessions" + }, { "defaultMessage": "Delete Account", "id": "settings.delete_account" @@ -5068,6 +5385,36 @@ ], "path": "app/soapbox/features/settings/media_display.json" }, + { + "descriptors": [ + { + "defaultMessage": "Ticker", + "id": "soapbox_config.crypto_address.meta_fields.ticker_placeholder" + }, + { + "defaultMessage": "Address", + "id": "soapbox_config.crypto_address.meta_fields.address_placeholder" + }, + { + "defaultMessage": "Note (optional)", + "id": "soapbox_config.crypto_address.meta_fields.note_placeholder" + } + ], + "path": "app/soapbox/features/soapbox_config/components/crypto-address-input.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Label", + "id": "soapbox_config.home_footer.meta_fields.label_placeholder" + }, + { + "defaultMessage": "URL", + "id": "soapbox_config.home_footer.meta_fields.url_placeholder" + } + ], + "path": "app/soapbox/features/soapbox_config/components/footer-link-input.json" + }, { "descriptors": [ { @@ -5095,18 +5442,6 @@ }, { "descriptors": [ - { - "defaultMessage": "Soapbox config", - "id": "column.soapbox_config" - }, - { - "defaultMessage": "Soapbox config saved!", - "id": "soapbox_config.saved" - }, - { - "defaultMessage": "Copyright footer", - "id": "soapbox_config.copyright_footer.meta_fields.label_placeholder" - }, { "defaultMessage": "Icon", "id": "soapbox_config.promo_panel.meta_fields.icon_placeholder" @@ -5118,26 +5453,32 @@ { "defaultMessage": "URL", "id": "soapbox_config.promo_panel.meta_fields.url_placeholder" + } + ], + "path": "app/soapbox/features/soapbox_config/components/promo-panel-input.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Preview", + "id": "site_preview.preview" + } + ], + "path": "app/soapbox/features/soapbox_config/components/site-preview.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Soapbox config", + "id": "column.soapbox_config" }, { - "defaultMessage": "Label", - "id": "soapbox_config.home_footer.meta_fields.label_placeholder" + "defaultMessage": "Soapbox config saved!", + "id": "soapbox_config.saved" }, { - "defaultMessage": "URL", - "id": "soapbox_config.home_footer.meta_fields.url_placeholder" - }, - { - "defaultMessage": "Ticker", - "id": "soapbox_config.crypto_address.meta_fields.ticker_placeholder" - }, - { - "defaultMessage": "Address", - "id": "soapbox_config.crypto_address.meta_fields.address_placeholder" - }, - { - "defaultMessage": "Note (optional)", - "id": "soapbox_config.crypto_address.meta_fields.note_placeholder" + "defaultMessage": "Copyright footer", + "id": "soapbox_config.copyright_footer.meta_fields.label_placeholder" }, { "defaultMessage": "Number of items to display in the crypto homepage widget", @@ -5195,6 +5536,22 @@ "defaultMessage": "@handle", "id": "soapbox_config.single_user_mode_profile_hint" }, + { + "defaultMessage": "Logo", + "id": "soapbox_config.fields.logo_label" + }, + { + "defaultMessage": "SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio", + "id": "soapbox_config.hints.logo" + }, + { + "defaultMessage": "Theme", + "id": "soapbox_config.headings.theme" + }, + { + "defaultMessage": "Default theme", + "id": "soapbox_config.fields.theme_label" + }, { "defaultMessage": "Brand color", "id": "soapbox_config.fields.brand_color_label" @@ -5204,16 +5561,12 @@ "id": "soapbox_config.fields.accent_color_label" }, { - "defaultMessage": "Default theme", - "id": "soapbox_config.fields.theme_label" + "defaultMessage": "Options", + "id": "soapbox_config.headings.options" }, { - "defaultMessage": "Logo", - "id": "soapbox_config.fields.logo_label" - }, - { - "defaultMessage": "SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio", - "id": "soapbox_config.hints.logo" + "defaultMessage": "Navigation", + "id": "soapbox_config.headings.navigation" }, { "defaultMessage": "Promo panel items", @@ -5223,14 +5576,6 @@ "defaultMessage": "You can have custom defined links displayed on the right panel of the timelines page.", "id": "soapbox_config.hints.promo_panel_fields" }, - { - "defaultMessage": "{ link }", - "id": "soapbox_config.hints.promo_panel_icons" - }, - { - "defaultMessage": "Add new Promo panel item", - "id": "soapbox_config.fields.promo_panel.add" - }, { "defaultMessage": "Home footer items", "id": "soapbox_config.fields.home_footer_fields_label" @@ -5240,8 +5585,8 @@ "id": "soapbox_config.hints.home_footer_fields" }, { - "defaultMessage": "Add new Home Footer Item", - "id": "soapbox_config.fields.home_footer.add" + "defaultMessage": "Cryptocurrency", + "id": "soapbox_config.headings.cryptocurrency" }, { "defaultMessage": "Cryptocurrency addresses", @@ -5252,8 +5597,8 @@ "id": "soapbox_config.hints.crypto_addresses" }, { - "defaultMessage": "Add new crypto address", - "id": "soapbox_config.fields.crypto_address.add" + "defaultMessage": "Advanced", + "id": "soapbox_config.headings.advanced" }, { "defaultMessage": "Save", @@ -5273,165 +5618,8 @@ "id": "status.redraft" }, { - "defaultMessage": "Direct message @{name}", - "id": "status.direct" - }, - { - "defaultMessage": "Chat with @{name}", - "id": "status.chat" - }, - { - "defaultMessage": "Mention @{name}", - "id": "status.mention" - }, - { - "defaultMessage": "Reply", - "id": "status.reply" - }, - { - "defaultMessage": "Repost", - "id": "status.reblog" - }, - { - "defaultMessage": "Repost to original audience", - "id": "status.reblog_private" - }, - { - "defaultMessage": "Un-repost", - "id": "status.cancel_reblog_private" - }, - { - "defaultMessage": "This post cannot be reposted", - "id": "status.cannot_reblog" - }, - { - "defaultMessage": "Like", - "id": "status.favourite" - }, - { - "defaultMessage": "Mute @{name}", - "id": "status.mute" - }, - { - "defaultMessage": "Mute conversation", - "id": "status.mute_conversation" - }, - { - "defaultMessage": "Unmute conversation", - "id": "status.unmute_conversation" - }, - { - "defaultMessage": "Block @{name}", - "id": "status.block" - }, - { - "defaultMessage": "Report @{name}", - "id": "status.report" - }, - { - "defaultMessage": "Share", - "id": "status.share" - }, - { - "defaultMessage": "Pin on profile", - "id": "status.pin" - }, - { - "defaultMessage": "Unpin from profile", - "id": "status.unpin" - }, - { - "defaultMessage": "Embed", - "id": "status.embed" - }, - { - "defaultMessage": "Open moderation interface for @{name}", - "id": "status.admin_account" - }, - { - "defaultMessage": "Open this post in the moderation interface", - "id": "status.admin_status" - }, - { - "defaultMessage": "Copy link to post", - "id": "status.copy" - }, - { - "defaultMessage": "Bookmark", - "id": "status.bookmark" - }, - { - "defaultMessage": "Remove bookmark", - "id": "status.unbookmark" - }, - { - "defaultMessage": "Deactivate @{name}", - "id": "admin.users.actions.deactivate_user" - }, - { - "defaultMessage": "Delete @{name}", - "id": "admin.users.actions.delete_user" - }, - { - "defaultMessage": "Delete post", - "id": "admin.statuses.actions.delete_status" - }, - { - "defaultMessage": "Mark post sensitive", - "id": "admin.statuses.actions.mark_status_sensitive" - }, - { - "defaultMessage": "Mark post not sensitive", - "id": "admin.statuses.actions.mark_status_not_sensitive" - }, - { - "defaultMessage": "Like", - "id": "status.reactions.like" - }, - { - "defaultMessage": "Love", - "id": "status.reactions.heart" - }, - { - "defaultMessage": "Haha", - "id": "status.reactions.laughing" - }, - { - "defaultMessage": "Wow", - "id": "status.reactions.open_mouth" - }, - { - "defaultMessage": "Sad", - "id": "status.reactions.cry" - }, - { - "defaultMessage": "Weary", - "id": "status.reactions.weary" - }, - { - "defaultMessage": "Select emoji", - "id": "status.reactions_expand" - }, - { - "defaultMessage": "More", - "id": "status.actions.more" - }, - { - "defaultMessage": "Quote post", - "id": "status.quote" - } - ], - "path": "app/soapbox/features/status/components/action_bar.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Delete", - "id": "status.delete" - }, - { - "defaultMessage": "Delete & re-draft", - "id": "status.redraft" + "defaultMessage": "Edit", + "id": "status.edit" }, { "defaultMessage": "Direct message @{name}", @@ -5588,16 +5776,11 @@ "descriptors": [ { "defaultMessage": "Post is unavailable.", - "id": "statuses.quote_tombstone" - } - ], - "path": "app/soapbox/features/status/components/detailed_status.json" - }, - { - "descriptors": [ + "id": "actualStatuses.quote_tombstone" + }, { - "defaultMessage": "Post is unavailable.", - "id": "statuses.quote_tombstone" + "defaultMessage": "Edited {date}", + "id": "actualStatus.edited" } ], "path": "app/soapbox/features/status/components/detailed-status.json" @@ -5609,7 +5792,7 @@ "id": "reply_indicator.cancel" }, { - "defaultMessage": "Replying to {accounts}{more}", + "defaultMessage": "Replying to {accounts}{more}", "id": "reply_mentions.reply" }, { @@ -5626,55 +5809,23 @@ { "descriptors": [ { - "defaultMessage": "Delete", - "id": "confirmations.delete.confirm" + "defaultMessage": "Continue the conversation", + "id": "thread_login.title" }, { - "defaultMessage": "Delete post", - "id": "confirmations.delete.heading" + "defaultMessage": "Join {siteTitle} to get the full story and details.", + "id": "thread_login.message" }, { - "defaultMessage": "Are you sure you want to delete this post?", - "id": "confirmations.delete.message" + "defaultMessage": "Log in", + "id": "thread_login.login" }, { - "defaultMessage": "Delete & redraft", - "id": "confirmations.redraft.confirm" - }, - { - "defaultMessage": "Delete & redraft", - "id": "confirmations.redraft.heading" - }, - { - "defaultMessage": "Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.", - "id": "confirmations.redraft.message" - }, - { - "defaultMessage": "Block", - "id": "confirmations.block.confirm" - }, - { - "defaultMessage": "Reply", - "id": "confirmations.reply.confirm" - }, - { - "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "id": "confirmations.reply.message" - }, - { - "defaultMessage": "Block & Report", - "id": "confirmations.block.block_and_report" - }, - { - "defaultMessage": "Block @{name}", - "id": "confirmations.block.heading" - }, - { - "defaultMessage": "Are you sure you want to block {name}?", - "id": "confirmations.block.message" + "defaultMessage": "Sign up", + "id": "thread_login.signup" } ], - "path": "app/soapbox/features/status/containers/detailed_status_container.json" + "path": "app/soapbox/features/status/components/thread-login-cta.json" }, { "descriptors": [ @@ -5737,18 +5888,6 @@ { "defaultMessage": "Block & Report", "id": "confirmations.block.block_and_report" - }, - { - "defaultMessage": "Block @{name}", - "id": "confirmations.block.heading" - }, - { - "defaultMessage": "Are you sure you want to block {name}?", - "id": "confirmations.block.message" - }, - { - "defaultMessage": "One or more posts are unavailable.", - "id": "statuses.tombstone" } ], "path": "app/soapbox/features/status/index.json" @@ -5758,6 +5897,10 @@ { "defaultMessage": "Test timeline", "id": "column.test" + }, + { + "defaultMessage": "The test timeline is empty.", + "id": "empty_column.test" } ], "path": "app/soapbox/features/test_timeline/index.json" @@ -5833,6 +5976,63 @@ ], "path": "app/soapbox/features/ui/components/action_button.json" }, + { + "descriptors": [ + { + "defaultMessage": "Block @{name}", + "id": "account.block" + }, + { + "defaultMessage": "Blocked", + "id": "account.blocked" + }, + { + "defaultMessage": "Edit profile", + "id": "account.edit_profile" + }, + { + "defaultMessage": "Follow", + "id": "account.follow" + }, + { + "defaultMessage": "Mute @{name}", + "id": "account.mute" + }, + { + "defaultMessage": "Remote follow", + "id": "account.remote_follow" + }, + { + "defaultMessage": "Awaiting approval. Click to cancel follow request", + "id": "account.requested" + }, + { + "defaultMessage": "Awaiting approval", + "id": "account.requested_small" + }, + { + "defaultMessage": "Unblock @{name}", + "id": "account.unblock" + }, + { + "defaultMessage": "Unfollow", + "id": "account.unfollow" + }, + { + "defaultMessage": "Unmute @{name}", + "id": "account.unmute" + }, + { + "defaultMessage": "Authorize", + "id": "follow_request.authorize" + }, + { + "defaultMessage": "Reject", + "id": "follow_request.reject" + } + ], + "path": "app/soapbox/features/ui/components/action-button.json" + }, { "descriptors": [ { @@ -5919,6 +6119,15 @@ ], "path": "app/soapbox/features/ui/components/column_forbidden.json" }, + { + "descriptors": [ + { + "defaultMessage": "Edit history", + "id": "compare_history_modal.header" + } + ], + "path": "app/soapbox/features/ui/components/compare_history_modal.json" + }, { "descriptors": [ { @@ -5937,6 +6146,10 @@ "defaultMessage": "Are you sure you want to delete this post?", "id": "confirmations.delete.message" }, + { + "defaultMessage": "Edit post", + "id": "navigation_bar.compose_edit" + }, { "defaultMessage": "Direct message", "id": "navigation_bar.compose_direct" @@ -5974,6 +6187,27 @@ ], "path": "app/soapbox/features/ui/components/confirmation_modal.json" }, + { + "descriptors": [ + { + "defaultMessage": "New to {site_title}?", + "id": "signup_panel.title" + }, + { + "defaultMessage": "Sign up now to discuss what's happening.", + "id": "signup_panel.subtitle" + }, + { + "defaultMessage": "Log in", + "id": "account.login" + }, + { + "defaultMessage": "Sign up", + "id": "account.register" + } + ], + "path": "app/soapbox/features/ui/components/cta-banner.json" + }, { "descriptors": [ { @@ -6016,10 +6250,6 @@ { "defaultMessage": "Embed this post on your website by copying the code below.", "id": "embed.instructions" - }, - { - "defaultMessage": "Here is what it will look like:", - "id": "embed.preview" } ], "path": "app/soapbox/features/ui/components/embed_modal.json" @@ -6035,36 +6265,7 @@ "id": "column.favourites" } ], - "path": "app/soapbox/features/ui/components/modals/favourites-modal.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Edit Profile", - "id": "account.edit_profile" - }, - { - "defaultMessage": "Preferences", - "id": "navigation_bar.preferences" - }, - { - "defaultMessage": "Security", - "id": "navigation_bar.security" - }, - { - "defaultMessage": "Lists", - "id": "column.lists" - }, - { - "defaultMessage": "Bookmarks", - "id": "column.bookmarks" - }, - { - "defaultMessage": "Follow requests", - "id": "navigation_bar.follow_requests" - } - ], - "path": "app/soapbox/features/ui/components/features_panel.json" + "path": "app/soapbox/features/ui/components/favourites_modal.json" }, { "descriptors": [ @@ -6244,6 +6445,10 @@ "defaultMessage": "Domain blocks", "id": "navigation_bar.domain_blocks" }, + { + "defaultMessage": "Soapbox config", + "id": "navigation_bar.soapbox_config" + }, { "defaultMessage": "Follow requests", "id": "navigation_bar.follow_requests" @@ -6252,10 +6457,6 @@ "defaultMessage": "Import data", "id": "navigation_bar.import_data" }, - { - "defaultMessage": "Move account", - "id": "navigation_bar.account_migration" - }, { "defaultMessage": "Logout", "id": "navigation_bar.logout" @@ -6335,10 +6536,118 @@ { "defaultMessage": "Register", "id": "header.register.label" + }, + { + "defaultMessage": "Preview Timeline", + "id": "header.preview_timeline.label" } ], "path": "app/soapbox/features/ui/components/modals/landing-page-modal.json" }, + { + "descriptors": [ + { + "defaultMessage": "You have removed all statuses from being selected.", + "id": "report.reason.blankslate" + }, + { + "defaultMessage": "Done", + "id": "report.done" + }, + { + "defaultMessage": "Next", + "id": "report.next" + }, + { + "defaultMessage": "Close", + "id": "lightbox.close" + }, + { + "defaultMessage": "Additional comments", + "id": "report.placeholder" + }, + { + "defaultMessage": "Submit", + "id": "report.submit" + }, + { + "defaultMessage": "Reporting {target}", + "id": "report.target" + } + ], + "path": "app/soapbox/features/ui/components/modals/report-modal/report-modal.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Thanks for submitting your report.", + "id": "report.confirmation.title" + }, + { + "defaultMessage": "If we find that this account is violating the {link} we will take further action on the matter.", + "id": "report.confirmation.content" + }, + { + "defaultMessage": "Terms of Service", + "id": "shared.tos" + } + ], + "path": "app/soapbox/features/ui/components/modals/report-modal/steps/confirmation-step.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Would you like to add additional statuses to this report?", + "id": "report.otherActions.addAdditional" + }, + { + "defaultMessage": "Add more", + "id": "report.otherActions.addMore" + }, + { + "defaultMessage": "Further actions:", + "id": "report.otherActions.furtherActions" + }, + { + "defaultMessage": "Hide additional statuses", + "id": "report.otherActions.hideAdditional" + }, + { + "defaultMessage": "Include other statuses?", + "id": "report.otherActions.otherStatuses" + }, + { + "defaultMessage": "Do you also want to block this account?", + "id": "report.block_hint" + }, + { + "defaultMessage": "Block {target}", + "id": "report.block" + }, + { + "defaultMessage": "The account is from another server. Send a copy of the report there as well?", + "id": "report.forward_hint" + }, + { + "defaultMessage": "Forward to {target}", + "id": "report.forward" + } + ], + "path": "app/soapbox/features/ui/components/modals/report-modal/steps/other-actions-step.json" + }, + { + "descriptors": [ + { + "defaultMessage": "Additional comments", + "id": "report.placeholder" + }, + { + "defaultMessage": "Reason for reporting", + "id": "report.reason.title" + } + ], + "path": "app/soapbox/features/ui/components/modals/report-modal/steps/reason-step.json" + }, { "descriptors": [ { @@ -6366,6 +6675,22 @@ }, { "descriptors": [ + { + "defaultMessage": "Log in", + "id": "navbar.login.action" + }, + { + "defaultMessage": "Email or username", + "id": "navbar.login.username.placeholder" + }, + { + "defaultMessage": "Password", + "id": "navbar.login.password.label" + }, + { + "defaultMessage": "Forgot password?", + "id": "navbar.login.forgot_password" + }, { "defaultMessage": "Home", "id": "tabs_bar.home" @@ -6381,6 +6706,23 @@ ], "path": "app/soapbox/features/ui/components/navbar.json" }, + { + "descriptors": [ + { + "defaultMessage": "New to {site_title}?", + "id": "signup_panel.title" + }, + { + "defaultMessage": "Sign up now to discuss.", + "id": "signup_panel.subtitle" + }, + { + "defaultMessage": "Sign up", + "id": "account.register" + } + ], + "path": "app/soapbox/features/ui/components/panels/sign-up-panel.json" + }, { "descriptors": [ { @@ -6393,15 +6735,15 @@ { "descriptors": [ { - "defaultMessage": "Add an existing account", - "id": "profile_dropdown.add_account" + "defaultMessage": "{count} {count, plural, one {other} other {others}} you follow", + "id": "account.familiar_followers.more" }, { - "defaultMessage": "Log out @{acct}", - "id": "profile_dropdown.logout" + "defaultMessage": "Followed by {accounts}", + "id": "account.familiar_followers" } ], - "path": "app/soapbox/features/ui/components/profile_dropdown.json" + "path": "app/soapbox/features/ui/components/profile_familiar_followers.json" }, { "descriptors": [ @@ -6493,6 +6835,10 @@ "defaultMessage": "Add an existing account", "id": "profile_dropdown.add_account" }, + { + "defaultMessage": "Theme", + "id": "profile_dropdown.theme" + }, { "defaultMessage": "Log out @{acct}", "id": "profile_dropdown.logout" @@ -6599,7 +6945,7 @@ "id": "account.register" } ], - "path": "app/soapbox/features/ui/components/panels/sign-up-panel.json" + "path": "app/soapbox/features/ui/components/sign_up_panel.json" }, { "descriptors": [ @@ -6612,44 +6958,23 @@ "id": "account.unsubscribe" }, { - "defaultMessage": "Subscribed", - "id": "account.subscribed" + "defaultMessage": "You have subscribed to this account.", + "id": "account.subscribe.success" + }, + { + "defaultMessage": "You have unsubscribed from this account.", + "id": "account.unsubscribe.success" + }, + { + "defaultMessage": "An error occurred trying to subscribe to this account.", + "id": "account.subscribe.failure" + }, + { + "defaultMessage": "An error occurred trying to unsubscribe to this account.", + "id": "account.unsubscribe.failure" } ], - "path": "app/soapbox/features/ui/components/subscription_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Post", - "id": "tabs_bar.post" - }, - { - "defaultMessage": "Home", - "id": "tabs_bar.home" - }, - { - "defaultMessage": "Notifications", - "id": "tabs_bar.notifications" - }, - { - "defaultMessage": "Chats", - "id": "tabs_bar.chats" - }, - { - "defaultMessage": "Dashboard", - "id": "tabs_bar.dashboard" - }, - { - "defaultMessage": "Log In", - "id": "account.login" - }, - { - "defaultMessage": "Sign up", - "id": "account.register" - } - ], - "path": "app/soapbox/features/ui/components/tabs_bar.json" + "path": "app/soapbox/features/ui/components/subscription-button.json" }, { "descriptors": [ @@ -6667,14 +6992,26 @@ { "descriptors": [ { - "defaultMessage": "Trends", - "id": "trends.title" + "defaultMessage": "Light", + "id": "theme_toggle.light" + }, + { + "defaultMessage": "Dark", + "id": "theme_toggle.dark" + }, + { + "defaultMessage": "System", + "id": "theme_toggle.system" } ], - "path": "app/soapbox/features/ui/components/trends_panel.json" + "path": "app/soapbox/features/ui/components/theme-selector.json" }, { "descriptors": [ + { + "defaultMessage": "View all", + "id": "trendsPanel.viewAll" + }, { "defaultMessage": "Trends", "id": "trends.title" @@ -6794,28 +7131,6 @@ ], "path": "app/soapbox/features/ui/components/video_modal.json" }, - { - "descriptors": [ - { - "defaultMessage": "Welcome", - "id": "account.welcome" - } - ], - "path": "app/soapbox/features/ui/components/welcome_button.json" - }, - { - "descriptors": [ - { - "defaultMessage": "Dismiss suggestion", - "id": "suggestions.dismiss" - }, - { - "defaultMessage": "Who To Follow", - "id": "who_to_follow.title" - } - ], - "path": "app/soapbox/features/ui/components/who_to_follow_panel.json" - }, { "descriptors": [ { @@ -6842,6 +7157,23 @@ ], "path": "app/soapbox/features/ui/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "8 characters", + "id": "registration.validation.minimum_characters" + }, + { + "defaultMessage": "1 capital letter", + "id": "registration.validation.capital_letter" + }, + { + "defaultMessage": "1 lowercase letter", + "id": "registration.validation.lowercase_letter" + } + ], + "path": "app/soapbox/features/verification/components/password-indicator.json" + }, { "descriptors": [ { @@ -6879,6 +7211,56 @@ ], "path": "app/soapbox/features/verification/email_passthru.json" }, + { + "descriptors": [ + { + "defaultMessage": "Welcome to {siteTitle}!", + "id": "registrations.success" + }, + { + "defaultMessage": "May only contain A-Z, 0-9, and underscores", + "id": "registrations.username.hint" + }, + { + "defaultMessage": "This username has already been taken.", + "id": "registrations.unprocessable_entity" + }, + { + "defaultMessage": "Failed to register your account.", + "id": "registrations.error" + }, + { + "defaultMessage": "Register your account", + "id": "registration.header" + }, + { + "defaultMessage": "By registering, you agree to the {terms} and {privacy}.", + "id": "registration.acceptance" + }, + { + "defaultMessage": "Terms of Service", + "id": "registration.tos" + }, + { + "defaultMessage": "Privacy Policy", + "id": "registration.privacy" + } + ], + "path": "app/soapbox/features/verification/registration.json" + }, + { + "descriptors": [ + { + "defaultMessage": "You must be {ageMinimum, plural, one {# year} other {# years}} old or older.", + "id": "age_verification.fail" + }, + { + "defaultMessage": "Enter your birth date", + "id": "age_verification.header" + } + ], + "path": "app/soapbox/features/verification/steps/age-verification.json" + }, { "descriptors": [ { @@ -6949,4 +7331,4 @@ ], "path": "app/soapbox/pages/profile_page.json" } -] +] \ No newline at end of file diff --git a/app/soapbox/locales/zh-CN.json b/app/soapbox/locales/zh-CN.json index 7a5aeb1f1..264e941ea 100644 --- a/app/soapbox/locales/zh-CN.json +++ b/app/soapbox/locales/zh-CN.json @@ -4,18 +4,20 @@ "accordion.expand": "展开", "account.add_or_remove_from_list": "从列表中添加或删除", "account.badges.bot": "机器人", - "account.birthday": "Born {date}", - "account.birthday_today": "Birthday is today!", + "account.birthday": "出生于 {date}", + "account.birthday_today": "祝你今天生日快乐!", "account.block": "屏蔽 @{name}", "account.block_domain": "隐藏来自 {domain} 的内容", "account.blocked": "已屏蔽。", "account.chat": "与 @{name} 聊天", - "account.column_settings.description": "These settings apply to all account timelines.", - "account.column_settings.title": "Account timeline settings", "account.deactivated": "帐号被禁用", "account.direct": "发送私信给 @{name}", + "account.domain_blocked": "网域被隐藏", "account.edit_profile": "修改个人资料", "account.endorse": "在个人资料中推荐此用户", + "account.endorse.success": "现在你在个人资料上展示了 @{acct}", + "account.familiar_followers": "被 {accounts} 关注", + "account.familiar_followers.more": "你关注了 {count} 位其他用户", "account.follow": "关注", "account.followers": "关注者", "account.followers.empty": "目前无人关注此用户。", @@ -23,8 +25,8 @@ "account.follows.empty": "此用户目前尚未关注任何人。", "account.follows_you": "关注了你", "account.hide_reblogs": "隐藏来自 @{name} 的转发", - "account.last_status": "Last active", - "account.link_verified_on": "此链接的所有权已在 {date} 被检查", + "account.last_status": "最后活跃", + "account.link_verified_on": "此链接的所有权已在 {date} 被验证", "account.locked_info": "此帐号已上锁。帐号的主人会手动审核关注者。", "account.login": "登录", "account.media": "媒体", @@ -32,45 +34,53 @@ "account.mention": "提及", "account.moved_to": "{name} 已经迁移到:", "account.mute": "隐藏 @{name}", - "account.never_active": "Never", + "account.muted": "已隐藏", + "account.never_active": "从未", "account.posts": "帖文", "account.posts_with_replies": "帖文和回复", "account.profile": "个人资料", "account.register": "注册", - "account.remote_follow": "取消关注", + "account.remote_follow": "远程关注", + "account.remove_from_followers": "删除此关注者", "account.report": "举报 @{name}", "account.requested": "正在等待对方同意。点击以取消发送关注请求。", "account.requested_small": "等待同意", + "account.search": "在 @{name} 的内容中搜索", + "account.search_self": "搜索你的帖文", "account.share": "分享 @{name} 的个人资料", "account.show_reblogs": "显示来自 @{name} 的转发", "account.subscribe": "订阅 @{name}", - "account.subscribed": "已订阅", + "account.subscribe.failure": "尝试订阅此帐户时发生错误", + "account.subscribe.success": "您已订阅此帐号", "account.unblock": "解除屏蔽 @{name}", "account.unblock_domain": "不再隐藏来自 {domain} 的内容", "account.unendorse": "不在个人资料中推荐此用户", + "account.unendorse.success": "你已不再展示 @{acct}", "account.unfollow": "取消关注", "account.unmute": "不再隐藏 @{name}", "account.unsubscribe": "取消订阅 @{name}", - "account.verified": "Verified Account", - "account.welcome": "Welcome", - "account_gallery.none": "没有内容", - "account_note.hint": "You can keep notes about this user for yourself (this will not be shared with them):", - "account_note.placeholder": "No comment provided", - "account_note.save": "Save", - "account_note.target": "Note for @{target}", + "account.unsubscribe.failure": "尝试取消订阅此帐户时发生错误", + "account.unsubscribe.success": "您已取消订阅此帐号", + "account.verified": "已认证账户", + "account_gallery.none": "没有可显示的媒体", + "account_note.hint": "你可以为自己保留关于这个用户的笔记(这不会与他们分享):", + "account_note.placeholder": "没有评论", + "account_note.save": "保存", + "account_note.target": " @{target} 的笔记", "account_search.placeholder": "搜索帐号", - "account_timeline.column_settings.show_pinned": "显示置顶", + "actualStatus.edited": "在 {date} 编辑", + "actualStatuses.quote_tombstone": "帖文不可用", "admin.awaiting_approval.approved_message": "{acct} 的注册申请已通过!", - "admin.awaiting_approval.empty_message": "没有未处理的举报,如果有新的举报,它就会显示在这里。", + "admin.awaiting_approval.empty_message": "没有未处理的注册申请,如果有新的申请,它就会显示在这里。", "admin.awaiting_approval.rejected_message": "{acct} 的注册请求被拒绝。", - "admin.dashboard.registration_mode.approval_hint": "在管理员同意注册请求后,任何人都可以加入。", + "admin.dashboard.registration_mode.approval_hint": "在管理员同意注册请求后才可以加入。", "admin.dashboard.registration_mode.approval_label": "需要审核", - "admin.dashboard.registration_mode.closed_hint": "关闭公开注册,但你仍然可以邀请其他人加入。", + "admin.dashboard.registration_mode.closed_hint": "关闭公开注册,但已注册用户仍然可以邀请其他人加入。", "admin.dashboard.registration_mode.closed_label": "私密", "admin.dashboard.registration_mode.open_hint": "任何人都可以加入。", "admin.dashboard.registration_mode.open_label": "公开", "admin.dashboard.registration_mode_label": "注册模式", - "admin.dashboard.settings_saved": "设定已保存!", + "admin.dashboard.settings_saved": "设置已保存!", "admin.dashcounters.domain_count_label": "互联站点", "admin.dashcounters.mau_label": "月度活跃用户", "admin.dashcounters.retention_label": "用户留存", @@ -78,13 +88,14 @@ "admin.dashcounters.user_count_label": "总用户数", "admin.dashwidgets.email_list_header": "邮件列表", "admin.dashwidgets.software_header": "软件版本", - "admin.latest_accounts_panel.more": "Click to see {count} {count, plural, one {account} other {accounts}}", + "admin.latest_accounts_panel.expand_message": "点击查看更多帐号", + "admin.latest_accounts_panel.more": "点击查看 {count} 个帐号", "admin.latest_accounts_panel.title": "最近帐号", "admin.moderation_log.empty_message": "没有管理记录,如果你进行管理,历史记录就会显示在这里。", "admin.reports.actions.close": "关闭举报", "admin.reports.actions.view_status": "查看帖文", "admin.reports.empty_message": "没有未处理的举报,如果有新的举报,它就会显示在这里。", - "admin.reports.report_closed_message": "对@{name} 的举报已关闭", + "admin.reports.report_closed_message": "对 @{name} 的举报已关闭", "admin.reports.report_title": "举报 {acct} 的帖文", "admin.statuses.actions.delete_status": "删除帖文", "admin.statuses.actions.mark_status_not_sensitive": "不再标记为敏感", @@ -104,29 +115,31 @@ "admin.users.actions.promote_to_admin_message": "@{acct} 已被升级为站长", "admin.users.actions.promote_to_moderator": "升级 @{name} 为站务", "admin.users.actions.promote_to_moderator_message": "@{acct} 已被升级为站务", - "admin.users.actions.remove_donor": "Remove @{name} as a donor", - "admin.users.actions.set_donor": "Set @{name} as a donor", - "admin.users.actions.suggest_user": "Suggest @{name}", - "admin.users.actions.unsuggest_user": "Unsuggest @{name}", + "admin.users.actions.remove_donor": "取消 @{name} 的捐赠者头衔", + "admin.users.actions.set_donor": "设置 @{name} 为捐赠者", + "admin.users.actions.suggest_user": "推荐 @{name}", + "admin.users.actions.unsuggest_user": "取消推荐 @{name}", "admin.users.actions.unverify_user": "取消认证 @{name}", "admin.users.actions.verify_user": "认证帐号 @{name}", - "admin.users.remove_donor_message": "@{acct} was removed as a donor", - "admin.users.set_donor_message": "@{acct} was set as a donor", + "admin.users.remove_donor_message": "@{acct} 从捐赠者列表中移除", + "admin.users.set_donor_message": "@{acct} 被设置为捐赠者", "admin.users.user_deactivated_message": "@{acct} 被禁用。", "admin.users.user_deleted_message": "@{acct} 被删除。", - "admin.users.user_suggested_message": "@{acct} was suggested", - "admin.users.user_unsuggested_message": "@{acct} was unsuggested", + "admin.users.user_suggested_message": "@{acct} 被推荐了", + "admin.users.user_unsuggested_message": "@{acct} 被取消推荐了", "admin.users.user_unverified_message": "@{acct} 被取消认证。", "admin.users.user_verified_message": "@{acct} 被认证。", "admin_nav.awaiting_approval": "等待同意", "admin_nav.dashboard": "管理中心", "admin_nav.reports": "举报", - "alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out).", - "alert.unexpected.browser": "Browser", + "age_verification.fail": "你必须最少满 {ageMinimum} 岁", + "age_verification.header": "输入你的生日", + "alert.unexpected.body": "我们对这次故障感到抱歉。如果问题持续存在,请联系我们的支持团队。你也可以尝试 {clearCookies} (注意:你的帐户会自动退出)。", + "alert.unexpected.browser": "浏览器", "alert.unexpected.clear_cookies": "清除cookie和浏览器数据", - "alert.unexpected.links.help": "Help Center", - "alert.unexpected.links.status": "Status", - "alert.unexpected.links.support": "Support", + "alert.unexpected.links.help": "帮助中心", + "alert.unexpected.links.status": "状态", + "alert.unexpected.links.support": "支持", "alert.unexpected.message": "发生了意外错误。", "alert.unexpected.return_home": "回到首页", "alert.unexpected.title": "哎呀!", @@ -136,16 +149,16 @@ "aliases.search": "搜索旧帐号", "aliases.success.add": "帐号别名创建成功", "aliases.success.remove": "帐号别名删除成功", - "app_create.name_label": "应用别名", + "app_create.name_label": "应用名称", "app_create.name_placeholder": "例如 'Soapbox'", "app_create.redirect_uri_label": "重定向网址", "app_create.restart": "创建另一个", "app_create.results.app_label": "应用", - "app_create.results.explanation_text": "你已创建一个新应用及其令牌,请复制密钥等信息,离开本页面后这些信息将不再展示。", + "app_create.results.explanation_text": "你已成功创建一个新应用及其令牌,请复制密钥等信息,离开本页面后这些信息将不再展示。", "app_create.results.explanation_title": "应用创建成功", "app_create.results.token_label": "OAuth令牌", "app_create.scopes_label": "权限范围", - "app_create.scopes_placeholder": "例如'read write follow'", + "app_create.scopes_placeholder": "例如 '读取 写入 关注'", "app_create.submit": "创建应用", "app_create.website_label": "网站", "auth.invalid_credentials": "用户名或密码错误", @@ -154,8 +167,8 @@ "backups.empty_message": "没有找到备份 {action}。", "backups.empty_message.action": "现在要备份吗?", "backups.pending": "等待备份", - "beta.also_available": "Available in:", - "birthday_panel.title": "Birthdays", + "beta.also_available": "在此时可用:", + "birthday_panel.title": "生日", "boost_modal.combo": "下次按住 {combo} 即可跳过此提示", "bundle_column_error.body": "载入组件时发生错误。", "bundle_column_error.retry": "重试", @@ -163,16 +176,16 @@ "bundle_modal_error.close": "关闭", "bundle_modal_error.message": "载入组件时发生错误。", "bundle_modal_error.retry": "重试", - "card.back.label": "Back", + "card.back.label": "返回", "chat_box.actions.send": "发送", - "chat_box.input.placeholder": "发送聊天信息", + "chat_box.input.placeholder": "发送聊天信息…", "chat_panels.main_window.empty": "还没有聊天信息,找人聊聊吧!", "chat_panels.main_window.title": "聊天", - "chats.actions.delete": "删除", + "chats.actions.delete": "删除信息", "chats.actions.more": "更多选项", "chats.actions.report": "举报用户", - "chats.attachment": "Attachment", - "chats.attachment_image": "Image", + "chats.attachment": "附件", + "chats.attachment_image": "图片", "chats.audio_toggle_off": "关闭声音提醒", "chats.audio_toggle_on": "打开声音提醒", "chats.dividers.today": "此刻", @@ -182,16 +195,16 @@ "column.admin.moderation_log": "管理记录", "column.admin.reports": "举报", "column.admin.reports.menu.moderation_log": "管理记录", - "column.admin.users": "Users", + "column.admin.users": "用户", "column.aliases": "帐号别名", "column.aliases.create_error": "创建别名出错", "column.aliases.delete": "删除", "column.aliases.delete_error": "删除别名出错", - "column.aliases.subheading_add_new": "新增别名", + "column.aliases.subheading_add_new": "添加新别名", "column.aliases.subheading_aliases": "当前别名", "column.app_create": "创建应用", "column.backups": "备份", - "column.birthdays": "Birthdays", + "column.birthdays": "生日", "column.blocks": "已屏蔽的用户", "column.bookmarks": "书签", "column.chats": "聊天", @@ -199,69 +212,71 @@ "column.crypto_donate": "加密货币捐款", "column.developers": "开发者", "column.direct": "私信", - "column.directory": "Browse profiles", + "column.directory": "发现更多", "column.domain_blocks": "已屏蔽的站点", "column.edit_profile": "编辑个人资料", "column.export_data": "导出数据", - "column.favourited_statuses": "Liked posts", + "column.favourited_statuses": "点赞的帖文", "column.favourites": "点赞", "column.federation_restrictions": "联邦限制", "column.filters": "过滤词", - "column.filters.add_new": "添加新的过滤词", + "column.filters.add_new": "新增过滤词", "column.filters.conversations": "对话", "column.filters.create_error": "添加过滤词时出错。", "column.filters.delete": "删除过滤词", "column.filters.delete_error": "删除过滤词时出错。", "column.filters.drop_header": "丢弃而非隐藏", - "column.filters.drop_hint": "被丢弃的帖文会不可逆转地消失,即便移除过滤词之后也一样。", + "column.filters.drop_hint": "被丢弃的帖文会不可逆转地消失,即便移除过滤词之后也不会回复", "column.filters.expires": "失效时间", "column.filters.expires_hint": "还未支持失效时间。", - "column.filters.home_timeline": "本站时间轴", + "column.filters.home_timeline": "主页时间轴", "column.filters.keyword": "关键词", "column.filters.notifications": "通知", "column.filters.public_timeline": "公共时间轴", "column.filters.subheading_add_new": "添加新的过滤词", "column.filters.subheading_filters": "查看已有过滤词", - "column.filters.whole_word_header": "整个词条", + "column.filters.whole_word_header": "全词匹配", "column.filters.whole_word_hint": "如果关键词只包含字母和数字,将只在词语完全匹配时才会应用。", "column.follow_requests": "关注请求", - "column.followers": "Followers", - "column.following": "Following", - "column.groups": "列表", + "column.followers": "关注者", + "column.following": "正在关注", + "column.groups": "群组", "column.home": "主页", "column.import_data": "导入数据", "column.info": "站点信息", "column.lists": "列表", - "column.mentions": "Mentions", + "column.mentions": "提及", "column.mfa": "双重认证", "column.mfa_cancel": "取消", "column.mfa_confirm_button": "确定", "column.mfa_disable_button": "关闭", "column.mfa_setup": "同意并继续", - "column.migration": "Account migration", + "column.migration": "账户迁移", "column.mutes": "已静音的用户", "column.notifications": "通知", - "column.pins": "Pinned posts", + "column.pins": "置顶帖文", "column.preferences": "选项", "column.public": "跨站公共时间轴", - "column.reactions": "Reactions", - "column.reblogs": "Reposts", + "column.reactions": "互动", + "column.reblogs": "转帖", "column.remote": "跨站公共时间轴", "column.scheduled_statuses": "定时帖文", "column.search": "搜索", - "column.security": "安全", - "column.settings_store": "Settings store", + "column.settings_store": "设置储存", "column.soapbox_config": "Soapbox设置", - "column.test": "Test timeline", + "column.test": "测试时间线", "column_back_button.label": "返回", - "column_forbidden.body": "You do not have permission to access this page.", - "column_forbidden.title": "Forbidden", + "column_forbidden.body": "你没有权限访问这个页面。", + "column_forbidden.title": "无权访问", "column_header.show_settings": "显示设置", - "common.cancel": "Cancel", + "common.cancel": "取消", + "common.error": "好像有什么不对劲?尝试重新加载页面", "community.column_settings.media_only": "仅媒体", "community.column_settings.title": "本地时间线设置", + "compare_history_modal.header": "编辑历史", "compose.character_counter.title": "最大字符数: {maxChars}; 已使用 {chars}", - "compose.invalid_schedule": "定时帖文只能设置为五分钟后或更晚", + "compose.edit_success": "你的帖文已编辑", + "compose.invalid_schedule": "定时帖文只能设置为五分钟后或更晚发送", "compose.submit_success": "帖文已发送", "compose_form.direct_message_warning": "这条帖文仅对所有被提及的用户可见。", "compose_form.hashtag_warning": "这条帖文被设置为“不公开”,因此它不会出现在任何话题标签的列表下。只有公开的帖文才能通过话题标签进行搜索。", @@ -273,149 +288,162 @@ "compose_form.placeholder": "在想什么?", "compose_form.poll.add_option": "添加一个选项", "compose_form.poll.duration": "投票持续时间", + "compose_form.poll.multiselect": "多选", + "compose_form.poll.multiselect_detail": "允许用户多选答案", "compose_form.poll.option_placeholder": "选项 {number}", + "compose_form.poll.remove": "删除投票", "compose_form.poll.remove_option": "移除这个选项", "compose_form.poll.switch_to_multiple": "投票改为多选", "compose_form.poll.switch_to_single": "投票改为单选", + "compose_form.poll_placeholder": "添加投票主题...", "compose_form.publish": "发布", "compose_form.publish_loud": "{publish}!", + "compose_form.save_changes": "保存更改", "compose_form.schedule": "定时发布", "compose_form.scheduled_statuses.click_here": "点击此处", "compose_form.scheduled_statuses.message": "你有定时帖文,{click_here}查看。", - "compose_form.sensitive.hide": "标记媒体为敏感内容", - "compose_form.sensitive.marked": "媒体已被标记为敏感内容。", - "compose_form.sensitive.unmarked": "媒体未被标记为敏感内容。", "compose_form.spoiler.marked": "正文已被折叠在警告信息之后。", "compose_form.spoiler.unmarked": "正文未被折叠。", "compose_form.spoiler_placeholder": "折叠部分的警告消息", + "compose_form.spoiler_remove": "移除敏感内容", + "compose_form.spoiler_title": "敏感内容", "confirmation_modal.cancel": "取消", - "confirmations.admin.deactivate_user.confirm": "禁用 @{name}", - "confirmations.admin.deactivate_user.heading": "Deactivate @{acct}", - "confirmations.admin.deactivate_user.message": "你确定要禁用帐号 @{acct} 吗?这无法撤回!", + "confirmations.admin.deactivate_user.confirm": "禁用帐号 @{name}", + "confirmations.admin.deactivate_user.heading": "禁用帐号 @{acct}", + "confirmations.admin.deactivate_user.message": "你确定要禁用帐号 @{acct} 吗?该操作无法撤回!", "confirmations.admin.delete_local_user.checkbox": "我确定我要删除本站帐号", "confirmations.admin.delete_status.confirm": "删除帖文", - "confirmations.admin.delete_status.heading": "Delete post", - "confirmations.admin.delete_status.message": "你确定要删除 @{acct} 的帖文吗?这无法撤回!", + "confirmations.admin.delete_status.heading": "删除帖文", + "confirmations.admin.delete_status.message": "你确定要删除帖文 @{acct} 吗?该操作无法撤回!", "confirmations.admin.delete_user.confirm": "删除帐号 @{name}", - "confirmations.admin.delete_user.heading": "Delete @{acct}", - "confirmations.admin.delete_user.message": "你确定要删除本站帐号 @{acct} 吗?这无法撤回!", - "confirmations.admin.mark_status_not_sensitive.confirm": "不再标记为敏感帖文", - "confirmations.admin.mark_status_not_sensitive.heading": "Mark post not sensitive.", - "confirmations.admin.mark_status_not_sensitive.message": "你要标记帐号 @{acct} 的帖文不再敏感", + "confirmations.admin.delete_user.heading": "删除帐号 @{acct}", + "confirmations.admin.delete_user.message": "你确定要删除本站帐号 @{acct} 吗?该操作无法撤回!", + "confirmations.admin.mark_status_not_sensitive.confirm": "敏感为不敏感", + "confirmations.admin.mark_status_not_sensitive.heading": "敏感为不敏感", + "confirmations.admin.mark_status_not_sensitive.message": "你要标记帐号 @{acct} 的帖文为不敏感", "confirmations.admin.mark_status_sensitive.confirm": "标记为敏感帖文", - "confirmations.admin.mark_status_sensitive.heading": "Mark post sensitive", + "confirmations.admin.mark_status_sensitive.heading": "标记为敏感帖文", "confirmations.admin.mark_status_sensitive.message": "你要标记帐号 @{acct} 的帖文为敏感", - "confirmations.admin.reject_user.confirm": "Reject @{name}", - "confirmations.admin.reject_user.heading": "Reject @{acct}", - "confirmations.admin.reject_user.message": "You are about to reject @{acct} registration request. This action cannot be undone.", + "confirmations.admin.reject_user.confirm": "拒绝 @{name}", + "confirmations.admin.reject_user.heading": "拒绝 @{acct}", + "confirmations.admin.reject_user.message": "你正准备拒绝 @{acct} 的注册请求。这一操作不能撤销。", "confirmations.block.block_and_report": "屏蔽与举报", "confirmations.block.confirm": "屏蔽", - "confirmations.block.heading": "Block @{name}", + "confirmations.block.heading": "屏蔽 @{name}", "confirmations.block.message": "你确定要屏蔽 {name} 吗?", "confirmations.delete.confirm": "删除", - "confirmations.delete.heading": "Delete post", + "confirmations.delete.heading": "删除帖文", "confirmations.delete.message": "你确定要删除这条帖文吗?", "confirmations.delete_list.confirm": "删除", - "confirmations.delete_list.heading": "Delete list", + "confirmations.delete_list.heading": "删除列表", "confirmations.delete_list.message": "你确定要永久删除这个列表吗?", - "confirmations.domain_block.confirm": "隐藏整个站点的内容", - "confirmations.domain_block.heading": "Block {domain}", - "confirmations.domain_block.message": "你真的确定要隐藏所有来自 {domain} 的内容吗?多数情况下,屏蔽或隐藏几个特定的用户就已经足够了。来自该站点的内容将不再出现在你的任何公共时间轴或通知列表里。来自该站点的关注者将会被移除。", + "confirmations.domain_block.confirm": "阻止整个站点的内容", + "confirmations.domain_block.heading": "阻止 {domain}", + "confirmations.domain_block.message": "你真的确定要阻止所有来自 {domain} 的内容吗?多数情况下,阻止几个特定的用户就已经足够了。来自该站点的内容将不再出现在你的任何公共时间轴或通知列表里。来自该站点的关注者将会被移除。", "confirmations.mute.confirm": "隐藏", - "confirmations.mute.heading": "Mute @{name}", + "confirmations.mute.heading": "隐藏 @{name}", "confirmations.mute.message": "你确定要隐藏 {name} 吗?", "confirmations.redraft.confirm": "删除并重新编辑", - "confirmations.redraft.heading": "Delete & redraft", + "confirmations.redraft.heading": "删除并重新编辑", "confirmations.redraft.message": "你确定要删除这条帖文并重新编辑它吗?所有相关的转发和点赞都会被清除,回复将会失去关联。", "confirmations.register.needs_approval": "你的帐号在被管理员审核,请稍等", - "confirmations.register.needs_approval.header": "Approval needed", - "confirmations.register.needs_confirmation": "请检查你的邮箱 {email} ,我们需要邮箱验证注册", - "confirmations.register.needs_confirmation.header": "Confirmation needed", + "confirmations.register.needs_approval.header": "需要审核", + "confirmations.register.needs_confirmation": "我们已经发送了指示到你的邮箱 {email} 中,请检查收件箱并点击邮件中的链接以继续。", + "confirmations.register.needs_confirmation.header": "需要验证", + "confirmations.remove_from_followers.confirm": "删除", + "confirmations.remove_from_followers.message": "确定要从你的关注者中删除 {name} 吗?", "confirmations.reply.confirm": "回复", "confirmations.reply.message": "回复此消息将会覆盖当前正在编辑的信息。确定继续吗?", - "confirmations.scheduled_status_delete.confirm": "Cancel", - "confirmations.scheduled_status_delete.heading": "Cancel scheduled post", - "confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?", + "confirmations.scheduled_status_delete.confirm": "取消", + "confirmations.scheduled_status_delete.heading": "取消帖文定时发布", + "confirmations.scheduled_status_delete.message": "你确定要取消这篇帖文的定时发布吗?", "confirmations.unfollow.confirm": "取消关注", - "confirmations.unfollow.heading": "Unfollow {name}", + "confirmations.unfollow.heading": "取消关注 {name}", "confirmations.unfollow.message": "你确定要取消关注 {name} 吗?", "crypto_donate.explanation_box.message": "{siteTitle} 接受用户向以下钱包地址捐赠任意数量的加密货币。感谢你的支持!", "crypto_donate.explanation_box.title": "发送加密货币捐赠", - "crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}", + "crypto_donate_panel.actions.view": "点击查看 {count} {count, plural, one {wallet} other {wallets}}", "crypto_donate_panel.heading": "捐赠加密货币", "crypto_donate_panel.intro.message": "{siteTitle} 接受用户捐赠加密货币。感谢你的支持!", + "datepicker.day": "日", "datepicker.hint": "设定发送时间……", - "datepicker.next_month": "Next month", - "datepicker.next_year": "Next year", - "datepicker.previous_month": "Previous month", - "datepicker.previous_year": "Previous year", - "developers.challenge.answer_label": "Answer", - "developers.challenge.answer_placeholder": "Your answer", - "developers.challenge.fail": "Wrong answer", - "developers.challenge.message": "What is the result of calling {function}?", - "developers.challenge.submit": "Become a developer", - "developers.challenge.success": "You are now a developer", - "developers.leave": "You have left developers", + "datepicker.month": "月", + "datepicker.next_month": "下个月", + "datepicker.next_year": "明年", + "datepicker.previous_month": "上个月", + "datepicker.previous_year": "去年", + "datepicker.year": "年", + "developers.challenge.answer_label": "回答", + "developers.challenge.answer_placeholder": "你的回答", + "developers.challenge.fail": "回答错误", + "developers.challenge.message": "调用函数 {function} 的结果是什么?", + "developers.challenge.submit": "成为开发者", + "developers.challenge.success": "你已成为开发者", + "developers.leave": "你已退出开发者", "developers.navigation.app_create_label": "创建应用", - "developers.navigation.intentional_error_label": "触发一个错误", - "developers.navigation.leave_developers_label": "Leave developers", - "developers.navigation.network_error_label": "Network error", - "developers.navigation.settings_store_label": "Settings store", - "developers.navigation.test_timeline_label": "Test timeline", - "developers.settings_store.hint": "It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.", + "developers.navigation.intentional_error_label": "触发错误", + "developers.navigation.leave_developers_label": "退出开发者", + "developers.navigation.network_error_label": "网络错误", + "developers.navigation.settings_store_label": "设置储存", + "developers.navigation.test_timeline_label": "测试时间线", + "developers.settings_store.hint": "在这里可以直接编辑你的用户设置。请注意! 编辑这一部分会破坏你的账户,你只能通过API恢复。", + "direct.body": "一个全新的私聊系统将很快推出。敬请期待!", "direct.search_placeholder": "发送私信给……", - "directory.federated": "From known fediverse", - "directory.local": "From {domain} only", - "directory.new_arrivals": "New arrivals", - "directory.recently_active": "Recently active", + "directory.federated": "来自已知联邦宇宙", + "directory.local": "仅来自 {domain}", + "directory.new_arrivals": "新增访客", + "directory.recently_active": "最近活跃", "edit_federation.followers_only": "对关注者以外的用户隐藏帖文", "edit_federation.force_nsfw": "将附件强制标记为敏感", "edit_federation.media_removal": "去掉媒体", "edit_federation.reject": "拒绝所有信息交互", "edit_federation.save": "保存", "edit_federation.success": "{host} 联邦设定已保存", - "edit_federation.unlisted": "将帖文强制标记为不公开", - "edit_password.header": "Change Password", + "edit_federation.unlisted": "将帖文强制标记移出公共时间轴", + "edit_password.header": "修改密码", "edit_profile.error": "个人资料更新失败", "edit_profile.fields.accepts_email_list_label": "接收邮件列表", "edit_profile.fields.avatar_label": "头像", "edit_profile.fields.bio_label": "简介", - "edit_profile.fields.bio_placeholder": "请介绍一下你自己。", - "edit_profile.fields.birthday_label": "Birthday", - "edit_profile.fields.birthday_placeholder": "Your birthday", - "edit_profile.fields.bot_label": "这是一个机器人帐号", - "edit_profile.fields.discoverable_label": "Allow account discovery", + "edit_profile.fields.bio_placeholder": "请介绍一下你自己", + "edit_profile.fields.birthday_label": "生日", + "edit_profile.fields.birthday_placeholder": "你的生日", + "edit_profile.fields.bot_label": "这是一个机器人账户", + "edit_profile.fields.discoverable_label": "允许别人发现你", "edit_profile.fields.display_name_label": "昵称", "edit_profile.fields.display_name_placeholder": "你的昵称", "edit_profile.fields.header_label": "个人资料页横幅图片", - "edit_profile.fields.hide_network_label": "隐藏关注信息", - "edit_profile.fields.location_label": "Location", - "edit_profile.fields.location_placeholder": "Location", - "edit_profile.fields.locked_label": "保护你的帐号", + "edit_profile.fields.hide_network_label": "隐藏网络状态", + "edit_profile.fields.location_label": "地点", + "edit_profile.fields.location_placeholder": "地点", + "edit_profile.fields.locked_label": "锁定账户", "edit_profile.fields.meta_fields.content_placeholder": "内容", "edit_profile.fields.meta_fields.label_placeholder": "标签", - "edit_profile.fields.stranger_notifications_label": "不接收来自陌生人的通知", - "edit_profile.fields.website_label": "Website", - "edit_profile.fields.website_placeholder": "Display a Link", - "edit_profile.header": "Edit Profile", + "edit_profile.fields.verified_display_name": "经过验证的用户无法修改昵称", + "edit_profile.fields.meta_fields_label": "自定义个人资料字段", + "edit_profile.fields.stranger_notifications_label": "屏蔽来自陌生人的通知", + "edit_profile.fields.website_label": "网站", + "edit_profile.fields.website_placeholder": "显示链接", + "edit_profile.header": "编辑个人资料", "edit_profile.hints.accepts_email_list": "接收新闻和推广邮件", - "edit_profile.hints.avatar": "只支持 PNG、GIF 或 JPG 格式,大小不超过{size}。图片分辨率将会压缩至 1500x500px。", - "edit_profile.hints.bot": "来自这个帐号的绝大多数操作都是自动进行的,并且可能无人监控。", - "edit_profile.hints.discoverable": "Display account in profile directory and allow indexing by external services", - "edit_profile.hints.header": "只支持 PNG、GIF 或 JPG 格式,大小不超过{size}。图片分辨率将会压缩至 1500x500px。", - "edit_profile.hints.hide_network": "个人资料中不显示你关注的用户和关注你的用户。", - "edit_profile.hints.locked": "你需要手动审核所有关注请求。", - "edit_profile.hints.stranger_notifications": "只显示来自你所关注用户的通知。", + "edit_profile.hints.avatar": "只支持 PNG、GIF 或 JPG 格式,大小将会被缩放至 {size}。", + "edit_profile.hints.bot": "此帐号主要执行自动化操作,可能不受用户控制", + "edit_profile.hints.discoverable": "在配置文件目录中显示帐户并允许外部服务索引", + "edit_profile.hints.header": "只支持 PNG、GIF 或 JPG 格式,大小将会被缩放至 {size}。", + "edit_profile.hints.hide_network": "您关注的人和关注您的人不会显示在您的个人资料中", + "edit_profile.hints.locked": "需要您手动批准关注请求", + "edit_profile.hints.meta_fields": "你能在个人资料页面上最多显示 {count} 条自定义信息", + "edit_profile.hints.stranger_notifications": "仅显示来自您关注的人的通知", "edit_profile.save": "保存", - "edit_profile.success": "个人资料已保存。", - "email_passthru.confirmed.body": "Close this tab and continue the registration process on the {bold} from which you sent this email confirmation.", - "email_passthru.confirmed.heading": "Email Confirmed!", - "email_passthru.generic_fail.body": "Please request a new email confirmation.", - "email_passthru.generic_fail.heading": "Something Went Wrong", - "email_passthru.token_expired.body": "Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.", - "email_passthru.token_expired.heading": "Token Expired", - "email_passthru.token_not_found.body": "Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.", - "email_passthru.token_not_found.heading": "Invalid Token", + "edit_profile.success": "个人资料已保存", + "email_passthru.confirmed.body": "关闭此标签,并在您发送此电子邮件确认的 {bold} 上继续注册过程", + "email_passthru.confirmed.heading": "邮箱地址已确认!", + "email_passthru.generic_fail.body": "请重新请求确认邮件。", + "email_passthru.generic_fail.heading": "有点不对劲…", + "email_passthru.token_expired.body": "您的电子邮件令牌已经过期。请从您发送此电子邮件确认的 {bold} 处申请新的电子邮件确认。", + "email_passthru.token_expired.heading": "令牌已经过期", + "email_passthru.token_not_found.body": "您的电子邮件令牌已经过期。请从您发送此电子邮件确认的 {bold} 处申请新的电子邮件确认。", + "email_passthru.token_not_found.heading": "非法令牌!", "embed.instructions": "要在你的站点上嵌入这条帖文,请复制以下代码:", "embed.preview": "它会像这样显示出来:", "emoji_button.activity": "活动", @@ -432,7 +460,7 @@ "emoji_button.search_results": "搜索结果", "emoji_button.symbols": "符号", "emoji_button.travel": "旅行和地点", - "empty_column.account_blocked": "你被 @{accountUsername} 屏蔽了。", + "empty_column.account_blocked": "你被 @{accountUsername} 屏蔽了", "empty_column.account_favourited_statuses": "没有点赞帖文", "empty_column.account_timeline": "这里没有帖文!", "empty_column.account_unavailable": "个人资料不可用", @@ -462,6 +490,7 @@ "empty_column.search.accounts": "无帐号匹配 \"{term}\"", "empty_column.search.hashtags": "无标签匹配 \"{term}\"", "empty_column.search.statuses": "无帖文匹配 \"{term}\"", + "empty_column.test": "测试时间线是空的。", "export_data.actions.export": "导出", "export_data.actions.export_blocks": "导出屏蔽列表", "export_data.actions.export_follows": "导出关注列表", @@ -475,19 +504,21 @@ "export_data.success.blocks": "屏蔽列表导出完毕", "export_data.success.followers": "关注列表导出完毕", "export_data.success.mutes": "静音列表导出完毕", - "federation_restriction.federated_timeline_removal": "Fediverse timeline removal", - "federation_restriction.followers_only": "Hidden except to followers", - "federation_restriction.full_media_removal": "Full media removal", - "federation_restriction.media_nsfw": "Attachments marked NSFW", - "federation_restriction.partial_media_removal": "Partial media removal", - "federation_restrictions.empty_message": "{siteTitle} has not restricted any instances.", - "federation_restrictions.explanation_box.message": "Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.", - "federation_restrictions.explanation_box.title": "Instance-specific policies", - "federation_restrictions.not_disclosed_message": "{siteTitle} does not disclose federation restrictions through the API.", + "federation_restriction.federated_timeline_removal": "从联邦宇宙时间线移除", + "federation_restriction.followers_only": "仅关注者可见", + "federation_restriction.full_media_removal": "完全移除媒体", + "federation_restriction.media_nsfw": "附件标注为 NSFW", + "federation_restriction.partial_media_removal": "部分移除媒体", + "federation_restrictions.empty_message": "{siteTitle} 没有限制任何实例。", + "federation_restrictions.explanation_box.message": "通常情况下,联邦宇宙上的服务器可以自由通信。然而 {siteTitle} 对以下服务器实施了限制。", + "federation_restrictions.explanation_box.title": "实例相关政策", + "federation_restrictions.not_disclosed_message": "{siteTitle} 没有通过API向联邦宇宙披露限制。", "fediverse_tab.explanation_box.dismiss": "不再显示", "fediverse_tab.explanation_box.explanation": "{site_title} 是联邦宇宙的一部分, 一个由数个站点组成的社交网络。你在这里看到的帖文来自其他站点。你可以自由地与他们打交道,或者屏蔽任何你不喜欢的站点。请注意第二个@符号后的完整用户名,以了解帖文来自哪个站点。要想只看到 {site_title} 的帖文,请访问 {local} 。", "fediverse_tab.explanation_box.title": "什么是联邦宇宙?", - "filters.added": "过滤已添加", + "feed_suggestions.heading": "建议的个人资料", + "feed_suggestions.view_all": "查看全部", + "filters.added": "过滤条件已添加", "filters.context_header": "过滤词场景", "filters.context_hint": "过滤词的应用场景", "filters.filters_list_context_label": "过滤词场景:", @@ -498,56 +529,46 @@ "filters.filters_list_phrase_label": "关键词:", "filters.filters_list_whole-word": "整个词条", "filters.removed": "过滤已移除", - "follow_recommendation.subhead": "Let's get started!", + "follow_recommendation.subhead": "让我们开始吧!", "follow_recommendations.done": "完成", - "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", - "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", + "follow_recommendations.heading": "关注你感兴趣的用户,这里是我们的推荐清单", + "follow_recommendations.lead": "你关注的人的帖子将按时间顺序显示在你的主页上。不用担心搞错,你可以在任何时候轻松地取消对别人的关注!", "follow_request.authorize": "同意", "follow_request.reject": "拒绝", "forms.copy": "复制", - "forms.hide_password": "Hide password", - "forms.show_password": "Show password", + "forms.hide_password": "隐藏密码", + "forms.show_password": "显示密码", "getting_started.open_source_notice": "{code_name} 是开源软件。欢迎前往 GitLab({code_link} (v{code_version}))贡献代码或反馈问题。", - "group.detail.archived_group": "Archived group", - "group.members.empty": "这个列表没有任何帐号。", + "group.members.empty": "这个列表没有任何帐号", "group.removed_accounts.empty": "这个列表没有被移除的帐号", "groups.card.join": "加入", "groups.card.members": "成员", - "groups.card.roles.admin": "You're an admin", - "groups.card.roles.member": "You're a member", + "groups.card.roles.admin": "你是管理员", + "groups.card.roles.member": "你是成员", "groups.card.view": "查看", - "groups.create": "创建列表", - "groups.detail.role_admin": "You're an admin", - "groups.edit": "Edit", - "groups.form.coverImage": "上传(可选)", + "groups.create": "创建群组", + "groups.form.coverImage": "上传横幅图片(可选)", "groups.form.coverImageChange": "横幅图片已上传", - "groups.form.create": "创建列表", + "groups.form.create": "创建群组", "groups.form.description": "描述", "groups.form.title": "标题", - "groups.form.update": "更新列表", - "groups.join": "Join group", - "groups.leave": "Leave group", + "groups.form.update": "更新群组", "groups.removed_accounts": "已移除帐号", - "groups.sidebar-panel.item.no_recent_activity": "No recent activity", - "groups.sidebar-panel.item.view": "new posts", - "groups.sidebar-panel.show_all": "Show all", - "groups.sidebar-panel.title": "Groups You're In", "groups.tab_admin": "管理", "groups.tab_featured": "热门", "groups.tab_member": "成员", - "hashtag.column_header.tag_mode.all": "以及 {additional}", - "hashtag.column_header.tag_mode.any": "或是 {additional}", - "hashtag.column_header.tag_mode.none": "而不用 {additional}", + "hashtag.column_header.tag_mode.all": "以及{additional}", + "hashtag.column_header.tag_mode.any": "或是{additional}", + "hashtag.column_header.tag_mode.none": "而不用{additional}", "header.home.label": "主页", - "header.login.forgot_password": "Forgot password?", + "header.login.forgot_password": "忘记了密码?", "header.login.label": "登录", - "header.login.password.label": "Password", - "header.login.username.placeholder": "Email or username", - "header.register.label": "Register", - "home.column_settings.show_direct": "显示私信", + "header.login.password.label": "密码", + "header.login.username.placeholder": "邮箱或用户名", + "header.preview_timeline.label": "浏览首页", + "header.register.label": "注册", "home.column_settings.show_reblogs": "显示转发", "home.column_settings.show_replies": "显示回复", - "home.column_settings.title": "Home settings", "icon_button.icons": "图标", "icon_button.label": "选择图标", "icon_button.not_found": "没有图标! (╯°□°)╯︵ ┻━┻", @@ -564,24 +585,11 @@ "import_data.success.blocks": "屏蔽帐号列表导入完成", "import_data.success.followers": "关注帐号列表导入完成", "import_data.success.mutes": "静音帐号列表导入完成", - "input.password.hide_password": "Hide password", - "input.password.show_password": "Show password", + "input.password.hide_password": "隐藏密码", + "input.password.show_password": "显示密码", "intervals.full.days": "{number} 天", "intervals.full.hours": "{number} 小时", "intervals.full.minutes": "{number} 分钟", - "introduction.federation.action": "下一步", - "introduction.federation.home.headline": "主页", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.interactions.action": "Finish tutorial!", - "introduction.interactions.favourite.headline": "Favorite", - "introduction.interactions.favourite.text": "You can save a post for later, and let the author know that you liked it, by favoriting it.", - "introduction.interactions.reblog.headline": "Repost", - "introduction.interactions.reblog.text": "You can share other people's posts with your followers by reposting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own posts, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", "keyboard_shortcuts.back": "返回上一页", "keyboard_shortcuts.blocked": "打开被屏蔽用户列表", "keyboard_shortcuts.boost": "转发", @@ -610,17 +618,17 @@ "keyboard_shortcuts.toot": "发帖", "keyboard_shortcuts.unfocus": "取消输入", "keyboard_shortcuts.up": "在列表中让光标上移", - "landing_page_modal.download": "Download", - "landing_page_modal.helpCenter": "Help Center", + "landing_page_modal.download": "下载", + "landing_page_modal.helpCenter": "帮助中心", "lightbox.close": "关闭", "lightbox.next": "下一个", "lightbox.previous": "上一个", "lightbox.view_context": "查看上下文", "list.click_to_add": "点击以添加帐号到列表", - "list_adder.header_title": "在列表内添加或删除帐号", + "list_adder.header_title": "从列表中添加或删除帐号", "lists.account.add": "添加到列表", "lists.account.remove": "从列表中移除", - "lists.delete": "Delete list", + "lists.delete": "删除列表", "lists.edit": "编辑列表", "lists.edit.submit": "更改列表", "lists.new.create": "新建列表", @@ -635,48 +643,59 @@ "login.fields.otp_code_hint": "输入双重认证应用里的代码,或者输入恢复代码", "login.fields.otp_code_label": "双重认证代码:", "login.fields.password_placeholder": "密码", - "login.fields.username_label": "Email or username", + "login.fields.username_label": "邮箱或用户名", "login.log_in": "登录", - "login.otp_log_in": "OTP方式登录", + "login.otp_log_in": "双重认证登录", + "login.otp_log_in.fail": "代码无效,请重新输入", "login.reset_password_hint": "登录时出现问题了吗?", - "login.sign_in": "Sign in", + "login.sign_in": "登录", + "login_form.header": "登录", "media_gallery.toggle_visible": "切换显示/隐藏", "media_panel.empty_message": "未找到媒体", "media_panel.title": "媒体", - "mfa.confirm.success_message": "MFA confirmed", - "mfa.disable.success_message": "MFA disabled", + "mfa.confirm.success_message": "多重身份认证 (MFA) 已成功启用", + "mfa.disable.success_message": "多重身份认证 (MFA) 已成功禁用", "mfa.mfa_disable_enter_password": "输入帐号密码以禁用双重认证:", - "mfa.mfa_setup.code_hint": "Enter the code from your two-factor app.", - "mfa.mfa_setup.code_placeholder": "Code", - "mfa.mfa_setup.password_hint": "Enter your current password to confirm your identity.", - "mfa.mfa_setup.password_placeholder": "Password", + "mfa.mfa_setup.code_hint": "输入显示在双重认证应用里的代码", + "mfa.mfa_setup.code_placeholder": "代码", + "mfa.mfa_setup.password_hint": "输入你的当前密码来确认身份", + "mfa.mfa_setup.password_placeholder": "密码", "mfa.mfa_setup_scan_description": "请使用 Google 身份验证器或其他的TOTP双重认证手机应用扫描此处的二维码。启用双重认证后,在登录时,你需要输入该应用生成的代码。", "mfa.mfa_setup_scan_title": "如果你无法扫描二维码,请手动输入下列文本:", "mfa.mfa_setup_verify_title": "启用", "mfa.otp_enabled_description": "你已经启用双重认证。", "mfa.otp_enabled_title": "双重认证已启用", "mfa.setup_recoverycodes": "恢复代码", - "mfa.setup_warning": "请立即将恢复代码保存或写到纸上,否则你可能无法登录帐号。", - "migration.fields.acct.label": "Handle of the new account", - "migration.fields.acct.placeholder": "username@domain", - "migration.fields.confirm_password.label": "Current password", - "migration.hint": "This will move your followers to the new account. No other data will be moved. To perform migration, you need to {link} on your new account first.", - "migration.hint.link": "create an account alias", - "migration.move_account.fail": "Account migration failed.", - "migration.move_account.success": "Account successfully moved.", - "migration.submit": "Move followers", + "mfa.setup_warning": "请立即将恢复代码保存或写到纸上,并建议与个人信息分开存放,否则在丢失双重认证应用时,你可能无法登录帐号。", + "migration.fields.acct.label": "新账户的用户名", + "migration.fields.acct.placeholder": "用户名@域名", + "migration.fields.confirm_password.label": "当前密码", + "migration.hint": "这将把你的关注者转移到新账户。没有其他数据会被转移。要执行迁移,你需要先{link}你的新账户", + "migration.hint.link": "创建一个账户别名", + "migration.move_account.fail": "账户迁移失败。", + "migration.move_account.success": "账户迁移成功了!", + "migration.submit": "迁移关注者", "missing_description_modal.cancel": "取消", "missing_description_modal.continue": "发布", "missing_description_modal.description": "仍然继续发布吗?", - "missing_description_modal.text": "附件没有描述信息。", + "missing_description_modal.text": "附件没有描述信息。仍然继续发布吗?", "missing_indicator.label": "找不到内容", "missing_indicator.sublabel": "无法找到此资源", - "mobile.also_available": "Available in:", - "morefollows.followers_label": "和{count} 来自其他站点的 {count, plural, one {关注者} other {关注者}} 。", - "morefollows.following_label": "和{count} 来自其他站点的 {count, plural, one {正在关注} other {正在关注}} 。", + "mobile.also_available": "在此处可用:", + "moderation_overlay.contact": "联系管理员", + "moderation_overlay.hide": "隐藏内容", + "moderation_overlay.show": "显示内容", + "moderation_overlay.subtitle": "此帖文已发送至站点管理员以供审核,目前仅自己可见。如果你认为这是系统错误,请联系管理员以获取支持。", + "moderation_overlay.title": "内容审核中", + "morefollows.followers_label": "和{count}来自其他站点的{count, plural, one {关注者} other {关注者}} 。", + "morefollows.following_label": "和{count}来自其他站点的{count, plural, one {正在关注} other {正在关注}} 。", "mute_modal.hide_notifications": "同时隐藏来自这个用户的通知?", + "navbar.login.action": "登录", + "navbar.login.forgot_password": "忘记密码?", + "navbar.login.password.label": "密码", + "navbar.login.username.placeholder": "邮箱或用户名", "navigation.chats": "聊天", - "navigation.compose": "Compose", + "navigation.compose": "发布新帖文", "navigation.dashboard": "管理中心", "navigation.developers": "开发者", "navigation.direct_messages": "私信", @@ -684,61 +703,43 @@ "navigation.invites": "邀请", "navigation.notifications": "通知", "navigation.search": "搜索", - "navigation_bar.account_migration": "Move account", + "navigation_bar.account_aliases": "用户别名", + "navigation_bar.account_migration": "迁移账户", "navigation_bar.blocks": "屏蔽", "navigation_bar.compose": "撰写新帖", "navigation_bar.compose_direct": "撰写私信", - "navigation_bar.compose_quote": "Quote post", - "navigation_bar.compose_reply": "Reply to post", + "navigation_bar.compose_edit": "编辑帖文", + "navigation_bar.compose_quote": "引用帖文", + "navigation_bar.compose_reply": "回复帖文", "navigation_bar.domain_blocks": "屏蔽站点", "navigation_bar.favourites": "点赞的内容", "navigation_bar.filters": "过滤", "navigation_bar.follow_requests": "关注请求", "navigation_bar.import_data": "导入数据", - "navigation_bar.in_reply_to": "In reply to", + "navigation_bar.in_reply_to": "在回复中", "navigation_bar.invites": "邀请", "navigation_bar.logout": "登出", "navigation_bar.mutes": "静音", "navigation_bar.preferences": "首选项", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.security": "安全", + "navigation_bar.profile_directory": "发现更多用户", "navigation_bar.soapbox_config": "Soapbox设置", - "notification.birthday": "{name} has a birthday today", - "notification.birthday.more": "{count} more {count, plural, one {friend} other {friends}}", - "notification.birthday_plural": "{name} and {more} have birthday today", - "notification.pleroma:chat_mention": "{name} 回复了你", + "notification.birthday": "{name} 今天生日啦!", + "notification.birthday.more": "{count} 位朋友", + "notification.birthday_plural": "{name} 和 {more} 今天生日", "notification.favourite": "{name} 赞了你的帖文", "notification.follow": "{name} 开始关注你", "notification.follow_request": "{name} 请求关注你", "notification.mention": "{name} 提及了你", - "notification.move": "{name} moved to {targetName}", - "notification.pleroma:emoji_reaction": "{name} 对你的帖文回应了表情", - "notification.poll": "你参与的一个投票已经结束", - "notification.reblog": "{name} 转发了你的帖文", - "notification.status": "{name} just posted", - "notifications.clear": "清空通知列表", - "notifications.clear_confirmation": "你确定要永久清空通知列表吗?", - "notifications.clear_heading": "Clear notifications", - "notifications.column_settings.alert": "桌面通知", - "notifications.column_settings.birthdays.category": "Birthdays", - "notifications.column_settings.birthdays.show": "Show birthday reminders", - "notifications.column_settings.emoji_react": "用Emoji互动:", - "notifications.column_settings.favourite": "当你的帖文被点赞时:", - "notifications.column_settings.filter_bar.advanced": "显示所有类别", - "notifications.column_settings.filter_bar.category": "快速过滤栏", - "notifications.column_settings.filter_bar.show": "显示", - "notifications.column_settings.follow": "当有人关注你时:", - "notifications.column_settings.follow_request": "新的关注请求:", - "notifications.column_settings.mention": "当有人在帖文中提及你时:", - "notifications.column_settings.move": "迁移:", - "notifications.column_settings.poll": "投票结果:", - "notifications.column_settings.push": "推送通知", - "notifications.column_settings.reblog": "当有人转发了你的帖文时:", - "notifications.column_settings.show": "在通知栏显示", - "notifications.column_settings.sound": "播放音效", - "notifications.column_settings.sounds": "音效设置", - "notifications.column_settings.sounds.all_sounds": "收到所有类型的通知时播放音效", - "notifications.column_settings.title": "通知设置", + "notification.mentioned": "{name} 提及了你", + "notification.move": "{name} 移动到了 {targetName}", + "notification.others": " + {count} 其他通知", + "notification.pleroma:chat_mention": "{name} 给你发送了信息", + "notification.pleroma:emoji_reaction": "{name} 表情回应了你的帖文", + "notification.poll": "一项你参加的投票已经结束", + "notification.reblog": "{name} 转载了你的帖文", + "notification.status": "{name} 刚刚发帖", + "notification.update": "{name} 修改了你参与互动的帖文", + "notification.user_approved": "欢迎来到 {instance}!", "notifications.filter.all": "全部", "notifications.filter.boosts": "转发", "notifications.filter.emoji_reacts": "Emoji互动", @@ -747,89 +748,110 @@ "notifications.filter.mentions": "提及", "notifications.filter.moves": "迁移", "notifications.filter.polls": "投票结果", - "notifications.filter.statuses": "Updates from people you follow", + "notifications.filter.statuses": "来自关注的人的更新", "notifications.group": "{count} 条通知", - "notifications.queue_label": "点击查看 {count} 新 {count, plural, one {通知} other {通知}}", - "onboarding.avatar.subtitle": "Just have fun with it.", - "onboarding.avatar.title": "Choose a profile picture", - "onboarding.display_name.subtitle": "You can always edit this later.", - "onboarding.display_name.title": "Choose a display name", - "onboarding.done": "Done", - "onboarding.finished.message": "We are very excited to welcome you to our community! Tap the button below to get started.", - "onboarding.finished.title": "Onboarding complete", - "onboarding.header.subtitle": "This will be shown at the top of your profile.", - "onboarding.header.title": "Pick a cover image", - "onboarding.next": "Next", - "onboarding.note.subtitle": "You can always edit this later.", - "onboarding.note.title": "Write a short bio", - "onboarding.saving": "Saving…", - "onboarding.skip": "Skip for now", - "onboarding.suggestions.subtitle": "Here are a few of the most popular accounts you might like.", - "onboarding.suggestions.title": "Suggested accounts", - "onboarding.view_feed": "View Feed", + "notifications.queue_label": "点击查看 {count} 条新通知", + "oauth_consumer.tooltip": "使用 {provider} 登录", + "oauth_consumers.title": "更多方式登录", + "onboarding.avatar.subtitle": "祝你玩得开心!", + "onboarding.avatar.title": "选择一张个人资料图片", + "onboarding.display_name.subtitle": "你可以稍后编辑", + "onboarding.display_name.title": "选择你的昵称", + "onboarding.done": "完成", + "onboarding.finished.message": "我们非常高兴地欢迎你加入我们的社区! 点击下面的按钮,让我们开始吧!", + "onboarding.finished.title": "新手教程结束", + "onboarding.header.subtitle": "这将显示在你个人资料的顶部。", + "onboarding.header.title": "选择封面图片", + "onboarding.next": "下一步", + "onboarding.note.subtitle": "你可以稍后编辑", + "onboarding.note.title": "简短地介绍你自己", + "onboarding.saving": "保存中…", + "onboarding.skip": "现在跳过", + "onboarding.suggestions.subtitle": "以下是几个受欢迎的账户,你可能会喜欢", + "onboarding.suggestions.title": "推荐账户", + "onboarding.view_feed": "浏览列表", "password_reset.confirmation": "请查阅确认邮件。", "password_reset.fields.username_placeholder": "电子邮件或用户名", "password_reset.reset": "重置密码", - "patron.donate": "Donate", - "patron.title": "Funding Goal", - "pinned_accounts.title": "{name}’s choices", + "patron.donate": "捐赠", + "patron.title": "筹集目标", + "pinned_accounts.title": "{name} 的选择", "pinned_statuses.none": "没有置顶帖文", + "poll.choose_multiple": "选你所想", "poll.closed": "已关闭", + "poll.non_anonymous": "公开投票", + "poll.non_anonymous.label": "投票的选择能被其他实例看到", "poll.refresh": "刷新", - "poll.total_votes": "{count} 票", + "poll.total_people": "还有 {count} 人", + "poll.total_votes": "投票", "poll.vote": "投票", "poll.voted": "你投给了这个选项", - "poll.votes": "{votes, plural, one {# vote} other {# votes}}", + "poll.votes": "投票", "poll_button.add_poll": "发起投票", "poll_button.remove_poll": "移除投票", - "pre_header.close": "Close", "preferences.fields.auto_play_gif_label": "自动播放GIF动图", "preferences.fields.autoload_more_label": "滚动到时间线底部时自动加载更多帖文", "preferences.fields.autoload_timelines_label": "滚动到时间线顶部时自动加载新帖", "preferences.fields.boost_modal_label": "转发前确认", "preferences.fields.delete_modal_label": "删除帖文前确认", "preferences.fields.display_media.default": "隐藏被标记为敏感内容的媒体", - "preferences.fields.display_media.hide_all": "显示所有的媒体", - "preferences.fields.display_media.show_all": "隐藏所有媒体", + "preferences.fields.display_media.hide_all": "总是隐藏所有媒体", + "preferences.fields.display_media.show_all": "总是显示所有媒体", "preferences.fields.expand_spoilers_label": "始终展开标有内容警告的帖文", "preferences.fields.language_label": "语言", "preferences.fields.media_display_label": "媒体展示", - "preferences.hints.feed": "In your home feed", + "preferences.fields.theme": "主题", + "preferences.hints.feed": "在你的主页信息流中", "privacy.change": "设置帖文可见范围", "privacy.direct.long": "只有被提及的用户能看到。", - "privacy.direct.short": "私信", + "privacy.direct.short": "仅被提及者", "privacy.private.long": "只有关注你的用户能看到。", "privacy.private.short": "仅关注者", - "privacy.public.long": "所有人可见,并会出现在公共时间轴上。", - "privacy.public.short": "公开", + "privacy.public.long": "所有人可见,出现在公共时间轴上。", + "privacy.public.short": "公共时间轴", "privacy.unlisted.long": "所有人可见,但不会出现在公共时间轴上。", - "privacy.unlisted.short": "不公开", + "privacy.unlisted.short": "所有人", "profile_dropdown.add_account": "添加一个已有帐号", - "profile_dropdown.logout": "登出 @{acct}", - "profile_fields_panel.title": "Profile fields", + "profile_dropdown.logout": "注销 @{acct}", + "profile_dropdown.theme": "主题", + "profile_fields_panel.title": "个人资料字段", "public.column_settings.title": "跨站公共时间轴设置", - "reactions.all": "All", + "reactions.all": "全部", "regeneration_indicator.label": "加载中……", "regeneration_indicator.sublabel": "你的主页时间轴正在准备中!", "register_invite.lead": "填写以下表单以创建帐号", - "register_invite.title": "你已被邀请加入 {siteTitle}!", - "registration.agreement": "我同意本站用户条款 {tos}。", - "registration.captcha.hint": "点击图像以重新加载验证码。", - "registration.closed_message": "{instance} 不再接受新的用户注册。", + "register_invite.title": "你已被邀请加入 {siteTitle}!", + "registration.acceptance": "一旦注册,意味着你已经同意本站的 {terms} 和 {privacy}.", + "registration.agreement": "我同意本站用户条款 {tos}", + "registration.captcha.hint": "点击图像以重新加载验证码", + "registration.captcha.placeholder": "输入图片中的文字", + "registration.closed_message": "{instance} 暂不开放注册", "registration.closed_title": "暂停注册", "registration.confirmation_modal.close": "关闭", - "registration.fields.confirm_placeholder": "请再输入密码", + "registration.fields.confirm_placeholder": "再次输入密码", "registration.fields.email_placeholder": "邮箱地址", "registration.fields.password_placeholder": "密码", - "registration.fields.username_hint": "只可以使用字母、数字、下划线。", + "registration.fields.username_hint": "只可以使用英文字母、数字和下划线。", "registration.fields.username_placeholder": "用户名", + "registration.header": "创建新账户", "registration.newsletter": "订阅新闻邮件", "registration.password_mismatch": "密码不匹配", - "registration.reason": "请填写注册理由", - "registration.reason_hint": "这会帮助我们审核你的帐号", + "registration.privacy": "隐私政策", + "registration.reason": "你为什么想要注册本站?", + "registration.reason_hint": "认真填写将会加快你通过的速度", "registration.sign_up": "注册", "registration.tos": "用户条款", - "registration.username_unavailable": "Username is already taken.", + "registration.username_unavailable": "用户名已被他人占用", + "registration.validation.capital_letter": "1 个大写字母", + "registration.validation.lowercase_letter": "1 个小写字母", + "registration.validation.minimum_characters": "8 个字符", + "registrations.create_account": "创建新账户", + "registrations.error": "创建您的账户失败,请联系管理员", + "registrations.get_started": "让我们开始吧!", + "registrations.success": "欢迎来到 {siteTitle}!", + "registrations.tagline": "一个没有言论审查的社交平台", + "registrations.unprocessable_entity": "用户名已被他人占用", + "registrations.username.hint": "只能包含字母、数字和下划线", "relative_time.days": "{number}天", "relative_time.hours": "{number}时", "relative_time.just_now": "刚刚", @@ -837,55 +859,67 @@ "relative_time.seconds": "{number}秒", "remote_instance.edit_federation": "编辑联邦设置", "remote_instance.federation_panel.heading": "联邦站点限制", - "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} 未对 {host} 设置限制。", - "remote_instance.federation_panel.restricted_message": "{siteTitle} 完全屏蔽了 {host} 。", - "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} 部分限制了 {host} 。", - "remote_instance.pin_host": "收藏 {host}", - "remote_instance.unpin_host": "取消收藏 {host}", - "remote_interaction.account_placeholder": "Enter your username@domain you want to act from", - "remote_interaction.divider": "or", - "remote_interaction.favourite": "Proceed to like", - "remote_interaction.favourite_title": "Like a post remotely", - "remote_interaction.follow": "Proceed to follow", - "remote_interaction.follow_title": "Follow {user} remotely", - "remote_interaction.poll_vote": "Proceed to vote", - "remote_interaction.poll_vote_title": "Vote in a poll remotely", - "remote_interaction.reblog": "Proceed to repost", - "remote_interaction.reblog_title": "Reblog a post remotely", - "remote_interaction.reply": "Proceed to reply", - "remote_interaction.reply_title": "Reply to a post remotely", - "remote_interaction.user_not_found_error": "Couldn't find given user", - "remote_timeline.filter_message": "你在查看 {instance} 的时间轴。", + "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} 未对 {host} 设置限制", + "remote_instance.federation_panel.restricted_message": "{siteTitle} 完全屏蔽了 {host}", + "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} 对 {host} 实施了部分限制", + "remote_instance.pin_host": "置顶 {host}", + "remote_instance.unpin_host": "取消置顶 {host}", + "remote_interaction.account_placeholder": "输入你想采取行动的账户(格式:用户名@域名)", + "remote_interaction.divider": "或", + "remote_interaction.favourite": "点赞", + "remote_interaction.favourite_title": "远程点赞一个帖文", + "remote_interaction.follow": "关注", + "remote_interaction.follow_title": "远程关注 {user} ", + "remote_interaction.poll_vote": "投票", + "remote_interaction.poll_vote_title": "远程参与投票", + "remote_interaction.reblog": "转帖", + "remote_interaction.reblog_title": "远程转帖", + "remote_interaction.reply": "回复", + "remote_interaction.reply_title": "远程回复", + "remote_interaction.user_not_found_error": "找不到该用户", + "remote_timeline.filter_message": "你在查看 {instance} 的时间轴", "reply_indicator.cancel": "取消", - "reply_mentions.account.add": "Add to mentions", - "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.reply_empty": "Replying to post", + "reply_mentions.account.add": "添加到提及列表", + "reply_mentions.account.remove": "从提及列表中移除", + "reply_mentions.more": "添加 {count} 个", + "reply_mentions.reply": "回复 {accounts}{more}", + "reply_mentions.reply_empty": "回复帖文", "report.block": "屏蔽帐号 {target}", "report.block_hint": "你是否也要屏蔽这个帐号呢?", + "report.confirmation.content": "如果我们发现这个帐号确实违反了 {link} ,我们会采取进一步的惩罚措施", + "report.confirmation.title": "感谢你提交的举报", + "report.done": "完成", "report.forward": "发送举报至 {target}", "report.forward_hint": "这名用户来自另一个服务器。是否要向那个服务器发送一条匿名的举报?", "report.hint": "举报将会发送给你所在服务器的监察员。你可以在下面填写举报该用户的理由:", + "report.next": "下一步", + "report.otherActions.addAdditional": "您想为这个举报添加更多的状态吗?", + "report.otherActions.addMore": "添加更多", + "report.otherActions.furtherActions": "进一步的措施:", + "report.otherActions.hideAdditional": "隐藏额外状态", + "report.otherActions.otherStatuses": "包括其他状态?", "report.placeholder": "备注", + "report.reason.blankslate": "你已移除选中的状态", + "report.reason.title": "举报原因", "report.submit": "提交", "report.target": "举报 {target}", - "reset_password.header": "Set New Password", - "schedule.post_time": "发布日期与时间", + "reset_password.fail": "令牌已过期,请重试", + "reset_password.header": "设置新的密码", + "schedule.post_time": "发布时间", "schedule.remove": "取消发布", "schedule_button.add_schedule": "定时发布", - "schedule_button.remove_schedule": "立即发布", + "schedule_button.remove_schedule": "取消定时发布", "scheduled_status.cancel": "取消", "search.action": "搜索 “{query}”", "search.placeholder": "搜索", "search_results.accounts": "用户", "search_results.hashtags": "话题标签", "search_results.statuses": "帖文", - "search_results.top": "结果", "security.codes.fail": "恢复代码错误", "security.confirm.fail": "密码错误,请重试。", - "security.delete_account.fail": "删除帐号失败。.", - "security.delete_account.success": "帐号删除成功。", + "security.delete_account.fail": "删除帐号失败", + "security.delete_account.success": "帐号删除成功", "security.disable.fail": "密码错误,请重试。", - "security.disable_mfa": "关闭", "security.fields.email.label": "邮箱地址", "security.fields.new_password.label": "输入新密码", "security.fields.old_password.label": "输入原密码", @@ -893,57 +927,59 @@ "security.fields.password_confirmation.label": "再次输入新密码", "security.headers.delete": "删除帐号", "security.headers.tokens": "会话", - "security.headers.update_email": "更改邮箱", - "security.headers.update_password": "更改密码", - "security.mfa": "设置双重认证", - "security.mfa_enabled": "你已经设置好了通过OTP进行双重认证。", - "security.mfa_header": "验证方式", - "security.mfa_setup_hint": "通过OTP进行双重认证", "security.qr.fail": "加载密钥失败", "security.submit": "保存更改", "security.submit.delete": "删除帐号", "security.text.delete": "输入密码后,本站会立即删除你的帐号,并且通知其他站点,但你的信息不会在其他站点上立即删除。", - "security.tokens.revoke": "吊销", - "security.update_email.fail": "更新邮箱地址失败。", - "security.update_email.success": "邮箱地址已更新。", - "security.update_password.fail": "更改密码失败。", - "security.update_password.success": "密码已更改。", - "settings.change_email": "Change Email", - "settings.change_password": "Change Password", - "settings.configure_mfa": "Configure MFA", - "settings.delete_account": "Delete Account", - "settings.edit_profile": "Edit Profile", - "settings.preferences": "Preferences", - "settings.profile": "Profile", - "settings.save.success": "Your preferences have been saved!", - "settings.security": "Security", - "settings.settings": "Settings", - "signup_panel.subtitle": "注册以参与讨论。", + "security.tokens.revoke": "撤销", + "security.update_email.fail": "更新邮箱地址失败", + "security.update_email.success": "邮箱地址已更新", + "security.update_password.fail": "更改密码失败", + "security.update_password.success": "密码已更改", + "settings.change_email": "修改邮箱", + "settings.change_password": "修改密码", + "settings.configure_mfa": "配置多重认证 (MFA)", + "settings.delete_account": "删除账户", + "settings.edit_profile": "编辑个人资料", + "settings.preferences": "偏好设置", + "settings.profile": "个人资料", + "settings.save.success": "你的偏好设置已保存!", + "settings.security": "安全性", + "settings.sessions": "活动会话", + "settings.settings": "设置", + "shared.tos": "服务条款", + "signup_panel.subtitle": "注册以参与讨论", "signup_panel.title": "还没有注册{site_title} ?", - "snackbar.view": "View", - "soapbox_config.authenticated_profile_hint": "Users must be logged-in to view replies and media on user profiles.", - "soapbox_config.authenticated_profile_label": "Profiles require authentication", + "site_preview.preview": "预览", + "snackbar.view": "浏览", + "soapbox_config.authenticated_profile_hint": "用户必须登录后才能查看用户个人资料上的回复和媒体。", + "soapbox_config.authenticated_profile_label": "个人资料需要授权才能查看", "soapbox_config.copyright_footer.meta_fields.label_placeholder": "版权页底", "soapbox_config.crypto_address.meta_fields.address_placeholder": "地址", - "soapbox_config.crypto_address.meta_fields.note_placeholder": "Note (optional)", - "soapbox_config.crypto_address.meta_fields.ticker_placeholder": "Ticker", - "soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder": "Number of items to display in the crypto homepage widget", + "soapbox_config.crypto_address.meta_fields.note_placeholder": "备注 (可选)", + "soapbox_config.crypto_address.meta_fields.ticker_placeholder": "币种", + "soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder": "在主页加密货币小组件中显示的项目数量", "soapbox_config.custom_css.meta_fields.url_placeholder": "URL", - "soapbox_config.display_fqn_label": "显示本站帐号的域名(如@user@domain)。", - "soapbox_config.fields.accent_color_label": "Accent color", + "soapbox_config.display_fqn_label": "显示本站帐号的域名(如 @用户名@域名 )", + "soapbox_config.fields.accent_color_label": "强调色", "soapbox_config.fields.brand_color_label": "主题颜色", - "soapbox_config.fields.crypto_address.add": "添加加密币地址", - "soapbox_config.fields.crypto_addresses_label": "加密币地址", - "soapbox_config.fields.home_footer.add": "添加主页页底", - "soapbox_config.fields.home_footer_fields_label": "主页页底", + "soapbox_config.fields.crypto_address.add": "添加加密货币地址", + "soapbox_config.fields.crypto_addresses_label": "加密货币地址", + "soapbox_config.fields.home_footer.add": "添加主页页尾", + "soapbox_config.fields.home_footer_fields_label": "主页页尾", "soapbox_config.fields.logo_label": "Logo", "soapbox_config.fields.promo_panel.add": "添加时间轴页底", "soapbox_config.fields.promo_panel_fields_label": "时间轴页底", "soapbox_config.fields.theme_label": "默认主题", "soapbox_config.greentext_label": "打开greentext支持", - "soapbox_config.hints.crypto_addresses": "Add cryptocurrency addresses so users of your site can donate to you. Order matters, and you must use lowercase ticker values.", + "soapbox_config.headings.advanced": "高级", + "soapbox_config.headings.cryptocurrency": "加密货币", + "soapbox_config.headings.navigation": "导航", + "soapbox_config.headings.options": "选项", + "soapbox_config.headings.theme": "主题", + "soapbox_config.hints.crypto_addresses": "添加加密货币地址以便用户可以向你捐赠。请注意顺序,同时你必须使用小写币种代码。", "soapbox_config.hints.home_footer_fields": "你可以将自定义的链接插入在未登录时显示的主页页底(如about)。", - "soapbox_config.hints.logo": "SVG。最多2MB。将被显示为50px高度,保持长宽比。", + "soapbox_config.hints.logo": "SVG。最多2MB。将被显示为50px高度,保持长宽比", "soapbox_config.hints.promo_panel_fields": "你可以将自定义的链接插入在时间轴右方或页底(如about)。", "soapbox_config.hints.promo_panel_icons": "{ link }", "soapbox_config.hints.promo_panel_icons.link": "Soapbox图标列表", @@ -956,17 +992,20 @@ "soapbox_config.raw_json_label": "高级:编辑JSON数据", "soapbox_config.save": "保存更改", "soapbox_config.saved": "Soapbox配置已保存", - "soapbox_config.single_user_mode_hint": "Front page will redirect to a given user profile.", - "soapbox_config.single_user_mode_label": "Single user mode", - "soapbox_config.single_user_mode_profile_hint": "@handle", - "soapbox_config.single_user_mode_profile_label": "Main user handle", + "soapbox_config.single_user_mode_hint": "头部页面将重定向到给定的用户资料。", + "soapbox_config.single_user_mode_label": "单用户模式", + "soapbox_config.single_user_mode_profile_hint": "@用户名", + "soapbox_config.single_user_mode_profile_label": "主账户的用户名", "soapbox_config.verified_can_edit_name_label": "允许经过验证的用户编辑他们自己的昵称。", - "status.actions.more": "More", + "sponsored.info.message": "{siteTitle} 展示广告以使服务可持续运行。", + "sponsored.info.title": "为什么我会看到这个广告?", + "sponsored.subtitle": "赞助", + "status.actions.more": "更多", "status.admin_account": "打开 @{name} 的管理界面", "status.admin_status": "打开这条帖文的管理界面", "status.block": "屏蔽 @{name}", "status.bookmark": "书签", - "status.bookmarked": "书签已添加。", + "status.bookmarked": "书签已添加", "status.cancel_reblog_private": "取消转发", "status.cannot_reblog": "这条帖文不允许被转发。", "status.chat": "与 @{name} 聊天", @@ -974,6 +1013,8 @@ "status.delete": "删除", "status.detailed_status": "对话详情", "status.direct": "发送私信给 @{name}", + "status.edit": "编辑", + "status.edited": "已在 {date} 编辑", "status.embed": "嵌入", "status.favourite": "点赞", "status.filtered": "已过滤", @@ -986,14 +1027,14 @@ "status.open": "展开帖文", "status.pin": "在个人资料页面置顶", "status.pinned": "置顶帖文", - "status.quote": "Quote post", - "status.reactions.cry": "Sad", + "status.quote": "引用帖文", + "status.reactions.cry": "哭", "status.reactions.empty": "尚未有人回应表情", - "status.reactions.heart": "Love", - "status.reactions.laughing": "Haha", - "status.reactions.like": "Like", - "status.reactions.open_mouth": "Wow", - "status.reactions.weary": "Weary", + "status.reactions.heart": "爱心", + "status.reactions.laughing": "哈哈", + "status.reactions.like": "喜欢", + "status.reactions.open_mouth": "哇哦", + "status.reactions.weary": "疲倦", "status.reactions_expand": "选择表情", "status.read_more": "阅读全文", "status.reblog": "转发", @@ -1008,33 +1049,44 @@ "status.report": "举报 @{name}", "status.sensitive_warning": "敏感内容", "status.share": "分享", - "status.show_less": "隐藏内容", - "status.show_less_all": "隐藏所有内容", - "status.show_more": "显示内容", - "status.show_more_all": "显示所有内容", + "status.show_less": "减少显示", + "status.show_less_all": "减少这类帖文的展示", + "status.show_more": "增加显示", + "status.show_more_all": "增加这类帖文的展示", "status.title": "帖文", "status.title_direct": "私信", "status.unbookmark": "移除书签", - "status.unbookmarked": "书签已移除。", + "status.unbookmarked": "书签已移除", "status.unmute_conversation": "不再隐藏此对话", "status.unpin": "在个人资料页面取消置顶", - "status_list.queue_label": "点击查看 {count} 则新{count, plural, one {帖文} other {帖文}}", - "statuses.quote_tombstone": "Post is unavailable.", + "status.sensitive_warning.subtitle": "此条内容可能并不适宜所有人查看", + "status.sensitive_warning.action": "显示", + "status_list.queue_label": "点击查看 {count} 则新帖文", + "statuses.quote_tombstone": "帖文不可用", "statuses.tombstone": "部分帖文不可见", + "streamfield.add": "添加", + "streamfield.remove": "移除", "suggestions.dismiss": "关闭建议", "tabs_bar.all": "全部", "tabs_bar.chats": "聊天", "tabs_bar.dashboard": "管理中心", "tabs_bar.fediverse": "联邦宇宙", "tabs_bar.home": "主页", - "tabs_bar.more": "More", + "tabs_bar.more": "更多", "tabs_bar.notifications": "通知", - "tabs_bar.post": "发帖", - "tabs_bar.profile": "Profile", + "tabs_bar.profile": "个人资料", "tabs_bar.search": "搜索", - "tabs_bar.settings": "Settings", + "tabs_bar.settings": "设置", + "tabs_bar.switch_accounts": "切换帐号", "tabs_bar.theme_toggle_dark": "切换到暗色主题", "tabs_bar.theme_toggle_light": "切换到明亮主题", + "theme_toggle.dark": "黑暗", + "theme_toggle.light": "明亮", + "theme_toggle.system": "系统", + "thread_login.login": "登录", + "thread_login.message": "加入 {siteTitle} 以获取完整资讯", + "thread_login.signup": "注册", + "thread_login.title": "继续这个对话", "time_remaining.days": "离预定时间还有 {number, plural, one {# 天} other {# 天}}", "time_remaining.hours": "离预定时间还有 {number, plural, one {# 小时} other {# 小时}}", "time_remaining.minutes": "离预定时间还有 {number, plural, one {# 分钟} other {# 分钟}}", @@ -1042,15 +1094,17 @@ "time_remaining.seconds": "离预定时间还有 {number, plural, one {# 秒} other {# 秒}}", "trends.count_by_accounts": "{count} 人正在讨论", "trends.title": "热门", + "trendsPanel.viewAll": "查看全部", "ui.beforeunload": "如果你现在离开网站,你的草稿内容将会丢失。", - "unauthorized_modal.text": "你需要帐号才能继续", - "unauthorized_modal.title": "注册帐号 {site_title}", + "unauthorized_modal.text": "你需要登录才能继续", + "unauthorized_modal.title": "注册 {site_title} 帐号", "upload_area.title": "将文件拖放到此处开始上传", "upload_button.label": "上传媒体文件 (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.image_size_limit": "Image exceeds the current file size limit ({limit})", + "upload_error.image_size_limit": "图片超过了当前的文件大小限制 ({limit})", "upload_error.limit": "文件大小超过限制。", "upload_error.poll": "投票中不允许上传文件。", - "upload_error.video_size_limit": "Video exceeds the current file size limit ({limit})", + "upload_error.video_duration_limit": "视频超出当前时长限制 ({limit} 秒)", + "upload_error.video_size_limit": "视频超过了当前的文件大小限制 ({limit})", "upload_form.description": "为视觉障碍人士添加文字说明", "upload_form.preview": "预览", "upload_form.undo": "删除", diff --git a/app/soapbox/locales/zh-HK.json b/app/soapbox/locales/zh-HK.json index 1a3fe6f4a..e43846838 100644 --- a/app/soapbox/locales/zh-HK.json +++ b/app/soapbox/locales/zh-HK.json @@ -1,1071 +1,1120 @@ { - "about.also_available": "Available in:", - "accordion.collapse": "Collapse", - "accordion.expand": "Expand", - "account.add_or_remove_from_list": "Add or Remove from lists", - "account.badges.bot": "機械人", - "account.birthday": "Born {date}", - "account.birthday_today": "Birthday is today!", + "about.also_available": "其他版本:", + "accordion.collapse": "收起", + "accordion.expand": "展开", + "account.add_or_remove_from_list": "從名單中新增或移除", + "account.badges.bot": "機器人", + "account.birthday": "出生於 {date}", + "account.birthday_today": "今天是你的生日!", "account.block": "封鎖 @{name}", - "account.block_domain": "隱藏來自 {domain} 的一切文章", - "account.blocked": "封鎖", - "account.chat": "Chat with @{name}", - "account.column_settings.description": "These settings apply to all account timelines.", - "account.column_settings.title": "Acccount timeline settings", - "account.deactivated": "Deactivated", - "account.direct": "私訊 @{name}", - "account.edit_profile": "修改個人資料", - "account.endorse": "Feature on profile", - "account.follow": "關注", - "account.followers": "關注的人", - "account.followers.empty": "No one follows this user yet.", - "account.follows": "正關注", - "account.follows.empty": "This user doesn't follow anyone yet.", - "account.follows_you": "關注你", - "account.hide_reblogs": "隱藏 @{name} 的轉推", - "account.last_status": "Last active", - "account.link_verified_on": "Ownership of this link was checked on {date}", - "account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.", - "account.login": "Log in", + "account.block_domain": "隱藏來自 {domain} 的所有內容", + "account.blocked": "已封鎖", + "account.chat": "和 @{name} 聊天", + "account.deactivated": "帳户已被停權", + "account.direct": "傳私訊給 @{name}", + "account.domain_blocked": "隱藏網域", + "account.edit_profile": "編輯個人資料", + "account.endorse": "在個人資料推薦對方", + "account.endorse.success": "你正在你的個人資料中展示 @{acct} ", + "account.familiar_followers": "被 {accounts} 追蹤", + "account.familiar_followers.more": "你追蹤了 {count} 位其他用户", + "account.follow": "追蹤", + "account.followers": "追蹤者", + "account.followers.empty": "尚沒有人追蹤這位使用者。", + "account.follows": "正在追蹤", + "account.follows.empty": "這位使用者尚未追蹤任何使用者。", + "account.follows_you": "追蹤了你", + "account.hide_reblogs": "隱藏來自 @{name} 的轉推", + "account.last_status": "最後活動", + "account.link_verified_on": "已在 {date} 檢查此連結的擁有者權限", + "account.locked_info": "這隻帳户的隱私狀態被設成鎖定。該擁有者會手動審核能追蹤這隻帳號的人。", + "account.login": "登入", "account.media": "媒體", - "account.member_since": "Joined {date}", + "account.member_since": "加入於 {date}", "account.mention": "提及", - "account.moved_to": "{name} 已經遷移到:", - "account.mute": "將 @{name} 靜音", - "account.never_active": "Never", - "account.posts": "文章", - "account.posts_with_replies": "包含回覆的文章", - "account.profile": "Profile", - "account.register": "Sign up", - "account.remote_follow": "Remote follow", - "account.report": "舉報 @{name}", - "account.requested": "等候審批", - "account.requested_small": "Awaiting approval", + "account.moved_to": "{name} 已遷移至:", + "account.mute": "隱藏 @{name}", + "account.muted": "已隱藏", + "account.never_active": "從未", + "account.posts": "帖文", + "account.posts_with_replies": "帖文與回覆", + "account.profile": "個人資料", + "account.register": "註冊", + "account.remote_follow": "遠端追蹤", + "account.remove_from_followers": "移除此追蹤者", + "account.report": "檢舉 @{name}", + "account.requested": "正在等待核准。按一下取消追蹤請求", + "account.requested_small": "等待核准", + "account.search": "搜尋關於 @{name} 的內容", + "account.search_self": "搜尋你的帖文", "account.share": "分享 @{name} 的個人資料", - "account.show_reblogs": "顯示 @{name} 的推文", - "account.subscribe": "Subscribe to notifications from @{name}", - "account.subscribed": "Subscribed", - "account.unblock": "解除對 @{name} 的封鎖", - "account.unblock_domain": "不再隱藏 {domain}", - "account.unendorse": "Don't feature on profile", - "account.unfollow": "取消關注", - "account.unmute": "取消 @{name} 的靜音", - "account.unsubscribe": "Unsubscribe to notifications from @{name}", - "account.verified": "Verified Account", - "account.welcome": "Welcome", - "account_gallery.none": "No media to show.", - "account_note.hint": "You can keep notes about this user for yourself (this will not be shared with them):", - "account_note.placeholder": "No comment provided", - "account_note.save": "Save", - "account_note.target": "Note for @{target}", - "account_search.placeholder": "Search for an account", - "account_timeline.column_settings.show_pinned": "Show pinned posts", - "admin.awaiting_approval.approved_message": "{acct} was approved!", - "admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.", - "admin.awaiting_approval.rejected_message": "{acct} was rejected.", - "admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.", - "admin.dashboard.registration_mode.approval_label": "Approval Required", - "admin.dashboard.registration_mode.closed_hint": "Nobody can sign up. You can still invite people.", - "admin.dashboard.registration_mode.closed_label": "Closed", - "admin.dashboard.registration_mode.open_hint": "Anyone can join.", - "admin.dashboard.registration_mode.open_label": "Open", - "admin.dashboard.registration_mode_label": "Registrations", - "admin.dashboard.settings_saved": "Settings saved!", - "admin.dashcounters.domain_count_label": "peers", - "admin.dashcounters.mau_label": "monthly active users", - "admin.dashcounters.retention_label": "user retention", - "admin.dashcounters.status_count_label": "posts", - "admin.dashcounters.user_count_label": "total users", - "admin.dashwidgets.email_list_header": "Email list", - "admin.dashwidgets.software_header": "Software", - "admin.latest_accounts_panel.more": "Click to see {count} {count, plural, one {account} other {accounts}}", - "admin.latest_accounts_panel.title": "Latest Accounts", - "admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.", - "admin.reports.actions.close": "Close", - "admin.reports.actions.view_status": "View post", - "admin.reports.empty_message": "There are no open reports. If a user gets reported, they will show up here.", - "admin.reports.report_closed_message": "Report on @{name} was closed", - "admin.reports.report_title": "Report on {acct}", - "admin.statuses.actions.delete_status": "Delete post", - "admin.statuses.actions.mark_status_not_sensitive": "Mark post not sensitive", - "admin.statuses.actions.mark_status_sensitive": "Mark post sensitive", - "admin.statuses.status_deleted_message": "Post by @{acct} was deleted", - "admin.statuses.status_marked_message_not_sensitive": "Post by @{acct} was marked not sensitive", - "admin.statuses.status_marked_message_sensitive": "Post by @{acct} was marked sensitive", - "admin.user_index.empty": "No users found.", - "admin.user_index.search_input_placeholder": "Who are you looking for?", - "admin.users.actions.deactivate_user": "Deactivate @{name}", - "admin.users.actions.delete_user": "Delete @{name}", - "admin.users.actions.demote_to_moderator": "Demote @{name} to a moderator", - "admin.users.actions.demote_to_moderator_message": "@{acct} was demoted to a moderator", - "admin.users.actions.demote_to_user": "Demote @{name} to a regular user", - "admin.users.actions.demote_to_user_message": "@{acct} was demoted to a regular user", - "admin.users.actions.promote_to_admin": "Promote @{name} to an admin", - "admin.users.actions.promote_to_admin_message": "@{acct} was promoted to an admin", - "admin.users.actions.promote_to_moderator": "Promote @{name} to a moderator", - "admin.users.actions.promote_to_moderator_message": "@{acct} was promoted to a moderator", - "admin.users.actions.remove_donor": "Remove @{name} as a donor", - "admin.users.actions.set_donor": "Set @{name} as a donor", - "admin.users.actions.suggest_user": "Suggest @{name}", - "admin.users.actions.unsuggest_user": "Unsuggest @{name}", - "admin.users.actions.unverify_user": "Unverify @{name}", - "admin.users.actions.verify_user": "Verify @{name}", - "admin.users.remove_donor_message": "@{acct} was removed as a donor", - "admin.users.set_donor_message": "@{acct} was set as a donor", - "admin.users.user_deactivated_message": "@{acct} was deactivated", - "admin.users.user_deleted_message": "@{acct} was deleted", - "admin.users.user_suggested_message": "@{acct} was suggested", - "admin.users.user_unsuggested_message": "@{acct} was unsuggested", - "admin.users.user_unverified_message": "@{acct} was unverified", - "admin.users.user_verified_message": "@{acct} was verified", - "admin_nav.awaiting_approval": "Awaiting Approval", - "admin_nav.dashboard": "Dashboard", - "admin_nav.reports": "Reports", - "alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out).", - "alert.unexpected.browser": "Browser", - "alert.unexpected.clear_cookies": "clear cookies and browser data", - "alert.unexpected.links.help": "Help Center", - "alert.unexpected.links.status": "Status", - "alert.unexpected.links.support": "Support", - "alert.unexpected.message": "發生不可預期的錯誤。", - "alert.unexpected.return_home": "Return Home", - "alert.unexpected.title": "噢!", - "aliases.account.add": "Create alias", - "aliases.account_label": "Old account:", - "aliases.aliases_list_delete": "Unlink alias", - "aliases.search": "Search your old account", - "aliases.success.add": "Account alias created successfully", - "aliases.success.remove": "Account alias removed successfully", - "app_create.name_label": "App name", - "app_create.name_placeholder": "e.g. 'Soapbox'", - "app_create.redirect_uri_label": "Redirect URIs", - "app_create.restart": "Create another", - "app_create.results.app_label": "App", - "app_create.results.explanation_text": "You created a new app and token! Please copy the credentials somewhere; you will not see them again after navigating away from this page.", - "app_create.results.explanation_title": "App created successfully", - "app_create.results.token_label": "OAuth token", - "app_create.scopes_label": "Scopes", - "app_create.scopes_placeholder": "e.g. 'read write follow'", - "app_create.submit": "Create app", - "app_create.website_label": "Website", - "auth.invalid_credentials": "Wrong username or password", - "auth.logged_out": "Logged out.", - "backups.actions.create": "Create backup", - "backups.empty_message": "No backups found. {action}", - "backups.empty_message.action": "Create one now?", - "backups.pending": "Pending", - "beta.also_available": "Available in:", - "birthday_panel.title": "Birthdays", - "boost_modal.combo": "如你想在下次路過這顯示,請按{combo},", - "bundle_column_error.body": "加載本組件出錯。", + "account.show_reblogs": "顯示來自 @{name} 的帖文", + "account.subscribe": "訂閲 @{name}", + "account.subscribe.failure": "訂閲此帳户時發生錯誤", + "account.subscribe.success": "您已訂閲此帳户", + "account.unblock": "取消封鎖 @{name}", + "account.unblock_domain": "取消隱藏 {domain}", + "account.unendorse": "不再於個人資料頁面推薦對方", + "account.unendorse.success": "您已不再展示 @{acct}", + "account.unfollow": "取消追蹤", + "account.unmute": "取消隱藏 @{name}", + "account.unsubscribe": "取消訂閲 @{name}", + "account.unsubscribe.failure": "取消訂閲此帳户時發生錯誤", + "account.unsubscribe.success": "你已取消訂閲此帳户", + "account.verified": "已認證的帳户", + "account_gallery.none": "沒有可顯示的媒體", + "account_note.hint": "你可以為自己保留關於這個用户的筆記(這不會和他們分享):", + "account_note.placeholder": "沒有評論", + "account_note.save": "儲存", + "account_note.target": "@{target} 的筆記", + "account_search.placeholder": "搜尋使用者", + "actualStatus.edited": "在 {date} 時編輯", + "actualStatuses.quote_tombstone": "帖文不可用", + "admin.awaiting_approval.approved_message": "{acct} 已經通過審核!", + "admin.awaiting_approval.empty_message": "沒有新使用者等待審核,如果有新的申請,它就會顯示在這裏。", + "admin.awaiting_approval.rejected_message": "{acct} 的註冊請求被拒絕!", + "admin.dashboard.registration_mode.approval_hint": "在管理員同意註冊申請後才可加入。", + "admin.dashboard.registration_mode.approval_label": "需要審核", + "admin.dashboard.registration_mode.closed_hint": "公開註冊已經關閉,但已註冊用户仍然可以邀請其他人加入。", + "admin.dashboard.registration_mode.closed_label": "私密", + "admin.dashboard.registration_mode.open_hint": "任何人都可以加入。", + "admin.dashboard.registration_mode.open_label": "公開", + "admin.dashboard.registration_mode_label": "註冊模式", + "admin.dashboard.settings_saved": "設定已儲存!", + "admin.dashcounters.domain_count_label": "互聯站點", + "admin.dashcounters.mau_label": "月度活躍使用者", + "admin.dashcounters.retention_label": "使用者留存", + "admin.dashcounters.status_count_label": "總帖數", + "admin.dashcounters.user_count_label": "使用者總數", + "admin.dashwidgets.email_list_header": "電郵列表", + "admin.dashwidgets.software_header": "軟體版本", + "admin.latest_accounts_panel.expand_message": "點擊展開更多帳户", + "admin.latest_accounts_panel.more": "點擊展開 {count} 個帳户", + "admin.latest_accounts_panel.title": "最近加入的帳户", + "admin.moderation_log.empty_message": "你還沒有進行任何管理,如果有任何操作,操作歷史就會顯示在這裏。", + "admin.reports.actions.close": "關閉檢舉", + "admin.reports.actions.view_status": "查看帖文", + "admin.reports.empty_message": "沒有未處理的檢舉,如果有任何檢舉,它就會顯示在這裏。", + "admin.reports.report_closed_message": "對 @{name} 的檢舉已關閉", + "admin.reports.report_title": "檢舉 {acct} 的帖文", + "admin.statuses.actions.delete_status": "刪除帖文", + "admin.statuses.actions.mark_status_not_sensitive": "不再標記為敏感內容", + "admin.statuses.actions.mark_status_sensitive": "標記為敏感內容", + "admin.statuses.status_deleted_message": "@{acct} 的帖文已被刪除", + "admin.statuses.status_marked_message_not_sensitive": "@{acct} 的帖文不會被標記為敏感內容", + "admin.statuses.status_marked_message_sensitive": "@{acct} 的帖文被標記為敏感內容", + "admin.user_index.empty": "找不到用户。", + "admin.user_index.search_input_placeholder": "你正在找誰?", + "admin.users.actions.deactivate_user": "禁用帳户 @{name}", + "admin.users.actions.delete_user": "刪除帳户 @{name}", + "admin.users.actions.demote_to_moderator": "降級 @{name} 為站務", + "admin.users.actions.demote_to_moderator_message": "@{acct} 被降級為站務", + "admin.users.actions.demote_to_user": "降級 @{name} 為普通用户", + "admin.users.actions.demote_to_user_message": "@{acct} 被降級為普通用户", + "admin.users.actions.promote_to_admin": "升級 @{name} 為管理員", + "admin.users.actions.promote_to_admin_message": "@{acct} 被升級為管理員", + "admin.users.actions.promote_to_moderator": "升級 @{name} 為站務", + "admin.users.actions.promote_to_moderator_message": "@{acct} 被升級為站務", + "admin.users.actions.remove_donor": "移除 @{name} 的捐贈者頭銜", + "admin.users.actions.set_donor": "將 @{name} 設為捐贈者", + "admin.users.actions.suggest_user": "推薦 @{name}", + "admin.users.actions.unsuggest_user": "取消推薦 @{name}", + "admin.users.actions.unverify_user": "撤銷認證 @{name}", + "admin.users.actions.verify_user": "認證帳户 @{name}", + "admin.users.remove_donor_message": "@{acct} 被移除出捐贈者名單", + "admin.users.set_donor_message": "@{acct} 被設為捐贈者", + "admin.users.user_deactivated_message": "@{acct} 被停權", + "admin.users.user_deleted_message": "@{acct} 被刪除了", + "admin.users.user_suggested_message": "@{acct} 被推薦", + "admin.users.user_unsuggested_message": "@{acct} 被取消推薦", + "admin.users.user_unverified_message": "@{acct} 未認證", + "admin.users.user_verified_message": "@{acct} 被認證", + "admin_nav.awaiting_approval": "等待核准", + "admin_nav.dashboard": "控制台", + "admin_nav.reports": "檢舉", + "age_verification.fail": "你至少必須滿 {ageMinimum} 歲", + "age_verification.header": "提供你的生日", + "alert.unexpected.body": "我們對這次的中斷感到抱歉。如果問題持續存在,請聯絡我們的支援團隊。你也可以嘗試 {clearCookies} (注意:你的帳户會自動退出)。", + "alert.unexpected.browser": "瀏覽器", + "alert.unexpected.clear_cookies": "清除cookie和瀏覽器資料", + "alert.unexpected.links.help": "支援中心", + "alert.unexpected.links.status": "狀態", + "alert.unexpected.links.support": "支援", + "alert.unexpected.message": "發生了非預期的錯誤。", + "alert.unexpected.return_home": "回到主頁", + "alert.unexpected.title": "哎喲!", + "aliases.account.add": "創建別名", + "aliases.account_label": "原帳户:", + "aliases.aliases_list_delete": "刪除別名", + "aliases.search": "搜尋原帳户", + "aliases.success.add": "帳户別名創建成功", + "aliases.success.remove": "帳户別名刪除成功", + "app_create.name_label": "應用名稱", + "app_create.name_placeholder": "例如 'Soapbox'", + "app_create.redirect_uri_label": "重定向網域", + "app_create.restart": "創建另一個", + "app_create.results.app_label": "應用", + "app_create.results.explanation_text": "您已成功創建一個新應用及其令牌,請複製密鑰等資訊,離開本頁面後這些資訊將不會再次顯示。", + "app_create.results.explanation_title": "應用創建成功", + "app_create.results.token_label": "OAuth令牌", + "app_create.scopes_label": "權限範圍", + "app_create.scopes_placeholder": "例如 '讀取 寫入 追蹤'", + "app_create.submit": "創建應用", + "app_create.website_label": "網站", + "auth.invalid_credentials": "無效的帳户名稱或密碼", + "auth.logged_out": "您已登出", + "backups.actions.create": "創建備份", + "backups.empty_message": "找不到備份 {action}", + "backups.empty_message.action": "現在創建嗎?", + "backups.pending": "等待備份", + "beta.also_available": "在此時可用:", + "birthday_panel.title": "生日", + "boost_modal.combo": "下次您可以按 {combo} 跳過", + "bundle_column_error.body": "載入此元件時發生錯誤。", "bundle_column_error.retry": "重試", - "bundle_column_error.title": "網絡錯誤", + "bundle_column_error.title": "網路錯誤", "bundle_modal_error.close": "關閉", - "bundle_modal_error.message": "加載本組件出錯。", + "bundle_modal_error.message": "載入此元件時發生錯誤。", "bundle_modal_error.retry": "重試", - "card.back.label": "Back", - "chat_box.actions.send": "Send", - "chat_box.input.placeholder": "Send a message…", - "chat_panels.main_window.empty": "No chats found. To start a chat, visit a user's profile.", - "chat_panels.main_window.title": "Chats", - "chats.actions.delete": "Delete message", - "chats.actions.more": "More", - "chats.actions.report": "Report user", - "chats.attachment": "Attachment", - "chats.attachment_image": "Image", - "chats.audio_toggle_off": "Audio notification off", - "chats.audio_toggle_on": "Audio notification on", - "chats.dividers.today": "Today", - "chats.search_placeholder": "Start a chat with…", - "column.admin.awaiting_approval": "Awaiting Approval", - "column.admin.dashboard": "Dashboard", - "column.admin.moderation_log": "Moderation Log", - "column.admin.reports": "Reports", - "column.admin.reports.menu.moderation_log": "Moderation Log", - "column.admin.users": "Users", - "column.aliases": "Account aliases", - "column.aliases.create_error": "Error creating alias", - "column.aliases.delete": "Delete", - "column.aliases.delete_error": "Error deleting alias", - "column.aliases.subheading_add_new": "Add New Alias", - "column.aliases.subheading_aliases": "Current aliases", - "column.app_create": "Create app", - "column.backups": "Backups", - "column.birthdays": "Birthdays", - "column.blocks": "封鎖用戶", - "column.bookmarks": "Bookmarks", - "column.chats": "Chats", + "card.back.label": "返回", + "chat_box.actions.send": "發送", + "chat_box.input.placeholder": "發送聊天訊息…", + "chat_panels.main_window.empty": "還沒有訊息。要開始聊天,可以從用户的個人資料頁面發起。", + "chat_panels.main_window.title": "聊天", + "chats.actions.delete": "刪除訊息", + "chats.actions.more": "更多選項", + "chats.actions.report": "檢舉用户", + "chats.attachment": "附件", + "chats.attachment_image": "圖片", + "chats.audio_toggle_off": "關閉消息提醒", + "chats.audio_toggle_on": "開啟消息提醒", + "chats.dividers.today": "今天", + "chats.search_placeholder": "開始聊天…", + "column.admin.awaiting_approval": "等待核准", + "column.admin.dashboard": "控制台", + "column.admin.moderation_log": "管理日誌", + "column.admin.reports": "檢舉", + "column.admin.reports.menu.moderation_log": "管理日誌", + "column.admin.users": "用户", + "column.aliases": "帳户別名", + "column.aliases.create_error": "創建帳户別名時出錯", + "column.aliases.delete": "刪除", + "column.aliases.delete_error": "刪除帳户別名時出錯", + "column.aliases.subheading_add_new": "添加新別名", + "column.aliases.subheading_aliases": "當前帳户別名", + "column.app_create": "創建應用", + "column.backups": "備份", + "column.birthdays": "生日", + "column.blocks": "封鎖的使用者", + "column.bookmarks": "書籤", + "column.chats": "聊天", "column.community": "本站時間軸", - "column.crypto_donate": "Donate Cryptocurrency", - "column.developers": "Developers", - "column.direct": "個人訊息", - "column.directory": "Browse profiles", - "column.domain_blocks": "隱藏的服務站", - "column.edit_profile": "Edit profile", - "column.export_data": "Export data", - "column.favourited_statuses": "Liked posts", - "column.favourites": "Likes", - "column.federation_restrictions": "Federation Restrictions", - "column.filters": "Muted words", - "column.filters.add_new": "Add New Filter", - "column.filters.conversations": "Conversations", - "column.filters.create_error": "Error adding filter", - "column.filters.delete": "Delete", - "column.filters.delete_error": "Error deleting filter", - "column.filters.drop_header": "Drop instead of hide", - "column.filters.drop_hint": "Filtered posts will disappear irreversibly, even if filter is later removed", - "column.filters.expires": "Expire after", - "column.filters.expires_hint": "Expiration dates are not currently supported", - "column.filters.home_timeline": "Home timeline", - "column.filters.keyword": "Keyword or phrase", - "column.filters.notifications": "Notifications", - "column.filters.public_timeline": "Public timeline", - "column.filters.subheading_add_new": "Add New Filter", - "column.filters.subheading_filters": "Current Filters", - "column.filters.whole_word_header": "Whole word", - "column.filters.whole_word_hint": "When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word", - "column.follow_requests": "關注請求", - "column.followers": "Followers", - "column.following": "Following", - "column.groups": "Groups", + "column.crypto_donate": "數字貨幣捐贈", + "column.developers": "開發者", + "column.direct": "私訊", + "column.directory": "發現更多", + "column.domain_blocks": "隱藏的網域", + "column.edit_profile": "編輯個人資料", + "column.export_data": "匯出資料", + "column.favourited_statuses": "按讚的貼文", + "column.favourites": "按讚", + "column.federation_restrictions": "聯邦限制", + "column.filters": "過濾詞", + "column.filters.add_new": "新增過濾詞", + "column.filters.conversations": "聊天", + "column.filters.create_error": "新增過濾詞時出錯。", + "column.filters.delete": "刪除過濾詞", + "column.filters.delete_error": "刪除過濾詞時出錯。", + "column.filters.drop_header": "丟棄而非隱藏", + "column.filters.drop_hint": "被丟棄的帖文會不可逆地消失,即使移除過濾詞之後也不會恢復", + "column.filters.expires": "過期時間", + "column.filters.expires_hint": "過期時間暫未被支援", + "column.filters.home_timeline": "主頁時間軸", + "column.filters.keyword": "關鍵詞", + "column.filters.notifications": "通知", + "column.filters.public_timeline": "公共時間軸", + "column.filters.subheading_add_new": "新增過濾詞", + "column.filters.subheading_filters": "當前過濾詞", + "column.filters.whole_word_header": "全詞匹配", + "column.filters.whole_word_hint": "如果關鍵詞只含字母和數字,則只有全詞匹配才會被過濾", + "column.follow_requests": "追蹤請求", + "column.followers": "追蹤者", + "column.following": "正在追蹤", + "column.groups": "群組", "column.home": "主頁", - "column.import_data": "Import data", - "column.info": "Server information", - "column.lists": "列表", - "column.mentions": "Mentions", - "column.mfa": "Multi-Factor Authentication", - "column.mfa_cancel": "Cancel", - "column.mfa_confirm_button": "Confirm", - "column.mfa_disable_button": "Disable", - "column.mfa_setup": "Proceed to Setup", - "column.migration": "Account migration", - "column.mutes": "靜音名單", + "column.import_data": "匯入資料", + "column.info": "伺服器資訊", + "column.lists": "名單", + "column.mentions": "提及", + "column.mfa": "兩步驟驗證", + "column.mfa_cancel": "取消", + "column.mfa_confirm_button": "確認", + "column.mfa_disable_button": "停用", + "column.mfa_setup": "同意並繼續", + "column.migration": "帳户遷移", + "column.mutes": "被隱藏的使用者", "column.notifications": "通知", - "column.pins": "Pinned posts", - "column.preferences": "Preferences", - "column.public": "跨站時間軸", - "column.reactions": "Reactions", - "column.reblogs": "Reposts", - "column.remote": "Federated timeline", - "column.scheduled_statuses": "Scheduled Posts", - "column.search": "Search", - "column.security": "Security", - "column.settings_store": "Settings store", - "column.soapbox_config": "Soapbox config", - "column.test": "Test timeline", - "column_back_button.label": "返回", - "column_forbidden.body": "You do not have permission to access this page.", - "column_forbidden.title": "Forbidden", + "column.pins": "釘選的貼文", + "column.preferences": "偏好設定", + "column.public": "聯邦時間軸", + "column.reactions": "心情回應", + "column.reblogs": "轉帖", + "column.remote": "聯邦時間軸", + "column.scheduled_statuses": "定時帖文", + "column.search": "搜尋", + "column.settings_store": "設定儲存", + "column.soapbox_config": "Soapbox設定", + "column.test": "測試時間軸", + "column_back_button.label": "上一頁", + "column_forbidden.body": "您無權訪問這個頁面。", + "column_forbidden.title": "無權訪問", "column_header.show_settings": "顯示設定", - "common.cancel": "Cancel", + "common.cancel": "取消", + "common.error": "有些東西好像壞了...嘗試重載頁面", "community.column_settings.media_only": "僅媒體", - "community.column_settings.title": "Local timeline settings", - "compose.character_counter.title": "Used {chars} out of {maxChars} characters", - "compose.invalid_schedule": "You must schedule a post at least 5 minutes out.", - "compose.submit_success": "Your post was sent", - "compose_form.direct_message_warning": "這文章只有被提及的用戶才可以看到。", - "compose_form.hashtag_warning": "這文章因為不是公開,所以不會被標籤搜索。只有公開的文章才會被標籤搜索。", - "compose_form.lock_disclaimer": "你的用戶狀態為「{locked}」,任何人都能立即關注你,然後看到「只有關注者能看」的文章。", - "compose_form.lock_disclaimer.lock": "公共", - "compose_form.markdown.marked": "Post markdown enabled", - "compose_form.markdown.unmarked": "Post markdown disabled", - "compose_form.message": "Message", - "compose_form.placeholder": "你在想甚麼?", - "compose_form.poll.add_option": "Add a choice", - "compose_form.poll.duration": "Poll duration", - "compose_form.poll.option_placeholder": "Choice {number}", - "compose_form.poll.remove_option": "Remove this choice", - "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices", - "compose_form.poll.switch_to_single": "Change poll to allow for a single choice", - "compose_form.publish": "發文", + "community.column_settings.title": "本地時間軸設定", + "compare_history_modal.header": "編輯歷史", + "compose.character_counter.title": "最大字符數: {maxChars}; 已使用 {chars}", + "compose.edit_success": "你的帖文已編輯", + "compose.invalid_schedule": "定時帖文只能設定在五分鐘後或更遲發送", + "compose.submit_success": "帖文已送出", + "compose_form.direct_message_warning": "這條帖文只有被提及的使用者才看得到。", + "compose_form.hashtag_warning": "由於這則帖文被設定成「不公開」,所以它將不會被列在任何主題標籤下。只有公開的帖文才能藉主題標籤找到。", + "compose_form.lock_disclaimer": "您的帳户尚未{locked}。任何人都能追蹤您並看到您設定成只有追蹤者能看的帖文。", + "compose_form.lock_disclaimer.lock": "上鎖", + "compose_form.markdown.marked": "Markdown已啟用", + "compose_form.markdown.unmarked": "Markdown已禁用", + "compose_form.message": "私訊", + "compose_form.placeholder": "您正在想些什麼?", + "compose_form.poll.add_option": "新增選擇", + "compose_form.poll.duration": "投票期限", + "compose_form.poll.multiselect": "多選", + "compose_form.poll.multiselect_detail": "允許參與者選擇多個答案", + "compose_form.poll.option_placeholder": "第 {number} 個選擇", + "compose_form.poll.remove": "移除投票", + "compose_form.poll.remove_option": "移除此選擇", + "compose_form.poll.switch_to_multiple": "投票改為多選", + "compose_form.poll.switch_to_single": "投票改為單選", + "compose_form.poll_placeholder": "添加投票主題...", + "compose_form.publish": "發佈", "compose_form.publish_loud": "{publish}!", - "compose_form.schedule": "Schedule", - "compose_form.scheduled_statuses.click_here": "Click here", - "compose_form.scheduled_statuses.message": "You have scheduled posts. {click_here} to see them.", - "compose_form.sensitive.hide": "Mark media as sensitive", - "compose_form.sensitive.marked": "媒體被標示為敏感", - "compose_form.sensitive.unmarked": "媒體沒有被標示為敏感", - "compose_form.spoiler.marked": "文字被警告隱藏", - "compose_form.spoiler.unmarked": "文字沒有被隱藏", - "compose_form.spoiler_placeholder": "敏感警告訊息", + "compose_form.save_changes": "儲存變更", + "compose_form.schedule": "定時發佈", + "compose_form.scheduled_statuses.click_here": "點擊此處", + "compose_form.scheduled_statuses.message": "你有定時帖文, {click_here} 查看", + "compose_form.spoiler.marked": "正文已隱藏到警告之後", + "compose_form.spoiler.unmarked": "正文未被隱藏", + "compose_form.spoiler_placeholder": "請在此處寫入警告訊息", + "compose_form.spoiler_remove": "移除敏感內容", + "compose_form.spoiler_title": "敏感內容", "confirmation_modal.cancel": "取消", - "confirmations.admin.deactivate_user.confirm": "Deactivate @{name}", - "confirmations.admin.deactivate_user.heading": "Deactivate @{acct}", - "confirmations.admin.deactivate_user.message": "You are about to deactivate @{acct}. Deactivating a user is a reversible action.", - "confirmations.admin.delete_local_user.checkbox": "I understand that I am about to delete a local user.", - "confirmations.admin.delete_status.confirm": "Delete post", - "confirmations.admin.delete_status.heading": "Delete post", - "confirmations.admin.delete_status.message": "You are about to delete a post by @{acct}. This action cannot be undone.", - "confirmations.admin.delete_user.confirm": "Delete @{name}", - "confirmations.admin.delete_user.heading": "Delete @{acct}", - "confirmations.admin.delete_user.message": "You are about to delete @{acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.", - "confirmations.admin.mark_status_not_sensitive.confirm": "Mark post not sensitive", - "confirmations.admin.mark_status_not_sensitive.heading": "Mark post not sensitive.", - "confirmations.admin.mark_status_not_sensitive.message": "You are about to mark a post by @{acct} not sensitive.", - "confirmations.admin.mark_status_sensitive.confirm": "Mark post sensitive", - "confirmations.admin.mark_status_sensitive.heading": "Mark post sensitive", - "confirmations.admin.mark_status_sensitive.message": "You are about to mark a post by @{acct} sensitive.", - "confirmations.admin.reject_user.confirm": "Reject @{name}", - "confirmations.admin.reject_user.heading": "Reject @{acct}", - "confirmations.admin.reject_user.message": "You are about to reject @{acct} registration request. This action cannot be undone.", - "confirmations.block.block_and_report": "Block & Report", + "confirmations.admin.deactivate_user.confirm": "禁用帳户 @{name}", + "confirmations.admin.deactivate_user.heading": "禁用帳户 @{acct}", + "confirmations.admin.deactivate_user.message": "你確定要禁用帳户 @{acct} 嗎?此操作不能撤回!", + "confirmations.admin.delete_local_user.checkbox": "我確定我不再需要這個帳户", + "confirmations.admin.delete_status.confirm": "刪除帖文", + "confirmations.admin.delete_status.heading": "刪除帖文", + "confirmations.admin.delete_status.message": "你確定要刪除帖文 @{acct} 嗎?此操作不能撤回!", + "confirmations.admin.delete_user.confirm": "刪除帳户 @{name}", + "confirmations.admin.delete_user.heading": "刪除帳户 @{acct}", + "confirmations.admin.delete_user.message": "你確定要刪除帳户 @{acct}嗎?此操作不能撤回!", + "confirmations.admin.mark_status_not_sensitive.confirm": "標記為不敏感", + "confirmations.admin.mark_status_not_sensitive.heading": "標記為不敏感", + "confirmations.admin.mark_status_not_sensitive.message": "你要標記帳户 @{acct} 的帖文為不敏感", + "confirmations.admin.mark_status_sensitive.confirm": "標記為敏感帖文", + "confirmations.admin.mark_status_sensitive.heading": "標記為敏感帖文", + "confirmations.admin.mark_status_sensitive.message": "你要標記帳户 @{acct} 的帖文為敏感", + "confirmations.admin.reject_user.confirm": "拒絕 @{name}", + "confirmations.admin.reject_user.heading": "拒絕 @{acct}", + "confirmations.admin.reject_user.message": "你正準備拒絕 @{acct} 的註冊請求。此操作不能撤銷。", + "confirmations.block.block_and_report": "封鎖並檢舉", "confirmations.block.confirm": "封鎖", - "confirmations.block.heading": "Block @{name}", - "confirmations.block.message": "你確定要封鎖{name}嗎?", + "confirmations.block.heading": "封鎖 @{name}", + "confirmations.block.message": "確定封鎖 {name} ?", "confirmations.delete.confirm": "刪除", - "confirmations.delete.heading": "Delete post", - "confirmations.delete.message": "你確定要刪除這文章嗎?", + "confirmations.delete.heading": "刪除帖文", + "confirmations.delete.message": "你確定要刪除這條帖文?", "confirmations.delete_list.confirm": "刪除", - "confirmations.delete_list.heading": "Delete list", - "confirmations.delete_list.message": "你確定要永久刪除這列表嗎?", - "confirmations.domain_block.confirm": "隱藏整個網站", + "confirmations.delete_list.heading": "刪除列表", + "confirmations.delete_list.message": "你確定要永久刪除這個列表?", + "confirmations.domain_block.confirm": "隱藏整個網域", "confirmations.domain_block.heading": "Block {domain}", - "confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或靜音幾個特定目標就好。你從此將不會再看到該站的內容和通知。來自該站的關注者亦會被移除。", - "confirmations.mute.confirm": "靜音", + "confirmations.domain_block.message": "真的非常確定封鎖整個 {domain} 嗎?大部分情況下,你只需要封鎖或隱藏少數特定的人就能滿足需求了。你將不能在任何公開的時間軸及通知中看到那個網域的內容。你來自該網域的追蹤者也會被移除。", + "confirmations.mute.confirm": "隱藏", "confirmations.mute.heading": "Mute @{name}", - "confirmations.mute.message": "你確定要將{name}靜音嗎?", - "confirmations.redraft.confirm": "刪除並編輯", - "confirmations.redraft.heading": "Delete & redraft", - "confirmations.redraft.message": "你確定要刪除並重新編輯嗎?所有相關的回覆、轉推與最愛都會被刪除。", - "confirmations.register.needs_approval": "Your account will be manually approved by an admin. Please be patient while we review your details.", - "confirmations.register.needs_approval.header": "Approval needed", - "confirmations.register.needs_confirmation": "Please check your inbox at {email} for confirmation instructions. You will need to verify your email address to continue.", - "confirmations.register.needs_confirmation.header": "Confirmation needed", - "confirmations.reply.confirm": "Reply", - "confirmations.reply.message": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", - "confirmations.scheduled_status_delete.confirm": "Cancel", - "confirmations.scheduled_status_delete.heading": "Cancel scheduled post", - "confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?", - "confirmations.unfollow.confirm": "取消關注", - "confirmations.unfollow.heading": "Unfollow {name}", - "confirmations.unfollow.message": "真的不要繼續關注 {name} 了嗎?", - "crypto_donate.explanation_box.message": "{siteTitle} accepts cryptocurrency donations. You may send a donation to any of the addresses below. Thank you for your support!", - "crypto_donate.explanation_box.title": "Sending cryptocurrency donations", - "crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}", - "crypto_donate_panel.heading": "Donate Cryptocurrency", - "crypto_donate_panel.intro.message": "{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!", - "datepicker.hint": "Scheduled to post at…", - "datepicker.next_month": "Next month", - "datepicker.next_year": "Next year", - "datepicker.previous_month": "Previous month", - "datepicker.previous_year": "Previous year", - "developers.challenge.answer_label": "Answer", - "developers.challenge.answer_placeholder": "Your answer", - "developers.challenge.fail": "Wrong answer", - "developers.challenge.message": "What is the result of calling {function}?", - "developers.challenge.submit": "Become a developer", - "developers.challenge.success": "You are now a developer", - "developers.leave": "You have left developers", - "developers.navigation.app_create_label": "Create an app", - "developers.navigation.intentional_error_label": "Trigger an error", - "developers.navigation.leave_developers_label": "Leave developers", - "developers.navigation.network_error_label": "Network error", - "developers.navigation.settings_store_label": "Settings store", - "developers.navigation.test_timeline_label": "Test timeline", - "developers.settings_store.hint": "It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.", - "direct.search_placeholder": "Send a message to…", - "directory.federated": "From known fediverse", - "directory.local": "From {domain} only", - "directory.new_arrivals": "New arrivals", - "directory.recently_active": "Recently active", - "edit_federation.followers_only": "Hide posts except to followers", - "edit_federation.force_nsfw": "Force attachments to be marked sensitive", - "edit_federation.media_removal": "Strip media", - "edit_federation.reject": "Reject all activities", - "edit_federation.save": "Save", - "edit_federation.success": "{host} federation was updated", - "edit_federation.unlisted": "Force posts unlisted", - "edit_password.header": "Change Password", - "edit_profile.error": "Profile update failed", - "edit_profile.fields.accepts_email_list_label": "Subscribe to newsletter", - "edit_profile.fields.avatar_label": "Avatar", - "edit_profile.fields.bio_label": "Bio", - "edit_profile.fields.bio_placeholder": "Tell us about yourself.", - "edit_profile.fields.birthday_label": "Birthday", - "edit_profile.fields.birthday_placeholder": "Your birthday", - "edit_profile.fields.bot_label": "This is a bot account", - "edit_profile.fields.discoverable_label": "Allow account discovery", - "edit_profile.fields.display_name_label": "Display name", - "edit_profile.fields.display_name_placeholder": "Name", - "edit_profile.fields.header_label": "Header", - "edit_profile.fields.hide_network_label": "Hide network", - "edit_profile.fields.location_label": "Location", - "edit_profile.fields.location_placeholder": "Location", - "edit_profile.fields.locked_label": "Lock account", - "edit_profile.fields.meta_fields.content_placeholder": "Content", - "edit_profile.fields.meta_fields.label_placeholder": "Label", - "edit_profile.fields.stranger_notifications_label": "Block notifications from strangers", - "edit_profile.fields.website_label": "Website", - "edit_profile.fields.website_placeholder": "Display a Link", - "edit_profile.header": "Edit Profile", - "edit_profile.hints.accepts_email_list": "Opt-in to news and marketing updates.", - "edit_profile.hints.avatar": "PNG, GIF or JPG. Will be downscaled to {size}", - "edit_profile.hints.bot": "This account mainly performs automated actions and might not be monitored", - "edit_profile.hints.discoverable": "Display account in profile directory and allow indexing by external services", - "edit_profile.hints.header": "PNG, GIF or JPG. Will be downscaled to {size}", - "edit_profile.hints.hide_network": "Who you follow and who follows you will not be shown on your profile", - "edit_profile.hints.locked": "Requires you to manually approve followers", - "edit_profile.hints.stranger_notifications": "Only show notifications from people you follow", - "edit_profile.save": "Save", - "edit_profile.success": "Profile saved!", - "email_passthru.confirmed.body": "Close this tab and continue the registration process on the {bold} from which you sent this email confirmation.", - "email_passthru.confirmed.heading": "Email Confirmed!", - "email_passthru.generic_fail.body": "Please request a new email confirmation.", - "email_passthru.generic_fail.heading": "Something Went Wrong", - "email_passthru.token_expired.body": "Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.", - "email_passthru.token_expired.heading": "Token Expired", - "email_passthru.token_not_found.body": "Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.", - "email_passthru.token_not_found.heading": "Invalid Token", - "embed.instructions": "要內嵌此文章,請將以下代碼貼進你的網站。", - "embed.preview": "看上去會是這樣:", + "confirmations.mute.message": "確定隱藏 {name} ?", + "confirmations.redraft.confirm": "刪除並重新編輯", + "confirmations.redraft.heading": "刪除並重新編輯", + "confirmations.redraft.message": "確定刪掉這則帖文並重新編輯嗎?將會失去這則帖文的轉帖及收藏,且回覆這則的帖文將會變成獨立的帖文。", + "confirmations.register.needs_approval": "你的帳户正在被管理員審核中,請等待一會", + "confirmations.register.needs_approval.header": "需要審核", + "confirmations.register.needs_confirmation": "我們已經發送了指引到你的電郵 {email} 中,請檢查收件箱並點擊鏈接以繼續。", + "confirmations.register.needs_confirmation.header": "需要確認", + "confirmations.remove_from_followers.confirm": "刪除", + "confirmations.remove_from_followers.message": "確定要從你的追蹤者中移走 {name} 嗎?", + "confirmations.reply.confirm": "回覆", + "confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?", + "confirmations.scheduled_status_delete.confirm": "取消", + "confirmations.scheduled_status_delete.heading": "取消帖文定時發佈", + "confirmations.scheduled_status_delete.message": "你確定要取消這篇帖文定時發佈嗎?", + "confirmations.unfollow.confirm": "取消追蹤", + "confirmations.unfollow.heading": "取消追蹤 {name}", + "confirmations.unfollow.message": "真的要取消追蹤 {name} 嗎?", + "crypto_donate.explanation_box.message": "{siteTitle} 接受用户向以下錢包地址捐贈任意數量的數字資產。你的抖內會令我們做得更好!", + "crypto_donate.explanation_box.title": "發起數字貨幣捐贈", + "crypto_donate_panel.actions.view": "點擊查看 {count} {count, plural, one {wallet} other {wallets}}", + "crypto_donate_panel.heading": "捐贈數字貨幣", + "crypto_donate_panel.intro.message": "{siteTitle} 接受用户捐贈數字貨幣。感謝你的支持!", + "datepicker.day": "Day", + "datepicker.hint": "設定發送時間…", + "datepicker.month": "Month", + "datepicker.next_month": "下個月", + "datepicker.next_year": "明年", + "datepicker.previous_month": "上個月", + "datepicker.previous_year": "去年", + "datepicker.year": "Year", + "developers.challenge.answer_label": "回答", + "developers.challenge.answer_placeholder": "你的回答", + "developers.challenge.fail": "回答錯誤", + "developers.challenge.message": "調用函數 {function} 的結果是什麼?", + "developers.challenge.submit": "成為開發者", + "developers.challenge.success": "你已成為開發者", + "developers.leave": "您已退出開發者", + "developers.navigation.app_create_label": "創建應用", + "developers.navigation.intentional_error_label": "觸發錯誤", + "developers.navigation.leave_developers_label": "退出開發者", + "developers.navigation.network_error_label": "網絡錯誤", + "developers.navigation.settings_store_label": "設置儲存", + "developers.navigation.test_timeline_label": "測試時間軸", + "developers.settings_store.hint": "可以在此處直接編輯您的用户設置。 當心!編輯此部分可能會破壞您的帳户,您只能通過 API 恢復。", + "direct.body": "新的私訊傳遞體驗即將推出。 敬請期待。", + "direct.search_placeholder": "發送私信給…", + "directory.federated": "來自已知聯邦宇宙", + "directory.local": "僅來自 {domain}", + "directory.new_arrivals": "新增訪客", + "directory.recently_active": "最近活動", + "edit_federation.followers_only": "對追蹤者以外的用户隱藏帖文", + "edit_federation.force_nsfw": "將附件強制標記為敏感內容", + "edit_federation.media_removal": "去除媒體", + "edit_federation.reject": "拒絕所有資訊交互", + "edit_federation.save": "儲存", + "edit_federation.success": "{host} 聯邦設定已儲存", + "edit_federation.unlisted": "將帖文強制標記移出公共時間軸", + "edit_password.header": "修改密碼", + "edit_profile.error": "個人資料更新失敗", + "edit_profile.fields.accepts_email_list_label": "訂閲電子郵件列表", + "edit_profile.fields.avatar_label": "頭帖", + "edit_profile.fields.bio_label": "簡介", + "edit_profile.fields.bio_placeholder": "介紹一下自己", + "edit_profile.fields.birthday_label": "生日", + "edit_profile.fields.birthday_placeholder": "你的生日", + "edit_profile.fields.bot_label": "這是一個機械人帳户", + "edit_profile.fields.discoverable_label": "公開自己", + "edit_profile.fields.display_name_label": "昵稱", + "edit_profile.fields.display_name_placeholder": "你的昵稱", + "edit_profile.fields.header_label": "個人資料頁橫幅圖片", + "edit_profile.fields.hide_network_label": "隱藏網絡狀態", + "edit_profile.fields.location_label": "地點", + "edit_profile.fields.location_placeholder": "地點", + "edit_profile.fields.locked_label": "鎖定帳户", + "edit_profile.fields.meta_fields.content_placeholder": "內容", + "edit_profile.fields.meta_fields.label_placeholder": "標籤", + "edit_profile.fields.verified_display_name": "經過驗證的用户無法修改昵稱", + "edit_profile.fields.meta_fields_label": "個人資料自定義列", + "edit_profile.fields.stranger_notifications_label": "封鎖來自陌生人的通知", + "edit_profile.fields.website_label": "網站", + "edit_profile.fields.website_placeholder": "顯示連結", + "edit_profile.header": "編輯個人資料", + "edit_profile.hints.accepts_email_list": "接收新聞和推廣電郵", + "edit_profile.hints.avatar": "只支援 PNG, GIF or JPG。 尺寸將會被縮放至 {size}", + "edit_profile.hints.bot": "此帳户主要執行自動化操作,可能不受用户控制", + "edit_profile.hints.discoverable": "在配置文件目錄中顯示帳户並允許外部服務索引", + "edit_profile.hints.header": "只支援 PNG, GIF or JPG。 尺寸將會被縮放至 {size}", + "edit_profile.hints.hide_network": "您追蹤的人和追蹤您的人不會顯示在您的個人資料中", + "edit_profile.hints.locked": "需要您手動批准追蹤請求", + "edit_profile.hints.meta_fields": "你能在個人資料頁面上最多顯示 {count} 行自定義列", + "edit_profile.hints.stranger_notifications": "僅顯示來自您追蹤的人的通知", + "edit_profile.save": "儲存", + "edit_profile.success": "個人資料已儲存", + "email_passthru.confirmed.body": "關閉此頁面,並在你發送此電子郵件確認的 {bold} 上繼續註冊過程", + "email_passthru.confirmed.heading": "電郵地址已確認!", + "email_passthru.generic_fail.body": "請重新請求確認郵件", + "email_passthru.generic_fail.heading": "好像出了一點問題…", + "email_passthru.token_expired.body": "你的電郵令牌已經過期。請從你發送此電郵確認的 {bold} 處重新申請電郵確認。", + "email_passthru.token_expired.heading": "令牌已過期", + "email_passthru.token_not_found.body": "你的電郵令牌已經過期。請從你發送此電郵確認的 {bold} 處重新申請電郵確認。", + "email_passthru.token_not_found.heading": "無效令牌!", + "embed.instructions": "要嵌入此帖文,請將以下程式碼貼進你的網站。", + "embed.preview": "他會顯示成這樣:", "emoji_button.activity": "活動", "emoji_button.custom": "自訂", - "emoji_button.flags": "旗幟", - "emoji_button.food": "飲飲食食", - "emoji_button.label": "加入表情符號", - "emoji_button.nature": "自然", - "emoji_button.not_found": "沒有表情符號!! (╯°□°)╯︵ ┻━┻", - "emoji_button.objects": "物品", - "emoji_button.people": "人物", - "emoji_button.recent": "常用", + "emoji_button.flags": "旗標", + "emoji_button.food": "飲食", + "emoji_button.label": "插入表情符號", + "emoji_button.nature": "大自然", + "emoji_button.not_found": "啊就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻", + "emoji_button.objects": "物件", + "emoji_button.people": "使用者", + "emoji_button.recent": "最常使用", "emoji_button.search": "搜尋…", "emoji_button.search_results": "搜尋結果", "emoji_button.symbols": "符號", - "emoji_button.travel": "旅遊景物", - "empty_column.account_blocked": "You are blocked by @{accountUsername}.", - "empty_column.account_favourited_statuses": "This user doesn't have any liked posts yet.", - "empty_column.account_timeline": "No posts here!", - "empty_column.account_unavailable": "Profile unavailable", - "empty_column.aliases": "You haven't created any account alias yet.", - "empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.", - "empty_column.blocks": "You haven't blocked any users yet.", - "empty_column.bookmarks": "You don't have any bookmarks yet. When you add one, it will show up here.", - "empty_column.community": "本站時間軸暫時未有內容,快寫一點東西來搶頭香啊!", - "empty_column.direct": "你沒有個人訊息。當你發出或接收個人訊息,就會在這裡出現。", - "empty_column.domain_blocks": "There are no hidden domains yet.", - "empty_column.favourited_statuses": "You don't have any favorite posts yet. When you favorite one, it will show up here.", - "empty_column.favourites": "No one has favorited this post yet. When someone does, they will show up here.", - "empty_column.filters": "You haven't created any muted words yet.", - "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", - "empty_column.follow_requests": "You don't have any follow requests yet. When you receive one, it will show up here.", - "empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.", - "empty_column.hashtag": "這個標籤暫時未有內容。", - "empty_column.home": "你還沒有關注任何用戶。快看看{public},向其他用戶搭訕吧。", - "empty_column.home.local_tab": "the {site_title} tab", - "empty_column.list": "這個列表暫時未有內容。", - "empty_column.lists": "You don't have any lists yet. When you create one, it will show up here.", - "empty_column.mutes": "You haven't muted any users yet.", - "empty_column.notifications": "你沒有任何通知紀錄,快向其他用戶搭訕吧。", - "empty_column.public": "跨站時間軸暫時沒有內容!快寫一些公共的文章,或者關注另一些服務站的用戶吧!你和本站、友站的交流,將決定這裏出現的內容。", - "empty_column.remote": "There is nothing here! Manually follow users from {instance} to fill it up.", - "empty_column.scheduled_statuses": "You don't have any scheduled statuses yet. When you add one, it will show up here.", - "empty_column.search.accounts": "There are no people results for \"{term}\"", - "empty_column.search.hashtags": "There are no hashtags results for \"{term}\"", - "empty_column.search.statuses": "There are no posts results for \"{term}\"", - "export_data.actions.export": "Export", - "export_data.actions.export_blocks": "Export blocks", - "export_data.actions.export_follows": "Export follows", - "export_data.actions.export_mutes": "Export mutes", - "export_data.blocks_label": "Blocks", - "export_data.follows_label": "Follows", - "export_data.hints.blocks": "Get a CSV file containing a list of blocked accounts", - "export_data.hints.follows": "Get a CSV file containing a list of followed accounts", - "export_data.hints.mutes": "Get a CSV file containing a list of muted accounts", - "export_data.mutes_label": "Mutes", - "export_data.success.blocks": "Blocks exported successfully", - "export_data.success.followers": "Followers exported successfully", - "export_data.success.mutes": "Mutes exported successfully", - "federation_restriction.federated_timeline_removal": "Fediverse timeline removal", - "federation_restriction.followers_only": "Hidden except to followers", - "federation_restriction.full_media_removal": "Full media removal", - "federation_restriction.media_nsfw": "Attachments marked NSFW", - "federation_restriction.partial_media_removal": "Partial media removal", - "federation_restrictions.empty_message": "{siteTitle} has not restricted any instances.", - "federation_restrictions.explanation_box.message": "Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.", - "federation_restrictions.explanation_box.title": "Instance-specific policies", - "federation_restrictions.not_disclosed_message": "{siteTitle} does not disclose federation restrictions through the API.", - "fediverse_tab.explanation_box.dismiss": "Don't show again", - "fediverse_tab.explanation_box.explanation": "{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka \"servers\"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don't like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.", - "fediverse_tab.explanation_box.title": "What is the Fediverse?", - "filters.added": "Filter added.", - "filters.context_header": "Filter contexts", - "filters.context_hint": "One or multiple contexts where the filter should apply", - "filters.filters_list_context_label": "Filter contexts:", - "filters.filters_list_delete": "Delete", - "filters.filters_list_details_label": "Filter settings:", - "filters.filters_list_drop": "Drop", - "filters.filters_list_hide": "Hide", - "filters.filters_list_phrase_label": "Keyword or phrase:", - "filters.filters_list_whole-word": "Whole word", - "filters.removed": "Filter deleted.", - "follow_recommendation.subhead": "Let's get started!", - "follow_recommendations.done": "Done", - "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", - "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", - "follow_request.authorize": "批准", + "emoji_button.travel": "旅遊與地點", + "empty_column.account_blocked": "你被 @{accountUsername} 封鎖了", + "empty_column.account_favourited_statuses": "他還沒有按讚任何貼文", + "empty_column.account_timeline": "這裡還沒有帖文!", + "empty_column.account_unavailable": "無法取得個人資料", + "empty_column.aliases": "你還沒有設定任何別名", + "empty_column.aliases.suggestions": "暫時沒有可用的帳户建議", + "empty_column.blocks": "你還沒有封鎖任何使用者。", + "empty_column.bookmarks": "你還沒有任何書籤收藏,一旦你開始將帖文加入書籤,它將會在這裡顯示", + "empty_column.community": "本地時間軸是空的。快公開發佈些文搶頭香啊!", + "empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。", + "empty_column.domain_blocks": "尚未隱藏任何網域。", + "empty_column.favourited_statuses": "你還沒收藏任何帖文。這裡將會顯示你收藏的帖文。", + "empty_column.favourites": "還沒有人收藏這則帖文。這裡將會顯示被收藏的帖文。", + "empty_column.filters": "你還未有添加任何過濾詞。", + "empty_column.follow_recommendations": "目前似乎暫時沒有推薦訊息,你可以嘗試搜尋用户或者瀏覽熱門標籤。", + "empty_column.follow_requests": "您尚未收到任何追蹤請求。這裡將會顯示收到的追蹤請求。", + "empty_column.group": "該組中還沒有任何內容。 當該群組的成員發布新帖子時,他們會出現在此處。", + "empty_column.hashtag": "這個主題標籤下什麼也沒有。", + "empty_column.home": "您的首頁時間軸是空的!前往 {public} 或使用搜尋功能來認識其他人。", + "empty_column.home.local_tab": "{site_title} 本站時間軸", + "empty_column.list": "這份名單還沒有東西。當此名單的成員發佈了新的帖文時,它們就會顯示於此。", + "empty_column.lists": "你還沒有建立任何名單。這裡將會顯示你所建立的名單。", + "empty_column.mutes": "你尚未隱藏任何使用者。", + "empty_column.notifications": "您尚未收到任何通知,和別人互動開啟對話吧。", + "empty_column.public": "這裡什麼都沒有!嘗試寫些公開的帖文,或著自己追蹤其他伺服器的使用者後就會有帖文出現了", + "empty_column.remote": "這裡什麼都沒有! 關注本站或者其他站點的成員,就會有用户出現在這裡。", + "empty_column.scheduled_statuses": "暫時沒有定時帖文。當你發佈定時帖文後,它們會顯示在這裡。", + "empty_column.search.accounts": "沒有匹配的帳户 \"{term}\"", + "empty_column.search.hashtags": "沒有匹配的標籤 \"{term}\"", + "empty_column.search.statuses": "沒有匹配的帖文 \"{term}\"", + "empty_column.test": "測試時間軸是空的", + "export_data.actions.export": "匯出", + "export_data.actions.export_blocks": "匯出封鎖名單", + "export_data.actions.export_follows": "匯出追蹤名單", + "export_data.actions.export_mutes": "匯出隱藏名單", + "export_data.blocks_label": "封鎖", + "export_data.follows_label": "追蹤", + "export_data.hints.blocks": "匯出封鎖名單為CSV檔案", + "export_data.hints.follows": "匯出追蹤名單為CSV檔案", + "export_data.hints.mutes": "匯出隱藏名單為CSV檔案", + "export_data.mutes_label": "隱藏", + "export_data.success.blocks": "封鎖名單匯出成功", + "export_data.success.followers": "追蹤名單匯出成功", + "export_data.success.mutes": "隱藏名單匯出成功", + "federation_restriction.federated_timeline_removal": "從聯邦宇宙時間軸移除", + "federation_restriction.followers_only": "僅追蹤者可見", + "federation_restriction.full_media_removal": "完全移除媒體", + "federation_restriction.media_nsfw": "附件標註為 NSFW", + "federation_restriction.partial_media_removal": "部分移除媒體", + "federation_restrictions.empty_message": "{siteTitle} 沒有限制任何實例", + "federation_restrictions.explanation_box.message": "通常情況下,聯邦宇宙上的伺服器可以自由通訊。然而 {siteTitle} 對以下伺服器實施了限制", + "federation_restrictions.explanation_box.title": "實例相關政策", + "federation_restrictions.not_disclosed_message": "{siteTitle} 沒有通過API向聯邦宇宙公開限制。", + "fediverse_tab.explanation_box.dismiss": "不再顯示", + "fediverse_tab.explanation_box.explanation": "{site_title} 是聯邦宇宙的一份子, 一個由數個站點組成的社交網絡集合。你在這裏看到的帖文來自於其他站點。你可以自由地與他們打交道,或者封鎖任何你不喜歡的站點。第二個 @ 符號後的完整帳户名表示帖文來自哪個站點。要想只看到 {site_title} 的帖文, 請瀏覽 {local} 。", + "fediverse_tab.explanation_box.title": "什麼是聯邦宇宙?", + "feed_suggestions.heading": "建議的個人資料", + "feed_suggestions.view_all": "檢視全部", + "filters.added": "過濾器已添加。", + "filters.context_header": "過濾器場景", + "filters.context_hint": "一個或多個應用至過濾器的條件", + "filters.filters_list_context_label": "過濾器場景:", + "filters.filters_list_delete": "刪除過濾詞", + "filters.filters_list_details_label": "過濾詞設定:", + "filters.filters_list_drop": "丟棄", + "filters.filters_list_hide": "隱藏", + "filters.filters_list_phrase_label": "關鍵詞:", + "filters.filters_list_whole-word": "全詞", + "filters.removed": "過濾器已移除", + "follow_recommendation.subhead": "讓我們開始吧!", + "follow_recommendations.done": "完成", + "follow_recommendations.heading": "追蹤你感興趣的人,這是我們的推薦列表", + "follow_recommendations.lead": "你追蹤的人的帖文將按照時間順序顯示在你的主頁上。不用擔心搞錯,你可以在任何時候輕鬆地撤銷追蹤!", + "follow_request.authorize": "授權", "follow_request.reject": "拒絕", - "forms.copy": "Copy", - "forms.hide_password": "Hide password", - "forms.show_password": "Show password", - "getting_started.open_source_notice": "{code_name}(萬象)是一個開放源碼的軟件。你可以在官方 GitLab ({code_link} (v{code_version})) 貢獻或者回報問題。", - "group.detail.archived_group": "Archived group", - "group.members.empty": "This group does not has any members.", - "group.removed_accounts.empty": "This group does not has any removed accounts.", - "groups.card.join": "Join", - "groups.card.members": "Members", - "groups.card.roles.admin": "You're an admin", - "groups.card.roles.member": "You're a member", - "groups.card.view": "View", - "groups.create": "Create group", - "groups.detail.role_admin": "You're an admin", - "groups.edit": "Edit", - "groups.form.coverImage": "Upload new banner image (optional)", - "groups.form.coverImageChange": "Banner image selected", - "groups.form.create": "Create group", - "groups.form.description": "Description", - "groups.form.title": "Title", - "groups.form.update": "Update group", - "groups.join": "Join group", - "groups.leave": "Leave group", - "groups.removed_accounts": "Removed Accounts", - "groups.sidebar-panel.item.no_recent_activity": "No recent activity", - "groups.sidebar-panel.item.view": "new posts", - "groups.sidebar-panel.show_all": "Show all", - "groups.sidebar-panel.title": "Groups You're In", - "groups.tab_admin": "Manage", - "groups.tab_featured": "Featured", - "groups.tab_member": "Member", - "hashtag.column_header.tag_mode.all": "and {additional}", - "hashtag.column_header.tag_mode.any": "or {additional}", - "hashtag.column_header.tag_mode.none": "without {additional}", - "header.home.label": "Home", - "header.login.forgot_password": "Forgot password?", - "header.login.label": "Log in", - "header.login.password.label": "Password", - "header.login.username.placeholder": "Email or username", - "header.register.label": "Register", - "home.column_settings.show_direct": "Show direct messages", - "home.column_settings.show_reblogs": "顯示被轉推的文章", - "home.column_settings.show_replies": "顯示回應文章", - "home.column_settings.title": "Home settings", - "icon_button.icons": "Icons", - "icon_button.label": "Select icon", - "icon_button.not_found": "No icons!! (╯°□°)╯︵ ┻━┻", - "import_data.actions.import": "Import", - "import_data.actions.import_blocks": "Import blocks", - "import_data.actions.import_follows": "Import follows", - "import_data.actions.import_mutes": "Import mutes", - "import_data.blocks_label": "Blocks", - "import_data.follows_label": "Follows", - "import_data.hints.blocks": "CSV file containing a list of blocked accounts", - "import_data.hints.follows": "CSV file containing a list of followed accounts", - "import_data.hints.mutes": "CSV file containing a list of muted accounts", - "import_data.mutes_label": "Mutes", - "import_data.success.blocks": "Blocks imported successfully", - "import_data.success.followers": "Followers imported successfully", - "import_data.success.mutes": "Mutes imported successfully", - "input.password.hide_password": "Hide password", - "input.password.show_password": "Show password", - "intervals.full.days": "{number, plural, one {# day} other {# days}}", - "intervals.full.hours": "{number, plural, one {# hour} other {# hours}}", - "intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}", - "introduction.federation.action": "Next", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.interactions.action": "Finish tutorial!", - "introduction.interactions.favourite.headline": "Favorite", - "introduction.interactions.favourite.text": "You can save a post for later, and let the author know that you liked it, by favoriting it.", - "introduction.interactions.reblog.headline": "Repost", - "introduction.interactions.reblog.text": "You can share other people's posts with your followers by reposting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own posts, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", - "keyboard_shortcuts.back": "後退", - "keyboard_shortcuts.blocked": "to open blocked users list", - "keyboard_shortcuts.boost": "轉推", - "keyboard_shortcuts.compose": "把標示移動到文字輸入區", - "keyboard_shortcuts.down": "在列表往下移動", - "keyboard_shortcuts.enter": "打開文章", + "forms.copy": "複製", + "forms.hide_password": "隱藏密碼", + "forms.show_password": "顯示密碼", + "getting_started.open_source_notice": "{code_name} 是開源軟體。你可以在 GitLab {code_link} (v{code_version}) 上貢獻或是回報問題。", + "group.members.empty": "這個名單中沒有任何成員", + "group.removed_accounts.empty": "這個名單中沒有任何被移除的帳户", + "groups.card.join": "加入", + "groups.card.members": "成員", + "groups.card.roles.admin": "你是管理員", + "groups.card.roles.member": "你是成員", + "groups.card.view": "檢視", + "groups.create": "創建群組", + "groups.form.coverImage": "上載橫幅圖片 (可選)", + "groups.form.coverImageChange": "橫幅圖片已上載", + "groups.form.create": "創建群組", + "groups.form.description": "描述", + "groups.form.title": "標題", + "groups.form.update": "更新群組", + "groups.removed_accounts": "已移除帳户", + "groups.tab_admin": "管理", + "groups.tab_featured": "精選", + "groups.tab_member": "成員", + "hashtag.column_header.tag_mode.all": "以及{additional}", + "hashtag.column_header.tag_mode.any": "或是{additional}", + "hashtag.column_header.tag_mode.none": "而無需{additional}", + "header.home.label": "首頁", + "header.login.forgot_password": "忘記了密碼?", + "header.login.label": "登入", + "header.login.password.label": "密碼", + "header.login.username.placeholder": "電郵地址或帳户名稱", + "header.preview_timeline.label": "瀏覽首頁", + "header.register.label": "註冊", + "home.column_settings.show_reblogs": "顯示轉帖", + "home.column_settings.show_replies": "顯示回覆", + "icon_button.icons": "圖示", + "icon_button.label": "選取圖示", + "icon_button.not_found": "沒有圖示!! (╯°□°)╯︵ ┻━┻", + "import_data.actions.import": "匯入", + "import_data.actions.import_blocks": "匯入封鎖名單", + "import_data.actions.import_follows": "匯入追蹤名單", + "import_data.actions.import_mutes": "匯入隱藏名單", + "import_data.blocks_label": "封鎖帳户", + "import_data.follows_label": "追蹤帳户", + "import_data.hints.blocks": "上載包含封鎖帳户名單的CSV檔案", + "import_data.hints.follows": "上載包含追蹤帳户名單的CSV檔案", + "import_data.hints.mutes": "上載包含隱藏帳户名單的CSV檔案", + "import_data.mutes_label": "隱藏帳户", + "import_data.success.blocks": "封鎖帳户名單已匯入", + "import_data.success.followers": "追蹤帳户名單已匯入", + "import_data.success.mutes": "隱藏帳户名單已匯入", + "input.password.hide_password": "隱藏密碼", + "input.password.show_password": "顯示密碼", + "intervals.full.days": "{number} 天", + "intervals.full.hours": "{number} 小時", + "intervals.full.minutes": "{number} 分鐘", + "keyboard_shortcuts.back": "返回上一頁", + "keyboard_shortcuts.blocked": "開啟「封鎖使用者」名單", + "keyboard_shortcuts.boost": "轉帖", + "keyboard_shortcuts.compose": "將焦點移至撰寫文字區塊", + "keyboard_shortcuts.down": "往下移動名單項目", + "keyboard_shortcuts.enter": "檢視帖文", "keyboard_shortcuts.favourite": "收藏", - "keyboard_shortcuts.favourites": "to open favorites list", + "keyboard_shortcuts.favourites": "開啟收藏名單", "keyboard_shortcuts.heading": "鍵盤快速鍵", - "keyboard_shortcuts.home": "to open home timeline", + "keyboard_shortcuts.home": "開啟首頁時間軸", "keyboard_shortcuts.hotkey": "快速鍵", - "keyboard_shortcuts.legend": "顯示這個說明", + "keyboard_shortcuts.legend": "顯示此名單", "keyboard_shortcuts.mention": "提及作者", - "keyboard_shortcuts.muted": "to open muted users list", - "keyboard_shortcuts.my_profile": "to open your profile", - "keyboard_shortcuts.notifications": "to open notifications column", - "keyboard_shortcuts.open_media": "to open media", - "keyboard_shortcuts.pinned": "to open pinned posts list", - "keyboard_shortcuts.profile": "to open author's profile", - "keyboard_shortcuts.react": "to react", + "keyboard_shortcuts.muted": "開啟隱藏使用者名單", + "keyboard_shortcuts.my_profile": "開啟個人資料頁面", + "keyboard_shortcuts.notifications": "開啟通知欄", + "keyboard_shortcuts.open_media": "開啟媒體", + "keyboard_shortcuts.pinned": "開啟釘選的帖文名單", + "keyboard_shortcuts.profile": "開啟作者的個人資料頁面", + "keyboard_shortcuts.react": "心情回應", "keyboard_shortcuts.reply": "回覆", - "keyboard_shortcuts.requests": "to open follow requests list", - "keyboard_shortcuts.search": "把標示移動到搜索", - "keyboard_shortcuts.toggle_hidden": "顯示或隱藏被標為敏感的文字", - "keyboard_shortcuts.toggle_sensitivity": "to show/hide media", - "keyboard_shortcuts.toot": "新的推文", - "keyboard_shortcuts.unfocus": "把標示移離文字輸入和搜索", - "keyboard_shortcuts.up": "在列表往上移動", - "landing_page_modal.download": "Download", - "landing_page_modal.helpCenter": "Help Center", + "keyboard_shortcuts.requests": "開啟追蹤請求名單", + "keyboard_shortcuts.search": "將焦點移至搜尋框", + "keyboard_shortcuts.toggle_hidden": "顯示/隱藏在內容警告之後的正文", + "keyboard_shortcuts.toggle_sensitivity": "顯示 / 隱藏媒體", + "keyboard_shortcuts.toot": "開始發出新帖文", + "keyboard_shortcuts.unfocus": "取消輸入文字區塊 / 搜尋的焦點", + "keyboard_shortcuts.up": "往上移動名單項目", + "landing_page_modal.download": "下載", + "landing_page_modal.helpCenter": "支援中心", "lightbox.close": "關閉", - "lightbox.next": "繼續", - "lightbox.previous": "回退", - "lightbox.view_context": "View context", - "list.click_to_add": "Click here to add people", - "list_adder.header_title": "Add or Remove from Lists", - "lists.account.add": "新增到列表", - "lists.account.remove": "從列表刪除", - "lists.delete": "Delete list", - "lists.edit": "編輯列表", - "lists.edit.submit": "Change title", - "lists.new.create": "新增列表", - "lists.new.create_title": "Add list", - "lists.new.save_title": "Save Title", - "lists.new.title_placeholder": "新列表標題", - "lists.search": "從你關注的用戶中搜索", - "lists.subheading": "列表", - "loading_indicator.label": "載入中...", - "login.fields.instance_label": "Instance", + "lightbox.next": "下一步", + "lightbox.previous": "上一步", + "lightbox.view_context": "檢視內文", + "list.click_to_add": "點擊新增帳户到名單", + "list_adder.header_title": "從名單中添加或刪除帳户", + "lists.account.add": "新增至名單", + "lists.account.remove": "從名單中移除", + "lists.delete": "刪除名單", + "lists.edit": "編輯名單", + "lists.edit.submit": "變更標題", + "lists.new.create": "新增名單", + "lists.new.create_title": "新增名單", + "lists.new.save_title": "儲存名單", + "lists.new.title_placeholder": "新名單標題", + "lists.search": "搜尋您追蹤的使用者", + "lists.subheading": "您的名單", + "loading_indicator.label": "讀取中...", + "login.fields.instance_label": "實例", "login.fields.instance_placeholder": "example.com", - "login.fields.otp_code_hint": "Enter the two-factor code generated by your phone app or use one of your recovery codes", - "login.fields.otp_code_label": "Two-factor code:", - "login.fields.password_placeholder": "Password", - "login.fields.username_label": "Email or username", - "login.log_in": "Log in", - "login.otp_log_in": "OTP Login", - "login.reset_password_hint": "Trouble logging in?", - "login.sign_in": "Sign in", - "media_gallery.toggle_visible": "打開或關上", - "media_panel.empty_message": "No media found.", - "media_panel.title": "Media", - "mfa.confirm.success_message": "MFA confirmed", - "mfa.disable.success_message": "MFA disabled", - "mfa.mfa_disable_enter_password": "Enter your current password to disable two-factor auth:", - "mfa.mfa_setup.code_hint": "Enter the code from your two-factor app.", - "mfa.mfa_setup.code_placeholder": "Code", - "mfa.mfa_setup.password_hint": "Enter your current password to confirm your identity.", - "mfa.mfa_setup.password_placeholder": "Password", - "mfa.mfa_setup_scan_description": "Using your two-factor app, scan this QR code or enter text key:", - "mfa.mfa_setup_scan_title": "Scan", - "mfa.mfa_setup_verify_title": "Verify", - "mfa.otp_enabled_description": "You have enabled two-factor authentication via OTP.", - "mfa.otp_enabled_title": "OTP Enabled", - "mfa.setup_recoverycodes": "Recovery codes", - "mfa.setup_warning": "Write these codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.", - "migration.fields.acct.label": "Handle of the new account", - "migration.fields.acct.placeholder": "username@domain", - "migration.fields.confirm_password.label": "Current password", - "migration.hint": "This will move your followers to the new account. No other data will be moved. To perform migration, you need to {link} on your new account first.", - "migration.hint.link": "create an account alias", - "migration.move_account.fail": "Account migration failed.", - "migration.move_account.success": "Account successfully moved.", - "migration.submit": "Move followers", - "missing_description_modal.cancel": "Cancel", - "missing_description_modal.continue": "Post", - "missing_description_modal.description": "Continue anyway?", - "missing_description_modal.text": "You have not entered a description for all attachments. Continue anyway?", - "missing_indicator.label": "找不到內容", - "missing_indicator.sublabel": "無法找到內容", - "mobile.also_available": "Available in:", - "morefollows.followers_label": "…and {count} more {count, plural, one {follower} other {followers}} on remote sites.", - "morefollows.following_label": "…and {count} more {count, plural, one {follow} other {follows}} on remote sites.", - "mute_modal.hide_notifications": "隱藏來自這用戶的通知嗎?", - "navigation.chats": "Chats", - "navigation.compose": "Compose", - "navigation.dashboard": "Dashboard", - "navigation.developers": "Developers", - "navigation.direct_messages": "Messages", - "navigation.home": "Home", - "navigation.invites": "Invites", - "navigation.notifications": "Notifications", - "navigation.search": "Search", - "navigation_bar.account_migration": "Move account", - "navigation_bar.blocks": "被你封鎖的用戶", - "navigation_bar.compose": "Compose new post", - "navigation_bar.compose_direct": "Direct message", - "navigation_bar.compose_quote": "Quote post", - "navigation_bar.compose_reply": "Reply to post", - "navigation_bar.domain_blocks": "隱藏的服務站", - "navigation_bar.favourites": "最愛的內容", - "navigation_bar.filters": "Muted words", - "navigation_bar.follow_requests": "關注請求", - "navigation_bar.import_data": "Import data", - "navigation_bar.in_reply_to": "In reply to", - "navigation_bar.invites": "Invites", + "login.fields.otp_code_hint": "輸入兩步驟驗證應用程式裏的代碼,或者輸入恢復代碼", + "login.fields.otp_code_label": "兩步驟驗證代碼:", + "login.fields.password_placeholder": "密碼", + "login.fields.username_label": "電郵地址或帳户名稱", + "login.log_in": "登入", + "login.otp_log_in": "兩步驟驗證登入", + "login.otp_log_in.fail": "兩步驟驗證代號無效,請重新輸入", + "login.reset_password_hint": "登入遇到了問題?", + "login.sign_in": "登入", + "login_form.header": "登入", + "media_gallery.toggle_visible": "切換可見性", + "media_panel.empty_message": "未找到媒體", + "media_panel.title": "媒體", + "mfa.confirm.success_message": "多重要素驗證 (MFA) 已啟用", + "mfa.disable.success_message": "多重要素驗證 (MFA) 已停用", + "mfa.mfa_disable_enter_password": "輸入當前密碼以停用兩步驟驗證:", + "mfa.mfa_setup.code_hint": "輸入兩步驟驗證應用程式裏的代碼", + "mfa.mfa_setup.code_placeholder": "代碼", + "mfa.mfa_setup.password_hint": "輸入當前密碼以確認你的身份", + "mfa.mfa_setup.password_placeholder": "密碼", + "mfa.mfa_setup_scan_description": "請使用 Google Authenticator 或其他應用程式來掃描 QR 碼。啟用兩步驟驗證後,在登入時你需要提供該應用程式生成的代碼", + "mfa.mfa_setup_scan_title": "如果你無法掃描 QR 碼,請手動輸入下列文本:", + "mfa.mfa_setup_verify_title": "啟用", + "mfa.otp_enabled_description": "您已成功啟用兩步驟驗證", + "mfa.otp_enabled_title": "兩步驟驗證已啟用", + "mfa.setup_recoverycodes": "恢復代碼", + "mfa.setup_warning": "請將你的恢復代碼記在安全的地方或寫在紙上,建議與個人資訊分開存放,以便在丟失兩步驟驗證應用程式時可以恢復你的帳户。", + "migration.fields.acct.label": "新帳户的帳户名稱", + "migration.fields.acct.placeholder": "帳户名稱@網域", + "migration.fields.confirm_password.label": "當前密碼", + "migration.hint": "你的追蹤者將被轉移到新帳户,除此之外其他資料將不會被轉移。要執行遷移,您需要先{link}你的新帳户", + "migration.hint.link": "新建一個帳户別名", + "migration.move_account.fail": "帳户遷移失敗。", + "migration.move_account.success": "帳户遷移成功了!", + "migration.submit": "遷移關注者", + "missing_description_modal.cancel": "取消", + "missing_description_modal.continue": "發佈", + "missing_description_modal.description": "仍然繼續發佈嗎?", + "missing_description_modal.text": "附件沒有提供描述。仍然繼續發佈嗎?", + "missing_indicator.label": "找不到", + "missing_indicator.sublabel": "找不到此資源", + "mobile.also_available": "在這裏可用:", + "moderation_overlay.contact": "聯絡", + "moderation_overlay.hide": "隱藏內容", + "moderation_overlay.show": "顯示內容", + "moderation_overlay.subtitle": "此帖文已發送至站務以供審核,目前只允許本人查看。如你認為該消息有誤,請聯絡站務", + "moderation_overlay.title": "內容正被審核中", + "morefollows.followers_label": "和{count}來自其他站點的{count, plural, one {追蹤者} other {追蹤者}} 。", + "morefollows.following_label": "和{count}來自其他站點的{count, plural, one {正在追蹤} other {正在追蹤}} 。", + "mute_modal.hide_notifications": "隱藏來自這個帳户的通知?", + "navbar.login.action": "登入", + "navbar.login.forgot_password": "忘記密碼?", + "navbar.login.password.label": "密碼", + "navbar.login.username.placeholder": "郵箱地址或帳户名稱", + "navigation.chats": "聊天", + "navigation.compose": "發佈新帖文", + "navigation.dashboard": "控制台", + "navigation.developers": "開發者", + "navigation.direct_messages": "私人訊息", + "navigation.home": "首頁", + "navigation.invites": "邀請", + "navigation.notifications": "通知", + "navigation.search": "搜尋", + "navigation_bar.account_aliases": "帳户別名", + "navigation_bar.account_migration": "帳户遷移", + "navigation_bar.blocks": "封鎖使用者", + "navigation_bar.compose": "撰寫新帖文", + "navigation_bar.compose_direct": "發送私人訊息", + "navigation_bar.compose_edit": "編輯帖文", + "navigation_bar.compose_quote": "引用帖文", + "navigation_bar.compose_reply": "回覆帖文", + "navigation_bar.domain_blocks": "隱藏的網域", + "navigation_bar.favourites": "收藏", + "navigation_bar.filters": "隱藏", + "navigation_bar.follow_requests": "追蹤請求", + "navigation_bar.import_data": "匯入資料", + "navigation_bar.in_reply_to": "回覆", + "navigation_bar.invites": "邀請", "navigation_bar.logout": "登出", - "navigation_bar.mutes": "被你靜音的用戶", + "navigation_bar.mutes": "隱藏的使用者", "navigation_bar.preferences": "偏好設定", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.security": "安全", - "navigation_bar.soapbox_config": "Soapbox config", - "notification.birthday": "{name} has a birthday today", - "notification.birthday.more": "{count} more {count, plural, one {friend} other {friends}}", - "notification.birthday_plural": "{name} and {more} have birthday today", - "notification.pleroma:chat_mention": "{name} sent you a message", - "notification.favourite": "{name} 收藏了你的文章", - "notification.follow": "{name} 開始關注你", - "notification.follow_request": "{name} has requested to follow you", - "notification.mention": "{name} 提及你", - "notification.move": "{name} moved to {targetName}", - "notification.pleroma:emoji_reaction": "{name} reacted to your post", - "notification.poll": "A poll you have voted in has ended", - "notification.reblog": "{name} 轉推你的文章", - "notification.status": "{name} just posted", - "notifications.clear": "清空通知紀錄", - "notifications.clear_confirmation": "你確定要清空通知紀錄嗎?", - "notifications.clear_heading": "Clear notifications", - "notifications.column_settings.alert": "顯示桌面通知", - "notifications.column_settings.birthdays.category": "Birthdays", - "notifications.column_settings.birthdays.show": "Show birthday reminders", - "notifications.column_settings.emoji_react": "Emoji reacts:", - "notifications.column_settings.favourite": "收藏了你的文章:", - "notifications.column_settings.filter_bar.advanced": "Display all categories", - "notifications.column_settings.filter_bar.category": "Quick filter bar", - "notifications.column_settings.filter_bar.show": "Show", - "notifications.column_settings.follow": "關注你:", - "notifications.column_settings.follow_request": "New follow requests:", - "notifications.column_settings.mention": "提及你:", - "notifications.column_settings.move": "Moves:", - "notifications.column_settings.poll": "Poll results:", - "notifications.column_settings.push": "推送通知", - "notifications.column_settings.reblog": "轉推你的文章:", - "notifications.column_settings.show": "在通知欄顯示", - "notifications.column_settings.sound": "播放音效", - "notifications.column_settings.sounds": "Sounds", - "notifications.column_settings.sounds.all_sounds": "Play sound for all notifications", - "notifications.column_settings.title": "Notification settings", - "notifications.filter.all": "All", - "notifications.filter.boosts": "Reposts", - "notifications.filter.emoji_reacts": "Emoji reacts", - "notifications.filter.favourites": "Favorites", - "notifications.filter.follows": "Follows", - "notifications.filter.mentions": "Mentions", - "notifications.filter.moves": "Moves", - "notifications.filter.polls": "Poll results", - "notifications.filter.statuses": "Updates from people you follow", + "navigation_bar.profile_directory": "發現更多帳户", + "navigation_bar.soapbox_config": "Soapbox配置", + "notification.favourite": "{name} 讚了你的帖文", + "notification.follow": "{name} 追蹤了你", + "notification.follow_request": "{name} 請求追蹤你", + "notification.mention": "{name} 提到了你", + "notification.mentioned": "{name} 提到了你", + "notification.move": "{name} 移動到了 {targetName}", + "notification.others": " + {count} 其他通知", + "notification.pleroma:chat_mention": "{name} 給你發送了訊息", + "notification.pleroma:emoji_reaction": "{name} 用表情回應了你的帖文", + "notification.poll": "你參與的一項投票已經結束", + "notification.reblog": "{name} 轉帖了你的帖文", + "notification.status": "{name} 剛剛發帖", + "notification.update": "{name} 編輯了你參與互動的帖文", + "notification.user_approved": "歡迎來到 {instance}!", + "notifications.filter.all": "全部", + "notifications.filter.boosts": "轉帖", + "notifications.filter.emoji_reacts": "Emoji心情回應", + "notifications.filter.favourites": "最愛", + "notifications.filter.follows": "追蹤的使用者", + "notifications.filter.mentions": "提及", + "notifications.filter.moves": "移動", + "notifications.filter.polls": "投票結果", + "notifications.filter.statuses": "來自追蹤的人的更新", "notifications.group": "{count} 條通知", - "notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}", - "onboarding.avatar.subtitle": "Just have fun with it.", - "onboarding.avatar.title": "Choose a profile picture", - "onboarding.display_name.subtitle": "You can always edit this later.", - "onboarding.display_name.title": "Choose a display name", - "onboarding.done": "Done", - "onboarding.finished.message": "We are very excited to welcome you to our community! Tap the button below to get started.", - "onboarding.finished.title": "Onboarding complete", - "onboarding.header.subtitle": "This will be shown at the top of your profile.", - "onboarding.header.title": "Pick a cover image", - "onboarding.next": "Next", - "onboarding.note.subtitle": "You can always edit this later.", - "onboarding.note.title": "Write a short bio", - "onboarding.saving": "Saving…", - "onboarding.skip": "Skip for now", - "onboarding.suggestions.subtitle": "Here are a few of the most popular accounts you might like.", - "onboarding.suggestions.title": "Suggested accounts", - "onboarding.view_feed": "View Feed", - "password_reset.confirmation": "Check your email for confirmation.", - "password_reset.fields.username_placeholder": "Email or username", - "password_reset.reset": "Reset password", - "patron.donate": "Donate", - "patron.title": "Funding Goal", - "pinned_accounts.title": "{name}’s choices", - "pinned_statuses.none": "No pins to show.", - "poll.closed": "Closed", - "poll.refresh": "Refresh", - "poll.total_votes": "{count, plural, one {# vote} other {# votes}}", - "poll.vote": "Vote", - "poll.voted": "You voted for this answer", - "poll.votes": "{votes, plural, one {# vote} other {# votes}}", - "poll_button.add_poll": "Add a poll", - "poll_button.remove_poll": "Remove poll", - "pre_header.close": "Close", - "preferences.fields.auto_play_gif_label": "Auto-play animated GIFs", - "preferences.fields.autoload_more_label": "Automatically load more items when scrolled to the bottom of the page", - "preferences.fields.autoload_timelines_label": "Automatically load new posts when scrolled to the top of the page", - "preferences.fields.boost_modal_label": "Show confirmation dialog before reposting", - "preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post", - "preferences.fields.display_media.default": "Hide media marked as sensitive", - "preferences.fields.display_media.hide_all": "Always hide media", - "preferences.fields.display_media.show_all": "Always show media", - "preferences.fields.expand_spoilers_label": "Always expand posts marked with content warnings", - "preferences.fields.language_label": "Language", - "preferences.fields.media_display_label": "Media display", - "preferences.hints.feed": "In your home feed", - "privacy.change": "調整私隱設定", - "privacy.direct.long": "只有提及的用戶能看到", - "privacy.direct.short": "私人訊息", - "privacy.private.long": "只有關注你用戶能看到", - "privacy.private.short": "關注者", - "privacy.public.long": "在公共時間軸顯示", - "privacy.public.short": "公共", - "privacy.unlisted.long": "公開,但不在公共時間軸顯示", - "privacy.unlisted.short": "公開", - "profile_dropdown.add_account": "Add an existing account", - "profile_dropdown.logout": "Log out @{acct}", - "profile_fields_panel.title": "Profile fields", - "public.column_settings.title": "Fediverse timeline settings", - "reactions.all": "All", - "regeneration_indicator.label": "載入中……", + "notifications.queue_label": "點擊查看 {count} 條新通知", + "oauth_consumer.tooltip": "通過 {provider} 登入", + "oauth_consumers.title": "更多登入方式", + "onboarding.avatar.subtitle": "祝你玩得開心!", + "onboarding.avatar.title": "設定你的頭像", + "onboarding.display_name.subtitle": "你可以稍後更改", + "onboarding.display_name.title": "設定你的顯示名稱", + "onboarding.done": "完成", + "onboarding.finished.message": "我們很高興歡迎您加入我們的社區! 點擊下面的按鈕,讓我們開始吧!", + "onboarding.finished.title": "新手教程完成", + "onboarding.header.subtitle": "這將顯示在你個人資料的上方。", + "onboarding.header.title": "選擇封面圖片", + "onboarding.next": "下一步", + "onboarding.note.subtitle": "你可以稍後更改", + "onboarding.note.title": "簡短地介紹下自己", + "onboarding.saving": "儲存中…", + "onboarding.skip": "現在跳過", + "onboarding.suggestions.subtitle": "以下是幾個受歡迎的帳户,你可能會喜歡", + "onboarding.suggestions.title": "推薦帳户", + "onboarding.view_feed": "檢視列表", + "password_reset.confirmation": "請查閲確認電郵", + "password_reset.fields.username_placeholder": "電郵地址或帳户名稱", + "password_reset.reset": "重設密碼", + "patron.donate": "捐贈", + "patron.title": "籌款目標", + "pinned_accounts.title": "{name} 的釘選", + "pinned_statuses.none": "沒有釘選的帖文", + "poll.choose_multiple": "盡情選擇你所感興趣的", + "poll.closed": "已關閉", + "poll.non_anonymous": "公共投票", + "poll.non_anonymous.label": "其他的實例可能可以見到你投票的選擇", + "poll.refresh": "重新整理", + "poll.total_people": "還有 {count} 人", + "poll.total_votes": "投票", + "poll.vote": "投票", + "poll.voted": "你投票給了這個選項", + "poll.votes": "投票", + "poll_button.add_poll": "發起投票", + "poll_button.remove_poll": "移除投票", + "preferences.fields.auto_play_gif_label": "自動播放GIF", + "preferences.fields.autoload_more_label": "滾動至時間軸底部自動載入更多帖文", + "preferences.fields.autoload_timelines_label": "滾動至時間軸頂部自動載入更多新帖", + "preferences.fields.boost_modal_label": "轉帖前顯示確認提示", + "preferences.fields.delete_modal_label": "刪除帖文前顯示確認提示", + "preferences.fields.display_media.default": "隱藏被標記為敏感內容的媒體", + "preferences.fields.display_media.hide_all": "始終隱藏所有媒體", + "preferences.fields.display_media.show_all": "始終顯示所有媒體", + "preferences.fields.expand_spoilers_label": "始終展開標有內容警告的帖文", + "preferences.fields.language_label": "語言", + "preferences.fields.media_display_label": "媒體顯示", + "preferences.fields.theme": "主題", + "preferences.hints.feed": "在你的主頁信息流中", + "privacy.change": "調整隱私狀態", + "privacy.direct.long": "只有被提到的使用者能看到", + "privacy.direct.short": "僅被提及的使用者", + "privacy.private.long": "只有追蹤你的使用者能看到", + "privacy.private.short": "僅追蹤者", + "privacy.public.long": "所有人可見,出現在公共時間軸上", + "privacy.public.short": "公共時間軸", + "privacy.unlisted.long": "公開,但不會顯示在公共時間軸", + "privacy.unlisted.short": "所有人", + "profile_dropdown.add_account": "新增已有帳户", + "profile_dropdown.logout": "登出 @{acct}", + "profile_dropdown.theme": "主題", + "profile_fields_panel.title": "個人資料字段", + "public.column_settings.title": "聯邦宇宙工具時間軸設定", + "reactions.all": "全部", + "regeneration_indicator.label": "載入中…", "regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!", - "register_invite.lead": "Complete the form below to create an account.", - "register_invite.title": "You've been invited to join {siteTitle}!", - "registration.agreement": "I agree to the {tos}.", - "registration.captcha.hint": "Click the image to get a new captcha", - "registration.closed_message": "{instance} is not accepting new members", - "registration.closed_title": "Registrations Closed", - "registration.confirmation_modal.close": "Close", - "registration.fields.confirm_placeholder": "Password (again)", - "registration.fields.email_placeholder": "E-Mail address", - "registration.fields.password_placeholder": "Password", - "registration.fields.username_hint": "Only letters, numbers, and underscores are allowed.", - "registration.fields.username_placeholder": "Username", - "registration.newsletter": "Subscribe to newsletter.", - "registration.password_mismatch": "Passwords don't match.", - "registration.reason": "Why do you want to join?", - "registration.reason_hint": "This will help us review your application", - "registration.sign_up": "Sign up", - "registration.tos": "Terms of Service", - "registration.privacy": "Privacy Policy", - "registration.acceptance": "By registering, you agree to the {terms} and {privacy}.", - "registration.username_unavailable": "Username is already taken.", - "relative_time.days": "{number}日", + "register_invite.lead": "填寫下列表單以註冊帳户", + "register_invite.title": "你已被邀請加入 {siteTitle}!", + "registration.acceptance": "註冊同時意味着你已經同意本站的 {terms} 和 {privacy}.", + "registration.agreement": "我同意本站用户條款 {tos}.", + "registration.captcha.hint": "點擊圖像以重載驗證碼", + "registration.captcha.placeholder": "輸入照片中顯示的文字", + "registration.closed_message": "{instance} 公開註冊暫時關閉", + "registration.closed_title": "暫停註冊", + "registration.confirmation_modal.close": "關閉", + "registration.fields.confirm_placeholder": "再次輸入密碼", + "registration.fields.email_placeholder": "電郵地址", + "registration.fields.password_placeholder": "密碼", + "registration.fields.username_hint": "只能使用英文字母、數字和下劃線", + "registration.fields.username_placeholder": "帳户名稱", + "registration.header": "創建你的帳户", + "registration.newsletter": "訂閲我們的新聞郵件", + "registration.password_mismatch": "密碼不匹配", + "registration.privacy": "隱私條款", + "registration.reason": "你為什麼想要註冊本站?", + "registration.reason_hint": "認真填寫將能加快你通過註冊的速度", + "registration.sign_up": "註冊", + "registration.tos": "用户條款", + "registration.username_unavailable": "帳户名稱已被使用", + "registration.validation.capital_letter": "1 個大寫字母", + "registration.validation.lowercase_letter": "1 個小寫字母", + "registration.validation.minimum_characters": "8 個字符", + "registrations.create_account": "創建新帳户", + "registrations.error": "創建你的帳户失敗,請聯絡管理員.", + "registrations.get_started": "讓我們開始吧!", + "registrations.success": "歡迎來到 {siteTitle}!", + "registrations.tagline": "一個沒有言論審查的社交平台", + "registrations.unprocessable_entity": "此用户名已被佔用", + "registrations.username.hint": "只能包含數字、字母和下劃線", + "relative_time.days": "{number}天", "relative_time.hours": "{number}小時", "relative_time.just_now": "剛剛", - "relative_time.minutes": "{number}分鐘", + "relative_time.minutes": "{number}分", "relative_time.seconds": "{number}秒", - "remote_instance.edit_federation": "Edit federation", - "remote_instance.federation_panel.heading": "Federation Restrictions", - "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} has placed no restrictions on {host}.", - "remote_instance.federation_panel.restricted_message": "{siteTitle} blocks all activities from {host}.", - "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} has placed some restrictions on {host}.", - "remote_instance.pin_host": "Pin {host}", - "remote_instance.unpin_host": "Unpin {host}", - "remote_interaction.account_placeholder": "Enter your username@domain you want to act from", - "remote_interaction.divider": "or", - "remote_interaction.favourite": "Proceed to like", - "remote_interaction.favourite_title": "Like a post remotely", - "remote_interaction.follow": "Proceed to follow", - "remote_interaction.follow_title": "Follow {user} remotely", - "remote_interaction.poll_vote": "Proceed to vote", - "remote_interaction.poll_vote_title": "Vote in a poll remotely", - "remote_interaction.reblog": "Proceed to repost", - "remote_interaction.reblog_title": "Reblog a post remotely", - "remote_interaction.reply": "Proceed to reply", - "remote_interaction.reply_title": "Reply to a post remotely", - "remote_interaction.user_not_found_error": "Couldn't find given user", - "remote_timeline.filter_message": "You are viewing the timeline of {instance}.", + "remote_instance.edit_federation": "編輯聯邦設定", + "remote_instance.federation_panel.heading": "聯邦限制", + "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} 未對 {host} 採取限制措施", + "remote_instance.federation_panel.restricted_message": "{siteTitle} 完全封鎖了 {host}", + "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} 對 {host} 實施了部分限制", + "remote_instance.pin_host": "釘選 {host}", + "remote_instance.unpin_host": "取消釘選 {host}", + "remote_interaction.account_placeholder": "輸入您想採取行動的帳户 (格式:帳户名@網域)", + "remote_interaction.divider": "或", + "remote_interaction.favourite": "按讚", + "remote_interaction.favourite_title": "遠端按讚一條帖文", + "remote_interaction.follow": "開始追蹤", + "remote_interaction.follow_title": "遠端追蹤 {user}", + "remote_interaction.poll_vote": "投票", + "remote_interaction.poll_vote_title": "遠端參與投票", + "remote_interaction.reblog": "轉帖", + "remote_interaction.reblog_title": "遠端轉帖", + "remote_interaction.reply": "回覆", + "remote_interaction.reply_title": "遠端回覆", + "remote_interaction.user_not_found_error": "找不到該帳户", + "remote_timeline.filter_message": "你正在查看 {instance} 的時間軸", "reply_indicator.cancel": "取消", - "reply_mentions.account.add": "Add to mentions", - "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.reply_empty": "Replying to post", - "report.block": "Block {target}", - "report.block_hint": "Do you also want to block this account?", + "reply_mentions.account.add": "添加到提及名單", + "reply_mentions.account.remove": "從提及名單中移除", + "reply_mentions.more": "添加 {count} 個", + "reply_mentions.reply": "回覆 {accounts}{more}", + "reply_mentions.reply_empty": "回覆帖文", + "report.block": "封鎖帳户 {target}", + "report.block_hint": "你是否要封鎖這個帳户呢?", + "report.confirmation.content": "如果我們發現此帳户確實違反了 {link} ,我們會採取進一步的措施", + "report.confirmation.title": "感謝你提交的檢舉", + "report.done": "完成", "report.forward": "轉寄到 {target}", - "report.forward_hint": "這個帳戶屬於其他服務站。要向該服務站發送匿名的舉報訊息嗎?", - "report.hint": "這訊息會發送到你服務站的管理員。你可以提供舉報這個帳戶的理由:", - "report.placeholder": "額外訊息", - "report.submit": "提交", - "report.target": "舉報", - "reset_password.header": "Set New Password", - "schedule.post_time": "Post Date/Time", - "schedule.remove": "Remove schedule", - "schedule_button.add_schedule": "Schedule post for later", - "schedule_button.remove_schedule": "Post immediately", - "scheduled_status.cancel": "Cancel", - "search.action": "Search for “{query}”", + "report.forward_hint": "這個帳户屬於其他站點。要像該站點發送匿名的檢舉訊息嗎?", + "report.hint": "這項訊息會發送到您伺服器的管理員。你可以提供檢舉這個帳户的理由:", + "report.next": "下一步", + "report.otherActions.addAdditional": "你還想為你的檢舉添加更多狀態嗎?", + "report.otherActions.addMore": "添加更多", + "report.otherActions.furtherActions": "進一步措施:", + "report.otherActions.hideAdditional": "隱藏額外狀態", + "report.otherActions.otherStatuses": "包含其他狀態?", + "report.placeholder": "更多訊息", + "report.reason.blankslate": "你已移除選中的所有狀態", + "report.reason.title": "檢舉原因", + "report.submit": "送出", + "report.target": "檢舉 {target}", + "reset_password.fail": "令牌已過期,請重試", + "reset_password.header": "設定新的密碼", + "schedule.post_time": "發佈時間", + "schedule.remove": "取消發佈", + "schedule_button.add_schedule": "定時發佈", + "schedule_button.remove_schedule": "取消定時發佈", + "scheduled_status.cancel": "取消", + "search.action": "搜尋 “{query}”", "search.placeholder": "搜尋", "search_results.accounts": "使用者", - "search_results.hashtags": "標籤", - "search_results.statuses": "文章", - "search_results.top": "Top", - "security.codes.fail": "Failed to fetch backup codes", - "security.confirm.fail": "Incorrect code or password. Try again.", - "security.delete_account.fail": "Account deletion failed.", - "security.delete_account.success": "Account successfully deleted.", - "security.disable.fail": "Incorrect password. Try again.", - "security.disable_mfa": "Disable", - "security.fields.email.label": "Email address", - "security.fields.new_password.label": "New password", - "security.fields.old_password.label": "Current password", - "security.fields.password.label": "Password", - "security.fields.password_confirmation.label": "New password (again)", - "security.headers.delete": "Delete Account", - "security.headers.tokens": "Sessions", - "security.headers.update_email": "Change Email", - "security.headers.update_password": "Change Password", - "security.mfa": "Set up 2-Factor Auth", - "security.mfa_enabled": "You have multi-factor authentication set up with OTP.", - "security.mfa_header": "Authorization Methods", - "security.mfa_setup_hint": "Configure multi-factor authentication with OTP", - "security.qr.fail": "Failed to fetch setup key", - "security.submit": "Save changes", - "security.submit.delete": "Delete Account", - "security.text.delete": "To delete your account, enter your password then click Delete Account. This is a permanent action that cannot be undone. Your account will be destroyed from this server, and a deletion request will be sent to other servers. It's not guaranteed that all servers will purge your account.", - "security.tokens.revoke": "Revoke", - "security.update_email.fail": "Update email failed.", - "security.update_email.success": "Email successfully updated.", - "security.update_password.fail": "Update password failed.", - "security.update_password.success": "Password successfully updated.", - "settings.change_email": "Change Email", - "settings.change_password": "Change Password", - "settings.configure_mfa": "Configure MFA", - "settings.delete_account": "Delete Account", - "settings.edit_profile": "Edit Profile", - "settings.preferences": "Preferences", - "settings.profile": "Profile", - "settings.save.success": "Your preferences have been saved!", - "settings.security": "Security", - "settings.settings": "Settings", - "signup_panel.subtitle": "Sign up now to discuss what's happening.", - "signup_panel.title": "New to {site_title}?", - "snackbar.view": "View", - "soapbox_config.authenticated_profile_hint": "Users must be logged-in to view replies and media on user profiles.", - "soapbox_config.authenticated_profile_label": "Profiles require authentication", - "soapbox_config.copyright_footer.meta_fields.label_placeholder": "Copyright footer", - "soapbox_config.crypto_address.meta_fields.address_placeholder": "Address", - "soapbox_config.crypto_address.meta_fields.note_placeholder": "Note (optional)", - "soapbox_config.crypto_address.meta_fields.ticker_placeholder": "Ticker", - "soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder": "Number of items to display in the crypto homepage widget", + "search_results.hashtags": "主題標籤", + "search_results.statuses": "帖文", + "security.codes.fail": "恢復代碼錯誤", + "security.confirm.fail": "密碼錯誤,請重試。", + "security.delete_account.fail": "帳户刪除失敗", + "security.delete_account.success": "帳户刪除成功", + "security.disable.fail": "密碼錯誤,請重試。", + "security.fields.email.label": "電郵地址", + "security.fields.new_password.label": "輸入新密碼", + "security.fields.old_password.label": "輸入原密碼", + "security.fields.password.label": "密碼", + "security.fields.password_confirmation.label": "再次輸入新密碼", + "security.headers.delete": "刪除帳户", + "security.headers.tokens": "會話", + "security.qr.fail": "加載密鑰失敗", + "security.submit": "儲存變更", + "security.submit.delete": "刪除帳户", + "security.text.delete": "要刪除您的帳户,請輸入您的密碼。注意:這是無法撤消的永久性操作!您的帳户將從該服務器中銷毀,並將向其他服務器發送刪除請求。但你的資訊不一定會在其他站點上立即刪除。", + "security.tokens.revoke": "撤銷", + "security.update_email.fail": "更新電郵地址失敗", + "security.update_email.success": "電郵地址已更新", + "security.update_password.fail": "更新密碼失敗", + "security.update_password.success": "密碼已更新", + "settings.change_email": "更改電郵地址", + "settings.change_password": "更改密碼", + "settings.configure_mfa": "設定多重要素驗證 (MFA)", + "settings.delete_account": "刪除帳户", + "settings.edit_profile": "編輯個人資料", + "settings.preferences": "首選項", + "settings.profile": "個人資料", + "settings.save.success": "你的設定已儲存", + "settings.security": "安全性", + "settings.sessions": "活動會話", + "settings.settings": "設定", + "shared.tos": "服務條款", + "signup_panel.subtitle": "註冊以參與討論", + "signup_panel.title": "初來乍到 {site_title} 嗎?", + "site_preview.preview": "預覽", + "snackbar.view": "檢視", + "soapbox_config.authenticated_profile_hint": "用户必須登錄才能查看用户個人資料上的回覆和媒體。", + "soapbox_config.authenticated_profile_label": "個人資料需要授權才能查看", + "soapbox_config.copyright_footer.meta_fields.label_placeholder": "版權頁底", + "soapbox_config.crypto_address.meta_fields.address_placeholder": "地址", + "soapbox_config.crypto_address.meta_fields.note_placeholder": "備註 (可選)", + "soapbox_config.crypto_address.meta_fields.ticker_placeholder": "幣種", + "soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder": "在主頁數字貨幣小部件中顯示的數量", "soapbox_config.custom_css.meta_fields.url_placeholder": "URL", - "soapbox_config.display_fqn_label": "Display domain (eg @user@domain) for local accounts.", - "soapbox_config.fields.accent_color_label": "Accent color", - "soapbox_config.fields.brand_color_label": "Brand color", - "soapbox_config.fields.crypto_address.add": "Add new crypto address", - "soapbox_config.fields.crypto_addresses_label": "Cryptocurrency addresses", - "soapbox_config.fields.home_footer.add": "Add new Home Footer Item", - "soapbox_config.fields.home_footer_fields_label": "Home footer items", + "soapbox_config.display_fqn_label": "顯示本站帳户的網域 (如 @帳户名稱@網域) ", + "soapbox_config.fields.accent_color_label": "強調色", + "soapbox_config.fields.brand_color_label": "主題色", + "soapbox_config.fields.crypto_address.add": "添加數字貨幣地址", + "soapbox_config.fields.crypto_addresses_label": "數字貨幣地址", + "soapbox_config.fields.home_footer.add": "添加頁尾", + "soapbox_config.fields.home_footer_fields_label": "主頁頁眉", "soapbox_config.fields.logo_label": "Logo", - "soapbox_config.fields.promo_panel.add": "Add new Promo panel item", - "soapbox_config.fields.promo_panel_fields_label": "Promo panel items", - "soapbox_config.fields.theme_label": "Default theme", - "soapbox_config.greentext_label": "Enable greentext support", - "soapbox_config.hints.crypto_addresses": "Add cryptocurrency addresses so users of your site can donate to you. Order matters, and you must use lowercase ticker values.", - "soapbox_config.hints.home_footer_fields": "You can have custom defined links displayed on the footer of your static pages", - "soapbox_config.hints.logo": "SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio", - "soapbox_config.hints.promo_panel_fields": "You can have custom defined links displayed on the right panel of the timelines page.", + "soapbox_config.fields.promo_panel.add": "添加時間軸頁底", + "soapbox_config.fields.promo_panel_fields_label": "時間軸頁底", + "soapbox_config.fields.theme_label": "默認主題", + "soapbox_config.greentext_label": "啟用greentext支援", + "soapbox_config.headings.advanced": "進階", + "soapbox_config.headings.cryptocurrency": "數字貨幣", + "soapbox_config.headings.navigation": "導航列", + "soapbox_config.headings.options": "選項", + "soapbox_config.headings.theme": "主題", + "soapbox_config.hints.crypto_addresses": "添加加密貨幣地址,以便您網站的用户可以向您捐款。請注意順序,同時您必須使用小寫的幣種代碼。", + "soapbox_config.hints.home_footer_fields": "您可以在靜態頁面的頁腳顯示自定義鏈接(如about)。", + "soapbox_config.hints.logo": "SVG. 最多 2 MB。 將顯示到 50px 高度,保持縱橫比", + "soapbox_config.hints.promo_panel_fields": "您可以在時間線頁面的右側面板上顯示自定義鏈接。", "soapbox_config.hints.promo_panel_icons": "{ link }", - "soapbox_config.hints.promo_panel_icons.link": "Soapbox Icons List", - "soapbox_config.home_footer.meta_fields.label_placeholder": "Label", + "soapbox_config.hints.promo_panel_icons.link": "Soapbox圖示", + "soapbox_config.home_footer.meta_fields.label_placeholder": "標籤", "soapbox_config.home_footer.meta_fields.url_placeholder": "URL", - "soapbox_config.promo_panel.meta_fields.icon_placeholder": "Icon", - "soapbox_config.promo_panel.meta_fields.label_placeholder": "Label", + "soapbox_config.promo_panel.meta_fields.icon_placeholder": "圖示", + "soapbox_config.promo_panel.meta_fields.label_placeholder": "標籤", "soapbox_config.promo_panel.meta_fields.url_placeholder": "URL", - "soapbox_config.raw_json_hint": "Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click Save to apply your changes.", - "soapbox_config.raw_json_label": "Advanced: Edit raw JSON data", - "soapbox_config.save": "Save", - "soapbox_config.saved": "Soapbox config saved!", - "soapbox_config.single_user_mode_hint": "Front page will redirect to a given user profile.", - "soapbox_config.single_user_mode_label": "Single user mode", - "soapbox_config.single_user_mode_profile_hint": "@handle", - "soapbox_config.single_user_mode_profile_label": "Main user handle", - "soapbox_config.verified_can_edit_name_label": "Allow verified users to edit their own display name.", - "status.actions.more": "More", - "status.admin_account": "Open moderation interface for @{name}", - "status.admin_status": "Open this post in the moderation interface", + "soapbox_config.raw_json_hint": "直接編輯設置數據。 直接對 JSON 文件進行的更改將覆蓋上面的表單字段。 單擊保存以應用您的更改。", + "soapbox_config.raw_json_label": "高級: 編輯原始JSON", + "soapbox_config.save": "儲存更改", + "soapbox_config.saved": "Soapbox配置已儲存!", + "soapbox_config.single_user_mode_hint": "首頁將重定向到給定的用户配置文件。", + "soapbox_config.single_user_mode_label": "單用户模式", + "soapbox_config.single_user_mode_profile_hint": "@帳户名稱", + "soapbox_config.single_user_mode_profile_label": "主帳户的帳户名稱", + "soapbox_config.verified_can_edit_name_label": "允許經過驗證的用户編輯自己的顯示名稱。", + "sponsored.info.message": "{siteTitle} 展示廣告以使實例可持續運行", + "sponsored.info.title": "為什麼我會看到這條廣告?", + "sponsored.subtitle": "贊助", + "status.actions.more": "更多", + "status.admin_account": "開啟 @{name} 的管理介面", + "status.admin_status": "在管理介面開啟此帖文", "status.block": "封鎖 @{name}", - "status.bookmark": "Bookmark", - "status.bookmarked": "Bookmark added.", - "status.cancel_reblog_private": "取消轉推", - "status.cannot_reblog": "這篇文章無法被轉推", - "status.chat": "Chat with @{name}", - "status.copy": "Copy link to post", + "status.bookmark": "書籤", + "status.bookmarked": "書籤已添加", + "status.cancel_reblog_private": "取消轉帖", + "status.cannot_reblog": "這篇帖文無法被轉載", + "status.chat": "和 @{name} 聊天", + "status.copy": "將連結複製到帖文中", "status.delete": "刪除", - "status.detailed_status": "Detailed conversation view", - "status.direct": "私訊 @{name}", - "status.embed": "鑲嵌", - "status.favourite": "收藏", - "status.filtered": "Filtered", + "status.detailed_status": "對話的詳細內容", + "status.direct": "發送私訊給 @{name}", + "status.edit": "編輯", + "status.edited": "已於 {date} 編輯", + "status.embed": "嵌入", + "status.favourite": "最愛", + "status.filtered": "已過濾", "status.load_more": "載入更多", "status.media_hidden": "隱藏媒體內容", - "status.mention": "提及 @{name}", + "status.mention": "提到 @{name}", "status.more": "更多", - "status.mute": "把 @{name} 靜音", - "status.mute_conversation": "靜音對話", - "status.open": "展開文章", - "status.pin": "置頂到資料頁", - "status.pinned": "置頂文章", - "status.quote": "Quote post", - "status.reactions.cry": "Sad", - "status.reactions.empty": "No one has reacted to this post yet. When someone does, they will show up here.", - "status.reactions.heart": "Love", - "status.reactions.laughing": "Haha", - "status.reactions.like": "Like", - "status.reactions.open_mouth": "Wow", - "status.reactions.weary": "Weary", - "status.reactions_expand": "Select emoji", - "status.read_more": "Read more", - "status.reblog": "轉推", - "status.reblog_private": "轉推到原讀者", - "status.reblogged_by": "{name} 轉推", - "status.reblogs.empty": "No one has reposted this post yet. When someone does, they will show up here.", - "status.redraft": "刪除並編輯", - "status.remove_account_from_group": "Remove account from group", - "status.remove_post_from_group": "Remove post from group", - "status.reply": "回應", - "status.replyAll": "回應所有人", - "status.report": "舉報 @{name}", + "status.mute": "隱藏 @{name}", + "status.mute_conversation": "隱藏對話", + "status.open": "展開帖文", + "status.pin": "釘選到個人資料頁", + "status.pinned": "釘選的帖文", + "status.quote": "引用帖文", + "status.reactions.cry": "傷心", + "status.reactions.empty": "尚未有人回應心情", + "status.reactions.heart": "愛", + "status.reactions.laughing": "哈哈哈", + "status.reactions.like": "讚哦", + "status.reactions.open_mouth": "哇!", + "status.reactions.weary": "疲憊", + "status.reactions_expand": "選擇心情", + "status.read_more": "閱讀更多", + "status.reblog": "轉帖", + "status.reblog_private": "轉帖給原有追蹤者", + "status.reblogged_by": "{name} 轉帖了", + "status.reblogs.empty": "還沒有人轉帖。如果有,會顯示在這裡。", + "status.redraft": "刪除 & 編輯", + "status.remove_account_from_group": "將帳户移出群組", + "status.remove_post_from_group": "將帖文移出群組", + "status.reply": "回覆", + "status.replyAll": "回覆所有人", + "status.report": "檢舉 @{name}", "status.sensitive_warning": "敏感內容", "status.share": "分享", "status.show_less": "減少顯示", - "status.show_less_all": "減少顯示這類文章", + "status.show_less_all": "減少顯示這類帖文", "status.show_more": "顯示更多", - "status.show_more_all": "顯示更多這類文章", - "status.title": "Post", - "status.title_direct": "Direct message", - "status.unbookmark": "Remove bookmark", - "status.unbookmarked": "Bookmark removed.", - "status.unmute_conversation": "解禁對話", - "status.unpin": "解除置頂", - "status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}", - "statuses.quote_tombstone": "Post is unavailable.", - "statuses.tombstone": "One or more posts are unavailable.", - "suggestions.dismiss": "Dismiss suggestion", - "tabs_bar.all": "All", - "tabs_bar.chats": "Chats", - "tabs_bar.dashboard": "Dashboard", - "tabs_bar.fediverse": "Fediverse", + "status.show_more_all": "顯示更多這類帖文", + "status.title": "帖文", + "status.title_direct": "私訊", + "status.unbookmark": "移除書籤", + "status.unbookmarked": "書籤已移除", + "status.unmute_conversation": "解除此對話的隱藏", + "status.unpin": "解除釘選", + "status.sensitive_warning.subtitle": "這則貼文可能含有不宜觀看的消息", + "status.sensitive_warning.action": "顯示", + "status_list.queue_label": "點選查看 {count} 個新帖文", + "statuses.quote_tombstone": "帖文不可用", + "statuses.tombstone": "部分帖文不可見", + "streamfield.add": "新增", + "streamfield.remove": "移除", + "suggestions.dismiss": "關閉建議", + "tabs_bar.all": "全部", + "tabs_bar.chats": "對話", + "tabs_bar.dashboard": "控制台", + "tabs_bar.fediverse": "聯邦宇宙", "tabs_bar.home": "主頁", - "tabs_bar.more": "More", + "tabs_bar.more": "更多", "tabs_bar.notifications": "通知", - "tabs_bar.post": "Post", - "tabs_bar.profile": "Profile", + "tabs_bar.profile": "個人資料", "tabs_bar.search": "搜尋", - "tabs_bar.settings": "Settings", - "tabs_bar.theme_toggle_dark": "Switch to dark theme", - "tabs_bar.theme_toggle_light": "Switch to light theme", - "time_remaining.days": "{number, plural, one {# day} other {# days}} left", - "time_remaining.hours": "{number, plural, one {# hour} other {# hours}} left", - "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left", - "time_remaining.moments": "Moments remaining", - "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left", - "trends.count_by_accounts": "{count} 位用戶在討論", - "trends.title": "Trends", - "ui.beforeunload": "如果你現在離開 Soapbox,你的草稿內容將會被丟棄。", - "unauthorized_modal.text": "You need to be logged in to do that.", - "unauthorized_modal.title": "Sign up for {site_title}", - "upload_area.title": "將檔案拖放至此上載", - "upload_button.label": "上載媒體檔案", - "upload_error.image_size_limit": "Image exceeds the current file size limit ({limit})", - "upload_error.limit": "File upload limit exceeded.", - "upload_error.poll": "File upload not allowed with polls.", - "upload_error.video_size_limit": "Video exceeds the current file size limit ({limit})", - "upload_form.description": "為視覺障礙人士添加文字說明", - "upload_form.preview": "Preview", + "tabs_bar.settings": "設定", + "tabs_bar.switch_accounts": "切換帳户", + "tabs_bar.theme_toggle_dark": "切換為深色主題", + "tabs_bar.theme_toggle_light": "切換為淺色主題", + "theme_toggle.dark": "深色", + "theme_toggle.light": "淺色", + "theme_toggle.system": "系統", + "thread_login.login": "登入", + "thread_login.message": "加入 {siteTitle} 以獲取完整資訊", + "thread_login.signup": "註冊", + "thread_login.title": "繼續這個對話", + "time_remaining.days": "剩餘{number, plural, one {# 天數} other {# 天數}}", + "time_remaining.hours": "剩餘{number, plural, one {# 小時} other {# 小時}}", + "time_remaining.minutes": "剩餘{number, plural, one {# 分鐘} other {# 分鐘}}", + "time_remaining.moments": "剩餘時間", + "time_remaining.seconds": "剩餘 {number, plural, one {# 秒} other {# 秒}}", + "trends.count_by_accounts": "{count} 位使用者在討論", + "trends.title": "趨勢", + "trendsPanel.viewAll": "顯示全部", + "ui.beforeunload": "如果離開,你的草稿將會丟失。", + "unauthorized_modal.text": "你需要登入才能繼續", + "unauthorized_modal.title": "註冊 {site_title} 帳户", + "upload_area.title": "拖放來上傳", + "upload_button.label": "上傳媒體檔案 (JPEG, PNG, GIF, WebM, MP4, MOV)", + "upload_error.image_size_limit": "圖片超出當前文件大小限制 ({limit})", + "upload_error.limit": "已達到檔案上傳限制。", + "upload_error.poll": "不允許在投票上傳檔案。", + "upload_error.video_duration_limit": "影片超出當前時長限制 ({limit} 秒)", + "upload_error.video_size_limit": "影片超出當前文件大小限制 ({limit})", + "upload_form.description": "為視障人士增加文字說明", + "upload_form.preview": "預覽", "upload_form.undo": "刪除", - "upload_progress.label": "上載中……", + "upload_progress.label": "上傳中...", "video.close": "關閉影片", - "video.download": "Download file", - "video.exit_fullscreen": "退出全熒幕", + "video.download": "下載", + "video.exit_fullscreen": "退出全螢幕", "video.expand": "展開影片", - "video.fullscreen": "全熒幕", + "video.fullscreen": "全螢幕", "video.hide": "隱藏影片", - "video.mute": "靜音", + "video.mute": "隱藏", "video.pause": "暫停", "video.play": "播放", - "video.unmute": "解除靜音", - "who_to_follow.title": "Who To Follow" + "video.unmute": "解除隱藏", + "who_to_follow.title": "推薦追蹤" } diff --git a/app/soapbox/locales/zh-TW.json b/app/soapbox/locales/zh-TW.json index 22c93958f..6ee6d49cd 100644 --- a/app/soapbox/locales/zh-TW.json +++ b/app/soapbox/locales/zh-TW.json @@ -1,161 +1,174 @@ { - "about.also_available": "Available in:", - "accordion.collapse": "Collapse", - "accordion.expand": "Expand", + "about.also_available": "其他版本:", + "accordion.collapse": "收起", + "accordion.expand": "展开", "account.add_or_remove_from_list": "從名單中新增或移除", "account.badges.bot": "機器人", - "account.birthday": "Born {date}", - "account.birthday_today": "Birthday is today!", + "account.birthday": "出生於 {date}", + "account.birthday_today": "今天是你的生日!", "account.block": "封鎖 @{name}", "account.block_domain": "隱藏來自 {domain} 的所有內容", "account.blocked": "已封鎖", - "account.chat": "Chat with @{name}", - "account.column_settings.description": "These settings apply to all account timelines.", - "account.column_settings.title": "Acccount timeline settings", - "account.deactivated": "Deactivated", + "account.chat": "和 @{name} 聊天", + "account.deactivated": "帳戶已被停權", "account.direct": "傳私訊給 @{name}", + "account.domain_blocked": "隱藏網域", "account.edit_profile": "編輯個人資料", "account.endorse": "在個人資料推薦對方", - "account.follow": "關注", - "account.followers": "關注者", - "account.followers.empty": "尚沒有人關注這位使用者。", - "account.follows": "正在關注", - "account.follows.empty": "這位使用者尚未關注任何使用者。", - "account.follows_you": "關注了你", + "account.endorse.success": "你正在你的個人資料中展示 @{acct} ", + "account.familiar_followers": "被 {accounts} 追蹤", + "account.familiar_followers.more": "你追蹤了 {count} 位其他用戶", + "account.follow": "追蹤", + "account.followers": "追蹤者", + "account.followers.empty": "尚沒有人追蹤這位使用者。", + "account.follows": "正在追蹤", + "account.follows.empty": "這位使用者尚未追蹤任何使用者。", + "account.follows_you": "追蹤了你", "account.hide_reblogs": "隱藏來自 @{name} 的轉推", - "account.last_status": "Last active", + "account.last_status": "最後活動", "account.link_verified_on": "已在 {date} 檢查此連結的擁有者權限", - "account.locked_info": "這隻帳戶的隱私狀態被設成鎖定。該擁有者會手動審核能關注這隻帳號的人。", - "account.login": "Log in", + "account.locked_info": "這隻帳戶的隱私狀態被設成鎖定。該擁有者會手動審核能追蹤這隻帳號的人。", + "account.login": "登入", "account.media": "媒體", - "account.member_since": "Joined {date}", + "account.member_since": "加入於 {date}", "account.mention": "提及", "account.moved_to": "{name} 已遷移至:", - "account.mute": "靜音 @{name}", - "account.never_active": "Never", - "account.posts": "嘟文", - "account.posts_with_replies": "嘟文與回覆", - "account.profile": "Profile", - "account.register": "Sign up", - "account.remote_follow": "Remote follow", + "account.mute": "隱藏 @{name}", + "account.muted": "已隱藏", + "account.never_active": "從未", + "account.posts": "帖文", + "account.posts_with_replies": "帖文與回覆", + "account.profile": "個人資料", + "account.register": "註冊", + "account.remote_follow": "遠端追蹤", + "account.remove_from_followers": "移除此追蹤者", "account.report": "檢舉 @{name}", - "account.requested": "正在等待核准。按一下取消關注請求", - "account.requested_small": "Awaiting approval", + "account.requested": "正在等待核准。按一下取消追蹤請求", + "account.requested_small": "等待核准", + "account.search": "搜尋關於 @{name} 的內容", + "account.search_self": "搜尋你的帖文", "account.share": "分享 @{name} 的個人資料", - "account.show_reblogs": "顯示來自 @{name} 的嘟文", - "account.subscribe": "Subscribe to notifications from @{name}", - "account.subscribed": "Subscribed", + "account.show_reblogs": "顯示來自 @{name} 的帖文", + "account.subscribe": "訂閲 @{name}", + "account.subscribe.failure": "訂閲此帳戶時發生錯誤", + "account.subscribe.success": "您已訂閲此帳戶", "account.unblock": "取消封鎖 @{name}", "account.unblock_domain": "取消隱藏 {domain}", "account.unendorse": "不再於個人資料頁面推薦對方", - "account.unfollow": "取消關注", - "account.unmute": "取消靜音 @{name}", - "account.unsubscribe": "Unsubscribe to notifications from @{name}", - "account.verified": "Verified Account", - "account.welcome": "Welcome", - "account_gallery.none": "No media to show.", - "account_note.hint": "You can keep notes about this user for yourself (this will not be shared with them):", - "account_note.placeholder": "No comment provided", - "account_note.save": "Save", - "account_note.target": "Note for @{target}", - "account_search.placeholder": "Search for an account", - "account_timeline.column_settings.show_pinned": "Show pinned posts", - "admin.awaiting_approval.approved_message": "{acct} was approved!", - "admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.", - "admin.awaiting_approval.rejected_message": "{acct} was rejected.", - "admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.", - "admin.dashboard.registration_mode.approval_label": "Approval Required", - "admin.dashboard.registration_mode.closed_hint": "Nobody can sign up. You can still invite people.", - "admin.dashboard.registration_mode.closed_label": "Closed", - "admin.dashboard.registration_mode.open_hint": "Anyone can join.", - "admin.dashboard.registration_mode.open_label": "Open", - "admin.dashboard.registration_mode_label": "Registrations", - "admin.dashboard.settings_saved": "Settings saved!", - "admin.dashcounters.domain_count_label": "peers", - "admin.dashcounters.mau_label": "monthly active users", - "admin.dashcounters.retention_label": "user retention", - "admin.dashcounters.status_count_label": "posts", - "admin.dashcounters.user_count_label": "total users", - "admin.dashwidgets.email_list_header": "Email list", - "admin.dashwidgets.software_header": "Software", - "admin.latest_accounts_panel.more": "Click to see {count} {count, plural, one {account} other {accounts}}", - "admin.latest_accounts_panel.title": "Latest Accounts", - "admin.moderation_log.empty_message": "You have not performed any moderation actions yet. When you do, a history will be shown here.", - "admin.reports.actions.close": "Close", - "admin.reports.actions.view_status": "View post", - "admin.reports.empty_message": "There are no open reports. If a user gets reported, they will show up here.", - "admin.reports.report_closed_message": "Report on @{name} was closed", - "admin.reports.report_title": "Report on {acct}", - "admin.statuses.actions.delete_status": "Delete post", - "admin.statuses.actions.mark_status_not_sensitive": "Mark post not sensitive", - "admin.statuses.actions.mark_status_sensitive": "Mark post sensitive", - "admin.statuses.status_deleted_message": "Post by @{acct} was deleted", - "admin.statuses.status_marked_message_not_sensitive": "Post by @{acct} was marked not sensitive", - "admin.statuses.status_marked_message_sensitive": "Post by @{acct} was marked sensitive", - "admin.user_index.empty": "No users found.", - "admin.user_index.search_input_placeholder": "Who are you looking for?", - "admin.users.actions.deactivate_user": "Deactivate @{name}", - "admin.users.actions.delete_user": "Delete @{name}", - "admin.users.actions.demote_to_moderator": "Demote @{name} to a moderator", - "admin.users.actions.demote_to_moderator_message": "@{acct} was demoted to a moderator", - "admin.users.actions.demote_to_user": "Demote @{name} to a regular user", - "admin.users.actions.demote_to_user_message": "@{acct} was demoted to a regular user", - "admin.users.actions.promote_to_admin": "Promote @{name} to an admin", - "admin.users.actions.promote_to_admin_message": "@{acct} was promoted to an admin", - "admin.users.actions.promote_to_moderator": "Promote @{name} to a moderator", - "admin.users.actions.promote_to_moderator_message": "@{acct} was promoted to a moderator", - "admin.users.actions.remove_donor": "Remove @{name} as a donor", - "admin.users.actions.set_donor": "Set @{name} as a donor", - "admin.users.actions.suggest_user": "Suggest @{name}", - "admin.users.actions.unsuggest_user": "Unsuggest @{name}", - "admin.users.actions.unverify_user": "Unverify @{name}", - "admin.users.actions.verify_user": "Verify @{name}", - "admin.users.remove_donor_message": "@{acct} was removed as a donor", - "admin.users.set_donor_message": "@{acct} was set as a donor", - "admin.users.user_deactivated_message": "@{acct} was deactivated", - "admin.users.user_deleted_message": "@{acct} was deleted", - "admin.users.user_suggested_message": "@{acct} was suggested", - "admin.users.user_unsuggested_message": "@{acct} was unsuggested", - "admin.users.user_unverified_message": "@{acct} was unverified", - "admin.users.user_verified_message": "@{acct} was verified", - "admin_nav.awaiting_approval": "Awaiting Approval", - "admin_nav.dashboard": "Dashboard", - "admin_nav.reports": "Reports", - "alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out).", - "alert.unexpected.browser": "Browser", - "alert.unexpected.clear_cookies": "clear cookies and browser data", - "alert.unexpected.links.help": "Help Center", - "alert.unexpected.links.status": "Status", - "alert.unexpected.links.support": "Support", + "account.unendorse.success": "您已不再展示 @{acct}", + "account.unfollow": "取消追蹤", + "account.unmute": "取消隱藏 @{name}", + "account.unsubscribe": "取消訂閲 @{name}", + "account.unsubscribe.failure": "取消訂閲此帳戶時發生錯誤", + "account.unsubscribe.success": "你已取消訂閲此帳戶", + "account.verified": "已認證的帳戶", + "account_gallery.none": "沒有可顯示的媒體", + "account_note.hint": "你可以為自己保留關於這個用戶的筆記(這不會和他們分享):", + "account_note.placeholder": "沒有評論", + "account_note.save": "儲存", + "account_note.target": "@{target} 的筆記", + "account_search.placeholder": "搜尋使用者", + "actualStatus.edited": "在 {date} 時編輯", + "actualStatuses.quote_tombstone": "帖文不可用", + "admin.awaiting_approval.approved_message": "{acct} 已經通過審核!", + "admin.awaiting_approval.empty_message": "沒有新使用者等待審核,如果有新的申請,它就會顯示在這裏。", + "admin.awaiting_approval.rejected_message": "{acct} 的註冊請求被拒絕!", + "admin.dashboard.registration_mode.approval_hint": "在管理員同意註冊申請後才可加入。", + "admin.dashboard.registration_mode.approval_label": "需要審核", + "admin.dashboard.registration_mode.closed_hint": "公開註冊已經關閉,但已註冊用戶仍然可以邀請其他人加入。", + "admin.dashboard.registration_mode.closed_label": "私密", + "admin.dashboard.registration_mode.open_hint": "任何人都可以加入。", + "admin.dashboard.registration_mode.open_label": "公開", + "admin.dashboard.registration_mode_label": "註冊模式", + "admin.dashboard.settings_saved": "設定已儲存!", + "admin.dashcounters.domain_count_label": "互聯站點", + "admin.dashcounters.mau_label": "月度活躍使用者", + "admin.dashcounters.retention_label": "使用者留存", + "admin.dashcounters.status_count_label": "總帖數", + "admin.dashcounters.user_count_label": "使用者總數", + "admin.dashwidgets.email_list_header": "電郵列表", + "admin.dashwidgets.software_header": "軟體版本", + "admin.latest_accounts_panel.expand_message": "點擊展開更多帳戶", + "admin.latest_accounts_panel.more": "點擊展開 {count} 個帳戶", + "admin.latest_accounts_panel.title": "最近加入的帳戶", + "admin.moderation_log.empty_message": "你還沒有進行任何管理,如果有任何操作,操作歷史就會顯示在這裏。", + "admin.reports.actions.close": "關閉檢舉", + "admin.reports.actions.view_status": "查看帖文", + "admin.reports.empty_message": "沒有未處理的檢舉,如果有任何檢舉,它就會顯示在這裏。", + "admin.reports.report_closed_message": "對 @{name} 的檢舉已關閉", + "admin.reports.report_title": "檢舉 {acct} 的帖文", + "admin.statuses.actions.delete_status": "刪除帖文", + "admin.statuses.actions.mark_status_not_sensitive": "不再標記為敏感內容", + "admin.statuses.actions.mark_status_sensitive": "標記為敏感內容", + "admin.statuses.status_deleted_message": "@{acct} 的帖文已被刪除", + "admin.statuses.status_marked_message_not_sensitive": "@{acct} 的帖文不會被標記為敏感內容", + "admin.statuses.status_marked_message_sensitive": "@{acct} 的帖文被標記為敏感內容", + "admin.user_index.empty": "找不到用戶。", + "admin.user_index.search_input_placeholder": "你正在找誰?", + "admin.users.actions.deactivate_user": "禁用帳戶 @{name}", + "admin.users.actions.delete_user": "刪除帳戶 @{name}", + "admin.users.actions.demote_to_moderator": "降級 @{name} 為站務", + "admin.users.actions.demote_to_moderator_message": "@{acct} 被降級為站務", + "admin.users.actions.demote_to_user": "降級 @{name} 為普通用戶", + "admin.users.actions.demote_to_user_message": "@{acct} 被降級為普通用戶", + "admin.users.actions.promote_to_admin": "升級 @{name} 為管理員", + "admin.users.actions.promote_to_admin_message": "@{acct} 被升級為管理員", + "admin.users.actions.promote_to_moderator": "升級 @{name} 為站務", + "admin.users.actions.promote_to_moderator_message": "@{acct} 被升級為站務", + "admin.users.actions.remove_donor": "移除 @{name} 的捐贈者頭銜", + "admin.users.actions.set_donor": "將 @{name} 設為捐贈者", + "admin.users.actions.suggest_user": "推薦 @{name}", + "admin.users.actions.unsuggest_user": "取消推薦 @{name}", + "admin.users.actions.unverify_user": "撤銷認證 @{name}", + "admin.users.actions.verify_user": "認證帳戶 @{name}", + "admin.users.remove_donor_message": "@{acct} 被移除出捐贈者名單", + "admin.users.set_donor_message": "@{acct} 被設為捐贈者", + "admin.users.user_deactivated_message": "@{acct} 被停權", + "admin.users.user_deleted_message": "@{acct} 被刪除了", + "admin.users.user_suggested_message": "@{acct} 被推薦", + "admin.users.user_unsuggested_message": "@{acct} 被取消推薦", + "admin.users.user_unverified_message": "@{acct} 未認證", + "admin.users.user_verified_message": "@{acct} 被認證", + "admin_nav.awaiting_approval": "等待核准", + "admin_nav.dashboard": "控制台", + "admin_nav.reports": "檢舉", + "age_verification.fail": "你至少必須滿 {ageMinimum} 歲", + "age_verification.header": "提供你的生日", + "alert.unexpected.body": "我們對這次的中斷感到抱歉。如果問題持續存在,請聯絡我們的支援團隊。你也可以嘗試 {clearCookies} (注意:你的帳戶會自動退出)。", + "alert.unexpected.browser": "瀏覽器", + "alert.unexpected.clear_cookies": "清除cookie和瀏覽器資料", + "alert.unexpected.links.help": "支援中心", + "alert.unexpected.links.status": "狀態", + "alert.unexpected.links.support": "支援", "alert.unexpected.message": "發生了非預期的錯誤。", - "alert.unexpected.return_home": "Return Home", - "alert.unexpected.title": "哎呀!", - "aliases.account.add": "Create alias", - "aliases.account_label": "Old account:", - "aliases.aliases_list_delete": "Unlink alias", - "aliases.search": "Search your old account", - "aliases.success.add": "Account alias created successfully", - "aliases.success.remove": "Account alias removed successfully", - "app_create.name_label": "App name", - "app_create.name_placeholder": "e.g. 'Soapbox'", - "app_create.redirect_uri_label": "Redirect URIs", - "app_create.restart": "Create another", - "app_create.results.app_label": "App", - "app_create.results.explanation_text": "You created a new app and token! Please copy the credentials somewhere; you will not see them again after navigating away from this page.", - "app_create.results.explanation_title": "App created successfully", - "app_create.results.token_label": "OAuth token", - "app_create.scopes_label": "Scopes", - "app_create.scopes_placeholder": "e.g. 'read write follow'", - "app_create.submit": "Create app", - "app_create.website_label": "Website", - "auth.invalid_credentials": "Wrong username or password", - "auth.logged_out": "Logged out.", - "backups.actions.create": "Create backup", - "backups.empty_message": "No backups found. {action}", - "backups.empty_message.action": "Create one now?", - "backups.pending": "Pending", - "beta.also_available": "Available in:", - "birthday_panel.title": "Birthdays", + "alert.unexpected.return_home": "回到主頁", + "alert.unexpected.title": "哎喲!", + "aliases.account.add": "創建別名", + "aliases.account_label": "原帳戶:", + "aliases.aliases_list_delete": "刪除別名", + "aliases.search": "搜尋原帳戶", + "aliases.success.add": "帳戶別名創建成功", + "aliases.success.remove": "帳戶別名刪除成功", + "app_create.name_label": "應用名稱", + "app_create.name_placeholder": "例如 'Soapbox'", + "app_create.redirect_uri_label": "重定向網域", + "app_create.restart": "創建另一個", + "app_create.results.app_label": "應用", + "app_create.results.explanation_text": "您已成功創建一個新應用及其令牌,請複製密鑰等資訊,離開本頁面後這些資訊將不會再次顯示。", + "app_create.results.explanation_title": "應用創建成功", + "app_create.results.token_label": "OAuth令牌", + "app_create.scopes_label": "權限範圍", + "app_create.scopes_placeholder": "例如 '讀取 寫入 追蹤'", + "app_create.submit": "創建應用", + "app_create.website_label": "網站", + "auth.invalid_credentials": "無效的帳戶名稱或密碼", + "auth.logged_out": "您已登出", + "backups.actions.create": "創建備份", + "backups.empty_message": "找不到備份 {action}", + "backups.empty_message.action": "現在創建嗎?", + "backups.pending": "等待備份", + "beta.also_available": "在此時可用:", + "birthday_panel.title": "生日", "boost_modal.combo": "下次您可以按 {combo} 跳過", "bundle_column_error.body": "載入此元件時發生錯誤。", "bundle_column_error.retry": "重試", @@ -163,260 +176,275 @@ "bundle_modal_error.close": "關閉", "bundle_modal_error.message": "載入此元件時發生錯誤。", "bundle_modal_error.retry": "重試", - "card.back.label": "Back", - "chat_box.actions.send": "Send", - "chat_box.input.placeholder": "Send a message…", - "chat_panels.main_window.empty": "No chats found. To start a chat, visit a user's profile.", - "chat_panels.main_window.title": "Chats", - "chats.actions.delete": "Delete message", - "chats.actions.more": "More", - "chats.actions.report": "Report user", - "chats.attachment": "Attachment", - "chats.attachment_image": "Image", - "chats.audio_toggle_off": "Audio notification off", - "chats.audio_toggle_on": "Audio notification on", - "chats.dividers.today": "Today", - "chats.search_placeholder": "Start a chat with…", - "column.admin.awaiting_approval": "Awaiting Approval", - "column.admin.dashboard": "Dashboard", - "column.admin.moderation_log": "Moderation Log", - "column.admin.reports": "Reports", - "column.admin.reports.menu.moderation_log": "Moderation Log", - "column.admin.users": "Users", - "column.aliases": "Account aliases", - "column.aliases.create_error": "Error creating alias", - "column.aliases.delete": "Delete", - "column.aliases.delete_error": "Error deleting alias", - "column.aliases.subheading_add_new": "Add New Alias", - "column.aliases.subheading_aliases": "Current aliases", - "column.app_create": "Create app", - "column.backups": "Backups", - "column.birthdays": "Birthdays", + "card.back.label": "返回", + "chat_box.actions.send": "發送", + "chat_box.input.placeholder": "發送聊天訊息…", + "chat_panels.main_window.empty": "還沒有訊息。要開始聊天,可以從用戶的個人資料頁面發起。", + "chat_panels.main_window.title": "聊天", + "chats.actions.delete": "刪除訊息", + "chats.actions.more": "更多選項", + "chats.actions.report": "檢舉用戶", + "chats.attachment": "附件", + "chats.attachment_image": "圖片", + "chats.audio_toggle_off": "關閉消息提醒", + "chats.audio_toggle_on": "開啟消息提醒", + "chats.dividers.today": "今天", + "chats.search_placeholder": "開始聊天…", + "column.admin.awaiting_approval": "等待核准", + "column.admin.dashboard": "控制台", + "column.admin.moderation_log": "管理日誌", + "column.admin.reports": "檢舉", + "column.admin.reports.menu.moderation_log": "管理日誌", + "column.admin.users": "用戶", + "column.aliases": "帳戶別名", + "column.aliases.create_error": "創建帳戶別名時出錯", + "column.aliases.delete": "刪除", + "column.aliases.delete_error": "刪除帳戶別名時出錯", + "column.aliases.subheading_add_new": "添加新別名", + "column.aliases.subheading_aliases": "當前帳戶別名", + "column.app_create": "創建應用", + "column.backups": "備份", + "column.birthdays": "生日", "column.blocks": "封鎖的使用者", - "column.bookmarks": "Bookmarks", - "column.chats": "Chats", - "column.community": "本機時間軸", - "column.crypto_donate": "Donate Cryptocurrency", - "column.developers": "Developers", + "column.bookmarks": "書籤", + "column.chats": "聊天", + "column.community": "本站時間軸", + "column.crypto_donate": "數字貨幣捐贈", + "column.developers": "開發者", "column.direct": "私訊", - "column.directory": "Browse profiles", + "column.directory": "發現更多", "column.domain_blocks": "隱藏的網域", - "column.edit_profile": "Edit profile", - "column.export_data": "Export data", - "column.favourited_statuses": "Liked posts", - "column.favourites": "Likes", - "column.federation_restrictions": "Federation Restrictions", - "column.filters": "Muted words", - "column.filters.add_new": "Add New Filter", - "column.filters.conversations": "Conversations", - "column.filters.create_error": "Error adding filter", - "column.filters.delete": "Delete", - "column.filters.delete_error": "Error deleting filter", - "column.filters.drop_header": "Drop instead of hide", - "column.filters.drop_hint": "Filtered posts will disappear irreversibly, even if filter is later removed", - "column.filters.expires": "Expire after", - "column.filters.expires_hint": "Expiration dates are not currently supported", - "column.filters.home_timeline": "Home timeline", - "column.filters.keyword": "Keyword or phrase", - "column.filters.notifications": "Notifications", - "column.filters.public_timeline": "Public timeline", - "column.filters.subheading_add_new": "Add New Filter", - "column.filters.subheading_filters": "Current Filters", - "column.filters.whole_word_header": "Whole word", - "column.filters.whole_word_hint": "When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word", - "column.follow_requests": "關注請求", - "column.followers": "Followers", - "column.following": "Following", - "column.groups": "Groups", + "column.edit_profile": "編輯個人資料", + "column.export_data": "匯出資料", + "column.favourited_statuses": "按讚的貼文", + "column.favourites": "按讚", + "column.federation_restrictions": "聯邦限制", + "column.filters": "過濾詞", + "column.filters.add_new": "新增過濾詞", + "column.filters.conversations": "聊天", + "column.filters.create_error": "新增過濾詞時出錯。", + "column.filters.delete": "刪除過濾詞", + "column.filters.delete_error": "刪除過濾詞時出錯。", + "column.filters.drop_header": "丟棄而非隱藏", + "column.filters.drop_hint": "被丟棄的帖文會不可逆地消失,即使移除過濾詞之後也不會恢復", + "column.filters.expires": "過期時間", + "column.filters.expires_hint": "過期時間暫未被支援", + "column.filters.home_timeline": "主頁時間軸", + "column.filters.keyword": "關鍵詞", + "column.filters.notifications": "通知", + "column.filters.public_timeline": "公共時間軸", + "column.filters.subheading_add_new": "新增過濾詞", + "column.filters.subheading_filters": "當前過濾詞", + "column.filters.whole_word_header": "全詞匹配", + "column.filters.whole_word_hint": "如果關鍵詞只含字母和數字,則只有全詞匹配才會被過濾", + "column.follow_requests": "追蹤請求", + "column.followers": "追蹤者", + "column.following": "正在追蹤", + "column.groups": "群組", "column.home": "主頁", - "column.import_data": "Import data", - "column.info": "Server information", + "column.import_data": "匯入資料", + "column.info": "伺服器資訊", "column.lists": "名單", - "column.mentions": "Mentions", - "column.mfa": "Multi-Factor Authentication", - "column.mfa_cancel": "Cancel", - "column.mfa_confirm_button": "Confirm", - "column.mfa_disable_button": "Disable", - "column.mfa_setup": "Proceed to Setup", - "column.migration": "Account migration", - "column.mutes": "被靜音的使用者", + "column.mentions": "提及", + "column.mfa": "兩步驟驗證", + "column.mfa_cancel": "取消", + "column.mfa_confirm_button": "確認", + "column.mfa_disable_button": "停用", + "column.mfa_setup": "同意並繼續", + "column.migration": "帳戶遷移", + "column.mutes": "被隱藏的使用者", "column.notifications": "通知", - "column.pins": "Pinned posts", - "column.preferences": "Preferences", + "column.pins": "釘選的貼文", + "column.preferences": "偏好設定", "column.public": "聯邦時間軸", - "column.reactions": "Reactions", - "column.reblogs": "Reposts", - "column.remote": "Federated timeline", - "column.scheduled_statuses": "Scheduled Posts", - "column.search": "Search", - "column.security": "Security", - "column.settings_store": "Settings store", - "column.soapbox_config": "Soapbox config", - "column.test": "Test timeline", + "column.reactions": "心情回應", + "column.reblogs": "轉帖", + "column.remote": "聯邦時間軸", + "column.scheduled_statuses": "定時帖文", + "column.search": "搜尋", + "column.settings_store": "設定儲存", + "column.soapbox_config": "Soapbox設定", + "column.test": "測試時間軸", "column_back_button.label": "上一頁", - "column_forbidden.body": "You do not have permission to access this page.", - "column_forbidden.title": "Forbidden", + "column_forbidden.body": "您無權訪問這個頁面。", + "column_forbidden.title": "無權訪問", "column_header.show_settings": "顯示設定", - "common.cancel": "Cancel", - "community.column_settings.media_only": "只有媒體", - "community.column_settings.title": "Local timeline settings", - "compose.character_counter.title": "Used {chars} out of {maxChars} characters", - "compose.invalid_schedule": "You must schedule a post at least 5 minutes out.", - "compose.submit_success": "Your post was sent", - "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才看得到。", - "compose_form.hashtag_warning": "由於這則嘟文被設定成「不公開」,所以它將不會被列在任何主題標籤下。只有公開的嘟文才能藉主題標籤找到。", - "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成只有關注者能看的嘟文。", + "common.cancel": "取消", + "common.error": "有些東西好像壞了...嘗試重載頁面", + "community.column_settings.media_only": "僅媒體", + "community.column_settings.title": "本地時間軸設定", + "compare_history_modal.header": "編輯歷史", + "compose.character_counter.title": "最大字符數: {maxChars}; 已使用 {chars}", + "compose.edit_success": "你的帖文已編輯", + "compose.invalid_schedule": "定時帖文只能設定在五分鐘後或更遲發送", + "compose.submit_success": "帖文已送出", + "compose_form.direct_message_warning": "這條帖文只有被提及的使用者才看得到。", + "compose_form.hashtag_warning": "由於這則帖文被設定成「不公開」,所以它將不會被列在任何主題標籤下。只有公開的帖文才能藉主題標籤找到。", + "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能追蹤您並看到您設定成只有追蹤者能看的帖文。", "compose_form.lock_disclaimer.lock": "上鎖", - "compose_form.markdown.marked": "Post markdown enabled", - "compose_form.markdown.unmarked": "Post markdown disabled", - "compose_form.message": "Message", + "compose_form.markdown.marked": "Markdown已啟用", + "compose_form.markdown.unmarked": "Markdown已禁用", + "compose_form.message": "私訊", "compose_form.placeholder": "您正在想些什麼?", "compose_form.poll.add_option": "新增選擇", "compose_form.poll.duration": "投票期限", + "compose_form.poll.multiselect": "多選", + "compose_form.poll.multiselect_detail": "允許參與者選擇多個答案", "compose_form.poll.option_placeholder": "第 {number} 個選擇", + "compose_form.poll.remove": "移除投票", "compose_form.poll.remove_option": "移除此選擇", - "compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices", - "compose_form.poll.switch_to_single": "Change poll to allow for a single choice", - "compose_form.publish": "嘟出去", + "compose_form.poll.switch_to_multiple": "投票改為多選", + "compose_form.poll.switch_to_single": "投票改為單選", + "compose_form.poll_placeholder": "添加投票主題...", + "compose_form.publish": "發佈", "compose_form.publish_loud": "{publish}!", - "compose_form.schedule": "Schedule", - "compose_form.scheduled_statuses.click_here": "Click here", - "compose_form.scheduled_statuses.message": "You have scheduled posts. {click_here} to see them.", - "compose_form.sensitive.hide": "標記媒體為敏感內容", - "compose_form.sensitive.marked": "此媒體被標記為敏感內容", - "compose_form.sensitive.unmarked": "此媒體未標記為敏感內容", + "compose_form.save_changes": "儲存變更", + "compose_form.schedule": "定時發佈", + "compose_form.scheduled_statuses.click_here": "點擊此處", + "compose_form.scheduled_statuses.message": "你有定時帖文, {click_here} 查看", "compose_form.spoiler.marked": "正文已隱藏到警告之後", "compose_form.spoiler.unmarked": "正文未被隱藏", "compose_form.spoiler_placeholder": "請在此處寫入警告訊息", + "compose_form.spoiler_remove": "移除敏感內容", + "compose_form.spoiler_title": "敏感內容", "confirmation_modal.cancel": "取消", - "confirmations.admin.deactivate_user.confirm": "Deactivate @{name}", - "confirmations.admin.deactivate_user.heading": "Deactivate @{acct}", - "confirmations.admin.deactivate_user.message": "You are about to deactivate @{acct}. Deactivating a user is a reversible action.", - "confirmations.admin.delete_local_user.checkbox": "I understand that I am about to delete a local user.", - "confirmations.admin.delete_status.confirm": "Delete post", - "confirmations.admin.delete_status.heading": "Delete post", - "confirmations.admin.delete_status.message": "You are about to delete a post by @{acct}. This action cannot be undone.", - "confirmations.admin.delete_user.confirm": "Delete @{name}", - "confirmations.admin.delete_user.heading": "Delete @{acct}", - "confirmations.admin.delete_user.message": "You are about to delete @{acct}. THIS IS A DESTRUCTIVE ACTION THAT CANNOT BE UNDONE.", - "confirmations.admin.mark_status_not_sensitive.confirm": "Mark post not sensitive", - "confirmations.admin.mark_status_not_sensitive.heading": "Mark post not sensitive.", - "confirmations.admin.mark_status_not_sensitive.message": "You are about to mark a post by @{acct} not sensitive.", - "confirmations.admin.mark_status_sensitive.confirm": "Mark post sensitive", - "confirmations.admin.mark_status_sensitive.heading": "Mark post sensitive", - "confirmations.admin.mark_status_sensitive.message": "You are about to mark a post by @{acct} sensitive.", - "confirmations.admin.reject_user.confirm": "Reject @{name}", - "confirmations.admin.reject_user.heading": "Reject @{acct}", - "confirmations.admin.reject_user.message": "You are about to reject @{acct} registration request. This action cannot be undone.", + "confirmations.admin.deactivate_user.confirm": "禁用帳戶 @{name}", + "confirmations.admin.deactivate_user.heading": "禁用帳戶 @{acct}", + "confirmations.admin.deactivate_user.message": "你確定要禁用帳戶 @{acct} 嗎?此操作不能撤回!", + "confirmations.admin.delete_local_user.checkbox": "我確定我不再需要這個帳戶", + "confirmations.admin.delete_status.confirm": "刪除帖文", + "confirmations.admin.delete_status.heading": "刪除帖文", + "confirmations.admin.delete_status.message": "你確定要刪除帖文 @{acct} 嗎?此操作不能撤回!", + "confirmations.admin.delete_user.confirm": "刪除帳戶 @{name}", + "confirmations.admin.delete_user.heading": "刪除帳戶 @{acct}", + "confirmations.admin.delete_user.message": "你確定要刪除帳戶 @{acct}嗎?此操作不能撤回!", + "confirmations.admin.mark_status_not_sensitive.confirm": "標記為不敏感", + "confirmations.admin.mark_status_not_sensitive.heading": "標記為不敏感", + "confirmations.admin.mark_status_not_sensitive.message": "你要標記帳戶 @{acct} 的帖文為不敏感", + "confirmations.admin.mark_status_sensitive.confirm": "標記為敏感帖文", + "confirmations.admin.mark_status_sensitive.heading": "標記為敏感帖文", + "confirmations.admin.mark_status_sensitive.message": "你要標記帳戶 @{acct} 的帖文為敏感", + "confirmations.admin.reject_user.confirm": "拒絕 @{name}", + "confirmations.admin.reject_user.heading": "拒絕 @{acct}", + "confirmations.admin.reject_user.message": "你正準備拒絕 @{acct} 的註冊請求。此操作不能撤銷。", "confirmations.block.block_and_report": "封鎖並檢舉", "confirmations.block.confirm": "封鎖", - "confirmations.block.heading": "Block @{name}", + "confirmations.block.heading": "封鎖 @{name}", "confirmations.block.message": "確定封鎖 {name} ?", "confirmations.delete.confirm": "刪除", - "confirmations.delete.heading": "Delete post", - "confirmations.delete.message": "你確定要刪除這條嘟文?", + "confirmations.delete.heading": "刪除帖文", + "confirmations.delete.message": "你確定要刪除這條帖文?", "confirmations.delete_list.confirm": "刪除", - "confirmations.delete_list.heading": "Delete list", - "confirmations.delete_list.message": "確定永久刪除此名單?", + "confirmations.delete_list.heading": "刪除列表", + "confirmations.delete_list.message": "你確定要永久刪除這個列表?", "confirmations.domain_block.confirm": "隱藏整個網域", "confirmations.domain_block.heading": "Block {domain}", - "confirmations.domain_block.message": "真的非常確定封鎖整個 {domain} 嗎?大部分情況下,你只需要封鎖或靜音少數特定的人就能滿足需求了。你將不能在任何公開的時間軸及通知中看到那個網域的內容。你來自該網域的關注者也會被移除。", - "confirmations.mute.confirm": "靜音", + "confirmations.domain_block.message": "真的非常確定封鎖整個 {domain} 嗎?大部分情況下,你只需要封鎖或隱藏少數特定的人就能滿足需求了。你將不能在任何公開的時間軸及通知中看到那個網域的內容。你來自該網域的追蹤者也會被移除。", + "confirmations.mute.confirm": "隱藏", "confirmations.mute.heading": "Mute @{name}", - "confirmations.mute.message": "確定靜音 {name} ?", + "confirmations.mute.message": "確定隱藏 {name} ?", "confirmations.redraft.confirm": "刪除並重新編輯", - "confirmations.redraft.heading": "Delete & redraft", - "confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及收藏,且回覆這則的嘟文將會變成獨立的嘟文。", - "confirmations.register.needs_approval": "Your account will be manually approved by an admin. Please be patient while we review your details.", - "confirmations.register.needs_approval.header": "Approval needed", - "confirmations.register.needs_confirmation": "Please check your inbox at {email} for confirmation instructions. You will need to verify your email address to continue.", - "confirmations.register.needs_confirmation.header": "Confirmation needed", + "confirmations.redraft.heading": "刪除並重新編輯", + "confirmations.redraft.message": "確定刪掉這則帖文並重新編輯嗎?將會失去這則帖文的轉帖及收藏,且回覆這則的帖文將會變成獨立的帖文。", + "confirmations.register.needs_approval": "你的帳戶正在被管理員審核中,請等待一會", + "confirmations.register.needs_approval.header": "需要審核", + "confirmations.register.needs_confirmation": "我們已經發送了指引到你的電郵 {email} 中,請檢查收件箱並點擊鏈接以繼續。", + "confirmations.register.needs_confirmation.header": "需要確認", + "confirmations.remove_from_followers.confirm": "刪除", + "confirmations.remove_from_followers.message": "確定要從你的追蹤者中移走 {name} 嗎?", "confirmations.reply.confirm": "回覆", "confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?", - "confirmations.scheduled_status_delete.confirm": "Cancel", - "confirmations.scheduled_status_delete.heading": "Cancel scheduled post", - "confirmations.scheduled_status_delete.message": "Are you sure you want to cancel this scheduled post?", - "confirmations.unfollow.confirm": "取消關注", - "confirmations.unfollow.heading": "Unfollow {name}", - "confirmations.unfollow.message": "真的要取消關注 {name} 嗎?", - "crypto_donate.explanation_box.message": "{siteTitle} accepts cryptocurrency donations. You may send a donation to any of the addresses below. Thank you for your support!", - "crypto_donate.explanation_box.title": "Sending cryptocurrency donations", - "crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}", - "crypto_donate_panel.heading": "Donate Cryptocurrency", - "crypto_donate_panel.intro.message": "{siteTitle} accepts cryptocurrency donations to fund our service. Thank you for your support!", - "datepicker.hint": "Scheduled to post at…", - "datepicker.next_month": "Next month", - "datepicker.next_year": "Next year", - "datepicker.previous_month": "Previous month", - "datepicker.previous_year": "Previous year", - "developers.challenge.answer_label": "Answer", - "developers.challenge.answer_placeholder": "Your answer", - "developers.challenge.fail": "Wrong answer", - "developers.challenge.message": "What is the result of calling {function}?", - "developers.challenge.submit": "Become a developer", - "developers.challenge.success": "You are now a developer", - "developers.leave": "You have left developers", - "developers.navigation.app_create_label": "Create an app", - "developers.navigation.intentional_error_label": "Trigger an error", - "developers.navigation.leave_developers_label": "Leave developers", - "developers.navigation.network_error_label": "Network error", - "developers.navigation.settings_store_label": "Settings store", - "developers.navigation.test_timeline_label": "Test timeline", - "developers.settings_store.hint": "It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.", - "direct.search_placeholder": "Send a message to…", - "directory.federated": "From known fediverse", - "directory.local": "From {domain} only", - "directory.new_arrivals": "New arrivals", - "directory.recently_active": "Recently active", - "edit_federation.followers_only": "Hide posts except to followers", - "edit_federation.force_nsfw": "Force attachments to be marked sensitive", - "edit_federation.media_removal": "Strip media", - "edit_federation.reject": "Reject all activities", - "edit_federation.save": "Save", - "edit_federation.success": "{host} federation was updated", - "edit_federation.unlisted": "Force posts unlisted", - "edit_password.header": "Change Password", - "edit_profile.error": "Profile update failed", - "edit_profile.fields.accepts_email_list_label": "Subscribe to newsletter", - "edit_profile.fields.avatar_label": "Avatar", - "edit_profile.fields.bio_label": "Bio", - "edit_profile.fields.bio_placeholder": "Tell us about yourself.", - "edit_profile.fields.birthday_label": "Birthday", - "edit_profile.fields.birthday_placeholder": "Your birthday", - "edit_profile.fields.bot_label": "This is a bot account", - "edit_profile.fields.discoverable_label": "Allow account discovery", - "edit_profile.fields.display_name_label": "Display name", - "edit_profile.fields.display_name_placeholder": "Name", - "edit_profile.fields.header_label": "Header", - "edit_profile.fields.hide_network_label": "Hide network", - "edit_profile.fields.location_label": "Location", - "edit_profile.fields.location_placeholder": "Location", - "edit_profile.fields.locked_label": "Lock account", - "edit_profile.fields.meta_fields.content_placeholder": "Content", - "edit_profile.fields.meta_fields.label_placeholder": "Label", - "edit_profile.fields.stranger_notifications_label": "Block notifications from strangers", - "edit_profile.fields.website_label": "Website", - "edit_profile.fields.website_placeholder": "Display a Link", - "edit_profile.header": "Edit Profile", - "edit_profile.hints.accepts_email_list": "Opt-in to news and marketing updates.", - "edit_profile.hints.avatar": "PNG, GIF or JPG. Will be downscaled to {size}", - "edit_profile.hints.bot": "This account mainly performs automated actions and might not be monitored", - "edit_profile.hints.discoverable": "Display account in profile directory and allow indexing by external services", - "edit_profile.hints.header": "PNG, GIF or JPG. Will be downscaled to {size}", - "edit_profile.hints.hide_network": "Who you follow and who follows you will not be shown on your profile", - "edit_profile.hints.locked": "Requires you to manually approve followers", - "edit_profile.hints.stranger_notifications": "Only show notifications from people you follow", - "edit_profile.save": "Save", - "edit_profile.success": "Profile saved!", - "email_passthru.confirmed.body": "Close this tab and continue the registration process on the {bold} from which you sent this email confirmation.", - "email_passthru.confirmed.heading": "Email Confirmed!", - "email_passthru.generic_fail.body": "Please request a new email confirmation.", - "email_passthru.generic_fail.heading": "Something Went Wrong", - "email_passthru.token_expired.body": "Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.", - "email_passthru.token_expired.heading": "Token Expired", - "email_passthru.token_not_found.body": "Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.", - "email_passthru.token_not_found.heading": "Invalid Token", - "embed.instructions": "要嵌入此嘟文,請將以下程式碼貼進你的網站。", + "confirmations.scheduled_status_delete.confirm": "取消", + "confirmations.scheduled_status_delete.heading": "取消帖文定時發佈", + "confirmations.scheduled_status_delete.message": "你確定要取消這篇帖文定時發佈嗎?", + "confirmations.unfollow.confirm": "取消追蹤", + "confirmations.unfollow.heading": "取消追蹤 {name}", + "confirmations.unfollow.message": "真的要取消追蹤 {name} 嗎?", + "crypto_donate.explanation_box.message": "{siteTitle} 接受用戶向以下錢包地址捐贈任意數量的數字資產。你的抖內會令我們做得更好!", + "crypto_donate.explanation_box.title": "發起數字貨幣捐贈", + "crypto_donate_panel.actions.view": "點擊查看 {count} {count, plural, one {wallet} other {wallets}}", + "crypto_donate_panel.heading": "捐贈數字貨幣", + "crypto_donate_panel.intro.message": "{siteTitle} 接受用戶捐贈數字貨幣。感謝你的支持!", + "datepicker.day": "Day", + "datepicker.hint": "設定發送時間…", + "datepicker.month": "Month", + "datepicker.next_month": "下個月", + "datepicker.next_year": "明年", + "datepicker.previous_month": "上個月", + "datepicker.previous_year": "去年", + "datepicker.year": "Year", + "developers.challenge.answer_label": "回答", + "developers.challenge.answer_placeholder": "你的回答", + "developers.challenge.fail": "回答錯誤", + "developers.challenge.message": "調用函數 {function} 的結果是什麼?", + "developers.challenge.submit": "成為開發者", + "developers.challenge.success": "你已成為開發者", + "developers.leave": "您已退出開發者", + "developers.navigation.app_create_label": "創建應用", + "developers.navigation.intentional_error_label": "觸發錯誤", + "developers.navigation.leave_developers_label": "退出開發者", + "developers.navigation.network_error_label": "網路錯誤", + "developers.navigation.settings_store_label": "設置儲存", + "developers.navigation.test_timeline_label": "測試時間軸", + "developers.settings_store.hint": "可以在此處直接編輯您的用戶設置。 當心!編輯此部分可能會破壞您的帳戶,您只能通過 API 恢復。", + "direct.body": "新的私訊傳遞體驗即將推出。 敬請期待。", + "direct.search_placeholder": "發送私信給…", + "directory.federated": "來自已知聯邦宇宙", + "directory.local": "僅來自 {domain}", + "directory.new_arrivals": "新增訪客", + "directory.recently_active": "最近活動", + "edit_federation.followers_only": "對追蹤者以外的用戶隱藏帖文", + "edit_federation.force_nsfw": "將附件強制標記為敏感內容", + "edit_federation.media_removal": "去除媒體", + "edit_federation.reject": "拒絕所有資訊交互", + "edit_federation.save": "儲存", + "edit_federation.success": "{host} 聯邦設定已儲存", + "edit_federation.unlisted": "將帖文強制標記移出公共時間軸", + "edit_password.header": "修改密碼", + "edit_profile.error": "個人資料更新失敗", + "edit_profile.fields.accepts_email_list_label": "訂閲電子郵件列表", + "edit_profile.fields.avatar_label": "頭帖", + "edit_profile.fields.bio_label": "簡介", + "edit_profile.fields.bio_placeholder": "介紹一下自己", + "edit_profile.fields.birthday_label": "生日", + "edit_profile.fields.birthday_placeholder": "你的生日", + "edit_profile.fields.bot_label": "這是一個機械人帳戶", + "edit_profile.fields.discoverable_label": "公開自己", + "edit_profile.fields.display_name_label": "昵稱", + "edit_profile.fields.display_name_placeholder": "你的昵稱", + "edit_profile.fields.header_label": "個人資料頁橫幅圖片", + "edit_profile.fields.hide_network_label": "隱藏網路狀態", + "edit_profile.fields.location_label": "地點", + "edit_profile.fields.location_placeholder": "地點", + "edit_profile.fields.locked_label": "鎖定帳戶", + "edit_profile.fields.meta_fields.content_placeholder": "內容", + "edit_profile.fields.meta_fields.label_placeholder": "標籤", + "edit_profile.fields.verified_display_name": "經過驗證的用戶無法修改昵稱", + "edit_profile.fields.meta_fields_label": "個人資料自定義列", + "edit_profile.fields.stranger_notifications_label": "封鎖來自陌生人的通知", + "edit_profile.fields.website_label": "網站", + "edit_profile.fields.website_placeholder": "顯示連結", + "edit_profile.header": "編輯個人資料", + "edit_profile.hints.accepts_email_list": "接收新聞和推廣電郵", + "edit_profile.hints.avatar": "只支援 PNG, GIF or JPG。 尺寸將會被縮放至 {size}", + "edit_profile.hints.bot": "此帳戶主要執行自動化操作,可能不受用戶控制", + "edit_profile.hints.discoverable": "在配置文件目錄中顯示帳戶並允許外部服務索引", + "edit_profile.hints.header": "只支援 PNG, GIF or JPG。 尺寸將會被縮放至 {size}", + "edit_profile.hints.hide_network": "您追蹤的人和追蹤您的人不會顯示在您的個人資料中", + "edit_profile.hints.locked": "需要您手動批准追蹤請求", + "edit_profile.hints.meta_fields": "你能在個人資料頁面上最多顯示 {count} 行自定義列", + "edit_profile.hints.stranger_notifications": "僅顯示來自您追蹤的人的通知", + "edit_profile.save": "儲存", + "edit_profile.success": "個人資料已儲存", + "email_passthru.confirmed.body": "關閉此頁面,並在你發送此電子郵件確認的 {bold} 上繼續註冊過程", + "email_passthru.confirmed.heading": "電郵地址已確認!", + "email_passthru.generic_fail.body": "請重新請求確認郵件", + "email_passthru.generic_fail.heading": "好像出了一點問題…", + "email_passthru.token_expired.body": "你的電郵令牌已經過期。請從你發送此電郵確認的 {bold} 處重新申請電郵確認。", + "email_passthru.token_expired.heading": "令牌已過期", + "email_passthru.token_not_found.body": "你的電郵令牌已經過期。請從你發送此電郵確認的 {bold} 處重新申請電郵確認。", + "email_passthru.token_not_found.heading": "無效令牌!", + "embed.instructions": "要嵌入此帖文,請將以下程式碼貼進你的網站。", "embed.preview": "他會顯示成這樣:", "emoji_button.activity": "活動", "emoji_button.custom": "自訂", @@ -432,550 +460,558 @@ "emoji_button.search_results": "搜尋結果", "emoji_button.symbols": "符號", "emoji_button.travel": "旅遊與地點", - "empty_column.account_blocked": "You are blocked by @{accountUsername}.", - "empty_column.account_favourited_statuses": "This user doesn't have any liked posts yet.", - "empty_column.account_timeline": "這裡還沒有嘟文!", + "empty_column.account_blocked": "你被 @{accountUsername} 封鎖了", + "empty_column.account_favourited_statuses": "他還沒有按讚任何貼文", + "empty_column.account_timeline": "這裡還沒有帖文!", "empty_column.account_unavailable": "無法取得個人資料", - "empty_column.aliases": "You haven't created any account alias yet.", - "empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.", + "empty_column.aliases": "你還沒有設定任何別名", + "empty_column.aliases.suggestions": "暫時沒有可用的帳戶建議", "empty_column.blocks": "你還沒有封鎖任何使用者。", - "empty_column.bookmarks": "You don't have any bookmarks yet. When you add one, it will show up here.", - "empty_column.community": "本地時間軸是空的。快公開嘟些文搶頭香啊!", + "empty_column.bookmarks": "你還沒有任何書籤收藏,一旦你開始將帖文加入書籤,它將會在這裡顯示", + "empty_column.community": "本地時間軸是空的。快公開發佈些文搶頭香啊!", "empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。", "empty_column.domain_blocks": "尚未隱藏任何網域。", - "empty_column.favourited_statuses": "你還沒收藏任何嘟文。這裡將會顯示你收藏的嘟文。", - "empty_column.favourites": "還沒有人收藏這則嘟文。這裡將會顯示被收藏的嘟文。", - "empty_column.filters": "You haven't created any muted words yet.", - "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", - "empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。", - "empty_column.group": "There is nothing in this group yet. When members of this group make new posts, they will appear here.", + "empty_column.favourited_statuses": "你還沒收藏任何帖文。這裡將會顯示你收藏的帖文。", + "empty_column.favourites": "還沒有人收藏這則帖文。這裡將會顯示被收藏的帖文。", + "empty_column.filters": "你還未有添加任何過濾詞。", + "empty_column.follow_recommendations": "目前似乎暫時沒有推薦訊息,你可以嘗試搜尋用戶或者瀏覽熱門標籤。", + "empty_column.follow_requests": "您尚未收到任何追蹤請求。這裡將會顯示收到的追蹤請求。", + "empty_column.group": "該組中還沒有任何內容。 當該群組的成員發布新帖子時,他們會出現在此處。", "empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.home": "您的首頁時間軸是空的!前往 {public} 或使用搜尋功能來認識其他人。", - "empty_column.home.local_tab": "the {site_title} tab", - "empty_column.list": "這份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會顯示於此。", + "empty_column.home.local_tab": "{site_title} 本站時間軸", + "empty_column.list": "這份名單還沒有東西。當此名單的成員發佈了新的帖文時,它們就會顯示於此。", "empty_column.lists": "你還沒有建立任何名單。這裡將會顯示你所建立的名單。", - "empty_column.mutes": "你尚未靜音任何使用者。", + "empty_column.mutes": "你尚未隱藏任何使用者。", "empty_column.notifications": "您尚未收到任何通知,和別人互動開啟對話吧。", - "empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或著自己關注其他伺服器的使用者後就會有嘟文出現了", - "empty_column.remote": "There is nothing here! Manually follow users from {instance} to fill it up.", - "empty_column.scheduled_statuses": "You don't have any scheduled statuses yet. When you add one, it will show up here.", - "empty_column.search.accounts": "There are no people results for \"{term}\"", - "empty_column.search.hashtags": "There are no hashtags results for \"{term}\"", - "empty_column.search.statuses": "There are no posts results for \"{term}\"", - "export_data.actions.export": "Export", - "export_data.actions.export_blocks": "Export blocks", - "export_data.actions.export_follows": "Export follows", - "export_data.actions.export_mutes": "Export mutes", - "export_data.blocks_label": "Blocks", - "export_data.follows_label": "Follows", - "export_data.hints.blocks": "Get a CSV file containing a list of blocked accounts", - "export_data.hints.follows": "Get a CSV file containing a list of followed accounts", - "export_data.hints.mutes": "Get a CSV file containing a list of muted accounts", - "export_data.mutes_label": "Mutes", - "export_data.success.blocks": "Blocks exported successfully", - "export_data.success.followers": "Followers exported successfully", - "export_data.success.mutes": "Mutes exported successfully", - "federation_restriction.federated_timeline_removal": "Fediverse timeline removal", - "federation_restriction.followers_only": "Hidden except to followers", - "federation_restriction.full_media_removal": "Full media removal", - "federation_restriction.media_nsfw": "Attachments marked NSFW", - "federation_restriction.partial_media_removal": "Partial media removal", - "federation_restrictions.empty_message": "{siteTitle} has not restricted any instances.", - "federation_restrictions.explanation_box.message": "Normally servers on the Fediverse can communicate freely. {siteTitle} has imposed restrictions on the following servers.", - "federation_restrictions.explanation_box.title": "Instance-specific policies", - "federation_restrictions.not_disclosed_message": "{siteTitle} does not disclose federation restrictions through the API.", - "fediverse_tab.explanation_box.dismiss": "Don't show again", - "fediverse_tab.explanation_box.explanation": "{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka \"servers\"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don't like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.", - "fediverse_tab.explanation_box.title": "What is the Fediverse?", - "filters.added": "Filter added.", - "filters.context_header": "Filter contexts", - "filters.context_hint": "One or multiple contexts where the filter should apply", - "filters.filters_list_context_label": "Filter contexts:", - "filters.filters_list_delete": "Delete", - "filters.filters_list_details_label": "Filter settings:", - "filters.filters_list_drop": "Drop", - "filters.filters_list_hide": "Hide", - "filters.filters_list_phrase_label": "Keyword or phrase:", - "filters.filters_list_whole-word": "Whole word", - "filters.removed": "Filter deleted.", - "follow_recommendation.subhead": "Let's get started!", - "follow_recommendations.done": "Done", - "follow_recommendations.heading": "Follow people you'd like to see posts from! Here are some suggestions.", - "follow_recommendations.lead": "Posts from people you follow will show up in chronological order on your home feed. Don't be afraid to make mistakes, you can unfollow people just as easily any time!", + "empty_column.public": "這裡什麼都沒有!嘗試寫些公開的帖文,或著自己追蹤其他伺服器的使用者後就會有帖文出現了", + "empty_column.remote": "這裡什麼都沒有! 關注本站或者其他站點的成員,就會有用戶出現在這裡。", + "empty_column.scheduled_statuses": "暫時沒有定時帖文。當你發佈定時帖文後,它們會顯示在這裡。", + "empty_column.search.accounts": "沒有匹配的帳戶 \"{term}\"", + "empty_column.search.hashtags": "沒有匹配的標籤 \"{term}\"", + "empty_column.search.statuses": "沒有匹配的帖文 \"{term}\"", + "empty_column.test": "測試時間軸是空的", + "export_data.actions.export": "匯出", + "export_data.actions.export_blocks": "匯出封鎖名單", + "export_data.actions.export_follows": "匯出追蹤名單", + "export_data.actions.export_mutes": "匯出隱藏名單", + "export_data.blocks_label": "封鎖", + "export_data.follows_label": "追蹤", + "export_data.hints.blocks": "匯出封鎖名單為CSV檔案", + "export_data.hints.follows": "匯出追蹤名單為CSV檔案", + "export_data.hints.mutes": "匯出隱藏名單為CSV檔案", + "export_data.mutes_label": "隱藏", + "export_data.success.blocks": "封鎖名單匯出成功", + "export_data.success.followers": "追蹤名單匯出成功", + "export_data.success.mutes": "隱藏名單匯出成功", + "federation_restriction.federated_timeline_removal": "從聯邦宇宙時間軸移除", + "federation_restriction.followers_only": "僅追蹤者可見", + "federation_restriction.full_media_removal": "完全移除媒體", + "federation_restriction.media_nsfw": "附件標註為 NSFW", + "federation_restriction.partial_media_removal": "部分移除媒體", + "federation_restrictions.empty_message": "{siteTitle} 沒有限制任何實例", + "federation_restrictions.explanation_box.message": "通常情況下,聯邦宇宙上的伺服器可以自由通訊。然而 {siteTitle} 對以下伺服器實施了限制", + "federation_restrictions.explanation_box.title": "實例相關政策", + "federation_restrictions.not_disclosed_message": "{siteTitle} 沒有通過API向聯邦宇宙公開限制。", + "fediverse_tab.explanation_box.dismiss": "不再顯示", + "fediverse_tab.explanation_box.explanation": "{site_title} 是聯邦宇宙的一份子, 一個由數個站點組成的社交網路集合。你在這裏看到的帖文來自於其他站點。你可以自由地與他們打交道,或者封鎖任何你不喜歡的站點。第二個 @ 符號後的完整帳戶名表示帖文來自哪個站點。要想只看到 {site_title} 的帖文, 請瀏覽 {local} 。", + "fediverse_tab.explanation_box.title": "什麼是聯邦宇宙?", + "feed_suggestions.heading": "建議的個人資料", + "feed_suggestions.view_all": "檢視全部", + "filters.added": "過濾器已添加。", + "filters.context_header": "過濾器場景", + "filters.context_hint": "一個或多個應用至過濾器的條件", + "filters.filters_list_context_label": "過濾器場景:", + "filters.filters_list_delete": "刪除過濾詞", + "filters.filters_list_details_label": "過濾詞設定:", + "filters.filters_list_drop": "丟棄", + "filters.filters_list_hide": "隱藏", + "filters.filters_list_phrase_label": "關鍵詞:", + "filters.filters_list_whole-word": "全詞", + "filters.removed": "過濾器已移除", + "follow_recommendation.subhead": "讓我們開始吧!", + "follow_recommendations.done": "完成", + "follow_recommendations.heading": "追蹤你感興趣的人,這是我們的推薦列表", + "follow_recommendations.lead": "你追蹤的人的帖文將按照時間順序顯示在你的主頁上。不用擔心搞錯,你可以在任何時候輕鬆地撤銷追蹤!", "follow_request.authorize": "授權", "follow_request.reject": "拒絕", - "forms.copy": "Copy", - "forms.hide_password": "Hide password", - "forms.show_password": "Show password", + "forms.copy": "複製", + "forms.hide_password": "隱藏密碼", + "forms.show_password": "顯示密碼", "getting_started.open_source_notice": "{code_name} 是開源軟體。你可以在 GitLab {code_link} (v{code_version}) 上貢獻或是回報問題。", - "group.detail.archived_group": "Archived group", - "group.members.empty": "This group does not has any members.", - "group.removed_accounts.empty": "This group does not has any removed accounts.", - "groups.card.join": "Join", - "groups.card.members": "Members", - "groups.card.roles.admin": "You're an admin", - "groups.card.roles.member": "You're a member", - "groups.card.view": "View", - "groups.create": "Create group", - "groups.detail.role_admin": "You're an admin", - "groups.edit": "Edit", - "groups.form.coverImage": "Upload new banner image (optional)", - "groups.form.coverImageChange": "Banner image selected", - "groups.form.create": "Create group", - "groups.form.description": "Description", - "groups.form.title": "Title", - "groups.form.update": "Update group", - "groups.join": "Join group", - "groups.leave": "Leave group", - "groups.removed_accounts": "Removed Accounts", - "groups.sidebar-panel.item.no_recent_activity": "No recent activity", - "groups.sidebar-panel.item.view": "new posts", - "groups.sidebar-panel.show_all": "Show all", - "groups.sidebar-panel.title": "Groups You're In", - "groups.tab_admin": "Manage", - "groups.tab_featured": "Featured", - "groups.tab_member": "Member", + "group.members.empty": "這個名單中沒有任何成員", + "group.removed_accounts.empty": "這個名單中沒有任何被移除的帳戶", + "groups.card.join": "加入", + "groups.card.members": "成員", + "groups.card.roles.admin": "你是管理員", + "groups.card.roles.member": "你是成員", + "groups.card.view": "檢視", + "groups.create": "創建群組", + "groups.form.coverImage": "上載橫幅圖片 (可選)", + "groups.form.coverImageChange": "橫幅圖片已上載", + "groups.form.create": "創建群組", + "groups.form.description": "描述", + "groups.form.title": "標題", + "groups.form.update": "更新群組", + "groups.removed_accounts": "已移除帳戶", + "groups.tab_admin": "管理", + "groups.tab_featured": "精選", + "groups.tab_member": "成員", "hashtag.column_header.tag_mode.all": "以及{additional}", "hashtag.column_header.tag_mode.any": "或是{additional}", "hashtag.column_header.tag_mode.none": "而無需{additional}", - "header.home.label": "Home", - "header.login.forgot_password": "Forgot password?", - "header.login.label": "Log in", - "header.login.password.label": "Password", - "header.login.username.placeholder": "Email or username", - "header.register.label": "Register", - "home.column_settings.show_direct": "Show direct messages", - "home.column_settings.show_reblogs": "顯示轉嘟", + "header.home.label": "首頁", + "header.login.forgot_password": "忘記了密碼?", + "header.login.label": "登入", + "header.login.password.label": "密碼", + "header.login.username.placeholder": "電郵地址或帳戶名稱", + "header.preview_timeline.label": "瀏覽首頁", + "header.register.label": "註冊", + "home.column_settings.show_reblogs": "顯示轉帖", "home.column_settings.show_replies": "顯示回覆", - "home.column_settings.title": "Home settings", - "icon_button.icons": "Icons", - "icon_button.label": "Select icon", - "icon_button.not_found": "No icons!! (╯°□°)╯︵ ┻━┻", - "import_data.actions.import": "Import", - "import_data.actions.import_blocks": "Import blocks", - "import_data.actions.import_follows": "Import follows", - "import_data.actions.import_mutes": "Import mutes", - "import_data.blocks_label": "Blocks", - "import_data.follows_label": "Follows", - "import_data.hints.blocks": "CSV file containing a list of blocked accounts", - "import_data.hints.follows": "CSV file containing a list of followed accounts", - "import_data.hints.mutes": "CSV file containing a list of muted accounts", - "import_data.mutes_label": "Mutes", - "import_data.success.blocks": "Blocks imported successfully", - "import_data.success.followers": "Followers imported successfully", - "import_data.success.mutes": "Mutes imported successfully", - "input.password.hide_password": "Hide password", - "input.password.show_password": "Show password", - "intervals.full.days": "{number, plural, one {# 天} other {# 天}}", - "intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}", - "intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}", - "introduction.federation.action": "Next", - "introduction.federation.home.headline": "Home", - "introduction.federation.home.text": "Posts from people you follow will appear in your home feed. You can follow anyone on any server!", - "introduction.interactions.action": "Finish tutorial!", - "introduction.interactions.favourite.headline": "Favorite", - "introduction.interactions.favourite.text": "You can save a post for later, and let the author know that you liked it, by favoriting it.", - "introduction.interactions.reblog.headline": "Repost", - "introduction.interactions.reblog.text": "You can share other people's posts with your followers by reposting them.", - "introduction.interactions.reply.headline": "Reply", - "introduction.interactions.reply.text": "You can reply to other people's and your own posts, which will chain them together in a conversation.", - "introduction.welcome.action": "Let's go!", - "introduction.welcome.headline": "First steps", - "introduction.welcome.text": "Welcome to the fediverse! In a few moments, you'll be able to broadcast messages and talk to your friends across a wide variety of servers. But this server, {domain}, is special—it hosts your profile, so remember its name.", + "icon_button.icons": "圖示", + "icon_button.label": "選取圖示", + "icon_button.not_found": "沒有圖示!! (╯°□°)╯︵ ┻━┻", + "import_data.actions.import": "匯入", + "import_data.actions.import_blocks": "匯入封鎖名單", + "import_data.actions.import_follows": "匯入追蹤名單", + "import_data.actions.import_mutes": "匯入隱藏名單", + "import_data.blocks_label": "封鎖帳戶", + "import_data.follows_label": "追蹤帳戶", + "import_data.hints.blocks": "上載包含封鎖帳戶名單的CSV檔案", + "import_data.hints.follows": "上載包含追蹤帳戶名單的CSV檔案", + "import_data.hints.mutes": "上載包含隱藏帳戶名單的CSV檔案", + "import_data.mutes_label": "隱藏帳戶", + "import_data.success.blocks": "封鎖帳戶名單已匯入", + "import_data.success.followers": "追蹤帳戶名單已匯入", + "import_data.success.mutes": "隱藏帳戶名單已匯入", + "input.password.hide_password": "隱藏密碼", + "input.password.show_password": "顯示密碼", + "intervals.full.days": "{number} 天", + "intervals.full.hours": "{number} 小時", + "intervals.full.minutes": "{number} 分鐘", "keyboard_shortcuts.back": "返回上一頁", "keyboard_shortcuts.blocked": "開啟「封鎖使用者」名單", - "keyboard_shortcuts.boost": "轉嘟", + "keyboard_shortcuts.boost": "轉帖", "keyboard_shortcuts.compose": "將焦點移至撰寫文字區塊", "keyboard_shortcuts.down": "往下移動名單項目", - "keyboard_shortcuts.enter": "檢視嘟文", + "keyboard_shortcuts.enter": "檢視帖文", "keyboard_shortcuts.favourite": "收藏", "keyboard_shortcuts.favourites": "開啟收藏名單", "keyboard_shortcuts.heading": "鍵盤快速鍵", "keyboard_shortcuts.home": "開啟首頁時間軸", "keyboard_shortcuts.hotkey": "快速鍵", - "keyboard_shortcuts.legend": "顯示此列表", + "keyboard_shortcuts.legend": "顯示此名單", "keyboard_shortcuts.mention": "提及作者", - "keyboard_shortcuts.muted": "開啟靜音使用者名單", + "keyboard_shortcuts.muted": "開啟隱藏使用者名單", "keyboard_shortcuts.my_profile": "開啟個人資料頁面", "keyboard_shortcuts.notifications": "開啟通知欄", - "keyboard_shortcuts.open_media": "to open media", - "keyboard_shortcuts.pinned": "開啟釘選的嘟文名單", + "keyboard_shortcuts.open_media": "開啟媒體", + "keyboard_shortcuts.pinned": "開啟釘選的帖文名單", "keyboard_shortcuts.profile": "開啟作者的個人資料頁面", - "keyboard_shortcuts.react": "to react", + "keyboard_shortcuts.react": "心情回應", "keyboard_shortcuts.reply": "回覆", - "keyboard_shortcuts.requests": "開啟關注請求名單", + "keyboard_shortcuts.requests": "開啟追蹤請求名單", "keyboard_shortcuts.search": "將焦點移至搜尋框", "keyboard_shortcuts.toggle_hidden": "顯示/隱藏在內容警告之後的正文", "keyboard_shortcuts.toggle_sensitivity": "顯示 / 隱藏媒體", - "keyboard_shortcuts.toot": "開始發出新嘟文", + "keyboard_shortcuts.toot": "開始發出新帖文", "keyboard_shortcuts.unfocus": "取消輸入文字區塊 / 搜尋的焦點", "keyboard_shortcuts.up": "往上移動名單項目", - "landing_page_modal.download": "Download", - "landing_page_modal.helpCenter": "Help Center", + "landing_page_modal.download": "下載", + "landing_page_modal.helpCenter": "支援中心", "lightbox.close": "關閉", "lightbox.next": "下一步", "lightbox.previous": "上一步", "lightbox.view_context": "檢視內文", - "list.click_to_add": "Click here to add people", - "list_adder.header_title": "Add or Remove from Lists", + "list.click_to_add": "點擊新增帳戶到名單", + "list_adder.header_title": "從名單中添加或刪除帳戶", "lists.account.add": "新增至名單", "lists.account.remove": "從名單中移除", - "lists.delete": "Delete list", + "lists.delete": "刪除名單", "lists.edit": "編輯名單", "lists.edit.submit": "變更標題", "lists.new.create": "新增名單", - "lists.new.create_title": "Add list", - "lists.new.save_title": "Save Title", + "lists.new.create_title": "新增名單", + "lists.new.save_title": "儲存名單", "lists.new.title_placeholder": "新名單標題", - "lists.search": "搜尋您關注的使用者", + "lists.search": "搜尋您追蹤的使用者", "lists.subheading": "您的名單", "loading_indicator.label": "讀取中...", - "login.fields.instance_label": "Instance", + "login.fields.instance_label": "實例", "login.fields.instance_placeholder": "example.com", - "login.fields.otp_code_hint": "Enter the two-factor code generated by your phone app or use one of your recovery codes", - "login.fields.otp_code_label": "Two-factor code:", - "login.fields.password_placeholder": "Password", - "login.fields.username_label": "Email or username", - "login.log_in": "Log in", - "login.otp_log_in": "OTP Login", - "login.reset_password_hint": "Trouble logging in?", - "login.sign_in": "Sign in", + "login.fields.otp_code_hint": "輸入兩步驟驗證應用程式裏的代碼,或者輸入恢復代碼", + "login.fields.otp_code_label": "兩步驟驗證代碼:", + "login.fields.password_placeholder": "密碼", + "login.fields.username_label": "電郵地址或帳戶名稱", + "login.log_in": "登入", + "login.otp_log_in": "兩步驟驗證登入", + "login.otp_log_in.fail": "兩步驟驗證代號無效,請重新輸入", + "login.reset_password_hint": "登入遇到了問題?", + "login.sign_in": "登入", + "login_form.header": "登入", "media_gallery.toggle_visible": "切換可見性", - "media_panel.empty_message": "No media found.", - "media_panel.title": "Media", - "mfa.confirm.success_message": "MFA confirmed", - "mfa.disable.success_message": "MFA disabled", - "mfa.mfa_disable_enter_password": "Enter your current password to disable two-factor auth:", - "mfa.mfa_setup.code_hint": "Enter the code from your two-factor app.", - "mfa.mfa_setup.code_placeholder": "Code", - "mfa.mfa_setup.password_hint": "Enter your current password to confirm your identity.", - "mfa.mfa_setup.password_placeholder": "Password", - "mfa.mfa_setup_scan_description": "Using your two-factor app, scan this QR code or enter text key:", - "mfa.mfa_setup_scan_title": "Scan", - "mfa.mfa_setup_verify_title": "Verify", - "mfa.otp_enabled_description": "You have enabled two-factor authentication via OTP.", - "mfa.otp_enabled_title": "OTP Enabled", - "mfa.setup_recoverycodes": "Recovery codes", - "mfa.setup_warning": "Write these codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.", - "migration.fields.acct.label": "Handle of the new account", - "migration.fields.acct.placeholder": "username@domain", - "migration.fields.confirm_password.label": "Current password", - "migration.hint": "This will move your followers to the new account. No other data will be moved. To perform migration, you need to {link} on your new account first.", - "migration.hint.link": "create an account alias", - "migration.move_account.fail": "Account migration failed.", - "migration.move_account.success": "Account successfully moved.", - "migration.submit": "Move followers", - "missing_description_modal.cancel": "Cancel", - "missing_description_modal.continue": "Post", - "missing_description_modal.description": "Continue anyway?", - "missing_description_modal.text": "You have not entered a description for all attachments. Continue anyway?", + "media_panel.empty_message": "未找到媒體", + "media_panel.title": "媒體", + "mfa.confirm.success_message": "多重要素驗證 (MFA) 已啟用", + "mfa.disable.success_message": "多重要素驗證 (MFA) 已停用", + "mfa.mfa_disable_enter_password": "輸入當前密碼以停用兩步驟驗證:", + "mfa.mfa_setup.code_hint": "輸入兩步驟驗證應用程式裏的代碼", + "mfa.mfa_setup.code_placeholder": "代碼", + "mfa.mfa_setup.password_hint": "輸入當前密碼以確認你的身份", + "mfa.mfa_setup.password_placeholder": "密碼", + "mfa.mfa_setup_scan_description": "請使用 Google Authenticator 或其他應用程式來掃描 QR 碼。啟用兩步驟驗證後,在登入時你需要提供該應用程式生成的代碼", + "mfa.mfa_setup_scan_title": "如果你無法掃描 QR 碼,請手動輸入下列文本:", + "mfa.mfa_setup_verify_title": "啟用", + "mfa.otp_enabled_description": "您已成功啟用兩步驟驗證", + "mfa.otp_enabled_title": "兩步驟驗證已啟用", + "mfa.setup_recoverycodes": "恢復代碼", + "mfa.setup_warning": "請將你的恢復代碼記在安全的地方或寫在紙上,建議與個人資訊分開存放,以便在丟失兩步驟驗證應用程式時可以恢復你的帳戶。", + "migration.fields.acct.label": "新帳戶的帳戶名稱", + "migration.fields.acct.placeholder": "帳戶名稱@網域", + "migration.fields.confirm_password.label": "當前密碼", + "migration.hint": "你的追蹤者將被轉移到新帳戶,除此之外其他資料將不會被轉移。要執行遷移,您需要先{link}你的新帳戶", + "migration.hint.link": "新建一個帳戶別名", + "migration.move_account.fail": "帳戶遷移失敗。", + "migration.move_account.success": "帳戶遷移成功了!", + "migration.submit": "遷移關注者", + "missing_description_modal.cancel": "取消", + "missing_description_modal.continue": "發佈", + "missing_description_modal.description": "仍然繼續發佈嗎?", + "missing_description_modal.text": "附件沒有提供描述。仍然繼續發佈嗎?", "missing_indicator.label": "找不到", "missing_indicator.sublabel": "找不到此資源", - "mobile.also_available": "Available in:", - "morefollows.followers_label": "…and {count} more {count, plural, one {follower} other {followers}} on remote sites.", - "morefollows.following_label": "…and {count} more {count, plural, one {follow} other {follows}} on remote sites.", - "mute_modal.hide_notifications": "隱藏來自這位使用者的通知?", - "navigation.chats": "Chats", - "navigation.compose": "Compose", - "navigation.dashboard": "Dashboard", - "navigation.developers": "Developers", - "navigation.direct_messages": "Messages", - "navigation.home": "Home", - "navigation.invites": "Invites", - "navigation.notifications": "Notifications", - "navigation.search": "Search", - "navigation_bar.account_migration": "Move account", + "mobile.also_available": "在這裏可用:", + "moderation_overlay.contact": "聯絡", + "moderation_overlay.hide": "隱藏內容", + "moderation_overlay.show": "顯示內容", + "moderation_overlay.subtitle": "此帖文已發送至站務以供審核,目前只允許本人查看。如你認為該消息有誤,請聯絡站務", + "moderation_overlay.title": "內容正被審核中", + "morefollows.followers_label": "和{count}來自其他站點的{count, plural, one {追蹤者} other {追蹤者}} 。", + "morefollows.following_label": "和{count}來自其他站點的{count, plural, one {正在追蹤} other {正在追蹤}} 。", + "mute_modal.hide_notifications": "隱藏來自這個帳戶的通知?", + "navbar.login.action": "登入", + "navbar.login.forgot_password": "忘記密碼?", + "navbar.login.password.label": "密碼", + "navbar.login.username.placeholder": "郵箱地址或帳戶名稱", + "navigation.chats": "聊天", + "navigation.compose": "發佈新帖文", + "navigation.dashboard": "控制台", + "navigation.developers": "開發者", + "navigation.direct_messages": "私人訊息", + "navigation.home": "首頁", + "navigation.invites": "邀請", + "navigation.notifications": "通知", + "navigation.search": "搜尋", + "navigation_bar.account_aliases": "帳戶別名", + "navigation_bar.account_migration": "帳戶遷移", "navigation_bar.blocks": "封鎖使用者", - "navigation_bar.compose": "撰寫新嘟文", - "navigation_bar.compose_direct": "Direct message", - "navigation_bar.compose_quote": "Quote post", - "navigation_bar.compose_reply": "Reply to post", + "navigation_bar.compose": "撰寫新帖文", + "navigation_bar.compose_direct": "發送私人訊息", + "navigation_bar.compose_edit": "編輯帖文", + "navigation_bar.compose_quote": "引用帖文", + "navigation_bar.compose_reply": "回覆帖文", "navigation_bar.domain_blocks": "隱藏的網域", "navigation_bar.favourites": "收藏", - "navigation_bar.filters": "靜音詞彙", - "navigation_bar.follow_requests": "關注請求", - "navigation_bar.import_data": "Import data", - "navigation_bar.in_reply_to": "In reply to", - "navigation_bar.invites": "Invites", + "navigation_bar.filters": "隱藏", + "navigation_bar.follow_requests": "追蹤請求", + "navigation_bar.import_data": "匯入資料", + "navigation_bar.in_reply_to": "回覆", + "navigation_bar.invites": "邀請", "navigation_bar.logout": "登出", - "navigation_bar.mutes": "靜音的使用者", + "navigation_bar.mutes": "隱藏的使用者", "navigation_bar.preferences": "偏好設定", - "navigation_bar.profile_directory": "Profile directory", - "navigation_bar.security": "安全性", - "navigation_bar.soapbox_config": "Soapbox config", - "notification.birthday": "{name} has a birthday today", - "notification.birthday.more": "{count} more {count, plural, one {friend} other {friends}}", - "notification.birthday_plural": "{name} and {more} have birthday today", - "notification.pleroma:chat_mention": "{name} sent you a message", - "notification.favourite": "{name} 把你的嘟文加入了最愛", - "notification.follow": "{name} 關注了你", - "notification.follow_request": "{name} has requested to follow you", + "navigation_bar.profile_directory": "發現更多帳戶", + "navigation_bar.soapbox_config": "Soapbox配置", + "notification.favourite": "{name} 讚了你的帖文", + "notification.follow": "{name} 追蹤了你", + "notification.follow_request": "{name} 請求追蹤你", "notification.mention": "{name} 提到了你", - "notification.move": "{name} moved to {targetName}", - "notification.pleroma:emoji_reaction": "{name} reacted to your post", - "notification.poll": "您投過的投票已經結束", - "notification.reblog": "{name}轉嘟了你的嘟文", - "notification.status": "{name} just posted", - "notifications.clear": "清除通知", - "notifications.clear_confirmation": "確定要永久清除你的通知嗎?", - "notifications.clear_heading": "Clear notifications", - "notifications.column_settings.alert": "桌面通知", - "notifications.column_settings.birthdays.category": "Birthdays", - "notifications.column_settings.birthdays.show": "Show birthday reminders", - "notifications.column_settings.emoji_react": "Emoji reacts:", - "notifications.column_settings.favourite": "最愛:", - "notifications.column_settings.filter_bar.advanced": "顯示所有分類", - "notifications.column_settings.filter_bar.category": "快速過濾欄", - "notifications.column_settings.filter_bar.show": "顯示", - "notifications.column_settings.follow": "新關注者:", - "notifications.column_settings.follow_request": "New follow requests:", - "notifications.column_settings.mention": "提及:", - "notifications.column_settings.move": "Moves:", - "notifications.column_settings.poll": "投票結果:", - "notifications.column_settings.push": "推送通知", - "notifications.column_settings.reblog": "轉嘟:", - "notifications.column_settings.show": "在欄位中顯示", - "notifications.column_settings.sound": "播放音效", - "notifications.column_settings.sounds": "Sounds", - "notifications.column_settings.sounds.all_sounds": "Play sound for all notifications", - "notifications.column_settings.title": "Notification settings", + "notification.mentioned": "{name} 提到了你", + "notification.move": "{name} 移動到了 {targetName}", + "notification.others": " + {count} 其他通知", + "notification.pleroma:chat_mention": "{name} 給你發送了訊息", + "notification.pleroma:emoji_reaction": "{name} 用表情回應了你的帖文", + "notification.poll": "你參與的一項投票已經結束", + "notification.reblog": "{name} 轉帖了你的帖文", + "notification.status": "{name} 剛剛發帖", + "notification.update": "{name} 編輯了你參與互動的帖文", + "notification.user_approved": "歡迎來到 {instance}!", "notifications.filter.all": "全部", - "notifications.filter.boosts": "轉嘟", - "notifications.filter.emoji_reacts": "Emoji reacts", + "notifications.filter.boosts": "轉帖", + "notifications.filter.emoji_reacts": "Emoji心情回應", "notifications.filter.favourites": "最愛", - "notifications.filter.follows": "關注的使用者", + "notifications.filter.follows": "追蹤的使用者", "notifications.filter.mentions": "提及", - "notifications.filter.moves": "Moves", + "notifications.filter.moves": "移動", "notifications.filter.polls": "投票結果", - "notifications.filter.statuses": "Updates from people you follow", + "notifications.filter.statuses": "來自追蹤的人的更新", "notifications.group": "{count} 條通知", - "notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}", - "onboarding.avatar.subtitle": "Just have fun with it.", - "onboarding.avatar.title": "Choose a profile picture", - "onboarding.display_name.subtitle": "You can always edit this later.", - "onboarding.display_name.title": "Choose a display name", - "onboarding.done": "Done", - "onboarding.finished.message": "We are very excited to welcome you to our community! Tap the button below to get started.", - "onboarding.finished.title": "Onboarding complete", - "onboarding.header.subtitle": "This will be shown at the top of your profile.", - "onboarding.header.title": "Pick a cover image", - "onboarding.next": "Next", - "onboarding.note.subtitle": "You can always edit this later.", - "onboarding.note.title": "Write a short bio", - "onboarding.saving": "Saving…", - "onboarding.skip": "Skip for now", - "onboarding.suggestions.subtitle": "Here are a few of the most popular accounts you might like.", - "onboarding.suggestions.title": "Suggested accounts", - "onboarding.view_feed": "View Feed", - "password_reset.confirmation": "Check your email for confirmation.", - "password_reset.fields.username_placeholder": "Email or username", - "password_reset.reset": "Reset password", - "patron.donate": "Donate", - "patron.title": "Funding Goal", - "pinned_accounts.title": "{name}’s choices", - "pinned_statuses.none": "No pins to show.", + "notifications.queue_label": "點擊查看 {count} 條新通知", + "oauth_consumer.tooltip": "通過 {provider} 登入", + "oauth_consumers.title": "更多登入方式", + "onboarding.avatar.subtitle": "祝你玩得開心!", + "onboarding.avatar.title": "設定你的頭像", + "onboarding.display_name.subtitle": "你可以稍後更改", + "onboarding.display_name.title": "設定你的顯示名稱", + "onboarding.done": "完成", + "onboarding.finished.message": "我們很高興歡迎您加入我們的社區! 點擊下面的按鈕,讓我們開始吧!", + "onboarding.finished.title": "新手教程完成", + "onboarding.header.subtitle": "這將顯示在你個人資料的上方。", + "onboarding.header.title": "選擇封面圖片", + "onboarding.next": "下一步", + "onboarding.note.subtitle": "你可以稍後更改", + "onboarding.note.title": "簡短地介紹下自己", + "onboarding.saving": "儲存中…", + "onboarding.skip": "現在跳過", + "onboarding.suggestions.subtitle": "以下是幾個受歡迎的帳戶,你可能會喜歡", + "onboarding.suggestions.title": "推薦帳戶", + "onboarding.view_feed": "檢視列表", + "password_reset.confirmation": "請查閲確認電郵", + "password_reset.fields.username_placeholder": "電郵地址或帳戶名稱", + "password_reset.reset": "重設密碼", + "patron.donate": "抖內", + "patron.title": "籌集目標", + "pinned_accounts.title": "{name} 的釘選", + "pinned_statuses.none": "沒有釘選的帖文", + "poll.choose_multiple": "盡情選擇你所感興趣的", "poll.closed": "已關閉", + "poll.non_anonymous": "公共投票", + "poll.non_anonymous.label": "其他的實例可能可以見到你投票的選擇", "poll.refresh": "重新整理", - "poll.total_votes": "{count, plural, one {# 個投票} other {# 個投票}}", + "poll.total_people": "還有 {count} 人", + "poll.total_votes": "投票", "poll.vote": "投票", - "poll.voted": "You voted for this answer", - "poll.votes": "{votes, plural, one {# vote} other {# votes}}", - "poll_button.add_poll": "建立投票", + "poll.voted": "你投票給了這個選項", + "poll.votes": "投票", + "poll_button.add_poll": "發起投票", "poll_button.remove_poll": "移除投票", - "pre_header.close": "Close", - "preferences.fields.auto_play_gif_label": "Auto-play animated GIFs", - "preferences.fields.autoload_more_label": "Automatically load more items when scrolled to the bottom of the page", - "preferences.fields.autoload_timelines_label": "Automatically load new posts when scrolled to the top of the page", - "preferences.fields.boost_modal_label": "Show confirmation dialog before reposting", - "preferences.fields.delete_modal_label": "Show confirmation dialog before deleting a post", - "preferences.fields.display_media.default": "Hide media marked as sensitive", - "preferences.fields.display_media.hide_all": "Always hide media", - "preferences.fields.display_media.show_all": "Always show media", - "preferences.fields.expand_spoilers_label": "Always expand posts marked with content warnings", - "preferences.fields.language_label": "Language", - "preferences.fields.media_display_label": "Media display", - "preferences.hints.feed": "In your home feed", + "preferences.fields.auto_play_gif_label": "自動播放GIF", + "preferences.fields.autoload_more_label": "滾動至時間軸底部自動載入更多帖文", + "preferences.fields.autoload_timelines_label": "滾動至時間軸頂部自動載入更多新帖", + "preferences.fields.boost_modal_label": "轉帖前顯示確認提示", + "preferences.fields.delete_modal_label": "刪除帖文前顯示確認提示", + "preferences.fields.display_media.default": "隱藏被標記為敏感內容的媒體", + "preferences.fields.display_media.hide_all": "始終隱藏所有媒體", + "preferences.fields.display_media.show_all": "始終顯示所有媒體", + "preferences.fields.expand_spoilers_label": "始終展開標有內容警告的帖文", + "preferences.fields.language_label": "語言", + "preferences.fields.media_display_label": "媒體顯示", + "preferences.fields.theme": "主題", + "preferences.hints.feed": "在你的主頁信息流中", "privacy.change": "調整隱私狀態", "privacy.direct.long": "只有被提到的使用者能看到", - "privacy.direct.short": "私訊", - "privacy.private.long": "只有關注你的使用者能看到", - "privacy.private.short": "僅關注者", - "privacy.public.long": "嘟到公開時間軸", - "privacy.public.short": "公開", - "privacy.unlisted.long": "公開,但不會顯示在公開時間軸", - "privacy.unlisted.short": "不公開", - "profile_dropdown.add_account": "Add an existing account", - "profile_dropdown.logout": "Log out @{acct}", - "profile_fields_panel.title": "Profile fields", - "public.column_settings.title": "Fediverse timeline settings", - "reactions.all": "All", + "privacy.direct.short": "僅被提及的使用者", + "privacy.private.long": "只有追蹤你的使用者能看到", + "privacy.private.short": "僅追蹤者", + "privacy.public.long": "所有人可見,出現在公共時間軸上", + "privacy.public.short": "公共時間軸", + "privacy.unlisted.long": "公開,但不會顯示在公共時間軸", + "privacy.unlisted.short": "所有人", + "profile_dropdown.add_account": "新增已有帳戶", + "profile_dropdown.logout": "登出 @{acct}", + "profile_dropdown.theme": "主題", + "profile_fields_panel.title": "個人資料字段", + "public.column_settings.title": "聯邦宇宙工具時間軸設定", + "reactions.all": "全部", "regeneration_indicator.label": "載入中…", - "regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!", - "register_invite.lead": "Complete the form below to create an account.", - "register_invite.title": "You've been invited to join {siteTitle}!", - "registration.agreement": "I agree to the {tos}.", - "registration.captcha.hint": "Click the image to get a new captcha", - "registration.closed_message": "{instance} is not accepting new members", - "registration.closed_title": "Registrations Closed", - "registration.confirmation_modal.close": "Close", - "registration.fields.confirm_placeholder": "Password (again)", - "registration.fields.email_placeholder": "E-Mail address", - "registration.fields.password_placeholder": "Password", - "registration.fields.username_hint": "Only letters, numbers, and underscores are allowed.", - "registration.fields.username_placeholder": "Username", - "registration.newsletter": "Subscribe to newsletter.", - "registration.password_mismatch": "Passwords don't match.", - "registration.reason": "Why do you want to join?", - "registration.reason_hint": "This will help us review your application", - "registration.sign_up": "Sign up", - "registration.tos": "Terms of Service", - "registration.privacy": "Privacy Policy", - "registration.acceptance": "By registering, you agree to the {terms} and {privacy}.", - "registration.username_unavailable": "Username is already taken.", - "relative_time.days": "{number} 天", - "relative_time.hours": "{number} 小時", + "regeneration_indicator.sublabel": "你的主頁時間軸正在準備中!", + "register_invite.lead": "填寫下列表單以註冊帳戶", + "register_invite.title": "你已被邀請加入 {siteTitle}!", + "registration.acceptance": "註冊同時意味着你已經同意本站的 {terms} 和 {privacy}.", + "registration.agreement": "我同意本站用戶條款 {tos}.", + "registration.captcha.hint": "點擊圖像以重載驗證碼", + "registration.captcha.placeholder": "輸入照片中顯示的文字", + "registration.closed_message": "{instance} 公開註冊暫時關閉", + "registration.closed_title": "暫停註冊", + "registration.confirmation_modal.close": "關閉", + "registration.fields.confirm_placeholder": "再次輸入密碼", + "registration.fields.email_placeholder": "電郵地址", + "registration.fields.password_placeholder": "密碼", + "registration.fields.username_hint": "只能使用英文字母、數字和下橫線", + "registration.fields.username_placeholder": "帳戶名稱", + "registration.header": "創建你的帳戶", + "registration.newsletter": "訂閲我們的新聞郵件", + "registration.password_mismatch": "密碼不匹配", + "registration.privacy": "隱私條款", + "registration.reason": "你為什麼想要註冊本站?", + "registration.reason_hint": "認真填寫將能加快你通過註冊的速度", + "registration.sign_up": "註冊", + "registration.tos": "用戶條款", + "registration.username_unavailable": "帳戶名稱已被使用", + "registration.validation.capital_letter": "1 個大寫字母", + "registration.validation.lowercase_letter": "1 個小寫字母", + "registration.validation.minimum_characters": "8 個字符", + "registrations.create_account": "創建新帳戶", + "registrations.error": "創建你的帳戶失敗,請聯絡管理員.", + "registrations.get_started": "讓我們開始吧!", + "registrations.success": "歡迎來到 {siteTitle}!", + "registrations.tagline": "一個沒有言論審查的社交平台", + "registrations.unprocessable_entity": "此用戶名已被佔用", + "registrations.username.hint": "只能包含數字、字母和下橫線", + "relative_time.days": "{number}天", + "relative_time.hours": "{number}小時", "relative_time.just_now": "剛剛", - "relative_time.minutes": "{number} 分", - "relative_time.seconds": "{number} 秒", - "remote_instance.edit_federation": "Edit federation", - "remote_instance.federation_panel.heading": "Federation Restrictions", - "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} has placed no restrictions on {host}.", - "remote_instance.federation_panel.restricted_message": "{siteTitle} blocks all activities from {host}.", - "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} has placed some restrictions on {host}.", - "remote_instance.pin_host": "Pin {host}", - "remote_instance.unpin_host": "Unpin {host}", - "remote_interaction.account_placeholder": "Enter your username@domain you want to act from", - "remote_interaction.divider": "or", - "remote_interaction.favourite": "Proceed to like", - "remote_interaction.favourite_title": "Like a post remotely", - "remote_interaction.follow": "Proceed to follow", - "remote_interaction.follow_title": "Follow {user} remotely", - "remote_interaction.poll_vote": "Proceed to vote", - "remote_interaction.poll_vote_title": "Vote in a poll remotely", - "remote_interaction.reblog": "Proceed to repost", - "remote_interaction.reblog_title": "Reblog a post remotely", - "remote_interaction.reply": "Proceed to reply", - "remote_interaction.reply_title": "Reply to a post remotely", - "remote_interaction.user_not_found_error": "Couldn't find given user", - "remote_timeline.filter_message": "You are viewing the timeline of {instance}.", + "relative_time.minutes": "{number}分", + "relative_time.seconds": "{number}秒", + "remote_instance.edit_federation": "編輯聯邦設定", + "remote_instance.federation_panel.heading": "聯邦限制", + "remote_instance.federation_panel.no_restrictions_message": "{siteTitle} 未對 {host} 採取限制措施", + "remote_instance.federation_panel.restricted_message": "{siteTitle} 完全封鎖了 {host}", + "remote_instance.federation_panel.some_restrictions_message": "{siteTitle} 對 {host} 實施了部分限制", + "remote_instance.pin_host": "釘選 {host}", + "remote_instance.unpin_host": "取消釘選 {host}", + "remote_interaction.account_placeholder": "輸入您想採取行動的帳戶 (格式:帳戶名@網域)", + "remote_interaction.divider": "或", + "remote_interaction.favourite": "按讚", + "remote_interaction.favourite_title": "遠端按讚一條帖文", + "remote_interaction.follow": "開始追蹤", + "remote_interaction.follow_title": "遠端追蹤 {user}", + "remote_interaction.poll_vote": "投票", + "remote_interaction.poll_vote_title": "遠端參與投票", + "remote_interaction.reblog": "轉帖", + "remote_interaction.reblog_title": "遠端轉帖", + "remote_interaction.reply": "回覆", + "remote_interaction.reply_title": "遠端回覆", + "remote_interaction.user_not_found_error": "找不到該帳戶", + "remote_timeline.filter_message": "你正在查看 {instance} 的時間軸", "reply_indicator.cancel": "取消", - "reply_mentions.account.add": "Add to mentions", - "reply_mentions.account.remove": "Remove from mentions", - "reply_mentions.reply_empty": "Replying to post", - "report.block": "Block {target}", - "report.block_hint": "Do you also want to block this account?", + "reply_mentions.account.add": "添加到提及名單", + "reply_mentions.account.remove": "從提及名單中移除", + "reply_mentions.more": "添加 {count} 個", + "reply_mentions.reply": "回覆 {accounts}{more}", + "reply_mentions.reply_empty": "回覆帖文", + "report.block": "封鎖帳戶 {target}", + "report.block_hint": "你是否要封鎖這個帳戶呢?", + "report.confirmation.content": "如果我們發現此帳戶確實違反了 {link} ,我們會採取進一步的措施", + "report.confirmation.title": "感謝你提交的檢舉", + "report.done": "完成", "report.forward": "轉寄到 {target}", - "report.forward_hint": "這個帳戶屬於其他站點。要像該站點發送匿名的檢舉訊息嗎?", + "report.forward_hint": "這個帳戶屬於其他站點。要像該站點發送匿名的檢舉訊息嗎?", "report.hint": "這項訊息會發送到您伺服器的管理員。你可以提供檢舉這個帳戶的理由:", + "report.next": "下一步", + "report.otherActions.addAdditional": "你還想為你的檢舉添加更多狀態嗎?", + "report.otherActions.addMore": "添加更多", + "report.otherActions.furtherActions": "進一步措施:", + "report.otherActions.hideAdditional": "隱藏額外狀態", + "report.otherActions.otherStatuses": "包含其他狀態?", "report.placeholder": "更多訊息", + "report.reason.blankslate": "你已移除選中的所有狀態", + "report.reason.title": "檢舉原因", "report.submit": "送出", "report.target": "檢舉 {target}", - "reset_password.header": "Set New Password", - "schedule.post_time": "Post Date/Time", - "schedule.remove": "Remove schedule", - "schedule_button.add_schedule": "Schedule post for later", - "schedule_button.remove_schedule": "Post immediately", - "scheduled_status.cancel": "Cancel", - "search.action": "Search for “{query}”", + "reset_password.fail": "令牌已過期,請重試", + "reset_password.header": "設定新的密碼", + "schedule.post_time": "發佈時間", + "schedule.remove": "取消發佈", + "schedule_button.add_schedule": "定時發佈", + "schedule_button.remove_schedule": "取消定時發佈", + "scheduled_status.cancel": "取消", + "search.action": "搜尋 “{query}”", "search.placeholder": "搜尋", "search_results.accounts": "使用者", "search_results.hashtags": "主題標籤", - "search_results.statuses": "嘟文", - "search_results.top": "Top", - "security.codes.fail": "Failed to fetch backup codes", - "security.confirm.fail": "Incorrect code or password. Try again.", - "security.delete_account.fail": "Account deletion failed.", - "security.delete_account.success": "Account successfully deleted.", - "security.disable.fail": "Incorrect password. Try again.", - "security.disable_mfa": "Disable", - "security.fields.email.label": "Email address", - "security.fields.new_password.label": "New password", - "security.fields.old_password.label": "Current password", - "security.fields.password.label": "Password", - "security.fields.password_confirmation.label": "New password (again)", - "security.headers.delete": "Delete Account", - "security.headers.tokens": "Sessions", - "security.headers.update_email": "Change Email", - "security.headers.update_password": "Change Password", - "security.mfa": "Set up 2-Factor Auth", - "security.mfa_enabled": "You have multi-factor authentication set up with OTP.", - "security.mfa_header": "Authorization Methods", - "security.mfa_setup_hint": "Configure multi-factor authentication with OTP", - "security.qr.fail": "Failed to fetch setup key", - "security.submit": "Save changes", - "security.submit.delete": "Delete Account", - "security.text.delete": "To delete your account, enter your password then click Delete Account. This is a permanent action that cannot be undone. Your account will be destroyed from this server, and a deletion request will be sent to other servers. It's not guaranteed that all servers will purge your account.", - "security.tokens.revoke": "Revoke", - "security.update_email.fail": "Update email failed.", - "security.update_email.success": "Email successfully updated.", - "security.update_password.fail": "Update password failed.", - "security.update_password.success": "Password successfully updated.", - "settings.change_email": "Change Email", - "settings.change_password": "Change Password", - "settings.configure_mfa": "Configure MFA", - "settings.delete_account": "Delete Account", - "settings.edit_profile": "Edit Profile", - "settings.preferences": "Preferences", - "settings.profile": "Profile", - "settings.save.success": "Your preferences have been saved!", - "settings.security": "Security", - "settings.settings": "Settings", - "signup_panel.subtitle": "Sign up now to discuss what's happening.", - "signup_panel.title": "New to {site_title}?", - "snackbar.view": "View", - "soapbox_config.authenticated_profile_hint": "Users must be logged-in to view replies and media on user profiles.", - "soapbox_config.authenticated_profile_label": "Profiles require authentication", - "soapbox_config.copyright_footer.meta_fields.label_placeholder": "Copyright footer", - "soapbox_config.crypto_address.meta_fields.address_placeholder": "Address", - "soapbox_config.crypto_address.meta_fields.note_placeholder": "Note (optional)", - "soapbox_config.crypto_address.meta_fields.ticker_placeholder": "Ticker", - "soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder": "Number of items to display in the crypto homepage widget", + "search_results.statuses": "帖文", + "security.codes.fail": "恢復代碼錯誤", + "security.confirm.fail": "密碼錯誤,請重試。", + "security.delete_account.fail": "帳戶刪除失敗", + "security.delete_account.success": "帳戶刪除成功", + "security.disable.fail": "密碼錯誤,請重試。", + "security.fields.email.label": "電郵地址", + "security.fields.new_password.label": "輸入新密碼", + "security.fields.old_password.label": "輸入原密碼", + "security.fields.password.label": "密碼", + "security.fields.password_confirmation.label": "再次輸入新密碼", + "security.headers.delete": "刪除帳戶", + "security.headers.tokens": "會話", + "security.qr.fail": "加載密鑰失敗", + "security.submit": "儲存變更", + "security.submit.delete": "刪除帳戶", + "security.text.delete": "要刪除您的帳戶,請輸入您的密碼。注意:這是無法撤消的永久性操作!您的帳戶將從該服務器中銷毀,並將向其他服務器發送刪除請求。但你的資訊不一定會在其他站點上立即刪除。", + "security.tokens.revoke": "撤銷", + "security.update_email.fail": "更新電郵地址失敗", + "security.update_email.success": "電郵地址已更新", + "security.update_password.fail": "更新密碼失敗", + "security.update_password.success": "密碼已更新", + "settings.change_email": "更改電郵地址", + "settings.change_password": "更改密碼", + "settings.configure_mfa": "設定多重要素驗證 (MFA)", + "settings.delete_account": "刪除帳戶", + "settings.edit_profile": "編輯個人資料", + "settings.preferences": "首選項", + "settings.profile": "個人資料", + "settings.save.success": "你的設定已儲存", + "settings.security": "安全性", + "settings.sessions": "活動會話", + "settings.settings": "設定", + "shared.tos": "服務條款", + "signup_panel.subtitle": "註冊以參與討論", + "signup_panel.title": "初來乍到 {site_title} 嗎?", + "site_preview.preview": "預覽", + "snackbar.view": "檢視", + "soapbox_config.authenticated_profile_hint": "用戶必須登錄才能查看用戶個人資料上的回覆和媒體。", + "soapbox_config.authenticated_profile_label": "個人資料需要授權才能查看", + "soapbox_config.copyright_footer.meta_fields.label_placeholder": "版權頁底", + "soapbox_config.crypto_address.meta_fields.address_placeholder": "地址", + "soapbox_config.crypto_address.meta_fields.note_placeholder": "備註 (可選)", + "soapbox_config.crypto_address.meta_fields.ticker_placeholder": "幣種", + "soapbox_config.crypto_donate_panel_limit.meta_fields.limit_placeholder": "在主頁數字貨幣小部件中顯示的數量", "soapbox_config.custom_css.meta_fields.url_placeholder": "URL", - "soapbox_config.display_fqn_label": "Display domain (eg @user@domain) for local accounts.", - "soapbox_config.fields.accent_color_label": "Accent color", - "soapbox_config.fields.brand_color_label": "Brand color", - "soapbox_config.fields.crypto_address.add": "Add new crypto address", - "soapbox_config.fields.crypto_addresses_label": "Cryptocurrency addresses", - "soapbox_config.fields.home_footer.add": "Add new Home Footer Item", - "soapbox_config.fields.home_footer_fields_label": "Home footer items", + "soapbox_config.display_fqn_label": "顯示本站帳戶的網域 (如 @帳戶名稱@網域) ", + "soapbox_config.fields.accent_color_label": "強調色", + "soapbox_config.fields.brand_color_label": "主題色", + "soapbox_config.fields.crypto_address.add": "添加數字貨幣地址", + "soapbox_config.fields.crypto_addresses_label": "數字貨幣地址", + "soapbox_config.fields.home_footer.add": "添加頁尾", + "soapbox_config.fields.home_footer_fields_label": "主頁頁眉", "soapbox_config.fields.logo_label": "Logo", - "soapbox_config.fields.promo_panel.add": "Add new Promo panel item", - "soapbox_config.fields.promo_panel_fields_label": "Promo panel items", - "soapbox_config.fields.theme_label": "Default theme", - "soapbox_config.greentext_label": "Enable greentext support", - "soapbox_config.hints.crypto_addresses": "Add cryptocurrency addresses so users of your site can donate to you. Order matters, and you must use lowercase ticker values.", - "soapbox_config.hints.home_footer_fields": "You can have custom defined links displayed on the footer of your static pages", - "soapbox_config.hints.logo": "SVG. At most 2 MB. Will be displayed to 50px height, maintaining aspect ratio", - "soapbox_config.hints.promo_panel_fields": "You can have custom defined links displayed on the right panel of the timelines page.", + "soapbox_config.fields.promo_panel.add": "添加時間軸頁底", + "soapbox_config.fields.promo_panel_fields_label": "時間軸頁底", + "soapbox_config.fields.theme_label": "默認主題", + "soapbox_config.greentext_label": "啟用greentext支援", + "soapbox_config.headings.advanced": "進階", + "soapbox_config.headings.cryptocurrency": "數字貨幣", + "soapbox_config.headings.navigation": "導航列", + "soapbox_config.headings.options": "選項", + "soapbox_config.headings.theme": "主題", + "soapbox_config.hints.crypto_addresses": "添加加密貨幣地址,以便您網站的用戶可以向您捐款。請注意順序,同時您必須使用小寫的幣種代碼。", + "soapbox_config.hints.home_footer_fields": "您可以在靜態頁面的頁腳顯示自定義鏈接(如about)。", + "soapbox_config.hints.logo": "SVG. 最多 2 MB。 將顯示到 50px 高度,保持縱橫比", + "soapbox_config.hints.promo_panel_fields": "您可以在時間線頁面的右側面板上顯示自定義鏈接。", "soapbox_config.hints.promo_panel_icons": "{ link }", - "soapbox_config.hints.promo_panel_icons.link": "Soapbox Icons List", - "soapbox_config.home_footer.meta_fields.label_placeholder": "Label", + "soapbox_config.hints.promo_panel_icons.link": "Soapbox圖示", + "soapbox_config.home_footer.meta_fields.label_placeholder": "標籤", "soapbox_config.home_footer.meta_fields.url_placeholder": "URL", - "soapbox_config.promo_panel.meta_fields.icon_placeholder": "Icon", - "soapbox_config.promo_panel.meta_fields.label_placeholder": "Label", + "soapbox_config.promo_panel.meta_fields.icon_placeholder": "圖示", + "soapbox_config.promo_panel.meta_fields.label_placeholder": "標籤", "soapbox_config.promo_panel.meta_fields.url_placeholder": "URL", - "soapbox_config.raw_json_hint": "Edit the settings data directly. Changes made directly to the JSON file will override the form fields above. Click Save to apply your changes.", - "soapbox_config.raw_json_label": "Advanced: Edit raw JSON data", - "soapbox_config.save": "Save", - "soapbox_config.saved": "Soapbox config saved!", - "soapbox_config.single_user_mode_hint": "Front page will redirect to a given user profile.", - "soapbox_config.single_user_mode_label": "Single user mode", - "soapbox_config.single_user_mode_profile_hint": "@handle", - "soapbox_config.single_user_mode_profile_label": "Main user handle", - "soapbox_config.verified_can_edit_name_label": "Allow verified users to edit their own display name.", - "status.actions.more": "More", + "soapbox_config.raw_json_hint": "直接編輯設置數據。 直接對 JSON 文件進行的更改將覆蓋上面的表單字段。 單擊保存以應用您的更改。", + "soapbox_config.raw_json_label": "高級: 編輯原始JSON", + "soapbox_config.save": "儲存更改", + "soapbox_config.saved": "Soapbox配置已儲存!", + "soapbox_config.single_user_mode_hint": "首頁將重定向到給定的用戶配置文件。", + "soapbox_config.single_user_mode_label": "單用戶模式", + "soapbox_config.single_user_mode_profile_hint": "@帳戶名稱", + "soapbox_config.single_user_mode_profile_label": "主帳戶的帳戶名稱", + "soapbox_config.verified_can_edit_name_label": "允許經過驗證的用戶編輯自己的顯示名稱。", + "sponsored.info.message": "{siteTitle} 展示廣告以使實例可持續運行", + "sponsored.info.title": "為什麼我會看到這條廣告?", + "sponsored.subtitle": "贊助", + "status.actions.more": "更多", "status.admin_account": "開啟 @{name} 的管理介面", - "status.admin_status": "在管理介面開啟此嘟文", + "status.admin_status": "在管理介面開啟此帖文", "status.block": "封鎖 @{name}", - "status.bookmark": "Bookmark", - "status.bookmarked": "Bookmark added.", - "status.cancel_reblog_private": "取消轉嘟", - "status.cannot_reblog": "這篇嘟文無法被轉嘟", - "status.chat": "Chat with @{name}", - "status.copy": "將連結複製到嘟文中", + "status.bookmark": "書籤", + "status.bookmarked": "書籤已添加", + "status.cancel_reblog_private": "取消轉帖", + "status.cannot_reblog": "這篇帖文無法被轉載", + "status.chat": "和 @{name} 聊天", + "status.copy": "將連結複製到帖文中", "status.delete": "刪除", "status.detailed_status": "對話的詳細內容", "status.direct": "發送私訊給 @{name}", + "status.edit": "編輯", + "status.edited": "已於 {date} 編輯", "status.embed": "嵌入", "status.favourite": "最愛", "status.filtered": "已過濾", @@ -983,89 +1019,102 @@ "status.media_hidden": "隱藏媒體內容", "status.mention": "提到 @{name}", "status.more": "更多", - "status.mute": "靜音 @{name}", - "status.mute_conversation": "靜音對話", - "status.open": "展開嘟文", + "status.mute": "隱藏 @{name}", + "status.mute_conversation": "隱藏對話", + "status.open": "展開帖文", "status.pin": "釘選到個人資料頁", - "status.pinned": "釘選的嘟文", - "status.quote": "Quote post", - "status.reactions.cry": "Sad", - "status.reactions.empty": "No one has reacted to this post yet. When someone does, they will show up here.", - "status.reactions.heart": "Love", - "status.reactions.laughing": "Haha", - "status.reactions.like": "Like", - "status.reactions.open_mouth": "Wow", - "status.reactions.weary": "Weary", - "status.reactions_expand": "Select emoji", + "status.pinned": "釘選的帖文", + "status.quote": "引用帖文", + "status.reactions.cry": "傷心", + "status.reactions.empty": "尚未有人回應心情", + "status.reactions.heart": "愛", + "status.reactions.laughing": "哈哈哈", + "status.reactions.like": "讚哦", + "status.reactions.open_mouth": "哇!", + "status.reactions.weary": "疲憊", + "status.reactions_expand": "選擇心情", "status.read_more": "閱讀更多", - "status.reblog": "轉嘟", - "status.reblog_private": "轉嘟給原有關注者", - "status.reblogged_by": "{name} 轉嘟了", - "status.reblogs.empty": "還沒有人轉嘟。如果有,會顯示在這裡。", + "status.reblog": "轉帖", + "status.reblog_private": "轉帖給原有追蹤者", + "status.reblogged_by": "{name} 轉帖了", + "status.reblogs.empty": "還沒有人轉帖。如果有,會顯示在這裡。", "status.redraft": "刪除 & 編輯", - "status.remove_account_from_group": "Remove account from group", - "status.remove_post_from_group": "Remove post from group", + "status.remove_account_from_group": "將帳戶移出群組", + "status.remove_post_from_group": "將帖文移出群組", "status.reply": "回覆", "status.replyAll": "回覆所有人", "status.report": "檢舉 @{name}", "status.sensitive_warning": "敏感內容", "status.share": "分享", "status.show_less": "減少顯示", - "status.show_less_all": "減少顯示這類嘟文", + "status.show_less_all": "減少顯示這類帖文", "status.show_more": "顯示更多", - "status.show_more_all": "顯示更多這類嘟文", - "status.title": "Post", - "status.title_direct": "Direct message", - "status.unbookmark": "Remove bookmark", - "status.unbookmarked": "Bookmark removed.", - "status.unmute_conversation": "解除此對話的靜音", - "status.unpin": "解除置頂", - "status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}", - "statuses.quote_tombstone": "Post is unavailable.", - "statuses.tombstone": "One or more posts are unavailable.", + "status.show_more_all": "顯示更多這類帖文", + "status.title": "帖文", + "status.title_direct": "私訊", + "status.unbookmark": "移除書籤", + "status.unbookmarked": "書籤已移除", + "status.unmute_conversation": "解除此對話的隱藏", + "status.unpin": "解除釘選", + "status.sensitive_warning.subtitle": "這則貼文可能含有不宜觀看的消息", + "status.sensitive_warning.action": "顯示", + "status_list.queue_label": "點選查看 {count} 個新帖文", + "statuses.quote_tombstone": "帖文不可用", + "statuses.tombstone": "部分帖文不可見", + "streamfield.add": "新增", + "streamfield.remove": "移除", "suggestions.dismiss": "關閉建議", - "tabs_bar.all": "All", - "tabs_bar.chats": "Chats", - "tabs_bar.dashboard": "Dashboard", - "tabs_bar.fediverse": "Fediverse", + "tabs_bar.all": "全部", + "tabs_bar.chats": "對話", + "tabs_bar.dashboard": "控制台", + "tabs_bar.fediverse": "聯邦宇宙", "tabs_bar.home": "主頁", - "tabs_bar.more": "More", + "tabs_bar.more": "更多", "tabs_bar.notifications": "通知", - "tabs_bar.post": "Post", - "tabs_bar.profile": "Profile", + "tabs_bar.profile": "個人資料", "tabs_bar.search": "搜尋", - "tabs_bar.settings": "Settings", - "tabs_bar.theme_toggle_dark": "Switch to dark theme", - "tabs_bar.theme_toggle_light": "Switch to light theme", + "tabs_bar.settings": "設定", + "tabs_bar.switch_accounts": "切換帳戶", + "tabs_bar.theme_toggle_dark": "切換為深色主題", + "tabs_bar.theme_toggle_light": "切換為淺色主題", + "theme_toggle.dark": "深色", + "theme_toggle.light": "淺色", + "theme_toggle.system": "系統", + "thread_login.login": "登入", + "thread_login.message": "加入 {siteTitle} 以獲取完整資訊", + "thread_login.signup": "註冊", + "thread_login.title": "繼續這個對話", "time_remaining.days": "剩餘{number, plural, one {# 天數} other {# 天數}}", "time_remaining.hours": "剩餘{number, plural, one {# 小時} other {# 小時}}", "time_remaining.minutes": "剩餘{number, plural, one {# 分鐘} other {# 分鐘}}", "time_remaining.moments": "剩餘時間", "time_remaining.seconds": "剩餘 {number, plural, one {# 秒} other {# 秒}}", "trends.count_by_accounts": "{count} 位使用者在討論", - "trends.title": "Trends", - "ui.beforeunload": "如果離開 Soapbox,你的草稿將會不見。", - "unauthorized_modal.text": "You need to be logged in to do that.", - "unauthorized_modal.title": "Sign up for {site_title}", + "trends.title": "趨勢", + "trendsPanel.viewAll": "顯示全部", + "ui.beforeunload": "如果離開,你的草稿將會丟失。", + "unauthorized_modal.text": "你需要登入才能繼續", + "unauthorized_modal.title": "註冊 {site_title} 帳戶", "upload_area.title": "拖放來上傳", "upload_button.label": "上傳媒體檔案 (JPEG, PNG, GIF, WebM, MP4, MOV)", - "upload_error.image_size_limit": "Image exceeds the current file size limit ({limit})", + "upload_error.image_size_limit": "圖片超出當前文件大小限制 ({limit})", "upload_error.limit": "已達到檔案上傳限制。", "upload_error.poll": "不允許在投票上傳檔案。", - "upload_error.video_size_limit": "Video exceeds the current file size limit ({limit})", + "upload_error.video_duration_limit": "影片超出當前時長限制 ({limit} 秒)", + "upload_error.video_size_limit": "影片超出當前文件大小限制 ({limit})", "upload_form.description": "為視障人士增加文字說明", - "upload_form.preview": "Preview", + "upload_form.preview": "預覽", "upload_form.undo": "刪除", "upload_progress.label": "上傳中...", "video.close": "關閉影片", - "video.download": "Download file", + "video.download": "下載", "video.exit_fullscreen": "退出全螢幕", "video.expand": "展開影片", "video.fullscreen": "全螢幕", "video.hide": "隱藏影片", - "video.mute": "靜音", + "video.mute": "隱藏", "video.pause": "暫停", "video.play": "播放", - "video.unmute": "解除靜音", - "who_to_follow.title": "Who To Follow" + "video.unmute": "解除隱藏", + "who_to_follow.title": "推薦追蹤" } From 245f6b9678caaae6b682285b9e1b521fc1840a19 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 4 Nov 2022 11:19:35 -0500 Subject: [PATCH 29/48] Fix link previews in statuses --- app/soapbox/components/quoted-status.tsx | 2 +- app/soapbox/components/status.tsx | 2 +- app/soapbox/features/status/components/detailed-status.tsx | 2 +- app/styles/components/status.scss | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index 9755e7120..b521c1172 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -116,7 +116,7 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => collapsable /> - {(status.media_attachments.size > 0) && ( + {(status.card || status.media_attachments.size > 0) && ( = (props) => { collapsable /> - {(quote || actualStatus.media_attachments.size > 0) && ( + {(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && ( = ({ - {(quote || actualStatus.media_attachments.size > 0) && ( + {(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && ( Date: Fri, 4 Nov 2022 12:20:55 -0500 Subject: [PATCH 30/48] Fix account.mute_expires_at type --- app/soapbox/normalizers/account.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts index bf58f1c81..97c640886 100644 --- a/app/soapbox/normalizers/account.ts +++ b/app/soapbox/normalizers/account.ts @@ -42,7 +42,7 @@ export const AccountRecord = ImmutableRecord({ location: '', locked: false, moved: null as EmbeddedEntity, - mute_expires_at: null as Date | null, + mute_expires_at: null as string | null, note: '', pleroma: ImmutableMap(), source: ImmutableMap(), From de4f1aaabf5a70af57f2d87b657c1bba9d09daf2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 4 Nov 2022 13:02:23 -0500 Subject: [PATCH 31/48] Notifications: exclude chat messages from "All" filter, improve exclude_types fallback --- app/soapbox/actions/notifications.ts | 6 ++++-- app/soapbox/utils/notification.ts | 7 +++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/soapbox/actions/notifications.ts b/app/soapbox/actions/notifications.ts index 4edd76c22..db8fd688f 100644 --- a/app/soapbox/actions/notifications.ts +++ b/app/soapbox/actions/notifications.ts @@ -11,7 +11,7 @@ import { getFilters, regexFromFilters } from 'soapbox/selectors'; import { isLoggedIn } from 'soapbox/utils/auth'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features'; import { unescapeHTML } from 'soapbox/utils/html'; -import { NOTIFICATION_TYPES } from 'soapbox/utils/notification'; +import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from 'soapbox/utils/notification'; import { joinPublicPath } from 'soapbox/utils/static'; import { fetchRelationships } from './accounts'; @@ -195,7 +195,9 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an if (activeFilter === 'all') { if (features.notificationsIncludeTypes) { - params.types = NOTIFICATION_TYPES; + params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any)); + } else { + params.exclude_types = EXCLUDE_TYPES; } } else { if (features.notificationsIncludeTypes) { diff --git a/app/soapbox/utils/notification.ts b/app/soapbox/utils/notification.ts index 907a97434..49c360b92 100644 --- a/app/soapbox/utils/notification.ts +++ b/app/soapbox/utils/notification.ts @@ -14,6 +14,12 @@ const NOTIFICATION_TYPES = [ 'update', ] as const; +/** Notification types to exclude from the "All" filter by default. */ +const EXCLUDE_TYPES = [ + 'pleroma:chat_mention', + 'chat', // TruthSocial +] as const; + type NotificationType = typeof NOTIFICATION_TYPES[number]; /** Ensure the Notification is a valid, known type. */ @@ -21,6 +27,7 @@ const validType = (type: string): type is NotificationType => NOTIFICATION_TYPES export { NOTIFICATION_TYPES, + EXCLUDE_TYPES, NotificationType, validType, }; From 1ea4ae3a573f03ab301b37bb37ca8d15cbdaba2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 4 Nov 2022 22:36:39 +0100 Subject: [PATCH 32/48] Only support Pleroma for now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/instance.ts | 18 ------------------ app/soapbox/actions/statuses.ts | 6 ++++-- app/soapbox/components/translate-button.tsx | 2 +- app/soapbox/reducers/instance.ts | 13 ------------- app/soapbox/utils/features.ts | 8 +------- 5 files changed, 6 insertions(+), 41 deletions(-) diff --git a/app/soapbox/actions/instance.ts b/app/soapbox/actions/instance.ts index 3a45abf49..151ad3672 100644 --- a/app/soapbox/actions/instance.ts +++ b/app/soapbox/actions/instance.ts @@ -1,6 +1,5 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import get from 'lodash/get'; -import { gte } from 'semver'; import KVStore from 'soapbox/storage/kv_store'; import { RootState } from 'soapbox/store'; @@ -38,12 +37,6 @@ const needsNodeinfo = (instance: Record): boolean => { return v.software === 'Pleroma' && !get(instance, ['pleroma', 'metadata']); }; -/** Mastodon exposes features availabiliy under /api/v2/instance since 4.0.0 */ -const supportsInstanceV2 = (instance: Record): boolean => { - const v = parseVersion(get(instance, 'version')); - return v.software === 'Mastodon' && gte(v.compatVersion, '4.0.0'); -}; - export const fetchInstance = createAsyncThunk( 'instance/fetch', async(_arg, { dispatch, getState, rejectWithValue }) => { @@ -52,9 +45,6 @@ export const fetchInstance = createAsyncThunk( if (needsNodeinfo(instance)) { dispatch(fetchNodeinfo()); } - if (supportsInstanceV2(instance)) { - dispatch(fetchInstanceV2()); - } return instance; } catch (e) { return rejectWithValue(e); @@ -74,14 +64,6 @@ export const loadInstance = createAsyncThunk( }, ); -export const fetchInstanceV2 = createAsyncThunk( - 'nodeinfo/fetch', - async(_arg, { getState }) => { - const { data: instance } = await api(getState).get('/api/v2/instance'); - return instance; - }, -); - export const fetchNodeinfo = createAsyncThunk( 'nodeinfo/fetch', async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'), diff --git a/app/soapbox/actions/statuses.ts b/app/soapbox/actions/statuses.ts index 5ebf1c95c..f9bfca3b0 100644 --- a/app/soapbox/actions/statuses.ts +++ b/app/soapbox/actions/statuses.ts @@ -310,10 +310,12 @@ const toggleStatusHidden = (status: Status) => { } }; -const translateStatus = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { +const translateStatus = (id: string, targetLanguage?: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_TRANSLATE_REQUEST, id }); - api(getState).post(`/api/v1/statuses/${id}/translate`).then(response => { + api(getState).post(`/api/v1/statuses/${id}/translate`, { + target_language: targetLanguage, + }).then(response => { dispatch({ type: STATUS_TRANSLATE_SUCCESS, id, diff --git a/app/soapbox/components/translate-button.tsx b/app/soapbox/components/translate-button.tsx index 5c6334fd3..d39cba1a6 100644 --- a/app/soapbox/components/translate-button.tsx +++ b/app/soapbox/components/translate-button.tsx @@ -27,7 +27,7 @@ const TranslateButton: React.FC = ({ status }) => { if (status.translation) { dispatch(undoStatusTranslation(status.id)); } else { - dispatch(translateStatus(status.id)); + dispatch(translateStatus(status.id, intl.locale)); } }; diff --git a/app/soapbox/reducers/instance.ts b/app/soapbox/reducers/instance.ts index ad6b5ae2f..4c1456dc4 100644 --- a/app/soapbox/reducers/instance.ts +++ b/app/soapbox/reducers/instance.ts @@ -10,7 +10,6 @@ import { rememberInstance, fetchInstance, fetchNodeinfo, - fetchInstanceV2, } from '../actions/instance'; import type { AnyAction } from 'redux'; @@ -33,20 +32,10 @@ const nodeinfoToInstance = (nodeinfo: ImmutableMap) => { })); }; -const instanceV2ToInstance = (instanceV2: ImmutableMap) => - normalizeInstance(ImmutableMap({ - configuration: instanceV2.get('configuration'), - })); - const importInstance = (_state: typeof initialState, instance: ImmutableMap) => { return normalizeInstance(instance); }; -const importInstanceV2 = (state: typeof initialState, instanceV2: ImmutableMap) => { - console.log(instanceV2.toJS()); - return state.mergeDeep(instanceV2ToInstance(instanceV2)); -}; - const importNodeinfo = (state: typeof initialState, nodeinfo: ImmutableMap) => { return nodeinfoToInstance(nodeinfo).mergeDeep(state); }; @@ -131,8 +120,6 @@ export default function instance(state = initialState, action: AnyAction) { case fetchInstance.fulfilled.type: persistInstance(action.payload); return importInstance(state, ImmutableMap(fromJS(action.payload))); - case fetchInstanceV2.fulfilled.type: - return importInstanceV2(state, ImmutableMap(fromJS(action.payload))); case fetchInstance.rejected.type: return handleInstanceFetchFail(state, action.error); case fetchNodeinfo.fulfilled.type: diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 716809612..f70402873 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -353,12 +353,6 @@ const getInstanceFeatures = (instance: Instance) => { */ importData: v.software === PLEROMA && gte(v.version, '2.2.0'), - /** - * Supports V2 instance endpoint. - * @see GET /api/v2/instance - */ - instanceV2: v.software === MASTODON && gte(v.compatVersion, '4.0.0'), - /** * Can create, view, and manage lists. * @see {@link https://docs.joinmastodon.org/methods/timelines/lists/} @@ -618,7 +612,7 @@ const getInstanceFeatures = (instance: Instance) => { * Can translate statuses. * @see POST /api/v1/statuses/:id/translate */ - translations: v.software === MASTODON && instance.configuration.getIn(['translation', 'enabled'], false), + translations: features.includes('translation'), /** * Trending statuses. From 99bd9f5e8e41947b78c8c3e9d6cab8aea8aa75c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 5 Nov 2022 11:46:15 +0100 Subject: [PATCH 33/48] Add form element on compose area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/ui/hstack/hstack.tsx | 8 ++++++-- app/soapbox/components/ui/stack/stack.tsx | 16 ++++++++++------ .../features/compose/components/compose-form.tsx | 10 +++++++--- .../compose/components/polls/poll-form.tsx | 4 ++-- .../compose/components/spoiler-input.tsx | 4 ++-- 5 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/soapbox/components/ui/hstack/hstack.tsx b/app/soapbox/components/ui/hstack/hstack.tsx index a109da608..996320dea 100644 --- a/app/soapbox/components/ui/hstack/hstack.tsx +++ b/app/soapbox/components/ui/hstack/hstack.tsx @@ -40,6 +40,8 @@ interface IHStack { space?: keyof typeof spaces /** Whether to let the flexbox grow. */ grow?: boolean + /** HTML element to use for container. */ + element?: keyof JSX.IntrinsicElements, /** Extra CSS styles for the
    */ style?: React.CSSProperties /** Whether to let the flexbox wrap onto multiple lines. */ @@ -48,10 +50,12 @@ interface IHStack { /** Horizontal row of child elements. */ const HStack = forwardRef((props, ref) => { - const { space, alignItems, grow, justifyContent, wrap, className, ...filteredProps } = props; + const { space, alignItems, justifyContent, className, grow, element = 'div', wrap, ...filteredProps } = props; + + const Elem = element as 'div'; return ( -
    { - /** Size of the gap between elements. */ - space?: keyof typeof spaces /** Horizontal alignment of children. */ alignItems?: 'center' + /** Extra class names on the element. */ + className?: string /** Vertical alignment of children. */ justifyContent?: keyof typeof justifyContentOptions - /** Extra class names on the
    element. */ - className?: string + /** Size of the gap between elements. */ + space?: keyof typeof spaces /** Whether to let the flexbox grow. */ grow?: boolean + /** HTML element to use for container. */ + element?: keyof JSX.IntrinsicElements, } /** Vertical stack of child elements. */ const Stack = React.forwardRef((props, ref: React.LegacyRef | undefined) => { - const { space, alignItems, justifyContent, className, grow, ...filteredProps } = props; + const { space, alignItems, justifyContent, className, grow, element = 'div', ...filteredProps } = props; + + const Elem = element as 'div'; return ( -
    ({ id, shouldCondense, autoFocus, clickab setComposeFocused(true); }; - const handleSubmit = () => { + const handleSubmit = (e?: React.FormEvent) => { if (text !== autosuggestTextareaRef.current?.textarea?.value) { // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) // Update the state to match the current text @@ -142,6 +142,10 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab // Submit disabled: const fulltext = [spoilerText, countableText(text)].join(''); + if (e) { + e.preventDefault(); + } + if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxTootChars || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) { return; } @@ -261,7 +265,7 @@ const ComposeForm = ({ id, shouldCondense, autoFocus, clickab } return ( - + {scheduledStatusCount > 0 && ( ({ id, shouldCondense, autoFocus, clickab
    )} -
    diff --git a/app/soapbox/features/compose/components/polls/poll-form.tsx b/app/soapbox/features/compose/components/polls/poll-form.tsx index 4daf54048..e61f1975f 100644 --- a/app/soapbox/features/compose/components/polls/poll-form.tsx +++ b/app/soapbox/features/compose/components/polls/poll-form.tsx @@ -168,7 +168,7 @@ const PollForm: React.FC = ({ composeId }) => { -
    diff --git a/app/soapbox/features/compose/components/spoiler-input.tsx b/app/soapbox/features/compose/components/spoiler-input.tsx index e6f53d04c..873450116 100644 --- a/app/soapbox/features/compose/components/spoiler-input.tsx +++ b/app/soapbox/features/compose/components/spoiler-input.tsx @@ -68,7 +68,7 @@ const SpoilerInput = React.forwardRef(({ />
    -
    @@ -77,4 +77,4 @@ const SpoilerInput = React.forwardRef(({ ); }); -export default SpoilerInput; \ No newline at end of file +export default SpoilerInput; From 5658ee77a468c76baf70db8a250af4085152ac9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 5 Nov 2022 16:55:23 +0100 Subject: [PATCH 34/48] Ask for confirmation before revoking current session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../features/auth_token_list/index.tsx | 32 ++++++++++++++++--- .../ui/components/profile-dropdown.tsx | 2 ++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/auth_token_list/index.tsx b/app/soapbox/features/auth_token_list/index.tsx index dec5aedba..c63f20e13 100644 --- a/app/soapbox/features/auth_token_list/index.tsx +++ b/app/soapbox/features/auth_token_list/index.tsx @@ -1,26 +1,45 @@ import React, { useEffect } from 'react'; import { defineMessages, FormattedDate, useIntl } from 'react-intl'; +import { openModal } from 'soapbox/actions/modals'; import { fetchOAuthTokens, revokeOAuthTokenById } from 'soapbox/actions/security'; import { Button, Card, CardBody, CardHeader, CardTitle, Column, Spinner, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { Token } from 'soapbox/reducers/security'; +import type { Map as ImmutableMap } from 'immutable'; + const messages = defineMessages({ header: { id: 'security.headers.tokens', defaultMessage: 'Sessions' }, revoke: { id: 'security.tokens.revoke', defaultMessage: 'Revoke' }, + revokeSessionHeading: { id: 'confirmations.revoke_session.heading', defaultMessage: 'Revoke current session' }, + revokeSessionMessage: { id: 'confirmations.revoke_session.message', defaultMessage: 'You are about to revoke your current session. You will be signed out.' }, + revokeSessionConfirm: { id: 'confirmations.revoke_session.confirm', defaultMessage: 'Revoke' }, }); interface IAuthToken { token: Token, + isCurrent: boolean, } -const AuthToken: React.FC = ({ token }) => { +const AuthToken: React.FC = ({ token, isCurrent }) => { const dispatch = useAppDispatch(); const intl = useIntl(); const handleRevoke = () => { - dispatch(revokeOAuthTokenById(token.id)); + if (isCurrent) + dispatch(openModal('CONFIRM', { + icon: require('@tabler/icons/alert-triangle.svg'), + heading: intl.formatMessage(messages.revokeSessionHeading), + message: intl.formatMessage(messages.revokeSessionMessage), + confirm: intl.formatMessage(messages.revokeSessionConfirm), + onConfirm: () => { + dispatch(revokeOAuthTokenById(token.id)); + }, + })); + else { + dispatch(revokeOAuthTokenById(token.id)); + } }; return ( @@ -42,7 +61,7 @@ const AuthToken: React.FC = ({ token }) => {
    -
    @@ -55,6 +74,11 @@ const AuthTokenList: React.FC = () => { const dispatch = useAppDispatch(); const intl = useIntl(); const tokens = useAppSelector(state => state.security.get('tokens').reverse()); + const currentTokenId = useAppSelector(state => { + const currentToken = state.auth.get('tokens').valueSeq().find((token: ImmutableMap) => token.get('me') === state.auth.get('me')); + + return currentToken?.get('id'); + }); useEffect(() => { dispatch(fetchOAuthTokens()); @@ -63,7 +87,7 @@ const AuthTokenList: React.FC = () => { const body = tokens ? (
    {tokens.map((token) => ( - + ))}
    ) : ; diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 0a9a3f297..8830515e4 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -39,6 +39,8 @@ const ProfileDropdown: React.FC = ({ account, children }) => { const features = useFeatures(); const intl = useIntl(); + useAppSelector((state) => console.log(state.auth.toJS())); + const authUsers = useAppSelector((state) => state.auth.get('users')); const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.get('id')))); From 3fbc5716db1f0fb604f28e67ec88ccca9d8967c7 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 7 Nov 2022 15:41:14 -0500 Subject: [PATCH 35/48] Update colors in sidebar --- app/soapbox/components/sidebar-navigation-link.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/sidebar-navigation-link.tsx b/app/soapbox/components/sidebar-navigation-link.tsx index 9a20ca482..5c9100f85 100644 --- a/app/soapbox/components/sidebar-navigation-link.tsx +++ b/app/soapbox/components/sidebar-navigation-link.tsx @@ -37,16 +37,17 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r ref={ref} onClick={handleClick} className={classNames({ - 'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rounded-full group text-gray-600 hover:text-primary-600 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-100 dark:hover:bg-primary-700': true, - 'dark:text-gray-100 text-primary-600': isActive, + 'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rounded-full group text-gray-600 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-200 dark:hover:bg-primary-900': true, + 'dark:text-gray-100 text-gray-900': isActive, })} > From ee05f794976ae016957d70edf9ced9f98c8dbad5 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 7 Nov 2022 15:42:55 -0500 Subject: [PATCH 36/48] Remove log --- app/soapbox/features/ui/components/profile-dropdown.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/features/ui/components/profile-dropdown.tsx b/app/soapbox/features/ui/components/profile-dropdown.tsx index 8830515e4..0a9a3f297 100644 --- a/app/soapbox/features/ui/components/profile-dropdown.tsx +++ b/app/soapbox/features/ui/components/profile-dropdown.tsx @@ -39,8 +39,6 @@ const ProfileDropdown: React.FC = ({ account, children }) => { const features = useFeatures(); const intl = useIntl(); - useAppSelector((state) => console.log(state.auth.toJS())); - const authUsers = useAppSelector((state) => state.auth.get('users')); const otherAccounts = useAppSelector((state) => authUsers.map((authUser: any) => getAccount(state, authUser.get('id')))); From 0e1a369c36adc38132e73ca4a1cde30e08d458db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Nov 2022 11:48:56 -0600 Subject: [PATCH 37/48] translationRunner: fix root directory --- translationRunner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translationRunner.js b/translationRunner.js index 26f3b2214..63c8022be 100644 --- a/translationRunner.js +++ b/translationRunner.js @@ -6,7 +6,7 @@ const { default: manageTranslations, readMessageFiles } = require('react-intl-tr const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/; -const rootDirectory = path.resolve(__dirname, '..'); +const rootDirectory = path.resolve(__dirname); const translationsDirectory = path.resolve(rootDirectory, 'app', 'soapbox', 'locales'); const messagesDirectory = path.resolve(rootDirectory, 'build', 'messages'); const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languages, filename) => { From 4772f012534725700b64410f18690227226a5557 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Nov 2022 12:57:12 -0600 Subject: [PATCH 38/48] translationRunner: convert to TypeScript --- package.json | 2 +- translationRunner.js => translationRunner.ts | 75 +++++---- .../index.d.ts | 154 ++++++++++++++++++ 3 files changed, 202 insertions(+), 29 deletions(-) rename translationRunner.js => translationRunner.ts (75%) create mode 100644 types/react-intl-translations-manager/index.d.ts diff --git a/package.json b/package.json index e4813bf50..576eec262 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "dev": "${npm_execpath} run start", "build": "npx webpack", "jsdoc": "npx jsdoc -c jsdoc.conf.js", - "manage:translations": "node ./translationRunner.js", + "manage:translations": "npx ts-node ./translationRunner.ts", "test": "npx cross-env NODE_ENV=test npx jest", "test:coverage": "${npm_execpath} run test --coverage", "test:all": "${npm_execpath} run test:coverage && ${npm_execpath} run lint", diff --git a/translationRunner.js b/translationRunner.ts similarity index 75% rename from translationRunner.js rename to translationRunner.ts index 63c8022be..8cbda7a1a 100644 --- a/translationRunner.js +++ b/translationRunner.ts @@ -1,8 +1,15 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; -const parser = require('intl-messageformat-parser'); -const { default: manageTranslations, readMessageFiles } = require('react-intl-translations-manager'); // eslint-disable-line import/order +import parser from 'intl-messageformat-parser'; +import manageTranslations, { readMessageFiles, ExtractedDescriptor } from 'react-intl-translations-manager'; + +type Validator = (language: string) => void; + +interface LanguageResult { + language: string, + error: any, +} const RFC5646_REGEXP = /^[a-z]{2,3}(?:-(?:x|[A-Za-z]{2,4}))*$/; @@ -15,29 +22,29 @@ const availableLanguages = fs.readdirSync(translationsDirectory).reduce((languag languages.push(basename); } return languages; -}, []); +}, [] as string[]); -const testRFC5646 = language => { +const testRFC5646: Validator = (language) => { if (!RFC5646_REGEXP.test(language)) { throw new Error('Not RFC5646 name'); } }; -const testAvailability = language => { +const testAvailability: Validator = (language) => { if (!availableLanguages.includes(language)) { throw new Error('Not an available language'); } }; -const validateLanguages = (languages, validators) => { - const invalidLanguages = languages.reduce((acc, language) => { +const validateLanguages = (languages: string[], validators: Validator[]): void => { + const invalidLanguages = languages.reduce((acc, language): LanguageResult[] => { try { validators.forEach(validator => validator(language)); } catch (error) { acc.push({ language, error }); } return acc; - }, []); + }, [] as LanguageResult[]); if (invalidLanguages.length > 0) { console.error(` @@ -80,13 +87,18 @@ Try to run "yarn build" first`); } // determine the languages list -const languages = (argv._.length > 0) ? argv._ : availableLanguages; +const languages: string[] = (argv._.length > 0) ? argv._ : availableLanguages; + +const validators: Validator[] = [ + testRFC5646, +]; + +if (!argv.force) { + validators.push(testAvailability); +} // validate languages -validateLanguages(languages, [ - testRFC5646, - !argv.force && testAvailability, -].filter(Boolean)); +validateLanguages(languages, validators); // manage translations manageTranslations({ @@ -104,7 +116,7 @@ manageTranslations({ // used in translations which are not used in the default message. /* eslint-disable no-console */ -function findVariablesinAST(tree) { +function findVariablesinAST(tree: parser.MessageFormatElement[]) { const result = new Set(); tree.forEach((element) => { switch (element.type) { @@ -130,14 +142,14 @@ function findVariablesinAST(tree) { return result; } -function findVariables(string) { +function findVariables(string: string) { return findVariablesinAST(parser.parse(string)); } const extractedMessagesFiles = readMessageFiles(translationsDirectory); const extractedMessages = extractedMessagesFiles.reduce((acc, messageFile) => { messageFile.descriptors.forEach((descriptor) => { - descriptor.descriptors.forEach((item) => { + descriptor.descriptors?.forEach((item) => { const variables = findVariables(item.defaultMessage); acc.push({ id: item.id, @@ -147,20 +159,20 @@ const extractedMessages = extractedMessagesFiles.reduce((acc, messageFile) => { }); }); return acc; -}, []); +}, [] as ExtractedDescriptor[]); -const translations = languages.map((language) => { +const translations = languages.map((language: string) => { return { language: language, data: JSON.parse(fs.readFileSync(path.join(translationsDirectory, language + '.json'), 'utf8')), }; }); -function difference(a, b) { - return new Set([...a].filter(x => !b.has(x))); +function difference(a: Set, b: Set) { + return new Set(Array.from(a).filter(x => !b.has(x))); } -function pushIfUnique(arr, newItem) { +function pushIfUnique(arr: T[], newItem: T): void { if (arr.every((item) => { return (JSON.stringify(item) !== JSON.stringify(newItem)); })) { @@ -168,18 +180,25 @@ function pushIfUnique(arr, newItem) { } } -const problems = translations.reduce((acc, translation) => { +interface Problem { + language: string, + id: ExtractedDescriptor['id'], + severity: 'error' | 'warning', + type: string, +} + +const problems: Problem[] = translations.reduce((acc, translation) => { extractedMessages.forEach((message) => { try { - const translationVariables = findVariables(translation.data[message.id]); - if ([...difference(translationVariables, message.variables)].length > 0) { + const translationVariables = findVariables(translation.data[message.id!]); + if (Array.from(difference(translationVariables, message.variables)).length > 0) { pushIfUnique(acc, { language: translation.language, id: message.id, severity: 'error', type: 'missing variable ', }); - } else if ([...difference(message.variables, translationVariables)].length > 0) { + } else if (Array.from(difference(message.variables, translationVariables)).length > 0) { pushIfUnique(acc, { language: translation.language, id: message.id, @@ -197,7 +216,7 @@ const problems = translations.reduce((acc, translation) => { } }); return acc; -}, []); +}, [] as Problem[]); if (problems.length > 0) { console.error(`${problems.length} messages found with errors or warnings:`); diff --git a/types/react-intl-translations-manager/index.d.ts b/types/react-intl-translations-manager/index.d.ts new file mode 100644 index 000000000..92d901d7c --- /dev/null +++ b/types/react-intl-translations-manager/index.d.ts @@ -0,0 +1,154 @@ +declare module 'react-intl-translations-manager' { + import type { MessageDescriptor } from 'react-intl'; + + export interface ExtractedDescriptor extends Omit { + variables: Set, + descriptors?: ExtractedDescriptor[], + defaultMessage: string, + } + + export interface ExtractedMessage { + path: string, + descriptors: ExtractedDescriptor[], + } + + export interface ManageTranslationsConfig { + /** + * Directory where the babel plugin puts the extracted messages. This path is relative to your projects root. + * + * example: `src/locales/extractedMessages` + */ + messagesDirectory: string, + /** + * Directory of the translation files the translation manager needs to maintain. + * + * example: `src/locales/lang` + */ + translationsDirectory: string, + /** + * Directory of the whitelist files the translation manager needs to maintain. These files contain the key of translations that have the exact same text in a specific language as the defaultMessage. Specifying this key will suppress `unmaintained translation` warnings. + * + * example: `Dashboard` in english is also accepted as a valid translation for dutch. + * + * (optional, default: `translationsDirectory`) + */ + whitelistsDirectory?: string, + /** + * What languages the translation manager needs to maintain. Specifying no languages actually doesn't make sense, but won't break the translationManager either. (Please do not include the default language, react-intl will automatically include it.) + * + * example: for `['nl', 'fr']` the translation manager will maintain a `nl.json`, `fr.json`, `whitelist_nl.json` and a w`hitelist_fr.json` file + * + * (optional, default: `[]`) + */ + languages?: string[], + /** + * Option to output a single JSON file containing the aggregate of all extracted messages, grouped by the file they were extracted from. + * + * example: + * + * ```json + * [ + * { + * "path": "src/components/foo.json", + * "descriptors": [ + * { + * "id": "bar", + * "description": "Text for bar", + * "defaultMessage": "Bar" + * } + * ] + * } + * ] + * ``` + * + * (optional, default: `false`) + */ + singleMessagesFile?: boolean, + /** + * If you want the translationManager to log duplicate message ids or not + * + * (optional, default: `true`) + */ + detectDuplicateIds?: boolean, + /** + * If you want the translationManager to sort it's output, both json and console output + * + * (optional, default: `true`) + */ + sortKeys?: boolean, + /** (optional, default: `{ space: 2, trailingNewline: false }`)) */ + jsonOptions?: any, + /** + * Here you can specify custom logging methods. If not specified a default printer is used. + * + * Possible printers to configure: + * + * ```js + * const printers = { + * printDuplicateIds: duplicateIds => { + * console.log(`You have ${duplicateIds.length} duplicate IDs`); + * }, + * printLanguageReport: report => { + * console.log('Log report for a language'); + * }, + * printNoLanguageFile: lang => { + * console.log( + * `No existing ${lang} translation file found. A new one is created.` + * ); + * }, + * printNoLanguageWhitelistFile: lang => { + * console.log(`No existing ${lang} file found. A new one is created.`); + * } + * }; + * ``` + * + * (optional, default: `{}`) + */ + overridePrinters?: any, + /** + * Here you can specify overrides for the core hooks. If not specified, the default methods will be used. + * + * Possible overrides to configure: + * + * ```js + * const overrideCoreMethods = { + * provideExtractedMessages: () => {}, + * outputSingleFile: () => {}, + * outputDuplicateKeys: () => {}, + * beforeReporting: () => {}, + * provideLangTemplate: () => {}, + * provideTranslationsFile: () => {}, + * provideWhitelistFile: () => {}, + * reportLanguage: () => {}, + * afterReporting: () => {} + * }; + * ``` + */ + overrideCoreMethods?: any, + } + + /** This will maintain all translation files. Based on your config you will get output for duplicate ids, and per specified language you will get the deleted translations, added messages (new messages that need to be translated), and not yet translated messages. It will also maintain a whitelist file per language where you can specify translation keys where the translation is identical to the default message. This way you can avoid untranslated message warnings for these messages. */ + export default function manageTranslations(config: ManageTranslationsConfig): void; + + /** + * This is a `babel-plugin-react-intl` specific helper method. It will read all extracted JSON file for the specified directory, filter out all files without any messages, and output an array with all messages. + * + * Example output: + * + * ```js + * const extractedMessages = [ + * { + * path: 'src/components/Foo.json', + * descriptors: [ + * { + * id: 'foo_ok', + * description: 'Ok text', + * defaultMessage: 'OK' + * } + * ] + * } + * ]; + * ``` + */ + export function readMessageFiles(messagesDirectory: string): ExtractedMessage[]; +} \ No newline at end of file From 51fc34ddea458f51bd9884f7824cd03bd9c5c80a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Nov 2022 13:10:13 -0600 Subject: [PATCH 39/48] translationRunner: fix import of parser --- translationRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translationRunner.ts b/translationRunner.ts index 8cbda7a1a..ad88f3db3 100644 --- a/translationRunner.ts +++ b/translationRunner.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import parser from 'intl-messageformat-parser'; +import * as parser from 'intl-messageformat-parser'; import manageTranslations, { readMessageFiles, ExtractedDescriptor } from 'react-intl-translations-manager'; type Validator = (language: string) => void; From 8ff1dddc7e3a53171454ee8ff51c786f717e1766 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Nov 2022 15:33:47 -0600 Subject: [PATCH 40/48] translationRunner: improve types --- app/soapbox/locales/whitelist_is.json | 2 ++ translationRunner.ts | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 app/soapbox/locales/whitelist_is.json diff --git a/app/soapbox/locales/whitelist_is.json b/app/soapbox/locales/whitelist_is.json new file mode 100644 index 000000000..0d4f101c7 --- /dev/null +++ b/app/soapbox/locales/whitelist_is.json @@ -0,0 +1,2 @@ +[ +] diff --git a/translationRunner.ts b/translationRunner.ts index ad88f3db3..c4994648b 100644 --- a/translationRunner.ts +++ b/translationRunner.ts @@ -116,8 +116,8 @@ manageTranslations({ // used in translations which are not used in the default message. /* eslint-disable no-console */ -function findVariablesinAST(tree: parser.MessageFormatElement[]) { - const result = new Set(); +function findVariablesinAST(tree: parser.MessageFormatElement[]): Set { + const result = new Set(); tree.forEach((element) => { switch (element.type) { case parser.TYPE.argument: @@ -142,7 +142,7 @@ function findVariablesinAST(tree: parser.MessageFormatElement[]) { return result; } -function findVariables(string: string) { +function findVariables(string: string): Set { return findVariablesinAST(parser.parse(string)); } @@ -161,14 +161,19 @@ const extractedMessages = extractedMessagesFiles.reduce((acc, messageFile) => { return acc; }, [] as ExtractedDescriptor[]); -const translations = languages.map((language: string) => { +interface Translation { + language: string, + data: Record, +} + +const translations: Translation[] = languages.map((language: string) => { return { language: language, data: JSON.parse(fs.readFileSync(path.join(translationsDirectory, language + '.json'), 'utf8')), }; }); -function difference(a: Set, b: Set) { +function difference(a: Set, b: Set): Set { return new Set(Array.from(a).filter(x => !b.has(x))); } From a93196b316b0a52d34cc1cb0476c0fd39690c8f8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 8 Nov 2022 22:34:17 -0600 Subject: [PATCH 41/48] Add support for Akkoma --- app/soapbox/__fixtures__/akkoma-instance.json | 105 ++++++++++++++++++ .../normalizers/__tests__/instance.test.ts | 8 ++ app/soapbox/normalizers/instance.ts | 12 ++ app/soapbox/utils/features.ts | 8 +- 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/__fixtures__/akkoma-instance.json diff --git a/app/soapbox/__fixtures__/akkoma-instance.json b/app/soapbox/__fixtures__/akkoma-instance.json new file mode 100644 index 000000000..a4da0dc94 --- /dev/null +++ b/app/soapbox/__fixtures__/akkoma-instance.json @@ -0,0 +1,105 @@ +{ + "approval_required": false, + "avatar_upload_limit": 2000000, + "background_image": "https://fe.disroot.org/images/city.jpg", + "background_upload_limit": 4000000, + "banner_upload_limit": 4000000, + "description": "FEDIsroot - Federated social network powered by Pleroma (open beta)", + "description_limit": 5000, + "email": "admin@example.lan", + "languages": [ + "en" + ], + "max_toot_chars": 5000, + "pleroma": { + "metadata": { + "account_activation_required": false, + "features": [ + "pleroma_api", + "akkoma_api", + "mastodon_api", + "mastodon_api_streaming", + "polls", + "v2_suggestions", + "pleroma_explicit_addressing", + "shareable_emoji_packs", + "multifetch", + "pleroma:api/v1/notifications:include_types_filter", + "editing", + "media_proxy", + "relay", + "pleroma_emoji_reactions", + "exposable_reactions", + "profile_directory", + "custom_emoji_reactions", + "pleroma:get:main/ostatus" + ], + "federation": { + "enabled": true, + "exclusions": false, + "mrf_hashtag": { + "federated_timeline_removal": [], + "reject": [], + "sensitive": [ + "nsfw" + ] + }, + "mrf_object_age": { + "actions": [ + "delist", + "strip_followers" + ], + "threshold": 604800 + }, + "mrf_policies": [ + "ObjectAgePolicy", + "TagPolicy", + "HashtagPolicy", + "InlineQuotePolicy" + ], + "quarantined_instances": [], + "quarantined_instances_info": { + "quarantined_instances": {} + } + }, + "fields_limits": { + "max_fields": 10, + "max_remote_fields": 20, + "name_length": 512, + "value_length": 2048 + }, + "post_formats": [ + "text/plain", + "text/html", + "text/markdown", + "text/bbcode", + "text/x.misskeymarkdown" + ], + "privileged_staff": false + }, + "stats": { + "mau": 83 + }, + "vapid_public_key": null + }, + "poll_limits": { + "max_expiration": 31536000, + "max_option_chars": 200, + "max_options": 20, + "min_expiration": 0 + }, + "registrations": false, + "stats": { + "domain_count": 6972, + "status_count": 8081, + "user_count": 357 + }, + "thumbnail": "https://fe.disroot.org/instance/thumbnail.jpeg", + "title": "FEDIsroot", + "upload_limit": 16000000, + "uri": "https://fe.disroot.org", + "urls": { + "streaming_api": "wss://fe.disroot.org" + }, + "version": "2.7.2 (compatible; Akkoma 3.3.1-0-gaf90a4e51)" +} diff --git a/app/soapbox/normalizers/__tests__/instance.test.ts b/app/soapbox/normalizers/__tests__/instance.test.ts index ab51c9159..0df95fcb3 100644 --- a/app/soapbox/normalizers/__tests__/instance.test.ts +++ b/app/soapbox/normalizers/__tests__/instance.test.ts @@ -192,4 +192,12 @@ describe('normalizeInstance()', () => { const result = normalizeInstance(instance); expect(result.title).toBe('pixelfed'); }); + + it('renames Akkoma to Pleroma', () => { + const instance = require('soapbox/__fixtures__/akkoma-instance.json'); + const result = normalizeInstance(instance); + + expect(result.version).toEqual('2.7.2 (compatible; Pleroma 2.4.5+akkoma)'); + + }); }); diff --git a/app/soapbox/normalizers/instance.ts b/app/soapbox/normalizers/instance.ts index e137c2165..ff55ef4a5 100644 --- a/app/soapbox/normalizers/instance.ts +++ b/app/soapbox/normalizers/instance.ts @@ -98,6 +98,17 @@ const normalizeVersion = (instance: ImmutableMap) => { }); }; +/** Rename Akkoma to Pleroma+akkoma */ +const fixAkkoma = (instance: ImmutableMap) => { + const version: string = instance.get('version', ''); + + if (version.includes('Akkoma')) { + return instance.set('version', '2.7.2 (compatible; Pleroma 2.4.5+akkoma)'); + } else { + return instance; + } +}; + // Normalize instance (Pleroma, Mastodon, etc.) to Mastodon's format export const normalizeInstance = (instance: Record) => { return InstanceRecord( @@ -117,6 +128,7 @@ export const normalizeInstance = (instance: Record) => { // Normalize version normalizeVersion(instance); + fixAkkoma(instance); // Merge defaults instance.mergeDeepWith(mergeDefined, InstanceRecord()); diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 823f8df1a..3cb7e98dc 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -57,6 +57,12 @@ export const SOAPBOX = 'soapbox'; */ export const GLITCH = 'glitch'; +/** + * Akkoma, a Pleroma fork. + * @see {@link https://akkoma.dev/AkkomaGang/akkoma} + */ +export const AKKOMA = 'akkoma'; + /** Parse features for the given instance */ const getInstanceFeatures = (instance: Instance) => { const v = parseVersion(instance.version); @@ -202,7 +208,7 @@ const getInstanceFeatures = (instance: Instance) => { * Pleroma chats API. * @see {@link https://docs.pleroma.social/backend/development/API/chats/} */ - chats: v.software === PLEROMA && gte(v.version, '2.1.0'), + chats: v.software === PLEROMA && gte(v.version, '2.1.0') && v.build !== AKKOMA, /** * Paginated chats API. From 5f83ba9324e682c6875daf9bcdc04a28ea046727 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 9 Nov 2022 13:43:15 -0500 Subject: [PATCH 42/48] 'inReview' -> 'isUnderReview' --- app/soapbox/components/status.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 961ca5fbe..2e05c7a84 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -296,7 +296,7 @@ const Status: React.FC = (props) => { const accountAction = props.accountAction || reblogElement; - const inReview = actualStatus.visibility === 'self'; + const isUnderReview = actualStatus.visibility === 'self'; const isSensitive = actualStatus.hidden; return ( @@ -354,11 +354,11 @@ const Status: React.FC = (props) => { - {(inReview || isSensitive) && ( + {(isUnderReview || isSensitive) && ( Date: Wed, 9 Nov 2022 14:03:38 -0500 Subject: [PATCH 43/48] Hide action bar if status is under review --- app/soapbox/components/status.tsx | 2 +- app/soapbox/features/status/index.tsx | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 2e05c7a84..f7bc351b2 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -392,7 +392,7 @@ const Status: React.FC = (props) => {
    - {!hideActionBar && ( + {(!hideActionBar && !isUnderReview) && (
    diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index dbcc373ee..2d419ca84 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -134,6 +134,7 @@ const Thread: React.FC = (props) => { const me = useAppSelector(state => state.me); const status = useAppSelector(state => getStatus(state, { id: props.params.statusId })); const displayMedia = settings.get('displayMedia') as DisplayMedia; + const isUnderReview = status?.visibility === 'self'; const { ancestorsIds, descendantsIds } = useAppSelector(state => { let ancestorsIds = ImmutableOrderedSet(); @@ -412,7 +413,7 @@ const Thread: React.FC = (props) => { if (next && status) { dispatch(fetchNext(status.id, next)).then(({ next }) => { setNext(next); - }).catch(() => {}); + }).catch(() => { }); } }, 300, { leading: true }), [next, status]); @@ -475,14 +476,18 @@ const Thread: React.FC = (props) => { onOpenCompareHistoryModal={handleOpenCompareHistoryModal} /> -
    + {!isUnderReview ? ( + <> +
    - + + + ) : null}
    From ce6915cded7c793043525af1d5c406e76d676c10 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 9 Nov 2022 14:23:33 -0500 Subject: [PATCH 44/48] Add test for StatusActionBar --- .../components/__tests__/status.test.tsx | 42 +++++++++++++++++++ app/soapbox/components/status-action-bar.tsx | 5 ++- 2 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 app/soapbox/components/__tests__/status.test.tsx diff --git a/app/soapbox/components/__tests__/status.test.tsx b/app/soapbox/components/__tests__/status.test.tsx new file mode 100644 index 000000000..ea9d04d98 --- /dev/null +++ b/app/soapbox/components/__tests__/status.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; + +import { render, screen, rootState } from '../../jest/test-helpers'; +import { normalizeStatus, normalizeAccount } from '../../normalizers'; +import Status from '../status'; + +import type { ReducerStatus } from 'soapbox/reducers/statuses'; + +const account = normalizeAccount({ + id: '1', + acct: 'alex', +}); + +const status = normalizeStatus({ + id: '1', + account, + content: 'hello world', + contentHtml: 'hello world', +}) as ReducerStatus; + +describe('', () => { + const state = rootState.setIn(['accounts', '1'], account); + + it('renders content', () => { + render(, undefined, state); + screen.getByText(/hello world/i); + expect(screen.getByTestId('status')).toHaveTextContent(/hello world/i); + }); + + describe('the Status Action Bar', () => { + it('is rendered', () => { + render(, undefined, state); + expect(screen.getByTestId('status-action-bar')).toBeInTheDocument(); + }); + + it('is not rendered if status is under review', () => { + const inReviewStatus = normalizeStatus({ ...status, visibility: 'self' }); + render(, undefined, state); + expect(screen.queryAllByTestId('status-action-bar')).toHaveLength(0); + }); + }); +}); diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index a5bd99d71..574d5f6e0 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -279,12 +279,12 @@ const StatusActionBar: React.FC = ({ }; const handleCopy: React.EventHandler = (e) => { - const { uri } = status; + const { uri } = status; const textarea = document.createElement('textarea'); e.stopPropagation(); - textarea.textContent = uri; + textarea.textContent = uri; textarea.style.position = 'fixed'; document.body.appendChild(textarea); @@ -552,6 +552,7 @@ const StatusActionBar: React.FC = ({ return (
    Date: Thu, 10 Nov 2022 17:01:41 +0100 Subject: [PATCH 45/48] Improve click handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/account.tsx | 10 +++++++-- app/soapbox/components/dropdown_menu.tsx | 11 ++++++---- app/soapbox/components/quoted-status.tsx | 7 +++++- app/soapbox/components/status-action-bar.tsx | 1 - .../components/status-reply-mentions.tsx | 2 +- app/soapbox/components/status.tsx | 22 ++++++++++++------- app/soapbox/components/status_content.tsx | 2 +- app/soapbox/components/translate-button.tsx | 2 +- .../features/ui/components/actions_modal.tsx | 2 +- app/styles/components/modal.scss | 2 +- 10 files changed, 40 insertions(+), 21 deletions(-) diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 9f2d17a0c..e981e66ae 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -22,7 +22,13 @@ const InstanceFavicon: React.FC = ({ account }) => { const handleClick: React.MouseEventHandler = (e) => { e.stopPropagation(); - history.push(`/timeline/${account.domain}`); + + const timelineUrl = `/timeline/${account.domain}`; + if (!(e.ctrlKey || e.metaKey)) { + history.push(timelineUrl); + } else { + window.open(timelineUrl, '_blank'); + } }; return ( @@ -219,7 +225,7 @@ const Account = ({ · {timestampUrl ? ( - + event.stopPropagation()}> ) : ( diff --git a/app/soapbox/components/dropdown_menu.tsx b/app/soapbox/components/dropdown_menu.tsx index bb20c8622..01d69483d 100644 --- a/app/soapbox/components/dropdown_menu.tsx +++ b/app/soapbox/components/dropdown_menu.tsx @@ -143,12 +143,14 @@ class DropdownMenu extends React.PureComponent {icon && } diff --git a/app/soapbox/components/quoted-status.tsx b/app/soapbox/components/quoted-status.tsx index b521c1172..863a5d3af 100644 --- a/app/soapbox/components/quoted-status.tsx +++ b/app/soapbox/components/quoted-status.tsx @@ -44,7 +44,12 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => const account = status.account as AccountEntity; if (!compose && e.button === 0) { - history.push(`/@${account.acct}/posts/${status.id}`); + const statusUrl = `/@${account.acct}/posts/${status.id}`; + if (!(e.ctrlKey || e.metaKey)) { + history.push(statusUrl); + } else { + window.open(statusUrl, '_blank'); + } e.stopPropagation(); e.preventDefault(); } diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index a5bd99d71..df737d7fc 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -459,7 +459,6 @@ const StatusActionBar: React.FC = ({ text: intl.formatMessage(messages.admin_status), href: `/pleroma/admin/#/statuses/${status.id}/`, icon: require('@tabler/icons/pencil.svg'), - action: (event) => event.stopPropagation(), }); } diff --git a/app/soapbox/components/status-reply-mentions.tsx b/app/soapbox/components/status-reply-mentions.tsx index ce0451fda..4d65abd11 100644 --- a/app/soapbox/components/status-reply-mentions.tsx +++ b/app/soapbox/components/status-reply-mentions.tsx @@ -50,7 +50,7 @@ const StatusReplyMentions: React.FC = ({ status, hoverable // The typical case with a reply-to and a list of mentions. const accounts = to.slice(0, 2).map(account => { const link = ( - @{account.username} + e.stopPropagation()}>@{account.username} ); if (hoverable) { diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index 961ca5fbe..1a2179df8 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -84,6 +84,8 @@ const Status: React.FC = (props) => { const actualStatus = getActualStatus(status); + const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; + // Track height changes we know about to compensate scrolling. useEffect(() => { didShowCard.current = Boolean(!muted && !hidden && status?.card); @@ -97,11 +99,17 @@ const Status: React.FC = (props) => { setShowMedia(!showMedia); }; - const handleClick = (): void => { - if (onClick) { - onClick(); + const handleClick = (e?: React.MouseEvent): void => { + e?.stopPropagation(); + + if (!e || !(e.ctrlKey || e.metaKey)) { + if (onClick) { + onClick(); + } else { + history.push(statusUrl); + } } else { - history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`); + window.open(statusUrl, '_blank'); } }; @@ -145,7 +153,7 @@ const Status: React.FC = (props) => { }; const handleHotkeyOpen = (): void => { - history.push(`/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`); + history.push(statusUrl); }; const handleHotkeyOpenProfile = (): void => { @@ -292,8 +300,6 @@ const Status: React.FC = (props) => { react: handleHotkeyReact, }; - const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; - const accountAction = props.accountAction || reblogElement; const inReview = actualStatus.visibility === 'self'; @@ -307,7 +313,7 @@ const Status: React.FC = (props) => { data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)} ref={node} - onClick={() => history.push(statusUrl)} + onClick={handleClick} role='link' > {featured && ( diff --git a/app/soapbox/components/status_content.tsx b/app/soapbox/components/status_content.tsx index 70ee87a15..b3592c62d 100644 --- a/app/soapbox/components/status_content.tsx +++ b/app/soapbox/components/status_content.tsx @@ -147,7 +147,7 @@ const StatusContent: React.FC = ({ status, onClick, collapsable return; } - if (deltaX + deltaY < 5 && e.button === 0 && onClick) { + if (deltaX + deltaY < 5 && e.button === 0 && !(e.ctrlKey || e.metaKey) && onClick) { onClick(); } diff --git a/app/soapbox/components/translate-button.tsx b/app/soapbox/components/translate-button.tsx index d39cba1a6..07c778fd2 100644 --- a/app/soapbox/components/translate-button.tsx +++ b/app/soapbox/components/translate-button.tsx @@ -19,7 +19,7 @@ const TranslateButton: React.FC = ({ status }) => { const me = useAppSelector((state) => state.me); - const renderTranslate = /* translationEnabled && */ me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language; + const renderTranslate = me && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language; const handleTranslate: React.MouseEventHandler = (e) => { e.stopPropagation(); diff --git a/app/soapbox/features/ui/components/actions_modal.tsx b/app/soapbox/features/ui/components/actions_modal.tsx index 27faaf909..cc00126f6 100644 --- a/app/soapbox/features/ui/components/actions_modal.tsx +++ b/app/soapbox/features/ui/components/actions_modal.tsx @@ -40,7 +40,7 @@ const ActionsModal: React.FC = ({ status, actions, onClick, onClo className={classNames('w-full', { active, destructive })} data-method={isLogout ? 'delete' : null} > - {icon && } + {icon && }
    {text}
    {meta}
    diff --git a/app/styles/components/modal.scss b/app/styles/components/modal.scss index f663c26ff..0ac611f91 100644 --- a/app/styles/components/modal.scss +++ b/app/styles/components/modal.scss @@ -311,7 +311,7 @@ } .svg-icon:first-child { - @apply w-5 h-5 mr-2.5; + @apply min-w-[1.25rem] w-5 h-5 mr-2.5; svg { stroke-width: 1.5; From 4d9e4879ed5e853fbbb737b00769d2c10e6d7ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 10 Nov 2022 21:46:51 +0100 Subject: [PATCH 46/48] Fix Escape key not working in modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/modal_root.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/soapbox/components/modal_root.tsx b/app/soapbox/components/modal_root.tsx index eabd450d0..f34597d5b 100644 --- a/app/soapbox/components/modal_root.tsx +++ b/app/soapbox/components/modal_root.tsx @@ -47,12 +47,13 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) const isEditing = useAppSelector(state => state.compose.get('compose-modal')?.id !== null); - const handleKeyUp = useCallback((e) => { - if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!children) { + const visible = !!children; + + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) { handleOnClose(); } - }, []); + }; const handleOnClose = () => { dispatch((_, getState) => { @@ -136,6 +137,8 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) }; useEffect(() => { + if (!visible) return; + window.addEventListener('keyup', handleKeyUp, false); window.addEventListener('keydown', handleKeyDown, false); @@ -143,7 +146,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) window.removeEventListener('keyup', handleKeyUp); window.removeEventListener('keydown', handleKeyDown); }; - }, []); + }, [visible]); useEffect(() => { if (!!children && !prevChildren) { @@ -172,8 +175,6 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) } }); - const visible = !!children; - if (!visible) { return (
    From e75f1ea07441f7352576acac7ba69abef1eef8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Fri, 11 Nov 2022 20:41:32 +0100 Subject: [PATCH 47/48] Update some dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/autosuggest_input.tsx | 2 +- .../components/autosuggest_textarea.tsx | 2 +- app/soapbox/components/ui/tooltip/tooltip.tsx | 2 +- package.json | 18 +- yarn.lock | 203 +++++++++--------- 5 files changed, 108 insertions(+), 119 deletions(-) diff --git a/app/soapbox/components/autosuggest_input.tsx b/app/soapbox/components/autosuggest_input.tsx index 155cd428a..eb51ae41f 100644 --- a/app/soapbox/components/autosuggest_input.tsx +++ b/app/soapbox/components/autosuggest_input.tsx @@ -1,4 +1,4 @@ -import Portal from '@reach/portal'; +import { Portal } from '@reach/portal'; import classNames from 'clsx'; import { List as ImmutableList } from 'immutable'; import React from 'react'; diff --git a/app/soapbox/components/autosuggest_textarea.tsx b/app/soapbox/components/autosuggest_textarea.tsx index c1c849b07..4f80d0b3b 100644 --- a/app/soapbox/components/autosuggest_textarea.tsx +++ b/app/soapbox/components/autosuggest_textarea.tsx @@ -1,4 +1,4 @@ -import Portal from '@reach/portal'; +import { Portal } from '@reach/portal'; import classNames from 'clsx'; import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; diff --git a/app/soapbox/components/ui/tooltip/tooltip.tsx b/app/soapbox/components/ui/tooltip/tooltip.tsx index e04221200..02a811bac 100644 --- a/app/soapbox/components/ui/tooltip/tooltip.tsx +++ b/app/soapbox/components/ui/tooltip/tooltip.tsx @@ -1,4 +1,4 @@ -import Portal from '@reach/portal'; +import { Portal } from '@reach/portal'; import { TooltipPopup, useTooltip } from '@reach/tooltip'; import React from 'react'; diff --git a/package.json b/package.json index 576eec262..afad054f8 100644 --- a/package.json +++ b/package.json @@ -56,17 +56,17 @@ "@lcdp/offline-plugin": "^5.1.0", "@metamask/providers": "^9.0.0", "@popperjs/core": "^2.11.5", - "@reach/menu-button": "^0.16.2", - "@reach/popover": "^0.16.2", - "@reach/portal": "^0.16.2", - "@reach/rect": "^0.16.0", - "@reach/tabs": "^0.16.4", - "@reach/tooltip": "^0.16.2", + "@reach/menu-button": "^0.18.0", + "@reach/popover": "^0.18.0", + "@reach/portal": "^0.18.0", + "@reach/rect": "^0.18.0", + "@reach/tabs": "^0.18.0", + "@reach/tooltip": "^0.18.0", "@reduxjs/toolkit": "^1.8.1", "@sentry/browser": "^7.11.1", "@sentry/react": "^7.11.1", "@sentry/tracing": "^7.11.1", - "@tabler/icons": "^1.109.0", + "@tabler/icons": "^1.111.0", "@tailwindcss/forms": "^0.5.3", "@tailwindcss/typography": "^0.5.7", "@tanstack/react-query": "^4.0.10", @@ -173,13 +173,13 @@ "react-redux": "^7.2.5", "react-router-dom": "^5.3.0", "react-router-scroll-4": "^1.0.0-beta.2", - "react-simple-pull-to-refresh": "^1.3.0", + "react-simple-pull-to-refresh": "^1.3.3", "react-sparklines": "^1.7.0", "react-sticky-box": "^1.0.2", "react-swipeable-views": "^0.14.0", "react-textarea-autosize": "^8.3.4", "react-toggle": "^4.1.2", - "react-virtuoso": "^2.16.2", + "react-virtuoso": "^3.1.3", "redux": "^4.1.1", "redux-immutable": "^4.0.0", "redux-thunk": "^2.2.0", diff --git a/yarn.lock b/yarn.lock index 9c2729f93..803003924 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2048,121 +2048,110 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== -"@reach/auto-id@0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.16.0.tgz#dfabc3227844e8c04f8e6e45203a8e14a8edbaed" - integrity sha512-5ssbeP5bCkM39uVsfQCwBBL+KT8YColdnMN5/Eto6Rj7929ql95R3HZUOkKIvj7mgPtEb60BLQxd1P3o6cjbmg== +"@reach/auto-id@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.18.0.tgz#4b97085cd1cf1360a9bedc6e9c78e97824014f0d" + integrity sha512-XwY1IwhM7mkHZFghhjiqjQ6dstbOdpbFLdggeke75u8/8icT8uEHLbovFUgzKjy9qPvYwZIB87rLiR8WdtOXCg== dependencies: - "@reach/utils" "0.16.0" - tslib "^2.3.0" + "@reach/utils" "0.18.0" -"@reach/descendants@0.16.1": - version "0.16.1" - resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.16.1.tgz#fa3d89c0503565369707f32985d87eef61985d9f" - integrity sha512-3WZgRnD9O4EORKE31rrduJDiPFNMOjUkATx0zl192ZxMq3qITe4tUj70pS5IbJl/+v9zk78JwyQLvA1pL7XAPA== +"@reach/descendants@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.18.0.tgz#16fe52a5154da262994b0b8768baff4f670922d1" + integrity sha512-GXUxnM6CfrX5URdnipPIl3Tlc6geuz4xb4n61y4tVWXQX1278Ra9Jz9DMRN8x4wheHAysvrYwnR/SzAlxQzwtA== dependencies: - "@reach/utils" "0.16.0" - tslib "^2.3.0" + "@reach/utils" "0.18.0" -"@reach/dropdown@0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@reach/dropdown/-/dropdown-0.16.2.tgz#4aa7df0f716cb448d01bc020d54df595303d5fa6" - integrity sha512-l4nNiX6iUpMdHQNbZMhgW5APtw0AUwJuRnkqE93vkjvdtrYl/sNJy1Jr6cGG7TrZIABLSOsfwbXU3C+qwJ3ftQ== +"@reach/dropdown@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/dropdown/-/dropdown-0.18.0.tgz#c2e2e99df682f2136558851b80dc05b4f9dd92a5" + integrity sha512-LriXdVgxJoUhIQfS2r2DHYv3X6fHyplYxa9FmSwQIMXdESpE/P9Zsb1pVEObcNf3ZQBrl0L1bl/5rk7SpK7qfA== dependencies: - "@reach/auto-id" "0.16.0" - "@reach/descendants" "0.16.1" - "@reach/popover" "0.16.2" - "@reach/utils" "0.16.0" - tslib "^2.3.0" + "@reach/auto-id" "0.18.0" + "@reach/descendants" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/popover" "0.18.0" + "@reach/utils" "0.18.0" -"@reach/menu-button@^0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.16.2.tgz#664e33e70de431f88abf1f8537c48b1b6ce87e88" - integrity sha512-p4n6tFVaJZHJZEznHWy0YH2Xr3I+W7tsQWAT72PqKGT+uryGRdtgEQqi76f/8cRaw/00ueazBk5lwLG7UKGFaA== +"@reach/menu-button@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.18.0.tgz#ae40dc86e47e7f925599ca720e3ba65263cc56f3" + integrity sha512-v1lj5rYSpavOKI4ipXj8OfvQmvVNAYXCv+UcltRkjOcWEKWADUUKkGX55wiUhsCsTGCJ7lGYz5LqOZrn3LP6PQ== dependencies: - "@reach/dropdown" "0.16.2" - "@reach/popover" "0.16.2" - "@reach/utils" "0.16.0" - prop-types "^15.7.2" - tiny-warning "^1.0.3" - tslib "^2.3.0" + "@reach/dropdown" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/popover" "0.18.0" + "@reach/utils" "0.18.0" "@reach/observe-rect@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== -"@reach/popover@0.16.2", "@reach/popover@^0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.16.2.tgz#71d7af3002ca49d791476b22dee1840dd1719c19" - integrity sha512-IwkRrHM7Vt33BEkSXneovymJv7oIToOfTDwRKpuYEB/BWYMAuNfbsRL7KVe6MjkgchDeQzAk24cYY1ztQj5HQQ== - dependencies: - "@reach/portal" "0.16.2" - "@reach/rect" "0.16.0" - "@reach/utils" "0.16.0" - tabbable "^4.0.0" - tslib "^2.3.0" +"@reach/polymorphic@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/polymorphic/-/polymorphic-0.18.0.tgz#2fe42007a774e06cdbc8e13e0d46f2dc30f2f1ed" + integrity sha512-N9iAjdMbE//6rryZZxAPLRorzDcGBnluf7YQij6XDLiMtfCj1noa7KyLpEc/5XCIB/EwhX3zCluFAwloBKdblA== -"@reach/portal@0.16.2", "@reach/portal@^0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.16.2.tgz#ca83696215ee03acc2bb25a5ae5d8793eaaf2f64" - integrity sha512-9ur/yxNkuVYTIjAcfi46LdKUvH0uYZPfEp4usWcpt6PIp+WDF57F/5deMe/uGi/B/nfDweQu8VVwuMVrCb97JQ== +"@reach/popover@0.18.0", "@reach/popover@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.18.0.tgz#1eba3e9ed826ac69dfdf3b01a1dab15ca889b5fc" + integrity sha512-mpnWWn4w74L2U7fcneVdA6Fz3yKWNdZIRMoK8s6H7F8U2dLM/qN7AjzjEBqi6LXKb3Uf1ge4KHSbMixW0BygJQ== dependencies: - "@reach/utils" "0.16.0" - tiny-warning "^1.0.3" - tslib "^2.3.0" + "@reach/polymorphic" "0.18.0" + "@reach/portal" "0.18.0" + "@reach/rect" "0.18.0" + "@reach/utils" "0.18.0" + tabbable "^5.3.3" -"@reach/rect@0.16.0", "@reach/rect@^0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.16.0.tgz#78cf6acefe2e83d3957fa84f938f6e1fc5700f16" - integrity sha512-/qO9jQDzpOCdrSxVPR6l674mRHNTqfEjkaxZHluwJ/2qGUtYsA0GSZiF/+wX/yOWeBif1ycxJDa6HusAMJZC5Q== +"@reach/portal@0.18.0", "@reach/portal@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.18.0.tgz#dd466f5110689d14a0e7491b3aa8a449e8cefb40" + integrity sha512-TImozRapd576ofRk30Le2L3lRTFXF1p47B182wnp5eMTdZa74JX138BtNGEPJFOyrMaVmguVF8SSwZ6a0fon1Q== + dependencies: + "@reach/utils" "0.18.0" + +"@reach/rect@0.18.0", "@reach/rect@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.18.0.tgz#d1dc45adc92f80cc54b64498e19de909ced40722" + integrity sha512-Xk8urN4NLn3F70da/DtByMow83qO6DF6vOxpLjuDBqud+kjKgxAU9vZMBSZJyH37+F8mZinRnHyXtlLn5njQOg== dependencies: "@reach/observe-rect" "1.2.0" - "@reach/utils" "0.16.0" - prop-types "^15.7.2" - tiny-warning "^1.0.3" - tslib "^2.3.0" + "@reach/utils" "0.18.0" -"@reach/tabs@^0.16.4": - version "0.16.4" - resolved "https://registry.yarnpkg.com/@reach/tabs/-/tabs-0.16.4.tgz#7da85e46f64052bdd1c0f9582f900e379b098ac5" - integrity sha512-4EK+1U0OoLfg2tJ1BSZf6/tx0hF5vlXKxY7qB//bPWtlIh9Xfp/aSDIdspFf3xS8MjtKeb6IVmo5UAxDMq85ZA== +"@reach/tabs@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/tabs/-/tabs-0.18.0.tgz#f2e789d445d61a371eace9415841502729d099c9" + integrity sha512-gTRJzStWJJtgMhn9FDEmKogAJMcqNaGZx0i1SGoTdVM+D29DBhVeRdO8qEg+I2l2k32DkmuZxG/Mrh+GZTjczQ== dependencies: - "@reach/auto-id" "0.16.0" - "@reach/descendants" "0.16.1" - "@reach/utils" "0.16.0" - prop-types "^15.7.2" - tslib "^2.3.0" + "@reach/auto-id" "0.18.0" + "@reach/descendants" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/utils" "0.18.0" -"@reach/tooltip@^0.16.2": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.16.2.tgz#8448cee341476e4f795fa7192f7a0864f06b8085" - integrity sha512-wtJPnbJ6l4pmudMpQHGU9v1NS4ncDgcwRNi9re9KsIdsM525zccZvHQLteBKYiaW4ib7k09t2dbwhyNU9oa0Iw== +"@reach/tooltip@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.18.0.tgz#6d416e77a82543af9a57d122962f9c0294fc2a5f" + integrity sha512-yugoTmTjB3qoMk/nUvcnw99MqpyE2TQMOXE29qnQhSqHriRwQhfftjXlTAGTSzsUJmbyms3A/1gQW0X61kjFZw== dependencies: - "@reach/auto-id" "0.16.0" - "@reach/portal" "0.16.2" - "@reach/rect" "0.16.0" - "@reach/utils" "0.16.0" - "@reach/visually-hidden" "0.16.0" - prop-types "^15.7.2" - tiny-warning "^1.0.3" - tslib "^2.3.0" + "@reach/auto-id" "0.18.0" + "@reach/polymorphic" "0.18.0" + "@reach/portal" "0.18.0" + "@reach/rect" "0.18.0" + "@reach/utils" "0.18.0" + "@reach/visually-hidden" "0.18.0" -"@reach/utils@0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.16.0.tgz#5b0777cf16a7cab1ddd4728d5d02762df0ba84ce" - integrity sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q== - dependencies: - tiny-warning "^1.0.3" - tslib "^2.3.0" +"@reach/utils@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" + integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A== -"@reach/visually-hidden@0.16.0": - version "0.16.0" - resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.16.0.tgz#2a5e834af9e93c554065ff8cbb0907fbeb26ad02" - integrity sha512-IIayZ3jzJtI5KfcfRVtOMFkw2ef/1dMT8D9BUuFcU2ORZAWLNvnzj1oXNoIfABKl5wtsLjY6SGmkYQ+tMPN8TA== +"@reach/visually-hidden@0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.18.0.tgz#17923c08acc5946624c2836b2b09d359b3aa8c27" + integrity sha512-NsJ3oeHJtPc6UOeV6MHMuzQ5sl1ouKhW85i3C0S7VM+klxVlYScBZ2J4UVnWB50A2c+evdVpCnld2YeuyYYwBw== dependencies: - prop-types "^15.7.2" - tslib "^2.3.0" + "@reach/polymorphic" "0.18.0" "@reduxjs/toolkit@^1.8.1": version "1.8.1" @@ -2271,10 +2260,10 @@ remark "^13.0.0" unist-util-find-all-after "^3.0.2" -"@tabler/icons@^1.109.0": - version "1.109.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.109.0.tgz#11626c3fc097f2f70c4c197e4b9909fb05380752" - integrity sha512-B0YetE4pB6HY2Wa57v/LJ3NgkJzKYPze4U0DurIqPoKSptatKv2ga76FZSkO6EUpkYfHMtGPM6QjpJljfuCmAQ== +"@tabler/icons@^1.111.0": + version "1.111.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-1.111.0.tgz#0538fdd8b5de7072ae60fc5d2f13a9a636d2d0e3" + integrity sha512-TZWYiMT5ccqfHxRE0Qtgvl+sGsMbNkrv7cJMt/tm8TN9l/CDXx/o7d8uAwUN+3PAlSFfF5e/rD1bi1WJs2xNIA== "@tailwindcss/forms@^0.5.3": version "0.5.3" @@ -10021,10 +10010,10 @@ react-side-effect@^2.1.0: resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ== -react-simple-pull-to-refresh@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/react-simple-pull-to-refresh/-/react-simple-pull-to-refresh-1.3.0.tgz#5f7bcd475ea5c33ecd505d097b14f56c3e5e3ce8" - integrity sha512-QPFGFsbroh2WoTcLCh3f6peMRfSettYJKCXMS9FNbFav7GWKD2whqACiNLx+Mi+VkP/I+aerB7kEirk+DQx41A== +react-simple-pull-to-refresh@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/react-simple-pull-to-refresh/-/react-simple-pull-to-refresh-1.3.3.tgz#118afe0d8ba6cade87094786b3889fb2ffd5b9bc" + integrity sha512-6qXsa5RtNVmKJhLWvDLIX8UK51HFtCEGjdqQGf+M1Qjrcc4qH4fki97sgVpGEFBRwbY7DiVDA5N5p97kF16DTw== react-sparklines@^1.7.0: version "1.7.0" @@ -10097,10 +10086,10 @@ react-transition-group@^2.2.1: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react-virtuoso@^2.16.2: - version "2.16.2" - resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-2.16.2.tgz#f0a290931ee358c40661d751df2d8b9f32496f0a" - integrity sha512-aH6XpizVYCpIkBm6td1q63Lqeu7KG1iAmpYVnzjThhm5tHEo9FJpeHAU73FJaMplFrfMKBrNfmO7a7jTOeM56g== +react-virtuoso@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-3.1.3.tgz#db811ff6fdd4749cfe9348f6d0b1333a348e65c4" + integrity sha512-sc4WICEZkyT+XdVc7gA/61UT43ZnMSX0ugh+xBG2cX+EDWs31wP1dSKQ2HSQ0YFLhZXRJ+Jqndqa8MTu4NE4CQ== dependencies: "@virtuoso.dev/react-urx" "^0.2.12" "@virtuoso.dev/urx" "^0.2.12" @@ -11329,10 +11318,10 @@ symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tabbable@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261" - integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ== +tabbable@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-5.3.3.tgz#aac0ff88c73b22d6c3c5a50b1586310006b47fbf" + integrity sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA== table@^6.0.9, table@^6.6.0: version "6.7.1" @@ -11608,7 +11597,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: +tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== From f2c5e138f3cc07ce6d05fb0d62441f93d4db213f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 12 Nov 2022 00:07:02 +0100 Subject: [PATCH 48/48] Do not define translatable messages inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../notifications/components/notification.tsx | 12 +- .../ui/components/modals/verify-sms-modal.tsx | 107 ++++++++++-------- .../features/verification/email_passthru.tsx | 24 ++-- .../verification/steps/email-verification.tsx | 24 ++-- .../verification/steps/sms-verification.tsx | 35 +++--- 5 files changed, 108 insertions(+), 94 deletions(-) diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 1871b3a89..f9ba8de33 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -1,6 +1,6 @@ import React, { useCallback } from 'react'; import { HotKeys } from 'react-hotkeys'; -import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl'; +import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor, defineMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { mentionCompose } from 'soapbox/actions/compose'; @@ -55,6 +55,11 @@ const icons: Record = { update: require('@tabler/icons/pencil.svg'), }; +const nameMessage = defineMessage({ + id: 'notification.name', + defaultMessage: '{link}{others}', +}); + const messages: Record = defineMessages({ follow: { id: 'notification.follow', @@ -115,10 +120,7 @@ const buildMessage = ( instanceTitle: string, ): React.ReactNode => { const link = buildLink(account); - const name = intl.formatMessage({ - id: 'notification.name', - defaultMessage: '{link}{others}', - }, { + const name = intl.formatMessage(nameMessage, { link, others: totalCount && totalCount > 0 ? ( void, } @@ -47,10 +78,7 @@ const VerifySmsModal: React.FC = ({ onClose }) => { setStatus(Statuses.IDLE); dispatch( snackbar.error( - intl.formatMessage({ - id: 'sms_verification.invalid', - defaultMessage: 'Please enter a valid phone number.', - }), + intl.formatMessage(messages.verificationInvalid), ), ); return; @@ -59,10 +87,7 @@ const VerifySmsModal: React.FC = ({ onClose }) => { dispatch(reRequestPhoneVerification(phone!)).then(() => { dispatch( snackbar.success( - intl.formatMessage({ - id: 'sms_verification.success', - defaultMessage: 'A verification code has been sent to your phone number.', - }), + intl.formatMessage(messages.verificationSuccess), ), ); }) @@ -70,10 +95,7 @@ const VerifySmsModal: React.FC = ({ onClose }) => { .catch(() => { dispatch( snackbar.error( - intl.formatMessage({ - id: 'sms_verification.fail', - defaultMessage: 'Failed to send SMS message to your phone number.', - }), + intl.formatMessage(messages.verificationFail), ), ); }); @@ -102,20 +124,11 @@ const VerifySmsModal: React.FC = ({ onClose }) => { const confirmationText = useMemo(() => { switch (status) { case Statuses.IDLE: - return intl.formatMessage({ - id: 'sms_verification.modal.verify_sms', - defaultMessage: 'Verify SMS', - }); + return intl.formatMessage(messages.verifySms); case Statuses.READY: - return intl.formatMessage({ - id: 'sms_verification.modal.verify_number', - defaultMessage: 'Verify phone number', - }); + return intl.formatMessage(messages.verifyNumber); case Statuses.REQUESTED: - return intl.formatMessage({ - id: 'sms_verification.modal.verify_code', - defaultMessage: 'Verify code', - }); + return intl.formatMessage(messages.verifyCode); default: return null; } @@ -126,12 +139,13 @@ const VerifySmsModal: React.FC = ({ onClose }) => { case Statuses.IDLE: return ( - {intl.formatMessage({ - id: 'sms_verification.modal.verify_help_text', - defaultMessage: 'Verify your phone number to start using {instance}.', - }, { - instance: title, - })} + ); case Statuses.READY: @@ -149,10 +163,10 @@ const VerifySmsModal: React.FC = ({ onClose }) => { return ( <> - {intl.formatMessage({ - id: 'sms_verification.modal.enter_code', - defaultMessage: 'We sent you a 6-digit code via SMS. Enter it below.', - })} + = ({ onClose }) => { }) .catch(() => dispatch( snackbar.error( - intl.formatMessage({ - id: 'sms_verification.invalid', - defaultMessage: 'Your SMS token has expired.', - }), + intl.formatMessage(messages.verificationExpired), ), )); }; @@ -201,10 +212,10 @@ const VerifySmsModal: React.FC = ({ onClose }) => { return ( } onClose={() => onClose('VERIFY_SMS')} cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined} @@ -212,10 +223,12 @@ const VerifySmsModal: React.FC = ({ onClose }) => { confirmationAction={onConfirmationClick} confirmationText={confirmationText} secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined} - secondaryText={status === Statuses.REQUESTED ? intl.formatMessage({ - id: 'sms_verification.modal.resend_code', - defaultMessage: 'Resend verification code?', - }) : undefined} + secondaryText={status === Statuses.REQUESTED ? ( + + ) : undefined} secondaryDisabled={requestedAnother} > diff --git a/app/soapbox/features/verification/email_passthru.tsx b/app/soapbox/features/verification/email_passthru.tsx index 43432c586..e51f3220f 100644 --- a/app/soapbox/features/verification/email_passthru.tsx +++ b/app/soapbox/features/verification/email_passthru.tsx @@ -28,6 +28,11 @@ const messages = defineMessages({ tokenNotFoundBody: { id: 'email_passthru.token_not_found.body', defaultMessage: 'Your email token was not found. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' }, tokenExpiredHeading: { id: 'email_passthru.token_expired.heading', defaultMessage: 'Token Expired' }, tokenExpiredBody: { id: 'email_passthru.token_expired.body', defaultMessage: 'Your email token has expired. Please request a new email confirmation from the {bold} from which you sent this email confirmation.' }, + emailConfirmed: { id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' }, + genericFail: { id: 'email_passthru.fail.generic', defaultMessage: 'Unable to confirm your email' }, + tokenExpired: { id: 'email_passthru.fail.expired', defaultMessage: 'Your email token has expired' }, + tokenNotFound: { id: 'email_passthru.fail.not_found', defaultMessage: 'Your email token is invalid.' }, + invalidToken: { id: 'email_passthru.fail.invalid_token', defaultMessage: 'Your token is invalid' }, }); const Success = () => { @@ -116,30 +121,21 @@ const EmailPassThru = () => { dispatch(confirmEmailVerification(token)) .then(() => { setStatus(Statuses.SUCCESS); - dispatch(snackbar.success(intl.formatMessage({ id: 'email_passthru.success', defaultMessage: 'Your email has been verified!' }))); + dispatch(snackbar.success(intl.formatMessage(messages.emailConfirmed))); }) .catch((error: AxiosError) => { const errorKey = error?.response?.data?.error; - let message = intl.formatMessage({ - id: 'email_passthru.fail.generic', - defaultMessage: 'Unable to confirm your email', - }); + let message = intl.formatMessage(messages.genericFail); if (errorKey) { switch (errorKey) { case 'token_expired': - message = intl.formatMessage({ - id: 'email_passthru.fail.expired', - defaultMessage: 'Your email token has expired.', - }); + message = intl.formatMessage(messages.tokenExpired); setStatus(Statuses.TOKEN_EXPIRED); break; case 'token_not_found': - message = intl.formatMessage({ - id: 'email_passthru.fail.not_found', - defaultMessage: 'Your email token is invalid.', - }); - message = 'Your token is invalid'; + message = intl.formatMessage(messages.tokenNotFound); + message = intl.formatMessage(messages.invalidToken); setStatus(Statuses.TOKEN_NOT_FOUND); break; default: diff --git a/app/soapbox/features/verification/steps/email-verification.tsx b/app/soapbox/features/verification/steps/email-verification.tsx index bec6c2868..95b888aef 100644 --- a/app/soapbox/features/verification/steps/email-verification.tsx +++ b/app/soapbox/features/verification/steps/email-verification.tsx @@ -1,6 +1,6 @@ import { AxiosError } from 'axios'; import React from 'react'; -import { useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import snackbar from 'soapbox/actions/snackbar'; import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification'; @@ -8,6 +8,13 @@ import Icon from 'soapbox/components/icon'; import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +const messages = defineMessages({ + verificationSuccess: { id: 'email_verification.success', defaultMessage: 'Verification email sent successfully.' }, + verificationFail: { id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' }, + verificationFailTakenAlert: { id: 'emai_verifilcation.exists', defaultMessage: 'This email has already been taken.' }, + verificationFailTaken: { id: 'email_verification.taken', defaultMessage: 'is taken' }, +}); + const Statuses = { IDLE: 'IDLE', REQUESTED: 'REQUESTED', @@ -77,26 +84,23 @@ const EmailVerification = () => { dispatch( snackbar.success( - intl.formatMessage({ - id: 'email_verification.exists', - defaultMessage: 'Verification email sent successfully.', - }), + intl.formatMessage(messages.verificationSuccess), ), ); }) .catch((error: AxiosError) => { const errorMessage = (error.response?.data as any)?.error; const isEmailTaken = errorMessage === 'email_taken'; - let message = intl.formatMessage({ id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' }); + let message = intl.formatMessage(messages.verificationFail); if (isEmailTaken) { - message = intl.formatMessage({ id: 'email_verification.exists', defaultMessage: 'This email has already been taken.' }); + message = intl.formatMessage(messages.verificationFailTakenAlert); } else if (errorMessage) { message = errorMessage; } if (isEmailTaken) { - setErrors([intl.formatMessage({ id: 'email_verification.taken', defaultMessage: 'is taken' })]); + setErrors([intl.formatMessage(messages.verificationFailTaken)]); } dispatch(snackbar.error(message)); @@ -111,7 +115,9 @@ const EmailVerification = () => { return (
    -

    {intl.formatMessage({ id: 'email_verification.header', defaultMessage: 'Enter your email address' })}

    +

    + +

    diff --git a/app/soapbox/features/verification/steps/sms-verification.tsx b/app/soapbox/features/verification/steps/sms-verification.tsx index 8f545e1ad..7337c706d 100644 --- a/app/soapbox/features/verification/steps/sms-verification.tsx +++ b/app/soapbox/features/verification/steps/sms-verification.tsx @@ -1,6 +1,6 @@ import { AxiosError } from 'axios'; import React from 'react'; -import { useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import OtpInput from 'react-otp-input'; import snackbar from 'soapbox/actions/snackbar'; @@ -8,6 +8,13 @@ import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/acti import { Button, Form, FormGroup, PhoneInput, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +const messages = defineMessages({ + verificationInvalid: { id: 'sms_verification.invalid', defaultMessage: 'Please enter a valid phone number.' }, + verificationSuccess: { id: 'sms_verification.success', defaultMessage: 'A verification code has been sent to your phone number.' }, + verificationFail: { id: 'sms_verification.fail', defaultMessage: 'Failed to send SMS message to your phone number.' }, + verificationExpired: { id: 'sms_verification.expired', defaultMessage: 'Your SMS token has expired.' }, +}); + const Statuses = { IDLE: 'IDLE', REQUESTED: 'REQUESTED', @@ -38,10 +45,7 @@ const SmsVerification = () => { setStatus(Statuses.IDLE); dispatch( snackbar.error( - intl.formatMessage({ - id: 'sms_verification.invalid', - defaultMessage: 'Please enter a valid phone number.', - }), + intl.formatMessage(messages.verificationInvalid), ), ); return; @@ -50,18 +54,12 @@ const SmsVerification = () => { dispatch(requestPhoneVerification(phone!)).then(() => { dispatch( snackbar.success( - intl.formatMessage({ - id: 'sms_verification.success', - defaultMessage: 'A verification code has been sent to your phone number.', - }), + intl.formatMessage(messages.verificationSuccess), ), ); setStatus(Statuses.REQUESTED); }).catch((error: AxiosError) => { - const message = (error.response?.data as any)?.message || intl.formatMessage({ - id: 'sms_verification.fail', - defaultMessage: 'Failed to send SMS message to your phone number.', - }); + const message = (error.response?.data as any)?.message || intl.formatMessage(messages.verificationFail); dispatch(snackbar.error(message)); setStatus(Statuses.FAIL); @@ -78,10 +76,7 @@ const SmsVerification = () => { dispatch(confirmPhoneVerification(verificationCode)) .catch(() => dispatch( snackbar.error( - intl.formatMessage({ - id: 'sms_verification.invalid', - defaultMessage: 'Your SMS token has expired.', - }), + intl.formatMessage(messages.verificationExpired), ), )); }; @@ -97,7 +92,7 @@ const SmsVerification = () => {

    - {intl.formatMessage({ id: 'sms_verification.sent.header', defaultMessage: 'Verification code' })} +

    @@ -136,7 +131,9 @@ const SmsVerification = () => { return (
    -

    {intl.formatMessage({ id: 'sms_verification.header', defaultMessage: 'Enter your phone number' })}

    +

    + +