123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 |
- # frozen_string_literal: true
- class TranslateStatusService < BaseService
- CACHE_TTL = 1.day.freeze
- include ERB::Util
- include FormattingHelper
- def call(status, target_language)
- @status = status
- @source_texts = source_texts
- target_language = target_language.split(/[_-]/).first unless target_languages.include?(target_language)
- @target_language = target_language
- raise Mastodon::NotPermittedError unless permitted?
- status_translation = Rails.cache.fetch("v2:translations/#{@status.language}/#{@target_language}/#{content_hash}", expires_in: CACHE_TTL) do
- translations = translation_backend.translate(@source_texts.values, @status.language, @target_language)
- build_status_translation(translations)
- end
- status_translation.status = @status
- status_translation
- end
- private
- def translation_backend
- @translation_backend ||= TranslationService.configured
- end
- def permitted?
- return false unless @status.distributable? && TranslationService.configured?
- target_languages.include?(@target_language)
- end
- def languages
- Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { translation_backend.languages }
- end
- def target_languages
- languages[@status.language] || []
- end
- def content_hash
- Digest::SHA256.base64digest(@source_texts.transform_keys { |key| key.respond_to?(:id) ? "#{key.class}-#{key.id}" : key }.to_json)
- end
- def source_texts
- texts = {}
- texts[:content] = wrap_emoji_shortcodes(status_content_format(@status)) if @status.content.present?
- texts[:spoiler_text] = wrap_emoji_shortcodes(html_escape(@status.spoiler_text)) if @status.spoiler_text.present?
- @status.preloadable_poll&.loaded_options&.each do |option|
- texts[option] = wrap_emoji_shortcodes(html_escape(option.title))
- end
- @status.media_attachments.each do |media_attachment|
- texts[media_attachment] = html_escape(media_attachment.description)
- end
- texts
- end
- def build_status_translation(translations)
- status_translation = Translation.new(
- detected_source_language: translations.first&.detected_source_language,
- language: @target_language,
- provider: translations.first&.provider,
- content: '',
- spoiler_text: '',
- poll_options: [],
- media_attachments: []
- )
- @source_texts.keys.each_with_index do |source, index|
- translation = translations[index]
- case source
- when :content
- node = unwrap_emoji_shortcodes(translation.text)
- Sanitize.node!(node, Sanitize::Config::MASTODON_STRICT)
- status_translation.content = node.to_html
- when :spoiler_text
- status_translation.spoiler_text = unwrap_emoji_shortcodes(translation.text).content
- when Poll::Option
- status_translation.poll_options << Translation::Option.new(
- title: unwrap_emoji_shortcodes(translation.text).content
- )
- when MediaAttachment
- status_translation.media_attachments << Translation::MediaAttachment.new(
- id: source.id,
- description: html_entities.decode(translation.text)
- )
- end
- end
- status_translation
- end
- def wrap_emoji_shortcodes(text)
- EmojiFormatter.new(text, @status.emojis, { raw_shortcode: true }).to_s
- end
- def unwrap_emoji_shortcodes(html)
- fragment = Nokogiri::HTML5.fragment(html)
- fragment.css('span[translate="no"]').each do |element|
- element.remove_attribute('translate')
- element.replace(element.children) if element.attributes.empty?
- end
- fragment
- end
- def html_entities
- HTMLEntities.new
- end
- end
|