Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions app/controllers/discourse_translator/translator_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module ::DiscourseTranslator
class TranslatorController < ::ApplicationController
requires_plugin PLUGIN_NAME

before_action :ensure_logged_in

def translate
if !current_user.staff?
RateLimiter.new(
current_user,
"translate_post",
SiteSetting.max_translations_per_minute,
1.minute,
).performed!
end

params.require(:post_id)
post = Post.find_by(id: params[:post_id])
raise Discourse::InvalidParameters.new(:post_id) if post.blank?
guardian.ensure_can_see!(post)

if !guardian.user_group_allow_translate?
raise Discourse::InvalidAccess.new(
"not_in_group",
SiteSetting.restrict_translation_by_group,
custom_message: "not_in_group.user_not_in_group",
group: current_user.groups.pluck(:id),
)
end

if !guardian.poster_group_allow_translate?(post)
raise Discourse::InvalidAccess.new(
"not_in_group",
SiteSetting.restrict_translation_by_poster_group,
custom_message: "not_in_group.poster_not_in_group",
)
end

begin
title_json = {}
detected_lang, translation =
"DiscourseTranslator::#{SiteSetting.translator}".constantize.translate(post)
if post.is_first_post?
_, title_translation =
"DiscourseTranslator::#{SiteSetting.translator}".constantize.translate(post.topic)
title_json = { title_translation: title_translation }
end
render json: { translation: translation, detected_lang: detected_lang }.merge(title_json),
status: 200
rescue ::DiscourseTranslator::TranslatorError => e
render_json_error e.message, status: 422
end
end
end
end
40 changes: 40 additions & 0 deletions app/jobs/scheduled/detect_posts_language.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module ::Jobs
class DetectPostsLanguage < ::Jobs::Scheduled
sidekiq_options retry: false
every 5.minutes

BATCH_SIZE = 100
MAX_QUEUE_SIZE = 1000

def execute(args)
return unless SiteSetting.translator_enabled

post_ids = Discourse.redis.spop(DiscourseTranslator::LANG_DETECT_NEEDED, MAX_QUEUE_SIZE)
return if post_ids.blank?

post_ids.each_slice(BATCH_SIZE) { |batch| process_batch(batch) }
end

private

def process_batch(post_ids)
posts = Post.where(id: post_ids).to_a
posts.each do |post|
DistributedMutex.synchronize("detect_translation_#{post.id}") do
begin
translator = "DiscourseTranslator::#{SiteSetting.translator}".constantize
translator.detect(post)
if !post.custom_fields_clean?
post.save_custom_fields
post.publish_change_to_clients!(:revised)
end
rescue ::DiscourseTranslator::ProblemCheckedTranslationError
# problem-checked translation errors gracefully
end
end
end
end
end
end
7 changes: 7 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

DiscourseTranslator::Engine.routes.draw do
post "/translate" => "translator#translate", :format => :json
end

Discourse::Application.routes.draw { mount ::DiscourseTranslator::Engine, at: "/translator" }
12 changes: 12 additions & 0 deletions lib/discourse_translator/engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true
module ::DiscourseTranslator
class Engine < ::Rails::Engine
engine_name PLUGIN_NAME
isolate_namespace DiscourseTranslator
config.autoload_paths << File.join(config.root, "lib")
scheduled_job_dir = "#{config.root}/app/jobs/scheduled"
config.to_prepare do
Rails.autoloaders.main.eager_load_dir(scheduled_job_dir) if Dir.exist?(scheduled_job_dir)
end
end
end
169 changes: 8 additions & 161 deletions plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,174 +12,25 @@
enabled_site_setting :translator_enabled
register_asset "stylesheets/common/post.scss"

after_initialize do
module ::DiscourseTranslator
PLUGIN_NAME = "discourse_translator".freeze
DETECTED_LANG_CUSTOM_FIELD = "post_detected_lang".freeze
TRANSLATED_CUSTOM_FIELD = "translated_text".freeze
LANG_DETECT_NEEDED = "lang_detect_needed".freeze
module ::DiscourseTranslator
PLUGIN_NAME = "discourse-translator".freeze

autoload :Microsoft,
"#{Rails.root}/plugins/discourse-translator/services/discourse_translator/microsoft"
autoload :Google,
"#{Rails.root}/plugins/discourse-translator/services/discourse_translator/google"
autoload :Amazon,
"#{Rails.root}/plugins/discourse-translator/services/discourse_translator/amazon"
autoload :Yandex,
"#{Rails.root}/plugins/discourse-translator/services/discourse_translator/yandex"
autoload :LibreTranslate,
"#{Rails.root}/plugins/discourse-translator/services/discourse_translator/libretranslate"
DETECTED_LANG_CUSTOM_FIELD = "post_detected_lang".freeze
TRANSLATED_CUSTOM_FIELD = "translated_text".freeze
LANG_DETECT_NEEDED = "lang_detect_needed".freeze
end

