Skip to content

Commit 7507795

Browse files
authored
FIX: Don't double cook (#297)
This PR fixes the bug where we were double-cooking when saving `PostLocalization` when the AI provider is used. The reason is due to the fact that we used to translate raw, then cook, and save the cooked. But now, we are required to save both the raw and cooked into `PostLocalization`. This PR also splits `translate_translatable` into `translate_post` and `translate_topic`. Originally they were taking the same route, but it was proving to be a bit annoying because post uses raw and cooked, whereas topic uses title. It makes understanding any issues with a single model (e.g. Posts) easier to understand if it is a single code path. Lastly, PR also fixes an Amazon provider bug as it was never tested.
1 parent a5ae31a commit 7507795

File tree

14 files changed

+354
-401
lines changed

14 files changed

+354
-401
lines changed

app/models/concerns/discourse_translator/translatable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def set_detected_locale(locale)
1515
# locales should be "en-US" instead of "en_US" per https://www.rfc-editor.org/rfc/rfc5646#section-2.1
1616
locale = locale.to_s.gsub("_", "-")
1717
(content_locale || build_content_locale).update!(detected_locale: locale)
18+
locale
1819
end
1920

2021
# This method is used to create a translation for a translatable (Post or Topic) and a specific locale.

app/services/discourse_translator/provider/amazon.rb

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -120,38 +120,25 @@ def self.detect!(topic_or_post)
120120
end
121121
end
122122

123-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
124-
detected_lang = detect(translatable)
123+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
124+
raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked]
125+
text = text_for_translation(post, raw:)
126+
translate_text!(text, target_locale_sym)
127+
end
125128

126-
begin
127-
client.translate_text(
128-
{
129-
text: truncate(text_for_translation(translatable)),
130-
source_language_code: "auto",
131-
target_language_code: SUPPORTED_LANG_MAPPING[target_locale_sym],
132-
},
133-
)
134-
rescue Aws::Translate::Errors::UnsupportedLanguagePairException
135-
raise I18n.t(
136-
"translator.failed.#{translatable.class.name.downcase}",
137-
source_locale: detected_lang,
138-
target_locale: target_locale_sym,
139-
)
140-
end
129+
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
130+
text = text_for_translation(topic)
131+
translate_text!(text, target_locale_sym)
141132
end
142133

143134
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
135+
client.translate_text(
136+
{
137+
text: truncate(text),
138+
source_language_code: "auto",
139+
target_language_code: SUPPORTED_LANG_MAPPING[target_locale_sym],
140+
},
141+
)&.translated_text
155142
end
156143

157144
def self.client

app/services/discourse_translator/provider/base_provider.rb

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,11 @@ def self.cache_key
2525
"#{key_prefix}#{access_token_key}"
2626
end
2727

28-
# Returns the stored translation of a post or topic.
29-
# If the translation does not exist yet, it will be translated first via the API then stored.
28+
# Translates and saves it into a PostTranslation/TopicTranslation
3029
# If the detected language is the same as the target language, the original text will be returned.
3130
# @param translatable [Post|Topic]
31+
# @return [Array] the detected language and the translated text
3232
def self.translate(translatable, target_locale_sym = I18n.locale)
33-
return if text_for_translation(translatable).blank?
3433
detected_lang = detect(translatable)
3534

3635
if translatable.locale_matches?(target_locale_sym)
@@ -50,35 +49,46 @@ def self.translate(translatable, target_locale_sym = I18n.locale)
5049
)
5150
end
5251

53-
translated = translate_translatable!(translatable, target_locale_sym)
54-
save_translation(translatable, target_locale_sym) { translated }
55-
[detected_lang, translated]
56-
end
52+
begin
53+
begin
54+
translated =
55+
case translatable.class.name
56+
when "Post"
57+
translate_post!(translatable, target_locale_sym, { cooked: true })
58+
when "Topic"
59+
translate_topic!(translatable, target_locale_sym)
60+
end
61+
end
62+
rescue => e
63+
raise I18n.t(
64+
"translator.failed.#{translatable.class.name.downcase}",
65+
source_locale: detected_lang,
66+
target_locale: target_locale_sym,
67+
)
68+
end
5769

58-
# TODO: Deprecate this in favour of translate_<model>
59-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
60-
raise "Not Implemented"
70+
translatable.set_translation(target_locale_sym, translated)
71+
[detected_lang, translated]
6172
end
6273

6374
def self.translate_text!(text, target_locale_sym = I18n.locale)
6475
raise "Not Implemented"
6576
end
6677

67-
def self.translate_post!(post, target_locale_sym = I18n.locale)
68-
translate_translatable!(post, target_locale_sym)
78+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
79+
raise "Not Implemented"
6980
end
7081

7182
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
72-
translate_translatable!(topic, target_locale_sym)
83+
raise "Not Implemented"
7384
end
7485

