Skip to content

Commit a43c603

Browse files
authored
DEV: Move saving of translations into base class (#203)
The following changes allow the saving of translation metadata to be in the single base class instead of sprinkled in all the subclasses. This is to prepare for the move from custom fields to proper tables. This PR does two things: 1. introduce a `detect!` (and also `translate!`) in translator subclasses (Amazon, Google, Microsoft, etc) which will set the value from the API all the time. The base class invokes `detect!`. https://github.com/discourse/discourse-translator/blob/4c5d8e74dea4ff006706772a356f1c2efa1445fa/app/services/discourse_translator/google.rb#L77-L85 2. update `detect` to return the stored value or invoke the `!` variant to get the value if it does not exist. https://github.com/discourse/discourse-translator/blob/4c5d8e74dea4ff006706772a356f1c2efa1445fa/app/services/discourse_translator/base.rb#L51-L57 There is already test coverage for this refactor.
1 parent 9bb3978 commit a43c603

File tree

13 files changed

+230
-180
lines changed

13 files changed

+230
-180
lines changed

app/jobs/scheduled/detect_posts_language.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ def process_batch(post_ids)
2626
begin
2727
translator = "DiscourseTranslator::#{SiteSetting.translator}".constantize
2828
translator.detect(post)
29-
if !post.custom_fields_clean?
30-
post.save_custom_fields
31-
post.publish_change_to_clients!(:revised)
32-
end
3329
rescue ::DiscourseTranslator::ProblemCheckedTranslationError
3430
# problem-checked translation errors gracefully
3531
end

app/services/discourse_translator/amazon.rb

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -107,47 +107,41 @@ def self.access_token_key
107107
"aws-translator"
108108
end
109109

110-
def self.detect(topic_or_post)
111-
text = truncate text_for_detection(topic_or_post)
112-
return if text.blank?
113-
114-
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] ||= (
110+
def self.detect!(topic_or_post)
111+
save_detected_locale(topic_or_post) do
115112
begin
116113
client.translate_text(
117114
{
118-
text: text,
115+
text: truncate(text_for_detection(topic_or_post)),
119116
source_language_code: "auto",
120117
target_language_code: SUPPORTED_LANG_MAPPING[I18n.locale],
121118
},
122119
)&.source_language_code
123120
rescue Aws::Errors::MissingCredentialsError
124121
raise I18n.t("translator.amazon.invalid_credentials")
125122
end
126-
)
123+
end
127124
end
128125

129-
def self.translate(topic_or_post)
126+
def self.translate!(topic_or_post)
130127
detected_lang = detect(topic_or_post)
131128

132-
from_custom_fields(topic_or_post) do
129+
save_translation(topic_or_post) do
133130
begin
134-
result =
135-
client.translate_text(
136-
{
137-
text: truncate(text_for_translation(topic_or_post)),
138-
source_language_code: "auto",
139-
target_language_code: SUPPORTED_LANG_MAPPING[I18n.locale],
140-
},
141-
)
131+
client.translate_text(
132+
{
133+
text: truncate(text_for_translation(topic_or_post)),
134+
source_language_code: "auto",
135+
target_language_code: SUPPORTED_LANG_MAPPING[I18n.locale],
136+
},
137+
)
142138
rescue Aws::Translate::Errors::UnsupportedLanguagePairException
143139
raise I18n.t(
144140
"translator.failed",
145141
source_locale: detected_lang,
146142
target_locale: I18n.locale,
147143
)
148144
end
149-
150-
[detected_lang, result.translated_text]
151145
end
152146
end
153147

@@ -170,12 +164,5 @@ def self.client
170164

171165
@client ||= Aws::Translate::Client.new(opts)
172166
end
173-
174-
def self.assign_lang_custom_field(post, value)
175-
if value.nil?
176-
return post.custom_fields.delete(DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD)
177-
end
178-
post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] ||= value
179-
end
180167
end
181168
end

app/services/discourse_translator/base.rb

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,64 @@ def self.cache_key
2424
"#{key_prefix}#{access_token_key}"
2525
end
2626

