Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Translate CW, poll options and media descriptions #24175

Merged
merged 25 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ab87008
Translate everything
c960657 Mar 9, 2023
2df1569
Translate spoiler text and poll options
c960657 Mar 18, 2023
3f957aa
Translate media attachment descriptions
c960657 Mar 19, 2023
316429e
Fix caching
c960657 Mar 19, 2023
76c15c3
Adjust language variable naming
c960657 Mar 19, 2023
96192d3
Show translate button even if only CW is visible
c960657 Mar 19, 2023
c6c7737
Adjust language variable naming
c960657 Mar 19, 2023
7136bb4
Translate aria-label
c960657 Mar 20, 2023
2ef7201
Namespace cache key
c960657 Mar 21, 2023
47b5c44
Do not use `translation` as boolean
c960657 Mar 22, 2023
d89cab4
Revert changes to account gallery view
c960657 Mar 22, 2023
aab0463
Merge branch 'main' into translate-everything
c960657 Mar 27, 2023
fd13362
Merge remote-tracking branch 'upstream/main' into translate-everything
c960657 Apr 4, 2023
8b783a3
Merge remote-tracking branch 'upstream/main' into translate-everything
c960657 Apr 16, 2023
bc53212
Allow translating statuses with empty content
c960657 Apr 23, 2023
e3788d7
Return empty string for spoiler_warning
c960657 Apr 23, 2023
40b0eb9
Merge branch 'main' into translate-everything
c960657 May 2, 2023
723d340
Rename StatusTranslation to Translation
c960657 May 2, 2023
80d8f5f
Use translate=no for round-tripping custom emojis
c960657 May 3, 2023
2bece47
Test that other span attributes are preserved
c960657 May 3, 2023
d358dec
HTML handling
c960657 May 10, 2023
cd7642d
Document new option
c960657 May 10, 2023
d0ec484
Merge remote-tracking branch 'upstream/main' into translate-everything
c960657 May 10, 2023
238dc6c
Merge remote-tracking branch 'upstream/main' into translate-everything
c960657 May 12, 2023
0b65bc4
Merge remote-tracking branch 'upstream/main' into translate-everything
c960657 May 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/controllers/api/v1/statuses/translations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Api::V1::Statuses::TranslationsController < Api::BaseController
rescue_from TranslationService::UnexpectedResponseError, TranslationService::QuotaExceededError, TranslationService::TooManyRequestsError, with: :service_unavailable

def create
render json: @translation, serializer: REST::TranslationSerializer
render json: @status_translation, serializer: REST::StatusTranslationSerializer
c960657 marked this conversation as resolved.
Show resolved Hide resolved
end

private
Expand All @@ -24,6 +24,6 @@ def set_status
end

def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale)
@status_translation = TranslateStatusService.new.call(@status, content_locale)
end
end
38 changes: 32 additions & 6 deletions app/javascript/mastodon/actions/importer/normalizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expandSpoilers } from '../../initial_state';

const domParser = new DOMParser();