7586
# Returns the stored detected locale of a post or topic.
7687
# If the locale does not exist yet, it will be detected first via the API then stored.
7788
# @param translatable [Post|Topic]
7889
def self.detect(translatable)
7990
return if text_for_detection(translatable).blank?
80-
get_detected_locale(translatable) ||
81-
save_detected_locale(translatable) { detect!(translatable) }
91+
translatable.detected_locale || translatable.set_detected_locale(detect!(translatable))
8292
end
8393

8494
# Subclasses must implement this method to detect the text of a post or topic
@@ -94,29 +104,6 @@ def self.access_token
94104
raise "Not Implemented"
95105
end
96106

97-
def self.save_translation(translatable, target_locale_sym = I18n.locale)
98-
begin
99-
translation = yield
100-
rescue Timeout::Error
101-
raise TranslatorError.new(I18n.t("translator.api_timeout"))
102-
end
103-
translatable.set_translation(target_locale_sym, translation)
104-
translation
105-
end
106-
107-
def self.get_detected_locale(translatable)
108-
translatable.detected_locale
109-
end
110-
111-
def self.save_detected_locale(translatable)
112-
# sometimes we may have a user post that is just an emoji
113-
# in that case, we will just indicate the post is in the default locale
114-
detected_locale = yield.presence || SiteSetting.default_locale
115-
translatable.set_detected_locale(detected_locale)
116-
117-
detected_locale
118-
end
119-
120107
def self.language_supported?(detected_lang)
121108
raise NotImplementedError unless self.const_defined?(:SUPPORTED_LANG_MAPPING)
122109
supported_lang = const_get(:SUPPORTED_LANG_MAPPING)

app/services/discourse_translator/provider/discourse_ai.rb

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,22 @@ def self.detect!(topic_or_post)
1515
::DiscourseAi::LanguageDetector.new(text_for_detection(topic_or_post)).detect
1616
end
1717

18-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
19-
if (translatable.class.name == "Post")
20-
translate_post!(translatable, target_locale_sym)
21-
elsif (translatable.class.name == "Topic")
22-
translate_topic!(translatable, target_locale_sym)
23-
end
24-
end
25-
26-
def self.translate_post!(post, target_locale_sym = I18n.locale)
18+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
2719
validate_required_settings!
2820

29-
text = text_for_translation(post, raw: true)
21+
raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked]
22+
text = text_for_translation(post, raw:)
3023
chunks = DiscourseTranslator::ContentSplitter.split(text)
31-
translated =
32-
chunks
33-
.map { |chunk| ::DiscourseAi::PostTranslator.new(chunk, target_locale_sym).translate }
34-
.join("")
35-
DiscourseTranslator::TranslatedContentNormalizer.normalize(post, translated)
24+
chunks
25+
.map { |chunk| ::DiscourseAi::PostTranslator.new(chunk, target_locale_sym).translate }
26+
.join("")
3627
end
3728

3829
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
3930
validate_required_settings!
4031

4132
language = get_language_name(target_locale_sym)
42-
translated =
43-
::DiscourseAi::TopicTranslator.new(text_for_translation(topic), language).translate
44-
DiscourseTranslator::TranslatedContentNormalizer.normalize(topic, translated)
33+
::DiscourseAi::TopicTranslator.new(text_for_translation(topic), language).translate
4534
end
4635

4736
def self.translate_text!(text, target_locale_sym = I18n.locale)

app/services/discourse_translator/provider/google.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,15 @@ def self.translate_supported?(source, target)
9696
end
9797
end
9898

99-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
100-
res =
101-
result(
102-
TRANSLATE_URI,
103-
q: text_for_translation(translatable),
104-
target: SUPPORTED_LANG_MAPPING[target_locale_sym],
105-
)
106-
res["translations"][0]["translatedText"]
99+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
100+
raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked]
101+
text = text_for_translation(post, raw:)
102+
translate_text!(text, target_locale_sym)
103+
end
104+
105+
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
106+
text = text_for_translation(topic)
107+
translate_text!(text, target_locale_sym)
107108
end
108109

109110
def self.translate_text!(text, target_locale_sym = I18n.locale)

app/services/discourse_translator/provider/libre_translate.rb

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,19 @@ def self.translate_supported?(source, target)
9191
res.any? { |obj| obj["code"] == source } && res.any? { |obj| obj["code"] == lang }
9292
end
9393

94-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
95-
detected_lang = detect(translatable)
94+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
95+
raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked]
96+
text = text_for_translation(post, raw:)
9697

97-
res =
98-
result(
99-
translate_uri,
100-
q: text_for_translation(translatable),
101-
source: detected_lang,
102-
target: SUPPORTED_LANG_MAPPING[target_locale_sym],
103-
format: "html",
104-
)
105-
res["translatedText"]
98+
detected_lang = detect(post)
99+
100+
send_for_translation(text, detected_lang, target_locale_sym)
101+
end
102+
103+
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
104+
detected_lang = detect(topic)
105+
text = text_for_translation(topic)
106+
send_for_translation(text, detected_lang, target_locale_sym)
106107
end
107108