27-
def self.translate(post)
27+
# Returns the stored translation of a post or topic.
28+
# If the translation does not exist yet, it will be translated first via the API then stored.
29+
# If the detected language is the same as the target language, the original text will be returned.
30+
# @param topic_or_post [Post|Topic]
31+
def self.translate(topic_or_post)
32+
return if text_for_translation(topic_or_post).blank?
33+
detected_lang = detect(topic_or_post)
34+
35+
return detected_lang, get_text(topic_or_post) if (detected_lang&.to_s == I18n.locale.to_s)
36+
37+
existing_translation = get_translation(topic_or_post)
38+
return detected_lang, existing_translation if existing_translation.present?
39+
40+
unless translate_supported?(detected_lang, I18n.locale)
41+
raise TranslatorError.new(
42+
I18n.t(
43+
"translator.failed",
44+
source_locale: detected_lang,
45+
target_locale: I18n.locale,
46+
),
47+
)
48+
end
49+
[detected_lang, translate!(topic_or_post)]
50+
end
51+
52+
# Subclasses must implement this method to translate the text of a post or topic
53+
# then use the save_translation method to store the translated text.
54+
# @param topic_or_post [Post|Topic]
55+
def self.translate!(topic_or_post)
2856
raise "Not Implemented"
2957
end
3058

31-
def self.detect(post)
59+
# Returns the stored detected locale of a post or topic.
60+
# If the locale does not exist yet, it will be detected first via the API then stored.
61+
# @param topic_or_post [Post|Topic]
62+
def self.detect(topic_or_post)
63+
return if text_for_detection(topic_or_post).blank?
64+
get_detected_locale(topic_or_post) || detect!(topic_or_post)
65+
end
66+
67+
# Subclasses must implement this method to translate the text of a post or topic
68+
# then use the save_translation method to store the translated text.
69+
# @param topic_or_post [Post|Topic]
70+
def self.detect!(post)
3271
raise "Not Implemented"
3372
end
3473

3574
def self.access_token
3675
raise "Not Implemented"
3776
end
3877

39-
def self.from_custom_fields(topic_or_post)
78+
def self.get_translation(topic_or_post)
79+
translated_custom_field =
80+
topic_or_post.custom_fields[DiscourseTranslator::TRANSLATED_CUSTOM_FIELD] || {}
81+
translated_custom_field[I18n.locale]
82+
end
83+
84+
def self.save_translation(topic_or_post)
4085
translated_custom_field =
4186
topic_or_post.custom_fields[DiscourseTranslator::TRANSLATED_CUSTOM_FIELD] || {}
4287
translated_text = translated_custom_field[I18n.locale]
@@ -54,6 +99,22 @@ def self.from_custom_fields(topic_or_post)
5499
translated_text
55100
end
56101

102+
def self.get_detected_locale(topic_or_post)
103+
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD]
104+
end
105+
106+
def self.save_detected_locale(topic_or_post)
107+
detected_locale = yield
108+
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] = detected_locale
109+
110+
if !topic_or_post.custom_fields_clean?
111+
topic_or_post.save_custom_fields
112+
topic_or_post.publish_change_to_clients!(:revised) if topic_or_post.class.name == "Post"
113+
end
114+
115+
detected_locale
116+
end
117+
57118
def self.get_text(topic_or_post)
58119
case topic_or_post.class.name
59120
when "Post"
@@ -70,6 +131,10 @@ def self.language_supported?(detected_lang)
70131
detected_lang != supported_lang[I18n.locale]
71132
end
72133

134+
def self.translate_supported?(detected_lang, target_lang)
135+
true
136+
end
137+
73138
private
74139

75140
def self.strip_tags_for_detection(detection_text)

app/services/discourse_translator/discourse_ai.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,20 @@ def self.language_supported?(detected_lang)
1111
detected_lang != locale_without_region
1212
end
1313

14-
def self.detect(topic_or_post)
14+
def self.detect!(topic_or_post)
1515
return unless required_settings_enabled
1616

17-
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] ||= begin
17+
save_detected_locale(topic_or_post) do
1818
::DiscourseAi::LanguageDetector.new(text_for_detection(topic_or_post)).detect
1919
end
20-
rescue => e
21-
Rails.logger.warn(
22-
"#{::DiscourseTranslator::PLUGIN_NAME}: Failed to detect language for #{topic_or_post.class.name} #{topic_or_post.id}: #{e}",
23-
)
2420
end
2521

2622
def self.translate(topic_or_post)
2723
return unless required_settings_enabled
2824

2925
detected_lang = detect(topic_or_post)
3026
translated_text =
31-
from_custom_fields(topic_or_post) do
27+
save_translation(topic_or_post) do
3228
::DiscourseAi::Translator.new(text_for_translation(topic_or_post), I18n.locale).translate
3329
end
3430

app/services/discourse_translator/google.rb

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -74,54 +74,33 @@ def self.access_token
7474
raise ProblemCheckedTranslationError.new("NotFound: Google Api Key not set.")
7575
end
7676

