translate
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import models, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TranslationDialog(models.AbstractModel):
|
||||
_name = "translation.dialog"
|
||||
_description = "Translation Dialog"
|
||||
|
||||
def _get_api_config(self):
|
||||
"""Get API configuration parameters."""
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('otoolkit_api_key')
|
||||
api_endpoint = self.env['ir.config_parameter'].sudo().get_param('otoolkit.api.endpoint')
|
||||
return api_key, api_endpoint
|
||||
|
||||
def _make_api_request(self, url, headers, payload, timeout=180):
|
||||
"""Make an API request with proper error handling and logging.
|
||||
|
||||
Default timeout is 180 seconds (3 minutes) to allow for OpenAI processing
|
||||
when translating to many languages.
|
||||
"""
|
||||
try:
|
||||
response = requests.request(
|
||||
"POST",
|
||||
url,
|
||||
headers=headers,
|
||||
data=payload,
|
||||
timeout=timeout
|
||||
)
|
||||
return response, None
|
||||
except requests.exceptions.Timeout:
|
||||
_logger.warning("OToolKit API request timed out: %s", url)
|
||||
return None, _("The translation service is taking too long to respond. Please try again.")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
_logger.error("OToolKit API connection error: %s - %s", url, str(e))
|
||||
return None, _("Unable to connect to the translation service. Please check your internet connection.")
|
||||
except requests.exceptions.RequestException as e:
|
||||
_logger.error("OToolKit API request failed: %s - %s", url, str(e))
|
||||
return None, _("An error occurred while contacting the translation service: %s") % str(e)
|
||||
|
||||
def _handle_api_response(self, response):
|
||||
"""Handle API response and return appropriate result."""
|
||||
if response.status_code == 200:
|
||||
return [True, response.json(), ""]
|
||||
|
||||
if response.status_code == 401:
|
||||
_logger.warning("OToolKit API authentication failed (401)")
|
||||
return [False, _("Your API key is invalid. This may be due to an input error, deletion or deactivation of the key. Please check your Otoolkit settings."), "settings"]
|
||||
|
||||
if response.status_code == 400:
|
||||
try:
|
||||
error = response.json()
|
||||
error_type = error.get("type", "unknown")
|
||||
if error_type == "empty_text":
|
||||
return [False, _("The default language text cannot be empty."), ""]
|
||||
if error_type == "insufficient_funds":
|
||||
return [False, _("You do not have enough credits to translate. Please add credits to your balance to use this function."), "credits"]
|
||||
if error_type == "invalid_object":
|
||||
return [False, _("No valid text fields found to translate."), ""]
|
||||
# Log unhandled 400 error types
|
||||
_logger.warning("OToolKit API returned unhandled 400 error: %s", error)
|
||||
return [False, _("Translation error: %s") % error.get("error", "Unknown error"), ""]
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
_logger.error("Failed to parse OToolKit API 400 response: %s", str(e))
|
||||
return [False, _("The translation service returned an invalid response."), ""]
|
||||
|
||||
if response.status_code == 429:
|
||||
_logger.warning("OToolKit API rate limited (429)")
|
||||
return [False, _("Too many translation requests. Please wait a moment and try again."), ""]
|
||||
|
||||
if response.status_code >= 500:
|
||||
_logger.error("OToolKit API server error (%d): %s", response.status_code, response.text[:500])
|
||||
return [False, _("The translation service is temporarily unavailable (Error %d). Please try again later.") % response.status_code, ""]
|
||||
|
||||
# Log any other unexpected status codes
|
||||
_logger.error("OToolKit API unexpected status code (%d): %s", response.status_code, response.text[:500])
|
||||
return [False, _("Unexpected response from translation service (Error %d). Please try again.") % response.status_code, ""]
|
||||
|
||||
def _get_effective_value(self, term, updated_terms):
|
||||
"""Get the effective value of a term, considering user edits in the dialog.
|
||||
|
||||
If the user edited the term (present in updated_terms), always use that
|
||||
value — even if empty (the user explicitly cleared it).
|
||||
"""
|
||||
str_id = str(term["id"])
|
||||
if str_id in updated_terms:
|
||||
return (updated_terms[str_id] or "").strip()
|
||||
return (term.get("value") or "").strip()
|
||||
|
||||
def _detect_source_and_filter(self, terms, updated_terms, source_lang_hint=None, target_lang_hint=None):
|
||||
"""Detect the best source language and filter terms to only translate empty ones.
|
||||
|
||||
Returns (base_language, filtered_terms) or (None, []) if nothing to translate.
|
||||
|
||||
When the caller knows the source/target (single-term flow), it must pass
|
||||
the hints — relying on auto-detection over `filled_langs` is unsafe
|
||||
because `next(iter(set))` is non-deterministic and may pick the user's
|
||||
target language as the source.
|
||||
|
||||
Auto-detection priority (used only when hints are absent):
|
||||
1. User's language (if it has content)
|
||||
2. en_US (Odoo's typical base language)
|
||||
3. Any language other than the target hint with content
|
||||
4. First language with content (last resort)
|
||||
|
||||
Terms whose value is identical to the source language's value are treated
|
||||
as untranslated copies (Odoo duplicates the source value to all languages
|
||||
by default) and will be sent for translation.
|
||||
|
||||
When all values are empty but source text exists (e.g. HTML fields where
|
||||
translations haven't been set yet), the source text is used as the base
|
||||
content for translation.
|
||||
"""
|
||||
user_lang = self.env.user.lang
|
||||
|
||||
# Build a map of lang -> effective value
|
||||
lang_values = {}
|
||||
for term in terms:
|
||||
lang_values[term["lang"]] = self._get_effective_value(term, updated_terms)
|
||||
|
||||
# First pass: identify which languages have any content
|
||||
filled_langs = {lang for lang, val in lang_values.items() if val}
|
||||
source_from_source_field = False
|
||||
|
||||
if not filled_langs:
|
||||
# All values are empty — fall back to source text if available
|
||||
# (e.g. HTML fields where translations haven't been set yet)
|
||||
for term in terms:
|
||||
source = (term.get("source") or "").strip()
|
||||
if source:
|
||||
lang_values[term["lang"]] = source
|
||||
filled_langs = {lang for lang, val in lang_values.items() if val}
|
||||
if not filled_langs:
|
||||
return None, []
|
||||
source_from_source_field = True
|
||||
|
||||
# Determine source language. Explicit hints win — the JS knows which
|
||||
# row the user clicked, so trust it over heuristic guessing.
|
||||
if source_lang_hint and source_lang_hint in filled_langs:
|
||||
source_lang = source_lang_hint
|
||||
elif user_lang in filled_langs and user_lang != target_lang_hint:
|
||||
source_lang = user_lang
|
||||
elif "en_US" in filled_langs and "en_US" != target_lang_hint:
|
||||
source_lang = "en_US"
|
||||
else:
|
||||
# Prefer any filled lang that is NOT the target — never pick the
|
||||
# user's target as source.
|
||||
non_target = [l for l in filled_langs if l != target_lang_hint]
|
||||
source_lang = non_target[0] if non_target else next(iter(filled_langs))
|
||||
|
||||
source_value = lang_values[source_lang]
|
||||
|
||||
# Second pass: languages that are empty OR identical to source are
|
||||
# considered untranslated (Odoo copies the source value by default)
|
||||
empty_langs = set()
|
||||
for lang, val in lang_values.items():
|
||||
if lang == source_lang:
|
||||
continue
|
||||
if not val or val == source_value:
|
||||
empty_langs.add(lang)
|
||||
|
||||
# If a target hint was provided, restrict the translation to that
|
||||
# language only — a single-term click should never translate every
|
||||
# untranslated peer in the dialog as a side effect.
|
||||
if target_lang_hint:
|
||||
empty_langs = {target_lang_hint} if target_lang_hint != source_lang else set()
|
||||
|
||||
if not empty_langs:
|
||||
return None, []
|
||||
|
||||
keep_langs = {source_lang} | empty_langs
|
||||
filtered_terms = [t for t in terms if t["lang"] in keep_langs]
|
||||
|
||||
if source_from_source_field:
|
||||
# Populate the base language terms' value with source text so the
|
||||
# API receives the actual content to translate from
|
||||
filtered_terms = [
|
||||
{**t, "value": (t.get("source") or "")} if t["lang"] == source_lang else t
|
||||
for t in filtered_terms
|
||||
]
|
||||
|
||||
return source_lang, filtered_terms
|
||||
|
||||
def otoolkit_api_translation(self, terms, updated_terms, source_lang_hint=None, target_lang_hint=None):
|
||||
api_key, api_endpoint = self._get_api_config()
|
||||
|
||||
if not api_key:
|
||||
return [False, _("Your API key is invalid. This may be due to an input error, deletion or deactivation of the key. Please check your Otoolkit settings."), "settings"]
|
||||
|
||||
if not api_endpoint:
|
||||
_logger.error("OToolKit API endpoint not configured")
|
||||
return [False, _("The translation service is not configured. Please contact your administrator."), ""]
|
||||
|
||||
base_language, filtered_terms = self._detect_source_and_filter(
|
||||
terms, updated_terms,
|
||||
source_lang_hint=source_lang_hint,
|
||||
target_lang_hint=target_lang_hint,
|
||||
)
|
||||
if not base_language:
|
||||
return [True, [], ""]
|
||||
|
||||
payload = json.dumps({
|
||||
"terms": filtered_terms,
|
||||
"updated_terms": updated_terms,
|
||||
"base_language": base_language,
|
||||
"odoo_user_id": self.env.user.id
|
||||
})
|
||||
headers = {
|
||||
'Odoo-Api-Key': api_key,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
url = f"{api_endpoint}/api/auto-translate-fields/translate/"
|
||||
|
||||
response, error_message = self._make_api_request(url, headers, payload)
|
||||
|
||||
if error_message:
|
||||
return [False, error_message, ""]
|
||||
|
||||
return self._handle_api_response(response)
|
||||
|
||||
@api.model
|
||||
def translate_text(self, terms, updated_terms, source_lang_hint=None, target_lang_hint=None):
|
||||
return self.otoolkit_api_translation(
|
||||
terms, updated_terms,
|
||||
source_lang_hint=source_lang_hint,
|
||||
target_lang_hint=target_lang_hint,
|
||||
)
|
||||
Reference in New Issue
Block a user