Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ GEOCODER_UNITS=km # Units for geocoder results (e.g., km or m
# DECIDIM_AI_BASIC_AUTH="<USER>:<PASSWORD>" Required for the AI Request Handler
# DECIDIM_AI_REPORTING_USER_EMAIL="<EMAIL>"
# DECIDIM_AI_SECRET="<SECRET_KEY>" # Not required for the AI Request Handler

# Machine translations
DEEPL_AUTH_KEY=<DEEPL_AUTH_KEY>
DECIDIM_MACHINE_TRANSLATIONS_ENABLED=true # Default: false
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ gem "decidim-initiatives", github: "decidim/decidim", tag: DECIDIM_TAG
gem "decidim-templates", github: "decidim/decidim", tag: DECIDIM_TAG

gem "bootsnap", "~> 1.4", require: false
gem "deepl-rb", "~> 3.2"
gem "puma", ">= 6.3.1"

gem "activerecord-postgis-adapter", "~> 8.0", ">= 8.0.3"
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,7 @@ GEM
declarative-builder (0.2.0)
trailblazer-option (~> 0.1.0)
declarative-option (0.1.0)
deepl-rb (3.7.0)
deface (1.9.0)
actionview (>= 5.2)
nokogiri (>= 1.6)
Expand Down Expand Up @@ -1160,6 +1161,7 @@ DEPENDENCIES
decidim-survey_multiple_answers!
decidim-templates!
decidim-term_customizer!
deepl-rb (~> 3.2)
deface
dotenv-rails (~> 2.7)
faker (~> 3.2)
Expand Down
40 changes: 40 additions & 0 deletions app/services/deepl_translator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "deepl"

class DeeplTranslator
attr_reader :resource, :field_name, :text, :target_locale, :source_locale

def initialize(resource, field_name, text, target_locale, source_locale)
@resource = resource
@field_name = field_name
@text = text
@target_locale = target_locale
@source_locale = source_locale
end

def translate
Rails.logger.info("[DeeplTranslator] CALLED #{source_locale} -> #{target_locale}")
Rails.logger.info("[DeeplTranslator] TEXT=#{text}")

return if text.blank?

api = DeepL::API.new(DeepL.configuration)
translation = DeepL::Requests::Translate.new(api, text, source_locale.to_s, target_locale.to_s).request

return nil if translation.nil? || translation.text.blank?

Decidim::MachineTranslationSaveJob.perform_later(
resource,
field_name,
target_locale,
translation.text
)
rescue DeepL::Exceptions::QuotaExceeded => e
Rails.logger.error("[DeeplTranslator] Quota exceeded: #{e.message}")
nil
rescue StandardError => e
Rails.logger.error("[DeeplTranslator] #{e.class} - #{e.message}")
raise
end
end
2 changes: 2 additions & 0 deletions config/i18n-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,5 @@ ignore_unused:
- decidim.searches.filters.jump_to
- decidim.components.proposals.settings.global.enable_iframe
- decidim.components.proposals.settings.global.iframe_url
- activemodel.attributes.organization.enable_machine_translations
- activemodel.attributes.organization.enable_machine_translations
4 changes: 2 additions & 2 deletions config/initializers/decidim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@
# for more information about how it works and how to set it up.
#
# Enable machine translations
config.enable_machine_translations = false
config.enable_machine_translations = ENV["DECIDIM_MACHINE_TRANSLATIONS_ENABLED"].present?
#
# If you want to enable machine translation you can create your own service
# to interact with third party service to translate the user content.
Expand All @@ -360,7 +360,7 @@
# end
# end
#
config.machine_translation_service = "Decidim::Dev::DummyTranslator"
config.machine_translation_service = "DeeplTranslator"

# Defines the social networking services used for social sharing
config.social_share_services = Rails.application.secrets.decidim[:social_share_services]
Expand Down
2 changes: 2 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ en:
attributes:
assembly:
copy_landing_page_blocks: Copy landing page blocks
organization:
enable_machine_translations: Enable machine translations
osp_authorization_handler:
document_number: Unique number
participatory_process:
Expand Down
2 changes: 2 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ fr:
document_number: Numéro unique
postal_code: Code postal
scope_id: Secteur
organization:
enable_machine_translations: Activer la traduction automatique
osp_authorization_handler:
document_number: Numéro unique
participatory_process:
Expand Down
1 change: 1 addition & 0 deletions db/seeds.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# frozen_string_literal: true

