Skip to content

Commit e42c0da

Browse files
authored
DEV: Add the ability to translate a single piece of text (#281)
* DEV: Add the ability to translate a single piece of text * rename method
1 parent 280f298 commit e42c0da

File tree

12 files changed

+186
-40
lines changed

12 files changed

+186
-40
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
module DiscourseAi
4+
class ShortTextTranslator < BaseTranslator
5+
PROMPT_TEMPLATE = <<~TEXT.freeze
6+
You are a translation service specializing in translating short pieces of text or a few words.
7+
These words may be things like a name, description, or title. Adhere to the following guidelines:
8+
9+
1. Keep proper nouns and technical terms in their original language
10+
2. Keep the translated content close to the original length
11+
3. Translation maintains the original meaning
12+
13+
Provide your translation in the following JSON format:
14+
15+
<output>
16+
{"translation": "target_language translation here"}
17+
</output>
18+
19+
Here are three examples of correct translation
20+
21+
Original: {"content":"Japan", "target_language":"Spanish"}
22+
Correct translation: {"translation": "Japón"}
23+
24+
Original: {"name":"Cats and Dogs", "target_language":"Chinese"}
25+
Correct translation: {"translation": "猫和狗"}
26+
27+
Original: {"name": "Q&A", "target_language": "Portuguese"}
28+
Correct translation: {"translation": "Perguntas e Respostas"}
29+
30+
Remember to keep proper nouns like "Minecraft" and "Toyota" in their original form. Translate the text now and provide your answer in the specified JSON format.
31+
TEXT
32+
33+
private def prompt_template
34+
PROMPT_TEMPLATE
35+
end
36+
end
37+
end

app/services/discourse_translator/provider/amazon.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
140140
end
141141
end
142142

143+
def self.translate_text!(text, target_locale_sym = I18n.locale)
144+
begin
145+
client.translate_text(
146+
{
147+
text: truncate(text),
148+
source_language_code: "auto",
149+
target_language_code: SUPPORTED_LANG_MAPPING[target_locale_sym],
150+
},
151+
)
152+
rescue Aws::Translate::Errors::UnsupportedLanguagePairException
153+
raise I18n.t("translator.not_supported")
154+
end
155+
end
156+
143157
def self.client
144158
opts = { region: SiteSetting.translator_aws_region }
145159

app/services/discourse_translator/provider/base_provider.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
6565
raise "Not Implemented"
6666
end
6767

68+
def self.translate_text(text, target_locale_sym = I18n.locale)
69+
raise "Not Implemented"
70+
end
71+
6872
# Returns the stored detected locale of a post or topic.
6973
# If the locale does not exist yet, it will be detected first via the API then stored.
7074
# @param translatable [Post|Topic]

app/services/discourse_translator/provider/discourse_ai.rb

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,13 @@ def self.language_supported?(detected_lang)
1010
end
1111

1212
def self.detect!(topic_or_post)
13-
unless required_settings_enabled
14-
raise TranslatorError.new(
15-
I18n.t(
16-
"translator.discourse_ai.ai_helper_required",
17-
{ base_url: Discourse.base_url },
18-
),
19-
)
20-
end
13+
validate_required_settings!
2114

2215
::DiscourseAi::LanguageDetector.new(text_for_detection(topic_or_post)).detect
2316
end
2417

2518
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
26-
unless required_settings_enabled
27-
raise TranslatorError.new(
28-
I18n.t(
29-
"translator.discourse_ai.ai_helper_required",
30-
{ base_url: Discourse.base_url },
31-
),
32-
)
33-
end
19+
validate_required_settings!
3420

3521
language = get_language_name(target_locale_sym)
3622
translated =
@@ -51,11 +37,25 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
5137
DiscourseTranslator::TranslatedContentNormalizer.normalize(translatable, translated)
5238
end
5339

40+
def self.translate_text!(text, target_locale_sym = I18n.locale)
41+
validate_required_settings!
42+
43+
language = get_language_name(target_locale_sym)
44+
::DiscourseAi::ShortTextTranslator.new(text, language).translate
45+
end
46+
5447
private
5548

56-
def self.required_settings_enabled
57-
SiteSetting.translator_enabled && SiteSetting.translator_provider == "DiscourseAi" &&
58-
SiteSetting.discourse_ai_enabled && SiteSetting.ai_helper_enabled
49+
def self.validate_required_settings!
50+
unless SiteSetting.translator_enabled && SiteSetting.translator_provider == "DiscourseAi" &&
51+
SiteSetting.discourse_ai_enabled && SiteSetting.ai_helper_enabled
52+
raise TranslatorError.new(
53+
I18n.t(
54+
"translator.discourse_ai.ai_helper_required",
55+
{ base_url: Discourse.base_url },
56+
),
57+
)
58+
end
5959
end
6060

6161
def self.get_language_name(target_locale_sym)

app/services/discourse_translator/provider/google.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
106106
res["translations"][0]["translatedText"]
107107
end
108108

109+
def self.translate_text!(text, target_locale_sym = I18n.locale)
110+
res = result(TRANSLATE_URI, q: text, target: SUPPORTED_LANG_MAPPING[target_locale_sym])
111+
res["translations"][0]["translatedText"]
112+
end
113+
109114
def self.result(url, body)
110115
body[:key] = access_token
111116

app/services/discourse_translator/provider/libre_translate.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
105105
res["translatedText"]
106106
end
107107

108+
def self.translate_text!(text, target_locale_sym = I18n.locale)
109+
# Unsupported - see https://libretranslate.com/docs/#/translate/post_translate
110+
# requires a source language
111+
raise TranslatorError.new(I18n.t("translator.not_supported"))
112+
end
113+
108114
def self.get(url)
109115
begin
110116
response = Excon.get(url)

app/services/discourse_translator/provider/microsoft.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
171171
response_body.first["translations"].first["text"]
172172
end
173173

174+
def self.translate_text!(text, target_locale_sym = I18n.locale)
175+
locale =
176+
SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported"))
177+
178+
query = default_query.merge("to" => locale, "textType" => "html")
179+
body = [{ "Text" => text }].to_json
180+
uri = URI(translate_endpoint)
181+
uri.query = URI.encode_www_form(query)
182+
response_body = result(uri.to_s, body, default_headers)
183+
response_body.first["translations"].first["text"]
184+
end
185+
174186
def self.translate_supported?(detected_lang, target_lang)
175187
SUPPORTED_LANG_MAPPING.keys.include?(detected_lang.to_sym) &&
176188
SUPPORTED_LANG_MAPPING.values.include?(detected_lang.to_s)

app/services/discourse_translator/provider/yandex.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
148148
response_body["text"][0]
149149
end
150150

151+
def self.translate_text!(translatable, target_locale_sym = I18n.locale)
152+
# Not supported for v1.5 https://translate.yandex.com/developers
153+
raise TranslatorError.new(I18n.t("translator.not_supported"))
154+
end
155+
151156
def self.translate_supported?(detected_lang, target_lang)
152157
SUPPORTED_LANG_MAPPING.keys.include?(detected_lang.to_sym) &&
153158
SUPPORTED_LANG_MAPPING.values.include?(detected_lang.to_s)

spec/services/amazon_spec.rb

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
end
4747
end
4848

49-
describe ".translate" do
49+
describe ".translate_translatable!" do
5050
let(:post) { Fabricate(:post) }
5151
let!(:client) { Aws::Translate::Client.new(stub_responses: true) }
5252

@@ -66,9 +66,33 @@
6666
end
6767

6868
it "raises an error when trying to translate an unsupported language" do
69-
expect { described_class.translate(post) }.to raise_error(
69+
expect { described_class.translate_translatable!(post) }.to raise_error(
7070
I18n.t("translator.failed.post", source_locale: "en", target_locale: "es"),
7171
)
7272
end
7373
end
74+
75+
describe ".translate_text!" do
76+
let!(:client) { Aws::Translate::Client.new(stub_responses: true) }
77+
78+
before do
79+
client.stub_responses(
80+
:translate_text,
81+
"UnsupportedLanguagePairException",
82+
{
83+
translated_text: "Probando traducciones",
84+
source_language_code: "en",
85+
target_language_code: "es",
86+
},
87+
)
88+
described_class.stubs(:client).returns(client)
89+
I18n.stubs(:locale).returns(:es)
90+
end
91+
92+
it "raises an error when trying to translate an unsupported language" do
93+
expect { described_class.translate_text!("derp") }.to raise_error(
94+
I18n.t("translator.not_supported", source_locale: "en", target_locale: "es"),
95+
)
96+
end
97+
end
7498
end

spec/services/discourse_ai_spec.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
end
3535
end
3636

37-
describe ".translate" do
37+
describe ".translate_translatable!" do
3838
before do
3939
post.set_detected_locale("de")
4040
topic.set_detected_locale("de")
@@ -44,8 +44,7 @@
4444
DiscourseAi::Completions::Llm.with_prepared_responses(
4545
[translation_json("some translated text")],
4646
) do
47-
locale, translated_text = DiscourseTranslator::Provider::DiscourseAi.translate(post)
48-
expect(locale).to eq "de"
47+
translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_translatable!(post)
4948
expect(translated_text).to eq "<p>some translated text</p>"
5049
end
5150
end
@@ -55,8 +54,7 @@
5554
DiscourseAi::Completions::Llm.with_prepared_responses(
5655
[translation_json("some translated text")],
5756
) do
58-
locale, translated_text = DiscourseTranslator::Provider::DiscourseAi.translate(topic)
59-
expect(locale).to eq "de"
57+
translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_translatable!(topic)
6058
expect(translated_text).to eq "some translated text"
6159
end
6260
end
@@ -73,6 +71,17 @@
7371
end
7472
end
7573

74+
describe ".translate_text!" do
75+
it "returns the translated text" do
76+
DiscourseAi::Completions::Llm.with_prepared_responses(
77+
[translation_json("some translated text")],
78+
) do
79+
translated_text = DiscourseTranslator::Provider::DiscourseAi.translate_text!("derp")
80+
expect(translated_text).to eq "some translated text"
81+
end
82+
end
83+
end
84+
7685
def locale_json(content)
7786
{ locale: content }.to_json
7887
end

0 commit comments

Comments
 (0)