Skip to content

Commit 7d411e4

Browse files
authored
FEATURE: Translates every post to automatic_translation_target_languages (#207)
Introduces two site settings which will automatically translate posts to the language: automatic_translation_target_languages automatic_translation_backfill_maximum_posts_per_hour (hidden)
1 parent 9efcf29 commit 7d411e4

File tree

16 files changed

+430
-17
lines changed

16 files changed

+430
-17
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# frozen_string_literal: true
2+
3+
module Jobs
4+
class TranslateTranslatable < ::Jobs::Base
5+
def execute(args)
6+
return unless SiteSetting.translator_enabled
7+
return if SiteSetting.automatic_translation_target_languages.blank?
8+
9+
translatable = args[:type].constantize.find_by(id: args[:translatable_id])
10+
return if translatable.blank?
11+
12+
target_locales = SiteSetting.automatic_translation_target_languages.split("|")
13+
target_locales.each do |target_locale|
14+
"DiscourseTranslator::#{SiteSetting.translator}".constantize.translate(
15+
translatable,
16+
target_locale.to_sym,
17+
)
18+
end
19+
20+
topic_id, post_id =
21+
translatable.is_a?(Post) ? [translatable.topic_id, translatable.id] : [translatable.id, 1]
22+
MessageBus.publish("/topic/#{topic_id}", type: :revised, id: post_id)
23+
end
24+
end
25+
end
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# frozen_string_literal: true
2+
3+
module Jobs
4+
class AutomaticTranslationBackfill < ::Jobs::Scheduled
5+
every 5.minutes
6+
7+
BACKFILL_LOCK_KEY = "discourse_translator_backfill_lock"
8+
9+
def execute(args = nil)
10+
return unless SiteSetting.translator_enabled
11+
return unless should_backfill?
12+
return unless secure_backfill_lock
13+
14+
begin
15+
process_batch
16+
ensure
17+
Discourse.redis.del(BACKFILL_LOCK_KEY)
18+
end
19+
end
20+
21+
def fetch_untranslated_model_ids(model = Post, limit = 100, target_locales = backfill_locales)
22+
m = model.name.downcase
23+
DB.query_single(<<~SQL, target_locales: target_locales, limit: limit)
24+
SELECT m.id
25+
FROM #{m}s m
26+
LEFT JOIN discourse_translator_#{m}_locales dl ON dl.#{m}_id = m.id
27+
LEFT JOIN LATERAL (
28+
SELECT array_agg(DISTINCT locale)::text[] as locales
29+
FROM discourse_translator_#{m}_translations dt
30+
WHERE dt.#{m}_id = m.id
31+
) translations ON true
32+
WHERE NOT (
33+
ARRAY[:target_locales]::text[] <@
34+
(COALESCE(
35+
array_cat(
36+
ARRAY[COALESCE(dl.detected_locale, '')]::text[],
37+
COALESCE(translations.locales, ARRAY[]::text[])
38+
),
39+
ARRAY[]::text[]
40+
))
41+
)
42+
ORDER BY m.id DESC
43+
LIMIT :limit
44+
SQL
45+
end
46+
47+
private
48+
49+
def should_backfill?
50+
return false if SiteSetting.automatic_translation_target_languages.blank?
51+
return false if SiteSetting.automatic_translation_backfill_maximum_translations_per_hour == 0
52+
true
53+
end
54+
55+
def secure_backfill_lock
56+
Discourse.redis.set(BACKFILL_LOCK_KEY, "1", ex: 5.minutes.to_i, nx: true)
57+
end
58+
59+
def translations_per_run
60+
[
61+
(SiteSetting.automatic_translation_backfill_maximum_translations_per_hour / 12) /
62+
backfill_locales.size,
63+
1,
64+
].max
65+
end
66+
67+
def backfill_locales
68+
@backfill_locales ||= SiteSetting.automatic_translation_target_languages.split("|")
69+
end
70+
71+
def translator
72+
@translator_klass ||= "DiscourseTranslator::#{SiteSetting.translator}".constantize
73+
end
74+
75+
def translate_records(type, record_ids)
76+
record_ids.each do |id|
77+
record = type.find(id)
78+
backfill_locales.each do |target_locale|
79+
begin
80+
translator.translate(record, target_locale.to_sym)
81+
rescue => e
82+
# continue with other locales even if one fails
83+
Rails.logger.warn(
84+
"Failed to machine-translate #{type.name}##{id} to #{target_locale}: #{e.message}\n#{e.backtrace.join("\n")}",
85+
)
86+
next
87+
end
88+
end
89+
end
90+
end
91+
92+
def process_batch
93+
models_translated = [Post, Topic].size
94+
translations_per_model = [translations_per_run / models_translated, 1].max
95+
topic_ids = fetch_untranslated_model_ids(Topic, translations_per_model)
96+
translations_per_model = translations_per_run - topic_ids.size
97+
post_ids = fetch_untranslated_model_ids(Post, translations_per_model)
98+
return if topic_ids.empty? && post_ids.empty?
99+
100+
translate_records(Topic, topic_ids)
101+
translate_records(Post, post_ids)
102+
end
103+
end
104+
end

app/services/discourse_translator/amazon.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def self.detect!(topic_or_post)
126126
def self.translate!(translatable, target_locale_sym = I18n.locale)
127127
detected_lang = detect(translatable)
128128

129-
save_translation(translatable) do
129+
save_translation(translatable, target_locale_sym) do
130130
begin
131131
client.translate_text(
132132
{

app/services/discourse_translator/base.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def self.translate(translatable, target_locale_sym = I18n.locale)
4848
),
4949
)
5050
end
51-
[detected_lang, translate!(translatable)]
51+
[detected_lang, translate!(translatable, target_locale_sym)]
5252
end
5353

5454
# Subclasses must implement this method to translate the text of a post or topic
@@ -77,9 +77,9 @@ def self.access_token
7777
raise "Not Implemented"
7878
end
7979

80-
def self.save_translation(translatable)
80+
def self.save_translation(translatable, target_locale_sym = I18n.locale)
8181
translation = yield
82-
translatable.set_translation(I18n.locale, translation)
82+
translatable.set_translation(target_locale_sym, translation)
8383
translation
8484
end
8585

app/services/discourse_translator/discourse_ai.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def self.detect!(topic_or_post)
2121

2222
def self.translate!(translatable, target_locale_sym = I18n.locale)
2323
return unless required_settings_enabled
24-
save_translation(translatable) do
24+
save_translation(translatable, target_locale_sym) do
2525
::DiscourseAi::Translator.new(
2626
text_for_translation(translatable),
2727
target_locale_sym,

app/services/discourse_translator/google.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def self.translate_supported?(source, target)
9191

9292
def self.translate!(translatable, target_locale_sym = I18n.locale)
9393
detected_locale = detect(translatable)
94-
save_translation(translatable) do
94+
save_translation(translatable, target_locale_sym) do
9595
res =
9696
result(
9797
TRANSLATE_URI,

app/services/discourse_translator/libre_translate.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def self.translate_supported?(source, target)
9898
def self.translate!(translatable, target_locale_sym = I18n.locale)
9999
detected_lang = detect(translatable)
100100

101-
save_translation(translatable) do
101+
save_translation(translatable, target_locale_sym) do
102102
res =
103103
result(
104104
translate_uri,

app/services/discourse_translator/microsoft.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def self.translate!(translatable, target_locale_sym = I18n.locale)
166166
locale =
167167
SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported"))
168168

169-
save_translation(translatable) do
169+
save_translation(translatable, target_locale_sym) do
170170
query = default_query.merge("from" => detected_lang, "to" => locale, "textType" => "html")
171171

172172
body = [{ "Text" => text_for_translation(translatable) }].to_json

app/services/discourse_translator/yandex.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def self.translate!(translatable, target_locale_sym = I18n.locale)
137137
locale =
138138
SUPPORTED_LANG_MAPPING[target_locale_sym] || (raise I18n.t("translator.not_supported"))
139139

140-
save_translation(translatable) do
140+
save_translation(translatable, target_locale_sym) do
141141
query =
142142
default_query.merge(
143143
"lang" => "#{detected_lang}-#{locale}",

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ en:
1818
restrict_translation_by_group: "Only allowed groups can translate"
1919
restrict_translation_by_poster_group: "Only allow translation of posts made by users in allowed groups. If empty, allow translations of posts from all users."
2020
experimental_anon_language_switcher: "Enable experimental language switcher for anonymous users. This will allow anonymous users to switch between translated versions of Discourse and user-contributed content in topics."
21+
translate_posts_to_languages: "Translate posts to languages"
2122
errors:
2223
set_locale_cookie_requirements: "The experimental language switcher for anonymous users requires the `set locale from cookie` site setting to be enabled."
2324
experimental_topic_translation: "Enable experimental topic translation feature. This replaces existing post in-line translation with a button that allows users to translate the entire topic."

0 commit comments

Comments
 (0)