Decidim.enable_machine_translations = false
Decidim.seed!
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ services:
sidekiq:
image: decidim-app:3.4.0
command: [ "bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml" ]
env_file:
- .env
environment:
- DATABASE_HOST=database
- DATABASE_USERNAME=postgres
Expand Down
144 changes: 144 additions & 0 deletions spec/services/deepl_translator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# frozen_string_literal: true

require "spec_helper"
require "deepl"

module Decidim
describe DeeplTranslator do
let(:title) { { en: "New Title" } }
let(:process) { build(:participatory_process, title:) }
let(:target_locale) { "fr" }
let(:source_locale) { "en" }
let(:translation) { double("translation", text: "Nouveau Titre") }
let(:translate_request) { instance_double(DeepL::Requests::Translate, request: translation) }
let(:api) { instance_double(DeepL::API) }

before do
allow(Decidim).to receive(:machine_translation_service_klass).and_return(DeeplTranslator)
allow(DeepL::API).to receive(:new).and_return(api)
allow(DeepL::Requests::Translate).to receive(:new).and_return(translate_request)
end

describe "When fields job is executed" do
before { clear_enqueued_jobs }

it "calls DeeplTranslator to create machine translations" do
expect(DeeplTranslator).to receive(:new).with(
process,
"title",
process["title"][source_locale],
target_locale,
source_locale
).and_call_original

process.save

MachineTranslationFieldsJob.perform_now(
process,
"title",
process["title"][source_locale],
target_locale,
source_locale
)
end
end

describe "#translate" do
subject { DeeplTranslator.new(process, "title", text, target_locale, source_locale).translate }

let(:text) { title[source_locale.to_sym] }

context "when translation is nil" do
let(:translate_request) { instance_double(DeepL::Requests::Translate, request: nil) }

it "does not enqueue a job" do
expect(Decidim::MachineTranslationSaveJob).not_to receive(:perform_later)
expect(subject).to be_nil
end
end

context "when text is empty" do
let(:text) { "" }

it "does not enqueue a job" do
expect(Decidim::MachineTranslationSaveJob).not_to receive(:perform_later)
expect(subject).to be_nil
end
end

context "when DeepL raises a non-quota error" do
before do
allow(DeepL::Requests::Translate).to receive(:new).and_raise(StandardError, "API failure")
end

it "logs the error and re-raises" do
expect(Rails.logger).to receive(:error).with(/\[DeeplTranslator\] StandardError - API failure/)
expect { subject }.to raise_error(StandardError, "API failure")
end
end

context "when DeepL raises QuotaExceeded" do
before do
allow(DeepL::Requests::Translate).to receive(:new).and_raise(DeepL::Exceptions::QuotaExceeded.new("Quota exceeded"))
end

it "logs the error and returns nil" do
expect(Rails.logger).to receive(:error).with(/\[DeeplTranslator\] Quota exceeded/)
expect(subject).to be_nil
end
end

context "when translation succeeds" do
it "enqueues MachineTranslationSaveJob with correct arguments" do
expect(Decidim::MachineTranslationSaveJob).to receive(:perform_later).with(
process,
"title",
target_locale,
"Nouveau Titre"
)
subject
end
end

context "when text is blank but not empty" do
let(:text) { " " }

it "does not enqueue a job" do
expect(Decidim::MachineTranslationSaveJob).not_to receive(:perform_later)
expect(subject).to be_nil
end
end

context "when translation text is blank" do
let(:translate_request) { instance_double(DeepL::Requests::Translate, request: double("translation", text: "")) }

it "does not enqueue a job" do
expect(Decidim::MachineTranslationSaveJob).not_to receive(:perform_later)
expect(subject).to be_nil
end
end

context "when DeepL raises EOFError" do
before do
allow(DeepL::Requests::Translate).to receive(:new).and_raise(EOFError, "end of file reached")
end

it "logs the error and re-raises" do
expect(Rails.logger).to receive(:error).with(/\[DeeplTranslator\] EOFError - end of file reached/)
expect { subject }.to raise_error(EOFError)
end
end

context "when DeepL raises IOError" do
before do
allow(DeepL::Requests::Translate).to receive(:new).and_raise(IOError, "stream closed in another thread")
end

it "logs the error and re-raises" do
expect(Rails.logger).to receive(:error).with(/\[DeeplTranslator\] IOError - stream closed in another thread/)
expect { subject }.to raise_error(IOError)
end
end
end
end
end
Loading
Loading