77-
def self.detect(topic_or_post)
78-
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] ||= result(
79-
DETECT_URI,
80-
q: text_for_detection(topic_or_post),
81-
)[
82-
"detections"
83-
][
84-
0
85-
].max { |a, b| a.confidence <=> b.confidence }[
86-
"language"
87-
]
77+
def self.detect!(topic_or_post)
78+
save_detected_locale(topic_or_post) do
79+
result(DETECT_URI, q: text_for_detection(topic_or_post))["detections"][0].max do |a, b|
80+
a.confidence <=> b.confidence
81+
end[
82+
"language"
83+
]
84+
end
8885
end
8986

9087
def self.translate_supported?(source, target)
9188
res = result(SUPPORT_URI, target: SUPPORTED_LANG_MAPPING[target])
9289
res["languages"].any? { |obj| obj["language"] == source }
9390
end
9491

95-
def self.translate(topic_or_post)
96-
detected_lang = detect(topic_or_post)
97-
98-
# the translate button appears if a given post is in a foreign language.
99-
# however the title of the topic may be in a different language, and may be in the user's language.
100-
# if this is the case, when this is called for a topic, the detected_lang will be the user's language,
101-
# so the user's language and the detected language will be the same. For example, both could be "en"
102-
# google will choke on this and return an error instead of gracefully handling it by returning the original
103-
# string.
104-
# ---
105-
# here we handle that situation by returning the original string if the source and target lang are the same.
106-
return detected_lang, get_text(topic_or_post) if (detected_lang&.to_s.eql? I18n.locale.to_s)
107-
108-
unless translate_supported?(detected_lang, I18n.locale)
109-
raise I18n.t("translator.failed", source_locale: detected_lang, target_locale: I18n.locale)
92+
def self.translate!(topic_or_post)
93+
detected_locale = detect(topic_or_post)
94+
save_translation(topic_or_post) do
95+
res =
96+
result(
97+
TRANSLATE_URI,
98+
q: text_for_translation(topic_or_post),
99+
source: detected_locale,
100+
target: SUPPORTED_LANG_MAPPING[I18n.locale],
101+
)
102+
res["translations"][0]["translatedText"]
110103
end
111-
112-
translated_text =
113-
from_custom_fields(topic_or_post) do
114-
res =
115-
result(
116-
TRANSLATE_URI,
117-
q: text_for_translation(topic_or_post),
118-
source: detected_lang,
119-
target: SUPPORTED_LANG_MAPPING[I18n.locale],
120-
)
121-
res["translations"][0]["translatedText"]
122-
end
123-
124-
[detected_lang, translated_text]
125104
end
126105

127106
def self.result(url, body)

app/services/discourse_translator/libre_translate.rb

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,14 @@ def self.access_token
7878
SiteSetting.translator_libretranslate_api_key
7979
end
8080

81-
def self.detect(topic_or_post)
82-
res =
83-
result(
84-
detect_uri,
85-
q: ActionController::Base.helpers.strip_tags(text_for_detection(topic_or_post)),
86-
)
87-
88-
if !res.empty?
89-
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] ||= res[0][
90-
"language"
91-
]
92-
else
93-
topic_or_post.custom_fields[DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] ||= "en"
81+
def self.detect!(topic_or_post)
82+
save_detected_locale(topic_or_post) do
83+
res =
84+
result(
85+
detect_uri,
86+
q: ActionController::Base.helpers.strip_tags(text_for_detection(topic_or_post)),
87+
)
88+
!res.empty? ? res[0]["language"] : "en"
9489
end
9590
end
9691

@@ -100,27 +95,20 @@ def self.translate_supported?(source, target)
10095
res.any? { |obj| obj["code"] == source } && res.any? { |obj| obj["code"] == lang }
10196
end
10297

103-
def self.translate(topic_or_post)
98+
def self.translate!(topic_or_post)
10499
detected_lang = detect(topic_or_post)
105100

106-
unless translate_supported?(detected_lang, I18n.locale)
107-
raise I18n.t("translator.failed", source_locale: detected_lang, target_locale: I18n.locale)
101+
save_translation(topic_or_post) do
102+
res =
103+
result(
104+
translate_uri,
105+
q: text_for_translation(topic_or_post),
106+
source: detected_lang,
107+
target: SUPPORTED_LANG_MAPPING[I18n.locale],
108+
format: "html",
109+
)
110+
res["translatedText"]
108111
end
109-
110-
translated_text =
111-
from_custom_fields(topic_or_post) do
112-
res =
113-
result(
114-
translate_uri,
115-
q: text_for_translation(topic_or_post),
116-
source: detected_lang,
117-
target: SUPPORTED_LANG_MAPPING[I18n.locale],
118-
format: "html",
119-
)
120-
res["translatedText"]
121-
end
122-
123-
[detected_lang, translated_text]
124112
end
125113

126114
def self.get(url)

0 commit comments

Comments
 (0)