discourse-translator/spec/services/google_spec.rb
Natalie Tay dbd9af6866
FIX: Skip locale region for Google Translate API (#263)
There is some peculiar behaviour with Google Translate API (translation provider) right now that requires this change.

### Extra context: 

For each translation provider, we can make an API call to get the language code (locale) of a piece of text. This can be things like "en", "pt", "zh", "pt-PT", "zh-CN".

We use the result here to determine if the user is able to translate the post or not. If the user is in the same locale as the post, we do not allow them to translate since it is already in a readable language.

For some translation providers, we take this language code and tell the API "please translate this content from pt-PT to en". For some other providers, we can just do "please translate this content to en".

### This fix

Google Translate API is returning "bn-Latn" for some texts. However, 
1. they do not support translation from "bn-Latn" to "en", 
2. they do support translation from "bn" to "en", 
3. same piece of text can be directly translated to "en" without supplying the source locale ("bn-Latn") 🤯 

This fix does two things specific to the Google provider:
- Re-check if an unsupported language is supported, by removing the region part of the locale
- Send content for translation without indicating the source language
2025-03-28 16:21:03 +08:00

184 lines
5.7 KiB
Ruby

# frozen_string_literal: true
require "rails_helper"
RSpec.describe DiscourseTranslator::Google do
let(:api_key) { "12345" }
before do
SiteSetting.translator_enabled = true
SiteSetting.translator_google_api_key = api_key
end
let(:mock_response) { Struct.new(:status, :body) }
describe ".access_token" do
describe "when set" do
it "should return back translator_google_api_key" do
expect(described_class.access_token).to eq(api_key)
end
end
end
describe ".detect" do
fab!(:post)
it "should store the detected language in a custom field" do
detected_lang = "en"
described_class.expects(:access_token).returns(api_key)
Excon
.expects(:post)
.returns(
mock_response.new(
200,
%{ { "data": { "detections": [ [ { "language": "#{detected_lang}", "isReliable": false, "confidence": 0.18397073 } ] ] } } },
),
)
.once
expect(described_class.detect(post)).to eq(detected_lang)
expect(post.detected_locale).to eq(detected_lang)
end
it "should truncate string to 1000 characters" do
length = 2000
post.raw = rand(36**length).to_s(36)
detected_lang = "en"
request_url = "#{DiscourseTranslator::Google::DETECT_URI}"
body = {
q: post.raw.truncate(DiscourseTranslator::Google::DETECTION_CHAR_LIMIT, omission: nil),
key: api_key,
}
Excon
.expects(:post)
.with(
request_url,
body: URI.encode_www_form(body),
headers: {
"Content-Type" => "application/x-www-form-urlencoded",
"Referer" => "http://test.localhost",
},
)
.returns(
mock_response.new(
200,
%{ { "data": { "detections": [ [ { "language": "#{detected_lang}", "isReliable": false, "confidence": 0.18397073 } ] ] } } },
),
)
.once
expect(described_class.detect(post)).to eq(detected_lang)
end
end
describe ".translate_supported?" do
let(:topic) { Fabricate(:topic, title: "This title is in english") }
it "equates source language to target" do
source = "en"
target = "fr"
stub_request(:post, DiscourseTranslator::Google::SUPPORT_URI).to_return(
status: 200,
body: %{ { "data": { "languages": [ { "language": "#{source}" }] } } },
)
expect(described_class.translate_supported?(source, target)).to be true
end
it "checks again without -* when the source language is not supported" do
source = "en"
target = "fr"
stub_request(:post, DiscourseTranslator::Google::SUPPORT_URI).to_return(
status: 200,
body: %{ { "data": { "languages": [ { "language": "#{source}" }] } } },
)
expect(described_class.translate_supported?("en-GB", target)).to be true
end
end
describe ".translate!" do
let(:post) { Fabricate(:post) }
it "raises an error and warns admin on failure" do
described_class.expects(:access_token).returns(api_key)
described_class.expects(:detect).returns("__")
stub_request(:post, DiscourseTranslator::Google::SUPPORT_URI).to_return(
status: 400,
body: {
error: {
code: "400",
message: "API key not valid. Please pass a valid API key.",
},
}.to_json,
)
ProblemCheckTracker[:translator_error].no_problem!
expect { described_class.translate(post) }.to raise_error(
DiscourseTranslator::ProblemCheckedTranslationError,
)
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.",
),
)
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, "<html><body>some html</body></html>"))
expect { described_class.translate(post) }.to raise_error DiscourseTranslator::TranslatorError
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" }] } } }),
)
expect { described_class.translate(post) }.to raise_error(
I18n.t("translator.failed.post", source_locale: "cat", target_locale: "dog"),
)
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,
}
translated_text = "hur dur hur dur"
stub_request(:post, DiscourseTranslator::Google::SUPPORT_URI).to_return(
status: 200,
body: %{ { "data": { "languages": [ { "language": "de" }] } } },
)
stub_request(:post, DiscourseTranslator::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!(post)).to eq(translated_text)
end
end
end