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 all 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
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 @@ -6,7 +6,7 @@ import { unescapeHTML } from '../../utils/html';

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 @@ -20,7 +20,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 @@ -86,7 +86,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 @@ -97,22 +97,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,
});
15 changes: 9 additions & 6 deletions app/javascript/mastodon/components/media_attachments.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,25 @@ 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 language = status.getIn(['language', 'translation']) || 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 = audio.getIn(['translation', 'description']) || audio.get('description');

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 @@ -81,6 +83,7 @@ export default class MediaAttachments extends ImmutablePureComponent {
);
} else if (mediaAttachments.getIn([0, 'type']) === 'video') {
const video = mediaAttachments.get(0);
const description = video.getIn(['translation', 'description']) || video.get('description');

return (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
Expand All @@ -90,8 +93,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 @@ -107,7 +110,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
12 changes: 7 additions & 5 deletions app/javascript/mastodon/components/media_gallery.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ class Item extends PureComponent {
badges.push(<span key='alt' className='media-gallery__gifv__label'>ALT</span>);
}

const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

if (attachment.get('type') === 'unknown') {
return (
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
<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 @@ -146,8 +148,8 @@ class Item extends 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 @@ -163,8 +165,8 @@ class Item extends 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
12 changes: 7 additions & 5 deletions app/javascript/mastodon/components/poll.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,12 @@ 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 title = option.getIn(['translation', 'title']) || option.get('title');
let titleHtml = option.getIn(['translation', '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 @@ -163,7 +165,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 @@ -182,7 +184,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
30 changes: 21 additions & 9 deletions app/javascript/mastodon/components/status.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@ import { RelativeTimestamp } from './relative_timestamp';
import StatusActionBar from './status_action_bar';
import StatusContent from './status_content';

const domParser = new DOMParser();

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

const spoilerText = status.getIn(['translation', 'spoiler_text']) || status.get('spoiler_text');
const contentHtml = status.getIn(['translation', '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 @@ -199,12 +205,14 @@ class Status extends ImmutablePureComponent {

handleOpenVideo = (options) => {
const status = this._properStatus();
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), status.get('language'), options);
const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, options);
};

handleOpenMedia = (media, index) => {
const status = this._properStatus();
this.props.onOpenMedia(status.get('id'), media, index, status.get('language'));
const lang = status.getIn(['translation', 'language']) || status.get('language');
this.props.onOpenMedia(status.get('id'), media, index, lang);
};

handleHotkeyOpenMedia = e => {
Expand All @@ -214,7 +222,7 @@ class Status extends ImmutablePureComponent {
e.preventDefault();

if (status.get('media_attachments').size > 0) {
const lang = status.get('language');
const lang = status.getIn(['translation', 'language']) || status.get('language');
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), lang, { startTime: 0 });
} else {
Expand Down Expand Up @@ -420,6 +428,8 @@ class Status extends ImmutablePureComponent {
if (pictureInPicture.get('inUse')) {
media = <PictureInPicturePlaceholder />;
} else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language');

if (this.props.muted) {
media = (
<AttachmentList
Expand All @@ -429,14 +439,15 @@ class Status extends ImmutablePureComponent {
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

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 @@ -456,6 +467,7 @@ class Status extends ImmutablePureComponent {
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');

media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
Expand All @@ -465,8 +477,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}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
Expand All @@ -483,7 +495,7 @@ class Status extends ImmutablePureComponent {
{Component => (
<Component
media={status.get('media_attachments')}
lang={status.get('language')}
lang={language}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.handleOpenMedia}
Expand Down
20 changes: 10 additions & 10 deletions app/javascript/mastodon/components/status_content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,11 +231,11 @@ class StatusContent extends PureComponent {
const renderReadMore = this.props.onClick && status.get('collapsed');
const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);

const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
const lang = status.get('translation') ? intl.locale : status.get('language');
const content = { __html: status.getIn(['translation', 'contentHtml']) || status.get('contentHtml') };
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
Expand All @@ -253,7 +253,7 @@ class StatusContent extends PureComponent {
);

const poll = !!status.get('poll') && (
<PollContainer pollId={status.get('poll')} lang={status.get('language')} />
<PollContainer pollId={status.get('poll')} lang={language} />
);

if (status.get('spoiler_text').length > 0) {
Expand All @@ -274,24 +274,24 @@ class StatusContent extends PureComponent {
return (
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={lang} />
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={language} />
{' '}
<button type='button' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick} aria-expanded={!hidden}>{toggleText}</button>
</p>

{mentionsPlaceholder}

<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={lang} dangerouslySetInnerHTML={content} />
<div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''} translate`} lang={language} dangerouslySetInnerHTML={content} />

{!hidden && poll}
{!hidden && translateButton}
{translateButton}
</div>
);
} else if (this.props.onClick) {
return (
<>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={lang} dangerouslySetInnerHTML={content} />
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />

{poll}
{translateButton}
Expand All @@ -303,7 +303,7 @@ class StatusContent extends PureComponent {
} else {
return (
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={lang} dangerouslySetInnerHTML={content} />
<div className='status__content__text status__content__text--visible translate' lang={language} dangerouslySetInnerHTML={content} />

{poll}
{translateButton}
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/mastodon/containers/status_container.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({

onTranslate (status) {
if (status.get('translation')) {
dispatch(undoStatusTranslation(status.get('id')));
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
} else {
dispatch(translateStatus(status.get('id')));
}
Expand Down