Merge branch 'translations' into 'develop'

Support translation feature

See merge request soapbox-pub/soapbox!1852
environments/review-v3-0-0-bet-vqueq9/deployments/1300 v3.0.0-beta.1
marcin mikołajczak 2022-11-05 22:08:59 +00:00
commit 639ba856ab
12 zmienionych plików z 131 dodań i 11 usunięć

Wyświetl plik

@ -66,7 +66,5 @@ export const loadInstance = createAsyncThunk<void, void, { state: RootState }>(
export const fetchNodeinfo = createAsyncThunk<void, void, { state: RootState }>(
'nodeinfo/fetch',
async(_arg, { getState }) => {
return await api(getState).get('/nodeinfo/2.1.json');
},
async(_arg, { getState }) => await api(getState).get('/nodeinfo/2.1.json'),
);

Wyświetl plik

@ -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,31 @@ const toggleStatusHidden = (status: Status) => {
}
};
const translateStatus = (id: string, targetLanguage?: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch({ type: STATUS_TRANSLATE_REQUEST, id });
api(getState).post(`/api/v1/statuses/${id}/translate`, {
target_language: targetLanguage,
}).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 +359,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 +379,6 @@ export {
hideStatus,
revealStatus,
toggleStatusHidden,
translateStatus,
undoStatusTranslation,
};

Wyświetl plik

@ -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';
@ -370,8 +371,11 @@ const Status: React.FC<IStatus> = (props) => {
status={actualStatus}
onClick={handleClick}
collapsable
translatable
/>
<TranslateButton status={actualStatus} />
{(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && (
<Stack space={4}>
<StatusMedia

Wyświetl plik

@ -39,10 +39,11 @@ interface IStatusContent {
status: Status,
onClick?: () => void,
collapsable?: boolean,
translatable?: boolean,
}
/** Renders the text content of a status */
const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable = false }) => {
const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable = false, translatable }) => {
const history = useHistory();
const [collapsed, setCollapsed] = useState(false);
@ -154,14 +155,14 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
};
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;

Wyświetl plik

@ -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<ITranslateButton> = ({ 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<HTMLButtonElement> = (e) => {
e.stopPropagation();
if (status.translation) {
dispatch(undoStatusTranslation(status.id));
} else {
dispatch(translateStatus(status.id, intl.locale));
}
};
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 (
<Stack className='text-gray-700 dark:text-gray-600 text-sm' alignItems='start'>
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue hover:underline' onClick={handleTranslate}>
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
</button>
</Stack>
);
}
return (
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
<FormattedMessage id='status.translate' defaultMessage='Translate' />
</button>
);
};
export default TranslateButton;

Wyświetl plik

@ -19,12 +19,15 @@ const justifyContentOptions = {
};
const alignItemsOptions = {
top: 'items-start',
bottom: 'items-end',
center: 'items-center',
start: 'items-start',
};
interface IStack extends React.HTMLAttributes<HTMLDivElement> {
/** Horizontal alignment of children. */
alignItems?: 'center'
alignItems?: keyof typeof alignItemsOptions
/** Extra class names on the element. */
className?: string
/** Vertical alignment of children. */

Wyświetl plik

@ -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';
@ -101,7 +102,9 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
)}
<Stack space={4}>
<StatusContent status={actualStatus} />
<StatusContent status={actualStatus} translatable />
<TranslateButton status={actualStatus} />
{(quote || actualStatus.card || actualStatus.media_attachments.size > 0) && (
<Stack space={4}>

Wyświetl plik

@ -1211,8 +1211,11 @@
"status.show_less_all": "Zwiń wszystkie",
"status.show_more": "Rozwiń",
"status.show_more_all": "Rozwiń wszystkie",
"status.show_original": "Pokaż oryginalny wpis",
"status.title": "Wpis",
"status.title_direct": "Wiadomość bezpośrednia",
"status.translated_from_with": "Przetłumaczono z {lang} z użyciem {provider}",
"status.translate": "Przetłumacz wpis",
"status.unbookmark": "Usuń z zakładek",
"status.unbookmarked": "Usunięto z zakładek.",
"status.unmute_conversation": "Cofnij wyciszenie konwersacji",

Wyświetl plik

@ -63,6 +63,7 @@ export const StatusRecord = ImmutableRecord({
hidden: false,
search_index: '',
spoilerHtml: '',
translation: null as ImmutableMap<string, string> | null,
});
const normalizeAttachments = (status: ImmutableMap<string, any>) => {

Wyświetl plik

@ -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). */

Wyświetl plik

@ -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:

Wyświetl plik

@ -617,6 +617,12 @@ const getInstanceFeatures = (instance: Instance) => {
features.includes('v2_suggestions'),
]),
/**
* Can translate statuses.
* @see POST /api/v1/statuses/:id/translate
*/
translations: features.includes('translation'),
/**
* Trending statuses.
* @see GET /api/v1/trends/statuses