108109
def self.translate_text!(text, target_locale_sym = I18n.locale)
@@ -154,6 +155,20 @@ def self.result(url, body)
154155
body
155156
end
156157
end
158+
159+
private
160+
161+
def self.send_for_translation(text, source_locale, target_locale)
162+
res =
163+
result(
164+
translate_uri,
165+
q: text,
166+
source: source_locale,
167+
target: SUPPORTED_LANG_MAPPING[target_locale],
168+
format: "html",
169+
)
170+
res["translatedText"]
171+
end
157172
end
158173
end
159174
end

app/services/discourse_translator/provider/microsoft.rb

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -154,27 +154,24 @@ def self.detect!(topic_or_post)
154154
result(uri.to_s, body, default_headers).first["language"]
155155
end
156156

157-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
158-
detected_lang = detect(translatable)
157+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
158+
raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked]
159+
text = text_for_translation(post, raw:)
159160

160-
if text_for_translation(translatable).length > LENGTH_LIMIT
161-
raise TranslatorError.new(I18n.t("translator.too_long"))
162-
end
163-
locale =
164-
SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported"))
161+
raise TranslatorError.new(I18n.t("translator.too_long")) if text.length > LENGTH_LIMIT
165162

166-
query = default_query.merge("from" => detected_lang, "to" => locale, "textType" => "html")
167-
body = [{ "Text" => text_for_translation(translatable) }].to_json
168-
uri = URI(translate_endpoint)
169-
uri.query = URI.encode_www_form(query)
170-
response_body = result(uri.to_s, body, default_headers)
171-
response_body.first["translations"].first["text"]
163+
translate_text!(text, target_locale_sym)
172164
end
173165

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"))
166+
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
167+
text = text_for_translation(topic)
168+
raise TranslatorError.new(I18n.t("translator.too_long")) if text.length > LENGTH_LIMIT
169+
170+
translate_text!(text, target_locale_sym)
171+
end
177172

173+
def self.translate_text!(text, target_locale_sym = I18n.locale)
174+
locale = SUPPORTED_LANG_MAPPING[target_locale_sym]
178175
query = default_query.merge("to" => locale, "textType" => "html")
179176
body = [{ "Text" => text }].to_json
180177
uri = URI(translate_endpoint)

app/services/discourse_translator/provider/yandex.rb

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -129,26 +129,25 @@ def self.detect!(topic_or_post)
129129
result(uri.to_s, "", default_headers)["lang"]
130130
end
131131

132-
def self.translate_translatable!(translatable, target_locale_sym = I18n.locale)
133-
detected_lang = detect(translatable)
134-
locale =
135-
SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported"))
132+
def self.translate_post!(post, target_locale_sym = I18n.locale, opts = {})
133+
raw = opts.key?(:raw) ? opts[:raw] : !opts[:cooked]
134+
text = text_for_translation(post, raw:)
136135

137-
query =
138-
default_query.merge(
139-
"lang" => "#{detected_lang}-#{locale}",
140-
"text" => text_for_translation(translatable),
141-
"format" => "html",
142-
)
136+
detected_lang = detect(post)
137+
locale = SUPPORTED_LANG_MAPPING[target_locale_sym]
143138

144-
uri = URI(TRANSLATE_URI)
145-
uri.query = URI.encode_www_form(query)
139+
send_for_translation(text, detected_lang, locale)
140+
end
146141

147-
response_body = result(uri.to_s, "", default_headers)
148-
response_body["text"][0]
142+
def self.translate_topic!(topic, target_locale_sym = I18n.locale)
143+
detected_lang = detect(topic)
144+
locale = SUPPORTED_LANG_MAPPING[target_locale_sym]
145+
text = text_for_translation(topic)
146+
147+
send_for_translation(text, detected_lang, locale)
149148
end
150149

151-
def self.translate_text!(translatable, target_locale_sym = I18n.locale)
150+
def self.translate_text!(text, target_locale_sym = I18n.locale)
152151
# Not supported for v1.5 https://translate.yandex.com/developers
153152
raise TranslatorError.new(I18n.t("translator.not_supported"))
154153
end
@@ -160,6 +159,21 @@ def self.translate_supported?(detected_lang, target_lang)
160159

161160
private
162161

162+
def self.send_for_translation(text, source_locale, target_locale)
163+
query =
164+
default_query.merge(
165+
"lang" => "#{source_locale}-#{target_locale}",
166+
"text" => text,
167+
"format" => "html",
168+
)
169+
170+
uri = URI(TRANSLATE_URI)
171+
uri.query = URI.encode_www_form(query)
172+
173+
response_body = result(uri.to_s, "", default_headers)
174+
response_body["text"][0]
175+
end
176+
163177
def self.post(uri, body, headers = {})
164178
Excon.post(uri, body: body, headers: headers)
165179
end

0 commit comments

Comments
 (0)