diff --git a/app/models/concerns/discourse_translator/translatable.rb b/app/models/concerns/discourse_translator/translatable.rb index 299bcd96..d608e612 100644 --- a/app/models/concerns/discourse_translator/translatable.rb +++ b/app/models/concerns/discourse_translator/translatable.rb @@ -15,6 +15,7 @@ def set_detected_locale(locale) # locales should be "en-US" instead of "en_US" per https://www.rfc-editor.org/rfc/rfc5646#section-2.1 locale = locale.to_s.gsub("_", "-") (content_locale || build_content_locale).update!(detected_locale: locale) + locale end # This method is used to create a translation for a translatable (Post or Topic) and a specific locale. diff --git a/app/services/discourse_translator/provider/amazon.rb b/app/services/discourse_translator/provider/amazon.rb index bfe2a321..dbd086b4 100644 --- a/app/services/discourse_translator/provider/amazon.rb +++ b/app/services/discourse_translator/provider/amazon.rb @@ -120,38 +120,25 @@ def self.detect!(topic_or_post) end end - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - detected_lang = detect(translatable) + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) + raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked] + text = text_for_translation(post, raw:) + translate_text!(text, target_locale_sym) + end - begin - client.translate_text( - { - text: truncate(text_for_translation(translatable)), - source_language_code: "auto", - target_language_code: SUPPORTED_LANG_MAPPING[target_locale_sym], - }, - ) - rescue Aws::Translate::Errors::UnsupportedLanguagePairException - raise I18n.t( - "translator.failed.#{translatable.class.name.downcase}", - source_locale: detected_lang, - target_locale: target_locale_sym, - ) - end + def self.translate_topic!(topic, target_locale_sym = I18n.locale) + text = text_for_translation(topic) + translate_text!(text, target_locale_sym) end def self.translate_text!(text, target_locale_sym = I18n.locale) - begin - client.translate_text( - { - text: truncate(text), - source_language_code: "auto", - target_language_code: SUPPORTED_LANG_MAPPING[target_locale_sym], - }, - ) - rescue Aws::Translate::Errors::UnsupportedLanguagePairException - raise I18n.t("translator.not_supported") - end + client.translate_text( + { + text: truncate(text), + source_language_code: "auto", + target_language_code: SUPPORTED_LANG_MAPPING[target_locale_sym], + }, + )&.translated_text end def self.client diff --git a/app/services/discourse_translator/provider/base_provider.rb b/app/services/discourse_translator/provider/base_provider.rb index 7c3f2429..b02d3638 100644 --- a/app/services/discourse_translator/provider/base_provider.rb +++ b/app/services/discourse_translator/provider/base_provider.rb @@ -25,12 +25,11 @@ def self.cache_key "#{key_prefix}#{access_token_key}" end - # Returns the stored translation of a post or topic. - # If the translation does not exist yet, it will be translated first via the API then stored. + # Translates and saves it into a PostTranslation/TopicTranslation # If the detected language is the same as the target language, the original text will be returned. # @param translatable [Post|Topic] + # @return [Array] the detected language and the translated text def self.translate(translatable, target_locale_sym = I18n.locale) - return if text_for_translation(translatable).blank? detected_lang = detect(translatable) if translatable.locale_matches?(target_locale_sym) @@ -50,26 +49,38 @@ def self.translate(translatable, target_locale_sym = I18n.locale) ) end - translated = translate_translatable!(translatable, target_locale_sym) - save_translation(translatable, target_locale_sym) { translated } - [detected_lang, translated] - end + begin + begin + translated = + case translatable.class.name + when "Post" + translate_post!(translatable, target_locale_sym, { cooked: true }) + when "Topic" + translate_topic!(translatable, target_locale_sym) + end + end + rescue => e + raise I18n.t( + "translator.failed.#{translatable.class.name.downcase}", + source_locale: detected_lang, + target_locale: target_locale_sym, + ) + end - # TODO: Deprecate this in favour of translate_ - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - raise "Not Implemented" + translatable.set_translation(target_locale_sym, translated) + [detected_lang, translated] end def self.translate_text!(text, target_locale_sym = I18n.locale) raise "Not Implemented" end - def self.translate_post!(post, target_locale_sym = I18n.locale) - translate_translatable!(post, target_locale_sym) + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) + raise "Not Implemented" end def self.translate_topic!(topic, target_locale_sym = I18n.locale) - translate_translatable!(topic, target_locale_sym) + raise "Not Implemented" end # Returns the stored detected locale of a post or topic. @@ -77,8 +88,7 @@ def self.translate_topic!(topic, target_locale_sym = I18n.locale) # @param translatable [Post|Topic] def self.detect(translatable) return if text_for_detection(translatable).blank? - get_detected_locale(translatable) || - save_detected_locale(translatable) { detect!(translatable) } + translatable.detected_locale || translatable.set_detected_locale(detect!(translatable)) end # Subclasses must implement this method to detect the text of a post or topic @@ -94,29 +104,6 @@ def self.access_token raise "Not Implemented" end - def self.save_translation(translatable, target_locale_sym = I18n.locale) - begin - translation = yield - rescue Timeout::Error - raise TranslatorError.new(I18n.t("translator.api_timeout")) - end - translatable.set_translation(target_locale_sym, translation) - translation - end - - def self.get_detected_locale(translatable) - translatable.detected_locale - end - - def self.save_detected_locale(translatable) - # sometimes we may have a user post that is just an emoji - # in that case, we will just indicate the post is in the default locale - detected_locale = yield.presence || SiteSetting.default_locale - translatable.set_detected_locale(detected_locale) - - detected_locale - end - def self.language_supported?(detected_lang) raise NotImplementedError unless self.const_defined?(:SUPPORTED_LANG_MAPPING) supported_lang = const_get(:SUPPORTED_LANG_MAPPING) diff --git a/app/services/discourse_translator/provider/discourse_ai.rb b/app/services/discourse_translator/provider/discourse_ai.rb index 8db9248b..b53beb7b 100644 --- a/app/services/discourse_translator/provider/discourse_ai.rb +++ b/app/services/discourse_translator/provider/discourse_ai.rb @@ -15,33 +15,22 @@ def self.detect!(topic_or_post) ::DiscourseAi::LanguageDetector.new(text_for_detection(topic_or_post)).detect end - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - if (translatable.class.name == "Post") - translate_post!(translatable, target_locale_sym) - elsif (translatable.class.name == "Topic") - translate_topic!(translatable, target_locale_sym) - end - end - - def self.translate_post!(post, target_locale_sym = I18n.locale) + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) validate_required_settings! - text = text_for_translation(post, raw: true) + raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked] + text = text_for_translation(post, raw:) chunks = DiscourseTranslator::ContentSplitter.split(text) - translated = - chunks - .map { |chunk| ::DiscourseAi::PostTranslator.new(chunk, target_locale_sym).translate } - .join("") - DiscourseTranslator::TranslatedContentNormalizer.normalize(post, translated) + chunks + .map { |chunk| ::DiscourseAi::PostTranslator.new(chunk, target_locale_sym).translate } + .join("") end def self.translate_topic!(topic, target_locale_sym = I18n.locale) validate_required_settings! language = get_language_name(target_locale_sym) - translated = - ::DiscourseAi::TopicTranslator.new(text_for_translation(topic), language).translate - DiscourseTranslator::TranslatedContentNormalizer.normalize(topic, translated) + ::DiscourseAi::TopicTranslator.new(text_for_translation(topic), language).translate end def self.translate_text!(text, target_locale_sym = I18n.locale) diff --git a/app/services/discourse_translator/provider/google.rb b/app/services/discourse_translator/provider/google.rb index 38f32607..c7a8f817 100644 --- a/app/services/discourse_translator/provider/google.rb +++ b/app/services/discourse_translator/provider/google.rb @@ -96,14 +96,15 @@ def self.translate_supported?(source, target) end end - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - res = - result( - TRANSLATE_URI, - q: text_for_translation(translatable), - target: SUPPORTED_LANG_MAPPING[target_locale_sym], - ) - res["translations"][0]["translatedText"] + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) + raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked] + text = text_for_translation(post, raw:) + translate_text!(text, target_locale_sym) + end + + def self.translate_topic!(topic, target_locale_sym = I18n.locale) + text = text_for_translation(topic) + translate_text!(text, target_locale_sym) end def self.translate_text!(text, target_locale_sym = I18n.locale) diff --git a/app/services/discourse_translator/provider/libre_translate.rb b/app/services/discourse_translator/provider/libre_translate.rb index 2ccfce96..680c15f0 100644 --- a/app/services/discourse_translator/provider/libre_translate.rb +++ b/app/services/discourse_translator/provider/libre_translate.rb @@ -91,18 +91,19 @@ def self.translate_supported?(source, target) res.any? { |obj| obj["code"] == source } && res.any? { |obj| obj["code"] == lang } end - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - detected_lang = detect(translatable) + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) + raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked] + text = text_for_translation(post, raw:) - res = - result( - translate_uri, - q: text_for_translation(translatable), - source: detected_lang, - target: SUPPORTED_LANG_MAPPING[target_locale_sym], - format: "html", - ) - res["translatedText"] + detected_lang = detect(post) + + send_for_translation(text, detected_lang, target_locale_sym) + end + + def self.translate_topic!(topic, target_locale_sym = I18n.locale) + detected_lang = detect(topic) + text = text_for_translation(topic) + send_for_translation(text, detected_lang, target_locale_sym) end def self.translate_text!(text, target_locale_sym = I18n.locale) @@ -154,6 +155,20 @@ def self.result(url, body) body end end + + private + + def self.send_for_translation(text, source_locale, target_locale) + res = + result( + translate_uri, + q: text, + source: source_locale, + target: SUPPORTED_LANG_MAPPING[target_locale], + format: "html", + ) + res["translatedText"] + end end end end diff --git a/app/services/discourse_translator/provider/microsoft.rb b/app/services/discourse_translator/provider/microsoft.rb index 7c05815b..cfb2c855 100644 --- a/app/services/discourse_translator/provider/microsoft.rb +++ b/app/services/discourse_translator/provider/microsoft.rb @@ -154,27 +154,24 @@ def self.detect!(topic_or_post) result(uri.to_s, body, default_headers).first["language"] end - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - detected_lang = detect(translatable) + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) + raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked] + text = text_for_translation(post, raw:) - if text_for_translation(translatable).length > LENGTH_LIMIT - raise TranslatorError.new(I18n.t("translator.too_long")) - end - locale = - SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported")) + raise TranslatorError.new(I18n.t("translator.too_long")) if text.length > LENGTH_LIMIT - query = default_query.merge("from" => detected_lang, "to" => locale, "textType" => "html") - body = [{ "Text" => text_for_translation(translatable) }].to_json - uri = URI(translate_endpoint) - uri.query = URI.encode_www_form(query) - response_body = result(uri.to_s, body, default_headers) - response_body.first["translations"].first["text"] + translate_text!(text, target_locale_sym) end - def self.translate_text!(text, target_locale_sym = I18n.locale) - locale = - SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported")) + def self.translate_topic!(topic, target_locale_sym = I18n.locale) + text = text_for_translation(topic) + raise TranslatorError.new(I18n.t("translator.too_long")) if text.length > LENGTH_LIMIT + + translate_text!(text, target_locale_sym) + end + def self.translate_text!(text, target_locale_sym = I18n.locale) + locale = SUPPORTED_LANG_MAPPING[target_locale_sym] query = default_query.merge("to" => locale, "textType" => "html") body = [{ "Text" => text }].to_json uri = URI(translate_endpoint) diff --git a/app/services/discourse_translator/provider/yandex.rb b/app/services/discourse_translator/provider/yandex.rb index 45f65ecf..95b6591e 100644 --- a/app/services/discourse_translator/provider/yandex.rb +++ b/app/services/discourse_translator/provider/yandex.rb @@ -129,26 +129,25 @@ def self.detect!(topic_or_post) result(uri.to_s, "", default_headers)["lang"] end - def self.translate_translatable!(translatable, target_locale_sym = I18n.locale) - detected_lang = detect(translatable) - locale = - SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported")) + def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {}) + raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked] + text = text_for_translation(post, raw:) - query = - default_query.merge( - "lang" => "#{detected_lang}-#{locale}", - "text" => text_for_translation(translatable), - "format" => "html", - ) + detected_lang = detect(post) + locale = SUPPORTED_LANG_MAPPING[target_locale_sym] - uri = URI(TRANSLATE_URI) - uri.query = URI.encode_www_form(query) + send_for_translation(text, detected_lang, locale) + end - response_body = result(uri.to_s, "", default_headers) - response_body["text"][0] + def self.translate_topic!(topic, target_locale_sym = I18n.locale) + detected_lang = detect(topic) + locale = SUPPORTED_LANG_MAPPING[target_locale_sym] + text = text_for_translation(topic) + + send_for_translation(text, detected_lang, locale) end - def self.translate_text!(translatable, target_locale_sym = I18n.locale) + def self.translate_text!(text, target_locale_sym = I18n.locale) # Not supported for v1.5 https://translate.yandex.com/developers raise TranslatorError.new(I18n.t("translator.not_supported")) end @@ -160,6 +159,21 @@ def self.translate_supported?(detected_lang, target_lang) private + def self.send_for_translation(text, source_locale, target_locale) + query = + default_query.merge( + "lang" => "#{source_locale}-#{target_locale}", + "text" => text, + "format" => "html", + ) + + uri = URI(TRANSLATE_URI) + uri.query = URI.encode_www_form(query) + + response_body = result(uri.to_s, "", default_headers) + response_body["text"][0] + end + def self.post(uri, body, headers = {}) Excon.post(uri, body: body, headers: headers) end diff --git a/spec/services/amazon_spec.rb b/spec/services/amazon_spec.rb index 6b51de74..df07c180 100644 --- a/spec/services/amazon_spec.rb +++ b/spec/services/amazon_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -require "rails_helper" - -RSpec.describe DiscourseTranslator::Provider::Amazon do - let(:mock_response) { Struct.new(:status, :body) } +describe DiscourseTranslator::Provider::Amazon do + def new_translate_client + client = Aws::Translate::Client.new(stub_responses: true) + Aws::Translate::Client.stubs(:new).returns(client) + client + end describe ".truncate" do it "truncates text to 10000 bytes" do @@ -18,23 +20,20 @@ describe ".detect" do let(:post) { Fabricate(:post) } - let!(:client) { Aws::Translate::Client.new(stub_responses: true) } let(:text) { described_class.truncate(post.cooked) } let(:detected_lang) { "en" } - before do + it "should store the detected language" do + client = new_translate_client client.stub_responses( :translate_text, { - translated_text: "Probando traducciones", - source_language_code: "en", - target_language_code: "es", + translated_text: "translated text", + source_language_code: detected_lang, + target_language_code: "de", }, ) - Aws::Translate::Client.stubs(:new).returns(client) - end - it "should store the detected language in a custom field" do expect(described_class.detect(post)).to eq(detected_lang) expect(post.detected_locale).to eq(detected_lang) @@ -46,53 +45,95 @@ end end - describe ".translate_translatable!" do - let(:post) { Fabricate(:post) } - let!(:client) { Aws::Translate::Client.new(stub_responses: true) } + describe ".translate_post!" do + fab!(:post) { Fabricate(:post, raw: "rawraw rawrawraw", cooked: "coocoo coococooo") } before do + post.set_detected_locale("en") + I18n.locale = :de + end + + it "translates post with raw" do + client = new_translate_client client.stub_responses( :translate_text, - "UnsupportedLanguagePairException", { - translated_text: "Probando traducciones", + translated_text: "translated text", source_language_code: "en", - target_language_code: "es", + target_language_code: "de", }, ) - described_class.stubs(:client).returns(client) - post.set_detected_locale("en") - I18n.stubs(:locale).returns(:es) + + expect(described_class.translate_post!(post, :de, { raw: true })).to eq("translated text") end - it "raises an error when trying to translate an unsupported language" do - expect { described_class.translate_translatable!(post) }.to raise_error( - I18n.t("translator.failed.post", source_locale: "en", target_locale: "es"), + it "translates post with cooked" do + client = new_translate_client + client.stub_responses( + :translate_text, + { + translated_text: "translated text", + source_language_code: "en", + target_language_code: "de", + }, ) + + expect(described_class.translate_post!(post, :de, { cooked: true })).to eq("translated text") + end + + it "translates post with raw when unspecified" do + client = new_translate_client + client.stub_responses( + :translate_text, + { + translated_text: "translated text", + source_language_code: "en", + target_language_code: "de", + }, + ) + + expect(described_class.translate_post!(post, :de)).to eq("translated text") end end - describe ".translate_text!" do - let!(:client) { Aws::Translate::Client.new(stub_responses: true) } + describe ".translate_topic!" do + fab!(:topic) before do + topic.set_detected_locale("en") + I18n.locale = :de + end + + it "translates topic's title" do + client = new_translate_client client.stub_responses( :translate_text, - "UnsupportedLanguagePairException", { - translated_text: "Probando traducciones", + translated_text: "translated text", source_language_code: "en", - target_language_code: "es", + target_language_code: "de", }, ) - described_class.stubs(:client).returns(client) - I18n.stubs(:locale).returns(:es) + + expect(described_class.translate_topic!(topic, :de)).to eq("translated text") end + end + + describe ".translate_text!" do + before { I18n.locale = :es } - it "raises an error when trying to translate an unsupported language" do - expect { described_class.translate_text!("derp") }.to raise_error( - I18n.t("translator.not_supported", source_locale: "en", target_locale: "es"), + it "translates the text" do + client = new_translate_client + client.stub_responses( + :translate_text, + { + translated_text: "translated text", + source_language_code: "en", + target_language_code: "es", + }, ) + + expect(described_class.translate_text!("derp")).to eq("translated text") end end end diff --git a/spec/services/base_provider_spec.rb b/spec/services/base_provider_spec.rb index 122b8f4d..5a4b630e 100644 --- a/spec/services/base_provider_spec.rb +++ b/spec/services/base_provider_spec.rb @@ -84,20 +84,12 @@ class EmptyTranslator < DiscourseTranslator::Provider::BaseProvider end it "returns cached detection if available" do - TestTranslator.save_detected_locale(post) { "en" } + post.set_detected_locale("en") TestTranslator.expects(:detect!).never expect(TestTranslator.detect(post)).to eq("en") end - it "saves the site default locale when detection is empty" do - SiteSetting.default_locale = "ja" - - TestTranslator.save_detected_locale(post) { "" } - - expect(post.detected_locale).to eq("ja") - end - it "performs detection if no cached result" do TestTranslator.expects(:detect!).with(post).returns("es") @@ -108,39 +100,27 @@ class EmptyTranslator < DiscourseTranslator::Provider::BaseProvider describe ".translate" do fab!(:post) - it "returns nil when text is blank" do - post.cooked = "" - expect(TestTranslator.translate(post)).to be_nil - end - it "returns original text when detected language matches current locale" do - TestTranslator.save_detected_locale(post) { I18n.locale.to_s } + post.set_detected_locale(I18n.locale.to_s) post.cooked = "hello" expect(TestTranslator.translate(post)).to eq(%w[en hello]) end it "returns cached translation if available" do - TestTranslator.save_detected_locale(post) { "es" } - TestTranslator.save_translation(post) { "hello" } + post.set_detected_locale("es") + post.set_translation(I18n.locale, "hello") expect(TestTranslator.translate(post)).to eq(%w[es hello]) end it "raises error when translation not supported" do - TestTranslator.save_detected_locale(post) { "xx" } + post.set_detected_locale("xx") TestTranslator.expects(:translate_supported?).with("xx", :en).returns(false) expect { TestTranslator.translate(post) }.to raise_error( DiscourseTranslator::Provider::TranslatorError, ) end - - it "performs translation when needed" do - TestTranslator.save_detected_locale(post) { "es" } - TestTranslator.expects(:translate_translatable!).returns("hello") - - expect(TestTranslator.translate(post)).to eq(%w[es hello]) - end end end diff --git a/spec/services/discourse_ai_spec.rb b/spec/services/discourse_ai_spec.rb index e93e1ece..97bfac46 100644 --- a/spec/services/discourse_ai_spec.rb +++ b/spec/services/discourse_ai_spec.rb @@ -34,7 +34,7 @@ end end - describe ".translate_translatable!" do + describe ".translate_post!" do before do post.set_detected_locale("de") topic.set_detected_locale("de") @@ -42,15 +42,7 @@ it "translates the post and returns [locale, translated_text]" do DiscourseAi::Completions::Llm.with_prepared_responses(["some translated text"]) do - translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_translatable!(post) - expect(translated_text).to eq "