class Engine < ::Rails::Engine
engine_name PLUGIN_NAME
isolate_namespace DiscourseTranslator
end
end
require_relative "lib/discourse_translator/engine"

require_relative "app/services/problem_check/missing_translator_api_key"
require_relative "app/services/problem_check/translator_error"
after_initialize do
register_problem_check ProblemCheck::MissingTranslatorApiKey
register_problem_check ProblemCheck::TranslatorError

class DiscourseTranslator::TranslatorController < ::ApplicationController
before_action :ensure_logged_in

def translate
raise PluginDisabled if !SiteSetting.translator_enabled

if !current_user.staff?
RateLimiter.new(
current_user,
"translate_post",
SiteSetting.max_translations_per_minute,
1.minute,
).performed!
end

params.require(:post_id)
post = Post.find_by(id: params[:post_id])
raise Discourse::InvalidParameters.new(:post_id) if post.blank?
guardian.ensure_can_see!(post)

if !guardian.user_group_allow_translate?
raise Discourse::InvalidAccess.new(
"not_in_group",
SiteSetting.restrict_translation_by_group,
custom_message: "not_in_group.user_not_in_group",
group: current_user.groups.pluck(:id),
)
end

if !guardian.poster_group_allow_translate?(post)
raise Discourse::InvalidAccess.new(
"not_in_group",
SiteSetting.restrict_translation_by_poster_group,
custom_message: "not_in_group.poster_not_in_group",
)
end

begin
title_json = {}
detected_lang, translation =
"DiscourseTranslator::#{SiteSetting.translator}".constantize.translate(post)
if post.is_first_post?
_, title_translation =
"DiscourseTranslator::#{SiteSetting.translator}".constantize.translate(post.topic)
title_json = { title_translation: title_translation }
end
render json: { translation: translation, detected_lang: detected_lang }.merge(title_json),
status: 200
rescue ::DiscourseTranslator::TranslatorError => e
render_json_error e.message, status: 422
end
end
end

Post.register_custom_field_type(::DiscourseTranslator::TRANSLATED_CUSTOM_FIELD, :json)
Topic.register_custom_field_type(::DiscourseTranslator::TRANSLATED_CUSTOM_FIELD, :json)

module ::Jobs
class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff
def execute_onceoff(args)
%w[translator_client_id translator_client_secret].each { |name| DB.exec <<~SQL }
DELETE FROM site_settings WHERE name = '#{name}'
SQL

DB.exec <<~SQL
UPDATE site_settings
SET name = 'translator_azure_subscription_key'
WHERE name = 'azure_subscription_key'
SQL
end
end

class DetectTranslation < ::Jobs::Base
sidekiq_options retry: false

def execute(args)
return if !SiteSetting.translator_enabled

post = Post.find_by(id: args[:post_id])
return unless post

DistributedMutex.synchronize("detect_translation_#{post.id}") do
begin
"DiscourseTranslator::#{SiteSetting.translator}".constantize.detect(post)
if !post.custom_fields_clean?
post.save_custom_fields
post.publish_change_to_clients! :revised
end
rescue ::DiscourseTranslator::ProblemCheckedTranslationError
# The error was handled by ProblemCheck., no need to log errors here
end
end
end
end

class DetectPostsTranslation < ::Jobs::Scheduled
sidekiq_options retry: false
every 5.minutes

BATCH_SIZE = 100
MAX_QUEUE_SIZE = 1000

def execute(args)
return unless SiteSetting.translator_enabled

post_ids = Discourse.redis.spop(DiscourseTranslator::LANG_DETECT_NEEDED, MAX_QUEUE_SIZE)
return if post_ids.blank?

post_ids.each_slice(BATCH_SIZE) { |batch| process_batch(batch) }
end

private

def process_batch(post_ids)
posts = Post.where(id: post_ids).to_a
posts.each do |post|
DistributedMutex.synchronize("detect_translation_#{post.id}") do
begin
translator = "DiscourseTranslator::#{SiteSetting.translator}".constantize
translator.detect(post)
if !post.custom_fields_clean?
post.save_custom_fields
post.publish_change_to_clients!(:revised)
end
rescue ::DiscourseTranslator::ProblemCheckedTranslationError
# problem-checked translation errors gracefully
end
end
end
end
end
end

topic_view_post_custom_fields_allowlister { [::DiscourseTranslator::DETECTED_LANG_CUSTOM_FIELD] }

require_relative "lib/discourse_translator/guardian_extension"
require_relative "lib/discourse_translator/post_extension"
require_relative "lib/discourse_translator/topic_extension"

reloadable_patch do |plugin|
Guardian.prepend(DiscourseTranslator::GuardianExtension)
Post.prepend(DiscourseTranslator::PostExtension)
Expand All @@ -205,8 +56,4 @@ def process_batch(post_ids)
]
end
end

DiscourseTranslator::Engine.routes.draw { post "translate" => "translator#translate" }

Discourse::Application.routes.append { mount ::DiscourseTranslator::Engine, at: "translator" }
end
Loading