const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => {
const makeEmojiMap = emojis => emojis.reduce((obj, emoji) => {
obj[`:${emoji.shortcode}:`] = emoji;
return obj;
}, {});
Expand All @@ -19,7 +19,7 @@ export function searchTextFromRawStatus (status) {
export function normalizeAccount(account) {
account = { ...account };

const emojiMap = makeEmojiMap(account);
const emojiMap = makeEmojiMap(account.emojis);
const displayName = account.display_name.trim().length === 0 ? account.username : account.display_name;

account.display_name_html = emojify(escapeTextContentForBrowser(displayName), emojiMap);
Expand Down Expand Up @@ -85,7 +85,7 @@ export function normalizeStatus(status, normalOldStatus) {

const spoilerText = normalStatus.spoiler_text || '';
const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
const emojiMap = makeEmojiMap(normalStatus);
const emojiMap = makeEmojiMap(normalStatus.emojis);

normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
Expand All @@ -96,22 +96,48 @@ export function normalizeStatus(status, normalOldStatus) {
return normalStatus;
}

export function normalizeStatusTranslation(translation, status) {
const emojiMap = makeEmojiMap(status.get('emojis').toJS());

const normalTranslation = {
detected_source_language: translation.detected_source_language,
language: translation.language,
provider: translation.provider,
contentHtml: emojify(translation.content, emojiMap),
spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap),
spoiler_text: translation.spoiler_text,
};

return normalTranslation;
}

export function normalizePoll(poll) {
const normalPoll = { ...poll };
const emojiMap = makeEmojiMap(normalPoll);
const emojiMap = makeEmojiMap(poll.emojis);

normalPoll.options = poll.options.map((option, index) => ({
...option,
voted: poll.own_votes && poll.own_votes.includes(index),
title_emojified: emojify(escapeTextContentForBrowser(option.title), emojiMap),
titleHtml: emojify(escapeTextContentForBrowser(option.title), emojiMap),
}));

return normalPoll;
}

export function normalizePollOptionTranslation(translation, poll) {
const emojiMap = makeEmojiMap(poll.get('emojis').toJS());

const normalTranslation = {
...translation,
titleHtml: emojify(escapeTextContentForBrowser(translation.title), emojiMap),
};

return normalTranslation;
}

export function normalizeAnnouncement(announcement) {
const normalAnnouncement = { ...announcement };
const emojiMap = makeEmojiMap(normalAnnouncement);
const emojiMap = makeEmojiMap.emojis(normalAnnouncement);

normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap);

Expand Down
3 changes: 2 additions & 1 deletion app/javascript/mastodon/actions/statuses.js
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ export const translateStatusFail = (id, error) => ({
error,
});

export const undoStatusTranslation = id => ({
export const undoStatusTranslation = (id, pollId) => ({
type: STATUS_TRANSLATE_UNDO,
id,
pollId,
});
16 changes: 10 additions & 6 deletions app/javascript/mastodon/components/media_attachments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,26 @@ export default class MediaAttachments extends ImmutablePureComponent {
};

render () {
const { status, lang, width, height } = this.props;
const { status, width, height } = this.props;
const mediaAttachments = status.get('media_attachments');
const translation = status.get('translation');
const language = translation ? translation.get('language') : (status.get('language') || this.props.lang);

if (mediaAttachments.size === 0) {
return null;
}

if (mediaAttachments.getIn([0, 'type']) === 'audio') {
const audio = mediaAttachments.get(0);
const description = translation ? audio.getIn(['translation', 'description']) : audio.get('description');
c960657 marked this conversation as resolved.
Show resolved Hide resolved

return (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={audio.get('url')}
alt={audio.get('description')}
lang={lang || status.get('language')}
alt={description}
lang={language}
width={width}
height={height}
poster={audio.get('preview_url') || status.getIn(['account', 'avatar_static'])}
Expand All @@ -79,6 +82,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
);
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
const video = mediaAttachments.get(0);
const description = translation ? video.getIn(['translation', 'description']) : video.get('description');
c960657 marked this conversation as resolved.
Show resolved Hide resolved

return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
Expand All @@ -88,8 +92,8 @@ export default class MediaAttachments extends ImmutablePureComponent {
frameRate={video.getIn(['meta', 'original', 'frame_rate'])}
blurhash={video.get('blurhash')}
src={video.get('url')}
alt={video.get('description')}
lang={lang || status.get('language')}
alt={description}
lang={language}
width={width}
height={height}
inline
Expand All @@ -105,7 +109,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
{Component => (
<Component
media={mediaAttachments}
lang={lang || status.get('language')}
lang={language}
sensitive={status.get('sensitive')}
defaultWidth={width}
height={height}
Expand Down
22 changes: 13 additions & 9 deletions app/javascript/mastodon/components/media_gallery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Item extends React.PureComponent {
static propTypes = {
attachment: ImmutablePropTypes.map.isRequired,
lang: PropTypes.string,
translation: ImmutablePropTypes.map,
standalone: PropTypes.bool,
index: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
Expand Down Expand Up @@ -79,7 +80,7 @@ class Item extends React.PureComponent {
};

render () {
const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
const { attachment, lang, translation, index, size, standalone, displayWidth, visible } = this.props;

let width = 50;
let height = 100;
Expand Down Expand Up @@ -132,10 +133,12 @@ class Item extends React.PureComponent {

let thumbnail = '';

const description = translation ? attachment.getIn(['translation', 'description']) : attachment.get('description');
c960657 marked this conversation as resolved.
Show resolved Hide resolved

if (attachment.get('type') === 'unknown') {
return (
<div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={attachment.get('description')} lang={lang} target='_blank' rel='noopener noreferrer'>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url') || attachment.get('url')} style={{ cursor: 'pointer' }} title={description} lang={lang} target='_blank' rel='noopener noreferrer'>
<Blurhash
hash={attachment.get('blurhash')}
className='media-gallery__preview'
Expand Down Expand Up @@ -173,8 +176,8 @@ class Item extends React.PureComponent {
src={previewUrl}
srcSet={srcSet}
sizes={sizes}
alt={attachment.get('description')}
title={attachment.get('description')}
alt={description}
title={description}
lang={lang}
style={{ objectPosition: `${x}% ${y}%` }}
onLoad={this.handleImageLoad}
Expand All @@ -188,8 +191,8 @@ class Item extends React.PureComponent {
<div className={classNames('media-gallery__gifv', { autoplay: autoPlay })}>
<video
className='media-gallery__item-gifv-thumbnail'
aria-label={attachment.get('description')}
title={attachment.get('description')}
aria-label={description}
title={description}
lang={lang}
role='application'
src={attachment.get('url')}
Expand Down Expand Up @@ -231,6 +234,7 @@ class MediaGallery extends React.PureComponent {
standalone: PropTypes.bool,
media: ImmutablePropTypes.list.isRequired,
lang: PropTypes.string,
translation: ImmutablePropTypes.map,
size: PropTypes.object,
height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
Expand Down Expand Up @@ -314,7 +318,7 @@ class MediaGallery extends React.PureComponent {
}

render () {
const { media, lang, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
const { media, lang, translation, intl, sensitive, height, defaultWidth, standalone, autoplay } = this.props;
const { visible } = this.state;
const width = this.state.width || defaultWidth;

Expand All @@ -336,9 +340,9 @@ class MediaGallery extends React.PureComponent {
const uncached = media.every(attachment => attachment.get('type') === 'unknown');

if (standalone && this.isFullSizeEligible()) {
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} displayWidth={width} visible={visible} />;
children = <Item standalone autoplay={autoplay} onClick={this.handleClick} attachment={media.get(0)} lang={lang} translation={translation} displayWidth={width} visible={visible} />;
} else {
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
children = media.take(4).map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} translation={translation} size={size} displayWidth={width} visible={visible || uncached} />);
}

if (uncached) {
Expand Down
13 changes: 8 additions & 5 deletions app/javascript/mastodon/components/poll.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,13 @@ class Poll extends ImmutablePureComponent {
const active = !!this.state.selected[`${optionIndex}`];
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));

let titleEmojified = option.get('title_emojified');
if (!titleEmojified) {
const translation = option.get('translation');
const title = translation ? translation.get('title') : option.get('title');
let titleHtml = translation ? translation.get('titleHtml') : option.get('titleHtml');

if (!titleHtml) {
const emojiMap = makeEmojiMap(poll);
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
}

return (
Expand All @@ -159,7 +162,7 @@ class Poll extends ImmutablePureComponent {
role={poll.get('multiple') ? 'checkbox' : 'radio'}
onKeyPress={this.handleOptionKeyPress}
aria-checked={active}
aria-label={option.get('title')}
aria-label={title}
lang={lang}
data-index={optionIndex}
/>
Expand All @@ -178,7 +181,7 @@ class Poll extends ImmutablePureComponent {
<span
className='poll__option__text translate'
lang={lang}
dangerouslySetInnerHTML={{ __html: titleEmojified }}
dangerouslySetInnerHTML={{ __html: titleHtml }}
/>

{!!voted && <span className='poll__voted'>
Expand Down
25 changes: 19 additions & 6 deletions app/javascript/mastodon/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';

const domParser = new DOMParser();

export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);

const translation = status.get('translation');
const spoilerText = translation ? translation.get('spoiler_text') : status.get('spoiler_text');
const contentHtml = translation ? translation.get('contentHtml') : status.get('contentHtml');
const contentText = domParser.parseFromString(contentHtml, 'text/html').documentElement.textContent;

const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && status.get('hidden') ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
spoilerText && status.get('hidden') ? spoilerText : contentText,
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
Expand Down Expand Up @@ -401,6 +408,9 @@ class Status extends ImmutablePureComponent {
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />;
} else if (status.get('media_attachments').size > 0) {
const translation = status.get('translation');
const language = translation ? translation.get('language') : status.get('language');

if (this.props.muted) {
media = (
<AttachmentList
Expand All @@ -410,14 +420,15 @@ class Status extends ImmutablePureComponent {
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = translation ? attachment.getIn(['translation', 'description']) : attachment.get('description');
c960657 marked this conversation as resolved.
Show resolved Hide resolved

media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
alt={attachment.get('description')}
lang={status.get('language')}
alt={description}
lang={language}
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
Expand All @@ -437,6 +448,7 @@ class Status extends ImmutablePureComponent {
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
const description = translation ? attachment.getIn(['translation', 'description']) : attachment.get('description');
c960657 marked this conversation as resolved.
Show resolved Hide resolved

media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
Expand All @@ -446,8 +458,8 @@ class Status extends ImmutablePureComponent {
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
lang={status.get('language')}
alt={description}
lang={language}
width={this.props.cachedMediaWidth}
height={110}
inline
Expand All @@ -467,7 +479,8 @@ class Status extends ImmutablePureComponent {
{Component => (
<Component
media={status.get('media_attachments')}
lang={status.get('language')}
lang={language}
translation={translation}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
Expand Down