some translated text

" - end - end - - it "translates the topic" do - allow(::DiscourseAi::TopicTranslator).to receive(:new).and_call_original - DiscourseAi::Completions::Llm.with_prepared_responses(["some translated text"]) do - translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_translatable!(topic) + translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_post!(post) expect(translated_text).to eq "some translated text" end end @@ -58,9 +50,17 @@ it "sends the content for splitting and the split content for translation" do post.update(raw: "#{"a" * 3000} #{"b" * 3000}") DiscourseAi::Completions::Llm.with_prepared_responses(%w[lol wut]) do - expect( - DiscourseTranslator::Provider::DiscourseAi.translate_translatable!(post), - ).to eq "

lolwut

" + expect(DiscourseTranslator::Provider::DiscourseAi.translate_post!(post)).to eq "lolwut" + end + end + end + + describe ".translate_topic!" do + it "translates the topic" do + allow(::DiscourseAi::TopicTranslator).to receive(:new).and_call_original + DiscourseAi::Completions::Llm.with_prepared_responses(["some translated text"]) do + translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_topic!(topic) + expect(translated_text).to eq "some translated text" end end end diff --git a/spec/services/google_spec.rb b/spec/services/google_spec.rb index bfe1ec32..8d0eaecc 100644 --- a/spec/services/google_spec.rb +++ b/spec/services/google_spec.rb @@ -1,14 +1,26 @@ # frozen_string_literal: true -require "rails_helper" - -RSpec.describe DiscourseTranslator::Provider::Google do +describe DiscourseTranslator::Provider::Google do let(:api_key) { "12345" } + let(:mock_response) { Struct.new(:status, :body) } + before do SiteSetting.translator_enabled = true SiteSetting.translator_google_api_key = api_key end - let(:mock_response) { Struct.new(:status, :body) } + + def stub_translate_request(text, target_locale, translated_text) + stub_request(:post, DiscourseTranslator::Provider::Google::TRANSLATE_URI).with( + body: URI.encode_www_form({ q: text, target: target_locale, key: api_key }), + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + "Referer" => "http://test.localhost", + }, + ).to_return( + status: 200, + body: %{ { "data": { "translations": [ { "translatedText": "#{translated_text}" } ] } } }, + ) + end describe ".access_token" do describe "when set" do @@ -100,110 +112,58 @@ end end - describe ".translate_translatable!" do - let(:post) { Fabricate(:post) } + describe ".translate_post!" do + fab!(:post) { Fabricate(:post, raw: "rawraw rawrawraw", cooked: "coocoo coococooo") } - it "raises an error and warns admin on failure" do - described_class.expects(:access_token).returns(api_key) - described_class.expects(:detect).returns("__") + before do + post.set_detected_locale("en") + I18n.locale = :de + end - stub_request(:post, DiscourseTranslator::Provider::Google::SUPPORT_URI).to_return( - status: 400, - body: { - error: { - code: "400", - message: "API key not valid. Please pass a valid API key.", - }, - }.to_json, - ) + it "translates post with raw" do + translated_text = "translated raw" + stub_translate_request(post.raw, "de", translated_text) - ProblemCheckTracker[:translator_error].no_problem! + expect(described_class.translate_post!(post, :de, { raw: true })).to eq(translated_text) + end - expect { described_class.translate(post) }.to raise_error( - DiscourseTranslator::Provider::ProblemCheckedTranslationError, - ) + it "translates post with cooked" do + translated_text = "translated cooked" + stub_translate_request(post.cooked, "de", translated_text) - expect(AdminNotice.problem.last.message).to eq( - I18n.t( - "dashboard.problem.translator_error", - locale: "en", - provider: "Google", - code: 400, - message: "API key not valid. Please pass a valid API key.", - ), - ) + expect(described_class.translate_post!(post, :de, { cooked: true })).to eq(translated_text) end - it "raises an error when the response is not JSON" do - described_class.expects(:access_token).returns(api_key) - described_class.expects(:detect).returns("__") - - Excon.expects(:post).returns(mock_response.new(413, "some html")) + it "translates post with raw when unspecified" do + translated_text = "translated raw" + stub_translate_request(post.raw, "de", translated_text) - expect { - described_class.translate(post) - }.to raise_error DiscourseTranslator::Provider::TranslatorError + expect(described_class.translate_post!(post, :de)).to eq(translated_text) end + end - it "returns error with source and target locale when translation is not supported" do - post.set_detected_locale("cat") - I18n.stubs(:locale).returns(:dog) - - Excon.expects(:post).returns( - mock_response.new(200, %{ { "data": { "languages": [ { "language": "kit" }] } } }), - ) + describe ".translate_topic!" do + fab!(:topic) - expect { described_class.translate(post) }.to raise_error( - I18n.t("translator.failed.post", source_locale: "cat", target_locale: "dog"), - ) + before do + topic.set_detected_locale("en") + I18n.locale = :de end - it "truncates text for translation to max_characters_per_translation setting" do - SiteSetting.max_characters_per_translation = 50 - post.cooked = "a" * 100 - post.set_detected_locale("de") - body = { - q: post.cooked.truncate(SiteSetting.max_characters_per_translation, omission: nil), - target: "en", - key: api_key, - } + it "translates topic's title" do + translated_text = "translated title" + stub_translate_request(topic.title, "de", translated_text) - translated_text = "hur dur hur dur" - stub_request(:post, DiscourseTranslator::Provider::Google::SUPPORT_URI).to_return( - status: 200, - body: %{ { "data": { "languages": [ { "language": "de" }] } } }, - ) - stub_request(:post, DiscourseTranslator::Provider::Google::TRANSLATE_URI).with( - body: URI.encode_www_form(body), - headers: { - "Content-Type" => "application/x-www-form-urlencoded", - "Referer" => "http://test.localhost", - }, - ).to_return( - status: 200, - body: %{ { "data": { "translations": [ { "translatedText": "#{translated_text}" } ] } } }, - ) - - expect(described_class.translate_translatable!(post)).to eq(translated_text) + expect(described_class.translate_topic!(topic, :de)).to eq(translated_text) end end describe ".translate_text!" do it "translates plain text" do text = "ABCDEFG" - body = { q: text, target: "ja", key: api_key } - - translated_text = "hur dur hur dur" - stub_request(:post, DiscourseTranslator::Provider::Google::TRANSLATE_URI).with( - body: URI.encode_www_form(body), - headers: { - "Content-Type" => "application/x-www-form-urlencoded", - "Referer" => "http://test.localhost", - }, - ).to_return( - status: 200, - body: %{ { "data": { "translations": [ { "translatedText": "#{translated_text}" } ] } } }, - ) + target_locale = "ja" + translated_text = "あいうえお" + stub_translate_request(text, target_locale, translated_text) expect(described_class.translate_text!(text, :ja)).to eq(translated_text) end diff --git a/spec/services/microsoft_spec.rb b/spec/services/microsoft_spec.rb index 5560b586..fc3ed7e9 100644 --- a/spec/services/microsoft_spec.rb +++ b/spec/services/microsoft_spec.rb @@ -1,18 +1,26 @@ # frozen_string_literal: true RSpec.describe DiscourseTranslator::Provider::Microsoft do - before { SiteSetting.translator_enabled = true } + before do + SiteSetting.translator_enabled = true + SiteSetting.translator_azure_subscription_key = "e1bba646088021aaf1ef972a48" + end after { Discourse.redis.del(described_class.cache_key) } - def translate_endpoint(from: "en", to: I18n.locale) + def translate_endpoint(to: I18n.locale) uri = URI(described_class.translate_endpoint) default_query = described_class.default_query.merge("textType" => "html") - default_query = default_query.merge("from" => from) if from default_query = default_query.merge("to" => to) if to uri.query = URI.encode_www_form(default_query) uri.to_s end + def stub_translate_request(source_text, target_locale, translated_text) + stub_request(:post, translate_endpoint(to: target_locale)).with( + { body: [{ "Text" => source_text }].to_json }, + ).to_return(status: 200, body: [{ "translations" => [{ "text" => translated_text }] }].to_json) + end + describe ".detect" do let(:post) { Fabricate(:post) } let(:detected_lang) { "en" } @@ -110,6 +118,7 @@ def detect_endpoint context "without azure key" do it "raise a MicrosoftNoAzureKeyError" do + SiteSetting.translator_azure_subscription_key = "" expect { described_class.detect(post) }.to raise_error( DiscourseTranslator::Provider::ProblemCheckedTranslationError, I18n.t("translator.microsoft.missing_key"), @@ -118,104 +127,65 @@ def detect_endpoint end end - describe ".translate" do - let(:post) { Fabricate(:post) } + describe ".translate_post!" do + fab!(:post) { Fabricate(:post, raw: "rawraw rawrawraw", cooked: "coocoo coococooo") } before do post.set_detected_locale("en") - SiteSetting.translator_azure_subscription_key = "e1bba646088021aaf1ef972a48" + I18n.locale = :de end - shared_examples "post translated" do - it "translates post" do - I18n.locale = "de" + it "translates post with raw" do + translated_text = "some text" + target_locale = "de" + stub_translate_request(post.raw, target_locale, translated_text) - stub_request(:post, translate_endpoint).to_return( - status: 200, - body: [{ "translations" => [{ "text" => "some de text" }] }].to_json, - ) - - expect(described_class.translate(post)).to eq(["en", "some de text"]) - end + expect(described_class.translate_post!(post, :de, { raw: true })).to eq(translated_text) end - context "with a custom endpoint" do - before { SiteSetting.translator_azure_custom_subdomain = "translator19191" } + it "translates post with cooked" do + translated_text = "some text" + target_locale = "de" + stub_translate_request(post.cooked, target_locale, translated_text) - include_examples "post translated" + expect(described_class.translate_post!(post, :de, { cooked: true })).to eq(translated_text) end - context "without a custom endpoint" do - include_examples "post translated" + it "translates post with raw when unspecified" do + translated_text = "some text" + target_locale = "de" + stub_translate_request(post.raw, target_locale, translated_text) - it "returns stored translation if post has already been translated" do - I18n.locale = "en" - - post.set_detected_locale("tr") - post.set_translation("en", "some english text") - - expect(described_class.translate(post)).to eq(["tr", "some english text"]) - end - - it "raises an error if detected language of the post is not supported" do - post.set_detected_locale("donkey") - - expect { described_class.translate(post) }.to raise_error( - DiscourseTranslator::Provider::TranslatorError, - I18n.t("translator.failed.post", source_locale: "donkey", target_locale: I18n.locale), - ) - end + expect(described_class.translate_post!(post, :de)).to eq(translated_text) + end + end - it "raises an error if the post is too long to be translated" do - I18n.locale = "ja" - SiteSetting.max_characters_per_translation = 100_000 - post.update_columns( - cooked: "*" * (DiscourseTranslator::Provider::Microsoft::LENGTH_LIMIT + 1), - ) + describe ".translate_topic!" do + fab!(:topic) - expect { described_class.translate(post) }.to raise_error( - DiscourseTranslator::Provider::TranslatorError, - I18n.t("translator.too_long"), - ) - end + before do + topic.set_detected_locale("en") + I18n.locale = :de + end - it "raises an error on failure" do - I18n.locale = "ja" - stub_request( - :post, - "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from=en&textType=html&to=ja", - ).with( - body: "[{\"Text\":\"\\u003cp\\u003eHello world\\u003c/p\\u003e\"}]", - headers: { - "Ocp-Apim-Subscription-Key" => SiteSetting.translator_azure_subscription_key, - "Content-Type" => "application/json", - }, - ).to_return( - status: 400, - body: { - error: "something went wrong", - error_description: "you passed in a wrong param", - }.to_json, - ) + it "translates topic's title" do + translated_text = "some text" + target_locale = "de" + stub_translate_request(topic.title, target_locale, translated_text) - expect { described_class.translate(post) }.to raise_error( - DiscourseTranslator::Provider::TranslatorError, - ) - end + expect(described_class.translate_topic!(topic, :de)).to eq(translated_text) end end describe ".translate_text!" do it "translates text" do - text = "ABCDEFG" - SiteSetting.translator_azure_subscription_key = "123123" - I18n.locale = :es - stub_request(:post, translate_endpoint(from: nil, to: "es")).with( - { body: [{ "Text" => text }].to_json }, - ).to_return(status: 200, body: [{ "translations" => [{ "text" => "some text" }] }].to_json) - expect(described_class.translate_text!(text)).to eq("some text") + text = "ABCDEFG" + translated_text = "some text" + stub_translate_request(text, "es", translated_text) + + expect(described_class.translate_text!(text)).to eq(translated_text) end end end diff --git a/spec/services/yandex_spec.rb b/spec/services/yandex_spec.rb index 85fe6b5f..a5b23470 100644 --- a/spec/services/yandex_spec.rb +++ b/spec/services/yandex_spec.rb @@ -1,7 +1,31 @@ # frozen_string_literal: true RSpec.describe DiscourseTranslator::Provider::Yandex do - let(:mock_response) { Struct.new(:status, :body) } + fab!(:post) + + def detect_endpoint(text) + described_class.expects(:access_token).returns("12345") + URI(described_class::DETECT_URI) + .tap { |uri| uri.query = URI.encode_www_form({ "key" => "12345", "text" => text }) } + .to_s + end + + def translate_endpoint(text, source_lang, target_lang) + described_class.expects(:access_token).returns("12345") + URI(described_class::TRANSLATE_URI) + .tap do |uri| + uri.query = + URI.encode_www_form( + { + "key" => "12345", + "text" => text, + "lang" => "#{source_lang}-#{target_lang}", + "format" => "html", + }, + ) + end + .to_s + end describe ".access_token" do describe "when set" do @@ -14,42 +38,29 @@ end end - describe ".detect" do - let(:post) { Fabricate(:post) } - - it "should store the detected language in a custom field" do + describe ".detect!" do + it "gets the detected language" do detected_lang = "en" - described_class.expects(:access_token).returns("12345") - Excon - .expects(:post) - .returns(mock_response.new(200, %{ { "code": 200, "lang": "#{detected_lang}" } })) - .once - expect(described_class.detect(post)).to eq(detected_lang) - - expect(post.detected_locale).to eq(detected_lang) + stub_request(:post, detect_endpoint(post.raw)).to_return( + status: 200, + body: { lang: "#{detected_lang}" }.to_json, + ) + expect(described_class.detect!(post)).to eq(detected_lang) end end - describe ".translate" do - let(:post) { Fabricate(:post) } - - it "raises an error on failure" do - described_class.expects(:access_token).returns("12345") + describe ".translate_post" do + it "translates the post" do + translated_text = "translated text" described_class.expects(:detect).at_least_once.returns("de") - Excon.expects(:post).returns( - mock_response.new( - 400, - { - error: "something went wrong", - error_description: "you passed in a wrong param", - }.to_json, - ), + stub_request(:post, translate_endpoint(post.raw, "de", I18n.locale)).to_return( + status: 200, + body: { "text" => [translated_text] }.to_json, ) - expect { - described_class.translate(post) - }.to raise_error DiscourseTranslator::Provider::TranslatorError + translation = described_class.translate_post!(post) + expect(translation).to eq(translated_text) end end end