Class: DeepLClient
- Inherits:
-
Object
- Object
- DeepLClient
- Defined in:
- app/services/deep_l_client.rb
Overview
Thin HTTP client for DeepL's document-translation API
(https://developers.deepl.com/docs/api-reference/document). DeepL is the sole
PDF-translation provider; it may reflow pages and substitute fonts, so every
result is staged for human review before it enters the publication library.
Auth: DeepL-Auth-Key from credentials (Heatwave::Configuration :deepl,
same key config/initializers/deepl.rb uses). Canadian French (FR-CA) is a
supported document target (generally available since 2026-07-17).
Billing note: every successfully downloaded PDF bills a 50,000-character
MINIMUM — surfaced in meta[:billed_characters] so the exact billable amount
stays auditable.
Defined Under Namespace
Classes: Error, NotConfigured, Result, Timeout, UnsupportedLanguage
Constant Summary collapse
- DOCUMENT_TARGETS =
Product locales exposed by the CRM and Sunny PDF flows, mapped to DeepL's
document target codes. Keep BCP 47 casing on our side (fr-CA) and the
uppercase codes DeepL expects on the API side (FR-CA). DeepL's live
/v3/languages?resource=translate_documentresponse omitted FR-CA on
2026-08-12, but the release notes declare it GA and an actual
/v2/documentrequest with this account succeeded; do not use that listing
as a runtime gate until DeepL fixes the inconsistency. { 'de' => 'DE', 'es' => 'ES', 'fr' => 'FR', 'fr-CA' => 'FR-CA', 'it' => 'IT', 'nl' => 'NL', 'pl' => 'PL', 'sv' => 'SV' }.freeze
- BASE_URL =
DeepL API Pro endpoint.
'https://api.deepl.com'- FREE_BASE_URL =
DeepL Free keys (ending ':fx') are rejected by the Pro endpoint — they
only work against api-free.deepl.com. 'https://api-free.deepl.com'- TRANSLATE_TIMEOUT =
Whole-task deadline in seconds. DeepL is fast for small docs; 20-page
manuals took under a minute in testing. 600- POLL_INTERVAL =
Status poll interval in seconds.
3
Class Method Summary collapse
-
.api_key ⇒ String?
The configured DeepL auth key.
-
.base_url ⇒ String
API base URL for the configured key — Free keys (ending ':fx') must hit api-free.deepl.com or every call 403s.
-
.canonical_document_locale(language) ⇒ String
Return the application's canonical BCP 47 locale for a document target.
-
.configured? ⇒ Boolean
Whether a DeepL auth key is configured.
-
.normalize_document_target_language(language) ⇒ String
Normalize and validate a target for the document endpoint.
-
.translate(path, lang_out:, lang_in: nil, filename: File.basename(path.to_s), resume_document_id: nil, resume_document_key: nil) {|document_id, document_key| ... } ⇒ Result
Translate a local PDF.
Class Method Details
.api_key ⇒ String?
Returns the configured DeepL auth key.
69 70 71 |
# File 'app/services/deep_l_client.rb', line 69 def api_key Heatwave::Configuration.fetch(:deepl).presence end |
.base_url ⇒ String
Returns API base URL for the configured key — Free keys
(ending ':fx') must hit api-free.deepl.com or every call 403s.
80 81 82 |
# File 'app/services/deep_l_client.rb', line 80 def base_url api_key.to_s.end_with?(':fx') ? FREE_BASE_URL : BASE_URL end |
.canonical_document_locale(language) ⇒ String
Return the application's canonical BCP 47 locale for a document target.
104 105 106 107 108 109 |
# File 'app/services/deep_l_client.rb', line 104 def canonical_document_locale(language) locale = document_target_pair(language)&.first return locale if locale normalize_document_target_language(language) # raises the detailed error end |
.configured? ⇒ Boolean
Returns whether a DeepL auth key is configured.
74 75 76 |
# File 'app/services/deep_l_client.rb', line 74 def configured? api_key.present? end |
.normalize_document_target_language(language) ⇒ String
Normalize and validate a target for the document endpoint.
90 91 92 93 94 95 96 97 |
# File 'app/services/deep_l_client.rb', line 90 def normalize_document_target_language(language) target = document_target_pair(language)&.last return target if target raise UnsupportedLanguage, "DeepL document translation does not support #{language.inspect} here " \ "(supported: #{DOCUMENT_TARGETS.keys.join(', ')})" end |
.translate(path, lang_out:, lang_in: nil, filename: File.basename(path.to_s), resume_document_id: nil, resume_document_key: nil) {|document_id, document_key| ... } ⇒ Result
Translate a local PDF. Layout is NOT page-faithful (DeepL reflows and
may change page counts) — the PDF-studio review shows the result.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'app/services/deep_l_client.rb', line 132 def translate(path, lang_out:, lang_in: nil, filename: File.basename(path.to_s), resume_document_id: nil, resume_document_key: nil) raise NotConfigured, 'DeepL auth key is not configured (credentials :deepl)' unless configured? lang_out = normalize_document_target_language(lang_out) document_id, document_key = if resume_document_id.present? && resume_document_key.present? [resume_document_id, resume_document_key] else submit(path, filename:, lang_in:, lang_out:) end yield(document_id, document_key) if block_given? billed = await(document_id, document_key) bytes = download(document_id, document_key) Result.new(bytes:, meta: { engine: 'deepl', document_id:, billed_characters: billed, lang_in:, lang_out:, filename:, warnings: [] }) rescue HTTP::ConnectTimeoutError => e # Connection never established — transient, worth a bounded retry. Must # precede HTTP::TimeoutError (ConnectTimeoutError < TimeoutError). raise Error, "deepl connection failed: #{e.}" rescue HTTP::TimeoutError => e raise Error, "deepl request timed out: #{e.}" rescue HTTP::Error => e raise Error, "deepl request failed: #{e.}" rescue JSON::ParserError raise Error, 'deepl returned invalid JSON' end |