This commit is contained in:
Christian Schmidt 2025-05-06 15:03:45 +00:00 committed by GitHub
commit 727c171659
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 119 additions and 54 deletions

View File

@ -23,6 +23,6 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl
private private
def set_translation def set_translation
@translation = TranslateStatusService.new.call(@status, I18n.locale.to_s) @translation = TranslateStatusService.new.call(@status, params[:source_language], I18n.locale.to_s)
end end
end end

View File

@ -109,6 +109,7 @@ export function normalizeStatusTranslation(translation, status) {
const emojiMap = makeEmojiMap(status.get('emojis').toJS()); const emojiMap = makeEmojiMap(status.get('emojis').toJS());
const normalTranslation = { const normalTranslation = {
source_language: translation.source_language,
detected_source_language: translation.detected_source_language, detected_source_language: translation.detected_source_language,
language: translation.language, language: translation.language,
provider: translation.provider, provider: translation.provider,

View File

@ -290,10 +290,10 @@ export function toggleStatusCollapse(id, isCollapsed) {
}; };
} }
export const translateStatus = id => (dispatch) => { export const translateStatus = (id, source_language) => (dispatch) => {
dispatch(translateStatusRequest(id)); dispatch(translateStatusRequest(id));
api().post(`/api/v1/statuses/${id}/translate`).then(response => { api().post(`/api/v1/statuses/${id}/translate`, { source_language }).then(response => {
dispatch(translateStatusSuccess(id, response.data)); dispatch(translateStatusSuccess(id, response.data));
}).catch(error => { }).catch(error => {
dispatch(translateStatusFail(id, error)); dispatch(translateStatusFail(id, error));

View File

@ -108,6 +108,7 @@ class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func, onToggleHidden: PropTypes.func,
onToggleCollapsed: PropTypes.func, onToggleCollapsed: PropTypes.func,
onTranslate: PropTypes.func, onTranslate: PropTypes.func,
onUndoStatusTranslation: PropTypes.func,
onInteractionModal: PropTypes.func, onInteractionModal: PropTypes.func,
muted: PropTypes.bool, muted: PropTypes.bool,
hidden: PropTypes.bool, hidden: PropTypes.bool,
@ -193,8 +194,12 @@ class Status extends ImmutablePureComponent {
this.props.onToggleCollapsed(this._properStatus(), isCollapsed); this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
}; };
handleTranslate = () => { handleTranslate = (sourceLanguage) => {
this.props.onTranslate(this._properStatus()); this.props.onTranslate(this._properStatus(), sourceLanguage);
};
handleUndoStatusTranslation = () => {
this.props.onUndoStatusTranslation(this._properStatus());
}; };
getAttachmentAspectRatio () { getAttachmentAspectRatio () {
@ -572,6 +577,7 @@ class Status extends ImmutablePureComponent {
status={status} status={status}
onClick={this.handleClick} onClick={this.handleClick}
onTranslate={this.handleTranslate} onTranslate={this.handleTranslate}
onUndoStatusTranslation={this.handleUndoStatusTranslation}
collapsible collapsible
onCollapsedToggle={this.handleCollapsedToggle} onCollapsedToggle={this.handleCollapsedToggle}
{...statusContentProps} {...statusContentProps}

View File

@ -30,32 +30,47 @@ class TranslateButton extends PureComponent {
static propTypes = { static propTypes = {
translation: ImmutablePropTypes.map, translation: ImmutablePropTypes.map,
onClick: PropTypes.func, onTranslate: PropTypes.func,
onDetectLanguage: PropTypes.func,
onUndoStatusTranslation: PropTypes.func,
}; };
render () { render () {
const { translation, onClick } = this.props; const { translation, onTranslate, onDetectLanguage, onUndoStatusTranslation } = this.props;
if (translation) { if (translation) {
const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language')); const language = preloadedLanguages.find(lang => lang[0] === translation.get('detected_source_language'));
const languageName = language ? language[1] : translation.get('detected_source_language'); const languageName = language ? language[1] : translation.get('detected_source_language');
const provider = translation.get('provider'); const provider = translation.get('provider');
const languageDetectionButton = onDetectLanguage && (
<>
<button className='link-button' onClick={onDetectLanguage}>
<FormattedMessage id='status.detect_language' defaultMessage='Detect language' />
</button>
·
</>
);
return ( return (
<div className='translate-button'> <div className='translate-button'>
<div className='translate-button__meta'> <div className='translate-button__meta'>
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} /> <FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
</div> </div>
<button className='link-button' onClick={onClick}> <div className='translate-button__buttons'>
<FormattedMessage id='status.show_original' defaultMessage='Show original' /> {languageDetectionButton}
</button>
<button className='link-button' onClick={onUndoStatusTranslation}>
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
</button>
</div>
</div> </div>
); );
} }
return ( return (
<button className='status__content__translate-button' onClick={onClick}> <button className='status__content__translate-button' onClick={onTranslate}>
<FormattedMessage id='status.translate' defaultMessage='Translate' /> <FormattedMessage id='status.translate' defaultMessage='Translate' />
</button> </button>
); );
@ -73,6 +88,7 @@ class StatusContent extends PureComponent {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
statusContent: PropTypes.string, statusContent: PropTypes.string,
onTranslate: PropTypes.func, onTranslate: PropTypes.func,
onUndoStatusTranslation: PropTypes.func,
onClick: PropTypes.func, onClick: PropTypes.func,
collapsible: PropTypes.bool, collapsible: PropTypes.bool,
onCollapsedToggle: PropTypes.func, onCollapsedToggle: PropTypes.func,
@ -216,6 +232,10 @@ class StatusContent extends PureComponent {
this.props.onTranslate(); this.props.onTranslate();
}; };
handleDetectLanguage = () => {
this.props.onTranslate('und');
};
setRef = (c) => { setRef = (c) => {
this.node = c; this.node = c;
}; };
@ -225,9 +245,13 @@ class StatusContent extends PureComponent {
const renderReadMore = this.props.onClick && status.get('collapsed'); const renderReadMore = this.props.onClick && status.get('collapsed');
const contentLocale = intl.locale.replace(/[_-].*/, ''); const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); const sourceLanguage = status.get('language') || 'und';
const targetLanguages = this.props.languages?.get(sourceLanguage);
const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale);
const languageDetectionTargetLanguages = this.props.languages?.get('und');
const renderDetectLanguage = sourceLanguage !== 'und' && languageDetectionTargetLanguages?.includes(contentLocale) && status.get('translation')?.get('source_language') !== 'und';
const content = { __html: statusContent ?? getStatusContent(status) }; const content = { __html: statusContent ?? getStatusContent(status) };
const language = status.getIn(['translation', 'language']) || status.get('language'); const language = status.getIn(['translation', 'language']) || status.get('language');
const classNames = classnames('status__content', { const classNames = classnames('status__content', {
@ -242,7 +266,12 @@ class StatusContent extends PureComponent {
); );
const translateButton = renderTranslate && ( const translateButton = renderTranslate && (
<TranslateButton onClick={this.handleTranslate} translation={status.get('translation')} /> <TranslateButton
onTranslate={this.handleTranslate}
onDetectLanguage={renderDetectLanguage && this.handleDetectLanguage}
onUndoStatusTranslation={this.props.onUndoStatusTranslation}
translation={status.get('translation')}
/>
); );
const poll = !!status.get('poll') && ( const poll = !!status.get('poll') && (

View File

@ -122,12 +122,12 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
}); });
}, },
onTranslate (status) { onTranslate (status, sourceLanguage) {
if (status.get('translation')) { dispatch(translateStatus(status.get('id'), sourceLanguage));
dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); },
} else {
dispatch(translateStatus(status.get('id'))); onUndoStatusTranslation (status) {
} dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
}, },
onDirect (account) { onDirect (account) {

View File

@ -43,7 +43,8 @@ export const DetailedStatus: React.FC<{
status: any; status: any;
onOpenMedia?: (status: any, index: number, lang: string) => void; onOpenMedia?: (status: any, index: number, lang: string) => void;
onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void; onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void;
onTranslate?: (status: any) => void; onTranslate?: (status: any, sourceLanguage?: string) => void;
onUndoStatusTranslation?: (status: any) => void;
measureHeight?: boolean; measureHeight?: boolean;
onHeightChange?: () => void; onHeightChange?: () => void;
domain: string; domain: string;
@ -58,6 +59,7 @@ export const DetailedStatus: React.FC<{
onOpenMedia, onOpenMedia,
onOpenVideo, onOpenVideo,
onTranslate, onTranslate,
onUndoStatusTranslation,
measureHeight, measureHeight,
onHeightChange, onHeightChange,
domain, domain,
@ -115,9 +117,16 @@ export const DetailedStatus: React.FC<{
[_measureHeight], [_measureHeight],
); );
const handleTranslate = useCallback(() => { const handleTranslate = useCallback(
if (onTranslate) onTranslate(status); (sourceLanguage?: string) => {
}, [onTranslate, status]); if (onTranslate) onTranslate(status, sourceLanguage);
},
[onTranslate, status],
);
const handleUndoStatusTranslation = useCallback(() => {
if (onUndoStatusTranslation) onUndoStatusTranslation(status);
}, [onUndoStatusTranslation, status]);
if (!properStatus) { if (!properStatus) {
return null; return null;
@ -368,6 +377,7 @@ export const DetailedStatus: React.FC<{
<StatusContent <StatusContent
status={status} status={status}
onTranslate={handleTranslate} onTranslate={handleTranslate}
onUndoStatusTranslation={handleUndoStatusTranslation}
{...(statusContentProps as any)} {...(statusContentProps as any)}
/> />

View File

@ -339,14 +339,16 @@ class Status extends ImmutablePureComponent {
} }
}; };
handleTranslate = status => { handleTranslate = (status, sourceLanguage) => {
const { dispatch } = this.props; const { dispatch } = this.props;
if (status.get('translation')) { dispatch(translateStatus(status.get('id'), sourceLanguage));
dispatch(undoStatusTranslation(status.get('id'), status.get('poll'))); };
} else {
dispatch(translateStatus(status.get('id'))); handleUndoStatusTranslation = status => {
} const { dispatch } = this.props;
dispatch(undoStatusTranslation(status.get('id'), status.get('poll')));
}; };
handleBlockClick = (status) => { handleBlockClick = (status) => {
@ -627,6 +629,7 @@ class Status extends ImmutablePureComponent {
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden} onToggleHidden={this.handleToggleHidden}
onTranslate={this.handleTranslate} onTranslate={this.handleTranslate}
onUndoStatusTranslation={this.handleUndoStatusTranslation}
domain={domain} domain={domain}
showMedia={this.state.showMedia} showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}

View File

@ -840,6 +840,7 @@
"status.copy": "Copy link to post", "status.copy": "Copy link to post",
"status.delete": "Delete", "status.delete": "Delete",
"status.detailed_status": "Detailed conversation view", "status.detailed_status": "Detailed conversation view",
"status.detect_language": "Detect language",
"status.direct": "Privately mention @{name}", "status.direct": "Privately mention @{name}",
"status.direct_indicator": "Private mention", "status.direct_indicator": "Private mention",
"status.edit": "Edit", "status.edit": "Edit",

View File

@ -1383,6 +1383,17 @@ body > [data-popper-placement] {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
color: $dark-text-color; color: $dark-text-color;
.translate-button__buttons {
margin-right: -6px;
float: right;
white-space: nowrap;
.link-button {
display: inline;
margin: 0 6px;
}
}
} }
.status__wrapper--filtered { .status__wrapper--filtered {

View File

@ -11,6 +11,7 @@ class TranslationService::DeepL < TranslationService
end end
def translate(texts, source_language, target_language) def translate(texts, source_language, target_language)
source_language = nil if source_language == 'und'
form = { text: texts, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' } form = { text: texts, source_lang: source_language&.upcase, target_lang: target_language, tag_handling: 'html' }
request(:post, '/v2/translate', form: form) do |res| request(:post, '/v2/translate', form: form) do |res|
transform_response(res.body_with_limit) transform_response(res.body_with_limit)
@ -18,7 +19,7 @@ class TranslationService::DeepL < TranslationService
end end
def languages def languages
source_languages = [nil] + fetch_languages('source') source_languages = ['und'] + fetch_languages('source')
# In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so # In DeepL, EN and PT are deprecated in favor of EN-GB/EN-US and PT-BR/PT-PT, so
# they are supported but not returned by the API. # they are supported but not returned by the API.

View File

@ -9,7 +9,8 @@ class TranslationService::LibreTranslate < TranslationService
end end
def translate(texts, source_language, target_language) def translate(texts, source_language, target_language)
body = Oj.dump(q: texts, source: source_language.presence || 'auto', target: target_language, format: 'html', api_key: @api_key) source_language = 'auto' if source_language.in? [nil, 'und']
body = Oj.dump(q: texts, source: source_language, target: target_language, format: 'html', api_key: @api_key)
request(:post, '/translate', body: body) do |res| request(:post, '/translate', body: body) do |res|
transform_response(res.body_with_limit, source_language) transform_response(res.body_with_limit, source_language)
end end
@ -20,7 +21,7 @@ class TranslationService::LibreTranslate < TranslationService
languages = Oj.load(res.body_with_limit).to_h do |language| languages = Oj.load(res.body_with_limit).to_h do |language|
[language['code'], language['targets'].without(language['code'])] [language['code'], language['targets'].without(language['code'])]
end end
languages[nil] = languages.values.flatten.uniq.sort languages['und'] = languages.values.flatten.uniq.sort
languages languages
end end
end end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Translation < ActiveModelSerializers::Model class Translation < ActiveModelSerializers::Model
attributes :status, :detected_source_language, :language, :provider, attributes :status, :source_language, :detected_source_language, :language, :provider,
:content, :spoiler_text, :poll_options, :media_attachments :content, :spoiler_text, :poll_options, :media_attachments
class Option < ActiveModelSerializers::Model class Option < ActiveModelSerializers::Model

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::TranslationSerializer < ActiveModel::Serializer class REST::TranslationSerializer < ActiveModel::Serializer
attributes :detected_source_language, :language, :provider, :spoiler_text, :content attributes :source_language, :detected_source_language, :language, :provider, :spoiler_text, :content
class PollSerializer < ActiveModel::Serializer class PollSerializer < ActiveModel::Serializer
attribute :id attribute :id

View File

@ -6,17 +6,18 @@ class TranslateStatusService < BaseService
include ERB::Util include ERB::Util
include FormattingHelper include FormattingHelper
def call(status, target_language) def call(status, source_language, target_language)
@status = status @status = status
@source_texts = source_texts @source_texts = source_texts
@source_language = source_language || @status.language.presence || 'und'
target_language = target_language.split(/[_-]/).first unless target_languages.include?(target_language) target_language = target_language.split(/[_-]/).first unless target_languages.include?(target_language)
@target_language = target_language @target_language = target_language
raise Mastodon::NotPermittedError unless permitted? raise Mastodon::NotPermittedError unless permitted?
status_translation = Rails.cache.fetch("v2:translations/#{@status.language}/#{@target_language}/#{content_hash}", expires_in: CACHE_TTL) do status_translation = Rails.cache.fetch("v3:translations/#{@source_language}/#{@target_language}/#{content_hash}", expires_in: CACHE_TTL) do
translations = translation_backend.translate(@source_texts.values, @status.language, @target_language) translations = translation_backend.translate(@source_texts.values, @source_language, @target_language)
build_status_translation(translations) build_status_translation(translations)
end end
@ -38,7 +39,7 @@ class TranslateStatusService < BaseService
end end
def languages def languages
Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { translation_backend.languages } Rails.cache.fetch('v2:translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { translation_backend.languages }
end end
def target_languages def target_languages
@ -67,6 +68,7 @@ class TranslateStatusService < BaseService
def build_status_translation(translations) def build_status_translation(translations)
status_translation = Translation.new( status_translation = Translation.new(
source_language: @source_language,
detected_source_language: translations.first&.detected_source_language, detected_source_language: translations.first&.detected_source_language,
language: @target_language, language: @target_language,
provider: translations.first&.provider, provider: translations.first&.provider,

View File

@ -38,7 +38,7 @@ RSpec.describe TranslationService::DeepL do
.with(body: 'text=Guten+Tag&source_lang&target_lang=en&tag_handling=html') .with(body: 'text=Guten+Tag&source_lang&target_lang=en&tag_handling=html')
.to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good morning"}]}') .to_return(body: '{"translations":[{"detected_source_language":"DE","text":"Good morning"}]}')
translations = service.translate(['Guten Tag'], nil, 'en') translations = service.translate(['Guten Tag'], 'und', 'en')
expect(translations.size).to eq 1 expect(translations.size).to eq 1
translation = translations.first translation = translations.first
@ -62,7 +62,7 @@ RSpec.describe TranslationService::DeepL do
describe '#languages' do describe '#languages' do
it 'returns source languages' do it 'returns source languages' do
expect(service.languages.keys).to eq [nil, 'en', 'uk'] expect(service.languages.keys).to eq %w(und en uk)
end end
it 'returns target languages for each source language' do it 'returns target languages for each source language' do
@ -71,7 +71,7 @@ RSpec.describe TranslationService::DeepL do
end end
it 'returns target languages for auto-detection' do it 'returns target languages for auto-detection' do
expect(service.languages[nil]).to eq %w(en pt en-GB zh) expect(service.languages['und']).to eq %w(en pt en-GB zh)
end end
end end

View File

@ -15,7 +15,7 @@ RSpec.describe TranslationService::LibreTranslate do
subject(:languages) { service.languages } subject(:languages) { service.languages }
it 'returns source languages' do it 'returns source languages' do
expect(languages.keys).to eq ['en', 'da', nil] expect(languages.keys).to eq %w(en da und)
end end
it 'returns target languages for each source language' do it 'returns target languages for each source language' do
@ -24,7 +24,7 @@ RSpec.describe TranslationService::LibreTranslate do
end end
it 'returns target languages for auto-detected language' do it 'returns target languages for auto-detected language' do
expect(languages[nil]).to eq %w(de en es pt) expect(languages['und']).to eq %w(de en es pt)
end end
end end

View File

@ -34,7 +34,7 @@ RSpec.describe 'API V1 Statuses Translations' do
translation = TranslationService::Translation.new(text: 'Hello') translation = TranslationService::Translation.new(text: 'Hello')
service = instance_double(TranslationService::DeepL, translate: [translation]) service = instance_double(TranslationService::DeepL, translate: [translation])
allow(TranslationService).to receive_messages(configured?: true, configured: service) allow(TranslationService).to receive_messages(configured?: true, configured: service)
Rails.cache.write('translation_service/languages', { 'es' => ['en'] }) Rails.cache.write('v2:translation_service/languages', { 'es' => ['en'] })
post "/api/v1/statuses/#{status.id}/translate", headers: headers post "/api/v1/statuses/#{status.id}/translate", headers: headers
end end

View File

@ -33,7 +33,7 @@ RSpec.describe TranslateStatusService do
end end
it 'returns translated status content and source language and provider and original status' do it 'returns translated status content and source language and provider and original status' do
expect(service.call(status, 'es')) expect(service.call(status, nil, 'es'))
.to have_attributes( .to have_attributes(
content: '<p>Hola</p>', content: '<p>Hola</p>',
detected_source_language: 'en', detected_source_language: 'en',
@ -47,13 +47,13 @@ RSpec.describe TranslateStatusService do
let(:text) { 'Hello & :highfive:' } let(:text) { 'Hello & :highfive:' }
it 'does not translate shortcode' do it 'does not translate shortcode' do
expect(service.call(status, 'es').content).to eq '<p>Hola &amp; :highfive:</p>' expect(service.call(status, nil, 'es').content).to eq '<p>Hola &amp; :highfive:</p>'
end end
end end
describe 'status has no spoiler_text' do describe 'status has no spoiler_text' do
it 'returns an empty string' do it 'returns an empty string' do
expect(service.call(status, 'es').spoiler_text).to eq '' expect(service.call(status, nil, 'es').spoiler_text).to eq ''
end end
end end
@ -61,7 +61,7 @@ RSpec.describe TranslateStatusService do
let(:spoiler_text) { 'Hello & Hello!' } let(:spoiler_text) { 'Hello & Hello!' }
it 'translates the spoiler text' do it 'translates the spoiler text' do
expect(service.call(status, 'es').spoiler_text).to eq 'Hola & Hola!' expect(service.call(status, nil, 'es').spoiler_text).to eq 'Hola & Hola!'
end end
end end
@ -69,7 +69,7 @@ RSpec.describe TranslateStatusService do
let(:spoiler_text) { 'Hello :highfive:' } let(:spoiler_text) { 'Hello :highfive:' }
it 'does not translate shortcode' do it 'does not translate shortcode' do
expect(service.call(status, 'es').spoiler_text).to eq 'Hola :highfive:' expect(service.call(status, nil, 'es').spoiler_text).to eq 'Hola :highfive:'
end end
end end
@ -77,7 +77,7 @@ RSpec.describe TranslateStatusService do
let(:spoiler_text) { 'Hello :Hello:' } let(:spoiler_text) { 'Hello :Hello:' }
it 'translates the invalid shortcode' do it 'translates the invalid shortcode' do
expect(service.call(status, 'es').spoiler_text).to eq 'Hola :Hola:' expect(service.call(status, nil, 'es').spoiler_text).to eq 'Hola :Hola:'
end end
end end
@ -85,7 +85,7 @@ RSpec.describe TranslateStatusService do
let(:poll) { Fabricate(:poll, options: ['Hello 1', 'Hello 2']) } let(:poll) { Fabricate(:poll, options: ['Hello 1', 'Hello 2']) }
it 'translates the poll option title' do it 'translates the poll option title' do
status_translation = service.call(status, 'es') status_translation = service.call(status, nil, 'es')
expect(status_translation.poll_options.size).to eq 2 expect(status_translation.poll_options.size).to eq 2
expect(status_translation.poll_options.first.title).to eq 'Hola 1' expect(status_translation.poll_options.first.title).to eq 'Hola 1'
end end
@ -95,7 +95,7 @@ RSpec.describe TranslateStatusService do
let(:media_attachments) { [Fabricate(:media_attachment, description: 'Hello & :highfive:')] } let(:media_attachments) { [Fabricate(:media_attachment, description: 'Hello & :highfive:')] }
it 'translates the media attachment description' do it 'translates the media attachment description' do
status_translation = service.call(status, 'es') status_translation = service.call(status, nil, 'es')
media_attachment = status_translation.media_attachments.first media_attachment = status_translation.media_attachments.first
expect(media_attachment.id).to eq media_attachments.first.id expect(media_attachment.id).to eq media_attachments.first.id
@ -105,11 +105,11 @@ RSpec.describe TranslateStatusService do
describe 'target language is regional' do describe 'target language is regional' do
it 'uses regional variant' do it 'uses regional variant' do
expect(service.call(status, 'es-MX').language).to eq 'es-MX' expect(service.call(status, nil, 'es-MX').language).to eq 'es-MX'
end end
it 'uses parent locale for unsupported regional variant' do it 'uses parent locale for unsupported regional variant' do
expect(service.call(status, 'es-XX').language).to eq 'es' expect(service.call(status, nil, 'es-XX').language).to eq 'es'
end end
end end
end end